본문 바로가기
JAVA/Convention

스프링 부트에서 일관된 API 응답과 예외 처리를 구현하는 방법

by devljy 2025. 1. 15.

 

스프링 부트(Spring Boot) 애플리케이션에서 일관된 API 응답 구조글로벌 예외 처리를 구현하는 방법에 대해 제가 구현한 방법에 대해 공유해보겠습니다.

 

( 연관이 있나 싶긴할수도 있지만 , 예외상황에도 일관된 API 를 제공하기 위해서는 곁들여 사용하는게 맞다고 생각하기때문에 한번에 묶어서 글을 작성했습니다. 더 좋은 방식은 댓글로 공유 부탁드립니다 ! ) 

 

소개

스프링 부트 애플리케이션을 개발할 때, API의 응답 구조를 일관되게 유지하는 것은 꼭 필요하다고 생각합니다 .

이는 클라이언트가 응답을 예측 가능하게 처리할 수 있도록 도와주며, 디버깅과 유지보수를 용이하게 합니다. 또한, 예외 처리를 중앙화하여 코드의 중복을 줄이고, 에러 핸들링을 체계적으로 관리할 수 있습니다.

아래 에서는 ApiResponse, GlobalExceptionHandler, ResponseUtil 클래스를 활용하여 이러한 목표를 달성하는 방법을 살펴보겠습니다.

 

 

 

관련 클래스

ApiResponse.java

ApiResponse 클래스는 모든 API 응답의 기본 구조를 정의합니다.

 -  date 의 타입은  제네릭 타입(T) 으로 지정하여 , 다양한 데이터 타입을 지원하여 유연성을 제공합니다.

 -  code , message 부분은 요청은 성공적으로 되었으나 클라이언트의 요구가 서버 측의 비즈니스로직단에서 정상적이지 않다고 판단되어 deny 된 사유 ( 예를들면 재고부족)  를 프론트쪽에 알려주기 위한 코드라고 보면 됩니다. 

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {

    @JsonProperty("data")
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;

    @JsonProperty("code")
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Integer code; // 커스텀 코드

    @JsonProperty("message")
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String message; // 메시지
}

 

 

ResponseUtil.java

ResponseUtil 클래스는 일관된 API 응답을 생성하기 위한 유틸리티 메서드를 제공합니다. 이를 통해 응답 생성 로직을 재사용 가능하게 만들고, 코드의 중복을 줄일 수 있습니다.

우선 기본적인 메소드 템플릿을 createResponse 정의해두었습니다. 

그리고 그 밑의 커스텀한 method declartion 들은 상황에 따라 자주 쓰는것들 위주로 추가해두었습니다. 

**  ( 비즈니스단의 로직에서 에러가 발생시 ResponsenEntity 의 응답 코드를 어떤것으로 내려줄지에 따라 아래 메소드가 달라질수도 있겠죠? )  

해당 프로젝트에서는 정상적인응답일시에는 ResponseUtil.createDefaultSuccessResponse( _list ); 메소드만 사용하긴했었네요  . 

package com.allmytour.google_crawler.core.util;

import com.allmytour.google_crawler.core.common.dto.ApiResponse;
import com.allmytour.google_crawler.core.common.enums.CustomResponseCode;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

public class ResponseUtil {

    /**
     * 기본적인 응답을 처리하기 위해 만들어진 response template
     */

    // 응답을 생성하는 basic template
    public static <T> ResponseEntity<ApiResponse<?>> createResponse(HttpStatus httpStatus, CustomResponseCode cusResCode, T data) {
        ApiResponse<T> response = ApiResponse.<T>builder()
                .data(data)
                .code(cusResCode.getCode())
                .message(cusResCode.getMessage())
                .build();
        return ResponseEntity.status(httpStatus).body(response);
    }

    public static <T> ResponseEntity<ApiResponse<?>> createResponse(HttpStatus httpStatus, CustomResponseCode cusResCode) {
        return createResponse(httpStatus, cusResCode, null);
    }

    /**
     * 성공 응답 처리 methods part ..
     */

    // 성공적인 응답 생성
    public static <T> ResponseEntity<ApiResponse<?>> createDefaultSuccessResponse(T data) {
        return createResponse(HttpStatus.OK, CustomResponseCode.SUCCESS, data);
    }

    // 성공적인 응답 생성 + when need (detail message, code)
    public static <T> ResponseEntity<ApiResponse<?>> createDefaultSuccessResponse(CustomResponseCode cusResCode, T data) {
        return createResponse(HttpStatus.OK, cusResCode, data);
    }

    /**
     * 오류 응답 처리 methods part ..
     */

