Java Spring Boot(6)

제이 용·2025년 11월 14일
post-thumbnail

어제 예외처리를 일부로 throw IllegarArgument를 통해 전부 처리를 해주었다.

바로 오늘 전역예외처리 를 진행하려고 하기에 여태껏 예외를 발견하고 처리하는 것을 해왔다고 볼 수 있다.

저번에 도전기능과제에 커스텀예외클래스와 전역예외처리 클래스를 통해 예외처리를 해보아라 라는 명세가 있었는데, 마지막에 있는만큼 나한테는 꽤나 시간이 오래걸리는 작업이었다.

  • 첫번째로 눈에 보이는 예외와 보이지 않는 예외들이 너무 방대해 내가 잘 처리하고 있는지 단계별 잔실수는 없는지 발목을 잡는 요소가 많았다.

  • 두번째로 예외처리를 하는데 커스텀 예외 클래스와 전역처리에 대한 원리를 잘 몰랐다.

  • 세번째로 구글링을 통해 찾아보면 대부분의 사람들이 Enum을 활용하여 생성하는 모습을 보이고 있는데 Enum을 잘 다루지 못했던 내가 보기에는 굉장히 어려워 보였다.

다만 운이 좋게도 예외처리 관련된 강의가 있었고, 예외처리 단계로 넘어가는 날 담당튜너님께서 예외처리에 대한 세션도 다뤄주셨기에 개념과 어떤역할을 하는지 등등 여러모로 이해가 얼추되어 구현할 수 있게 되었다.


전역처리 예외 클래스 적용

1. 파일 생성

  • 전역처리 예외클래스를 생성하기 위해 필요한 파일들이 여러개 이기에 담아둘 패키지를 미리 생성해준다.

2. 클래스 생성

ErrorResponse dto 생성

  • 커스텀한 클래스를 통해 오류코드를 body에도 표시하기 위한 ErrorResponse dto를 생성해준다.
@Getter
@Builder
public class ErrorResponse {
    private int status; //상태 숫자 코드를 보여줄 필드
    private String error; // 에러코드를 보여줄 필드
    private String message; // 부연 설명을 위한 메세지 필드
}

튜터님의 세션을 통해 각 회사별로 오류코드를 응답시키는 통일된 틀이 있다는 것을 알게 되었고 접목시키려고 노력하였다.


CustomException 클래스 생성

@Getter
public class CustomException extends RuntimeException{ // RuntimeException 상속
	//속성
    private final ErrorCode errorCode;
    
	//생성자
    public CustomException(ErrorCode errorCode) { 
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }
}
  • 문득 생각이 든 것이 커스텀 예외는 왜 런타임을 상속을 받아야될까가 궁금했다.

  • 결론(3가지)

  1. Checked Exception을 피할 수 있음
    커스텀 예외를 정의할 때 RuntimeException을 상속받으면 언체크 예외가 됩니다. 이는 호출자가 예외를 처리하도록 강제받지 않기 때문에 코드가 더 간결해질 수 있습니다.

  2. 사용자 편의성
    개발자가 특정한 상황에서 발생하는 예외를 명확하게 정의할 수 있어, 코드의 가독성을 높이고 예외 처리 로직을 간단하게 유지할 수 있습니다. 불필요한 예외 처리를 피할 수 있는 장점이 있습니다.

  3. 비즈니스 로직과의 일관성
    대부분의 비즈니스 로직에서 발생하는 예외는 런타임 예외로 간주되므로, 커스텀 예외도 이를 따르는 것이 자연스럽습니다.

  • 다음과 같이 Custom 클래스를 하나만 만들어준 이유는 가독성과 무분별한 파일생성을 막기 위해 만들었다.

  • 왜냐하면 Enum class를 활용해서 내가 커스텀한 예외들을 관리할 것이기 때문이다.


ErrorCode enum 클래스 생성

@Getter
public enum ErrorCode {
	//관리할 상수화시킬 필드
    USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다."),
    USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."),
    USER_FORBIDDEN(HttpStatus.FORBIDDEN, "해당 유저는 권한이 없습니다."),
    EMAIL_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "이미 존재하는 이메일입니다."),
