코드 품질 개선 기법 28편: 제약 조건에도 상속세가 발생한다 (새 탭에서 열림)

코드의 불변성을 보장하기 위해 설계된 클래스가 상속을 허용할 경우, 자식 클래스에서 해당 제약을 위반함으로써 시스템 전체의 안정성을 해칠 수 있습니다. 특히 Immutable이라는 이름을 가진 클래스가 가변적인 자식 클래스를 가질 수 있게 되면 개발자의 의도와 다른 런타임 동작이 발생할 위험이 큽니다. 따라서 특정 제약 조건을 강제하고 싶다면 클래스를 상속 불가능하게 설계하거나, 공통의 '읽기 전용' 인터페이스를 활용하는 구조적 접근이 필요합니다.

불변성 보장을 방해하는 상속 구조

  • Kotlin의 IntArray를 래핑하여 성능과 불변성을 동시에 잡으려는 ImmutableIntList 예시를 통해 상속의 위험성을 설명합니다.
  • 클래스를 상속 가능(open)하게 설정하면, Immutable이라는 명칭에도 불구하고 이를 상속받아 내부 상태를 변경하는 MutableIntList와 같은 자식 클래스가 생성될 수 있습니다.
  • 외부에서는 ImmutableIntList 타입으로 참조하더라도 실제 인덱스 값이 변할 수 있는 객체를 다루게 되어, 불변성을 전제로 한 로직에서 오류가 발생합니다.

멤버 오버라이딩을 통한 제약 조건 우회

  • 내부 데이터 구조를 private이나 protected로 보호하더라도, 메서드 오버라이딩을 통해 불변성 제약을 우회할 수 있습니다.
  • 예를 들어 get 연산자를 오버라이딩하여 내부 배열이 아닌 가변적인 외부 필드 값을 반환하도록 재정의하면, 클래스의 핵심 규약인 '불변 데이터 제공'이 깨지게 됩니다.
  • 범용적인 클래스일수록 예상치 못한 곳에서 잘못된 상속이 발생할 가능성이 높으므로, 어떤 멤버를 노출하고 오버라이딩을 허용할지 엄격하게 제한해야 합니다.

가변·불변 객체의 올바른 상속 관계

  • 가변 객체가 불변 객체를 상속하면 불변성 제약이 깨지고, 불변 객체가 가변 객체를 상속하면 불필요한 변경 메서드(add, set)로 인해 런타임 에러가 발생할 수 있습니다.
  • 가장 이상적인 구조는 가변 객체와 불변 객체가 모두 '읽기 전용(Read-only)' 인터페이스나 클래스를 상속받는 형태입니다.
  • 가변 객체는 읽기 전용 부모의 메서드 집합을 확장하고, 불변 객체는 읽기 전용 부모의 제약 조건을 확장하는 방식(예: Kotlin의 List 구조)이 안전합니다.

특정 제약 조건(불변성 등)이 핵심인 클래스를 설계할 때는 기본적으로 상속을 금지(final)하고, 확장이 필요하다면 상속 대신 독립된 타입을 정의하거나 읽기 전용 인터페이스를 통한 계층 분리를 권장합니다.