한 줄 정의

예외는 API의 일부이므로, 표준 라이브러리에 상황을 나타내는 예외가 있다면 그것을 재사용하고 사용자 정의 예외는 대응하는 표준 예외가 없을 때만 정의해야 합니다.

쉽게 말하면

예외를 고르는 일은 도로 표지판 을 세우는 일입니다. 정지·양보·진입 금지 같은 표준 표지판은 처음 지나는 길에서도, 다른 나라 운전자라도 한눈에 알아봅니다. 어떤 동네가 “정지” 대신 자체 디자인한 표지판을 세우면 그 동네를 지나는 운전자마다 새로 배워야 하고, 잘못 읽고 지나칠 위험도 생깁니다.

자체 표지판이 정당한 순간은 딱 하나, 표준 표지판 목록에 그 상황이 아예 없을 때뿐입니다. IllegalArgumentException 을 본 개발자는 “내가 인수를 잘못 넘겼구나”를 바로 알지만, JsonParsingException 은 문서를 열어 봐야 합니다. 그래서 후자는 “JSON 형식 오류”라는 표준에 없는 상황을 위해서만 만듭니다.

왜 중요한가?

예외는 함수 시그니처만큼이나 API의 일부 입니다. 호출자는 어떤 예외가 날아오는지 알아야 catch 를 짜고 대응 흐름을 결정할 수 있습니다.

표준 예외는 이름과 명세가 이미 정립되어 있어 그 자체로 의미를 전달합니다. 잘 알려진 요소를 재사용하면 API를 처음 보는 사람도 별도 학습 없이 이해할 수 있고, 반대로 사용자 정의 예외는 그 의미와 발생 조건을 API마다 새로 익혀야 하는 비용을 호출자에게 떠넘깁니다.

핵심 내용

사용자 정의 예외가 정당한 경우

JSON 파싱 라이브러리에서 입력 형식이 올바르지 않은 상황은 표준 라이브러리에 대응하는 예외가 없습니다. 이럴 때만 사용자 정의 예외를 만듭니다.

inline fun <reified T> String.readObject(): T {
    // ...
    if (incorrectSign) {
        throw JsonParsingException()
    }
    // ...
    return result
}

Note

이 예시 자체도 “재사용” 원칙 위에 있습니다. JSON 파서처럼 이미 잘 테스트되고 문서화되고 최적화된 라이브러리가 있는 영역은 타당한 이유가 없는 한 직접 구현하지 않습니다.

재사용해야 할 표준 예외

예외나타내는 상황던지는 방법 · 전형적인 예
IllegalArgumentException메서드에 전달된 인수가 잘못됨require, requireNotNull
IllegalStateException프로그램 상태가 잘못됨 (초기화되지 않은 변수 사용 등)check, checkNotNull, error
UnsupportedOperationException선언된 메서드를 객체가 지원하지 않음표준 라이브러리의 TODO 함수, 인텔리제이 자동 생성 코드
IndexOutOfBoundsException인덱스 매개변수가 범위를 벗어남컬렉션·배열, ArrayList.get(Int)
ConcurrentModificationException동시 수정이 금지된 상황에서 동시 수정이 감지됨순회 중인 컬렉션 변경
NoSuchElementException요청한 요소가 존재하지 않음Iterator.next() 에 남은 요소가 없을 때

UnsupportedOperationException 은 던질 게 아니라 없애야 할 상황입니다

지원하지 않는 메서드가 클래스에 있다는 것 자체가 설계 문제입니다. 클라이언트가 자신이 쓰지 않는 메서드에 의존하게 만드는 인터페이스 분리 원칙(Interface Segregation Principle) 위반이므로, 이 예외를 던지기보다 그 메서드가 인터페이스에서 빠지도록 설계를 고쳐야 합니다.

TODO() 가 실제로 던지는 예외

코틀린 표준 라이브러리의 TODO() 는 UnsupportedOperationException 이 아니라 Error 를 상속한 NotImplementedError 를 던집니다. 미구현 상태를 나타낸다는 취지는 같지만 catch (e: Exception) 으로는 잡히지 않는다는 점이 다릅니다.

비교 / 트레이드오프

기준표준 예외사용자 정의 예외
의미 전달이름만으로 즉시문서나 코드를 읽어야
호출자의 학습 비용없음API마다 새로 발생
선택 시점상황에 맞는 표준 예외가 있을 때 (기본값)대응하는 표준 예외가 없을 때만

내 생각

  • HTTP 상태 코드와 같은 원리입니다. 404 대신 자체 코드를 만들지 않듯, 코드 내부 계약에서도 NoSuchElementException 이 있는데 ItemNotFoundException 을 새로 만드는 것은 호출자의 학습 비용만 늘립니다.
  • 다만 HTTP 응답 계약은 이 원칙의 경계 밖입니다. 클라이언트에 에러 코드와 메시지를 구조적으로 내려 줘야 하는 REST API에서는 도메인 예외와 에러 코드 체계가 필요한데, 이는 “표준 라이브러리에 대응하는 예외가 없는 상황”에 해당합니다. 원칙은 라이브러리·내부 모듈 계약에 강하게, API 응답 계층에서는 약하게 적용합니다.
  • 전역 핸들러에서 표준 예외를 먼저 매핑합니다. IllegalArgumentException 은 400, IllegalStateException 은 409, NoSuchElementException 은 404로 잡아 두면, 서비스 계층이 require · check 만으로도 의미 있는 응답을 내고 사용자 정의 예외는 정말 필요한 곳에만 남습니다.
  • UnsupportedOperationException 은 자바 Collections.unmodifiableList 의 교훈입니다. 읽기 전용인데 add 가 인터페이스에 남아 있어 호출하면 런타임에 터지는 구조가 전형적인 ISP 위반이고, 코틀린이 List 와 MutableList 를 타입으로 분리한 이유가 바로 이것입니다.

관련 개념