Ch.11 합성과 유연한 설계
About
상속과 합성은 객체지향 프로그래밍에서 가장 널리 사용되는 코드 재사용 기법이다.
하지만 아래에서 설명하듯 단순히 코드 재사용만을 위한 목적으로 상속을 이용한다면 변경하기 어려운 설계가 될 확률이 높다.
상속과 합성의 비교
구분 | 상속 (Inheritance) | 합성 (Composition) |
---|---|---|
관계 | is-a 관계 (정확히는 is a kind of) | has-a 관계 |
의존성 해결 시점 | 부모 클래스와 자식 클래스 사이의 의존성은 컴파일타임에 해결된다. (정적인 관계.) - 코드 작성 시점에 결정한 상속 관계는 변경이 불가능하다. | 두 객체 사이의 의존성은 런타임에 해결된다. (동적인 관계.) - 실행 시점에 동적으로 관계를 변경할 수 있다. 따라서 변경하기 쉽고 유연하다. |
방식 | 부모 클래스의 이름을 덧붙이는 것만으로 부모 클래스의 코드를 재사용할 수 있다. 다른 부분만 추가하거나 재정의하여 기존 코드를 쉽게 확장할 수 있다. | 합성은 내부에 포함되는 객체의 구현이 아닌 퍼블릭 인터페이스에 의존한다. |
의존성 | 내부 구현에 대해 상세하게 알아야 하므로 결합도가 높아진다. | 합성은 구현에 의존하지 않는다. 따라서 변경의 영향을 최소화하여 변경에 더 안정적인 코드를 얻을 수 있다. |
가시성 | 부모 클래스의 내부가 자식 클래스에 공개되므로 따라 화이트박스 재사용(white-box reuse)이라고 부른다. | 객체의 내부는 공개되지 않고 인터페이스를 통해서만 재사용되므로 블랙박스 재사용(black-box reuse)이라고 부른다. |
(코드 재사용을 위해서는) 변경에 유연하게 대처할 수 있는 설계인 객체 합성이 상속보다 더 좋은 방법이다.
상속의 문제점
Chapter 10의 내용이다. Ch.10 상속과 코드 재사용에 따로 정리하진 않았다.
1. 불필요한 인터페이스 상속 문제
자식 클래스에 부적합한 / 불필요한 부모 클래스의 오퍼레이션이 상속되어 자식 클래스가 불안정해진다.
e.g.,
java.util.Properties
,java.util.Stack
java.util.HashTable
을 상속받는 java.util.Properties
클래스는 제네릭이 도입되기 전에 만들어졌기 때문에 컴파일러가 키와 값의 타입이 String
인지 여부를 체크할 수 있는 방법이 없었다. 그래서 의도와 다르게 HashTable
의 인터페이스에 있는 put
메서드를 이용하면 String
이외의 타입의 키와 값을 Properties로 저장할 수 있는 사태가 벌어질 수 있다. 이를 합성으로 개선해본다.
이제 더 이상 불필요한 HashTable의 오퍼레이션이 Properties의 퍼블릭 인터페이스를 오염시키지 않고, 클라이언트는 정의된 오퍼레이션만 사용할 수 있다.
java.util.Vector
를 상속받는 java.util.Stack
은 Vector을 상속받았기 때문에 임의의 요소에서 요소를 추가하거나 제거할 수 있어 LIFO 구조를 구현하는 Stack의 정의에 맞지 않다. 합성으로 개선해본다.
2. 메서드 오버라이딩의 오작용 문제
자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받는다.
e.g.,
java.util.HashSet
<-InstrumentedHashSet
addAll
메서드를 실행하게 되면 내부적으로 super.addAll
에서 add
가 호출되기 때문에 addCount
가 이상한 값이 된다.
이제 합성으로 개선해본다.
이런 결합도를 제거하면서도 퍼블릭 인터페이스를 그대로 상속받고 싶다면 인터페이스 (Set) 구현을 택하면 된다.
포워딩(forwarding)이라는 기법으로 동일한 메서드를 그대로 호출을 위임할 수 있다. 이러한 메서드를 포워딩 메서드(forwarding method)라고 부른다.
3. 부모 클래스와 자식 클래스의 동시 수정 문제
부모 클래스와 자식 클래스의 개념적인 "결합"으로 인해, 부모 클래스를 변경할 때 자식 클래스도 함께 변경해야 한다.
e.g.,
Playlist
<-PersonalPlaylist
상속을 이용해 구현했더니 요구사항이 변경됨에 따라 부모 클래스와 자식 클래스를 변경해야되는 강한 결합도에 고통받을 수 있다. 합성을 이용해 개선해보자.
여전히 함께 수정해야되는 문제는 해결되지 않았지만, 내부 구현을 변경하더라도 파급효과를 최대한 PersonalPlaylist
내부로 캡슐화할 수 있기 때문에 합성이 더 유리하다.
몽키 패치(Monkey Patch)란 현재 실행 중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나 확장하는 것을 가리킨다. Playlist
를 수정할 권한이 없더라도 몽키 패치가 지원되는 환경이라면 Playlist
에 직접 remove
메서드를 추가할 수 있다. (자바의 경우 바이트 코드 직접 변환 / AOP 등)
상속에서 합성으로 변경하기
합성을 사용하면 위의 세 가지 문제를 해결할 수 있다.
적용법은 간단하다. 상속 관계를 제거하고 부모 클래스의 인스턴스를 자식 클래스의 인스턴스 변수로 선언하자.
클래스 폭발 (Class Explosion) / 조합의 폭발 (Combinational Explosion)
상속을 이용하게 되면 다음과 같은 이유로 조합의 폭발이 발생할 수 있다.
하나의 기능을 추가하거나 수정하기 위해 불필요하게 많은 클래스를 수정하거나 수정해야 한다.
단일 상속만 지원하는 언어에서는 오히려 상속으로 인해 오히려 중복 코드의 양이 늘어날 수 있다.
반면 합성의 경우 하나의 클래스만 추가하고 런타임에 필요한 기능을 조합해 원하는 기능을 얻을 수 있다. 게다가 요구사항을 변경할 때 오직 하나의 클래스만 수정해도 된다.
Tip: 훅 메서드
메서드를 자식 클래스에서 오버라이딩할 의도로 메서드를 추가했지면 편의를 위해 기본 구현을 제공하는 메서드를 훅 메서드(hook method)라고 부른다.
믹스인 (Mixin)
상속의 장점인 구체적인 코드를 재사용하는 것, 그리고 합성의 장점인 낮은 결합도를 유지할 수 있는 유일한 방법은 재사용에 적합한 추상화를 도입하는 것이다.
믹스인은 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법이다.
클래스 폭발 문제의 단점은 클래스가 늘어나는 것이 아니라, 클래스가 늘어날 수록 중복 코드도 기하급수적으로 늘어난다는 점인데, 믹스인에서는 이런 문제가 발생하지 않는다.
합성이 실행 시점에 객체를 조합하는 재사용 방법이라면, 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법이다.
믹스인은 상속 계층 안에서 확장한 클래스보다 더 하위에 위치하게 되어, 대상 클래스의 자식 클래스처럼 사용될 용도로 만들어진다. 따라서 믹스인을 추상 서브클래스(abstract subclass)라고 부르기도 한다.
믹스인을 사용하면 특정한 클래스에 대한 변경 / 확장을 독립적으로 구현한 후 필요한 시점에 차례대로 추가할 수 있다. 이를 쌓을 수 있는 변경(stackable modification이라고 부른다.
책의 예제에서는 스칼라의 트레이트(Trait)를 이용해 믹스인을 구현한다.
Last updated