ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot 시리즈 7편 – API 응답 구조 표준화 전략: 성공과 실패를 구분하는 통일 설계
    기술과 산업/언어 및 프레임워크 2025. 4. 23. 20:00
    728x90

    Spring Boot REST API의 응답 구조를 표준화하는 전략을 소개합니다. 성공/실패 응답 포맷 통일, 공통 응답 객체 설계, 예외 처리 통합까지 포함합니다.


    Spring Boot 시리즈 7편 – API 응답 구조 표준화 전략: 성공과 실패를 구분하는 통일 설계

    REST API에서 가장 흔한 실수 중 하나는 각 API마다 다른 응답 구조입니다.
    예를 들어 성공 응답은 그냥 JSON 객체로, 실패 응답은 상태 코드만 주거나,
    경우에 따라 메시지 형태도 제각각이라면 프론트엔드와 앱 개발자는 매번 예외 처리를 따로 해야 하는 불편함을 겪게 됩니다.

    이번 편에서는 Spring Boot 환경에서 API 응답을 성공/실패 케이스 모두 포함하여 통일된 형식으로 설계하고,
    예외 상황까지 일관성 있게 처리하는 방법을 소개합니다.


    📌 1. 왜 응답 구조를 표준화해야 하나?

    • 프론트/앱 개발자와 협업이 쉬워짐
    • 에러 발생 시 원인 추적과 디버깅 속도 증가
    • API 명세(Swagger)에서 응답 구조 일관성 확보
    • 테스트 코드에서 응답 구조 검증이 단순화됨

    ✅ 2. 통일된 공통 응답 포맷 설계

    가장 많이 쓰이는 형식은 다음과 같습니다:

    {
      "success": true,
      "code": "S200",
      "message": "정상 처리되었습니다.",
      "data": {
        "id": 1,
        "name": "홍길동"
      }
    }
    

    🔁 실패 응답 예시

    {
      "success": false,
      "code": "C001",
      "message": "필수 파라미터 누락",
      "errors": [
        {
          "field": "email",
          "message": "이메일은 필수입니다."
        }
      ]
    }
    

    🧱 3. 공통 응답 객체 클래스 구성

    1️⃣ ApiResponse<T> 클래스

    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    public class ApiResponse<T> {
        private boolean success;
        private String code;
        private String message;
        private T data;
    
        public static <T> ApiResponse<T> success(T data) {
            return new ApiResponse<>(true, "S200", "요청이 성공적으로 처리되었습니다.", data);
        }
    
        public static <T> ApiResponse<T> fail(String code, String message, T errorDetail) {
            return new ApiResponse<>(false, code, message, errorDetail);
        }
    }
    

    2️⃣ FieldErrorResponse

    @Getter
    @AllArgsConstructor
    public class FieldErrorResponse {
        private String field;
        private String message;
    }
    

    🛠️ 4. Controller에서의 적용 예시

    @PostMapping
    public ResponseEntity<ApiResponse<UserDto>> register(@Valid @RequestBody RegisterUserRequest request) {
        UserDto result = userService.register(request);
        return ResponseEntity.ok(ApiResponse.success(result));
    }
    

    ❌ 예외 응답 통합 처리

    1️⃣ BusinessException 기반 커스텀 예외 정의

    @Getter
    public class BusinessException extends RuntimeException {
        private final ErrorCode errorCode;
        public BusinessException(ErrorCode errorCode) {
            super(errorCode.getMessage());
            this.errorCode = errorCode;
        }
    }
    

    2️⃣ ErrorCode Enum 예시

    @Getter
    @AllArgsConstructor
    public enum ErrorCode {
        INVALID_INPUT("C001", "입력값이 올바르지 않습니다."),
        DUPLICATE_EMAIL("U001", "이미 사용 중인 이메일입니다."),
        INTERNAL_ERROR("S500", "서버 내부 오류");
    
        private final String code;
        private final String message;
    }
    

    3️⃣ GlobalExceptionHandler 통합 처리

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(BusinessException.class)
        public ResponseEntity<ApiResponse<?>> handleBusinessException(BusinessException e) {
            ErrorCode code = e.getErrorCode();
            return ResponseEntity.badRequest().body(ApiResponse.fail(code.getCode(), code.getMessage(), null));
        }
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResponseEntity<ApiResponse<List<FieldErrorResponse>>> handleValidationException(MethodArgumentNotValidException ex) {
            List<FieldErrorResponse> errors = ex.getBindingResult().getFieldErrors().stream()
                .map(err -> new FieldErrorResponse(err.getField(), err.getDefaultMessage()))
                .toList();
            return ResponseEntity.badRequest().body(ApiResponse.fail("C001", "유효성 검사 실패", errors));
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseEntity<ApiResponse<Void>> handleAll(Exception ex) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(ApiResponse.fail("S500", "알 수 없는 오류가 발생했습니다.", null));
        }
    }
    

    🧠 실무 설계 팁

    항목 설계 전략

    공통 응답 구조 success, code, message, data 필드 유지
    Swagger 문서화 @ApiResponse에 응답 구조 통일
    오류 코드 관리 ErrorCode Enum으로 체계적 관리
    테스트 코드 JSON 구조 검증 → jsonPath("success").value(true) 등
    코드 통일 모든 Controller는 ApiResponse<T>로 반환 통일

    ✅ 마무리 요약

    항목 요약

    핵심 클래스 ApiResponse<T>, ErrorCode, FieldErrorResponse
    예외 통합 처리 @RestControllerAdvice + BusinessException 구조
    표준 응답 포맷 성공/실패 구조를 동일하게 유지
    협업 효과 프론트/앱 개발자, QA 모두 예측 가능한 처리 가능
    Swagger 연계 명세 자동화와 테스트 연계에 유리

    📌 다음 편 예고

    Spring Boot 시리즈 8편: 입력값 검증 전략 – @Valid, 커스텀 Validator, 도메인 유효성 설계까지

     

    728x90
Designed by Tistory.