26장. API 개발 — 기존 Convention을 따르게 만들기
6부는 매일 하는 작업들이다.
첫 번째는 API 추가.
Agent가 가장 빠르게 해내는 작업이면서,
가장 티 나게 틀리는 작업이기도 하다.
문제는 만드는 것이 아니다
“주문 취소 API를 만들어줘” 라고 하면
Agent는 30초 만에 완성도 높은 코드를 낸다.
Controller, Service, DTO, 예외 처리까지 갖춰져 있다.
문제는 그 코드가 우리 프로젝트의 것처럼 보이지 않는다는 점이다.
우리 프로젝트 Agent가 만든 것
──────────────── ────────────────
ApiResponse<T> 래핑 바로 DTO 반환
BusinessException IllegalArgumentException
Facade에서 조합 Controller에서 Service 두 개 호출
@field:NotNull @NotNull
각각은 틀린 코드가 아니다.
다만 우리 것이 아니다.
API 개발에서 Agent에게 시킬 일은
새로 만드는 것이 아니라 닮게 만드는 것이다.
유사 API를 지목한다
가장 효과가 큰 지시 방법이다.
주문 취소 API를 추가해줘.
@src/main/kotlin/order/OrderConfirmController.kt 와
@src/main/kotlin/order/OrderConfirmFacade.kt 를 먼저 읽고
같은 구조로 만들어줘.
다른 점은 이것뿐이야:
- 취소는 이미 배송이 시작된 주문이면 400을 반환한다
이 지시가 하는 일은 셋이다.
- 컨벤션을 문서가 아니라 코드로 전달한다
- Agent가 탐색할 필요가 없다 (13장)
- 차이점만 판단하면 되니 실수가 줄어든다
⚠️ 이때 지목하는 파일은
가장 최근에 잘 만든 것이어야 한다.
레거시에는 세대가 섞여 있다.
오래된 것을 지목하면 오래된 패턴이 복제된다.
계층별 실패 패턴
Kotlin + Spring 프로젝트에서 반복되는 것들이다.
| 계층 | 자주 나는 사고 |
|---|---|
| Controller | 응답 래퍼 누락, 상태 코드 임의 선택 |
| Facade | 만들지 않고 Controller에서 Service 여러 개 호출 |
| Service | 다른 도메인 Service 직접 호출 |
| Repository | 필요 없는 조회 추가, 페이징 누락 |
| DTO | 엔티티를 그대로 반환 |
| Validation | 검증 위치가 계층마다 다름 |
| Exception | 새 예외 클래스 생성 |
🔥 이 중 두 개는 매번 나온다.
엔티티 직접 반환과 새 예외 생성이다.
둘 다 Agent 입장에서는 자연스러운 선택이다.
엔티티에 필요한 필드가 다 있고, 새 예외가 의미를 정확히 표현한다.
우리 사정을 모르면 그렇게 한다.
반복되는 것은 규칙으로
같은 지적을 두 번 했다면 CLAUDE.md 로 올린다.
15장의 기준이다.
## API Convention
- 응답은 `ApiResponse<T>` 로 감싼다. 엔티티를 직접 반환하지 않는다
- 예외는 `common/exception` 의 기존 것을 쓴다. 새로 만들지 않는다
- 도메인 간 조합은 Facade에서 한다. Controller는 Facade 하나만 호출한다
- 요청 검증은 DTO의 `@field:` 애노테이션으로 한다
- 상태 코드: 검증 실패 400, 권한 403, 없음 404, 도메인 규칙 위반 409
마지막 줄이 특히 값지다.
상태 코드 선택은 정답이 없어서
Agent가 매번 다르게 고른다.
우리 팀의 답을 한 줄로 적어두면 끝난다.
계약을 먼저 정한다
구현 전에 스펙을 확정하면 되돌릴 일이 줄어든다.
구현하기 전에 API 스펙만 먼저 제시해줘.
- 경로, 메서드, 요청/응답 JSON
- 실패 케이스별 상태 코드와 에러 코드
- 기존 API와 다른 점
동의하면 그때 구현하자.
21장의 Plan First를 API 작업에 맞춘 형태다.
스펙은 되돌리기 비용이 큰 결정이다.
클라이언트가 이미 붙었으면 바꿀 수 없다.
완료 조건
10장의 형식으로 쓰면 이 정도다.
## Acceptance Criteria
- 정상 취소 요청 200 + ApiResponse 래핑 확인 테스트
- 배송 시작된 주문 취소 시 409 + 에러 코드 `ORDER_ALREADY_SHIPPED`
- 존재하지 않는 주문 404
- 권한 없는 사용자 403
- `./gradlew test --tests '*OrderCancelApi*'` 통과
- `./gradlew ktlintCheck` 통과
⚠️ 실패 케이스를 완료 조건에 넣지 않으면
Agent는 happy path만 만들고 끝낸다.
30장에서 이 성향을 다시 다룬다.
API 문서와 클라이언트 영향
잊기 쉬운 두 가지다.
이 API 추가가 영향을 주는 곳을 확인해줘.
- OpenAPI 스펙 파일 갱신이 필요한가
- 기존 응답 스펙이 바뀌는가 (클라이언트 배포 필요 여부)
- 이 엔드포인트를 호출하는 내부 서비스가 있는가
세 번째가 모놀리스에서 특히 중요하다.
같은 레포 안에서 다른 모듈이
이 Service를 직접 호출하고 있을 수 있다.
38장에서 이 호출 관계가 경계 탐색의 재료가 된다.
이 장의 핵심
- Agent는 API를 잘 만든다 — 다만 우리 프로젝트의 것처럼 보이지 않는다
- 시킬 일은 새로 만드는 것이 아니라 기존 것과 닮게 만드는 것이다
- 유사 API를 지목하면 컨벤션이 문서가 아니라 코드로 전달된다
- 지목할 파일은 가장 최근에 잘 만든 것이어야 한다
- 엔티티 직접 반환과 새 예외 생성은 매번 나오는 두 가지다
- 상태 코드 선택 기준을 한 줄로 적어두면 매번 흔들리지 않는다
- 스펙은 되돌리기 비용이 크다 — 구현 전에 확정한다
- 실패 케이스를 완료 조건에 넣지 않으면 happy path만 만들어진다
- 모놀리스에서는 내부 호출자 확인이 빠지기 쉽다