💻문제점
서비스에서 중복 회원이나, 비밀번호 불일치와 같은 문제 상황이 발생하면 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
'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 |