한 줄 정의
예외는 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를 타입으로 분리한 이유가 바로 이것입니다.
관련 개념
- 아이템 05 인수와 상태에 대한 기대치를 명시하라 — 표에서 가장 많이 쓰이는
IllegalArgumentException·IllegalStateException을 던지는 구체적 도구인require·check·error를 다룹니다 - 아이템 01 가변성을 제한하라 — 읽기 전용 컬렉션을 다운캐스팅하면
UnsupportedOperationException이 나는 이유와, 타입 분리가 그 상황을 설계 단계에서 없애는 방식을 다룹니다