2024. 12. 22. 07:29ㆍCS/Spring
@RestContollerAdvice
- RestControllerAdvice = ControllerAdvice + ResponseBody
- @RestControllerAdvice 로 선언하면 컨트롤러에서 리턴하는 값이 응답 값의 body로 세팅되어 클라이언트에게 전달됨
@RestControllerAdvice는 애플리케이션 전반에서 발생하는 예외를 전역적으로 처리함
- @RestController에서 발생하는 예외가 @RestControllerAdvice에 의해서 관리됨
@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity<Object> onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request);
}
@ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
e.printStackTrace();
return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), request, e.getMessage());
}
}
- @ExceptionHandler
- @ExceptionHandler 메서드들 (onThrowException, exception 등)은 각기 다른 예외 유형을 처리
- GeneralException이 발생하면 onThrowException이 호출
- 일반적인 Exception이 발생하면 exception 메서드가 호출
- @ExceptionHandler 메서드들 (onThrowException, exception 등)은 각기 다른 예외 유형을 처리
- handleExceptionInternal
- 표준화된 에러 응답(ApiResponse)을 생성하여 반환
⇒ 전역적으로 예외를 처리하므로, 컨트롤러에서 별도의 예외 처리 로직을 작성하지 않아도 됨
⇒ 이는 @RestControllerAdvice가 없으면
- 컨트롤러별 예외 처리 코드를 작성해야해서 예외 처리 코드가 중복
- 응답 형식의 일관성 떨어짐
- 예외 발생 시, 각 컨트롤러에서 개별적으로 로깅을 처리해야해서, 중앙화된 예외 로깅이 부족함을 의미
클라이언트의 요청이 들어오고 일관된 API 응답을 보내기까지 흐름
1. 클라이언트 요청 -> 컨트롤러(TempRestController)
- 클라이언트가 /temp/test와 같은 엔드포인트에 요청을 보내면, 이 요청은 가장 먼저 컨트롤러(TempRestController)에 도달합니다. 컨트롤러는 요청을 받고, 이를 처리하기 위해 서비스 계층(TempQueryService)으로 전달합니다.
@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {
private final TempQueryService tempQueryService;
@GetMapping("/test")
public ApiResponse<TempResponse.TempTestDTO> testAPI(@RequestParam String param){
TempResponse.TempTestDTO result = tempQueryService.getTempData(param);
if (result != null) {
return ApiResponse.onSuccess(result);
} else {
return ApiResponse.onFailure("404", "Data not found", null);
}
}
}
2. 서비스 계층 (TempQueryService) ➔ 데이터 검색 및 비즈니스 로직 처리
컨트롤러에서 전달받은 요청은 서비스 계층에서 비즈니스 로직을 통해 처리됩니다. 예를 들어 데이터베이스에서 특정 정보를 조회하거나 필요한 계산을 수행합니다.
- 성공: 데이터가 정상적으로 조회되면 성공 응답에 필요한 데이터를 반환합니다.
- 실패: 데이터가 없거나 요청이 유효하지 않을 경우 null 또는 예외를 반환하여 실패 상황을 알립니다.
@Service
public class TempQueryService {
public TempResponse.TempTestDTO getTempData(String param) {
Optional<TempEntity> tempEntity = tempRepository.findByParam(param);
if (tempEntity.isPresent()) {
return TempConverter.toTempTestDTO(tempEntity.get());
} else {
return null;
}
}
}
3. TempConverter ➔ DTO로 변환하여 응답 데이터 준비
서비스 계층에서 데이터를 가져오면, 클라이언트에게 반환하기 전 TempConverter를 통해 DTO(Data Transfer Object)로 변환합니다. DTO는 클라이언트에 전달할 데이터를 정의하는 객체로, 엔티티나 비즈니스 로직과 분리된 형태로 데이터를 담습니다.
- TempTestDTO: 성공한 응답 데이터를 담는 DTO입니다.
- TempExceptionDTO: 예외나 오류 상황에서 반환할 데이터를 담는 DTO입니다.
public class TempConverter {
public static TempResponse.TempTestDTO toTempTestDTO(TempEntity entity){
return TempResponse.TempTestDTO.builder()
.testString(entity.getSomeValue()) // entity의 값을 사용해 변환
.build();
}
public static TempResponse.TempExceptionDTO toTempExceptionDTO(Integer flag){
return TempResponse.TempExceptionDTO.builder()
.flag(flag)
.build();
}
}
- TempConverter를 사용하면 엔티티 데이터를 클라이언트와 주고받기 쉬운 형태로 바꿀 수 있습니다.
- 예를 들어, TempEntity 데이터를 TempTestDTO로 변환하면, 실제로 필요한 정보만 포함된 객체를 생성해 반환하게 됩니다.
4. TempResponse ➔ 다양한 응답 데이터를 위한 DTO 정의
TempResponse 클래스는 여러 종류의 응답을 처리할 수 있도록 여러 내부 클래스 형태의 DTO를 정의하고 있습니다.
- TempTestDTO: 정상적인 데이터를 담을 때 사용하는 DTO입니다. testString과 같은 필드를 통해 성공적인 응답 데이터를 표현합니다.
- TempExceptionDTO: 예외 상황에서 사용하는 DTO입니다. flag와 같은 필드를 통해 실패 응답 데이터를 구조화할 수 있습니다.
public class TempResponse {
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempTestDTO{
String testString;
}
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TempExceptionDTO{
Integer flag;
}
}
각각의 DTO는 빌더 패턴을 사용하여 필요에 따라 유연하게 데이터를 생성할 수 있도록 설계되었습니다.
5. ApiResponse ➔ 통일된 API 응답 생성
- onSuccess(): 성공 응답을 생성할 때, TempTestDTO와 같은 데이터 객체를 전달하여 응답의 result 필드에 담깁니다.
- onFailure(): 실패 응답을 생성할 때, 에러 코드와 메시지를 통해 실패 상황을 설명합니다.
public class ApiResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result;
public static <T> ApiResponse<T> onSuccess(T result){
return new ApiResponse<>(true, "200", "OK", result);
}
public static <T> ApiResponse<T> onFailure(String code, String message, T data){
return new ApiResponse<>(false, code, message, data);
}
}
- ApiResponse 덕분에, 클라이언트는 항상 일정한 형식의 응답을 받게 됩니다.
- 성공 여부(isSuccess), 코드(code), 메시지(message), 데이터(result)가 포함된 일관된 JSON 응답을 받을 수 있게 되죠.
전체 흐름 요약
- 컨트롤러: 클라이언트 요청을 수신하고, 이를 서비스 계층으로 전달합니다.
- 서비스 계층: 요청을 처리하고, 필요한 데이터를 조회하거나 비즈니스 로직을 실행합니다. 결과에 따라 성공/실패 상태를 결정합니다.
- TempConverter: 엔티티 데이터를 클라이언트 응답에 적합한 형태인 DTO로 변환하여 TempResponse 내부 클래스 형태로 반환합니다.
- ApiResponse: onSuccess()와 onFailure() 메서드를 통해 성공/실패 응답을 통일된 형태로 생성하여 클라이언트에 전달합니다.
이 흐름 덕분에 서버는 클라이언트에게 안정적이고 일관된 응답을 제공할 수 있습니다.
클라이언트의 요청이 들어오고, 요청 처리 중 발생하는 에러 핸들링 흐름
전체 흐름 개요
- 클라이언트 요청이 들어오면, 이를 처리하는 서비스 메서드(TempQueryServiceImpl)가 실행됩니다.
- 서비스 메서드에서 요청 조건을 확인하는 과정에서 예외가 발생하면, 사용자 정의 예외(TempHandler)가 발생합니다.
- 이 예외는 ExceptionAdvice 클래스에 의해 처리되어, 클라이언트에게 표준화된 에러 응답이 반환됩니다.
- ExceptionAdvice 클래스는 발생한 예외의 종류에 따라 적절한 방식으로 에러 메시지를 구성하고, ApiResponse 형식으로 클라이언트에 반환합니다.
1. 클라이언트 요청 처리
클라이언트가 서버로 요청을 보내면, 해당 요청은 비즈니스 로직을 처리하는 서비스 클래스인 TempQueryServiceImpl로 전달됩니다.
이 클래스에서는 CheckFlag 메서드를 사용해 요청에 전달된 flag 값을 확인하고, 특정 조건에 따라 예외를 발생시킵니다.
TempQueryServiceImpl 코드 설명
@Service
@RequiredArgsConstructor
public class TempQueryServiceImpl implements TempQueryService {
@Override
public void CheckFlag(Integer flag) {
if(flag == 1){
throw new TempHandler(ErrorStatus.TEMP_EXCEPTION); // 조건에 따라 예외 발생
}
}
}
- flag 값이 1인 경우, TempHandler 예외를 발생시키고, ErrorStatus.TEMP_EXCEPTION 에러 코드를 함께 전달합니다.
- TempHandler 예외는 GeneralException을 상속하여, 사용자 정의 예외를 통한 에러 처리가 가능합니다.
2. 사용자 정의 예외 클래스 : TempHandler와 GeneralException
TempHandler는 GeneralException을 상속한 사용자 정의 예외로, 발생한 예외를 표준화된 에러 응답 형식으로 반환하는 데 필요합니다.
GeneralException과 TempHandler 코드 설명
public class TempHandler extends GeneralException {
public TempHandler(BaseErrorCode errorCode) {
super(errorCode);
}
}
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode code;
public ErrorReasonDTO getErrorReason(){
return this.code.getReason();
}
public ErrorReasonDTO getErrorReasonHttpStatus(){
return this.code.getReasonHttpStatus();
}
}
- TempHandler는 GeneralException을 상속하며, ErrorStatus.TEMP_EXCEPTION과 같은 에러 코드를 기반으로 예외를 정의합니다.
- GeneralException은 에러 코드를 기반으로 에러 메시지와 HTTP 상태 코드를 포함하는 ErrorReasonDTO 객체를 반환합니다.
- 이 예외가 발생하면, ExceptionAdvice 클래스에서 이를 감지하여 적절한 응답을 구성합니다.
3. ExceptionAdvice: 예외를 감지하고 처리하는 클래스
ExceptionAdvice 클래스는 발생한 예외들을 감지하여 적절한 방식으로 처리하고, 표준화된 에러 응답을 클라이언트에 반환하는 역할을 합니다. 이 클래스는 @RestControllerAdvice 어노테이션으로 컨트롤러의 예외를 전역적으로 처리합니다.
ExceptionAdvice 주요 메서드 설명
- onThrowException: GeneralException을 감지하여 처리합니다.
- validation 및 handleMethodArgumentNotValid: 요청 값 검증 에러(ConstraintViolationException, MethodArgumentNotValidException)를 처리합니다.
- exception: 모든 Exception을 처리합니다.
@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity<Object> onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request);
}
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
HttpHeaders headers, HttpServletRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(), reason.getMessage(), null);
WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}
}
- onThrowException 메서드는 GeneralException이 발생할 경우 에러 메시지와 상태 코드를 포함하는 ApiResponse 객체를 생성해 반환합니다.
- handleExceptionInternal 메서드는 ApiResponse 형식의 응답을 생성하며, 표준화된 에러 응답을 클라이언트에 전달합니다.
- ApiResponse.onFailure() 메서드를 호출해 실패 응답을 생성하며, 이 응답은 일관된 형식으로 클라이언트에 전달됩니다.
4. ApiResponse: 표준화된 에러 응답 생성
ApiResponse 클래스는 모든 API 응답을 표준화된 형식으로 생성하는 역할을 합니다. 성공 응답(onSuccess)과 실패 응답(onFailure) 메서드가 있으며, 예외 처리 시에는 실패 응답을 생성하여 클라이언트에 반환합니다.
public class ApiResponse<T> {
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result;
public static <T> ApiResponse<T> onFailure(String code, String message, T data) {
return new ApiResponse<>(false, code, message, data);
}
}
- ApiResponse.onFailure는 에러 코드와 메시지를 기반으로 실패 응답 객체를 생성합니다.
- ExceptionAdvice 클래스에서 이 객체를 사용하여 클라이언트에게 표준화된 에러 응답을 전송합니다.
예외 발생부터 클라이언트 응답까지의 전체 흐름 요약
- 클라이언트 요청: flag 값을 포함한 요청이 들어오면, TempQueryServiceImpl의 CheckFlag 메서드가 실행됩니다.
- 예외 발생: flag 값이 1일 경우, TempHandler 예외가 발생합니다.
- ExceptionAdvice에서 예외 처리:
- TempHandler 예외가 GeneralException을 상속하므로, ExceptionAdvice의 onThrowException 메서드에서 감지됩니다.
- onThrowException 메서드는 GeneralException의 에러 코드를 사용해 ErrorReasonDTO를 가져와 표준화된 에러 응답을 생성합니다.
- ApiResponse 응답 생성: ApiResponse.onFailure 메서드를 통해 표준화된 실패 응답을 생성하고, 클라이언트에 전송합니다.
- 클라이언트 응답: 클라이언트는 통일된 형식으로 에러 메시지, 에러 코드, 상태 코드를 포함한 JSON 응답을 받게 됩니다.
이 과정 덕분에 서버는 발생하는 모든 예외를 표준화된 형식으로 클라이언트에게 응답할 수 있으며, 예외 처리 과정이 일관성을 유지합니다.
'CS > Spring' 카테고리의 다른 글
[Spring/Springboot] @Valid 란? (0) | 2024.12.23 |
---|---|
[Spring/Springboot] Spring Data JPA - Paging/Slice (0) | 2024.12.23 |
[Spring/Springboot] N+1 문제 해결 방안 - Batch Size/2차 캐시/Subselect Fetching/DTO (0) | 2024.12.22 |
[Spring/Springboot] QueryDSL이란? (0) | 2024.12.22 |
[Spring/Springboot] @EntityGraph (0) | 2024.12.22 |