Free

3,000줄을 한 번에 배포—복잡한 기능은 왜 plan부터 시작해야 하는가

복잡한 기능 앞에서의 본능은 "일단 한번 해보자"다—하지만 아키텍처 결정은 세 번째 모델, 다섯 번째 edge case에 숨어 있다. plan mode의 진짜 가치는 아키텍처급 대화를 코드 전의 문자 단계로 옮기는 것이다. 실전: Pickful의 커뮤니티 배심 시스템—3,032줄, 119 spec 전부 통과, 단일 commit으로 배포, 이후 아키텍처 재설계 0회.


복잡한 기능 앞에서 대부분의 사람이 보이는 본능은 "일단 좀 써보자"다.

문제는 복잡한 시스템에는 특징이 있다는 것이다—아키텍처 결정은 세 번째 모델, 다섯 번째 edge case, 여덟 번째 포인트 규칙 속에 숨어 있다. 코드 속에 머리부터 뛰어들었다가 그런 결정을 마주치고 되돌아가 고치면, 처음부터 문자 단계에서 합의했을 때보다 10배 비싸진다.

나는 Claude Code의 plan mode로 Pickful의 TopicReview 커뮤니티 배심 시스템을 만들면서 이 부등식의 극단적인 버전을 목격했다—단일 commit, 3,032줄, 119개의 spec 전부 녹색, 한 번의 배포. 이후 열몇 개의 commit 중 아키텍처 재작업은 하나도 없었고, 전부 파라미터 튜닝, UI 다듬기, edge case 보수뿐이었다. 시스템은 계속 안정적으로 돌아가고 있고, 지금 커뮤니티 자치의 핵심이다.

이 글은 왜 복잡한 기능이 plan부터 시작해야 하는지, plan 단계에서 실제로 무엇을 하는지, 대화가 어디까지 진행되면 코드를 쓰기 시작해도 되는지에 관한 것이다.

시스템이 얼마나 복잡한가

TopicReview는 질 낮은 게시물을 제거할지 말지를 커뮤니티 투표로 결정하는 시스템이다. 한 문장이면 끝나지만—스펙은 겹겹이 펼쳐진다:

  • 5개의 상태: open → voting → decided → appealed → closed
  • 3개의 판결: remove / warn / keep
  • 2단계 심리: 초심은 배심원 12명이 투표; 항소는 judge 5명이 재심(포인트 상위 20명에서 추첨)
  • 다축 포인트 흐름: 10 pt 가처분, 10 pt 항소 stake, 배심원이 다수와 일치하면 +5, judge가 다수와 일치하면 +10, 신고자의 신고가 인용되면 +3, 항소 승소 시 stake + bonus + 가처분 환급
  • 4종의 스케줄 작업: 24h 초심 투표 창, 24h 항소 창, 24h 항소 심리 창, 그리고 remove 판결의 경우 배심원 포인트 지급을 24h 지연(항소로 뒤집힐 수 있으니)
  • 병렬 + 롤백: provisional removal 중에 표가 뒤집히면 게시물 복구 + 가처분 환급; 항소가 뒤집으면 stake 환급 + bonus + 필요 시 가처분 환급 + 배심원은 새 판결로 재계산

복잡성의 핵심은 단일 규칙이 아니라 규칙 간 상호작용에 있다. 규칙 하나를 더할 때마다 다른 어딘가의 롤백을 촉발할 수 있다.

plan을 거르면 부딪히는 벽

그냥 바로 쓰다 보면 제일 어려운 문제는 눈에 보이는 사실이 아니다—당신이 물어볼 생각조차 못한 질문이다. TopicReview 코드를 거꾸로 짚어보면, plan 없이 가면 반드시 부딪힐 벽이 최소 네 곳 있다:

배심원 자격 규칙. 겉보기는 그저 User.jurors_and_judges.sample(12). 하지만 실제 규칙은: 원작자 제외, 신고자 제외, 이미 초심에서 투표한 사람 제외(같은 사람이 항소에도 투표하는 것을 막기 위해)—세 겹의 제외. 모델을 한 번에 쓰다 보면 한두 개 빠뜨리기 쉽다.

