커스텀 예외가 필요 할까?

Alex·2024년 9월 3일

Binder프로젝트

목록 보기
2/18

예외는 개발자들이 처리할 수 있는 예외가 있고
그렇지 않은 예외가 있다.

지금 고민하는 DTO에서 잘못된 값이 왔을 때는 흔히 말하는 '개발자가 처리할 수 있는 예외'다.
null값을 허용하지 않는 곳에 null이 들어왔다면?
NullPointerException이 터지도록 해서 null값이 사용되지 못하도록 막아야 한다.
쉽게 생각하면 개발자가 부주의해서 발생할 수 있는 경우를 막는 방어책같은 것이다.

만약, Enum으로 정해진 값만 받아야 하는데 잘못된 값이 들어왔다면?

이 경우에는 클라이언트에게 잘못된 값이 왔다고 알려주면 된다.
사실 이걸 어떻게 살려보려고 하는 거 자체가 큰 의미는 없다고 본다.
멀티 스레드 환경에서는 스레드가 계속 작업을 요청할텐데 예외가 터졌다고 그걸 하나씩 다 복구하는 것보다는 그냥 그 작업을 중단하고 예외가 터졌다는 것만 클라이언트로 메시지를 보내주는 게 좋다고 들었다.

Custom 예외는 필요하지 않다.

참고 : custom exception을 언제 써야 할까?

평소에는 자바의 예외를 쓰지 않고 커스텀 예외를 만들어서 썼다.
그 이유는 클래스 이름만으로 예외의 의미를 명확하게 전달할 수 있어서다.

UserNameEmptyException같은 커스텀 예외를 만들면 직관적으로 의미를 이해할 수 있다. 다만, IllegalArgumentException에서 정의한 예외에 메세지만 추가해도 돼서 사실 커스텀 예외가 필수는 아니라는 의견도 있다.

또한, 표준 예외를 사용하면 가독성이 높아질 수도 있다. IllegalArgumentException(부적절한 인자), IllegalStateException(적절하지 않은 상태의 객체), UnsupportedOperationException(요청받은 작업을 지원하지 않음) 이런 것들은 많은 개발자에게 익숙한 것이라서 가독성이 높을수밖에 없다.

낯선 예외를 만났을 땐, 당연하게도 그 커스텀 익셉션을 파악하는 작업이 따라온다. 이 또한 비용이 될 수 있다.

표준 예외에 대한 쓰임은 공식문서를 참고하면 된다.

Custom 예외는 필요하다.

다만, 커스텀 예외는 이름만으로도 정보 전달이 가능하다는 점에서 사용 가치가 있을 수 있다. NoSuchElementException만으로는 어던 요소가 없는지 알 수 없다. PostNotFoundException는 Post를 찾는 요청을 보냈는데 해당 요소가 없다는 상황을 바로 알 수 있게 해준다. 다만, 메시지로도 가능하다는 점을 분명하다.

추가로, 상세한 예외 정보를 제공할 수 있다는 이점이 있다.

컬렉션의 범위를 벗어난 index 접근 요청이 생겼다고 해보자.

기존 예외에선 IllegalArgumentException이나 IndexOutOfBoundsException을 후보로 생각해볼 수 있을 것이다. 예외 메시지로는 "범위를 벗어났습니다." 정도면 적당하다.

하지만 전체 범위가 얼마인지, 요청한 index가 몇인지 파악하기 위해서는 프로그래머가 직접 디버깅하거나 정보를 담은 메시지를 만들어줘야 한다.

이런 상황에서 사용자 정의 예외는 좋은 해결책이 될 수 있다.

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

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

이렇게 하면 요청 받은 컬렉션의 최대 범위가 어디까지인지, 요청한 인덱스는 몇인지 바로 알 수 있다.

또한, 예외 발생 후처리가 용이하기도 하다.
Spring에서는 ControllerAdvice를 통해서 전역적인 예외 처리가 가능하다.

예외는 상속 관계에 있기 때문에, Exception이나 RuntimeException을 잡아두면 프로그램 내에서 발생하는 거의 모든 예외에 대해 처리가 가능하다. 하지만 프로그래머가 의도하지 않은 예외까지 모두 잡아내 혼란을 야기할 수 있다. 발생 위치를 정확하게 파악하기 힘들다는 단점이다.

// in GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgumentException(final IllegalArgumentException error) {
        // ...
    }
    // ...
}
// in SomeController.java
@Controller
public class SomeController {
    // ...
    @PostMapping("/some")
    public ResponseEntity<Void> Some(@RequestBody SomeRequest request) {
        Something something = someService.someMethod(request);
        if (somevalidate(something)) {
            throw new IllegalArgumentException();
        }

        SomeExternalLibrary.doSomething(something);

        return ResponseEntity.ok().build();
    }
    // ...
}

IllegalArgumentException이 발생하고 잡아냈다면 정말 some()안에서 발생했다고 장담할 수 있을까?
SomeExternalLibrary라는 외부 라이브러리에서 발생시켰을지도 모르고, 프레임워크 자체에서 발생시켰을지도 모른다.
Adivce에서 일괄적인 처리를 하고 싶어도 발생 장소에 따라 처리 방법이 달라질 수 있다.

사용자 정의 예외를 사용하면 이런 혼란스러움을 줄일 수 있다.

게다가 5. 예외 생성 비용을 절감한다.
자바에서 예외를 생성하는 행위는 생각보다 많은 비용이 소모된다. 바로 stack trace 때문이다.

-->실제로 배포서버에서 스택트레이스가 계속 쌓여서 OOM이 발생했던 적이 있다.

stack trace는 예외 발생 시 call stack에 있는 메소드 리스트를 저장한다.
이를 통해 예외가 발생한 정확한 위치를 파악할 수 있다. 하지만 try/catch나 Advice를 통해 예외를 처리한다면 해당 예외의 stack trace는 사용하지 않을 때가 많다.
비용을 들여 만들었지만 사용하지 않고 사라지는 형태. 너무나도 비효율적이다.

stack trace의 생성은 예외의 부모 클래스 중 Throwable의 fillInStackTrace()메소드를 통해 이루어진다. 사용자 정의 예외는 해당 메소드를 Override 함으로 stack trace의 생성 비용을 줄일 수 있다. 필요하다면 짧게 일부만을 생성할 수도, 아예 생성하지 않을 수도 있다.

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글