예외 처리

김영훈·2024년 3월 4일

Area

목록 보기
3/5
post-thumbnail

PR 리뷰 중 이런 코멘트가 달렸다.

질문:

  @Transactional
    public void createTeam(CreateTeamRequest request) {
        if (teamRepository.existsByName(request.getName())) throw new IllegalArgumentException("존재하는 팀입니다.");

📌 단순히 IllegalArgumentException을 사용하지 말고 커스텀 Exception을 만들어서 어느 예외인지 눈에 띄면 좋을 것 같습니다!


목표:

💡 스프링 부트에서 예외를 처리하는 방법에 대해 알아보고, 커스텀 Exception을 만들어보자

  • 예외를 처리하는 두 가지 방법
  1. DefaultErrorAttruibutes 를 상속받아 구현
@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {

	@Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options){
    Map<String, Object> result = super.getErrorAttributes(webRequest, options);
    result.put("customValue", "Hello, World!");
    return result;
    }
}

📌 임의 키 값이 추가된 포맷의 에러 메세지

{
	"timestamp": "2024-03-04T12:16:17.999+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/api/articles/333",
    "customValue": "Hello, World!"
}
  1. 에러 메세지용 객체를 별도로 만든다.

01. ErrorCode를 관리할 enum을 만든다.

@Getter
public enum ErrorCode {
    INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "E1", "올바르지 않은 입력값입니다."),
    METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "E2", "잘못된 HTTP 메서드를 호출했습니다."),
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E3", "서버 에러가 발생했습니다."),
    NOT_FOUND(HttpStatus.NOT_FOUND, "E4", "존재하지 않는 대상입니다."),
    MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M1", "해당 멤버는 존재하지 않습니다."),
    TEAM_NOT_FOUND(HttpStatus.NOT_FOUND, "T1", "해당 팀은 존재하지 않습니다."),
    TEAM_ALREADY_EXISTS(HttpStatus.NOT_FOUND, "T2", "이미 동일한 이름의 팀이 존재하고 있습니다.")
    ;


    private final HttpStatus status;
    private final String code;
    private final String message;

    ErrorCode(HttpStatus status, String code, String message) {
        this.message = message;
        this.code = code;
        this.status = status;
    }
}    
    

02. ErrorResponse를 생성한다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {

    private String Message;
    private String code;

    public ErrorResponse(final ErrorCode code) {
        this.Message = code.getMessage();
        this.code = code.getCode();
    }
    public ErrorResponse(final ErrorCode code, final String message) {
        this.Message = message;
        this.code = code.getCode();
    }
    public static ErrorResponse of(final ErrorCode code) {
        return new ErrorResponse(code);
    }
    public static ErrorResponse of(final ErrorCode code, final String message) {
        return new ErrorResponse(code, message);
    }
}

💡 ErrprAttributes를 대체할 에러 메시지용 객체.
에러 메세지가 포함된 message 필드와 고유 에러 코드인 code를 가지고 있다.


03. RuntimeException을 상속받는 CustomBaseException 클래스를 생성한다.

public class CustomBaseException extends RuntimeException{
    private final ErrorCode errorCode;

    public CustomBaseException(String message, ErrorCode errorCode) {
        super(message);
        this.errorCode = errorCode;
    }
    public CustomBaseException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }
    public ErrorCode getErrorCode(){
        return errorCode;
    }
}

💡 CustomBaseException 클래스는 해당 멤버를 조회했을 때 존재하지 않거나, 팀을 생성하려하는데 이미 동일한 이름의 팀이 존재하거나, 팀을 조회했는데 해당 팀이 존재하지 않는 경우 등등
비즈니스 로직을 작성하다 발생하는 예외를 모아둘 최상위 클래스이다.


04. CustomBaseException을 상속받는 예외 클래스들을 생성한다.

public class NotFoundException extends CustomBaseException{
    public NotFoundException(ErrorCode errorCode){
        super(errorCode.getMessage(), errorCode);
    }
    public NotFoundException(){
        super(ErrorCode.NOT_FOUND);
    }
}
public class TeamAlreadyExistsException extends NotFoundException{
    public TeamAlreadyExistsException() {
        super(ErrorCode.TEAM_ALREADY_EXISTS);
    }
}
public class TeamNotFoundException extends NotFoundException{
    public TeamNotFoundException() {
        super(ErrorCode.TEAM_NOT_FOUND);
    }
}
public class MemberNotFoundException extends NotFoundException{
    public MemberNotFoundException() {
        super(ErrorCode.MEMBER_NOT_FOUND);
    }
}

💡 TeamAlreadyExistsException, TeamNotFoundException, MemberNotFoundException 모두
NotFoundException을 상속받고 있다.
만약 (MemberNotFoundException::new); 를 선언한다면,
ErrorCode(enum) -> MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M1", "해당 멤버는 존재하지 않습니다.")
값을 super을 통해 상위 클래스의 생성자로 전달하게 된다.


05. GlobalExceptionHandler을 생성한다.

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    protected ResponseEntity<ErrorResponse> handle(HttpRequestMethodNotSupportedException e){
        log.error("HttpRequestMethodNotSupportedException", e);
        return createErrorResponse(ErrorCode.METHOD_NOT_ALLOWED);
    }

    @ExceptionHandler(CustomBaseException.class)
    protected ResponseEntity<ErrorResponse> handle(CustomBaseException e){
        log.error("CustomBaseException", e);
        return createErrorResponse(e.getErrorCode());
    }

    @ExceptionHandler(Exception.class)
    protected ResponseEntity<ErrorResponse> handle(Exception e){
        e.printStackTrace();
        log.error("Exception", e);
        return createErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR);
    }

    private ResponseEntity<ErrorResponse> createErrorResponse(ErrorCode errorCode){
        return new ResponseEntity<>(
                ErrorResponse.of(errorCode),
                errorCode.getStatus());
    }
}

💡 @ControllerAdvice를 샤용한 예외 처리 핸들러.
모든 컨트롤러에서 발생하는 예외를 중앙에서 한꺼번에 처리할 수 있다.


결과:

💡 설정한 커스텀 Exception 대로 에러메세지가 출력된다.


0개의 댓글