[Study-8, 9주차] @RestControllerAdvice 활용 Exception + 동적쿼리 적용 및 고찰 + N+1 문제에 대한 고찰 - ②

2023. 5. 21. 17:29· Study/Pet-Clinic-Project
728x90

 

[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은 조금 심화 공부가 필요하다고 봄
    1. 클래스 이름이랑 파라미터 정보 찍기
    2. 실행시간 로그 찍는거
    3. 내부 메소드 어떻게 할 지?
  • 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의 경우에도 예외처리가 잘 되는 것을 확인할 수 있다.

 

 


728x90
저작자표시 (새창열림)
'Study/Pet-Clinic-Project' 카테고리의 다른 글
  • [Study-12, 13주차] Spring Security, JWT + 회원가입, 로그인 - ①
  • [Study-10, 11주차] SpringSecurity 적용
  • [Study-8, 9주차] @RestControllerAdvice 활용 Exception + 동적쿼리 적용 및 고찰 + N+1 문제에 대한 고찰 - ①
  • [Study-6, 7주차] AOP + QueryDSL + (N+1) 문제 및 RequestDTO 수정 + JPA Auditing 사용 (regDate, modDate) - ②
soohykeee
soohykeee
Computer Science. 2017~2023 / Java, Spring, Backend
soohykeee
Coding_
soohykeee
전체
오늘
어제
  • 분류 전체보기
    • 회고
    • Info
      • 개념 정리
      • 정보
    • Study
      • Pet-Clinic-Project
      • Concept
    • Inflearn
      • 스프링 핵심 원리_기본편
      • Git
    • 코드로 배우는 스프링 부트 웹 프로젝트
      • Guestbook
      • MovieReview
      • Security & API

블로그 메뉴

  • 홈
  • 방명록
  • Github

인기 글

최근 댓글

최근 글

250x250
hELLO · Designed By 정상우.v4.2.1
soohykeee
[Study-8, 9주차] @RestControllerAdvice 활용 Exception + 동적쿼리 적용 및 고찰 + N+1 문제에 대한 고찰 - ②
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.