백준이 사라졌다?
"알고리즘 문제는 백준이죠"가 깨진 날
공지를 처음 봤을 때의 감정은 놀람보다 어색함이었습니다. "백준이 사라진다고?" 알고리즘 공부를 해본 사람이라면 한 번쯤은 거쳐가는 플랫폼, 10년 넘게 그 자리에 있던 서비스가 사라진다는 건 머리로는 가능하다고 알고 있어도 체감으로는 멀리 있는 얘기였죠.
AlgoSu는 백준(BOJ) 기반으로 설계된 서비스였습니다. 문제 메타데이터, 난이도 배지, 제출 파이프라인, GitHub 동기화, AI 피드백 — 흐름 곳곳에 BOJ가 스며 있었습니다. 공지를 읽고 난 뒤, 저는 코드가 아니라 제가 무의식적으로 품고 있던 가정 하나를 마주해야 했습니다.
의존은 알고 있었습니다
오해하지 마세요. AlgoSu가 BOJ에 의존한다는 건 몰랐던 게 아닙니다. 프로젝트 초기부터 알고 있었기 때문에 여러 안전장치를 설계에 넣어뒀습니다.
sourcePlatform이라는 컬럼을 문제 테이블에 일찍 추가해둔 것- 문제 메타데이터 API를 외부 모듈로 분리해 Gateway 경계에 둔 것
- 난이도 enum과 UI 토큰을 플랫폼과 독립적으로 설계한 것
추상화는 이미 있었습니다. 언젠가 다른 플랫폼이 추가될 수도 있다는 생각을 그때도 하긴 했으니까요.
그런데도 마음 한구석에는 "설마 백준이?"라는 전제가 남아 있었습니다. 다른 플랫폼이 추가될 가능성은 고려했지만, BOJ가 사라질 가능성은 고려하지 않았던 겁니다. 추가는 능동적 확장, 사라짐은 생존의 문제인데, 저는 전자만 설계하고 후자는 미뤄놨던 거죠.
영원한 건 없다
백준 서비스 종료 공지가 깨뜨린 건 코드가 아니라 그 무의식이었습니다.
- "BOJ가 사라질 리 없다"가 아니라 "BOJ도 사라질 수 있다"
- "외부 API는 항상 호출 가능하다"가 아니라 "외부 API는 언제든 끊길 수 있다"
- "이 플랫폼만큼은 예외다"가 아니라 "예외는 없다"
이게 이번 전환에서 진짜 배운 한 줄이었습니다. 기술적으로 새로 알게 된 건 많지 않았어요. 오히려 이미 해둔 설계 결정들이 옳았다는 확인에 가까웠죠. 하지만 그 설계를 하게 만든 태도 — "영원한 건 없다는 전제로 설계한다" — 는 이번에야 비로소 체화됐습니다.
이식의 3 스프린트
공지를 보자마자 단일 스프린트에 모든 전환을 쑤셔 넣으려 했습니다. 데이터 + 백엔드 + 프런트 + 제출 + 문서까지. 플랜 초안을 보다가 스스로 멈췄습니다. 회귀 리스크가 너무 컸거든요. 그래서 3-스프린트 로드맵으로 재설계했습니다.
- Sprint 95 — 백엔드 인프라. 프로그래머스 문제 373건을 사전 큐레이션 JSON으로 번들링하고, Gateway에 BOJ와 대칭되는
/api/external/programmers/*엔드포인트를 추가했습니다. 사용자 가시 변화 0, BOJ 회귀 0을 원칙으로 인프라만 선행 구축. - Sprint 96 — 프런트 UX.
AddProblemModal플랫폼 토글,useProgrammersSearch훅, 기본값을 프로그래머스로 승격. 사용자가 실제로 볼 수 있는 변화가 이때 들어왔습니다. - Sprint 97 — 파이프라인 마감. GitHub Worker
formatPlatform()확장(prg_접두어), AI 피드백 프롬프트에sourcePlatform주입, tags 보강, WCAG AA 검증, E2E.
이 과정에서 내린 결정 중 지금 돌이켜봐도 가장 다행이었던 건 실시간 파싱 대신 사전 큐레이션 JSON 번들링을 선택한 것입니다. 프로그래머스는 공식 API가 없고 Cloudflare JA3 차단이 걸려 있었죠. 실시간으로 긁어오는 아키텍처를 짜는 건 가능했지만, 그 순간 새로운 외부 의존을 또 심는 셈이었습니다. 데이터를 우리 리포지토리 안으로 가져와 소유하는 선택은 운영 안정성을 위한 것이기도 했지만, "외부 API는 언제든 끊길 수 있다"는 이번 교훈을 그대로 적용한 결과이기도 했습니다.
그리고 기존 BOJ 데이터는 지웠을까요? 아니요, 그대로 뒀습니다. 마이그레이션 0, 공존 전략. 사용자가 BOJ로 풀었던 기존 문제 히스토리는 서비스 종료 공지 이후에도 계속 기록으로 남았습니다. 플랫폼은 사라져도 그 위에서 쌓은 것들은 우리 서비스 안에서 살아 있어야 하니까요.
성과
- 프로그래머스 문제 373건 이식 (Lv.1
5 → BRONZEDIAMOND 1:1 매핑으로 디자인 토큰 0줄 수정) - 최종 검증 2,445 tests ALL PASS, WCAG AA 6/6 PASS
- 기존 BOJ 사용자 경험 0 손상, DB 마이그레이션 0건
- Gateway 엔드포인트는 BOJ(Solved.ac)와 대칭 구조로 추상 레이어 유지
숫자 자체보다 의미 있었던 건 "기존을 부수지 않고 새 플랫폼을 얹었다"는 것입니다. 외부 충격을 내부 구조 변경으로 흡수한 형태죠.
외부 의존 서비스가 잊지 말아야 할 것
여기까지가 기술적인 이야기라면, 이 뒤가 이번 스프린트의 진짜 결론입니다.
"설마"를 설계에서 지워야 한다
의존을 안다는 것과 의존이 끊길 수 있다는 전제로 설계하는 것은 다릅니다. 저는 앞만 하고 뒤는 미뤄뒀습니다. sourcePlatform 컬럼을 둔 건 "다른 플랫폼이 추가될 수 있다"는 능동적 확장성을 생각한 것이지, "BOJ가 사라질 수 있다"는 생존 시나리오를 생각한 게 아니었거든요.
두 가정은 비슷해 보이지만 설계의 무게가 다릅니다. 확장성은 "있으면 좋다"지만 생존은 "없으면 안 된다"입니다. "설마 이 플랫폼이?"라는 전제가 남아 있으면, 막상 그 일이 벌어졌을 때 대응이 한 박자 늦어집니다. 다행히 저는 기본 추상화는 해뒀지만, 그 추상화를 더 깊게 뽑지 않은 부분들이 Sprint 99 PM QA 5라운드에서 드러났습니다. 난이도 배지가 플랫폼을 인지하지 못했고, 주차 계산 로직이 BOJ 전용 가정으로 짜여 있었고, 스터디룸 4개 호출처에 sourcePlatform prop이 전파되지 않았죠. 전부 "설마 여기까지는"이 남긴 자국이었습니다.
외부 API는 언제든 끊길 수 있다
실시간 파싱과 사전 번들링 중에서 후자를 고른 건 이번엔 운이 좋았습니다. 사실 프로그래머스 데이터를 번들링한 이유는 처음엔 Cloudflare 우회가 귀찮아서였어요. 그런데 결과적으로 그게 외부 의존을 한 겹 더 줄이는 결정이 됐습니다.
이번 전환 이후로는 외부 API를 붙일 때 질문 순서가 바뀌었습니다. 전에는 "이 API가 안정적인가?"를 먼저 물었다면, 이제는 "이 API가 없어져도 서비스가 돌아가는가?" 를 먼저 묻습니다. 답이 "아니오"라면, 그 API에 의존하는 로직을 더 좁은 경계 안에 가두거나, 데이터 소유권을 우리 쪽으로 가져오는 방법을 고민합니다.
추상화는 "있으면 좋은 것"이 아니라 "생존 조건"이다
sourcePlatform 컬럼 하나가 이번에 얼마나 큰일을 했는지, 이번 스프린트를 지나고서야 실감했습니다. 이 컬럼이 없었다면 DB 마이그레이션, 기존 레코드 재분류, 사용자 히스토리 보존을 전부 한꺼번에 해야 했을 겁니다. 그 컬럼을 추가한 초기의 저는 "혹시 몰라서" 정도의 느낌이었을 거예요. 그런데 그 "혹시"가 실제로 왔습니다.
많은 추상화 결정이 이런 식입니다. 지금 당장은 과해 보이고, 한 겹 더 두르는 건 품이 들죠. 그런데 외부 의존을 다루는 시스템에서 그 한 겹은 종종 서비스의 생명줄이 됩니다. "과하다 싶은 경계"가 나중의 저를 구해주는 경험은 이번이 처음도 아닙니다. 그리고 아마 마지막도 아닐 겁니다.
마무리
이번 스프린트에서 새로운 기술을 배운 건 많지 않습니다. Playwright로 크롤러를 짰고, JSON 번들링 구조를 설계했고, DTO @IsIn으로 입력 경계를 좁혔죠. 다 이미 알던 것들의 조합이었습니다.
배운 건 태도였습니다. 영원한 플랫폼은 없다는 것. 백준만큼 오래된 서비스도 사라지는 판에, 우리가 지금 의존하고 있는 어떤 외부도 언젠가는 끊길 수 있습니다. 우리가 할 수 있는 건 그 가능성을 설계에 녹여 두는 것뿐입니다. "이 플랫폼만큼은 예외"라는 생각을 지우고, 의존의 깊이를 설계할 때부터 "끊겼을 때 어떻게 살아남을까"를 함께 그려두는 것. 그게 외부에 기대어 만든 서비스가 오래 가는 방법이었습니다.
백준은 사라집니다. AlgoSu는 남습니다. 다음에 또 어떤 플랫폼이 사라질지는 모르지만, 그때의 저는 조금 덜 놀랄 준비가 되어 있기를 바랍니다.