[Study-8, 9주차] @RestControllerAdvice 활용 Exception + 동적쿼리 적용 및 고찰 + N+1 문제에 대한 고찰 - ①
[Study-8, 9주차] @RestControllerAdvice 활용 Exception + 동적쿼리 적용 및 고찰 + N+1 문제에 대한 고찰 - ①
앞서 우리는 AOP와 QueryDSL을 사용해주었고, 추가적으로 N+1 문제 해결을 위해서 fetch join 도 진행해주었다. 또한 Auditing을 사용하여 생성, 수정시간을 저장하는 속성을 BaseEntity에 추가해주었다. 이
soohykeee.tistory.com
- @RestControllerAdvice 활용해서 Exception Handler 처리하기
- AOP와 Querydsl은 조금 심화 공부가 필요하다고 봄
- 클래스 이름이랑 파라미터 정보 찍기
- 실행시간 로그 찍는거
- 내부 메소드 어떻게 할 지?
Querydsl일대다 관계에서 페치조인Querydsl로 구현하면 기능이 좋아질만한 것들 리펙토링PersistenceContext
Auditing날짜 사이에 있는 데이터 조회하기
이번에는 @RestControllerAdvice를 활용하여 예외처리를 해줄 것이다.
[Study] @RestContollerAdvice 란
[Study] @RestContollerAdvice 란
들어가기 앞서 .. 여태 스터디 프로젝트에서 진행하며 예외처리는 모두 try-catch를 이용해서 해왔다. 또한 학과 수업으로 배울때도 예외처리는 모두 try-catch를 사용했다. 이번 스터디를 진행하며
soohykeee.tistory.com
우선 예외처리를 위한 RestControllerAdvice를 작성해주기 전, 예외 응답 양식을 위한 ResponseErrorFormat 클래스를 생성해주었다. 예외 상태 코드, 메시지, 추가적으로 @Valid 어노테이션을 사용했을 때 발생하는 예외를 처리해주는 메서드까지 생성해주었다.
package kr.co.jshpetclinicstudy.infra.model;
@Getter
@Builder
@RequiredArgsConstructor
public class ResponseErrorFormat {
private final String message;
private final HttpStatus statusCode;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private final List<ValidationException> validationExceptions;
@Getter
@Builder
@RequiredArgsConstructor
public static class ValidationException {
private final String message;
private final String field;
public static ValidationException of(final FieldError fieldError) {
return ValidationException.builder()
.message(fieldError.getDefaultMessage())
.field(fieldError.getField())
.build();
}
}
}
이제 컨트롤러에서 발생하는 예외를 전역적으로 담당하는 클래스인 GlobalExceptionHandler를 생성해줬다. DuplicatedException, NotFoundException, RuntimeException을 처리하는 메서드를 생성해줬다.
package kr.co.jshpetclinicstudy.infra.handler;
//전역적으로 예외 처리를 담당하는 클래스
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(DuplicatedException.class)
protected ResponseEntity<ResponseErrorFormat> handleDuplicatedException(DuplicatedException e) {
log.warn("-------HandleDuplicatedException-------", e);
ResponseErrorFormat responseErrorFormat = ResponseErrorFormat.builder()
.message(e.getMessage())
.statusCode(ResponseStatus.FAIL_BAD_REQUEST.getStatusCode())
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseErrorFormat);
}
@ExceptionHandler(NotFoundException.class)
protected ResponseEntity<ResponseErrorFormat> handleNotFoundException(NotFoundException e) {
log.warn("-------HandleNotFoundException-------", e);
ResponseErrorFormat responseErrorFormat = ResponseErrorFormat.builder()
.message(e.getMessage())
.statusCode(ResponseStatus.FAIL_NOT_FOUND.getStatusCode())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(responseErrorFormat);
}
@ExceptionHandler(RuntimeException.class)
protected ResponseEntity<ResponseErrorFormat> handleRuntimeException(RuntimeException e) {
log.warn("-------HandleRuntimeException-------", e);
ResponseErrorFormat responseErrorFormat = ResponseErrorFormat.builder()
.message(e.getMessage())
.statusCode(ResponseStatus.FAIL_BAD_REQUEST.getStatusCode())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(responseErrorFormat);
}
}
위에서의 GlobalExceptionHandler에 ResponseEntityExceptionHandler를 상속받으면 예외처리를 해주지만, 이에 대해 우리가 직접 제어해주기 위해서 NotValidException이란 예외 핸들러 클래스를 커스텀해서 생성해줬다.
package kr.co.jshpetclinicstudy.infra.handler;
/*
검증 예외(ValidationException)에 대한 처리를 담당하는 클래스
스프링에선 스프링 예외를 미리 처리해둔 추상 클래스를 제공하는데 이 추상 클래스가ResponseEntityExceptionHandler
그래서, 위 예시에선 이것을 사용하지만 무엇인가 직관적이지 못하고, 내가 확실하게 제어를 하고 싶었기 떄문에,
수혁님이 말한것처럼 NotValidException이란 예외 핸들러 클래스를 커스텀해서 만든 것입니당
*/
@RequiredArgsConstructor
@Slf4j
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ResponseErrorFormat> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.warn("-------HandleMethodArgumentNotValidException-------", e);
final ResponseStatus responseStatus = ResponseStatus.FAIL_INVALID_PARAMETER;
return handleExceptionInternal(e, responseStatus);
}
private ResponseEntity<ResponseErrorFormat> handleExceptionInternal(final BindException e,
final ResponseStatus responseStatus) {
return ResponseEntity
.status(responseStatus.getStatusCode())
.body(makeResponseErrorFormat(e, responseStatus));
}
private ResponseErrorFormat makeResponseErrorFormat(final BindException e,
final ResponseStatus responseStatus) {
final List<ResponseErrorFormat.ValidationException> validationExceptions = e.getBindingResult()
.getFieldErrors()
.stream()
.map(ResponseErrorFormat.ValidationException::of)
.collect(Collectors.toList());
return ResponseErrorFormat.builder()
.message(responseStatus.getMessage())
.statusCode(responseStatus.getStatusCode())
.validationExceptions(validationExceptions)
.build();
}
}
이제 controller에서 try-catch문을 사용해주지 않고 아래처럼 작성해줘도, RestControllerAdivce에 의해서 예외 발생 시 예외 처리를 해주게 된다. 정말 예외 처리가 정상적으로 되는지 PostMan을 통해서 확인해보겠다.
package kr.co.jshpetclinicstudy.controller;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/owners")
public class OwnerController {
private final OwnerService ownerService;
/**
* Create Owner API
*
* @param create
* @return
*/
@PostMapping
public ResponseFormat<Void> createOwner(@RequestBody @Valid OwnerRequestDto.CREATE create){
ownerService.createOwner(create);
return ResponseFormat.success(ResponseStatus.SUCCESS_CREATE);
}
/**
* Read(Get) Owner API
*
* @param condition
* @return
*/
@PostMapping("/search")
public ResponseFormat<List<OwnerResponseDto.READ>> getOwnersByCondition(@RequestBody @Valid OwnerRequestDto.CONDITION condition) {
return ResponseFormat.successWithData(ResponseStatus.SUCCESS_OK, ownerService.getOwnersByCondition(condition));
}
/**
* Update Owner API
*
* @param update
* @return
*/
@PutMapping
public ResponseFormat<Void> updateOwner(@RequestBody @Valid OwnerRequestDto.UPDATE update) {
ownerService.updateOwner(update);
return ResponseFormat.success(ResponseStatus.SUCCESS_NO_CONTENT);
}
/**
* Delete Owner API
*
* @param ownerId
* @return
*/
@DeleteMapping("/{owner_id}")
public ResponseFormat<Void> deleteOwner(@PathVariable(name = "owner_id") Long ownerId) {
ownerService.deleteOwner(ownerId);
return ResponseFormat.success(ResponseStatus.SUCCESS_NO_CONTENT);
}
}
또한 추가적으로 더 자세한 에러코드를 아래처럼 각 entity별로 추가로 작성해주었다.
package kr.co.jshpetclinicstudy.infra.model;
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum ResponseStatus {
// Success Status
SUCCESS_OK("요청이 성공적으로 처리되었습니다.", HttpStatus.OK),
SUCCESS_CREATE("요청이 성공적으로 처리되어 새로운 리소스가 생성되었습니다.", HttpStatus.CREATED),
SUCCESS_ACCEPTED("요청이 성공적으로 처리되었지만, 결과가 아직 완료되지 않았습니다.", HttpStatus.ACCEPTED),
SUCCESS_NO_CONTENT("요청이 성공적으로 처리되었지만, 응답 데이터가 없습니다.", HttpStatus.NO_CONTENT),
// Failed Status
FAIL_BAD_REQUEST("클라이언트의 요청이 잘못되었습니다.", HttpStatus.BAD_REQUEST),
FAIL_UNAUTHORIZED("클라이언트가 인증되지 않았습니다.", HttpStatus.UNAUTHORIZED),
FAIL_FORBIDDEN("클라이언트가 요청한 리소스에 접근할 권한이 없습니다.", HttpStatus.FORBIDDEN),
FAIL_NOT_FOUND("클라이언트가 요청한 리소스를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
FAIL_METHOD_NOT_ALLOWED("클라이언트가 요청한 HTTP 메소드가 허용되지 않았습니다.", HttpStatus.METHOD_NOT_ALLOWED),
// Valid Failed Status
FAIL_INVALID_PARAMETER("파라미터 값이 유효하지 않습니다.", HttpStatus.BAD_REQUEST),
// Owner Failed Status
FAIL_OWNER_NOT_FOUND("클라이언트가 요청한 소유자를 찾을 수 업습니다.", HttpStatus.NOT_FOUND),
FAIL_TELEPHONE_DUPLICATED("클라이언트의 전화번호가 중복되었습니다.", HttpStatus.BAD_REQUEST),
// Pet Failed Status
FAIL_PET_NOT_FOUND("클라이언트가 요청한 반려동물을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
FAIL_TYPE_NOT_FOUND("클라이언트가 요청한 반려동물의 종류를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
// Vet Failed Status
FAIL_VET_NOT_FOUND("클라이언트가 요청한 수의사를 찾을 수 업습니다.", HttpStatus.NOT_FOUND),
// Specialty Failed Status
FAIL_SPECIALTY_NOT_FOUND("클라이언트가 요청한 전문학위를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
// Visit Failed Status
FAIL_VISIT_NOT_FOUND("클라이언트가 요청한 방문기록을 찾을 수 없습니다.", HttpStatus.NOT_FOUND);
private String message;
private HttpStatus statusCode;
}
Valid의 경우에도 예외처리가 잘 되는 것을 확인할 수 있다.