포인트 지연 지급. remove 판결은 보통 배심원 포인트 지급을 유발한다. 하지만 항소가 remove를 뒤집을 수 있다—뒤집히면 배심원의 다수파가 반전되므로 포인트를 새 판결로 재계산해야 한다. 그래서 remove 판결은 24h 항소 창이 닫힌 뒤에야 포인트를 지급해야 한다. 이 규칙을 적어두지 않고 먼저 지급하면, 포인트를 되돌리러 가야 한다—지연 지급보다 10배 더 성가시다.

스케줄 작업 인터페이스 설계. CloseTopicReviewJob은 겉보기엔 "review를 끝낸다"다. 실제로는 세 가지 시나리오를 처리한다:

# 초심 투표 창 만료
CloseTopicReviewJob.set(wait_until: voting_ends_at).perform_later(review.id)
# remove 판결 후 배심원 포인트 지연 지급
CloseTopicReviewJob.set(wait: 24.hours).perform_later(review.id, award_juror_points: true)
# 항소 창 만료
CloseTopicReviewJob.set(wait: 24.hours).perform_later(review.id, appeal_id: appeal.id)

먼저 정리하지 않으면, 단일 시그니처로 먼저 쓰고, 두 번째 시나리오를 구현할 때 전체 인터페이스를 재설계해야 한다.

가장 비싼 벽: provisional removal 중 항소 가능 여부. 표가 다수 remove에 도달하면 게시물은 즉시 감춰진다(provisional), 그러나 전체 review는 여전히 voting 상태—decided가 아니다. 사용자는 이 시점에 항소할 수 있는가?

  • 가능 → decidedappealed 사이의 transition을 확장해야 한다
  • 불가능 → UX가 나쁘다. 게시물은 감춰져 있는데 24h를 기다려야 항의할 수 있다
  • TopicReview의 실제 선택: provisional 중에도 항소 가능. 단, 먼저 finalize → decided 를 통과한 뒤 appeal을 open한다

이 결정 하나가 거꾸로 open_appeal!의 파라미터 검증과 상태 머신 논리 전체에 영향을 준다. 중간에 결정한다 = 시스템의 절반을 다시 쓰는 것.

plan 단계에서 실제로 무엇을 하는가

Claude Code의 plan mode는 코드 작성이 허용되지 않는 모드다—Claude는 레포를 읽고, 접근 방법을 고민하고, 당신과 논의할 수 있지만, 어떤 파일 수정도 당신이 plan을 승인할 때까지 hard-block된다.

그 기계적 제약이 바로 plan mode의 핵심 가치다—아키텍처급 대화를 문자 단계에서 일어나도록 강제한다.

plan 단계에서 실제로 하는 몇 가지:

1. 상태 머신과 역할 그리기. 5개 상태, 4개 역할(배심원 / judge / 게시자 / 신고자), 각 역할이 각 상태에서 무엇을 할 수 있고 할 수 없는지—전부 몇 줄의 markdown에 담긴다. 몇 줄 대 수십 파일, 변경 비용은 두 자릿수 차이.

2. 각 역할 흐름 walkthrough:

  • 배심원 관점: 알림 수신 → review 열기 → 게시물과 이유 읽기 → 투표 + reasoning → 포인트 수령
  • 게시자 관점: 알림 수신 → 판결 확인 → remove면 항소 고려 → stake 예치 → 대기
  • Judge 관점: top-20에서 추첨된 appeal 수신 → 투표 → 포인트 수령

걷다가 막히는 곳에서 즉시 숨은 질문이 튀어나온다: "배심원은 서로의 vote를 볼 수 있는가?", "게시자는 provisional removal 중 무엇을 할 수 있는가?"

3. Edge case 심문. plan 단계는 "정상 경로"를 설계하는 것이 아니라, 평소 떠올리지 않을 질문들을 일부러 묻는 것이다:

  • 투표 창 만료 시 0표면? (최종 선택: 기본 keep)
  • 항소 창 만료 시 0표면? (dismissed, stake 몰수)
  • judge가 자신이 신고한 post에 투표 가능한가? (불가, 동일 제외 규칙)
  • 여러 배심원이 동시에 finalize 조건을 trigger하면 race로 포인트 중복 지급이 되는가? (juror_points_awarded 멱등 필드 추가)

이 질문들의 95%는 코드를 쓰는 동안 자연스럽게 떠오르지 않는다—plan 단계가 하나씩 짚어내도록 강제한다.

