Joyuno's Dev Blog

2026-09-21 (월) 개발일지

dev-blog

오늘의 회고: 120제 저작 마무리와 복습 API 구현

오늘은 게임 프로젝트에서 밀린 120제 저작을 완료하고, 오래전 유보했던 복습 목록 엔드포인트와 화면을 구현했다. Piston 저지에서 한 미션이 internal_error로 떨어진 이유를 추적하다 보니 예산 문제였고, 복습 API는 계산 함수만 덩그러니 있는 상태에서 라우터부터 스키마까지 전부 새로 만들어야 했다.

boj-14502-laboratory Piston 저지 internal_error — cpu_time_limit 예산이 원인이었던 이유

전체 184개 미션이 통과한 가운데 boj-14502-laboratory만 internal_error로 떨어졌다. 처음엔 정답 로직이 틀렸다고 생각했다. 문제는 8×8 격자에서 벽 3개를 세워 바이러스 확산을 막는 시뮬레이션 — 로직 자체는 단순한 BFS라 검증된 코드였다. 로그를 다시 뒤져도 internal_error는 저지 내부에서 발생한 예외일 뿐, 구체적인 사유가 찍히지 않았다.

첫 가설은 숨은 케이스에 문제가 있다는 쪽이었다. 혹시 내가 만든 히든 케이스가 잘못된 정답을 요구하는 건 아닐지, 정답과 출력을 대조해봤지만 모두 일치했다. 그제서야 저지의 cpu_time_limit 설정을 확인했다 — 2.0초. 숨은 케이스 하나를 직접 실행해보니 8×8 격자에서 빈 칸이 54개, C(54,3) ≈ 24,800회의 BFS를 돌고 있었다. 호스트 CPython에서 1.81초. 에뮬레이션 환경의 오버헤드를 감안하면 2.0초를 넘길 게 분명했다.

진짜 원인은 정답 오류가 아니라 예산이었다. 숨은 케이스가 cpu_time_limit을 넘겨 저지가 강제 종료시킨 것.

해법은 숨은 케이스의 빈 칸을 줄이는 쪽으로 정리했다. 격자 크기를 8×8에서 6×6으로 낮추고, 벽을 세울 수 있는 빈 칸을 20개 미만으로 제한해 BFS 호출 횟수를 1,000회 안쪽으로 떨어뜨렸다. 다른 옵션으로 cpu_time_limit 자체를 늘리는 것도 고려했지만, 모든 미션이 동일한 제한을 공유하기 때문에 한 미션만 예외를 주는 건 일관성을 해친다고 판단했다.

검증: 수정 후 전체 185개 미션을 Piston 저지에 다시 돌렸고, boj-14502-laboratory까지 포함해 185 passed, 0 failed. 히든 케이스도 정상 통과했다.

남은 과제: cpu_time_limit이 2.0초로 고정된 상태에서 앞으로 미션을 저작할 때 숨은 케이스의 계산 복잡도를 사전에 체크하는 룰을 추가할 예정이다.

복습 목록 API — review.py에 라우터가 없어서 처음부터 만든 과정

app/services/review.py를 열었을 때 모듈 docstring에 이런 문장이 있었다: “이 스펙의 범위는 계산 함수까지다. 복습 목록을 노출하는 엔드포인트와 화면은 §6.3의 별도 사이클이다.” 즉, 복습 일정 계산(review_due_dates, is_memorized, blank_attempts)은 이미 구현돼 있었지만, 이걸 클라이언트에 내려줄 API가 전혀 없었다.

처음엔 기존 app/routers/submissions.py에 엔드포인트 하나만 추가하면 될 거라 생각했다. 하지만 submissions.py는 제출 내역 조회에 특화돼 있었고, 복습 목록은 사용자별로 due date가 계산된 항목을 반환해야 했다. 응답 스키마도 없었다.

원인 추적은 review.py의 함수 시그니처를 분석하는 데서 시작했다. review_due_dates(user_id)는 (problem_id, due_date) 리스트를 반환하고, is_memorized(user_id, problem_id)는 연속 3회 정답 여부를 판단한다. 이 두 정보를 합쳐 클라이언트에 보여줄 ReviewItem을 정의해야 했다. 또한 blank_attempts로 사용자가 해당 문제를 몇 번 시도했는지도 필요했다.

해법은 세 가지 파일로 나뉘었다:

파일 변경
app/schemas.py ReviewItem 스키마 추가 (problem_id, title, due_date, attempts, is_memorized)
app/routers/review.py 신규 생성. GET /api/review/due 엔드포인트 — review_due_dates() 호출 후 submissions.py의 get_submission_status와 조인
Godot 클라이언트 api_response.gd에 ReviewItem 매핑 추가, 복습 목록 화면 구현

review.py의 계산 함수는 이미 테스트가 완료된 상태였기 때문에, 라우터에서는 데이터를 조합해 반환하는 역할만 했다. submissions.py의 get_submission_status를 재사용한 점이 시간을 많이 절약해줬다.

검증: 백엔드 pytest 27 passed, Godot 클라이언트 스크립트 16개 전부 0 failed. 실제로 복습 목록 화면을 띄워 due date가 오늘인 문제 3건이 정확히 노출되는 걸 확인했다.

남은 과제: 복습 목록에 페이지네이션이 없다. 사용자가 100개 이상의 문제를 풀었을 때를 대비해 커서 기반 페이지네이션을 추가할 계획이다.

오늘의 한줄

계산 로직과 API 엔드포인트를 분리한 설계는 옳았지만, 유보 항목을 언제 구현할지 명시하지 않으면 그대로 방치된다 — §6.3은 오늘에서야 해결됐다.