본문 바로가기
개발

Spring Boot(Kotlin) — 2편. API Response 포맷 설계

by 새싹 아빠 2025. 11. 24.

📚 Spring Boot(Kotlin) 서버 기본 셋팅 — 시리즈 안내

    1. 왜 멀티 모듈 구조인가? (아키텍처 철학 & 전체 설계 편) 
    2. API Response 포맷 설계  ← 현재 글
    3. 글로벌 예외 처리(GlobalExceptionHandler)
    4. Swagger(OpenAPI) 설정
    5. Security(JWT) 기본 골격
    6. JWT TokenProvider
    7. Redis 설정
    8. Validation 설정
    9. Logging + MDC(traceId)
    10. application.yml 프로필 분리 (local/dev/prod)
    11. 멀티모듈 + JPA 기본 구조 정리
    12. 완성된 프로젝트 템플릿 git 공유

 

API 개발을 시작할 때 가장 먼저 부딪히는 문제 중 하나는 “응답(Response)을 어떤 형식으로 설계할 것인가?”입니다.
처음에는 단순히 데이터를 그대로 반환하거나, 컨트롤러마다 다른 형태로 응답을 내려도 크게 불편함을 못 느낄 수 있습니다.
하지만 프로젝트가 커질수록, API가 늘어날수록, 예외와 인증/인가 로직이 추가될수록, 응답 포맷의 일관성은 매우 중요한 기준이 됩니다.

 

📌 왜 API Response 포맷을 초기에 설계해야 할까?

  • 일관성(Consistency) — 모든 API가 동일한 구조를 가지면, 클라이언트(Flutter, React, iOS 등)는 응답을 해석하는 로직를 재사용할 수 있습니다.
  • 확장성(Extensibility) — 에러 코드, 메시지, 인증 실패, Validation 오류 등이 발생해도 새로운 규칙을 만들 필요가 없습니다.
  • 디버깅/모니터링 개선 — 실패 케이스가 표준화되면 로그 추적과 예외 분석이 쉬워집니다.
  • 보안(Security) 측면에서 유리 — 서버 내부 메시지 노출을 줄이고, 프론트에서 표시할 메시지를 통제할 수 있습니다.
  • 팀 협업에 필수 — 백엔드/프론트/QA가 공통된 규약을 기준으로 대화할 수 있습니다.

즉, API Response 포맷을 잡는 것은 단순한 형식 정의가 아니라 서비스 전체의 규칙을 확립하는 작업입니다.

 

📌 기본 Response 구조

본 구조는 성공/실패 여부를 명확히 구분하고, 모든 요청이 동일한 형태를 갖도록 설계되었습니다.

data class ApiResponse<T>(
    val success: Boolean,
    val data: T? = null,
    val error: ApiError? = null
) {
    companion object {
        fun <T> ok(data: T): ApiResponse<T> =
            ApiResponse(success = true, data = data)

        fun <T> empty(): ApiResponse<T> =
            ApiResponse(success = true, data = null)

        fun fail(
            code: String,
            messageKey: String,
            detail: Any? = null
        ): ApiResponse<Unit> =
            ApiResponse(
                success = false,
                error = ApiError(code, messageKey, detail)
            )
    }
}

data class ApiError(
    val code: String,
    val message: String,
    val detail: Any? = null
)

 

 

✅ 설계 포인트 정리

1️⃣ success 필드로 성공/실패를 명확하게

HTTP Status 200 OK가 내려오더라도 비즈니스적으로 실패할 수 있습니다.
그래서 응답 본문 내에서 success = true/false를 명확히 구분하도록 설계했습니다.

2️⃣ data는 성공한 경우에만

성공 시에는 실제 응답 데이터를 data에 담습니다.
실패 시에는 data를 사용하지 않고, error 필드를 통해 원인을 전달합니다.

3️⃣ error 객체 분리

에러는 code, message, detail을 분리하여 구조화했습니다.

  • code — 시스템이 식별 가능한 고유 에러 코드
  • message — 사용자 메시지(또는 메시지 키)
  • detail — 디버깅에 필요한 부가 정보

이 구조는 이후 ValidationException, AuthenticationException, AuthorizationException 등의 예외 처리에서 큰 장점을 제공합니다.

 

📌 왜 message가 아니라 messageKey인가?

직접 문장을 내려버리면 이후에 아래 문제가 생깁니다.

  • 문구 변경 시 서버 코드 수정이 필요
  • 다국어 지원 불가능
  • 보안상 민감한 서버 메시지가 노출될 위험

그래서 서버는 메시지 키만 내려주고, 실제 문구는 클라이언트 또는 별도 메시지 서버에서 관리할 수 있도록 설계했습니다.

 