4. 포인트 장부. 포인트 시스템은 논의만으로는 부족할 만큼 복잡해서, 실제로 표를 그려야 한다: 각 point 흐름에 대해 trigger 조건 + 금액 + 롤백 경로를 명기한다. 장부가 맞아떨어지면 모든 edge case(가처분 환급, 항소 환급, bonus)가 계산에 들어맞는다.

대화가 어디까지 진행되면 코드를 쓰기 시작해도 되는가

단단한 기준:

  • 각 경로를 막힘 없이 walk-through 할 수 있다—review를 여는 것부터 closed까지의 모든 조합(keep / remove / warn × 항소 유 / 무 × provisional / 비 provisional)을 끝까지 이야기로 풀 수 있다
  • Edge case마다 명확한 행동이 있다—"그건 이따가"가 아니라, "0표면 기본 keep", "provisional 중 항소 가능하나 먼저 finalize"
  • "어, 이건 아직 안 생각했어"가 없다—당신이 던질 수 있는 모든 질문에 답이 준비돼 있다

이 기준에 이르면, 코드 작성은 plan을 Ruby로 번역하는 일이 된다.

일기가성 실행 단계는 어떻게 보이는가

이 시스템의 첫 ship:

d162f1e Add community moderation system with jury/judge review and appeals
  63 files changed, 3032 insertions(+)
  119 specs, 0 failures

63개 파일, 3,032줄, 119 spec 전부 녹색. 한 번에 배포.

이후 열몇 개의 commit이 아키텍처의 견고함을 입증한다:

Apply legacy penalty (1pt) for posts created before 2026-03-26
Fix topic review appeal bugs: window mismatch and verdict not updated
Add topic_removed status for posts removed by community review
Default to keep verdict when review expires with zero votes
Reduce provisional removal penalty to 1pt during trial period
Allow topic creator to withdraw active reviews
Add handled tab to jury dashboard, fix tabs styling

모두 튜닝 / 수정 / 다듬기 / 작은 기능 추가. "가서 상태 머신을 재설계하자" 나 "포인트 모델을 바꾸자" 같은 건 하나도 없다. 골격은 그대로 다 맞았다.

이것이 plan의 진짜 보상이다—실행이 중단되지 않는다. "잠깐, 이 경로는 어떻게 처리하지?"가 없다—plan에 이미 쓰여 있다. "이 edge case를 생각 못 했어"가 없다—plan 단계에서 물었다. "이 인터페이스 다시 설계해야 해"가 없다—plan에서 이미 정리됐다.

몇 시간 연속으로 코드를 쓰면서 하는 일은 오직 하나: 명확한 설계를 키보드에 옮기는 것.

plan을 쓰지 말아야 할 때

모든 작업이 plan을 필요로 하지는 않는다:

  • 단순 버그 수정—탐지 + 수정 + regression test 추가, 사전 논의 불필요
  • 기계적 리팩터링—이름 변경, 함수 추출, 파일 이동—경로가 유일하므로 plan은 여분의 단계
  • 명백한 길이 하나뿐—GET endpoint 하나, UI toggle 하나 추가, 논의할 게 없다
  • 탐색적 프로토타입—당신 자신도 원하는 게 뭔지 모른다, 거친 버전을 돌리는 게 더 생각하는 것보다 빠르다

plan mode의 이익은 재작성 비용을 낮추는 것에서 온다. 작업에 재작성 위험이 없다면, plan은 그저 오버헤드다.

마무리

"하기 전에 생각하라"는 성격 조언처럼 들린다. plan mode는 그런 것이 아니다.

plan mode는 아키텍처급 대화가 10글자로 바꿀 수 있는 곳에서 일어나게 하는 것이다—30개 파일을 바꿔야 하는 곳이 아니라.

복잡성 하에서, 소프트웨어 업계의 오래된 격언 "words are cheap, code is expensive"를 뒤집어야 한다—"일단 코드부터 쓰고 보자"(이건 비용 차이가 작을 때만 성립)가 아니라, 그 비용 차이를 이용해서 값비싼 대화를 값싼 매체로 앞당기라는 것이다.

3,000줄을 한 번에 배포하는 감각은 아주 좋다. 내가 빨리 써서가 아니다—plan이 "쓰기"를 순수하게 기계적인 행동으로 바꾸었기 때문이다.