병목 현상이 이동했습니다. 눈치채셨나요?
요약
AI 에이전트 도입으로 소프트웨어 개발의 병목 현상이 구현 속도에서 명세 품질(specification quality)로 이동하고 있음을 설명합니다. 에이전트가 코드를 빠르게 생성함에 따라, 개발자의 핵심 역량은 정밀한 요구사항을 정의하는 능력으로 변화하고 있습니다.
핵심 포인트
- 개발 병목 현상이 구현 속도에서 명세 품질로 이동함
- 에이전트 활용 시 모호한 아이디어는 환각이나 오류를 유발함
- 레벨 2 단계에서는 구현 능력이 아닌 명세 능력이 핵심임
- 정밀한(precisely) 설명 능력이 에이전트 제어의 핵심
레벨 5 엔지니어 (A Level 5 Engineer) — 제2호
서문
본론으로 들어가기 전에 한 가지 솔직하게 말씀드리고 싶습니다. 이 글에 언급된 프레임워크 중 그 어떤 것도 제 것이 아닙니다. 여기에 담긴 아이디어는 저보다 훨씬 더 깊고 오래 이 문제를 고민해 온 두 분으로부터 왔으며, 제가 한 마디 더 하기 전에 그분들에게 모든 공로를 돌려야 마땅합니다.
Dan Shapiro — Glowforge의 CEO이자 Wharton 연구원이며, 이 모든 대화에 어휘를 제공한 분입니다. 그의 블로그 포스트 “The Five Levels: from Spicy Autocomplete to the Dark Factory(5단계: 매콤한 자동 완성에서 다크 팩토리까지)”는 제가 하려는 모든 말의 개념적 중추입니다. 원문을 읽어보세요. 짧고 날카로우며, 가장 좋은 방식으로 당신을 불편하게 만들 것입니다. danshapiro.com
만약 제1호를 읽으셨다면, 여러분은 지도를 얻으셨을 것입니다. 6개의 단계, 대부분의 엔지니어가 결코 벗어나지 못하는 정체기(plateau), 그리고 소수의 팀이 조용히 프로덕션 환경에서 운영하고 있는 다크 팩토리(Dark Factory)에 대해서 말이죠. 만약 놓치셨다면, 먼저 읽어보시기 바랍니다. 이번 호는 그 내용 위에서 직접적으로 구축됩니다.
이번 호는 레벨 3에서 레벨 4로 이동하려고 할 때 발생하는 단 하나의 가장 중요한 변화에 관한 것입니다. 도구가 아닙니다. 사고방식(mindset)도 아닙니다. 바로 _병목 현상(bottleneck)_입니다.
왜냐하면 그것이 이동했기 때문입니다. 그리고 우리 대부분은 눈치채지 못했습니다.
속도가 더 이상 문제가 되지 않을 때
우리 경력의 대부분 동안, 소프트웨어 개발의 병목 현상은 구현 속도(implementation speed)였습니다. 아이디어가 있고, 설계가 있고, 티켓(ticket)이 있었습니다. 제약 사항은 손가락이 그것을 작동하는 코드(working code)로 얼마나 빨리 바꿀 수 있느냐였습니다. 그것이 우리가 최적화해 온 세상입니다. 그것이 우리가 속도(velocity)를 측정했던 이유입니다. 그것이 스탠드업(standups) 미팅이 존재하는 이유입니다. 그것이
레벨 2(Level 2)에 도달하면, 구현(implementation)은 거의 하룻밤 사이에 더 이상 제약 사항이 아니게 됩니다. 당신은 에이전트(agent)와 페어 프로그래밍(pairing)을 하고 있으며, 코드는 그저... 나타납니다. 며칠이 걸리던 기능들이 몇 시간 만에 완성됩니다. 몇 시간이 걸리던 일은 몇 분 만에 끝납니다. 마치 문제가 해결된 것처럼 느껴집니다.
하지만 당신은 문제를 해결한 것이 아닙니다. 그저 그 뒤에 숨어 있던 문제를 드러냈을 뿐입니다.
새로운 병목 현상(bottleneck)은 명세 품질(specification quality)입니다.
에이전트는 당신이 충분히 정밀하게 설명할 수 있는 것이라면 무엇이든 구축할 수 있습니다. 여기서 핵심 단어는 _정밀하게(precisely)_입니다. 당신이 모호하고 불완전한 아이디어 — 인간 개발자라면 합리적인 가정과 빠른 Slack 메시지로 채워 넣었을 법한 그런 아이디어 — 를 넘겨주려는 순간, 에이전트는 당신이 원하지 않는 그럴듯해 보이는 무언가를 환각(hallucinate)하거나, 동작을 멈추거나, 혹은 더 나쁜 경우, 끝까지 잘못된 것을 자신 있게 구축해 버립니다.
제약 사항은 더 이상 구현 능력(ability to implement)이 아닙니다. 그것은 명세 능력(ability to specify)입니다.
나쁜 명세(spec)는 실제로 어떤 모습인가
여기 불편한 진실이 있습니다. 엔지니어로서 우리가 작성하는 대부분의 "요구사항(requirements)"은 명세(specifications)가 아닙니다. 그것들은 Jira 티켓으로 꾸며진 느낌(vibes)일 뿐입니다.
"사용자 엔드포인트(users endpoint)에 페이지네이션(pagination)을 추가하세요." 이것은 명세가 아닙니다. 페이지당 결과는 몇 개인가요? 기본값은 설정 가능한가요? 페이지 번호가 전체를 초과하면 어떻게 되나요 — 빈 배열(empty array)인가요, 아니면 404인가요? 정렬 순서는 무엇인가요? 커서 기반(cursor-based)인가요, 아니면 오프셋 기반(offset-based)인가요? 아직 페이지 파라미터(page parameters)를 보내지 않는 기존 API 소비자들에게는 어떤 일이 발생하나요?
인간 개발자는 스탠드업(standup)에서 이러한 질문을 던지거나 문맥(context)을 통해 파악합니다. 레벨 4(Level 4)에서 자율적으로 작동하는 에이전트는 그렇게 할 수 없습니다. 에이전트는 선택을 내릴 것입니다 — 조용히, 자신 있게, 그리고 운영 환경(production)에 도달하기 전까지는 당신이 알아차리지 못할 방식으로 일관되게 틀린 선택을 말입니다.
이것이 바로 명세 품질에 대한 Dan Shapiro의 통찰이 단순한 생산성 팁이 아닌 이유입니다. 그것은 단계(ladder)를 올라가기 위한 전제 조건입니다. 레벨 2 수준의 명세로는 레벨 4에 도달할 수 없습니다. 시스템이 당신을 허용하지 않을 것입니다.
그래서 저는 하나를 만들었습니다. 그 결과는 다음과 같습니다.
저는 이번 이슈에서 단순히 이론을 제시하기보다 구체적인 무언가를 해보고 싶었습니다. 그래서 두 개의 외부 의존성(external dependencies)을 가진 이커머스 주문 관리 API라는 실제 세계와 유사한 시나리오를 선택했습니다. 그리고 WireMock으로 의존성을 시뮬레이션하고, 코드 작성 전에 Gherkin 시나리오를 작성하여 엔드 투 엔드(end-to-end)로 구축했습니다.
전체 프로젝트는 GitHub에 공개되어 있으므로, 이를 클론하여 여러분의 환경에서 정확히 동일한 설정을 실행해 볼 수 있습니다. 아래의 모든 내용은 재현 가능합니다.
시나리오
두 개의 외부 서비스와 통신하는 POST /orders 엔드포인트입니다:
- 결제 게이트웨이 (payment gateway) (Stripe를 생각하세요): 성공, 거절 또는 타임아웃(time out)이 발생할 수 있습니다.
- 재고 서비스 (inventory service): 재고 확인, 품절 보고 또는 부분적 가용성(partial availability) 보고를 수행할 수 있습니다.
공감할 수 있을 만큼 현실적이며, 오후 한나절 안에 끝낼 수 있을 만큼 범위가 적절합니다. 모든 백엔드 엔지니어가 다루는 종류의 통합 복잡성(integration complexity)을 담고 있습니다.
1단계 — 명세(spec)를 먼저 작성하세요. 정말로 먼저 작성해야 합니다.
여기에 제가 단 한 줄의 구현 코드도 작성하기 전에 작성한 다섯 가지 Gherkin 시나리오가 있습니다:
Feature: Order Creation
Scenario: Order is successfully created when payment succeeds and all items are in stock
...
이것들이 무엇인지 주목해 보세요. 구현 문서가 아닙니다. 의사 코드(pseudocode)도 아닙니다. 이것들은 **행위 계약 (behavioural contract)**입니다. 즉, 특정 상황에서 시스템이 정확히 무엇을 해야 하는지를 평이한 언어로 기술한 것이며, 어떤 팀원, PM, 혹은 에이전트(agent)라도 읽을 수 있는 형식으로 작성되었습니다.
이것들을 먼저 작성하는 규율은 제가 평소라면 미루었을 결정들을 내리도록 강제했습니다:
- 결제 전에 재고를 확인할 것인가, 아니면 결제를 먼저 할 것인가? (재고 먼저. 네 번째 시나리오에서 이를 확정합니다.)
- 부분적 가용성 상황에서는 어떻게 되는가 — 가능한 것만 자동으로 이행할 것인가, 아니면 사용자에게 물어볼 것인가? (물어보기. 시나리오 4에 인코딩되어 있습니다.)
- 결제 게이트웨이의 타임아웃 SLA는 얼마인가? (5초, 최대 2회 재시도. 시나리오 5를 통해 테스트 가능하게 만듭니다.)
- 부분적 가용성에 대한 응답 코드는 무엇인가? (207 Multi-Status.)
이러한 시나리오들이 없다면, 그 모든 결정은 코드를 처음 작성한 사람이 아무런 알림 없이 내리게 되었을 것입니다.
2단계 — WireMock으로 의존성 스터빙 (Stub) 하기
실제로 실행 가능한 테스트를 작성하기 전에, 외부 서비스들을 시뮬레이션해야 합니다. 이것이 Dan Shapiro가 말하는 디지털 트윈 유니버스 (digital twin universe) 입니다. 즉, 실제 서비스의 예측 불가능성, 비용, 또는 속도 제한 (rate limits) 없이 실제와 동일하게 동작하도록 완전히 시뮬레이션된 의존성 버전을 의미합니다.
WireMock은 이를 위한 업계 표준입니다. WireMock 스터브 (stub)는 서비스가 어떻게 응답해야 하는지를 기술하는 단순한 JSON 파일입니다:
{
"request": {
"method": "POST",
...
결제 타임아웃 (payment timeout) 시나리오의 경우, WireMock에는 fixedDelayMilliseconds라는 내장 파라미터가 있습니다. 단 한 줄이면 모의 객체 (mock)가 응답하는 데 6초가 걸리도록 설정할 수 있습니다:
{
"request": {
"method": "POST",
...
이 작은 설정 한 줄이 시나리오 5를 테스트 가능하게 만듭니다. 이것이 없다면, OS 레벨에서 네트워크 연결을 끊지 않고는 로컬 환경에서 타임아웃 동작을 테스트할 수 없습니다. 저도 과거에 그런 방식을 시도해 본 적이 있는데, 들리는 것만큼이나 정말 고통스러운 작업입니다.
3단계 — 시나리오를 실제 어설션 (Assertion)에 연결하기
Gherkin 자체는 그저 텍스트일 뿐입니다. 이를 실행 가능한 테스트 스위트 (test suite)로 바꾸기 위해 저는 pytest-bdd를 사용했습니다. 이를 통해 각 Given/When/Then 라인을 Python 함수에 매핑할 수 있습니다:
@given("the payment gateway will decline the charge", target_fixture="payment_scenario")
def pay_declined():
return "declined"
...
마지막 어설션인 the payment gateway is never called (결제 게이트웨이가 호출되지 않음)는 전통적인 단위 테스트 (unit test)로는 검증이 거의 불가능에 가깝지만, WireMock을 사용하면 매우 간단합니다 (trivial). WireMock은 수신하는 모든 호출을 기록합니다. 여러분은 그 로그를 대상으로 직접 어설션을 수행하면 됩니다.
4단계 — 스위트 실행하기
$ pytest tests/steps/test_order_creation.py -v
============================= test session starts ==============================
collected 5 items
...
5개 중 5개 모두 통과했습니다. 하지만 그 과정은 매우 교육적이었습니다.
진행 과정에서 발생한 문제들
실패한 부분들에 대해 솔직하게 말씀드리고 싶습니다. 왜냐하면 바로 그 지점에서 실제 학습이 일어났기 때문입니다. 다섯 가지 테스트는 첫 번째 실행에서 통과하지 못했습니다. 두 번째 실행에서도 통과하지 못했습니다.
실패 1 — "no stub matched"로 인한 침묵의 성공
WireMock 스텁 (stub) 중 어떤 것도 처리할 방법을 모르는 요청이 들어오면, 기본 동작은 404를 반환하는 것입니다. 제 API 코드는 다음과 같았습니다:
try:
pay = httpx.post(f"{PAYMENT_URL}/payments/charge/{scenario}", ...)
payment_result = pay.json()
...
httpx에서 404는 예외 (exception)가 아닙니다. 그것은 단지 응답 (response)일 뿐입니다. 따라서 API는 기꺼이 pay.json()을 호출하여 {"error": "No stub matched"}를 받고, 전체 상호작용을 성공으로 취급했습니다. 즉, 실제 결제가 처리되지 않았음에도 불구하고 주문 ID를 발행하고 주문을 확정해 버린 것입니다.
이것은 정말로 위험합니다. 잘못 설정된 모의 객체 (mock)는 실제 서비스 경로가 깨졌다는 사실을 숨긴 채 모든 테스트를 통과하게 만들었을 것입니다. 교훈: 모의 객체 (mock)로부터 오는 응답 상태 (response status)를 항상 명시적으로 확인하십시오. 해결책:
try:
pay = httpx.post(f"{PAYMENT_URL}/payments/charge/{scenario}", ...)
if pay.status_code == 404:
...
실패 2 — 공유된 호출 로그 (call log) 버그
처음에는 단일 클래스 수준의 호출 로그 (call log)를 보유한 하나의 MockServer 클래스로 시작했습니다. 결제와 재고 모의 객체 (mock) 모두 동일한 리스트에 기록을 남겼습니다. 테스트가 "결제 게이트웨이가 정확히 하나의 결제 요청을 받았다"라고 단언 (assert)할 때, 실패 1의 영향으로 결제 호출은 로그에 없었지만 재고 호출은 로그에 있었고, 단언 (assertion)은 이 결합된 로그를 확인하고 있었습니다.
해결책은 개념적으로는 작았지만 아키텍처 측면에서는 중요했습니다. 각 모의 서버 (mock server) 인스턴스가 자신만의 호출 로그 (call log)를 갖도록 하는 것이었습니다:
def start_mock_server(port: int, mappings_dir: str) -> tuple[HTTPServer, MockCallLog]:
stubs = [json.loads(f.read_text()) for f in Path(mappings_dir).glob("*.json")]
log = MockCallLog() # ← 인스턴스별 로그
...
이는 실제 운영 환경에서 WireMock이 작동하는 방식과 유사합니다. 서비스당 별도의 WireMock 인스턴스를 실행하며, 각 인스턴스는 자신만의 요청 로그 (request log)를 가집니다. 발생한 버그는 이러한 절차를 생략한 데서 비롯된 직접적인 결과였습니다.
실패 3 — 피스처 와이어링 (fixture wiring)의 간극
시나리오 3과 4는 Given 절에서 결제 시나리오를 정의하지 않는데, 이는 해당 케이스에서 결제 게이트웨이 (payment gateway)가 호출되어서는 안 되기 때문입니다. 하지만 pytest-bdd는 여전히 payment_scenario 피스처 (fixture)를 기대하고 있었고, 테스트가 실행되기도 전에 에러를 발생시켰습니다.
이는 명시할 가치가 있는 미묘한 차이입니다. Gherkin 명세 (spec)는 올바랐습니다. 명세는 있어야 할 내용을 정확히 말하고 있었습니다. 에러는 명세를 어설션 (assertion)과 연결하는 테스트 와이어링 (test wiring) 에 있었습니다. 해결책은 기본 피스처 (default fixture)를 사용하는 것이었습니다:
@pytest.fixture
def payment_scenario():
"""기본값 — 특정 Given 단계에 의해 오버라이드됨."""
...
이렇게 하면 명세는 깔끔하게 유지됩니다. 와이어링 (wiring) 단계에서 특정 시나리오가 특정 설정에 신경 쓰지 않는 경우를 처리하게 됩니다.
이 연습이 실제로 나에게 증명해 준 것
이제 추상적인 개념이 아닌, 몸으로 느껴지는 몇 가지 사실들이 있습니다:
빌드 과정에서 AI가 볼 수 없는 명세 (specs)는 독보적으로 강력합니다. 나의 시나리오들은 tests/features/order_creation.feature에 존재합니다. 구현 (implementation)은 app/main.py에 있습니다. 에이전트 (agent)에게 API를 수정하라고 요청할 때, 나는 구현부만 제공할 수 있었습니다. 명세는 외부에 남아 있었습니다. 에이전트는 어설션 (assertion) 자체로부터 역공학 (reverse-engineer)할 수 없는 동작에 대해 테스트를 통과시켜야만 했습니다. 이것이 바로 레벨 4 (Level 4)에서 진정으로 상황을 변화시키는 부분입니다.
WireMock의 '매칭되지 않을 시 404 반환'은 버그가 아니라 기능입니다. 이는 그렇지 않았다면 영원히 숨겨져 있었을 통합 (integration) 실수를 드러내 줍니다. 404 패스스루 (passthrough) 때문에 테스트가 조용히 성공하는 것을 처음 보았을 때는 짜증이 났습니다. 하지만 이제는 더 명확하게(louder) 알려줘야 한다고 생각합니다.
시나리오를 먼저 작성하는 것이 제가 구축하는 것을 바꾸어 놓았습니다. 시나리오 4 — 부분적 가용성 (partial availability) — 는 만약 제가 코드를 먼저 작성했다면 존재하지 않았을 것입니다. 저는 "모두 사용 가능하거나 아니면 실패(all available or fail)"하는 방식을 구현하고 배포했을 것입니다. 명세(spec)를 먼저 작성한 덕분에 저는 그 질문에 직면할 수 있었습니다. 그리고 그 답은 시스템의 일부가 되었습니다.
직접 시도해 보세요
위의 모든 내용은 클론(clone)하고, 실행하고, 망가뜨려 볼 수 있는 프로젝트에 포함되어 있습니다. 5개의 시나리오, 2개의 모의 서비스 (mock services), 1개의 API로 구성됩니다. Python과 pip가 설치되어 있다면 총 설정 시간은 15분 미만입니다.
git clone <repo-url> order-api
cd order-api
pip install fastapi uvicorn httpx pytest pytest-bdd requests
...
Python 기반의 모의 객체 (mock) 대신 실제 WireMock을 사용하고 싶다면:
# WireMock standalone 다운로드
curl -L -o wiremock.jar \
https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/3.3.1/wiremock-standalone-3.3.1.jar
...
제가 작성한 WireMock 매핑 JSON 파일들은 변경 사항 없이 실제 WireMock에서 그대로 작동합니다. 이는 의도된 것입니다. Python 모의 객체는 빠르게 시작하기 위한 용도입니다. 실제 WireMock은 이 패턴을 실제 서비스 메쉬 (service mesh) 전체로 확장하고자 할 때 사용하기 위한 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기