회고백준 서비스 종료에 따른 프로그래머스 이전과 외부 의존성 정리
· 알고수
- #external-api
- #platform-migration
"알고리즘은 백준"이 깨진 날
공지를 처음 봤을 때 든 감정은 놀람보다 어색함이었습니다. "백준이 사라진다고?" 백준(BOJ)은 한국에서 알고리즘 공부를 해본 사람이라면 한 번쯤 거쳐 가는 문제 풀이 사이트입니다. 10년 넘게 그 자리에 있던 서비스였죠. 어떤 서비스든 끝날 수 있다는 건 머리로는 알았지만, 체감으로는 먼 얘기였습니다.
알고수는 백준을 기반으로 설계한 서비스였습니다. 문제 정보, 난이도 배지, 제출 흐름, GitHub 동기화, AI 피드백까지 곳곳에 백준이 스며 있었습니다. 공지를 읽고 나서 제가 마주한 건 코드가 아니었습니다. 저도 모르게 품고 있던 가정 하나였습니다.
문제
외부 플랫폼(백준)에 문제 제공을 의존하던 알고수에 서비스 종료 공지가 왔다. 의존은 알았지만 "설마 백준이?"라는 전제가 남아 있었다.
결정
문제 제공처를 프로그래머스로 3 스프린트(짧은 작업 주기)에 걸쳐 옮겼다. 실시간으로 긁어오는 대신 미리 골라 둔 문제를 JSON 파일로 담았고, 기존 백준 데이터는 지우지 않고 함께 두었다.
결과
문제 373건을 DB 마이그레이션 0건, 기존 사용자 경험 손상 0으로 얹었다. 미리 둔 sourcePlatform(출처 플랫폼) 컬럼이 생존 조건이었다.
알고 있던 의존
오해하지 마세요. 알고수가 백준에 의존한다는 건 알고 있었습니다. 프로젝트 초기부터 알았기 때문에 설계에 안전장치를 몇 가지 넣어 두었습니다. (백준 문제 정보는 백준 문제에 난이도·태그를 붙여 주는 커뮤니티 서비스 Solved.ac를 통해 가져오고 있었습니다.)
- 문제 테이블에 문제의 출처 사이트를 적는
sourcePlatform컬럼을 일찍 추가해 둔 것 - 외부 문제 정보를 가져오는 API를 별도 모듈로 떼어, 모든 요청을 받는 관문 서비스(Gateway)의 경계에 둔 것
- 난이도 값(enum)과 화면 디자인 토큰을 특정 플랫폼과 무관하게 설계한 것
추상화는 이미 있었습니다. 언젠가 다른 플랫폼이 추가될 수 있다는 생각은 그때도 했으니까요.
그런데도 마음 한구석에는 "설마 백준이?"라는 전제가 남아 있었습니다. 다른 플랫폼이 추가될 가능성은 생각했지만, 백준이 사라질 가능성은 생각하지 않았던 겁니다. 추가는 확장의 문제이고, 사라짐은 생존의 문제입니다. 저는 앞의 것만 설계하고 뒤의 것은 미뤄 두었습니다.
영원하지 않은 외부 서비스
백준 서비스 종료 공지가 깨뜨린 건 코드가 아니라 그 무의식이었습니다.
이전의 전제
- 백준이 사라질 리 없다
- 외부 API는 항상 호출 가능하다
- 이 플랫폼만큼은 예외다
이제의 전제
- 백준도 사라질 수 있다
- 외부 API는 언제든 끊길 수 있다
- 예외는 없다
이번 전환에서 진짜 배운 건 이 한 줄이었습니다. 기술적으로 새로 알게 된 건 많지 않았습니다. 오히려 이미 해 둔 설계가 옳았다는 확인에 가까웠죠. 하지만 그 설계를 하게 만든 태도, 즉 "영원한 건 없다는 전제로 설계한다"는 것은 이번에야 몸에 배었습니다.
3 스프린트의 이식
옮겨 갈 곳은 프로그래머스였습니다. 한국의 코딩 테스트·문제 풀이 플랫폼입니다.
공지를 보자마자 모든 전환을 한 스프린트에 몰아넣으려 했습니다. 데이터, 백엔드, 프런트, 제출, 문서까지 한꺼번에요. 계획 초안을 보다가 스스로 멈췄습니다. 기존 기능이 깨질 위험이 너무 컸거든요. 그래서 3 스프린트 로드맵으로 다시 짰습니다.
Sprint 95
백엔드 인프라
프로그래머스 문제 373건을 미리 골라 JSON 파일로 저장소에 담고, Gateway에 백준용과 같은 모양의
/api/external/programmers/*엔드포인트를 추가했습니다. 사용자에게 보이는 변화 0, 백준 기능 회귀 0을 원칙으로 인프라만 먼저 깔았습니다.Sprint 96
프런트 UX
문제 추가 창(
AddProblemModal)에 플랫폼 선택 토글을 넣고, 프로그래머스 문제 검색(useProgrammersSearch훅)을 붙이고, 기본값을 프로그래머스로 바꿨습니다. 사용자가 실제로 볼 수 있는 변화는 이때 들어왔습니다.Sprint 97
파이프라인 마감
풀이를 사용자의 GitHub에 올리는 서비스(GitHub Worker)가 파일명에
prg_접두어를 붙이도록formatPlatform()을 확장했습니다. AI 피드백 프롬프트에 출처 플랫폼(sourcePlatform)을 넣고, 태그를 보강하고, 웹 접근성 기준(WCAG AA)과 E2E 테스트로 검증했습니다.
이 과정의 결정 중 지금 돌아봐도 가장 다행인 것은 미리 골라 둔 문제를 JSON으로 묶어 저장소에 넣는 방식을 고른 것입니다. 실시간으로 긁어오는 방식(파싱) 대신이요.
프로그래머스에는 공식 API가 없습니다. 게다가 Cloudflare의 JA3 차단이 걸려 있었습니다. 접속하는 프로그램의 TLS 연결 지문(JA3)을 보고 브라우저가 아닌 자동 요청을 막는 기능입니다. 실시간으로 긁어오는 구조도 만들 수는 있었지만, 그 순간 새로운 외부 의존을 또 심는 셈이었습니다.
데이터를 우리 저장소 안으로 가져와 직접 소유하는 선택은 운영 안정성을 위한 것이기도 했습니다. 동시에 "외부 API는 언제든 끊길 수 있다"는 이번 교훈을 그대로 적용한 결과였습니다.
그럼 기존 백준 데이터는 지웠을까요? 아니요, 그대로 뒀습니다. 마이그레이션 0, 두 플랫폼을 함께 두는 전략입니다. 사용자가 백준으로 풀었던 문제 기록은 종료 공지 이후에도 그대로 남았습니다. 플랫폼은 사라져도, 그 위에서 쌓은 것은 우리 서비스 안에서 살아 있어야 하니까요.
문제 제공처가 바뀌어도 흔들리지 않는 구조
성과
373건
프로그래머스 문제 이식
Lv.1~5 → BRONZE~DIAMOND(알고수의 난이도 등급)에 1:1, 디자인 토큰 수정 0줄
2,445건
테스트 ALL PASS
웹 접근성 WCAG AA 6/6 PASS
0건
DB 마이그레이션
기존 백준 사용자 경험 손상 0
프로그래머스용 Gateway 엔드포인트는 기존 백준용과 같은 모양으로 만들어 추상화 계층을 유지했습니다.
숫자보다 의미 있었던 건 "기존을 부수지 않고 새 플랫폼을 얹었다"는 점입니다. 외부에서 온 충격을 내부 구조 안에서 흡수한 셈이죠.
외부 의존 서비스의 교훈
여기까지가 기술 이야기라면, 이제부터가 이번 전환의 진짜 결론입니다.
"설마"를 설계에서 지워야 한다
의존을 안다는 것과, 의존이 끊길 수 있다는 전제로 설계하는 것은 다릅니다. 저는 앞의 것만 하고 뒤의 것은 미뤄 뒀습니다. sourcePlatform 컬럼을 둔 건 "다른 플랫폼이 추가될 수 있다"는 확장성을 생각한 것이지, "백준이 사라질 수 있다"는 생존 시나리오를 생각한 게 아니었거든요.
두 가정은 비슷해 보여도 설계의 무게가 다릅니다. 확장성은 "있으면 좋다"이고, 생존은 "없으면 안 된다"입니다. "설마 이 플랫폼이?"라는 전제가 남아 있으면, 막상 그 일이 벌어졌을 때 대응이 한 박자 늦습니다.
다행히 기본 추상화는 해 뒀습니다. 하지만 그 추상화를 충분히 깊게 하지 않은 부분들이, 두 스프린트 뒤 PM인 제가 직접 다섯 차례 돌린 QA에서 드러났습니다. 난이도 배지가 플랫폼을 구분하지 못했고, 주차 계산 로직이 백준 전용 가정으로 짜여 있었고, 스터디룸 화면의 네 군데 호출부에 sourcePlatform 값이 전달되지 않았습니다. 전부 "설마 여기까지는"이 남긴 자국이었습니다.
외부 API는 언제든 끊길 수 있다
실시간 파싱 대신 미리 묶어 두기를 고른 건, 공식 API가 없고 Cloudflare 차단이 걸려 있는 데다 문제가 373건으로 정해져 있고 자주 늘지도 않았기 때문입니다. 운영 안정성을 우선한 결정이었는데, 결과적으로 외부 의존을 한 겹 더 줄인 셈이 됐습니다.
이번 전환 이후로 외부 API를 붙일 때 묻는 순서가 바뀌었습니다. 전에는 "이 API가 안정적인가?"를 먼저 물었다면, 이제는 "이 API가 없어져도 서비스가 돌아가는가?"를 먼저 묻습니다. 답이 "아니오"라면, 그 API에 기대는 로직을 더 좁은 경계 안에 가두거나 데이터 소유권을 우리 쪽으로 가져올 방법을 고민합니다.
추상화는 "있으면 좋은 것"이 아니라 "생존 조건"이다
sourcePlatform 컬럼 하나가 얼마나 큰일을 했는지, 이번 전환을 지나고서야 실감했습니다. 이 컬럼이 없었다면 DB 마이그레이션, 기존 기록 재분류, 사용자 기록 보존을 전부 한꺼번에 해야 했을 겁니다. 그 컬럼을 추가하던 초기의 저는 "혹시 몰라서" 정도의 생각이었을 거예요. 그런데 그 "혹시"가 실제로 왔습니다.
많은 추상화 결정이 이렇습니다. 당장은 과해 보이고, 한 겹 더 두르는 건 품이 들죠. 하지만 외부에 기대는 시스템에서는 그 한 겹이 종종 생명줄이 됩니다. "과하다 싶었던 경계"가 나중의 저를 구해 준 경험은 이번이 처음이 아닙니다. 아마 마지막도 아닐 겁니다.
마무리
이번 전환에서 새로 배운 기술은 많지 않습니다. JSON 묶음을 만들 때 쓰는 수집용 크롤러를 브라우저 자동화 도구 Playwright로 짰고, JSON 묶음 구조를 설계했고, 허용 값 목록 검증(@IsIn)으로 입력 범위를 좁혔습니다. 다 이미 알던 것들의 조합이었습니다.
배운 건 태도였습니다. 영원한 플랫폼은 없다는 것. 백준만큼 오래된 서비스도 사라지는데, 지금 우리가 기대고 있는 어떤 외부 서비스도 언젠가 끊길 수 있습니다. 우리가 할 수 있는 건 그 가능성을 설계에 녹여 두는 것뿐입니다. "이 플랫폼만큼은 예외"라는 생각을 지우고, 의존의 깊이를 정할 때부터 "끊기면 어떻게 살아남을까"를 함께 그려 두는 것. 그게 외부에 기대어 만든 서비스가 오래 가는 방법이었습니다.
백준은 사라집니다. 알고수는 남습니다. 다음에 또 어떤 플랫폼이 사라질지는 모르지만, 그때의 저는 조금 덜 놀랄 준비가 되어 있기를 바랍니다.