프로젝트를 하다가 AI에게 물었다. "개발 시간이 더 걸리는 느낌인데, 난이도가 높아서 그래?" 답은 "난이도보다 프로세스 무게"였다. 그런데 그 뒤 두 주 치 기록을 다시 보니 그 답은 반만 맞았다. 같은 프로세스를 밟는데도 이슈 하나가 20분 만에 닫히기도 하고, 이슈 네 개에 하루가 걸리기도 했다.
0. 질문과 첫 번째 답
2026-07-27 오전, AIFid(멀티테넌트 실시간 상담 시스템) 세션에서 이렇게 물었다.
이 프로젝트 개발할 때 시간이 더 걸리는 느낌인데 난이도가 높아서 그런 거야?
AI의 답을 요약하면 이랬다.
- 커밋 로그를 보면 기능 하나마다
설계 확정 → 구현 계획(N개 태스크) → feat3단계를 거친다. 코드 쓰기 전에 문서화 비용이 먼저 붙는다. CLAUDE.md의 절대 규칙 9개(테넌트 격리, 내부 노트 유출 방지 테스트, PHPStan 6, 커버리지 70%, 마이그레이션down()필수 등)가 매 기능마다 체크리스트로 걸린다.- 스택이 4갈래(CI4 백오피스 / Node 엔진 / React 콘솔 / Preact 위젯)라 도메인 자체도 단순하지는 않다.
- 결론: 난이도와 의도된 프로세스 오버헤드가 둘 다 기여하지만, 체감 지연의 상당 부분은 후자다.
이어서 "프로세스 무게를 줄일 방법이 있냐"고 물었고, 작업 크기별로 풀 사이클 여부를 판단하자는 제안이 나왔다. 그 기준을 CLAUDE.md에 "설계 문서 생략 기준"으로 적었다.
- 생략 가능: 선택지가 하나뿐인 버그 수정, 마이그레이션 없이 컴포넌트 하나 안에서 끝나는 소규모 기능, 3절 절대 규칙에 영향이 없는 변경, 이슈에 수용 기준이 이미 구체적인 백로그.
- 생략 금지: 여러 컴포넌트에 걸친 변경, 새 도메인·테이블·외부 연동, 테넌트 격리·권한에 영향을 주는 변경, 트레이드오프 결정이 필요한 경우.
여기까지가 그날의 이야기다. 이 글은 그 이후, 이슈 #69~#113을 /dev-issue로 연달아 처리한 기록을 가지고 "정말 프로세스 무게 때문이었나"를 다시 따져 본 회고다.
먼저 짚어 둘 점. 첫 답은 커밋 로그를 보고 추정한 것이지 시간을 잰 결과가 아니었다. 아래 숫자는 GitHub 이슈의 생성·종료 시각과 PR 변경량이다. PR은 작업이 끝난 뒤에 올리므로 "PR 생성→머지 시간"은 작업 시간이 아니라서 쓰지 않았다. 이슈 종료 시각에는 잠자는 시간이 섞여 있다.
1. 프로세스 무게는 상수다
첫 답의 약점은 이것이다. 프로세스 무게(설계 → 계획 → TDD → 게이트 통과)는 이슈마다 거의 같다. 그런데 소요 시간은 이슈마다 열 배 넘게 달랐다. 상수로는 변동을 설명할 수 없다. 프로세스는 **바닥(기본 비용)**을 정할 뿐이고, 그 위에 얹히는 시간은 다른 변수가 정한다.
실제 기록을 두 덩어리로 나눠 보자.
| 모바일 SDK (iOS·Android·Flutter) | 세션 무효화 후속 (#110~#113) | |
|---|---|---|
| 이슈 | #69~#80, #92~#95 (14개) | #110, #111, #112, #113 (4개) |
| 기간 | 07-31 16:26 ~ 08-01 19:59 (KST) | 08-06 20:48 생성 ~ 08-07 17:45 종료 (KST) |
| 관련 PR 변경량 | #83~#99 합계 약 +18,000줄 | #114~#119 합계 약 +4,650줄 |
| 이슈당 간격 | 대개 15~35분 | 이슈 하나에 2시간 이상 (첫 이슈는 밤을 넘김) |
코드량은 SDK 쪽이 네 배 가까이 많은데 시간은 훨씬 짧다. 난이도가 "코드가 많음"이라면 이 표는 말이 안 된다.
2. 빨랐던 쪽: 결정이 끝나 있었다
iOS SDK 이슈 네 개(#72~#75)는 07-31 하루 오후에 16:59, 17:20, 17:55, 18:11로 연달아 닫혔다. 변경량은 +1,766, +837, +1,348, +240줄. Android는 다음 날 12:00, 12:26, 12:57에 세 개가 닫혔고(+2,218, +766, +1,201줄), Flutter는 PR 하나가 +7,057줄·95파일이었는데 이슈 네 개(#92~#95)가 08-01 18:30~19:59, 한 시간 반 사이에 닫혔다.
빨랐던 이유는 코드가 쉬워서가 아니라 결정할 게 남아 있지 않아서였다.
- 07-30에 모바일 SDK 설계 문서를 먼저 확정했고, 백엔드 푸시 API(#71, #81, #82)도 SDK 이슈보다 앞서 끝나 있었다. 계약이 이미 있었다.
- Android는 커밋 메시지에 "iOS 미러링"이라고 적을 만큼 iOS를 번역하는 작업이었다. Flutter는 네이티브 SDK를 감싸는 브릿지였다.
- 이슈에 수용 기준이 구체적으로 적혀 있어서
CLAUDE.md의 생략 기준을 대부분 만족했다. 계획 문서를 별도로 쓰지 않고 TDD로 바로 들어갈 수 있었다.
타자 치는 양과 걸리는 시간은 별개라는 얘기다. 결정이 끝난 작업은 양이 많아도 번역에 가깝다.
3. 느렸던 쪽: 고칠 때마다 다음 구멍이 나왔다
세션 무효화(정지·삭제된 계정의 WebSocket을 즉시 끊기)는 설계 문서가 +2,001줄이었고(#105), 구현(#108)은 +1,566줄이었다. 여기까지는 보통이었다. 시간은 그 뒤에 들어갔다. 이슈 네 개가 서로의 후속이었다.
- #110 — 토큰 재사용(탈취 의심)을 탐지해 세션을 끊는데 계정은 그대로
active라서, 탈취범이 재접속하면valid: true로 다시 붙는다. 끊는 효과가 사실상 없었다. - #111 — 폐기 시 엔진이
connection.error를 보내는데 그걸 처리하는 클라이언트가 하나도 없었다. 폐기된 소켓이 재연결 → 거부 → 재연결을 무한 반복한다. - #112 —
account_disabled폐기가 정지하는 순간이 아니라 정지된 계정이 토큰 갱신을 시도할 때만 발행된다. 정지 기능 자체가 아직 없어서 훅을 달 곳이 없었다. - #113 — bootstrap 경합과 Pub/Sub 메시지 유실로 폐기를 놓친 소켓이 재검증 없이 남는다.
하나를 고치면 그 고침이 전제하던 가정이 깨져 다음 이슈가 됐다. 이슈마다 diff는 작거나 중간인데(#115는 +433줄, #119는 +350줄) 시간은 길었다. 줄 수가 아니라 되돌아간 횟수가 시간을 먹었다.
가장 아팠던 건 #114였다. 로그아웃 시 세션 폐기가 아무 소켓도 못 끊고 있었다. 로그아웃 봉투에 실리는 jti는 API 토큰의 것이고, 소켓이 쥔 건 엔진 토큰의 jti다. 두 토큰은 issue()를 각각 호출해 만들어지고 issue()는 매번 새 uuid를 채우니 구조적으로 일치할 수 없다. 그런데 백오피스 테스트는 "봉투에 값이 실렸는지"만, 엔진 테스트는 목 소켓에 같은 값을 스스로 심어 확인했다. 양쪽 다 초록불이었고, 로그에는 revoke.applied가 정상처럼 찍혔다. 공통 sid를 도입해서 고쳤다.
그날 얻은 교훈은 CLAUDE.md에 규칙 10번으로 들어갔다. "컴포넌트 경계를 넘는 값은 '같은 값인가'를 테스트로 고정한다."
이 구간에는 난이도 말고도 시간을 먹은 것이 더 있었다.
- 병렬 세션이 같은 계획서를 보고 각자 구현해서 PR 하나를 통째로 버렸다. (큐 룸 인가, 세션 무효화)
- 같은 체크아웃에서 테스트를 두 프로세스로 동시에 돌리면 스키마가 깨져
Table doesn't exist,Deadlock found같은 에러가 매번 다르게 나온다. 코드 문제로 오인하기 쉬워서 디버깅에 시간을 썼다. - 콘솔을 두 탭에 띄우면 두 탭이 같은 refresh 토큰으로 동시에 회전해서, 정상 사용자가 "탈취"로 잡혀 로그아웃됐다(#115). 서버 로직이 아니라 브라우저 탭이라는 환경 변수 때문에 생긴 버그였다.
4. 코드가 쉬운데도 오래 걸린 날
난이도가 아닌 이유로 오래 걸린 사례가 두 개 더 있다.
- #104 / #100 — Android Maven groupId를
io.aifid에서kr.aivance.fid로 바꾸는 일. diff는 +132/−128줄, 단순 치환이다. 하지만 48개 파일이고, 이슈 #100은 Sonatype 네임스페이스 검증을 기다리느라 08-01에 열려 08-05에 닫혔다. 코드 난이도는 0에 가깝고 외부 대기가 4일이었다. - CI 전환(#129~#144) — 러너를 옮긴 날 PR이 열여섯 개 쌓였다. 하나하나는 한 줄짜리였다. CI에서 "한 줄 찍고 재실행"이 한 번에 5~10분이라 가설 하나 검증하는 비용이 컸다. 자세한 건 CI 고치다 PR이 15개 쌓인 날에 적었다. 여기서 시간을 정한 건 코드 난이도가 아니라 피드백 루프의 길이였다.
5. 정리: 소요 시간을 가늠하는 식
기록을 놓고 보니 "난이도"라는 한 단어가 서로 다른 네 가지를 뭉쳐 놓고 있었다.
소요 시간 ≈ 프로세스 고정비
+ 작업량 × (결정이 끝났으면 작고, 안 끝났으면 크다)
+ 되돌아간 횟수 × 한 번 되돌아가는 비용
+ 대기 (외부 검증, 느린 피드백 루프)
- 프로세스 고정비는 이슈마다 거의 같다. 첫 답이 말한 부분이다. 줄일 수 있지만(생략 기준) 변동은 설명하지 못한다.
- 작업량은 SDK가 보여 줬듯 결정이 끝난 상태면 많아도 빠르다.
- 되돌아간 횟수가 가장 큰 변수였다. 특히 컴포넌트 경계를 넘는 불변식(
sid, 룸 이름, 봉투 필드)은 한쪽 테스트가 초록불이어도 맞는지 알 수 없다. - 대기는 코드가 쉬워도 시간을 먹는다.
착수 전에 던질 질문을 세 개로 줄이면 이렇다.
- 결정이 이미 끝났나? 설계·계약이 문서로 있고 이슈에 수용 기준이 있으면 빠르다. 없으면 설계부터 한다. 생략 기준이 이걸 가른다.
- 값이나 계약이 컴포넌트 경계를 넘나? 넘으면 "양쪽이 같은 값을 보는지"를 확인하는 테스트를 먼저 하나 둔다. 이 질문에 "예"인 작업은 예상 시간에 두 배를 곱해도 모자란다.
- 한 번 확인하는 데 몇 분 드나, 외부에서 기다릴 게 있나? CI에서만 재현되거나 외부 승인이 걸려 있으면 코드 난이도와 무관하게 길어진다. 로컬 재현부터 만든다.
6. 그래서 처음 질문의 답은
"난이도가 높아서 오래 걸리나?"에는 **"난이도 하나로는 설명이 안 된다"**가 답이다. 첫 답처럼 프로세스 무게가 바닥을 깔고, 그 위의 변동은 결정이 끝났는지, 경계를 넘는 불변식이 있는지, 피드백이 느린지가 정했다. 프로세스를 줄이는 건 바닥을 낮출 뿐이고, 오래 걸린 이슈들은 대부분 바닥이 아니라 그 위에서 시간을 썼다.
한 가지 더 남은 소득은, 설계 문서 생략 기준이 실제로 작동했다는 점이다. 07-27 이후 IDOR·인가 버그 수정(#101~#103)은 문서 없이 바로 TDD로 갔고, 권한 모델에 영향이 있는 세션 무효화(#105)는 기준대로 설계 문서를 먼저 썼다. 그리고 그 설계가 놓친 구멍(#110~#113)이 바로 그 뒤에 줄줄이 나왔다는 점은, 설계 문서가 있어도 경계를 넘는 불변식은 테스트로 고정하지 않으면 안 잡힌다는 걸 보여 준다.
근거: AIFid 저장소 GitHub 이슈 #69~#113의 생성·종료 시각(UTC를 KST로 환산), PR #83~#119의 변경량, 2026-07-27 세션 로그의 질의응답, CLAUDE.md의 생략 기준·규칙 10번, 커밋 #114·#115 메시지. 시각에는 수면 시간이 포함되어 있어 "순수 작업 시간"이 아니다.
댓글 0개