예외 처리(Exception Handling) Part 3: [커스텀 예외]

Frog Lemon·2025년 10월 14일
0

Java

목록 보기
6/6
post-thumbnail

이전 글 💡throws, try-catch 에서는 Java의 기본 예외 처리 메커니즘과 예외 전파 방식에 대해 살펴보았다.

이번 글에서는 한 단계 더 나아가 커스텀 예외(Custom Exception)를 가 무엇인지, 올바른 커스텀 예외 생성 방법에 대하여 써보았다.


커스텀 예외(Custom Exception)

예외 처리를 학습하다 보면 한 가지 의문이 생긴다.

“표준 예외(IllegalArgumentException, IOException 등)만으로 충분할까?”
“우리 서비스의 도메인 로직을 반영한 의미 있는 예외는 어떻게 만들어야 할까?”

이 질문의 답을 찾는 과정에서 커스텀 예외(Custom Exception) 개념이 등장한다.

커스텀 예외가 필요한 이유

표준 예외는 대부분 일반적인 오류 상황을 표현한다.
예를 들어 IllegalArgumentException은 잘못된 인자를 의미하지만 “사용자 포인트가 부족하다”와 같은 도메인 고유의 상황을 담기에는 한계가 있다.

즉, 비즈니스 맥락(Context)을 명확히 전달하기 어렵다.

예시

구분표준 예외커스텀 예외
예외명IllegalArgumentExceptionInsufficientPointException
의미잘못된 인자포인트 부족으로 인한 결제 실패
맥락 반영 여부❌ 일반적✅ 도메인에 특화됨

이처럼 커스텀 예외를 통해 “어떤 문제인지”뿐만 아니라
“왜 발생했는지”를 코드 레벨에서 명확히 표현할 수 있다.

커스텀 예외 설계 기본 구조

커스텀 예외는 Throwable와 상속관계에 있는 클래스를 상속받아 사용한다. 일반적으로 RuntimeException 을 상속받아 정의한다.
이는 명시적인 try-catch 없이도 전파된다.

