프로젝트를 하다가 AI에게 물었다. "개발 시간이 더 걸리는 느낌인데, 난이도가 높아서 그래?" 답은 "난이도보다 프로세스 무게"였다. 그런데 그 뒤 두 주 치 기록을 다시 보니 그 답은 반만 맞았다. 같은 프로세스를 밟는데도 이슈 하나가 20분 만에 닫히기도 하고, 이슈 네 개에 하루가 걸리기도 했다.

0. 질문과 첫 번째 답

2026-07-27 오전, AIFid(멀티테넌트 실시간 상담 시스템) 세션에서 이렇게 물었다.

이 프로젝트 개발할 때 시간이 더 걸리는 느낌인데 난이도가 높아서 그런 거야?

AI의 답을 요약하면 이랬다.

  • 커밋 로그를 보면 기능 하나마다 설계 확정 → 구현 계획(N개 태스크) → feat 3단계를 거친다. 코드 쓰기 전에 문서화 비용이 먼저 붙는다.
  • 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, 룸 이름, 봉투 필드)은 한쪽 테스트가 초록불이어도 맞는지 알 수 없다.
  • 대기는 코드가 쉬워도 시간을 먹는다.

착수 전에 던질 질문을 세 개로 줄이면 이렇다.

  1. 결정이 이미 끝났나? 설계·계약이 문서로 있고 이슈에 수용 기준이 있으면 빠르다. 없으면 설계부터 한다. 생략 기준이 이걸 가른다.
  2. 값이나 계약이 컴포넌트 경계를 넘나? 넘으면 "양쪽이 같은 값을 보는지"를 확인하는 테스트를 먼저 하나 둔다. 이 질문에 "예"인 작업은 예상 시간에 두 배를 곱해도 모자란다.
  3. 한 번 확인하는 데 몇 분 드나, 외부에서 기다릴 게 있나? 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 메시지. 시각에는 수면 시간이 포함되어 있어 "순수 작업 시간"이 아니다.