    // 200 응답과 detail 한 메시지로 control 하고싶은 경우
    public static <T> ResponseEntity<ApiResponse<?>> createErrorResponseOk(CustomResponseCode cusResCode) {
        return createResponse(HttpStatus.OK, cusResCode);
    }

    // 400 error, with customError
    public static <T> ResponseEntity<ApiResponse<?>> createErrorResponseBadRequest(CustomResponseCode cusResCode) {
        return createResponse(HttpStatus.BAD_REQUEST, cusResCode);
    }

    // message 만 들어있는 bad Request handler
    public static <T> ResponseEntity<ApiResponse<?>> createErrorResponseException(Exception e) {
        ApiResponse<T> response = ApiResponse.<T>builder()
                .message(e.getMessage())
                .build();
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }
}

 

 

CustomResponseCode.java

비즈니스적인 부분에서 에러발생시 프론트 team 과 의사소통이 용이해지며 , 프론트 역시도 세분화된 에러코드에 따라 클라이언트 들에게 대응 할수 있는 방법이 다양해집니다.  또한 아래의 스프링 AOP 기능과 함께 사용시 정의해둔 예외에 따라 throw new CommonCustomException(CustomResponseCode.NO_GOOGLE_HOTEL_ID)   같은 구문만으로도 간단하게 일관된 Api response 가 가능합니다 ! 

 

package com.allmytour.google_crawler.core.common.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public enum CustomResponseCode {

    SUCCESS(200, "성공") ,

    BAD_REQUEST(400 , "잘못된 요청입니다. ") ,
    TSINFO_ERROR(4000,"ts_info 를 가져오는도중 에러가 발생") ,
    REAL_SEARCH_ERROR(4001 , "google 가격 스크래핑중 에러발생 , 관리자 문의 ") ,
    NOT_FOUND(404 , "해당 RESOURCE 가 존재하지않습니다.") ,
    NO_THIRD_PART_PRICE_INFO(4040 , "조회된 OTA & 가격이 없습니다") ,
    NO_GOOGLE_HOTEL_ID(4041,"조회한 호텔은 GOOGLE ID 가 없습니다. "),
    NO_HASH_KEY_IN_AMT(4042,"조회한 checkin_date , checkout_date , count 에 일치하는 올마이투어에 저장된  HASH_KEY 가 없습니다."),

    INTERNAL_SERVER_ERROR(500 , "예상치 못한 서버에러 , 관리자 문의 ")
    ;


    private final Integer code;
    private final String message;
}

 

 

GlobalExceptionHandler.java

아래에는 정의해둔 Exception 이 발생시 정의해둔 return 타입을 리턴하는 스프링의 AOP 기능입니다. 

package com.allmytour.google_crawler.core.exception;

import com.allmytour.google_crawler.core.common.dto.ApiResponse;
import com.allmytour.google_crawler.core.util.ResponseUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<?>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        return ResponseUtil.createErrorResponseException(ex);
    }

    @ExceptionHandler(CommonCustomException.class)
    public ResponseEntity<ApiResponse<?>> handleCustomExceptions(CommonCustomException ex) {
        return ResponseUtil.createErrorResponseOk(ex.getCustomResponseCode());
    }

    // 위의 exception 처리기에서 마저 filter 되지 못한 Exception 처리
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<?>> handleTheOutherExceptions(Exception ex) {
        log.error("error occured =====> {} ", ex.getMessage(), ex);
        return ResponseUtil.createErrorResponseException(ex);
    }
}

 

 

 

결론

애플리케이션에서 일관된 API 응답과 글로벌 예외 처리를 구현하는 것은 클라이언트와의 원활한 통신을 위해 필수적입니다. ApiResponse, GlobalExceptionHandler, ResponseUtil ,CustomResponseCode 처럼 활용한다면 이러한 목표를 효율적으로 달성할 수 있습니다.

이를 통해 코드의 유지보수성을 높이고, 예외 처리 로직을 체계적으로 관리할 수 있습니다.

특히, ApiResponse의 제네릭 타입과 커스텀 코드 사용은 다양한 비즈니스 로직 시나리오에 유연하게 대응할 수 있게 해줍니다.

예를 들어, 재고 부족, 인증 실패, 데이터 불일치 등 다양한 상황에서 명확하고 일관된 응답을 제공할 수 있습니다.

여러분의 프로젝트에서도 이와 같은 접근 방식을 도입해 보세요. 더욱 견고하고 신뢰할 수 있는 API를 구축하는 데 큰 도움이 될 것입니다. 감사합니다!

 

'JAVA > Convention' 카테고리의 다른 글

Strategy pattern 사용 소스 예제  (0) 2025.01.16