Spring 어디부터 custom exception을 써야 할까?

justindevcode·2022년 10월 2일
0

Spring 어디부터 custom exception을 써야 할까?


intro

스프링을 연습하며 예외를 처리할때가 종종있는데 연습 코드에 어떤것은 IllegalStateException 같은 표준 예외처리를 어떤 코드는 NotEnouhStockException("need more stock")이런식의 custom exception사용하여

public class NotEnouhStockException extends RuntimeException{
    public NotEnouhStockException() {
        super();
    }

    public NotEnouhStockException(String message) {
        super(message);
    }

    public NotEnouhStockException(String message, Throwable cause) {
        super(message, cause);
    }

    public NotEnouhStockException(Throwable cause) {
        super(cause);
    }

}

이런식으로 RuntimeException을 상속받아 처리하였다. 일반적으로 java를 쓰면서 예외처리를 할일도 없기에 사람들이 Spring을 쓰면서 많이 마주하는 예외를 좀 알아보았다.


기본적인 try/catch/finally, throw

"예외"란 무언가 예외적인 상황이나 에러가 발생했음을 가리키는 신호입니다.
예외를 '발생시키다(throw)'라는 것은 그런 에러나 예외 상황을 알린다는 뜻입니다.
한편 예외를 '잡아내다(catch)'라는 것은 그것을 처리한다는 뜻입니다.(즉 그 예외에서 회복하기 위해 무언가 필요하거나 적절한 행동을 취한다는 뜻이다.)

런타임 에러가 일어날 때마다 예외를 발생시킵니다. 또한 프로그램에서 throw문을 사용하여 명시적으로 예외를 발생시킬 때에도 마찬가지로 예외를 발생시킵니다.
이렇듯 예외를 강제로 발생시켜야 경우가 생길 때는 throw 키워드를 사용합니다.
그리고 예외를 잡아내는 데에는 try/catch/finally문을 사용합니다.

그리고 이 예외의 종류는 2가지입니다.
1) Checked Exception

컴파일 단계에서 확일할 수 있는 예외.
컴파일 단계에서 처리해줘야한다.
Checked Exception이 발생한 메서드에서 try catch로 잡거나
throws를 이용하여 메서드 밖으로 던져야한다.

2) Unchecked Exception

컴파일 단계에서 확인할 수 없는 예외.
컴파일 단계에서 처리를 반드시 할 필요는 없다.

그리고 상황에 따라 예외처리의 방향이 있다.

유저의 이름을 넣으려는데 같은 사람이 이미 존재한다는 상황

public void add(User user) throws DuplicateUserIdException, SQLException {
    try {

    } catch (SQLException e) {
        if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
            throw new DuplicateUserIdException(e).initCause(e);
        } else {
            throw e;
        }
    }
}

이상황의 경우 기본적으로 SQLException라는 예외를 발생시키는데 이 예외는 어떻게든 SQL문이 잘 작동하지만 않으면 무조건 나오는 예외라서 catch에서 중복 유저명에의해 에외가 생긴것이면 DuplicateUserIdException(e)로 변경해주는 예외방식도 있다.


그럼 custom exception은?

기본적으로 표준 예외를 적극적으로 사용하자는 의견이 많은거같다.

public class UserNameEmptyException extends RuntimeException {
    public UserNameEmptyException(String message) {
    ...
    }
}

위 커스텀 예외의 이름만 봐도 사용자 이름의 입력값이 비어있는 경우 발생하는 예외임을 알 수 있다.
그러나, 단지 그 하나의 이유를 위해서 커스텀 예외를 만드는 것은 지나친 구현이다. 유효하지 않은 입력(인자)값에 대한 예외이므로, 자바에서 정의해 놓은 IllegalArgumentException을 사용하고 메시지만 예외사항에 맞게 재정의해준다면 충분히 그 의미를 파악할 수 있다.

또한 표준적인 예외는 그 이름을보고 언제 이 예외가 튀어나오는지 모든사람이 알수있다. 내가 커스텀한 이름의 예외는 언제 그예외가 뜨는지 다시 봐야한다.

물론 custom exception장점도있다.

반대로 이름을 잘 명시한다면 그 예외 이름만보고도 어떤 특별한 상황에서 예외가 나왔구나 하고 알수있는것이다.
PostNotFoundException이 발생했다면, Post를 찾는 요청을 보냈지만 해당 요소가 없다는 상황을 유추할 수 있을 것이다.

그리고 오류가 났을때 그 오류에 해당하는 값을 계산해서 도출해줄때 이런상황에서 custom exception이 유용할 수 있다.

public class IllegalIndexException extends IndexOutOfBoundsException {
	private static final String message = "범위를 벗어났습니다.";

