개발을 너무 일찍 시작할 때 발생하는 숨겨진 비용
요약
제품의 동작 정의가 완료되기 전 성급하게 개발을 시작할 때 발생하는 기술적 부채와 비용을 경고합니다. 구현 단계에서 발생하는 모호함이 시스템의 복잡성을 높이고 예상치 못한 오류를 초래함을 설명합니다.
핵심 포인트
- 동작 정의 없는 구현은 모호함을 코드에 저장하는 행위임
- 단순한 API 엔드포인트라도 멱등성, 동기/비동기 여부 등의 결정이 선행되어야 함
- 기획의 공백을 구현상의 가정으로 대체하면 시스템 호환성 문제가 발생함
- 코드 작성 전 최소한의 공유된 정밀도(Shared Precision) 확보가 필수적임
월요일에는 기능 하나가 진행 중 (In Progress) 상태로 이동합니다.
화요일이 되면 저장소(repository)에는 새로운 모듈, 데이터베이스 마이그레이션(database migration), 그리고 일련의 엔드포인트(endpoints)가 생겨납니다. 수요일에는 첫 번째 풀 리퀘스트(pull request)가 열립니다. 금요일이 되면 해피 패스(happy path)가 데모에서 정상적으로 작동합니다.
업무는 건강해 보입니다. 보드(board)는 움직이고 있습니다. 코드는 쌓여가고 있습니다.
하지만 팀은 요청이 반복될 때, 의존성(dependency)이 타임아웃(timeout)될 때, 이벤트(event)가 순서대로 도착하지 않을 때, 또는 사용자가 작업 중간에 상태를 변경할 때 어떤 일이 일어나야 하는지에 대해 아직 합의하지 못했습니다.
이것은 작은 기획의 공백이 아닙니다. 제품의 동작(behaviour)이 정의되기 전에 구현(implementation)이 시작되었음을 의미합니다.
초기 코딩은 커밋(commits), 화면(screens), 엔드포인트(endpoints), 통과하는 테스트(passing tests)와 같이 눈에 보이는 결과물(artifacts)을 만들어내기 때문에 종종 진척이 있다는 착각을 불러일으킵니다. 동시에, 해결되지 않은 결정 사항들을 구현상의 가정(implementation assumptions)으로 조용히 전환시킵니다.
그리고 코드는 모호함(ambiguity)을 저장하기에 가장 비용이 많이 드는 장소 중 하나입니다.
티켓이 상세하더라도 결정 준비가 되지 않았을 수 있습니다
많은 티켓이 기능의 동작(behaviour)을 정의하지 않은 채 기능의 _형태(shape)_만을 설명합니다.
겉보기에 단순해 보이는 작업을 생각해 봅시다:
POST /subscriptions/{id}/activate
202 Accepted
이 계약(contract)은 구현 가능한 것처럼 보입니다. 하지만 아직은 아닙.
해당 엔드포인트는 실제 시스템을 결정짓는 다음과 같은 질문들에 대해 아무것도 말해주지 않습니다:
- 활성화(activation)는 동기(synchronous) 방식인가, 비동기(asynchronous) 방식인가, 아니면 최종적 일관성(eventually consistent)을 따르는가?
- 해당 작업은 멱등성(idempotent)을 갖는가? 클라이언트가 타임아웃 후 재시도하면 어떻게 되는가?
- 결제가 대기 중인 동안 구독을 취소할 수 있는가?
- 만약
payment.succeeded가 취소 후에 도착한다면, 어떤 상태 전이(transition)가 우선하는가? - 이벤트(events)는 최대 한 번(at most once), 최소 한 번(at least once) 전달되는가, 아니면 순서 보장(ordering guarantee) 없이 전달되는가?
- 어떤 서비스가 권위 있는 상태(authoritative state)를 소유하는가?
- 부분적 실패(partial failure) 동안 호출자(caller)는 무엇을 관찰하게 되는가?
- 기능이 수락되었음을 증명하는 근거는 무엇인가?
경험 많은 두 명의 엔지니어가 동일한 티켓을 올바르게 구현하더라도, 각자가 그 빈칸을 서로 다르게 채웠기 때문에 서로 호환되지 않는 시스템을 구축할 수 있습니다.
문제는 코딩 능력이 아닙니다. 문제는 제품에 대한 결정(product decision)이 구현 세부 사항(implementation detail)인 것처럼 위장하도록 방치되었다는 점입니다.
프로덕션 구현 전 존재해야 할 네 가지 합의 사항
팀이 코드를 작성하기 전에 백 페이지 분량의 명세서가 필요한 것은 아닙니다. 하지만 각 엔지니어가 서로 다른 제품을 만들어내는 것을 방지할 수 있을 만큼 충분한 공유된 정밀도(shared precision)는 필요합니다.
최소한 다음 네 가지 사항은 합의되어야 합니다.
1. 동작 (Behaviour)
동작(Behaviour)은 사용자 또는 호출자(caller)의 관점에서 시스템이 무엇을 하는지를 정의합니다.
상태 유지(stateful) 기능의 경우, 산문(prose)만으로는 충분한 경우가 드뭅니다. 상태 전이(transitions)를 명시적으로 만드세요:
draft -> pending_payment -> active
| |
v v
...
그런 다음 다이어그램에 의문을 제기하십시오:
- 각 상태에서 유효한 명령(commands)은 무엇인가?
- 어떤 전이(transitions)가 가역적인가(reversible)?
- 중복되거나, 지연되거나, 순서가 잘못된 이벤트(out-of-order events)가 발생하면 어떻게 되는가?
- 어떤 결과가 사용자에게 보이는가?
만약 팀이 이러한 질문에 일관되게 답할 수 없다면, 그 동작(behaviour)은 아직 코드로 인코딩할 준비가 되지 않은 것입니다.
2. 제약 조건 (Constraints)
제약 조건(Constraints)은 구현이 반드시 유지해야 하는 불변량(invariants)과 운영 한계치입니다.
여기에는 플랜 제한, 권한 경계, 법적 제한과 같은 비즈니스 규칙뿐만 아니라, 지연 시간 예산(latency budgets), 데이터 거주성(data residency), 트랜잭션 경계(transaction boundaries), 가용성 목표(availability targets), 호환성 요구 사항과 같은 시스템 속성도 포함됩니다.
기술적으로 올바른 구현이라 할지라도, 너무 늦게 드러난 제약 조건을 위반한다면 여전히 사용할 수 없는 상태가 될 수 있습니다. 개발이 끝난 후에야 특정 작업이 두 저장소(stores) 간에 원자적(atomic)이어야 한다거나, 200밀리초 이내에 완료되어야 한다거나, 지역 간 데이터 전송 없이 실행되어야 한다는 사실을 발견하는 것은 개선(refinement)이 아닙니다. 그것은 재설계(redesign)입니다.
3. 통합 계약 (Integration contracts)
통합 계약(Integration contract)은 요청(request) 및 응답 스키마(response schema) 그 이상을 의미합니다.
이는 소유권(ownership), 버전 관리(versioning), 실패 의미론(failure semantics), 타임아웃 동작(timeout behaviour), 재시도 정책(retry policy), 멱등성(idempotency), 순서 보장(ordering guarantees), 그리고 호환성 기대치(compatibility expectations)를 정의해야 합니다. 이벤트 기반 시스템(event-driven systems)의 경우, 누가 이벤트를 발행할 수 있는지, 무엇이 이벤트를 고유하게 만드는지, 컨슈머(consumer)가 어떻게 복구하는지, 그리고 스키마 진화(schema evolution)가 어떻게 처리되는지에 대해서도 답해야 합니다.
정상 경로(happy-path)의 페이로드(payload)는 보통 통합에서 가장 비용이 적게 드는 부분입니다. 비용이 많이 드는 부분은 네트워크가 네트워크답게 동작할 때 발생하는 모든 상황입니다.
4. 수락 기준 (Acceptance criteria)
수락 기준(Acceptance criteria)은 성공을 관찰 가능하고(observable) 반증 가능하게(falsifiable) 만들어야 합니다.
"사용자가 구독을 활성화할 수 있다"는 요약일 뿐, 테스트 가능한 기준이 아닙니다. 더 강력한 기준은 초기 상태, 동작, 예상 결과, 그리고 중요한 실패 사례를 기술합니다.
예를 들어:
Given a subscription is pending payment
And the user has cancelled it
When a delayed payment-success event is received
...
이 단일 시나리오는 제품 동작, 도메인 상태 머신(domain state machine), 통합 의미론(integration semantics), 멱등성(idempotency), 그리고 관찰 가능성(observability)을 일치시킵니다. 이는 추측에 기반한 구현을 하루 더 하는 것보다 훨씬 더 많은 불확실성을 제거해 줍니다.
조기 코딩이 더 빨라 보이는 이유
구현(Implementation)은 심리적인 이점이 있습니다. 바로 구체적이라는 점입니다.
팀은 풀 리퀘스트(pull requests), 스토리 포인트(story points), 엔드포인트(endpoints), 컴포넌트(components), 그리고 통과된 테스트의 개수를 셀 수 있습니다. 합의를 도출하는 작업은 배포를 지연시킬 가능성이 가장 높은 불확실성을 제거함에도 불구하고, 눈에 보이는 결과물(artifacts)은 더 적게 만들어냅니다.
이 때문에 활동(activity)을 진척(progress)으로 혼동하기 쉽습니다.
또한 조기 구현은 엔지니어들이 답변되지 않은 모든 질문에 대해 편리한 기본값(defaults)을 선택하기 때문에 빠르게 진행되는 경향이 있습니다. 첫 번째 버전은 미뤄두었던 결정 사항들을 아직 마주하지 않았기 때문에 정확히 효율적으로 보일 수 있습니다.
테스트(Tests)는 이러한 환상을 강화할 수 있습니다. 테스트 스위트(test suite)는 코드가 작성자의 의도대로 동작한다는 것을 증명합니다. 하지만 그것이 그 기대치가 공유되었는지, 완전한지, 혹은 올바른지를 증명하지는 않습니다. 만약 팀이 동작 방식에 대해 합의한 적이 없다면, 통과된 테스트(green tests)는 단지 잘못된 가정을 더 엄격하게 보존할 뿐일 수도 있습니다.
브랜치(branch)는 움직이지만, 결정 그래프(decision graph)는 움직이지 않습니다.
재작업(Rework)은 파일이 아닌 여러 표면(surfaces)을 통해 확산됩니다
해결되지 않은 결정의 비용은 단순히 함수 하나를 다시 쓰는 것에 국한되지 않습니다.
유용한 엔지니어링 휴리스틱(heuristic)은 다음과 같습니다:
예상 재작업(Expected rework) ~= 해결되지 않은 결정(unresolved decisions)
x 영향을 받는 표면(affected surfaces)
x 되돌리는 비용(cost of reversal)
팀이 처음에 구독 활성화(subscription activation)를 동기적(synchronous)으로 처리하다가, 나중에 이것이 비동기적(asynchronous)이어야 하며 중복 전달에 탄력적(resilient)이어야 한다는 데 합의했다고 가정해 봅시다. 그 결정은 다음과 같은 것들을 변화시킬 수 있습니다:
- 도메인 상태 모델(domain state model)
- 데이터베이스 스키마(database schema) 및 기존 레코드
- 공개 API(public API) 응답
- 이벤트 스키마(event schemas) 및 컨슈머 로직(consumer logic)
- 클라이언트 측 로딩 및 에러 상태(error states)
- 재시도(retry) 및 멱등성(idempotency) 메커니즘
- 테스트 및 테스트 데이터
- 메트릭(metrics), 알림(alerts) 및 운영 런북(operational runbooks)
- 분석 정의(analytics definitions) 및 지원 문서
이 시점에서 이 작업을 단순히 "리팩터링(refactor)"이라고 부르는 것은 과소평가하는 것입니다. 팀은 이미 시스템 전체로 전파된 분산된 결정(distributed decision)을 되돌리고 있는 것입니다.
이것이 바로 시작 단계의 작은 모호함이 나중에 커다란 인도 비용(delivery cost)이 될 수 있는 이유입니다. 모호함 자체는 일정하게 유지되었지만, 그 폭발 반경(blast radius)이 커진 것입니다.
정답은 무거운 명세(heavyweight specification)가 아닙니다
조기 구현(premature implementation)의 대안은 몇 달간의 분석이나 완벽한 확실성에 대한 요구가 아닙니다.
목표는 **결정 준비가 된 슬라이스(decision-ready slice)**입니다. 즉, 코드베이스 내부에서 제품의 동작을 임의로 만들어내지 않고도 엔지니어링을 시작할 수 있게 해주는 최소한의 산출물 세트입니다.
많은 기능의 경우, 이는 가볍게 진행될 수 있습니다:
- 주요 경로(main paths)와 실패 경로(failure paths)를 다루는 몇 가지 구체적인 예시;
- 상태 유지 동작(stateful behaviour)을 위한 상태 전이 스케치(state transition sketch);
- 명시적인 불변량(invariants) 및 비기능적 제약 조건(non-functional constraints);
- 버전이 관리되는 요청(request), 응답(response) 또는 이벤트 스키마(event schemas);
- 문서화된 타임아웃(timeout), 재시도(retry) 및 멱등성(idempotency) 의미론(semantics);
- 테스트 가능한 수용 기준(acceptance criteria);
- 각각의 담당자(owner)가 지정된, 남아있는 미지의 요소(unknowns)에 대한 짧은 목록.
이러한 산출물들은 별도의 기능에 의해 생성되어 인수인계 경계(handoff boundaries) 너머로 던져지는 것이 아니라, 함께 검토되어야 합니다. 제품(Product), 디자인(Design), 엔지니어링(Engineering) 및 의존 관계에 있는 팀들은 각자의 관점에서 동일한 동작을 확인해야 합니다.
목표는 학습을 없애는 것이 아닙니다. 변경 비용이 배가되기 전에 어떤 불확실성이 수용 가능한지를 결정하는 것입니다.
탐색 코드(Exploration code)와 확정 코드(Commitment code)는 다릅니다
때로는 일찍 코딩을 시작하는 것이 정확히 옳은 선택일 수 있습니다.
시간 제한이 있는 스파이크(spike)를 통해 특정 라이브러리가 요구되는 프로토콜을 지원하는지, 쿼리가 지연 시간 예산(latency budget)을 충족할 수 있는지, 또는 외부 API가 문서화된 대로 동작하는지 테스트할 수 있습니다. 프로토타입(prototype)은 회의보다 더 빠르게 사용성 문제를 드러낼 수 있습니다.
하지만 탐색 코드(exploratory code)에는 그 자체의 명시적인 계약(contract)이 필요합니다:
- 어떤 질문에 답하고 있는가?
- 탐색은 얼마나 오래 지속될 것인가?
- 어떤 증거를 생성할 것인가?
- 이 코드는 폐기 가능한(disposable) 것인가?
- 이것이 프로덕션 구현(production implementation)이 되기 전에 어떤 결정이 내려져야 하는가?
이러한 경계가 없다면, 프로토타입은 토대(foundations)가 되어버리는 습성이 있습니다. 일시적인 가정들이 사용자, 데이터 및 의존성을 확보하게 됩니다. 그러면 팀은 의식적으로 내린 적이 없는 결정들을 보존해야만 합니다.
탐색 코드는 일찍 시작할 수 있습니다. 확정 코드(commitment code)는 팀이 무엇을 확정하고 있는지 알 때까지 기다려야 합니다.
실질적인 개발 시작 게이트(start-development gate)
기능이 프로덕션 구현 단계로 진입하기 전에, 저는 팀이 다음 질문들에 답할 수 있기를 기대합니다:
- 사용자에게 보이는 결과물을 동일한 언어로 설명할 수 있는가?
- 재시도(retries) 및 순서가 바뀐 이벤트(out-of-order events)를 포함하여, 중요한 상태(states)와 전이(transitions)가 명시적인가?
- 비즈니스 및 기술적 불변량(invariants)이 문서화되었는가?
- 통합 계약(integration contracts)이 페이로드(payloads)뿐만 아니라 실패 의미론(failure semantics)을 정의하고 있는가?
- 수락 기준(acceptance criteria)이 테스트가 될 수 있을 만큼 객관적이며, 프로덕션에서 운영될 수 있을 만큼 관찰 가능한가(observable)?
- 남아 있는 미지의 요소(unknowns)들이 명명되고, 담당자가 지정되었으며, 의도적으로 수용되었는가?
만약 답변이 '아니오'라면, 다음 단계가 반드시 "중단"이어야 하는 것은 아닙니다. 짧은 동작 워크숍(behaviour workshop), 계약 검토(contract review), 또는 타겟팅된 기술적 스파이크(technical spike)가 될 수도 있습니다.
하지만 그것은 이미 결정이 내려진 것처럼 가장하는 프로덕션 코드여서는 안 됩니다.
더 빨리 출시하는 것은 더 빨리 결정하는 것에서 시작된다
개발을 일찍 시작하는 것은 서류상으로는 일정을 개선할 수 있지만, 실제 인도 날짜(delivery date)를 예측 불가능하게 만들 수 있습니다. 첫 주에는 속도가 빨라 보이지만, 이는 팀이 나중의 주에서 시간을 빌려 쓰고 있기 때문입니다. 나중에는 가정이 충돌하고, 인터페이스가 변경되며, 데이터를 복구해야 하고, 테스트를 다시 작성해야 하는 상황이 닥칩니다.
고성과 팀은 의례적인 절차를 위해 구현을 미루지 않습니다. 대신 관련 결정이 명시적일 때까지 되돌릴 수 없는 약속(irreversible commitments)을 미룹니다.
두 팀이 말하는 "활성화됨(activated)"의 의미가 서로 다르다는 것을 발견하기에 가장 좋은 시점은, 두 정의가 모두 프로덕션에서 실행되기 전입니다.
의도와 구현 사이의 간극—동작, 제약 조건, 계약, 그리고 증거가 일치하거나 이탈하는 지점—이 바로 제가 Lopeline을 통해 탐구하고 있는 문제 공간입니다. 이에 대해 곧 더 자세히 공유하겠습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기