본문으로 건너뛰기
개발 머꼬
개발 노트Java
hohyeon.dev15

체크 예외를 남발했다가 인터페이스가 계속 깨진 이유

  • #API Design
  • #Engineering Note
  • #Exception Handling
  • #Java

문제 발생

인터페이스 메서드에 새로운 체크 예외를 하나 추가했더니, 그 인터페이스를 구현한 수십 개 클래스와 호출부 전부가 컴파일 에러를 내며 연쇄적으로 수정이 필요해졌습니다.

public interface PaymentGateway {
    void charge(Order order) throws PaymentDeclinedException; // 새로 추가
}
// 이 인터페이스를 구현한 모든 클래스, 호출하는 모든 코드가 영향받음

원인 분석

Java는 예외를 두 종류로 나눕니다.

  • 체크 예외(checked, Exception을 상속하되 RuntimeException은 아님): 컴파일러가 호출부에서 반드시 try/catch로 처리하거나 throws로 다시 선언하도록 강제합니다.
  • 언체크 예외(unchecked, RuntimeException과 그 하위): 이런 강제가 없습니다. 처리하지 않아도 컴파일됩니다.

체크 예외는 "이 예외는 호출한 쪽이 반드시 인지하고 대응해야 한다"는 의도로 설계됐지만, 실무에서는 다음 문제가 반복적으로 발생합니다.

  1. API 변경의 전파 범위가 커짐: 인터페이스 메서드의 throws 목록을 바꾸면 그 인터페이스를 구현한 모든 곳, 호출하는 모든 곳이 함께 바뀌어야 합니다.
  2. 의미 없는 예외 삼키기: 처리할 방법이 없는 체크 예외를 억지로 catch해야 하는 상황에서, 개발자가 그냥 빈 catch 블록이나 e.printStackTrace()만 남기고 넘어가는 경우가 흔합니다 — 오히려 예외를 진짜로 처리하는 대신 형식적으로만 통과시키게 됩니다.
  3. 람다/스트림과 궁합이 나쁨: Stream.map() 같은 함수형 인터페이스는 체크 예외를 던지는 람다를 직접 받지 못해, 우회를 위한 보일러플레이트가 필요해집니다.

해결 방안

  1. 정말로 호출부가 복구 가능한 상황(예: 파일이 없을 때 다른 경로를 시도, 재시도 로직)에만 체크 예외를 씁니다.
  2. 호출부가 사실상 복구할 수 없는 상황(설정 오류, 프로그래밍 실수, 외부 서비스가 예상 못한 응답을 준 경우 등)은 언체크 예외(RuntimeException 상속)로 만듭니다 — Spring 자체가 이 원칙을 따라 대부분의 예외를 RuntimeException 계열로 설계했습니다.
  3. 라이브러리/공개 API를 설계할 때는 체크 예외를 추가하는 것이 API의 breaking change라는 것을 인지하고 신중하게 결정합니다.
  4. 이미 있는 체크 예외를 억지로 캐치만 하고 아무 처리도 안 하는 코드가 있다면, 그건 애초에 언체크 예외였어야 할 신호일 수 있습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.