기술과 산업/언어 및 프레임워크

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

B컷개발자 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