Claude Code로 Go API 스캐폴딩: 하나의 깔끔한 핸들러-서비스-오류 패턴을 정의하고, Go API 전반에 걸쳐 일관된 새 엔드포인트를 생성하세요.

Go API는 보통 깔끔하게 시작합니다: 엔드포인트가 몇 개 있고, 담당자가 한두 명이며, 구현이 모두의 머릿속에 있습니다. 하지만 API가 성장하고, 기능이 압박 속에 배포되면 작은 차이들이 스며듭니다. 각각은 무해해 보이지만 합쳐지면 이후 변경을 느리게 만듭니다.
흔한 예시: 한 핸들러는 JSON을 구조체로 디코딩하고 친절한 메시지로 400을 반환하고, 다른 핸들러는 다른 형태로 422를 반환하며, 또 다른 핸들러는 오류를 다른 형식으로 로깅합니다. 이들 중 어느 것도 컴파일을 깨지 않습니다. 다만 새 엔드포인트를 추가할 때마다 계속된 의사결정과 작은 수정이 필요하게 됩니다.
문제는 다음과 같은 곳에서 느껴집니다:
CreateUser, AddUser, RegisterUser)로 검색이 어려워짐여기서 "스캐폴딩"은 새 작업에 반복적으로 적용되는 템플릿을 의미합니다: 코드가 어디에 있어야 하는지, 각 레이어가 무엇을 담당하는지, 응답이 어떻게 생겨야 하는지. 많은 코드를 생성하는 것보다 일관된 형태를 고정하는 것이 목적입니다.
Claude 같은 도구는 새 엔드포인트를 빠르게 스캐폴드하는 데 도움이 되지만, 패턴을 규칙으로 다룰 때만 유용합니다. 규칙을 정의하고, 모든 diff를 리뷰하고, 테스트를 실행하세요. 모델은 표준 부분을 채워줄 뿐, 아키텍처를 재정의하게 해서는 안 됩니다.
모든 요청이 동일한 경로를 따를 때 Go API는 확장하기 쉽습니다. 엔드포인트를 생성하기 전에 하나의 레이어 분할을 선택하고 지키세요.
핸들러의 역할은 HTTP에 국한됩니다: 요청을 읽고, 서비스를 호출하고, 응답을 작성합니다. 비즈니스 규칙, SQL, 또는 "이 경우만 특별히" 같은 로직을 포함해서는 안 됩니다.
서비스는 유즈케이스를 담당합니다: 비즈니스 규칙, 결정, 여러 리포지토리나 외부 호출 간의 오케스트레이션을 담당합니다. 상태 코드, 헤더, 오류 렌더링 방식 같은 HTTP 문제를 알면 안 됩니다.
데이터 접근(리포지토리/스토어)은 영속성 세부사항을 담당합니다. 서비스 의도를 SQL/쿼리/트랜잭션으로 번역합니다. 기본적인 데이터 무결성 이상으로 비즈니스 규칙을 강제하거나 API 응답을 형성해서는 안 됩니다.
실무적인 분리 체크리스트:
한 가지 규칙을 선택하고 굽히지 마세요.
간단한 접근법:
예시: 핸들러는 email이 존재하고 이메일처럼 보이는지 확인합니다. 서비스는 그 이메일이 허용되는지, 이미 사용 중인지 확인합니다.
서비스가 도메인 타입을 반환할지 DTO를 반환할지 일찍 결정하세요.
깔끔한 기본은: 핸들러는 요청/응답 DTO를 사용하고, 서비스는 도메인 타입을 사용하며, 핸들러가 도메인을 응답으로 매핑합니다. 이렇게 하면 HTTP 계약이 바뀌어도 서비스는 안정적으로 유지됩니다.
매핑이 부담스럽다면, 그래도 일관되게 유지하세요: 서비스는 도메인 타입과 타입화된 오류를 반환하고 JSON 형태화는 핸들러가 담당합니다.
생성된 엔드포인트가 같은 사람이 작성한 것처럼 느껴지게 하려면 오류 응답을 초기에 고정하세요. 생성은 출력 형식이 비협상적일 때 가장 잘 작동합니다: 하나의 JSON 형태, 하나의 상태 코드 맵, 그리고 노출되는 항목에 대한 규칙을 고정하세요.
모든 실패에 대해 모든 엔드포인트가 반환하는 단일 오류 봉투로 시작하세요. 작고 예측 가능하게 유지합니다:
{
"code": "validation_failed",
"message": "One or more fields are invalid.",
"details": {
"fields": {
"email": "must be a valid email address",
"age": "must be greater than 0"
}
},
"request_id": "req_01HR..."
}
code는 기계용(안정적, 예측 가능), message는 사람용(짧고 안전)으로 사용하세요. 구조화된 추가 정보는 details에 넣습니다. 검증의 경우 details.fields 맵은 생성하기 쉽고 클라이언트가 입력 옆에 표시하기에도 편합니다.
다음으로 상태 코드 맵을 적고 고수하세요. 엔드포인트마다 논쟁이 적을수록 좋습니다. 400과 422를 둘 다 사용하려면 분기를 명시하세요:
bad_json -> 400 Bad Request (잘못된 JSON)validation_failed -> 422 Unprocessable Content (형식은 맞지만 필드가 유효하지 않음)not_found -> 404 Not Foundconflict -> 409 Conflict (중복 키, 버전 불일치)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server Error무엇을 로그에 남기고 무엇을 반환할지 결정하세요. 좋은 규칙: 클라이언트에는 안전한 메시지와 request ID를 주고, 로그에는 전체 오류와 내부 컨텍스트(SQL, 업스트림 페이로드, 사용자 ID)를 남깁니다. 절대 유출해서는 안 됩니다.
마지막으로 request_id를 표준화하세요. 들어오는 ID 헤더가 있으면 수락(예: API 게이트웨이에서 전달), 없으면 엣지(미들웨어)에서 생성하세요. 이를 컨텍스트에 붙여 로그에 포함하고 모든 오류 응답에 반환합니다.
스캐폴딩이 일관되게 유지되려면 폴더 구조가 지루하고 반복 가능해야 합니다. 생성기는 볼 수 있는 패턴을 따르지만 파일이 흩어지거나 이름이 기능마다 달라지면 표류합니다.
하나의 명명 규칙을 선택하고 지키세요. 각 항목에 대해 하나의 단어를 사용하세요: handler, service, repo, request, response. 예를 들어 POST /users 라우트라면 파일과 타입을 users와 create를 중심으로 이름 지으세요(때로는 register, 때로는 addUser 하지 않기).
일반적인 레이어와 일치하는 단순 레이아웃 예:
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
공유 타입이 어디에 위치할지 결정하세요. 많은 프로젝트가 여기서 혼란스러워집니다. 한 가지 유용한 규칙:
internal/apitypes에 둡니다( JSON 및 검증 요구에 맞춤).JSON 태그가 있고 클라이언트를 위해 설계된 타입이라면 API 타입으로 취급하세요.
핸들러 의존성을 최소화하고 그 규칙을 명확히 하세요:
레포 루트에 짧은 패턴 문서를 Markdown으로 작성하세요. 폴더 트리, 명명 규칙, 그리고 한 가지 작은 예시 흐름(handler -> service -> repo 및 각 조각이 어느 파일에 들어가는지)을 포함하세요. 이 문서는 생성기에 붙여넣는 정확한 참조가 되어 새 엔드포인트가 매번 동일한 구조를 갖게 합니다.
열 개의 엔드포인트를 생성하기 전에 신뢰할 수 있는 하나의 엔드포인트를 만드세요. 이것이 골드 스탠다드입니다: "새 코드가 이렇게 보여야 한다"고 가리킬 수 있는 파일입니다. 새로 작성해도 되고 기존 것을 리팩터해 맞춰도 됩니다.
핸들러는 얇게 유지하세요. 도움이 되는 한 가지 기법: 핸들러와 서비스 사이에 인터페이스를 두어 핸들러가 구체적인 구조체가 아니라 계약에 의존하도록 만드세요.
참조 엔드포인트에만 향후 생성 코드가 실수할 수 있는 곳에 작은 주석을 달아주세요. 결정(왜 400 대 422, 왜 생성은 201을 반환하는지, 왜 내부 오류를 일반 메시지로 숨기는지)을 설명하세요. 코드 자체를 반복해서 설명하는 주석은 피하세요.
참조 엔드포인트가 동작하면 헬퍼를 추출해 새 엔드포인트마다 표류할 가능성을 줄이세요. 가장 재사용성이 높은 헬퍼들:
"얇은 핸들러 + 인터페이스"의 실무 예시는 다음과 같습니다:
type UserService interface {
CreateUser(ctx context.Context, in CreateUserInput) (User, error)
}
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
var in CreateUserRequest
if err := BindJSON(r, &in); err != nil {
WriteError(w, ErrBadJSON) // 400: malformed JSON
return
}
if err := Validate(in); err != nil {
WriteError(w, err) // 422: validation details
return
}
user, err := h.svc.CreateUser(r.Context(), in.ToInput())
if err != nil {
WriteError(w, err)
return
}
WriteJSON(w, http.StatusCreated, user)
}
몇 개의 테스트(오류 매핑에 대한 작은 테이블 테스트 등)를 추가해 패턴을 고정하세요. 생성은 명확한 표적이 있을 때 가장 잘 작동합니다.
일관성은 당신이 붙여넣는 것과 금지하는 것에서 시작합니다. 새 엔드포인트에 대해 두 가지를 제공하세요:
핸들러, 서비스 메서드, 요청/응답 타입, 엔드포인트가 사용하는 공유 헬퍼를 포함하세요. 그런 다음 계약을 명확히 적으세요:
POST /v1/widgets)이름, 패키지 경로, 헬퍼 함수(WriteJSON, BindJSON, WriteError, 당신의 밸리데이터)를 정확히 일치시켜야 할 것을 명시하세요.
너무 친절한 리팩터를 막기 위해 엄격한 프롬프트를 사용하세요. 예시:
Using the reference endpoint below and the pattern notes, generate a new endpoint.
Contract:
- Route: POST /v1/widgets
- Request: {"name": string, "color": string}
- Response: {"id": string, "name": string, "color": string, "createdAt": string}
- Errors: invalid JSON -> 400; validation -> 422; duplicate name -> 409; unexpected -> 500
Output ONLY these files:
1) internal/http/handlers/widgets_create.go
2) internal/service/widgets.go (add method only)
3) internal/types/widgets.go (add types only)
Do not change: router setup, existing error format, existing helpers, or unrelated files.
Must use: package paths and helper functions exactly as in the reference.
테스트를 원하면 명시적으로 요청하세요(그리고 테스트 파일명을 지정). 그렇지 않으면 모델이 테스트를 생략하거나 테스트 환경을 임의로 만들 수 있습니다.
생성 후 빠른 diff 검사를 하세요. 공유 헬퍼, 라우터 등록, 표준 오류 응답을 수정했다면 출력물을 거부하고 "변경 금지" 규칙을 더 엄격히 하세요.
출력은 입력만큼 일관됩니다. "거의 맞는" 코드를 피하는 가장 빠른 방법은 매번 같은 프롬프트 템플릿을 재사용하고, 저장소에서 작은 컨텍스트 스냅샷을 붙이는 것입니다.
붙여넣고 자리표시자를 채우세요:
You are editing an existing Go HTTP API.
CONTEXT
- Folder tree (only the relevant parts):
<paste a small tree: internal/http, internal/service, internal/repo, etc>
- Key types and patterns:
- Handler signature style: <example>
- Service interface style: <example>
- Request/response DTOs live in: <package>
- Standard error response JSON:
{
"error": {
"code": "invalid_argument",
"message": "...",
"details": {"field": "reason"}
}
}
- Status code map:
invalid_json -> 400
invalid_argument -> 422
not_found -> 404
conflict -> 409
internal -> 500
TASK
Add a new endpoint: <METHOD> <PATH>
- Handler name: <Name>
- Service method: <Name>
- Request JSON example:
{"name":"Acme"}
- Success response JSON example:
{"id":"123","name":"Acme"}
CONSTRAINTS
- No new dependencies.
- Keep functions small and single-purpose.
- Match existing naming, folder layout, and error style exactly.
- Do not refactor unrelated files.
ACCEPTANCE CHECKS
- Code builds.
- Existing tests pass (add tests only if the repo already uses them for handlers/services).
- Run gofmt on changed files.
FINAL INSTRUCTION
Before writing code, list any assumptions you must make. If an assumption is risky, ask a short question instead.
이 템플릿은 컨텍스트 블록(존재하는 것), 제약 블록(금지할 것), 구체적인 JSON 예시(형식이 흐트러지지 않도록)를 강제합니다. 마지막 지시는 모델이 불확실하면 코드 작성을 실행하기 전에 질문하도록 합니다.
예: "프로젝트 생성(Create project)" 엔드포인트를 추가한다고 합시다. 목표는 간단합니다: 이름을 받아 몇 가지 규칙을 적용하고 저장한 뒤 새 ID를 반환합니다. 까다로운 부분은 동일한 handler-service-repo 분할과 이미 사용 중인 오류 JSON을 유지하는 것입니다.
일관된 흐름:
핸들러가 수용하는 요청 예:
{ "name": "Roadmap", "owner_id": "u_123" }
성공 시 201 Created를 반환하세요. ID는 항상 한 곳에서 나오게 하세요. 예: Postgres가 생성하게 하고 리포지토리가 반환하도록 합니다:
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
두 가지 현실적인 실패 경로:
검증 실패 시(이름 누락 또는 너무 짧음)는 표준 형식으로 필드 수준 오류를 반환하세요:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
이름이 소유자별로 고유해야 하고 서비스에서 기존 프로젝트를 찾으면 409 Conflict를 반환하세요:
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
핸들러는 "이 요청이 형식상 올바른가?"를 검증하고 서비스가 "이것이 허용되는가?"를 검증하도록 분리하는 것이 패턴을 예측 가능하게 만듭니다.
생성기가 즉흥적으로 변경하는 것을 허용하면 가장 빨리 일관성을 잃습니다.
한 가지 흔한 표류는 새로운 오류 형태입니다. 어떤 엔드포인트는 {error: "..."}를 반환하고 다른 엔드포인트는 {message: "..."}를 반환하거나 또 다른 엔드포인트는 중첩 객체를 추가합니다. 이를 고치려면 하나의 오류 봉투와 상태 코드 맵을 한 곳에 두고 새 엔드포인트가 해당 경로와 함수 이름으로 재사용하도록 요구하세요. 생성기가 새 필드를 제안하면 편의성 변경이 아니라 API 변경 요청으로 다루세요.
핸들러 비대화도 빠르게 일관성을 깨뜨립니다. 처음에는 작게 시작합니다: 검증, 권한 검사, DB 조회, 비즈니스 분기. 곧 각 핸들러가 달라집니다. 한 규칙을 유지하세요: 핸들러는 HTTP를 타입화된 입력/출력으로 번역하고, 서비스는 결정을 소유하고, 데이터 접근은 쿼리를 소유합니다.
명명 불일치도 누적됩니다. 어떤 엔드포인트는 CreateUserRequest를 쓰고 다른 엔드포인트는 NewUserPayload를 쓰면 타입을 찾고 글루 코드를 쓰느라 시간이 낭비됩니다. 명명 규칙을 선택하고 강력한 이유가 아니면 새 이름을 거부하세요.
원시 데이터베이스 오류를 클라이언트에 반환하지 마세요. 내부 정보 유출뿐 아니라 일관성 없는 메시지와 상태 코드를 만듭니다. 내부 오류는 래핑해 로그에 원인을 남기고, 안정적인 공개 오류 코드를 반환하세요.
편의성 때문에 새 라이브러리를 추가하지 마세요. 각 추가 라이브러리는 맞춰야 할 또 다른 스타일이 됩니다.
대부분의 손상을 방지하는 가드레일:
두 엔드포인트를 비교했을 때 같은 형태(임포트, 흐름, 오류 처리)를 볼 수 없다면 프롬프트를 강화하고 병합 전 재생성하세요.
무엇이든 머지하기 전에 구조를 먼저 확인하세요. 형태가 맞으면 논리적 버그를 찾기 쉽습니다.
구조 검사:
request_id 행동행동 검사:
not found, conflict)를 트리거해 HTTP 상태와 JSON 형태를 확인패턴을 선호가 아니라 공유된 계약으로 다루세요. "어떻게 엔드포인트를 만드는가" 문서를 코드 근처에 두고 패턴 전체를 보여주는 하나의 참조 엔드포인트를 유지하세요.
작게 묶어 생성을 확장하세요. 간단한 읽기, 검증이 있는 생성, not-found 케이스가 있는 업데이트 등 서로 다른 에지를 건드리는 엔드포인트 2~3개를 생성한 뒤 멈추고 개선하세요. 리뷰에서 같은 스타일 이탈이 반복되면 기준 문서와 참조 엔드포인트를 수정한 뒤 더 생성하세요.
반복 가능한 루프:
더 빠른 빌드-리뷰 루프를 원하면 대화형 워크플로우를 제공하는 도구(Koder.ai)를 사용해 스캐폴딩하고, 표준과 일치하면 소스 코드를 내보내는 방법도 있습니다. 도구보다 중요한 것은 규칙입니다: 기준선이 주도권을 갖습니다.
초기부터 반복 가능한 템플릿을 고정하세요: 일관된 레이어 분리(handler → service → data access), 하나의 오류 봉투, 그리고 상태 코드 맵을 정합니다. 그런 다음 모든 새 엔드포인트는 복사해야 할 단일 “참조 엔드포인트”를 따르게 하세요.
핸들러는 HTTP만 담당하게 하세요:
핸들러에서 SQL, 권한 검사 또는 비즈니스 분기가 보이면 그것을 서비스로 밀어내세요.
서비스에는 비즈니스 규칙과 결정을 넣으세요:
서비스는 도메인 결과와 타입화된 오류를 반환해야 합니다—HTTP 상태 코드나 JSON 형식화를 포함하면 안 됩니다.
영속성 관련 로직을 격리하세요:
리포지토리에서 API 응답 형식을 인코딩하거나 비즈니스 규칙을 강제하지 마세요(기본 데이터 무결성 수준을 넘지 않음).
간단한 기본 규칙:
예: 핸들러는 email이 존재하고 이메일처럼 보이는지 확인하고, 서비스는 그 이메일이 허용되는지 또는 이미 사용 중인지 확인합니다.
항상 하나의 표준 오류 봉투를 사용하고 안정적으로 유지하세요. 실무적으로는 다음과 같은 구조가 적절합니다:
code는 기계용(안정적)message는 사람용(짧고 안전하게)details는 필드 오류 같은 구조화된 추가 정보request_id는 추적용이렇게 하면 클라이언트 측 예외 처리가 줄고 생성된 엔드포인트가 예측 가능해집니다.
상태 코드 맵을 문서화하고 항상 따르세요. 흔한 분류:
클라이언트에는 안전하고 일관된 공개 오류를 반환하고, 실제 원인은 로그에 남기세요.
code, 짧은 message, 그리고 request_id이렇게 하면 내부 정보를 유출하지 않고 엔드포인트들 사이의 임의적인 오류 메시지 차이를 피할 수 있습니다.
“골든” 참조 엔드포인트 하나를 만들고 새 엔드포인트가 그것과 일치하도록 요구하세요:
BindJSON, WriteJSON, WriteError 등)그리고 오류 매핑을 위한 소규모 테스트(테이블 테스트 등)를 추가해 패턴을 강제하세요.
Claude에 새 엔드포인트 생성을 요청할 때는 엄격한 컨텍스트와 제약을 제공하세요:
생성 후에는 공유 헬퍼, 라우터 등록, 표준 오류 응답이 변경되면 결과를 거부하고 규칙을 더 엄격히 재지시하세요.
400bad_json422는 검증 실패(예: validation_failed)404는 리소스 없음(예: not_found)409는 충돌(중복/버전 불일치)500은 예기치 않은 실패핵심은 일관성입니다: 엔드포인트마다 토론하지 마세요.