스프링 부트(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 |
---|