.
.
.
;
	//속성
    private HttpStatus status;
    private String message;
	//생성자
    ErrorCode(HttpStatus status, String message) {
        this.status = status;
        this.message = message;
    }
}
  • Enum 또한 클래스 이기 때문에 속성과 생성자를 통해 데이터를 받아올 수 있게 설계를 해주어야하고 특별한 점은 상수화시킬 데이터들을 가지고 있다는 것이다.

Enum을 써야하는 이유(중요)

  • 우리가 프로젝트를 하다보면 소규모인 프로젝트임에도 불구하고 여러개의 예외들을 전부 처리해줘야하는 상황이 나온다.
  • 다만 이걸 throw로만 처리를 해버리면 응답에 서버에러인 500에러가 뜰 수 밖에 없고, 정확한 에러가 난 지점을 유추하기가 힘들어진다.
  • 하지만 커스텀 예외를 통해 내가 이 에러를 어떤 문구로 설명을 할 것이고, 상태코드 또한 정의해줄 수 있기 때문에 커스텀 예외를 사용하게 된다.
  • 다만, 그러한 커스텀 예외들이 여러개가 된다면 클래스가 많아질 것이고, 구분하기도 어려워지는 상태가 된다.
  • 따라서 커스텀 예외 클래스 자체를 Enum을 필드로 가져옴으로써 클래스를 따로 생성하지 않고 Enum에서 관리를 하게 된다면 가독성과 유지보수하기가 굉장히 용이해진다는 장점이 있다!

GlobalExceptionHandler 클래스 생성(전역처리 클래스)

@RestControllerAdvice
public class GlobalExceptionHandler {

    //커스텀 예외처리
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
        ErrorCode errorCode = e.getErrorCode();

        ErrorResponse errorResponse = ErrorResponse.builder()
                .status(errorCode.getStatus().value())
                .error(errorCode.getStatus().name())
                .message(errorCode.getMessage())
                .build();

        return ResponseEntity.status(errorCode.getStatus()).body(errorResponse);
    }

    // @Valid 실패 처리 (Validation 예외)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex
    ) {
        String message = "유효성 검사에 실패하였습니다.";
        if(ex.getBindingResult().getFieldError() != null ) {}
        message = ex.getBindingResult().getFieldError().getDefaultMessage();

        ErrorResponse response = ErrorResponse.builder()
                .status(400)
                .error("BAD_REQUEST")
                .message(message)
                .build();

        return ResponseEntity.badRequest().body(response);

    }

    // 나머지 예외 서버에러 처리
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception ex) {
        ErrorResponse response = ErrorResponse.builder()
                .status(500)
                .error("INTERNAL_SERVER_ERROR")
                .message(ex.getMessage())
                .build();

        return ResponseEntity.internalServerError().body(response);
    }

}
  • 전체 프로젝트에 예외를 처리할 @RestControllerAdvice 어노테이션
  • 해당 에러가 난 클래스에 예외를 처리하는 @ExceptionHandler(.class)
  • 위에 얘기한 필수적인 어노테이션을 제외하고는 기본적으로 컨트롤러에 API로직을 생성하는 틀이랑 기본적으로 비슷하게 생긴 것을 볼 수 있다.
    @어노테이션
    public ResponseEntity<DTO> handleException(Exception ex) {
        Dto dto = Dto.builder()
                .필드값들(),
                .build();

        return ResponseEntity.status.body(response);
    }
  • 이후 서비스와 컨트롤러에 내가 커스텀한 예외들을 적용시켜 바꿔주기만하면 정상적으로 실행되는 것을 볼 수 있다.
new IllegalArgumentException()

-> new CustomException(ErrorCode.이넘안 상수코드)

예시

new IllegalArgumentException("존재하지 않는 유저입니다.")

-> new CustomException(ErrorCode.USER_NOT_FOUND))

USER_NOT_FOUND = USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다.")

저번에는 Enum을 사용하지 않고 커스텀 클래스를 하나하나 작성했는데 정말 편리하다는 것을 느꼈고,

초반 과제에서 어거지로 사용해보라는 이유를 조금은 알 수 있었던 파트였지 않았나 싶다.

결국에는 쓰이는 곳이 있고 알고 있어야지만 응용할 수 있는 것이고, 조금이라도 만져본 사람은 얼추

느낌만 알고 있을 지언정 이러한 과정을 통해 지식을 확고하게 다질 수 있게 되는 것 같다.

끝내지 못한 숙제를 할 수 있게된 날이었기에 속이 좀 시원한 날이었다.

0개의 댓글