pblic class ExternalApiException extends RuntimeException {

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


커스텀 예외 Best Practice

필요한 예외를 직접 정의하는 것은 좋은 접근이다.
하지만 만든 예외의 이름이 "NoData", "GoodException", "BadSituation" 과 같다면 어떨까?

이런 이름의 예외가 애플리케이션 실행 중 발생한다면 문제가 ‘무엇 때문인지’ 파악하기가 매우 어려울 것이다.
예외 메시지를 보고도 원인을 유추할 수 없다면 그 예외는 사실상 의미 없는 신호에 불과하다.

단독으로 개발하는 상황이라면 그나마 감내할 수 있겠지만 프로젝트가 팀 단위로 진행되는 협업 환경이라면 이야기가 달라진다.
코드 리뷰나 디버깅 과정에서
“이 예외가 정확히 어떤 상황을 의미하는가?”라는 질문이 쏟아질 것이고 팀원들 사이에서 좋지 않은 인상을 줄 가능성이 높다.

즉, 예외 이름은 예외의 본질을 드러내야 한다.
“무엇이 잘못되었는가?”를 명확히 표현하지 못하는 예외는 오히려 문제 해결을 더디게 만든다.

이러한 상황을 방지하고 좋은 예외를 만드는 4가지 Best Practice를 살펴보자.

1. Always Provide a Benefit

항상 “이 예외를 통해 얻을 수 있는 정보”를 제공하자.

커스텀 예외는 단순히 오류를 알리는 용도가 아니다.
개발자에게 문제의 원인을 명확히 설명하고, 해결 방향을 제시해야 한다.

즉, “이 예외를 던짐으로써 팀원이 얻을 수 있는 이점(benefit)”이 있어야 한다.

아무런 이점도 제공할 수 없다면 UnsupportedOperationException 이나 IllegalArgumentException 과 같은 표준 예외 중 하나를 사용하는 것이 좋다 . 모든 Java 개발자는 이미 이러한 예외를 알고 있을 것이다. 이렇게 하면 코드와 API를 더 쉽게 이해할 수 있다.

예시

// ❌ 나쁜 예시
throw new NoDataException("데이터 없음");

// ✅ 좋은 예시
throw new UserNotFoundException("ID가 42인 사용자를 찾을 수 없습니다.");

좋은 예외는 에러 로그를 읽는 순간 무엇이, 왜, 어디서 발생했는지 즉시 이해할 수 있어야한다. 이는 디버깅 속도를 높이고 장애에 대응하는 시간을 단축시킨다.


2. Follow the Naming Convention

예외 클래스는 반드시 Exception으로 끝나야 한다.

명명 규칙을 통일하면 프로젝트 전체에서 예외를 식별하기 쉬워진다.
또한 IDE나 검색 도구를 사용할 때, Exception 접미사만으로 관련 클래스를 빠르게 찾을 수 있다.

실제로 Java에서 기본적으로 구현한 예외들을 살펴보면 모두 Exception으로 끝나는걸 확인할 수 있다.

❌ 잘못된 이름✅ 올바른 이름
UserNotFoundUserNotFoundException
ApiErrorExternalApiException
PointLackInsufficientPointException

3. Provide Javadoc Comments for Your Exception Class

예외의 목적과 사용 의도를 명확히 문서화하자.

커스텀 예외 클래스는 재사용될 가능성이 높다.
따라서 예외가 언제 발생하는지, 어떤 상황에서 던져지는지를 Javadoc 주석으로 명확히 기술하는 것이 중요하다.
이를 통해 팀원이나 후속 개발자가 예외의 용도를 이해하기 쉽다.

실제로 모잇지 서비스를 구현하면서 커스텀 예외 문서화의 중요성을 크게 느꼈다.
프론트파트 개발시 간헐적 API 에러가 발생했는데 Swagger를 활용하여 다양한 커스텀 예외를 문서화하여 프론트엔드와 협업할 때 예외 발생 상황과 의미를 명확히 전달할 수 있었고

그 결과 장애 대응을 빠르게 할 수 있었다.

/**
 * 외부 API 호출 중 예기치 않은 오류가 발생했을 때 던져진다.
 * 주로 네트워크 지연, 응답 파싱 실패, 인증 오류 등의 상황에서 사용된다.
 */
public class ExternalApiException extends RuntimeException {
    public ExternalApiException(String message) {
        super(message);
    }
}
   

4. Provider Constructor That Sets the Cause

원인 예외(cause)를 함께 전달하는 생성자를 제공하자.

커스텀 예외가 감싸는 내부 예외(예: IOException, SQLException)를 명시적으로 연결하면 전체 예외 흐름을 추적하기가 훨씬 쉬워진다.

public class ExternalApiException extends RuntimeException {

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

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

이 패턴을 사용하면 예외 스택 트레이스에 원인(cause) 이 함께 포함되어 로그 분석 및 장애 추적이 용이해진다.

커스텀 예외 체크리스트

항목체크
예외 이름이 Exception으로 끝나는가?
예외 이름이 발생 원인을 명확히 표현하는가?
Javadoc 주석으로 예외의 목적을 설명했는가?
원인 예외를 전달하는 생성자를 제공하는가?
표준 예외로 충분한지 검토했는가?
비즈니스 맥락을 반영하고 있는가?

이 표는 커스텀 예외를 설계할 때 확인해야 할 체크리스트이다. 각 항목을 검토하면서 좋은 커스텀 예외를 만들 수 있다.


마무리

커스텀 예외가 무엇인지, 어떻게 사용하는지 알아보았다.
가장 중요한 것은 커스텀예외를 통해 무엇을 전달할 것인지 명확히 해야한다.

예외 처리는 비용이 크다. 그렇기에 예외를 남발하기 보다는 꼭 필요한 부분에 구현하고 문제 상황에 따른 적절한 에외를 발생하는게 중요하다.

profile
도전하며 굴러가는 돌멩이, 인생 마라톤 중 😎

0개의 댓글