	public IllegalIndexException(List<?> target, int index) {
		super(message + " size: "  + target.size() + " index: " + index);
	}
}

이렇게 만들어 두면 try/catch를 통해 발생한 예외를 붙잡아 새로 만든 예외를 던져도 되고, 기존 예외가 발생하기 전에 index를 검사해 새로 만든 예외를 직접 발생시켜도 괜찮다.

또한 코드가 복잡해지며 같은 오류가 발생할곳이 많아진다면??
따로따로 분리해서 예외이름을 정해주는것이 더 좋을수도있다.

또한 후술할 Spring에서는 ControllerAdvice를 통해서 전역적인 예외 처리가 가능하기에 나의 임의 대로 예외를 분류하여 카테고리처럼 묶어서 예외를 처리 할 수 있기다. 다만 이는 묶여있는 예외들중 어디서 예외가 터졌는지 찾는 불편함이 있긴하다.


Spring의 특별한 예외처리

스프링에서 예외처리는 크게 3가지로 나눌 수 있다.

  • 컨트롤러단에서 처리 Controller Level - @ExceptionHandler
  • 전역 처리 Global Level - @ControllerAdvice
  • 메서드단위 처리 Method Level - try/catch

3번째는 위에서 설명한 가장 기본적인 방식이고 위의 2가지를 조금 살펴보겠다.

Controller Level - @ExceptionHandler

스프링에서 Controller에서 발생하는 예외를 공통적으로 처리할 수 있는 기능을 제공한다.

@ExceptionHandler애노테이션을 통해 Controller의 메서드에서 throw된 Exception에 대한 공통적인 처리를 할 수 있다.

@RestController
public class TestController {

    private final Logger logger = LoggerFactory.getLogger(UserController.class);

    // 예외 핸들러
    @ExceptionHandler(value = TestException.class)
    public String controllerExceptionHandler(Exception e) {
        logger.error(e.getMessage());
        return "/error/404";
    }

    @GetMapping("hello1")
    public String hello1() {
        throw new TestException("hello1 에러 "); // 강제로 예외 발생
    }

    @GetMapping("hello2")
    public String hello2() {
        throw new TestException("hello2 에러 "); // 강제로 예외 발생
    }
}

이 예외는 TestController내에서 발생하는 TestException에 대해서 예외가 발생하면 controllerExceptionHandler메서드에서 모두 처리해준다.

Controller 메서드 내의 하위 서비스 (Service, Repository등등)에서 예외가 발생하더라도, 중간에 처리하지 않는 이상 Controller단까지 예외가 던져지게 되고 @ExceptionHandler가 예외를 처리하게 된다.

Checked Exception, Runtime Exception 상관 없이 Controller까지 예외를 throw하면 처리가 가능하다.

전역 처리 Global Level - @ControllerAdvice

만약 하나의 Controller말고 여러 Controller에서 발생하는 예외를 처리하려면 @ControllerAdvice를 사용해야 한다.

@ControllerAdvice : 모든 Controller에서 발생하는 예외를 처리할 수 있게 해주는 애노테이션DispatcherServlet에서 발생하는 예외를 전역적으로 처리해준다.

@RestControllerAdvice : @ControllerAdvice + @ResponseBody

@ControllerAdvice는 DispatcherServlet에서 발생하는 예외만 처리할 수 있다.
필터에서 발생하는 예외는 따로 처리해주지 않으면 처리가 불가하다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(value = TestException.class)
    public String testExceptionHandler(Exception e) {
        logger.error(e.getMessage());
        return "/error/404";
    }
}

Controller에서 발생하는 예외를 전역적으로 처리해준다.
Controller의 @ExceptionHandler와 ControllerAdvice의 @ExceptionHandler중 높은 우선순위는?
Controller의 @ExceptionHandler가 먼저다.


추신

닥치기전까지는 아무생각이없었는데 예외처리도 미리 보니깐 처리해야할 경우의수가 예상이 안간다. 상황에따라 올바른 방향으로 처리를하는 예외를 적재적소에 또 때에 따라 Controller Level에 전역 Global Level에 해야한다

공부하면 할수록 너무너무 깊다. 수백개의 어디서 튀어나올지 모르는 class를 전부 파악해서 최적화 하고 효율적으로 설계를 해야한다.

이 글로 예외에 대한 정보를 한번 둘러보긴한거지만 삽질할거도 분명한데
오늘도 사실 jwt 대한 글을 쓰려고 찾아보다 이건 당장 빠르게 이해할만한 내용이 아닌거같아서 접었는데 갈길이멀다.


참고사이트

profile
("Hello World!");

0개의 댓글