Chef.Yeon
Code Cook
Chef.Yeon
전체 방문자
오늘
어제
  • 분류 전체보기 (230)
    • 게임 개발 (1)
      • Unity (1)
    • Android (27)
      • Kotlin (19)
      • 우아한테크코스 5기 (4)
    • Language (11)
      • 파이썬 (3)
      • Java (7)
    • DB (2)
      • SQL (16)
    • Spring (25)
    • 코딩테스트 (56)
    • Git (1)
    • TIL (85)
    • DevOps (6)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 코틀린 인 액션
  • 안드로이드
  • webflux
  • Docker
  • 다이나믹 프로그래밍
  • 코딩테스트
  • til
  • 문자열
  • 프리코스
  • 우아한테크코스
  • 레포지토리
  • 내림차순
  • elasticsearch
  • 에라토스테네스의 체
  • Android
  • spring
  • kibana
  • Wil
  • 프로그래머스
  • 파이썬
  • java
  • ec2
  • enum
  • MariaDB
  • kotlin
  • SQL
  • 코틀린
  • grafana
  • 백준
  • rsocket

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Chef.Yeon

Code Cook

[TIL - 20230420] ControllerAdvice, ExceptionHandler 전역 예외 처리
TIL

[TIL - 20230420] ControllerAdvice, ExceptionHandler 전역 예외 처리

2023. 4. 20. 23:04

 

💻문제점

서비스에서 중복 회원이나, 비밀번호 불일치와 같은 문제 상황이 발생하면 throw 를 통해 에러를 던졌다.

단순히 에러를 던지기만 하면 클라이언트에서는 제대로된 응답을 받지 못하기 때문에 이 에러를 어떻게 처리할까 고민했다. 더불어 @Valid를 사용해 회원가입시 필드에 대한 유효성 검사를 할 때, 유효하지 않을 경우 발생하는 예외는 어떻게 처리할지 문제였다.


📃시도

다음과 같이 Comment, Post, Member에 대해 CustomException을 만들어서 각각의 에러 상황에 대해 해당 예외를 던졌다.CustomException을 만들때는 RuntimeException을 상속하도록 하여 메서드에 throws를 작성하지 않도록 했다.

public class MemberException extends RuntimeException {
    public MemberException() {
        super();
    }

    public MemberException(String message) {
        super(message);
    }

    public MemberException(String message, Throwable cause) {
        super(message, cause);
    }

