오브젝트 독서회 10회차 - 11장 합성과 유연한 설계

이번에도 지난 번과 참여 멤버들이 같아서 장소도 동일하게 진행했습니다.

@vincent @freebear @은딩 참여 감사드립니다.


P346

상속과 합성은 객체지향 프로그래밍에서 가장 널리 사용되는 코드 재사용 기법이다. 상속이 부모 클래스와 자식 클래스를 연결해서 부모 클래스의 코드를 재사용하는 데 비해 합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용한다.

@freebear

상속에서 부모클래스와 자식클래스 사이의 의존성은 컴파일타임에 해결되지만 합성에서 두 객체 사이의 의존성은 런타임에 해결된다. 상속 관계는 is-a 관계 라고 부르고 합성 관계는 has-a 관계 라고 부른다.

@은딩 @freebear @vincent

상속과 합성은 코드 재사용이라는 동일한 목적을 가진다는 점을 제외하면 구현 방법부터 변경을 다루는 방식에 이르기까지 모든 면에서 도드라진 차이를 보인다.

@freebear

그러나 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 자식 클래스와 부모 클래스 사이의 결합도가 높아질 수 밖에 없다. 결과적으로 상속은 코드를 재사용할 수 있는 쉽고 간단한 방법일지는 몰라도 우아한 방법이라고 할 수는 없다.

@은딩 @freebear @vincent

합성은 구현에 의존하지 않는 다는 점에서 상속과 다르다.

@freebear

합성은 내부에 포함되는 객체의 구현이 아닌 퍼블릭 인터페이스에 의존한다.

@은딩

P347

상속 관계는 클래스 사이의 정적인 관게인 데 비해 합성 관계는 객체 사이의 동적인 관계다. 이 차이점은 생각보다 중효한데, 코드 작성 시점에 결정한 상속 관계는 변경이 불가능하지만 합성 관계는 실행 시점에 동적으로 변경할 수 있기 때문이다.

@은딩

따라서 상속 대신 합성을 사용하면 변경하기 쉽고 유연한 설계를 얻을 수 있다.

@freebear

하지만 설계는 변경과 관련된 것이라는 점을 기억하라.

@vincent

[코드 재사용을 위해서는] 객체 합성이 클래스 상속보다 더 좋은 방법이다[GOF94].

@은딩 @vincent

클래스 상속은 다른 클래스를 이용해서 한 클래스의 구현을 정의하는 것이다. 서브클래싱에 의한 재사용을 화이트박스 재사용(white-box reuse)이라고 부른다. 화이트박스라는 말은 가시성때문에 나온말이다. 상속을 받으면 부모 클래스의 내부가 자식 클래스에 공개되기 때문에 화이트박스인 셈이다.

@은딩 @vincent

객체를 합성하려면 합성할 객체들의 인터페이스를 명확하게 정의해야만 한다. 이런 스타일의 재사용을 블랙박스 재사용(black-box reuse)이라고 하는데, 객체의 내부는 공개되지 않고 인터페이스를 통해서만 재사용되기 때문이다[GOF94].

@은딩 @vincent

P348

합성을 사용하면 상속이 초래하는 세 가지 문제점을 해결할 수 있다.

@은딩

상속을 합성으로 바꾸는 방법은 매우 간단한데 자식 클래스에 선언된 상속 관계를 제거하고 부모 클래스의 인스턴스를 자식 클래스의 인스턴스 변수로 선언하면 된다.

@은딩

P349

Vector를 상속받는 Stack 역시 Vector의 인스턴스 변수를 Stack 클래스의 인스턴스 변수로 선언함으로써 합성 관계로 변경할 수 있다.

@vincent

P352

InstrumentedHashSet의 코드를 보면 Set의 오퍼레이션을 오버라이딩한 인스턴스 메서드에서 내부의 HashSet 인스턴스에게 동일한 메서드 호출을 그대로 전달한다는 것을 알 수 있다. 이를 포워딩(forwarding) 이라 부르고 동일한 메서드를 호출하기 위해 추가된 메서드를 포워딩 메서드(forwarding method)[Bloch08]라고 부른다.

@vincent

P353

C#의 확장 메서드(Extension Method)와 스칼라의 암시적 변환(implict conversion) 역시 몽키 패치를 위해 사용할 수 있다.

@은딩 @vincent

이번 장을 시작할 때 상속과 비교해서 합성은 안전성과 유연성이라는 장점을 제공한다고 말했다.

@은딩

구현이 아니라 인터페이스에 의존하면 설계가 유연해진다는 것이다.

@은딩

