백엔드 · 파이프라인
2026-09-17 기준 · 담당 정어진. 전체 구성 속 위치는 프로젝트 전체 그림의 그림 1·2에 있다. 이 장은 무엇을 만들었고, 무엇에 막혔고, 어떻게 풀었나를 다룬다.
1) 맡은 것 한눈에
API 서버와 데이터베이스, 그리고 분석 파이프라인의 양 끝 — 들어오는 영상의 검사, 분석 작업 대기열, 돌아온 결과의 적재 — 를 맡았다. 분석 자체(자세 추출·채점)는 정상호, 배포 앞단과 k3s 전환은 박민호가 맡았다.
2) 구조
도메인 지도
분석 작업의 상태 흐름
3) 수치로 본 진행
표로 보기
| 날짜 | 테스트 |
|---|---|
| 08-25 | 70 |
| 08-26 | 127 |
| 08-28 | 165 |
| 09-02 | 318 |
| 09-03 | 439 |
| 09-04 | 452 |
| 09-08 | 633 |
| 09-09 | 693 |
| 09-10 | 697 |
| 09-11 | 776 |
| 09-15 | 887 |
| 09-16 | 1,023 |
| 09-17 | 1,068 |
| 날짜 | 테이블(누적) | 마이그레이션(누적) |
|---|---|---|
| 08-26 | 8 | 3 |
| 08-28 | 14 | 6 |
| 09-01 | 16 | 7 |
| 09-02 | 19 | 9 |
| 09-03 | 27 | 12 |
| 09-04 | 27 | 14 |
| 09-08 | 30 | 20 |
| 09-09 | 30 | 25 |
| 09-10 | 31 | 30 |
| 09-11 | 31 | 32 |
| 09-15 | 41 | 37 |
| 09-16 | 43 | 44 |
| 09-17 | 43 | 47 |
4) 어려운 문제와 해결
A. 소리 없는 장애 — 하루 넘게 분석이 멈췄는데, 백엔드는 몰랐다
상황. 09-16 08:27부터 분석이 한 건도 돌지 않았다. 작업은 대기열에 쌓이는데 가져가는 워커가 없었고, 백엔드 로그에는 워커의 요청이 아예 찍히지 않았다.
막힌 점. 백엔드에서 보이는 것은 “요청이 안 온다”뿐이라, 워커가 죽었는지·네트워크 문제인지·서버 문제인지 가를 단서가 없었다.
어떻게 좁혔나.
- (정상호) 워커 기록에서 실패 응답을 찾았고, 그 응답을 보낸 것이 우리 서버가 아니라 앞단이라는 흔적을 확인했다. 마지막 정상 처리는 08:11, 첫 실패는 08:27 — 워커는 코드·설정·재시작 없이 같은 프로세스 안에서 갈렸다. 원인은 워커 밖에 있었다.
- (정상호) 조건을 하나씩 바꿔 같은 경로를 불러 차단 기준을 특정했다. 이때 작업을 소비하는 호출은 쓰지 않고 조회 방식으로만 시험했다 — 시험이 운영 데이터를 바꾸지 않게.
- (정상호) 워커가 요청마다 자기 이름을 밝히도록 고쳐 분석을 되살렸다.
- (정어진) 서버에서 같은 시험을 재현하고, 앞단을 거치지 않은 직접 호출은 정상인 것을 확인했다. 그런데 배포 문서 어디에도 그 앞단이 없었다 — 문서는 nginx를 맨 앞으로 적고 있었다.
고른 것. 워커 쪽 수정은 규칙을 피한 것이지 규칙이 우리를 안 막게 된 것이 아니라서, 문제를 닫지 않고 앞단 규칙을 맡은 박민호에게 근본 조치(워커 전용 경로를 차단 대상에서 빼기)를 넘겼다. 배포 문서에 앞단을 적고 「백엔드는 멀쩡한데 밖에서만 막히면 앞단부터 본다」를 남겼다.
배운 것. 설정을 확인하는 명령 하나가 서버 이관 뒤로 아무것도 못 잡는 상태였는데, “안 걸린다”가 “문제가 없다”로 읽혀 왔다. 확인 명령은 살아 있는지 판별되게 고쳤다 — 결과가 비면 그 자체가 이상 신호가 되는 형태로.
B. 지우지 않는 삭제 — 마지막 주장이 팀을 버릴 방법이 없었다
상황. 팀의 마지막 주장은 나갈 수 없게 막혀 있었다 — 주장이 없는 팀에는 아무도 사람을 넣을 수 없어서다. 그런데 팀을 없애는 길도, 다른 사람을 주장으로 세우는 길도 없었다. 오류 안내는 “다른 주장을 먼저 세우라”고 했지만 세울 방법이 없었다. 혼자 만든 팀은 영영 버릴 수 없었다.
막힌 점. 요청은 “팀 삭제”였다. 그런데 팀을 가리키는 외래키가 9개였고 그중 5개가 삭제를 막는다(경기·스쿼드·경기 신청·구성원). 설계 문서의 삭제 연쇄도 팀 삭제를 정해 두지 않았다.
| 선택지 | 얻는 것 | 잃는 것 |
|---|---|---|
| 진짜 삭제 + 삭제 연쇄를 새로 정의 | 요청 그대로 | 세 도메인에 걸친 규칙을 새로 만들어야 하고, 지난 경기·평가가 가리키던 팀이 사라진다 |
| 해체 표시 (행은 남김) | 이력 보존 · 스키마 변경은 칸 하나 | 새로 만드는 자리마다 해체 여부를 봐야 한다 |
| 그대로 둠 | 변경 없음 | 사용자가 계속 막힌다 |
고른 것과 이유. 해체 표시. 구성원 탈퇴도 이미 같은 이유로 행을 지우지 않고 탈퇴 시각만 남기고 있었다 — 팀에도 같은 원칙을 적용했다. 여럿인 팀의 주장이 나갈 수 있게 주장 세우기도 함께 냈다.
구현하다 찾은 구멍. 처음엔 “구성원을 전부 내보내면 되지 않나”였는데, 본인 가입은 누구나 할 수 있어서 표시가 없으면 해체한 팀에 아무나 들어와 팀이 되살아난다. 그래서 표시 칸이 꼭 필요했고, 막는 범위는 가입·초대·수정(새로 만드는 자리)으로만 잡았다 — 읽기와 이력은 그대로다. 앞으로 있을 경기가 있으면 해체를 막았다(상대 팀에게는 약속이다). 경기 탐색이 다가오는 경기만 보여 주므로, 이 규칙 하나로 “없는 팀의 경기가 탐색에 뜨는” 문제가 구조적으로 생기지 않는다.
결과. 검사 21건(계약 15 · 실제 DB 6)을 더했고 전체 1,055건이 통과했다.
C. 첫 설계를 스스로 버리다 — 같은 영상을 다시 올렸을 때
상황. 같은 사람이 같은 영상을 다시 올리면 GPU로 똑같은 분석을 또 돌렸다. 앞서 실패한 영상이면 같은 이유로 또 실패했다.
1차안. 새 분석 작업을 만들되 곧바로 “성공” 또는 “실패”로 끝내고 앞 결과를 복사한다. 기존 흐름(작업 → 결과)을 그대로 쓸 수 있어 간단해 보였다.
버린 이유. 코드로 옮기기 전에 상태 규칙을 다시 읽다가 발견했다 — 대기에서 바로 끝내기는 금지다(그림 2의 빨간 점선). 워커가 가져가지 않은 작업이 끝난 것처럼 보이면, 시작·끝 시각의 차이로 재는 처리 시간 지표가 거짓이 된다. 게다가 결과를 복사하려면 리포트를 다시 적재하는 일까지 따라왔다.
2차안(채택). 새 작업을 아예 만들지 않고, 새 영상이 앞 영상을 가리키기만 한다. 등록 응답에 앞 결과(성공·실패와 사유)를 바로 싣는다. 재적재가 통째로 사라졌다.
좁힌 조건 셋. 내용이 같은지는 저장소가 이미 알려 주는 파일 지문으로 판단한다(다시 내려받지 않는다) · 본인 영상끼리만 · 분석 대상을 따로 지정한 영상은 제외한다(같은 영상이라도 누구를 봤는지가 다르면 결과가 다를 수 있다).
구현하며 밟은 함정. 두 JSON 칸은 “비어 있음”이 SQL의 NULL이 아니라 JSON의 null 값으로 저장돼서, 조건이 실제 데이터에 한 건도 걸리지 않았다. 실제 DB로 조회해 보는 통합 검사에서야 드러났다 — 가짜 저장소로만 검사했다면 통과했을 자리다.
D. 경계를 말이 아니라 검사로 — 도메인 일곱이 섞이지 않게
상황. 백엔드를 도메인 일곱으로 나눴다(그림 1). 다른 팀원도 백엔드에 도메인을 붙이는 기간이 있었고(과금은 박민호가 만들었다), 규칙을 문서에만 적으면 반드시 무너진다.
고른 것. 규칙을 검사로 바꿨다. 검사 12개가 도메인끼리 코드를 가져다 쓰는지, 계층이 거꾸로 의존하는지, 새 테이블이 마이그레이션 도구에 등록됐는지를 매번 확인하고, 어기면 CI가 막는다.
치른 대가 둘.
- 다른 도메인의 값이 필요하면 그 도메인 코드를 부를 수 없어 테이블을 직접 읽는다 — 15쌍, 45곳이다. 그런데 저쪽 칸 이름이 바뀌면 파이썬이 잡아 주지 못한다. 그래서 그런 자리마다 실제 DB로 대조하는 통합 검사를 두었고, DB가 없어 검사가 건너뛰어지면 CI가 실패하게 했다 — “초록불인데 사실은 안 돌았다”를 막으려는 것이다.
- 계산 규칙도 빌려 쓸 수 없다. 추천 판(매칭)이 등급(영상 분석)을 계산해야 해서 등급 규칙을 매칭 쪽에 복제했다. 둘이 갈라지지 않게 지키는 일이 따라온다.
또 하나의 판단. 도메인들을 조율하는 별도 계층은 넣지 않았다. 지금 도메인 사이의 흐름은 전부 읽기라서, 도메인을 넘나드는 쓰기 흐름이 실제로 생기면 그 사례로 뽑아내기로 했다 — 상상한 흐름에 맞춰 미리 만들면 실제 코드가 왔을 때 다시 깎게 된다.
이 장의 수치를 다시 뽑는 명령
# 테스트 수 — 커밋 메시지의 전체 실행 결과(두 표기)를 날짜별로
git log --reverse --author=roqkf --format='@@%ad%n%B' --date=short \
| awk '/^@@/{d=substr($0,3); next}
{ l=$0; while (match(l, /(pytest|전체)[^0-9]{0,20}[0-9]+ passed/)) {
s=substr(l,RSTART,RLENGTH); gsub(/[^0-9]/," ",s); n=split(s,b," ");
if (b[n]+0>m[d]) m[d]=b[n]+0; l=substr(l,RSTART+RLENGTH) } }
END{for(k in m) print k, m[k]}' | sort
# 테이블·마이그레이션 — 마이그레이션 파일이 들어온 날짜별로
for f in fastapi/alembic/versions/*.py; do
echo "$(git log --diff-filter=A --format=%ad --date=short -- "$f" | tail -1) $(grep -c 'op.create_table(' "$f")"
done | sort | awk '{m[$1]++; t[$1]+=$2} END{for(k in m) print k, m[k], t[k]}' | sort
# 계약 변경 항목 수 (번호 5~9는 처음부터 없다)
grep -cE '^## [0-9]+\.' fastapi/docs/client-contract-changes.md