개발 초기엔 구현에만 급급하여 예외처리를 전혀 신경쓰지 못 했다. 그러나 개발이 점점 진행 될수록 프론트엔드 개발자를 위해 예외 API를 깔끔하게 전달 해줘야겠다는 생각이 들기 시작했다.
난 처음에 프로그램 내부의 예외 처리를 아래의 코드처럼 모두 IllegalArgumentException으로 처리했다.
Member findMember = memberRepository.findById(memberId)
.orElseThrow(() -> new IllegalArgumentException("Id not exist"));
그러나 이렇게 구현을 하니 해당 예외가 왜 발생했는지는 오직 나 혼자만 알 수 있었다. 백엔드를 혼자 구현하다보니 이런 예외처리가 문제라고 쉽게 생각하지 못 했다. 또한, 예외의 메시지를 타이핑으로 통일을 시켜야 했다.
그래서 두 번째 방법으로 아래의 코드처럼 예외 클래스를 하나씩 추가했다.
public class MemberNotFountException extends RuntimeException {
public MemberNotFountException() {
super();
}
public MemberNotFountException(String message) {
super(message);
}
public MemberNotFountException(String message, Throwable cause) {
super(message, cause);
}
public MemberNotFountException(Throwable cause) {
super(cause);
}
protected MemberNotFountException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
Member findMember = memberRepository.findById(memberId).orElseThrow(
() -> new MemberNotFountException("Member Not Exist")
);
확실히 IllegalArgumentException으로 처리하는 것 보단 해당 예외가 왜 발생했는지 조금 더 직관적으로 볼 수 있었지만 여전히 문제는 많이 남아있었다. "Member Not Exist" 라는 예외 메시지를 직접 입력해야 했다. 물론 static 변수 등으로 만들 수도 있지만 이는 IllegalArgumentException 을 사용할 때랑 크게 차이가 나지 않았다.
두 번째로 @RestControllerAdvice를 이용해 예외처리 하기에 너무 불편했다. 게시글을 못 찾았을 때, 댓글을 못 찾았을 때 등등 예외처리가 필요해 클래스를 만들어 나가니 아래의 코드처럼 exceptionHandler도 클래스 수 만큼 만들어야 했다.
@Slf4j
@RestControllerAdvice
public class NotFoundExceptionAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MemberNotFountException.class)
public ErrorResult memberNotFoundHandler(MemberNotFountException e) {
return new ErrorResult(HttpStatus.BAD_REQUEST, "회원이 존재하지 않습니다.", e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(PostNotFountException.class)
public ErrorResult postNotFoundHandler(PostNotFountException e) {
return new ErrorResult(HttpStatus.BAD_REQUEST, "게시글이 존재하지 않습니다.", e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CommentNotFountException.class)
public ErrorResult commentNotFoundHandler(CommentNotFountException e) {
return new ErrorResult(HttpStatus.BAD_REQUEST, "댓글이 존재하지 않습니다.", e.getMessage());
}
}
exceptionHandler를 하나씩 추가하니 이 방법이 매우 잘못됐다는 것을 깨달았다.
예외처리를 고민하던 중 TickerBell 프로젝트의 백엔드를 함께 개발하던 팀원분이 좋은 방법을 소개해주셨다. 우선 ErrorCode 라는 enum을 HttpStatus, errorMessage 필드와 함께 만든다.
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "회원이 존재하지 않습니다."),
MEMBER_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "이미 존재하는 아이디입니다.");
private final HttpStatus status;
private final String errorMessage;
}
이후 CustomException 객체에 RuntimeException을 상속하고 앞서 구현한 ErrorCode를 필드로 선언한다.
@Getter
public class CustomException extends RuntimeException {
private ErrorCode errorCode;
public CustomException(ErrorCode errorCode) {
super(errorCode.getErrorMessage());
this.errorCode = errorCode;
}
}
이렇게 구현을 하니 아래의 코드처럼 프로그램 로직 내에서도 매우 직관적으로 어떤 예외가 발생했는지 파악할 수 있게됐다.
Member findMember = memberRepository.findById(memberId).orElseThrow(
() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)
);
그리고 다른 예외를 추가하기도 쉬웠는데 ErroCode 라는 enum에 아래의 코드처럼 추가만 해주면 됐다.
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "회원이 존재하지 않습니다."),
MEMBER_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "이미 존재하는 아이디입니다."),
RECOMMEND_POST_NOT_FOUND(HttpStatus.BAD_REQUEST, "게시글 추천 정보가 존재하지 않습니다."),
COMMENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "댓글이 존재하지 않습니다");
//== 추가생략 ==//
private final HttpStatus status;
private final String errorMessage;
}
마지막으로 Advice를 적용하기에도 앞선 방식보다 매우 수월하였는데
@Slf4j
@RestControllerAdvice
public class CustomExceptionAdvice {
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResult> customExceptionHandler(CustomException e) {
ErrorResult errorResult = new ErrorResult(e.getErrorCode().getStatus(), e.getErrorCode().getErrorMessage());
return new ResponseEntity<>(errorResult, e.getErrorCode().getStatus());
}
}
@Data
@AllArgsConstructor
public class ErrorResult {
private HttpStatus status;
private String message;
}
ErrorCode가 status와 message를 가지고 있기 때문에 하나의 exceptionHandler를 이용해 모든 예외를 처리할 수 있게됐다.
이러한 방법을 통해 프로젝트의 예외를 깔끔하게 처리할 수 있게됐다.
'Project > 구현' 카테고리의 다른 글
| [Tickerbell] 테스트 시 S3, Redis 목킹하기 (1) | 2023.12.23 |
|---|---|
| [TickerBell] 로그인 필터에서 Controller로 변경한 이유 (0) | 2023.09.26 |