합성을 사용하면 상속으로 인해 발생하는 클래스의 증가와 중복 코드 문제를 간단하게 해결 할 수 있다.

@freebear

P354

따라서 설계는 다양한 조합을 수용할 수 있도록 유연해야 한다.

@은딩

P357-358

부모클래스의 메서드를 재사용하기 위해 super 호출을 사용하면 원하는 결과를 쉽게 얻을 수는 있지만 자식 클래스와 부모 클래스 사이의 결합도가 높아지고 만다. 결합도를 낮추는 방법은 자식 클래스가 부모 클래스의 메서드를 호출하지 않도록 부모 클래스에 추상 메서드를 제공하는 것이다. 부모 클래스가 자신이 정의한 추상 메서드를 호출하고 자식 클래스가 이 메서드를 오버라이딩해서 부모 클래스가 원하는 로직을 제공하도록 수정하면 부모 클래스와 자식 클래스 사이의 결합도를 느슨하게 만들 수 있다. 이 방법은 자식 클래스가 부모 클래스의 구체적인 구현이 아니라 필요한 동작의 명세를 기술하는 추상화에 의존하도록 만든다.

@은딩

P360

위 코드에서 알 수 있는 것처럼 부모 클래스에 추상 메서드를 추가하면 모든 자식 클래스들이 추상 메서드를 오버라이딩해야 하는 문제가 발생한다. 자식 클래스의 수가 적다면 큰 문제가 아니겠지만 자식 클래스의 수가 많을 경우에는 꽤나 번거로운 일이 될 수 밖에 없다.

@은딩

P362

사실 자바를 비롯한 대부분의 객체지향 언어는 단일 상속만 지원하기 때문에 상속으로 인해 발생하는 중복 코드 문제를 해결하기가 쉽지 않다.

@은딩

P364

상속을 이용한 해결 방법은 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 것이다.

@은딩

P366

바로 새로운 정책을 추가하기가 어렵다는 것이다. 현재의 설계로 새로운 정책을 추가하기 위해서는 불필요하게 많은 수의 클래스를 상속 계층 안에 추가해야 한다.

@은딩

P367

그림 11.5의 상속 계층에 새로운 기본 정책을 추가해야 한다고 가정해보자.

@은딩

따라서 새로운 기본 정책을 추가하면 그에 따라 조합 가능한 부가 정책의 수만큼 새로운 클래스를 추가해야 한다.

@은딩

이처럼 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가리켜 클래스 폭발(class explosion)[Shalloway01] 문제 또는 조합의 폭발(combinational explosion) 문제라고 부른다. 클래스 폭발 문제는 자식 클래스가 부모 클래스의 구현에 강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생하는 문제다.

@은딩 @vincent

P368

이 문제를 해결할 수 있는 최선의 방법은 상속을 포기하는 것이다.

@은딩

합성은 컴파일타임 관계를 런타임 관계로 변경함으로써 이 문제를 해결한다. 합성을 사용하면 구현이 아닌 퍼블릭 인터페이스에 대해서만 의존할 수 있기 때문에 런타임에 객체의 관계를 변경할 수 있다.

@은딩

상속이 조합의 결과를 개별 클래스 안으로 밀어 넣는 방법이라면 합성은 조합을 구성하는 요소들은 개별 클래스로 구현한 후 실행 시점에 인스턴스를 조립하는 방법을 사용하는 것이라고 할 수 있다. 컴파일 의존성에 속박되지 않고 다양한 방식의 런타임 의존성을 구성할 수 있다는 것이 합성이 제공하는 가장 커다란 장점인 것이다.

@은딩

P369

물론 컴파일타임 의존성과 런타임 의존성의 거리가 멀면 멀수록 설꼐의 복잡도가 상승하기 때문에 코드를 이해하기 어려워지는 것 역시 사실이다. 하지만 설계는 변경과 유지보수를 위해 존재한다는 사실을 기억하라. 설계는 트레이드오프의 산물이다.

@은딩

대부분의 경우에는 단순한 설계가 정답이지만 변경에 따르는 고통이 복잡성으로 인한 혼란을 넘어서고 있다면 유연성의 손을 들어주는 것이 현명한 판단일 확률이 높다.

아이러니하게도 변경하기 편리한 설계를 만들기 위해 복잡성을 더하고 나면 원래의 설계보다 단순해지는 경우를 종종 볼 수 있다. 상속을 합성으로 변경한 핸드폰 과금 시스템이 바로 그런 경우다.

@freebear

P371

