본문 바로가기

Project/구현

[Independe] 예외처리 방법

개발 초기엔 구현에만 급급하여 예외처리를 전혀 신경쓰지 못 했다. 그러나 개발이 점점 진행 될수록 프론트엔드 개발자를 위해 예외 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를 이용해 모든 예외를 처리할 수 있게됐다.

 

이러한 방법을 통해 프로젝트의 예외를 깔끔하게 처리할 수 있게됐다.