Skip to content

오브젝트, CHAPTER 10 상속과 코드 재사용, 2024-09-22 #466

Description

@jongfeel

CHAPTER 10 상속과 코드 재사용

01 상속과 중복 코드

중복 코드는 동료를 의심하게 만든다.
중복 코드를 제거해야할 결정적인 이유는 따로 있다.

DRY 원칙

신뢰할 수 있고 수정하기 쉬운 소프트웨어를 만드는 효과적인 방법 중 하나는 중복을 제거하는 것이다.
앤드류 헌트와 데이비드 토마스는 DRY 원칙을 얘기했다.
Don't Repeat Yourself의 뜻으로 동일한 지식을 중복하지 말라는 뜻이다.

중복과 변경

같은 일을 하지만 조금은 다른 로직의 처리는 중복 코드를 생성하는 것이다.
중복을 제거하기 위해 타입 코드를 쓰긴 하지만 그것도 문제는 존재한다.

상속을 이용해서 중복 코드 제거하기

상속을 염두에 두고 설계되지 않은 클래스를 상속을 이용해 재사용하는 것은 쉬운 일이 아니다.
재사용을 위해 상속 계층 사이에 많은 가정을 세워야 한다.
그리고 그 갖어은 코드를 이해하기 어렵게 만들고 직관에도 어긋날 수 있다.

상속은 결합도를 높인다.
상속이 초래하는 부모 클래스와 자식 클래스 사이의 강한 결합은 코드를 수정하기 어렵게 만든다.

강하게 결합된 Phone과 NightlyDiscountPhone

사실 중복 코드 제거를 위해 상속을 사용하지만
세금을 부과하는 로직을 추가했을 때 Phone도 수정하고 NightDiscountPhone도 수정해야 한다.
이는 코드 중복을 제거하기 위해 상속을 사용해도 새로운 중복 코드가 추가되는 걸 의미한다.

이런 문제가 발생하는 이유는 NightlyDiscountPhone이 Phone의 구현에 너무 강하게 결합돼 있기 때문이다.
상속을 할 때는 아래 경고에 귀를 귀울일 필요가 있다.

상속을 위한 경고1
자식 클래스의 메서드 안에서 super 참조를 이용해 부모 클래스의 메서드를 직접 호출할 경우 두 클래스는 강하게 결합된다.
super 호출을 제거할 수 있는 방법을 찾아 결합도를 제거하라.

상속 관계로 연결된 자식 클래스가 부모 클래스의 변경에 취약해지는 현상을 가리켜 취약한 기반 클래스 문제라고 부른다.

02 취약한 기반 클래스 문제

부모 클래스 변경에 의해 자식 클래스가 영향을 받는 현상을
취약한 기반 클래스 문제(Fragile Base Class Problem, Brittle Base Class problem)[Holub04]라고 부른다.
이 문제는 상속을 사용하면 피할 수 없는 객체지향 프로그래밍의 근본적인 취약성이다.

불필요한 인터페이스 상속 문제

자바 초기 버전에 상속을 잘못 사용한 예로 java.util.Properties와 java.util.Stack이다.
두 클래스의 공통점은 부모 클래스에서 상속받은 메서드를 사용할 경우 자식 클래스의 규칙이 위반된다는 것이다.

자바 초기 컬렉션 프레임워크 개발자들은 vector를 통해 get, add, remove 메서드를 만들었고
이를 재사용하기 위해 stack을 vector의 자식 클래스로 구현했다.

이렇게 하면 Stack이 Vector의 퍼블릭 인터페이스를 사용할 수 있으므로
임의의 위치에서 요소를 추가하거나 삭제할 수 있다.
원래 Stack의 규칙을 위반하는 것이다.

퍼블릭 인터페이스에 대한 고려 없이 단순히 코드 재사용을 위해 상속을 이용하는 것이 얼마나 위험한지 보여주는 예시이다.
객체지향의 핵심은 객체들의 협력이다.
단순히 코드를 재사용하기 위해 불필요한 오퍼레이션이 인터페이스에 스며들도록 방치해서는 안 된다.

상속을 사용할 때 알아둬야 하는 두 번째 주의사항은 다음과 같다.

상속을 위한 경고 2
상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨트릴 수 있다.

메서드 오버라이딩의 오작용 문제

조슈아 블로치(Joshua Bloch)는 < 이펙티브 자바 > [Bloch08]에서 HashSet의 구현에 강하게 결합된 InstrumentedHashSet 클래스를 소개한다.
InstrumentedHashSet은 HashSet의 내부에 저장된 요소의 수를 셀 수 있는 기능을 추가한 클래스로 HashSet의 자식 클래스로 구현돼 있다.

메서드 오버라이드를 통해 add, addAll에서 addCount를 추가하는 로직을 추가했지만
부작용으로 인해 addAll을 할 때 list의 요소를 3개를 넣어서 호출하면 내부의 addCount의 값이 6이 됐다.
이유는 HashSet의 addAll은 내부적으로 add를 3번 호출하므로 addCount가 3이 더 추가되기 때문이다.

