본문 바로가기
백엔드

[Spring Boot] 예외 처리 방법

by 육빔 2024. 9. 25.
728x90
반응형

프로젝트를 진행하던 중 예외처리를 해야되는 상황이 놓였다. 각 상황별로 에러코드, 메세지를 전부 다루어야 했다. 좀 효율적인 설계를 위해 자바에서 하는 예외처리 방법을  살펴봤다.

 

1. Controller 레벨 예외 처리 (@ExceptionHandler)

특정 컨트롤러에서 발생하는 예외를 개별적으로 처리하는 방법입니다.

@RestController
public class MyController {

    @GetMapping("/example")
    public String example() {
        throw new IllegalArgumentException("잘못된 요청입니다.");
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}

 

예전에 내가 이렇게 한 기억이 있다. 이렇게 하면 단점은 Controller코드가 더러워지고 한번에 관리하기 어려운 점이 있다.

 

2. 글로벌 예외 처리 (@ControllerAdvice)

애플리케이션 전역에서 발생하는 예외를 공통적으로 처리하는 방법입니다. 여러 컨트롤러에서 발생하는 예외를 하나의 클래스에서 처리할 수 있습니다.

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body("잘못된 요청입니다: " + e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("서버 오류: " + e.getMessage());
    }
}

 

@ControllerAdvice는 전역에서 예외를 처리할 수 있도록 도와주는 방식. 모든 컨트롤러에서 에러가 난 것을 IllegalArgumentException과 Exception을 각각 다른 방식으로 처리할 수 있습니다. 편하지만 상세하게 에러를 알려줄 수 없는 점에서 내가 원하는 코드가 아니라고 생각했다.

 

다른 사람들의 코드를 참고해 공부하니 거의 자바에서 예외처리를 계층구조로 나눠 처리하고 있었다. 생각해보니 계층구조로 하면 여러 장점이 있었다. 예외처리 순서는 아래와 같다.

 

  • 구체적인 하위 예외를 먼저 처리:
    • 예외 처리 핸들러 메서드에서 구체적인 하위 예외(예: FileNotFoundException, IOException, MethodArgumentNotValidException 등)를 먼저 처리해야 합니다.
    • 이를 통해 특정 예외에 대한 맞춤형 응답이나 로직을 실행할 수 있습니다.
  • 상위 예외 처리:
    • 그 다음에 일반적인 상위 예외를 처리합니다. 예를 들어, 모든 예외를 포괄하는 Exception 클래스나 RuntimeException 같은 상위 클래스 예외는 마지막에 처리해야 합니다.
    • 이 경우 구체적인 예외가 처리되지 않으면, 상위 예외 핸들러가 호출되어 기본적인 오류 응답을 반환하게 됩니다.

정리하면 크게 나누고 그 아래 상세한 에러를 좀 더 알려줄 수 있는 방법이다.

 

내가 본 많은 프로젝트에서 스프링에서의 예외처리는 위 2가지 방법을 섞어서 사용했다.

 

우선 다양하고 유연한 예외처리를 위해 상위 예외처리(ex: RuntimeException (이걸 받는 이유는 unchecked, checked))에서 상속받아 새로운 클래스 생성 ->

RestControllerAdvice에서 전역예외처리를 진행할때 상속받아 새로운 클래스로 예외처리 던지기 ->

각각의 도메인에 예외 클래스를 생성 후 새로운 클래스를 상속받아 자세한 예외 생성 ->

서비스계층에서 도메인예외로 throw

 

이와 같은 형태로 정리할 수 있을 것 같다.

 

 

 

public abstract class BaseException extends RuntimeException {
    public BaseException(){
    }

    public abstract BaseExceptionType exceptionType();
}

 

런타임 상속

 

public class ExceptionControllerAdvice {

    @ExceptionHandler(BaseException.class)
    ResponseEntity<ExceptionResponse> handleException(HttpServletRequest request, BaseException e) {
        BaseExceptionType type = e.exceptionType();
        log.info("잘못된 요청이 들어왔습니다. URI: {},  내용:  {}", request.getRequestURI(), type.errorMessage());
        return ResponseEntity.status(type.httpStatus())
                .body(new ExceptionResponse(type.errorMessage()));
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    ResponseEntity<ExceptionResponse> handleMissingParams(MissingServletRequestParameterException e) {
        String errorMessage = e.getParameterName() + " 값이 누락 되었습니다.";
        log.info("잘못된 요청이 들어왔습니다. 내용:  {}", errorMessage);
        return ResponseEntity.status(BAD_REQUEST)
                .body(new ExceptionResponse(errorMessage));
    }

    @ExceptionHandler(Exception.class)
    ResponseEntity<ExceptionResponse> handleException(HttpServletRequest request, Exception e) {
        log.error("예상하지 못한 예외가 발생했습니다. URI: {}, ", request.getRequestURI(), e);
        return ResponseEntity.internalServerError()
                .body(new ExceptionResponse("알 수 없는 오류가 발생했습니다."));
    }
}

 

전역처리

@RequiredArgsConstructor
public class DepartmentException extends BaseException {

    private final DepartmentExceptionType exceptionType;

    @Override
    public BaseExceptionType exceptionType() {
        return exceptionType;
    }
}

 

도메인 별 예외 생성

@RequiredArgsConstructor
public enum DepartmentExceptionType implements BaseExceptionType {
    NOT_FOUND_DEPARTMENT(NOT_FOUND, "부서을 찾을 수 없습니다"),
    ;

    private final HttpStatus httpStatus;
    private final String errorMessage;

    @Override
    public HttpStatus httpStatus() {
        return httpStatus;
    }

    @Override
    public String errorMessage() {
        return errorMessage;
    }
}

 

enum으로 상세 메세지 제공

private Department findDepartmentById(Long departmentId) {
        return departmentRepository.findById(departmentId)
                .orElseThrow(() -> new DepartmentException(NOT_FOUND_DEPARTMENT));
}

 

서비스 로직  적용

728x90
반응형