    public MemberException(Throwable cause) {
        super(cause);
    }
}
//작성자 일치 여부 판단
private void isPostAuthor(Member member, Post post) {
    if (!post.getMember().getUsername().equals(member.getUsername())) {
        if (member.isAdmin()) return;
        throw new MemberException(ExceptionMessage.NO_AUTHORIZATION_EXCEPTION.getMessage());
 }

 

Controller 측에서 단순하게 try-catch문을 사용하여 해당 에러를 잡으려고 했으나, 각 Service 코드에서 발생하는 MemberExeption, PostException, CommentException을 여러 catch문을 통해 잡는게 맞나라는 의문이 들었다. 그래서 최상위 예외인 Exception을 사용했다. 할거면 각각 catch를 해주어야 한다고 생각하지만 코드의 가독성이 좋지 않았다. try-catch문을 모든 메서드에 작성하는 것 또한 반복적인 작업으로 비효율적이라고 생각했고, 더 효율적인 방법이 있을 것이라고 생각했다.

@RestController
public class MemberController {

    @PostMapping("/signup")
    public ResponseEntity signup(@Valid @RequestBody SignupRequestDTO signupRequestDto) {
        ResponseEntity responseEntity;
        try {
            responseEntity = memberService.signup(signupRequestDto);
        } catch (Exception e) {
            BasicResponseDTO basicResponseDTO = BasicResponseDTO.setBadRequest(e.getMessage());
            responseEntity = new ResponseEntity(basicResponseDTO, HttpStatus.BAD_REQUEST);
        }
        return responseEntity;
    }

    @PostMapping("/login")
    public ResponseEntity login(
            @RequestBody LoginRequestDTO loginRequestDto,
            HttpServletResponse response
    ) {
        ResponseEntity responseEntity;
        try {
            responseEntity = memberService.login(loginRequestDto, response);
        } catch (Exception e) {
            BasicResponseDTO basicResponseDTO = BasicResponseDTO.setBadRequest(e.getMessage());
            responseEntity = new ResponseEntity(basicResponseDTO, HttpStatus.BAD_REQUEST);
        }
        return responseEntity;
    }
}

🔍해결

예전에 강의로 공통 관심 사항에 대한 처리를 위해 AOP를 사용한 적이 있는데, 왜 떠올리지 못했나 싶다.

공통 예외처리는 AOP라는 관점에서 @ControllerAdvice 애노테이션을 사용하여 구현할 수 있다.

@ControllerAdvice를 통해 Controller 계층 전역에서 발생하는 에러를 캐치하도록 하고, @ExceptionHandler를 통해 어떤 에러를 잡을지 명시했다.

@Valid 를 통해 유효성 검사에 실패하면 발생하는 MethodArgumentNotValidException 에러를 잡아 메서드 내에서 처리하도록 하고, MemberException, PostException, CommentExceptin을 모두 잡아 처리하는 메서드를 생성했다.

@ControllerAdvice
public class ExceptionAdvisor {

    /**
     * 회원가입 Valid 예외
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String signValidException(MethodArgumentNotValidException exception) {
        BindingResult bindingResult = exception.getBindingResult();

        StringBuilder builder = new StringBuilder();

        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            builder.append("[");
            builder.append(fieldError.getField());
            builder.append("](은)는 ");
            builder.append(fieldError.getDefaultMessage() + "\n");
        }

        return builder.toString();
    }

    /**
     * MemberException, PostException, CommentException 예외
     */
    @ExceptionHandler({MemberException.class, PostException.class, CommentException.class})
    public ResponseEntity exceptionHandler(Exception exception) {
        String message = exception.getMessage();
        BasicResponseDto basicResponseDTO = BasicResponseDto.setBadRequest(message);
        return new ResponseEntity(basicResponseDTO, HttpStatus.BAD_REQUEST);
    }
}

 

이후, Custom예외도 사용할 수 있지만, Java 표준 예외를 최대한 활용하는 것이 좋다는 것을 알게 되었다.

커스텀 예외의 의도는 자바 표준 예외로 표현할 수 없는 정보나 기능을 제공하는 것이기 때문에 표준 예외로 변경하였다.

표준 예외를 사용하면 다른 사람이 코드를 이해하기도 쉬워지고 가독성도 좋아진다.

 

Valid 예외 처리 로직도 ResponseEntity를 반환하도록 변경했다.

@ControllerAdvice
public class ExceptionAdvisor {

    /**
     * Valid 예외
     */
@ExceptionHandler({MethodArgumentNotValidException.class})
    public ResponseEntity signValidException(MethodArgumentNotValidException exception) {
        BindingResult bindingResult = exception.getBindingResult();

        Map<String, String> errorMap = new HashMap<>();

        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
        }

        ResponseDto responseDto = ResponseDto.setBadRequest("valid error", errorMap);
        return ResponseEntity.badRequest().body(responseDto);
    }

    /**
     *  예외
     */
    @ExceptionHandler({NoSuchElementException.class, IllegalArgumentException.class})
    public ResponseEntity exceptionHandler(Exception exception) {
        String message = exception.getMessage();
        ResponseDto responseDto = ResponseDto.setBadRequest(message);
        return new ResponseEntity(responseDto, HttpStatus.BAD_REQUEST);
    }
}

💡알게 된 점

AOP 관점에서 예외처리를 전역에서 할 수 있는 방법을 알 수 있었다.

@Valid 와 전역 예외처리에 관련한 내용은 아래 포스팅에 따로 정리했다.

 

https://yeon-dev.tistory.com/158

 

[Spring] @Valid를 사용한 객체 유효성 검증

구성은 다음과 같다. MemberController @RestController @RequestMapping("member") @RequiredArgsConstructor public class MemberController { private final MemberService memberService; @PostMapping("/signup") public ResponseEntity signup(@Valid @RequestB

yeon-dev.tistory.com

https://yeon-dev.tistory.com/161

 

[Spring] @Valid @ControllerAdvice와 @ExceptionHandler 사용한 전역 예외 처리

@ControllerAdvice @ControllerAdvice란 Controller 계층 전역에서 발생하는 에러를 잡아 처리해주는 애노테이션이다. @ExceptionHandler @ExceptionHandler는 Controller로 등록된 빈에서 발생하는 에러를 메서드에서 처

yeon-dev.tistory.com

728x90

'TIL' 카테고리의 다른 글

[TIL - 20230427] message properties 사용한 @Valid 메시지 처리  (0) 2023.04.27
[TIL - 20230426] Enum 타입 필드 검증을 위한 커스텀 애노테이션 생성  (0) 2023.04.26
[TIL - 20230419] JWT  (0) 2023.04.19
[TIL - 20230418] JWT  (0) 2023.04.18
[TIL - 20230415]  (0) 2023.04.15
    'TIL' 카테고리의 다른 글
    • [TIL - 20230427] message properties 사용한 @Valid 메시지 처리
    • [TIL - 20230426] Enum 타입 필드 검증을 위한 커스텀 애노테이션 생성
    • [TIL - 20230419] JWT
    • [TIL - 20230418] JWT
    Chef.Yeon
    Chef.Yeon
    보기 좋고 깔끔한 코드를 요리하기 위해 노력하고 있습니다.

    티스토리툴바