체크 예외를 남발했다가 인터페이스가 계속 깨진 이유
문제 발생
인터페이스 메서드에 새로운 체크 예외를 하나 추가했더니, 그 인터페이스를 구현한 수십 개 클래스와 호출부 전부가 컴파일 에러를 내며 연쇄적으로 수정이 필요해졌습니다.
public interface PaymentGateway {
void charge(Order order) throws PaymentDeclinedException; // 새로 추가
}
// 이 인터페이스를 구현한 모든 클래스, 호출하는 모든 코드가 영향받음원인 분석
Java는 예외를 두 종류로 나눕니다.
- 체크 예외(checked,
Exception을 상속하되RuntimeException은 아님): 컴파일러가 호출부에서 반드시try/catch로 처리하거나throws로 다시 선언하도록 강제합니다. - 언체크 예외(unchecked,
RuntimeException과 그 하위): 이런 강제가 없습니다. 처리하지 않아도 컴파일됩니다.
체크 예외는 "이 예외는 호출한 쪽이 반드시 인지하고 대응해야 한다"는 의도로 설계됐지만, 실무에서는 다음 문제가 반복적으로 발생합니다.
- API 변경의 전파 범위가 커짐: 인터페이스 메서드의
throws목록을 바꾸면 그 인터페이스를 구현한 모든 곳, 호출하는 모든 곳이 함께 바뀌어야 합니다. - 의미 없는 예외 삼키기: 처리할 방법이 없는 체크 예외를 억지로
catch해야 하는 상황에서, 개발자가 그냥 빈catch블록이나e.printStackTrace()만 남기고 넘어가는 경우가 흔합니다 — 오히려 예외를 진짜로 처리하는 대신 형식적으로만 통과시키게 됩니다. - 람다/스트림과 궁합이 나쁨:
Stream.map()같은 함수형 인터페이스는 체크 예외를 던지는 람다를 직접 받지 못해, 우회를 위한 보일러플레이트가 필요해집니다.
해결 방안
- 정말로 호출부가 복구 가능한 상황(예: 파일이 없을 때 다른 경로를 시도, 재시도 로직)에만 체크 예외를 씁니다.
- 호출부가 사실상 복구할 수 없는 상황(설정 오류, 프로그래밍 실수, 외부 서비스가 예상 못한 응답을 준 경우 등)은 언체크 예외(
RuntimeException상속)로 만듭니다 — Spring 자체가 이 원칙을 따라 대부분의 예외를RuntimeException계열로 설계했습니다. - 라이브러리/공개 API를 설계할 때는 체크 예외를 추가하는 것이 API의 breaking change라는 것을 인지하고 신중하게 결정합니다.
- 이미 있는 체크 예외를 억지로 캐치만 하고 아무 처리도 안 하는 코드가 있다면, 그건 애초에 언체크 예외였어야 할 신호일 수 있습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.