📌 ApiResponse는 어느 모듈에 두는 것이 좋을까?

이 프로젝트는 멀티 모듈 구조를 사용합니다.

  • api 모듈 — 컨트롤러, 요청/응답 DTO, 인증 필터 등 “외부 요청을 받는 레이어”
  • application 모듈 — 서비스 로직, 비즈니스 예외, 공통 응답 규약 등 “애플리케이션 코어”

여기서 ApiResponse<T>는 단순히 컨트롤러 편의를 위한 유틸 클래스가 아니라, 서비스 전체가 공유하는 “응답 규칙”에 가깝습니다. 그래서 다음과 같은 이유로 application 모듈에 두는 것이 더 적절하다고 생각했습니다.

  • 1. 의존성 방향을 깨지 않기 위해
    일반적으로 의존성 방향은 api → application 입니다.
    만약 ApiResponse를 api 모듈에 두면, application에서 이 타입을 쓰고 싶을 때 application → api로 역참조가 생기면서 의존성 방향이 꼬이게 됩니다.
  • 2. “규약”은 코어에 두는 것이 유지보수에 유리
    응답 포맷은 HTTP 컨트롤러에서만 쓰이는 개념이 아니라, 이후에
    • 배치 작업에서 REST API를 추가로 열거나,
    • 내부용 어드민 API를 별도 모듈로 분리하거나,
    • 다른 어댑터(예: gRPC, 메시지 큐 등)를 붙일 때도
    동일한 규칙을 재사용하고 싶어집니다.
    이런 “전 시스템 공통 규칙”은 api 모듈보다는 application 모듈에 두는 편이 훨씬 안정적입니다.
  • 3. GlobalExceptionHandler와 자연스럽게 연결
    GlobalExceptionHandler는 api 모듈에 위치하지만, 실제로 만드는 응답 객체는 ApiResponse입니다.
    이때 응답 타입이 application에 있으면, api 모듈은 단순히 “규칙을 사용”하는 쪽이 되고,
    규칙 자체의 정의는 application에 모여 있어 설계가 더 깔끔해집니다.

정리하면, ApiResponse는 “웹 계층 전용 DTO”라기보다는, 애플리케이션이 외부 세계와 통신할 때 지키는 공통 계약(Contract)에 가깝기 때문에, application 모듈의 공통 패키지에 두는 방향으로 설계했습니다.

 

📌 실패 응답 예시

{
  "success": false,
  "error": {
    "code": "AUTH_001",
    "message": "error.auth.invalid_token",
    "detail": {
      "expiredAt": "2025-01-01T10:00:00Z"
    }
  }
}

실패하더라도 구조가 항상 동일하다는 것이 핵심입니다.

 

📌 글로벌 예외 처리와의 연결

이 Response 포맷은 앞으로 구현할 GlobalExceptionHandler와 자연스럽게 연결됩니다.

  • 모든 예외를 동일한 포맷으로 반환
  • 로그 & 추적이 쉬워짐
  • MDC(traceId)와 함께 사용하면 장애 분석 속도 향상

즉, 이 설계는 단순한 Response Wrapper가 아니라 프로젝트 전반의 표준화 기반입니다.

 

📌 보안 측면에서의 이점

  • 내부 시스템 메시지 노출 방지
  • stacktrace / exception message 숨김 처리 가능
  • 클라이언트에 필요한 정보만 전달할 수 있음

결과적으로 “실패했지만 보안적으로 안전한 응답”을 제공할 수 있습니다.

 

✅ 결론

API Response 포맷을 초기에 설계하는 것은 아키텍처의 기준선을 정하는 작업입니다.
이 한 가지 규칙만으로도 이후에 구현할 예외 처리, JWT 보안, Validation, Logging, Redis 등 모든 기능을 일관된 방식으로 확장할 수 있습니다.

프로젝트가 커질수록, 표준화는 선택이 아니라 필수입니다.

 

📌 다음 편 안내

3편 — 글로벌 예외 처리(GlobalExceptionHandler)

📌 이 Response 포맷이 실제 예외 처리에서 어떻게 활용되는지 알아볼 예정입니다.

 

https://jaemoi8.tistory.com/32 

 

⚠️ Spring Boot(Kotlin) — 3편. 글로벌 예외 처리(GlobalExceptionHandler)

📚 Spring Boot(Kotlin) 서버 기본 셋팅 — 시리즈 안내왜 멀티 모듈 구조인가? (아키텍처 철학 & 전체 설계 편)API Response 포맷 설계글로벌 예외 처리(GlobalExceptionHandler) ← 현재 글Swagger(OpenAPI) 설정Securi

jaemoi8.tistory.com