Phone의 경우처럼 다양한 종류의 객체와 협력하기 위해 합성 관계를 사용하는 경우에는 합성하는 객체의 타입을 인터페이스나 추상 클래스로 선언하고 의존성 주입을 사용해 런타임에 필요한 객체를 설정할 수 있도록 구현하는 것이 일반적이다.

@freebear

상속을 사용한 경우에는 어떤 클래스의 인스턴스를 조합해야 하는지 고민할 필요 없이 기본 요금제를 적용하고 싶은 경우에는 RegularPhone을, 심야 할인 요금제를 적용하고 싶은 경우에는 NightlyDiscountPhone의 인스턴스를 생성하면 됐기 때문이다. 하지만 현재의 설계에 부가 정책을 추가해 보면 합성의 강력함을 실감할 수 있을 것이다.

@vincent

P378

우리는 오직 하나의 클래스만 추가하고 런타임에 필요한 정책들을 조합해서 원하는 기능을 얻을 수 있다. 이 설계를 필요한 조합의 수만큼 매번 새로운 클래스를 추가해야 했던 상속과 비교해보라. 왜 많은 사람들이 그렇게 코드 재사용을 위해 상속보다는 합성을 사용하라고 하는 지 그 이유를 이해할 수 있을 것이다.

@은딩 @vincent

더 중요한 것은 요구사항을 변경할 때 오직 하나의 클래스만 수정해도 된다는 것이다.

@은딩 @freebear

그렇다면 상속은 사용해서는 안 되는 것인가? 상속을 사용해야 하는 경우는 언제인가? 이 의문에 대답하기 위해서는 먼저 상속을 구현 상속과 인터페이스 상속의 두 가지로 나눠야 한다는 사실을 이해해야 한다. 그리고 이 번 장에서 살펴본 상속에 대한 모든 단점들은 구현 상속에 구한된다는 점 또한 이해해야 한다.

@freebear

P379

상속과 클래스를 기반으로 하는 재사용 방법을 사용하면 클래스의 확장과 수정을 일관성 있게 표현할 수 있는 추상화의 부족으로 인해 변경하기 어려운 코드를 얻게 된다. 따라서 구체적인 코드를 재사용하면서도 낮은 결합도를 유지할 수 있는 유일한 방법은 재사용에 적합한 추상화를 도입하는 것이다.

@freebear

믹스인은 합성처럼 유연하면서도 상속처럼 쉽게 코드를 재사용할 수 있는 방법이다.

@freebear

어떤 언어는 믹스인을 위한 구성 요소를 언어 차원에서 직접 지원하는 데 비해 어떤 언어는 다른 용도로 고안된 요소를 이용해 믹스인을 구현하기도 한다. 그 방법이 무엇이건 코드를 다른 코드 안에 유연하게 섞어 넣을 수 있다면 믹스인이라고 부를 수 있다.

@freebear @vincent

P380

믹스인은 Flavors라는 언어에서 처음으로 도입됐고 이후 Flavors의 특징을 흡수한 CLOS(Common Lisp Object System)에 의해 대중화됐다. 여기서느 스칼라 언어에서 제공하는 트레이트(trait) 를 이용해 믹스인을 구현해 보겠다. 스칼라의 트레이트는 CLOS에서 제공했던 믹스인의 기본 철학을 가장 유사한 형태로 재현하고 있다.

@vincent

P382

상속은 부모 클래스와 자식 클래스의 관계를 코드를 작성하는 시점에 고정시켜 버리지만 믹스인은 제약을 둘 뿐 실제로 어떤 코드에 믹스인될 것인지를 결정하지 않는다.

@vincent

P387

사실 클래스 폭발 문제의 단점은 클래스가 늘어난다는 것이 아니라 클래스가 늘어날수록 중복 코드도 함께 기하급수적으로 늘어난다는 점이다. 믹스인에는 이런 문제가 발생하지 않는다.

@vincent

하지만 코드 여러 곳에서 동일한 트레이트를 믹스인해서 사용해야 한다면 명시적으로 클래스를 정의하는 것이 좋다.

@vincent

다시 말해서 믹스인은 대상 클래스의 자식 클래스처럼 사용될 용도로 만들어지는 것이다.

@vincent

P388

따라서 믹스인을 추상 서브클래스(abstract subclass) 라고 부르기도 한다[Brach90].

@은딩

마틴 오더스키(Martin Odersky)는 믹스인의 이러한 특징을 쌓을 수 있는 변경(stackable modification) 이라고 부른다.

@은딩

클래스와 트레이트의 또 다른 차이는 클래스에서는 super 호출을 정적으로 바인딩하지만, 트레이트에서는 동적으로 바인딩한다는 것이다.

@vincent

4개의 좋아요