Java

Java: 커스텀 예외 사용에 대한 생각

teo_99 2023. 3. 16. 20:38

표준 예외 & 커스텀 예외란 

표준 예외, 커스텀 예외란 무엇일까?

 

표준 예외는 쉽게 말해, JDK가 제공하는 예외 클래스들을 의미한다.

커스텀 예외는 개발자가 직접 표준 예외를 커스텀 해(상속 받아) 만든 예외를 의미한다.

 

그렇지만 둘은 완전히 다른 개념이 아니다.

두 종류의 예외 모두 '예외 상황의 문맥을 제공'한다는 같은 목적을 갖고, '어떻게 예외 상황의 문맥을 제공할 수 있는지'의 방법만 다르다.

 

그렇다면 언제 무엇을 사용할지에 대한 생각을 안해볼 수 없을 것 같다.

주관적인 내용이 많이 포함되어 있는 글임을 미리 밝힙니다.

커스텀 예외 사용 시 어떤 이득을 취할 수 있는가

커스텀 예외를 사용하는 경우, 다음과 같은 이득이 존재할 수 있다.

 

도메인 집약적인 표현이 가능하다

예외는 도메인의 정보일까, 아닐까?

앞서 예외는 '예외 상황의 문맥을 제공'한다는 목적을 갖는다고 했다.

그렇다면 예외는 도메인 예외 상황의 문맥을 제공하므로, 예외 또한 도메인의 한 부분이 아닐까? 라고 생각해볼 수 있다.

 

자동차 경주 게임을 생각해보자.

우리가 만든 자동차 경주 게임 세상에서는, 자동차 이름은 5글자를 넘을 수 없다.

 

아래는 해당 제약조건을 가진 자동차를 실체화한 Car 클래스다.

class Car {

    private final String name;
    
    public Car(String name) {
    	validateName(name);
    	this.name = name;
    }
    
    private void validateName(String name) {
    	if (name.length() > 5) {
        	throw new IllegalArgumentException("자동차 이름은 5글자를 넘을 수 없어요.");
        }
    }
}

예외는 어떤 문맥을 담고 있는가?
위 예제에서 IllegalArgumentException은 '자동차의 이름이 5글자 이상이다'라는 문맥을 담고 있다.

 

하지만 IllegalArgumentException을 catch하는 쪽에서는 어떤 문맥에서 예외가 발생했는지 메세지를 통해서만 알 수 있다.

즉, 도메인적인 예외 문맥을 오로지 String으로만 표현할 수 있다는 것이다. 

 

모든 표준 예외가 이렇다. 메세지를 통해 문맥을 전달하는 방법 밖에 없다.

엥, IndexOutOfBoundsException은 index도 넣을 수 있는데요?

IndexOutOfBoundsException도 마찬가지다. 내부적으로는 생성자에서 정수형의 index를 String 타입의 메세지로 변환한다.

 

반면 아래처럼 커스텀 예외를 사용하면 어떨까?

class Car {

    private final String name;
    
    public Car(String name) {
    	validateName(name);
    	this.name = name;
    }
    
    private void validateName(String name) {
    	if (name.length() > 5) {
        	throw new CarNameLengthExceedException();
        }
    }
}

// CarNameLengthExceedException
public CarNameLengthExceedException extends RuntimeException {

	public CarNameLengthExceedException() {
    	super("자동차 이름은 5글자를 넘을 수 없어요.");
  	}
}

이제는 예외 문맥까지도 리터럴이 아닌 객체로 다룰 수 있게 되었다.

이처럼 커스텀 예외를 사용하면 보다 도메인 집약적인 표현이 가능하고, 예외 문맥까지도 하나의 객체로 관리할 수 있다.

 

상세한 예외 문맥을 제공할 수 있다

예외는 예외 문맥을 제공하기 위해 존재한다.

그런 관점에서 보면 커스텀 예외는 완벽하게 역할을 수행하는 객체가 아닌가?

 

앞서 코드에서 소개한 자동차 경주 코드를 보다 상세한 예외 문맥을 제공하도록 개선해보겠다.

 

class Car {

    private static final NAME_LENGTH_LIMIT = 5;
    
    private final String name;
    
    public Car(String name) {
    	validateName(name);
    	this.name = name;
    }
    
    private void validateName(String name) {
    	if (name.length() > NAME_LENGTH_LIMIT) {
        	throw new CarNameLengthExceedException(NAME_LENGTH_LIMIT, name.length());
        }
    }
}

// CarNameLengthExceedException
public CarNameLengthExceedException extends RuntimeException {
	
    public CarNameLengthExceedException(int limit, int actual) {
    	super(String.format("자동차 이름은 %d글자를 넘을 수 없어요.. 그런데 %d글자네요!", limit, actual));
    }
}

위처럼 구성하면 보다 상세한 문맥 기술이 가능하다.

자동차 이름에 대해 어떤 제약조건이 적용되고 있는지, 실제로 문제를 일으킨 값은 어떤 값인지까지 나타낼 수 있다.

 

이런 접근방식으로 보면, '예외 문맥을 제공한다' 라는 기준에서는 이보다 완벽한 객체가 있을까? 라는 생각이 든다.

 


반대로 커스텀 예외는 어떤 단점을 가지는가

예외 객체 또한 관리의 대상이다

예외 객체가 도메인의 성질을 띠기 때문에, 예외 객체 또한 관리의 대상이 된다.

즉, 비즈니스 로직이나 도메인의 변경이 예외 객체에게 영향을 미친다.

 

위 예제에서 자동차 이름에 대한 규칙이 변경된다면 어떻게 될까?

만약 자동차 이름에 대한 제약조건이 사라졌다면?

 

예외 객체까지 관리되어야 한다.

 

도메인 객체를 잘 짜야 하는 이유는 변경을 격리하기 위함이다.

하지만 예외 객체를 따로 정의하는 순간부터는 고정적인 의존성이 하나 더 생기기에 문제가 될 수 있다.

 

혹여나 도메인 로직은 수정되었지만, 예외 객체가 이를 따라가지 못한다면 문제가 된다.

 

커뮤니케이션 오버헤드가 발생한다

표준이 중요한 이유는 무엇인가?

 

통용되는 정보를 제공할 수 있기 때문이다.

그런 측면에서 표준 예외를 사용하는 경우 이점을 가질 수 있다.

코드를 읽는 사람으로 하여금 쉽게 어떠한 맥락을 전달하는지 유추하게 하는 것이 가능하다.

 

반면 커스텀 예외를 사용하면 예외 객체의 구현까지 알아야 한다는 문제가 생긴다.

 


무엇을 선택할 것인가

결국은 trade-off의 영역이다.

표준 예외, 커스텀 예외 중 무엇을 사용할지에 대한 정답은 없다.

다만 장단점을 이해하고 상황에 맞춰서 사용할 수만 있으면 된다고 생각한다.

 

커스텀 예외든, 표준 예외든 중요한 것은 '예외 상황의 문맥을 제공해야 한다는 것'이다.

 

이러한 본질을 잊지 않고, 합당한 이유만 있다면 무엇을 사용하든 괜찮지 않을까?

혹은 팀 컨벤션을 따르는 것도 이유가 될 것 같다.