코드 품질 개선 기법 28편: 제약 조건에도 상속세가 발생한다 (새 탭에서 열림)
코드의 불변성을 보장하기 위해 설계된 클래스가 상속을 허용할 경우, 자식 클래스에서 해당 제약을 위반함으로써 시스템 전체의 안정성을 해칠 수 있습니다. 특히 `Immutable`이라는 이름을 가진 클래스가 가변적인 자식 클래스를 가질 수 있게 되면 개발자의 의도와 다른 런타임 동작이 발생할 위험이 큽니다. 따라서 특정 제약 조건을 강제하고 싶다면 클래스를 상속 불가능하게 설계하거나, 공통의 '읽기 전용' 인터페이스를 활용하는 구조적 접근이 필요합니다. ### 불변성 보장을 방해하는 상속 구조 * Kotlin의 `IntArray`를 래핑하여 성능과 불변성을 동시에 잡으려는 `ImmutableIntList` 예시를 통해 상속의 위험성을 설명합니다. * 클래스를 상속 가능(`open`)하게 설정하면, `Immutable`이라는 명칭에도 불구하고 이를 상속받아 내부 상태를 변경하는 `MutableIntList`와 같은 자식 클래스가 생성될 수 있습니다. * 외부에서는 `ImmutableIntList` 타입으로 참조하더라도 실제 인덱스 값이 변할 수 있는 객체를 다루게 되어, 불변성을 전제로 한 로직에서 오류가 발생합니다. ### 멤버 오버라이딩을 통한 제약 조건 우회 * 내부 데이터 구조를 `private`이나 `protected`로 보호하더라도, 메서드 오버라이딩을 통해 불변성 제약을 우회할 수 있습니다. * 예를 들어 `get` 연산자를 오버라이딩하여 내부 배열이 아닌 가변적인 외부 필드 값을 반환하도록 재정의하면, 클래스의 핵심 규약인 '불변 데이터 제공'이 깨지게 됩니다. * 범용적인 클래스일수록 예상치 못한 곳에서 잘못된 상속이 발생할 가능성이 높으므로, 어떤 멤버를 노출하고 오버라이딩을 허용할지 엄격하게 제한해야 합니다. ### 가변·불변 객체의 올바른 상속 관계 * 가변 객체가 불변 객체를 상속하면 불변성 제약이 깨지고, 불변 객체가 가변 객체를 상속하면 불필요한 변경 메서드(`add`, `set`)로 인해 런타임 에러가 발생할 수 있습니다. * 가장 이상적인 구조는 가변 객체와 불변 객체가 모두 '읽기 전용(Read-only)' 인터페이스나 클래스를 상속받는 형태입니다. * 가변 객체는 읽기 전용 부모의 메서드 집합을 확장하고, 불변 객체는 읽기 전용 부모의 제약 조건을 확장하는 방식(예: Kotlin의 `List` 구조)이 안전합니다. 특정 제약 조건(불변성 등)이 핵심인 클래스를 설계할 때는 기본적으로 상속을 금지(`final`)하고, 확장이 필요하다면 상속 대신 독립된 타입을 정의하거나 읽기 전용 인터페이스를 통한 계층 분리를 권장합니다.