0. 들어가기 전
클라이언트 단에서는 백엔드에서 개발한 API Response를 토대로 데이터 모델이라는 것을 만들어 화면에 데이터를 출력해야 합니다.
만약 통일된 규격의 API Response가 없으면 클라이언트 개발자들은 API마다 데이터 모델을 일일이 만들어야 합니다. 그러므로 백엔드에서는 어느 정도 통일된 양식의 API를 전달해 주는 것이 좋습니다.
공통 응답 패키지 구조는 다음과 같습니다.
1. 공통 응답 포맷
a. 정상 응답
정상 응답일 경우, 위와 같은 형태의 응답 포맷을 반환하는 것이 목표입니다. success는 정상 응답 시 true로 반환되며, data는 객체의 상태를 JSON 형식으로 반환합니다. error는 에러가 발생한 경우 에러 코드와 설명을 포함하며, 정상 응답일 경우 null을 반환합니다.
b. 예외 처리 응답
예외 처리 응답의 경우, error를 통해 예외 상황에 대한 설명을 제공하고, 요청 값 중 잘못된 부분을 구체적으로 명시합니다. 이때 success는 false로 설정되며, data는 null 값을 가집니다.
🤔 공통 응답이 필요한 이유
1. 비일관성 : 성공과 실패에 대해 응답 포맷이 다르면 클라이언트에서는 성공과 실패를 구분하는 로직을 처리하기 위한 코드를 구현해야 하고 로직이 복잡해집니다.
2. 정보 부족 : 위에서의 응답은 상세한 정보가 부족하여 클라이언트가 어떤 문제로 인해 실패했는지 정확히 파악하기 어려워 디버깅이 어려워집니다.
3. 확장성 부족 : 클라이언트에 API가 여러 타입의 메세지, 상태, 데이터 등을 반환하기가 어렵습니다.
2. 공통 응답 포맷 구현
a. ApiResponse
@Getter
@NoArgsConstructor
public class ApiResponse<T> {
private boolean success;
private T data;
private ErrorResponse error;
// 성공 응답 생성 메서드
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.success = true;
response.data = data;
return response;
}
// 실패 응답 생성 메서드
public static <T> ApiResponse<T> error(ErrorResponse errorResponse) {
ApiResponse<T> response = new ApiResponse<>();
response.success = false;
response.error = errorResponse;
return response;
}
}
이 클래스는 세 개의 필드를 가지고 있습니다: success는 응답이 성공적인지 여부를 나타내는 불리언 값이며, data는 성공적인 응답에 포함될 데이터, error는 실패한 응답에서 에러 정보를 담기 위한 필드입니다.
ApiResponse 클래스는 두 개의 정적 메서드를 제공합니다. success 메서드는 성공적인 응답을 생성하며, 성공 시 success 필드를 true로 설정하고, 전달받은 데이터를 data 필드에 저장합니다. 반면, error 메서드는 실패한 응답을 생성하며, success 필드는 false로 설정되고, 전달된 ErrorResponse 객체가 error 필드에 설정됩니다.
b. GlobalExceptionHandler
GlobalExceptionHandler는 Spring Framework에서 발생하는 예외를 애플리케이션 전역에서 처리하는 역할을 하는 클래스입니다. 이를 통해 예외 처리 로직을 중앙집중화하여 코드의 일관성을 유지하고, 각 예외에 대해 적절한 HTTP 응답을 반환할 수 있습니다.
@RestControllerAdvice 어노테이션을 사용하여 이 클래스를 정의하면, 이 클래스는 Spring MVC의 모든 컨트롤러에서 발생하는 예외를 처리할 수 있는 글로벌 예외 처리기가 됩니다. 이 클래스는 예외가 발생했을 때 특정 처리를 할 수 있도록 @ExceptionHandler 어노테이션을 이용해 예외 처리 메서드를 정의합니다.
💡GlobalExceptionHandler 장점 정리
1. 중앙 집중식 예외 처리 : 각 컨트롤러에서 예외 처리 로직을 별도로 작성할 필요 없이, 한 곳에서 모든 예외를 처리할 수 있습니다.
2, 일관성 유지 : 모든 예외가 동일한 방식으로 처리되므로, 응답 포맷이나 에러 메세지 형식을 일관되게 유지할 수 있습니다.
3. 확장성 : 새로운 예외가 추가될 때마다 별도로 각 컨트롤러에 예외 처리 코드를 작성할 필요 없이 GlobalExceptionHandler에서 처리하면 되므로 유지보수가 용이합니다.
아래는 프로젝트를 진행하면서 GlobalExceptionHandler 작성한 코드입니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
// 400 Bad Request
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<?>> handleBadRequestException(IllegalArgumentException ex, HttpServletRequest request) {
ErrorResponse errorResponse = ErrorResponse.of(ex.getErrorCode(), request.getRequestURI());
return new ResponseEntity<>(ApiResponse.error(errorResponse), ex.getErrorCode().getStatus());
}
// 403 Forbidden
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<?>> handleAccessDeniedException(AccessDeniedException ex, HttpServletRequest request) {
ErrorResponse errorResponse = ErrorResponse.of(ex.getErrorCode(), request.getRequestURI());
return new ResponseEntity<>(ApiResponse.error(errorResponse), ex.getErrorCode().getStatus());
}
// 404 Not Found
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<?>> handleNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
ErrorResponse errorResponse = ErrorResponse.of(ex.getErrorCode(), request.getRequestURI());
return new ResponseEntity<>(ApiResponse.error(errorResponse), ex.getErrorCode().getStatus());
}
// Security Error
@ExceptionHandler(SecurityTokenException.class)
public ResponseEntity<ApiResponse<?>> handleNotFoundException(SecurityTokenException ex, HttpServletRequest request) {
ErrorResponse errorResponse = ErrorResponse.of(ex.getErrorCode(), request.getRequestURI());
return new ResponseEntity<>(ApiResponse.error(errorResponse), ex.getErrorCode().getStatus());
}
// 500 Internal Server Error
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<?>> handleAllExceptions(Exception ex, HttpServletRequest request) {
ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR, request.getRequestURI());
return new ResponseEntity<>(ApiResponse.error(errorResponse), ErrorCode.INTERNAL_SERVER_ERROR.getStatus());
}
}
각 예외를 처리하는 메서드는 @ExceptionHandler 어노테이션을 사용하여 특정 예외 타입을 처리하며, ResponseEntity<ApiResponse<?>>를 반환합니다. 이때 ApiResponse.error() 메서드를 통해 실패한 응답을 생성하고, ErrorResponse 객체에 에러에 대한 상세 정보를 담아 반환합니다.
예를 들어, IllegalArgumentException 예외가 발생하면 400(Bad Request) 상태 코드와 함께 해당 예외에 대한 설명을 포함한 에러 메시지를 반환합니다. 마찬가지로, AccessDeniedException은 403(Forbidden), ResourceNotFoundException은 404(Not Found) 상태 코드를 반환하며, 기타 예외는 500(Internal Server Error) 상태 코드와 함께 처리됩니다.
이 클래스는 예외 발생 시 응답을 표준화하여 클라이언트가 예상할 수 있는 형태로 정보를 제공하고, 문제 해결을 쉽게 할 수 있도록 합니다.
c. ErrorResponse
@Getter
@Builder
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String code;
private String message;
private String path;
public static ErrorResponse of(ErrorCode errorCode, String path) {
return ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(errorCode.getStatus().value())
.code(errorCode.getCode())
.message(errorCode.getMessage())
.path(path)
.build();
}
}
ErrorResponse 클래스는 애플리케이션에서 발생한 오류 정보를 구조화하여 클라이언트에게 전달하는 역할을 합니다. 이 클래스는 오류가 발생한 시점(timestamp), HTTP 상태 코드(status), 애플리케이션에서 정의한 오류 코드(code), 오류 메시지(message), 그리고 오류가 발생한 요청 경로(path)를 포함합니다.
d. ErrorCode
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
// 400 Bad Request
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "INVALID_INPUT_VALUE", "잘못된 입력 값입니다."),
INVALID_ARGUMENT(HttpStatus.BAD_REQUEST, "INVALID_ARGUMENT", "잘못된 요청입니다."),
// 404 Not Found
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "RESOURCE_NOT_FOUND", "리소스를 찾을 수 없습니다."),
// 500 Internal Server Error
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "서버 에러가 발생했습니다.");
private final HttpStatus status;
private final String code;
private final String message;
}
ErrorCode 클래스는 애플리케이션에서 발생할 수 있는 다양한 오류를 관리하기 위해 정의된 열거형 클래스입니다. 이 클래스는 각 오류를 HTTP 상태 코드, 오류 코드, 그리고 사용자에게 전달될 메시지로 표현합니다. 예를 들어, 잘못된 입력 값이 전달되었을 때는 INVALID_INPUT_VALUE라는 오류 코드와 함께 400 (Bad Request) 상태를 반환하고, 리소스를 찾을 수 없는 경우에는 RESOURCE_NOT_FOUND 코드와 404 (Not Found) 상태를 반환합니다.
이렇게 미리 정의된 오류 코드를 사용하면, 오류 상황에 대해 일관된 응답을 제공할 수 있습니다.
'Programming > Spring' 카테고리의 다른 글
[Spring] Virtual Thread 기반 최적화 (0) | 2024.12.09 |
---|---|
[Spring] OAuth 없이 소셜 로그인 구현 (0) | 2024.11.27 |
[Spring] 동시성 처리 (18) | 2024.11.15 |
[Spring] Security + JWT + OAuth2를 이용한 로그인 구현 (5) - OAuth2.0 로그인 관련 클래스 생성 (0) | 2024.11.12 |
[Spring] Security + JWT + OAuth2를 이용한 로그인 구현 (4) - OAuth란? (0) | 2024.11.06 |