상속을 위한 경고 3
자식 클래스가 부모 클래스의 메서드를 오버라이딩 할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다.

조슈아 블로치는 클래스가 상속되기를 원하면 상속을 위해 클래스를 설계하고 문서화해야 하며,
그렇지 않은 경우에는 상속을 금지시켜야 한다고 주장한다.

하지만 이는 내부 구현을 문서화하라는 것이므로
객체지향의 핵심인 구현을 캡슐화하라는 것에 위배된다.

설계는 트레이드오프이다.
상속은 코드 재사용을 위해 캡슐화를 희생한다.
완벽한 캡슐화를 원하면 코드 재사용을 포기하거나 상속이 아닌 방법을 써야 한다.

부모 클래스와 자식 클래스의 동시 수정 문제

자식 클래스가 부모 클래스의 메서드를 오버라이딩하거나 불필요한 인터페이스를 상속받지 않았음에도
부모 클래스를 수정할 때 자식 클래스를 함께 수정해야 할 수도 있다는 사실을 잘 보여준다.
상속을 사용하면 자식 클래스가 부모 클래스의 구현에 강하게 결합되므로 이 문제를 피하기는 어렵다.

조슈아 블로치는 이 문제에 대해 다음과 같이 조언한다.

서브클래스는 올바른 기능을 위해 슈퍼클래스의 세부적인 구현에 의존한다.
슈퍼클래스의 구현은 릴리스를 거치면서 변경될 수 있고,
그에 따라 서브클래스의 코드를 변경하지 않더라도 깨질 수 있다.
결과적으로, 슈퍼클래스의 작성자가 확장될 목적으로 특별히 그 클래스를 설계하지 않았다면
서브클래스는 슈퍼클래스와 보조를 맞춰서 진화해야 한다[Bloch08].

상속을 위한 경고 4
클래스를 상속하면 결합도로 인해 자식 클래스와 부모 클래스의 구현을 영원히 변경하지 않거나,
자식 클래스와 부모 클래스를 동시에 변경하거나 둘 중 하나를 선택할 수밖에 없다.

03 Phone 다시 살펴보기

추상화에 의존하자

부모 클래스가 자식 클래스 모두 추상화에 의존하도록 수정한다.

  • 두 메서드가 유사하게 보인다면 차이점을 메서드로 추출하라.
  • 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라.

차이를 메서드로 추출하라

중복 코드 안에서 차이점을 별도의 메서드로 추출한다.
"변하는 것으로부터 변하지 않는 것을 분리하라"
"변하는 부분을 찾고 이를 캡슐화하라"
조언을 메서드 수준에서 적용하는 것이다.

중복 코드를 부모 클래스로 올려라

추상 클래스로 구현하는 것이 적합하며
AbstractPhone을 통해 Phone, NightlyDiscountPhone이 상속받도록 한다.

추상화가 핵심이다

상속 계층이 코드를 진화시키는데 걸림돌이 된다면 추상화를 찾아내고
상속 계층 안의 클래스들이 그 추상화에 의존하도록 코드를 리팩터링하라.
차이점을 메서드로 추출하고 공통적인 부분은 부모 클래스로 이동하라.

의도를 드러내는 이름 선택하기

이름 변경을 통해 의도를 드러낸다.

AbstractPhone -> Phone
Phone -> RegularPhone
으로 변경

세금 추가하기

인스턴스 초기화 로직을 변경하는 것이
두 클래스에 동일한 세금 계산 코드를 중복시키는 것보다는 현명한 선택이다.
객체생성 로직에 대한 변경을 막기 보다는 핵심 로직의 중복을 막아라.
핵심 로직은 한 곳에 모아 놓고 조심스럽게 캡슐화해야 한다.
그리고 공통적인 핵심 로직은 최대한 추상화해야 한다.

04 차이에 의한 프로그래밍

기존 코드와 다른 부분만을 추가해 애플리케이션의 기능을 확장하는 방법을
차이에 의한 프로그래밍(programming by difference)[Feathers 2004]이라고 부른다.
상속을 이용하면 이미 존재하는 클래스의 코드를 쉽게 재사용할 수 있으므로
애플리케이션의 점진적인 정의(incremental definition)가 가능해진다[Taivalsaari96]

차이에 의한 프로그래밍의 목표는 중복 코드를 제거하고 코드를 재사용하는 것이다.
코드를 재사용하면 코드의 품질은 유지하면서 코드를 작성하는 노력과 테스트는 줄일 수 있다.

객체지향 세계에서 중복 코드를 제거하고 코드를 재사용할 수 있는 유명한 방법은 상속이다.

Metadata

Metadata

Assignees

Labels

GDGGDG Songdo and Incheon activity

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions