TL;DR

  • 상속과 합성은 객체지향 프로그래밍에서 가장 널리 사용되는 코드 재사용 기법입니다.
  • 상속을 이용하면 자식 클래스의 정의에 부모 클래스의 이름을 덧붙이는 것만으로 부모 클래스 코드를 재사용할 수 있게 됩니다. 상속을 통해 자식 클래스는 부모 클래스의 정의 대부분을 물려받게 되며 부모 클래스와 다른 부분만 추가하거나 재정의함으로써 기존 코드를 쉽게 확장할 수 있습니다. 그러나 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 자식 클래스와 부모 클래스 사이의 결합도가 높아질 수밖에 없습니다. 결과적으로 상속은 코드를 재사용할 수 있는 쉽고 간단한 방법일지는 몰라도 우아한 방법이라고 할 수는 없습니다.
  • 코드를 재사용하기 위해 상속 대신 사용할 수 있는 기법인 합성을 소개합니다.
    • 상속이 구현에 대한 높은 결합도를 유발하는 데 반해 합성을 사용하면 퍼블릭 인터페이스에 대해 느슨한 결합도를 유지할 수 있기 때문에 설계를 유연하게 마들 수 있다는 사실을 설명합니다.
    • 상속과 비교해서 합성은 안정성유연성이라는 장점을 제공합니다. 핵심은 구현이 아니라 인터페이스에 의존하면 설계가 유연해집니다.
    • 상속이 부모 클래스와 자식 클래스를 연결해서 부모 클래스의 코드를 재사용하는 데 비해 합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용합니다.
    • 상속에서 부모 클래스와 자식 클래스 사이의 의존성은 컴파이타임에 해결되지만 합성에서 두 객체 사이의 의존성은 런타임에 해결됩니다.
    • 상속 관계는 is-a 관계라고 부르고 합성 관계는 has-a관계라고 불브니다.
    • 합성은 구현에 의존하지 않는다는 점에서 상속과 다릅니다. 합성은 내부에 포함되는 객체의 구현이 아닌 퍼블릭 인터페이스에 의존합니다. 따라서 합성을 이용하면 포함된 객체의 내부 구현이 변경되더라도 영향을 최소화할 수 있기 때문에 변경에 더 안정적인 코드를 얻을 수 있게 됩니다.
    • 상속과 합성은 코드 재사용이라는 동일한 목적을 가진다는 점을 제외하면 구현 방법부터 변경을 다루는 방식에 이르기까지 모든 면에서 도드라진 차이를 보입니다.
    • 상속 관계는 클래스 사이의 정적인 관계인 데 비해 합성 관계는 객체 사이의 동적인 관계입니다. 이 차이점은 생각보다 중요한데, 코드 작성 시점에 결정한 상속 관계는 변경이 불가능하지만 합성 관계는 실행시점에 동적으로 변경할 수 있기 때문입니다. 따라서 상속 대신 합성을 사용하면 변경이 쉽고 유연한 설계를 얻을 수 있습니다.
  • 추가로 객체의 행동을 유연하게 조합하기 위해 사용할 수있는 기법인 믹스인에 대해서도 다룹니다.
  • 상속보다 합성을 이용하는 것이 구현 관점에서 좀 더 번거롭고 복잡하게 느껴질 수도 있습니다. 하지만 설계는 변경과 관련된 것이라는 점을 기억해야 합니다. 변경에 유연하게 대처할 수 있는 설계가 대부분의 경우에 정답일 가능성이 높습니다.
  • 코드 재사용을 위해서는 객체 합성이 클래스 상속보다 좋은 방법입니다.
  • 상속과 합성은 재사용의 대상이 다릅니다. 상속은 부모 클래스 안에 구현된 코드 자체를 재사용하지만 합성은 포함되는 객체의 퍼블릭 인터페이스를 재사용합니다. 따라서 상속 대신 합성을 사용하면 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경할 수 있습니다. 다시 말해서 클래스 사이의 높은 결합도를 객체 사이의 낮은 결합도로 대체할 수 있습니다.
    • 객체지향 시스템에서 기능을 재사용할 수 있는 가장 대표적인 기법은 클래스 상속(class inheritance)과 객체 합성(object composition)입니다. 클래스 상속은 다른 클래스를 이용해서 한 클래스의 구현을 정의하는 것입니다. 서브클래싱에 의한 재사용을 화이트박스 재사용(white-box reuse)이라고 부릅니다. 화이트박스라는 말은 가시성 때문에 나온 말입니다. 상속을 받으면 부모 클래스의 내부가 자식 클래스에 공개되기 때문에 화이트박스인 셈입니다.
    • 객체 합성은 클래스 상속의 대안입니다. 새로운 기능을 위해 객체들을 합성합니다. 객체를 합성하려면 합성할 객체들의 인터페이스를 명확하게 정의해야만 합니다. 이런 스타일의 재사용을 블랙박스 재사용(black-box reuse)이라고 하는데, 객체의 내부의 공개되지 않고 인터페이스를 통해서만 재사용되기 때문입니다.

01. 상속을 합성으로 변경하기

코드 재사용을 위해 상속을 남용했을 때 직면할 수 있는 세 가지 문제점

  • 불필요한 인터페이스 상속 문제 : java.util.Properties와 java.util.Stack
    • 자식 클래스에게는 부적합한 부모 클래스의 오퍼레이션이 상속되기 때문에 자식 클래스 인스턴스의 상태가 불안정해지는 문제.
  • 메서드 오버라이딩의 오작용 문제 : InstrumentedHashSet
    • 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받는 문제.
  • 부모 클래스와 자식 클래스의 동시 수정 문제 : PersonalPlaylist

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

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

불필요한 인터페이스 상속 문제 : java.util.Properties와 java.util.Stack

  • 자식 클래스에게는 부적합한 부모 클래스의 오퍼레이션이 상속되기 때문에 자식 클래스 인스턴스의 상태가 불안정해지는 문제입니다.
  • 클래스에서 상속 관계를 제거하고 인스턴스 변수로 포함시키면 합성 관계로 변경할 수 있습니다.

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

  • 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받는 문제입니다.
  • 인스턴스를 내부에 포함한 후 퍼블릭 인터페이스에서 제공하는 오퍼레이션들을 이용해 필요한 기능을 구현하면 됩니다.
  • 오퍼레이션을 오버라이딩한 인스턴스 메서드에서 내부의 인스턴스에게 동일한 메서드 호출을 그대로 전달한다는 것을 알 수 있습니다. 이를 포워딩(forwarding)이라 부르고 동일한 메서드를 호출하기 위해 추가된 메서드를 포워딩 메서드(forwarding method)라고 부릅니다. 포워딩은 기존 클래스의 인터페이스를 그대로 외부에 제공하면서 구현에 대한 결합 없이 일부 작동 방식을 변경하고 싶은 경우에 사용할 수 있는 유용한 기법입니다.

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

  • 부모 클래스와 자식 클래스 사이의 개념적인 결합으로 인해 부모 클래스를 변경할 때 자식 클래스도 함께 변경해야 하는 문제입니다.
  • 합성으로 변경하더라도 함께 수정해야 하는 문제는 해결되지 않을 수 있습니다. 하지만 파급효과를 최대한 내부로 캡슐화할 수 있습니다. 대부분의 경우 구현에 대한 결합보다는 인터페이스에 대한 결합이 더 좋습니다.

몽키 패치

  • 몽키 패치(Monkey Patch)란 현재 실행 중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나 확장하는 것을 가리킵니다.

02. 상속으로 인한 조합의 폭발적인 증가

  • 상속으로 인해 결합도가 높아지면 코드를 수정하는 데 필요한 작업의 양이 과도하게 늘어나는 경향이 있습니다. 가장 일반적인 상황은 작은 기능들을 조합해서 더 큰 기능을 수행하는 객체를 만들어야 하는 경우입니다.
    • 하나의 기능을 추가하거나 수정하기 위해 불필요하게 많은 수의 클래스를 추가하거나 수정해야 합니다.
    • 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복 코드의 양이 늘어날 수 있습니다.
  • 합성을 사용하면 상속으로 인해 발생하는 클래스의 증가와 중복 코드 문제를 간단하게 해결할 수 있습니다.

기본 정책과 부가 정책 조합하기

상속을 이용해서 기본 정책 구현하기

기본 정택에 세금 정책 조합하기

  • 부모 클래스의 메서드를 재사용하기 위해 super 호출을 사용하면 원하는 결과를 쉽게 얻을 수는 있지만 자식 클래스와 부모 클래스 사이의 결합도가 높아집니다. 결합도를 낮추는 방법은 자식 클래스가 부모 클래스의 메서드를 호출하지 않도록 부모 클래스에 추상 메서드를 제공하는 것입니다. 부모 클래스가 자신이 정의한 추상 메서드를 호출하고 자식 클래스가 이 메서드를 오버라이딩해서 부모 클래스가 원하는 로직을 제공하도록 수정하면 부모 클래스와 자식 클래스 사이의 결합도를 느슨하게 만들 수 있습니다. 이 방법은 자식 클래스가 부모 클래스의 구체적인 구현이 아니라 필요한 동작의 명세를 기술하는 추상화에 의존하도록 만듭니다.
  • 부모 크랠스에 추상 메서드를 추가하면 모든 자식 클래스들이 추상 메서드를 오버라이딩해야 하는 문제가 발생합니다. 자식 클래스의 수가 적다면 큰 문제가 아니겠지만 자식 클래스의 수가 많을 경우에는 꽤나 번거로운 일이 될 수 밖에 없습니다.
    • 기본 구현을 제공함으로써 위 문제를 해결할 수 있습니다.
  • 자바를 비롯한 대부분의 객체지향 언어는 단일 상속만 지원하기 때문에 상속으로 인해 발생하는 중복 코드 문제를 해결하기가 쉽지 않습니다.

추상 메서드와 훅 메서드

  • 계팡-폐쇄 원칙을 만족하는 설계를 만들 수 있는 한 가지 방법은 부모 클래스에 새로운 추상 메서드를 추가하고 부모 클래스의 다른 메서드 안에서 호출하는 것입니다. 자식 클래스는 추상 메서드를 오바라이딩하고 자신만의 로직을 구현해서 부모 클래스에서 정의한 플로우에 개입할 수 있게 됩니다.
  • 추상 메서드의 단점은 상속 계층에 속하는 모든 자식 클래스가 추상 메서드를 오버라이딩해야 한다는 것입니다. 대부분의 자식 클래스가 추상 메서드를 동일한 방식으로 구현한다면 상속 계층 전반에 걸쳐 중복 코드가 존재하게 될 것입니다. 해결 방법은 메서드에 기본 구현을 제공하는 것입니다. 이처럼 추상 메서드와 동일하게 자식 클래스에서 오버라이딩할 의도록 메서드를 추가했지만 편의를 위해 기본 구현을 제공하는 메서드를 훅 메서드(hook method)라고 부릅니다.

기본 정책에 기본 요금 할인 정책 조합하기

중복 코드의 덫에 걸리다

  • 부가 정책은 자유롭게 조합할 수 있어야 하고 적용되는 순서 역시 임의로 결정할 수 있어야 합니다.
  • 상속을 이용한 해결 방법은 모든 가능한 조합별로 자식 클래스를 하니씩 추가하는 것입니다.
  • 모든 부가 정책은 기본 정책에 적용 가능히야 하며 조합 순서 역시 자유로워야 합니다. 따라서 새로운 기본 정책을 추가하면 그에 따라 조합 가능한 부가 정책의 수만큼 새로운 클레스를 추가해야 합니다.
  • 상속의 남용으로 하나의 기능을 추가하기 위해 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가르켜 클래스 폭발(class explosion) 문제 또는 조합 폭발(combinations explosion) 문제라고 부릅니다.
    • 클래스 폭발 문제는 자식 클래스가 부모 클래스의 구현에 강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생하는 문제입니다. 컴파일타임에 결정 된 자식 클래스와 부모 클래스 사이의 관계는 변경 될 수 없기 때문에 자식 클래스와 부모 클래스의 다양한 조합이 필요한 상황에서 유일한 해결 방법은 조합의 수만큼 새로운 클래스를 추가하는 것뿐입니다.
    • 클래스 폭발 문제는 새로운 기능을 추가할 때뿐만 아니라 기능을 수정할 때도 문제가 됩니다.
  • 이 문제를 해결할 수 있는 최선의 방법은 상속을 포기하는 것입니다.

03. 합성 관계로 변경하기

  • 상속 관계는 컴파일타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에는 변경할 수 없습니다. 따라서 여러 기능을 조합해야 하는 설계에 상속을 이용하면 모든 조합 가능한 경우별로 클래스를 추가해야 합니다.
  • 합성은 컴파일타임 관계를 런타임 관계로 변경함으로써 이 문제를 해결합니다. 합성을 사용하면 구현이 아닌 퍼블릭 인터페이스에 대해서만 의존할 수 있기 때문에 런타임에 객체의 관계를 변경할 수 있습니다.
  • 컴파일타임 의존성과 런타임 의존성의 거리가 멀수록 설계가 유연해진다고 했던 것을 기억해야 합니다. 상속을 사용하는 것은 컴파일타임의 의존성과 런타임의 의존성을 동일하게 만들겠다고 선언하는 것입니다. 따라서 상속을 사용하면 부모 클래스와 자식 클래스 사이의 관계가 정적으로 고정되기 때문에 실행 시점에 동적으로 관계를 변경할 수 있는 방법이 없습니다.
  • 상속과 달리 합성 관계는 런타임에 동적으로 변경할 수 있습니다. 합성을 사용하면 컴파일타임 의존성과 런타임 의존성을 다르게 만들 수 있습니다. 사실 8장에서 살펴본 유연한 설계를 만들기 위한 대부분의 의존성 관리 기법은 상속이 아닌 합성을 기반으로 합니다. 클래스 폭발 문제를 해결하기 위해 합성을 사용하는 이유는 런타임에 객체 사이의 의존성을 자유롭게 변경할 수 있기 때문입니다.
  • 합성을 사용하면 구현 시점에 정책들의 관계를 고정시킬 필요가 없으며 실행 시점에 정책들의 관계를 유연하게 변경할 수 있게 됩니다. 상속이 조합의 결과를 개별 클래스 안으로 밀어 넣는 방법이라면 합성은 조합을 구성하는 요소들을 개별 클래스로 구현한 후 실행 시점에 인스턴스를 조립하는 방법을 사용하는 것이라고 할 수 있습니다. 컴파일 의존성에 속박되지 않고 다양한 방식의 런타임 의존성을 구성할 수 있다는 것이 합성이 제공하는 가장 커다란 장점인 것입니다.
  • 컴파일타임 의존성과 런타임 의존성의 거리가 멀면 멀수록 설계의 복잡도가 상승하기 때문에 코드를 이해하기 어려워지는 것 역시 사실입니다. 하지만 설계는 변경과 유지보수를 위해 존재한다는 사실을 기억해야 합니다. 설계는 트레이드오프의 산물입니다. 대부분의 경우에는 단순한 설계가 정답이지만 변경에 따르는 고통이 복잡성으로 인한 혼란을 넘어서고 있다면 유연성의 손을 들어주는 것이 현명한 판단일 확률이 높습니다.
  • 아이러니하게도 변경하기 편리한 설계를 만들기 위해 복잡성을 더하고 나면 원래의 설계보다 단순해지는 경우를 종종 볼 수 있습니다.

기본 정책 합성하기

  • 컴파일타임 의존성을 구체적인 런타임 의존성을 대체하기 위해 생성자를 통해 인스터스에 대한 의존성을 주입 받습니다.
  • 다양한 종류의 객체와 협력하기 위해 합성 관계를 사용하는 경우에는 합성하는 객체의 타입을 인터페이스나 추상 클래스로 선언하고 의존성 주입을 사용해 런타임에 필요한 객체를 설정할 수 있도록 구현하는 것이 일반적입니다.
  • 단순히 원하는 클래스를 선택하는 상속보다 더 복잡해졌다는 생각이 들 수도 있습니다. 상속을 사용하는 경우에는 해당 인스턴스를 생성하면 됐기 때문입니다. 하지만 현재의 설계에 부가 정책을 추가해 보면 합성의 강력함을 실감할 수 있을 것입니다.

부가 정책 사용하기

  • 부가 정책은 기본 정책이나 다른 부가 정책의 인스턴스를 참조할 수 있어야 합니다. 다시 말해서 부가 정책의 인스턴스는 어떤 종류의 정책과도 합성될 수 있어야 합니다.
  • 기본 정책과 부가 정책은 협력 안에서 동일한 ‘역할’을 수행해야 합니다. 이것은 부가 정책이 기본 정책과 동일한 인터페이스를 구현해야 한다는 것을 의미합니다.

기본 정책과 부가 정책 합성하기

  • 원하는 정책의 인스턴스를 생성한 후 의존성 주입을 통해 다른 정책의 인스턴스에 전달하는 것뿐입니다.
  • 상속을 사용한 설계보다 복잡하고 정해진 규칙에 따라 객체를 생성하고 조합해야 하기 때문에 처음에는 코드를 이해하기 어려울 수도 있습니다. 하지만 일단 설계에 익숙해지고 나면 객체를 조합하고 사용하는 방식이 상속을 사용한 방식보다 더 예측 가능하고 일관성 있다는 사실을 알게 될 것입니다.
  • 합성의 진가는 새로운 클래스를 추가하거나 수정하는 시점이 돼서야 비로소 알 수 있습니다.

새로운 정책 추가하기

  • 상속을 기반으로 한 설계에 새로운 부가 정책을 추가하기 위해서는 상속 계층에 불필요할 정도로 많은 클래스를 추가해야만 했습니다. 합성을 기반으로 한 설계에서는 이 문제를 간단하게 해결할 수 있습니다.
  • 오직 하나의 클래스만 추가하고 런타임에 필요한 정책들을 조합해서 원하는 기능을 얻을 수 있습니다.
  • 중요한 것은 요구사항을 변경할 때 오직 하나의 클래스만 수정해도 된다는 것입니다.
  • 변경 후의 설계는 단일 책임 원칙을 준수하고 있습니다.

객체 합성이 클래스 상속보다 더 좋은 방법이다

  • 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법은 상속입니다. 하지만 상속은 코드 재사용을 위한 우아한 해결책은 아닙니다. 상속은 부모 클래스의 세부적인 구현에 자식 클래스를 강하게 결합 시키기 때문에 코드의 진화를 방해합니다.
  • 코드를 재사용하면서도 건전한 결합도를 유지할 수 있는 더 좋은 방법은 합성을 이용하는 것입니다. 상속이 구현을 재사용하는 데 비해 합성은 객체의 인터페이스를 재사용합니다.
  • 인터페이스 상속을 피하고 인터페이스 상속을 사용해야 합니다.
  • 믹스인이라는 이름으로 널리 알려져 있는 기법으로 상속과 합성의 특성을 모두 보유하고 있는 독특한 코드 재사용 방법이 있습니다. 믹스인을 이해하고 나면 상속과 합성의 장단점에 관해 좀 더 깊이 있게 이해하게 될 것입니다.

04. 믹스인

  • 상속을 사용하면 다른 클래스를 간편하게 재사용하고 점진적으로 확장할 수 있지만 부모 클래스와 자식 클래스가 강하게 결합되기 때문에 수정과 확장에 취약한 설계를 낳게 됩니다. 우리가 원하는 것은 코드를 재사용하면서도 납득할 만한 결합도를 유지하는 것입니다. 합성이 상속과 같은 문제점을 초래하지 않는 이유는 클래스의 구체적인 구현이 아니라 객체의 추상적인 인터페이스에 의존하기 때문입니다.
  • 상속과 클래스를 기반으로 하는 재사용 방법을 사용하면 클래스의 확장과 수정을 일관성 있게 표현할 수 있는 추상화의 부족으로 인해 변경하기 어려운 코드를 얻게 됩니다. 따라서 구체적인 코드를 재사용하면서도 낮은 결합도를 유지할 수 있는 유일한 방법은 재사용에 적합한 추상화를 도입하는 것입니다.
  • 믹스인(mixin)은 객체를 생성할 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법을 가리키는 용어입니다. 합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합하는 재사용 방법입니다.
  • 믹스인은 상속과는 다릅니다. 비록 상속의 결과로 부모 클래스의 코드를 재사용할 수 있기는 하지만 상속의 진정한 목적은 자식 클래스를 부모 클래스와 동일한 개념적인 범주로 묶어 is-a 관계를 만들기 위한 것입니다. 반면 믹스인은 말 그대로 코드를 다른 코드를 다른 코드 안에 섞어 넣기 위한 방법입니다.
  • 상속이 클래스와 클래스 사이의 관계를 고정시키는 데 비해 믹스인은 유연하게 관계를 재구성할 수 있습니다. 믹스인은 코드 재사용에 특화된 방법이면서도 상속과 같은 결합도 문제를 초래하지 않습니다. 믹스인은 합성처럼 유연하면서도 상속처럼 쉽게 코드를 재사용 할 수 있는 방법입니다.
  • 그 방법이 무엇이건 코드를 다른 코드 안에 유연하게 섞어 넣을 수 있다면 믹스인이라고 부를 수 있습니다.

기본 정책 구현하기

트레이트로 부가 정책 구현하기

  • 스칼라에서는 다른 코드와 조합해서 확장할 수 있는 기능을 트레이트로 구현할 수 있습니다.
  • 상속은 정적이지만 믹스인은 동적입니다. 상속은 부모 클래스와 자식 클래스의 관계를 코드를 작성하는 시점에 고정시켜 버리지만 믹스인은 제약을 둘 뿐 실제로 어떤 코드에 믹스인될 것인지를 결정하지 않습니다.
  • 실제로 트레이트를 믹스인하는 시점에 가서야 믹스인할 대상을 결정할 수 있습니다. 부가 정책과 기본 정책을 부모 클래스와 자식 클래스라는 관계로 결합시켜야 했던 상속과 달리 부가 정책과 기본 정책을 구현한 코드 사이에 어떤 관계도 존재하지 않습니다. 이들은 독립적으로 작성된 후 원하는 기능을 구현하기 위해 조합됩니다.
  • 트레이트는 부모 클래스를 고정시키지 않습니다. 따라서 super로 참조되는 코드 역시 고정되지 않습니다. super 호출로 실행되는 메서드를 보관한 코드는 실제로 트레이트가 믹스인 되는 시점에 결정됩니다.
  • super 참조가 가리키는 대상이 컴파일 시점이 아닌 실행 시점에 결정됩니다. 상속의 경우 일반적으로 this 참조는 동적으로 결정되지만 super 참조는 컴파일 시점에 결정됩니다. 따라서 상속에서는 부모 클래스와 자식 클래스 관계를 변경 할 수 있는 방법은 없습니다. 하지만 스칼라의 트레이트에서 super 참조는 동적으로 결정됩니다. 따라서 트레이트 경우 this 호출뿐만 아니라 super 호출 역시 실행 시점에 바인딩 됩니다.
  • 상속은 재사용 가능한 문맥을 고정시키지만 트레이트는 문맥을 확장 가능하도록 열어 놓습니다. 이런 면에서 믹스인은 상속보다는 합성과 유사합니다. 합성은 독립적으로 작성된 객체들을 실행 시점에 조합해서 더 큰 기능을 만들어내는 데 비해 믹스인은 독립적으로 작성된 트레이트와 클래스를 코드 작성 시점에 조합해서 더 큰 기능을 만들어 낼 수 있습니다.

부가 정책 트레이트 믹스인하기

  • 스칼라는 트레이트를 클래스나 다른 트레이트에 믹스인할 수 있도록 extends와 with 키워드를 제공합니다. 믹스인하려는 대상 클래스의 부모 클래스가 존재하는 경우 부모 클래스는 extends를 이용해 상속 받고 트레이트는 with를 이용해 믹스인해야 합니다. 이를 트레이트 조합(trait composition)이라고 부릅니다.
  • 스칼라는 특정 클래스에 믹스인한 클래스와 트레이트를 선형화(linearization)해서 어떤 메서드를 호출할지 결정합니다. 클래스의 인스턴스를 생성할 때 스칼라는 클래스 자신과 조상 클래스, 트레이트를 일렬로 나열해서 순서를 정합니다. 그리고 실행 중인 메서드 내부에서 super 호출을 하면 다음 단계에 위치한 클래스나 트레이트의 메서드가 호출됩니다.
  • 선형화를 할 때 항상 맨 앞에서 구현한 클래스 자기 자신이 위치합니다.
  • 믹스인은 재사용 가능한 코드를 독립적으로 작성한 후 필요한 곳에서 쉽게 조합할 수 있게 해줍니다.
  • 믹스인을 사용하더라도 상속에서 클래스의 숫자가 기하급수적으로 늘어나는 클래스 폭발 문제는 여전히 남아 있다고 볼 수 있습니다. 하지만 클래스 폭발 문제의 단점은 클래스가 늘어난다는 것이 아니라 클래스가 늘어날수록 중복 코드도 함께 기하급수적으로 늘어난다는 점입니다. 믹스인에는 이런 문제가 발생하지 않습니다.
    • 만약 클래스를 만들어야 하는 것이 불만이라면 클래스를 만들지 않고 인스턴스를 생성할 때 트레이트를 믹스인할 수도 있습니다.

쌓을 수 있는 변경

  • 전통적으로 믹스인은 특정한 클래스의 메서드를 재사용하고 기능을 확장하기 위해 사용돼 왔습니다.
  • 믹스인은 상속 계층 안에서 확장한 클래스보다 더 하위에 위치하게 됩니다. 다시 말해서 믹스인은 대상 클래스의 자식 클래스처럼 사용될 용도로 만들어지는 것입니다. 믹스인은 추상 서브클래스(abstract subclass)라고 부르기도 합니다.
  • 객체지향 언어에서 슈퍼클래스는 서브클래스를 명시하지 않고도 정의될 수 있습니다. 그러나 이것은 대칭적이지는 않습니다. 서브클래스가 정의될 때는 슈퍼클래스를 명시해야 합니다. 믹스인(추상 서브클래스라고도 합니다)은 결혼적으로 슈퍼클래스로부터 상속될 클래스를 명시하는 메커니즘을 표현합니다. 따라서 하나의 믹스인은 매우 다양한 클래스를 도출하면서 서로 다른 서브클래스를 이용해 인스턴스화될 수 있습니다. 믹스인의 이런 특성은 다중 클래스를 위한 단일의 점진적인 확장을 정의하는 데 적절하게 만듭니다. 이 클래스들 중 하나를 슈퍼클래스로 삼아 믹스인이 인스턴스화될 때 추가적인 행위가 확장된 클래스를 생성합니다.
  • 믹스인을 사용하면 특정한 클래스에 대한 변경 또는 확장을 독립적으로 구현한 후 필요한 시점에 차례대로 추가할 수 있습니다. 믹스인의 이런한 특징을 쌓을 수 있는 변경(stackable modification)이라고 부릅니다.
  • 스칼라에서 트레이트는 코드 재사용의 근간을 이루는 단위입니다. 트레이트로 메서드와 필드 정의를 캡슐화하면 트레이트를 조합한 클래스에서 그 메서드나 필드를 재사용할 수 있습니다. 하나의 부모 클래스만 갖는 클래스의 상속과 달리 트레이트의 경우 몇 개라도 믹스인 될 수 있습니다. 클래스와 트레이트의 또 다른 차이는 클래스에서는 super 호출을 정적으로 바인딩하지만, 트레이트에서는 동적으로 바인딩한다는 것입니다. 호출할 메서드의 구현을 트레이트를 클래스 구현에 믹스인 할 때마다(클래스에 따라) 새로 정해집니다. super가 이렇게 동작하기 때문에 트레이트를 이용해 변경 위에 변경을 쌓아 올리는 쌓을 수 있는 변경이 가능해집니다.
  • 코드의 구성 방식이 변경에 큰 영향을 미칩니다.

참고