inheritance

3 개의 포스트

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

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

코드 품질 개선 기법 24편: 유산의 가치 (새 탭에서 열림)

코드에서 로직은 동일하고 단순히 값만 달라지는 경우, 인터페이스와 상속을 사용하는 대신 데이터 클래스와 인스턴스 생성을 활용하는 것이 더 효율적입니다. 상속은 동적 디스패치나 의존성 역전과 같은 특정한 목적이 있을 때 강력한 도구가 되지만, 단순한 값의 차이를 표현하기 위해 사용하면 불필요한 복잡성을 초래할 수 있습니다. 따라서 객체의 속성 값만 변경되는 상황이라면 상속 불가능한 클래스를 정의하고 값만 다른 인스턴스를 만들어 사용하는 것이 코드의 가독성과 예측 가능성을 높이는 지름길입니다. **상속이 과용되는 사례와 문제점** * UI 테마(색상, 아이콘 등)를 적용할 때 인터페이스를 정의하고 각 테마(Light, Dark)별로 클래스를 상속받아 구현하는 방식은 흔히 발견되는 과잉 설계의 예시입니다. * 바인딩 로직이 모든 테마에서 동일함에도 불구하고 상속을 사용하면, 각 하위 클래스가 자신만의 고유한 로직을 가질 수 있다는 가능성을 열어두게 되어 코드 추적을 어렵게 만듭니다. * 특히 단순히 속성 값을 반환하는 목적일 때는 인터페이스를 통한 동적 디스패치가 성능상 미미한 오버헤드를 발생시킬 뿐만 아니라 구조를 복잡하게 만듭니다. **상속을 사용해야 하는 정당한 경우** * **로직의 동적 변경:** 상황에 따라 실행 시점에 로직 자체를 갈아끼워야 하는 동적 디스패치가 필요할 때 사용합니다. * **합 타입(Sum Types) 구현:** Kotlin의 `sealed class`처럼 정해진 하위 타입들의 집합을 정의해야 할 때 유용합니다. * **추상화와 구현의 분리:** 프레임워크의 제약 조건 대응, 의존성 주입(DI), 또는 빌드 속도 향상을 위해 인터페이스가 필요할 때 활용합니다. * **의존성 역전 법칙(DIP) 적용:** 순환 의존성을 해결하거나 의존성 방향을 단방향으로 유지해야 할 때 상속과 인터페이스를 사용합니다. **인스턴스 기반 모델링의 이점** * 인터페이스 대신 모든 속성을 포함하는 단일 클래스(Model)를 정의하고, 각 테마를 이 클래스의 인스턴스로 생성하면 구조가 훨씬 간결해집니다. * Kotlin의 기본 클래스처럼 상속이 불가능한(`final`) 클래스로 정의할 경우, 해당 인스턴스에 고유한 로직이 포함되지 않았음을 보장할 수 있습니다. * 속성 값이 변하지 않는다면 각 인스턴스는 데이터의 묶음으로서만 존재하게 되어, 상태 변화나 로직의 부수 효과를 걱정할 필요 없이 안전하게 재사용할 수 있습니다. 단순히 값만 다른 여러 타입을 구현해야 한다면 먼저 "상속이 반드시 필요한 로직의 차이가 있는가?"를 자문해 보시기 바랍니다. 로직이 같다면 클래스 상속보다는 데이터 모델의 인스턴스를 생성하는 방식이 유지보수하기 훨씬 쉬운 코드를 만들어줍니다.

코드 품질 개선 기법 19편: 차일드 록 (새 탭에서 열림)

상속 구조에서 자식 클래스가 부모 클래스의 함수를 오버라이딩할 때 발생할 수 있는 결함을 방지하기 위해, 오버라이딩 가능한 범위를 최대한 제한해야 합니다. 부모 클래스의 공통 로직과 자식 클래스의 확장 로직을 분리하지 않으면 `super` 호출 누락이나 책임 범위의 혼선과 같은 버그가 발생하기 쉽습니다. 따라서 부모 클래스에서 전체적인 흐름을 제어하고 자식 클래스는 특정 지점의 로직만 구현하도록 설계하는 '차일드 록(child lock)' 기법이 필요합니다. **기존 오버라이딩 방식의 문제점** * **super 호출 누락의 위험:** 자식 클래스에서 부모의 기능을 실행하기 위해 `super.bind()`를 명시적으로 호출해야 하는 구조는 실수를 유발하기 쉽습니다. 호출을 잊더라도 컴파일 에러가 발생하지 않아 헤더나 푸터가 업데이트되지 않는 등의 버그가 방치될 수 있습니다. * **구현 강제성 부족:** 오버라이딩이 필수적인 상황임에도 불구하고 주석으로만 안내되어 있다면, 개발자가 실수로 구현을 누락할 가능성이 큽니다. * **책임 범위의 모호함:** 하나의 함수(`bind`)가 공통 로직과 개별 로직을 모두 포함하고 있으면 오버라이딩의 책임 범위를 오해하기 쉽고, 결과적으로 자식 클래스에 부적절한 코드가 포함될 수 있습니다. **차일드 록을 통한 구조 개선** * **공통 흐름의 고정:** 부모 클래스의 메인 함수(예: `bind`)에서 `open` 키워드를 제거하여 자식 클래스가 전체 흐름을 수정할 수 없도록 '록'을 겁니다. * **추상 메서드 분리:** 자식 클래스마다 달라져야 하는 로직만 별도의 `abstract` 메서드(예: `updateMessageList`)로 추출합니다. * **템플릿 메서드 패턴 적용:** 부모 클래스의 `bind` 함수에서 공통 로직(헤더/푸터 업데이트)을 실행한 후, 자식 클래스가 구현한 추상 메서드를 호출하는 방식으로 설계합니다. 이를 통해 자식 클래스는 부모의 로직을 신경 쓰지 않고 자신의 역할에만 집중할 수 있습니다. **견고한 상속 설계를 위한 가이드라인** * **super 호출 지양:** 라이프사이클 관리나 플랫폼 API의 제약이 있는 특수한 경우를 제외하고는, 자식 클래스에서 `super`를 호출해야만 기능이 완성되는 구조를 피해야 합니다. * **제어 흐름의 중앙 집중화:** 자식 클래스들이 공통으로 사용하는 함수의 실행 순서나 흐름은 반드시 부모 클래스에서 정의하고 관리해야 합니다. * **캡슐화 강화:** C++의 `private virtual` 기법처럼, 부모 클래스에서만 호출 가능하면서 자식 클래스에서 동작을 재정의할 수 있는 구조를 활용하여 오버라이딩 범위를 엄격하게 제한하는 것이 좋습니다. 상속을 설계할 때는 자식 클래스에게 과도한 자유를 주기보다, 필요한 부분만 안전하게 확장할 수 있도록 제약 장치를 마련하는 것이 시스템의 안정성을 높이는 핵심입니다. 이는 코드 리뷰 과정에서 발견하기 어려운 논리적 오류를 컴파일 단계나 구조적 제약으로 사전에 차단하는 효과를 줍니다.