Ch2 객체지향 프로그래밍

본문


들어가며

객체지향 프로그래밍에서 사용되는 전반적인 주제들을 영화 표 예매(할인 조건, 할인 정책) 코드와 함께 대략적으로 설명한다.

키워드는 객체, 협력, 도메인 구조를 따르는 프로그램 구조, 인터페이스와 구현의 분리, 상속, 다형성, 합성 등이 있다.


핵심

  • 클래스 구현시 가장 중요한 점은 클래스의 내부와 경계를 구분짓는 것이다. 이는 1장에서 가장 중요한 자율적인 객체를 위한 캡슐화와 관련이 깊다.

  • 클래스내부(구현)외부(퍼블릭 인터페이스)로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부로 드러내고 어떤 부분을 내부로 감출지를 결정하는 것이다.

  • 경계의 명확성이 바로 적절한 캡슐화이고 이를 통해 객체의 자율성을 보존할 수 있다.

  • 불필요한 세부사항을 내부로 감추는 캡슐화를 통해 객체는 스스로 판단하고 스스로 행동하며 자신의 상태를 스스로 관리할 수 있다.

  • 메서드메시지의 구분에서부터 다형성 개념이 출발한다.

  • 동일한 메시지에 대해 실제로 실행될 메서드가 어떤 것인지는 수신한 객체의 구체 타입(클래스)에 따라 다르다.

  • 위처럼 동일한 메시지에 대해 다양한 메서드로 응답 가능한 것이 바로 다형성이다.

  • 컴파일 의존성런타임 의존성은 서로 다를 수 있다.

  • 상속이 가치있는 이유는 부모클래스가 제공하는 모든 인터페이스를 물려받음으로써 동일한 타입으로 간주할 수 있기 때문이다.

  • 인터페이스 상속이 아닌 코드 재사용을 위한 상속은 지양되어야 한다.

  • 코드 재사용을 위해서는 상속보다 합성이 더 좋은 방법이다.

  • 상속부모 클래스의 코드자식 클래스의 코드컴파일 시점에 통합하기 때문에 런타임에 유연하게 변경하지 못한다.

  • 합성인터페이스 재사용이기 때문에 구현을 효과적으로 캡슐화하고 런타임에 유연하게 변경 가능하다. (추상화에 의존하고 의존성 주입을 통해 런타임에 변경되는 경우를 생각해보면 쉽게 떠오른다.)

  • 추상화를 이용하면 상위 정책(개념)수준에서 흐름을 정의할 수 있으므로 설계가 유연해진다.

  • 추상화를 이용해 상위 정책 흐름을 정의한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는것을 의미한다.

  • 추상화를 구체화한 구체 클래스들의 협력은 결국 추상화를 이용해 정의한 상위 흐름을 그대로 따르게 된다. 이것은 협력의 일관성과도 관련이 있다.

  • 추상화를 이용한 협력의 일관성재사용 가능한 설계의 기본을 이룬다.

  • 항상 예외케이스를 두지 말고 협력을 일관성있게 유지하기 위한 방법을 고민하라.



객체지향 프로그래밍을 향해

협력, 객체, 클래스

객체지향은 말 그대로 객체를 지향하는 것이다. 이 문장은 굉장히 중요한데, 아직 객체지향 설계에 경험이 많지 않은 나를 비롯한 초보자들은 어떤 요구사항을 구현하기 위해 먼저 어떤 클래스들이 필요할까? 를 고민한다. 하지만 이는 객체지향적인 사고와는 거리가 멀다. 클래스객체를 코드로 구현하기 위한 수 많은 메커니즘 중 하나일 뿐이다. 진정한 객체디향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.

이를 위해서 어떤 클래스가 필요한지 고민하기 전에 어떤 객체들이 필요한지 고민하라. 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것이다. 따라서 클래스의 윤곽을 잡기 전에 어떤 객체들이 어떤 상태와 행동을 가지는지 먼저 결정해야 한다. 그리고 객체를 독립적인 존재가 아닌 협력하는 공동체의 일원으로 봐야한다.

객체는 다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재다. 객체를 협력하는 공동체의 일원으로 바라보는 것은 설계를 유연하고 확장 가능하게 만든다. 객체지향적으로 생각하고 싶다면 객체를 고립된 존재로 바라보지 말고 협력에 참여하는 협력자로 바라보기 바란다.

이렇게 객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현한다. 훌륭한 협력이 훌륭한 객체를 낳고 훌륭한 객체가 훌륭한 클래스를 낳는다.


도메인의 구조를 따르는 프로그램 구조

이시점에서 도메인이라는 용어를 살펴보자. 도메인이란 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야 를 일컫는다.

객체지향 패러다임이 강력한 이유는 요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있기 때문이다. 즉, 요구사항 분석 단계에서 드러난 개념들을 객체로 매끄럽게 연결할 수 있기 때문에 어플리케이션의 전체적인 흐름을 파악하기 용이하다. 이를 도메인이라는 키워드와 연결시키자면 도메인 개념들을 객체에 투영시킴으로써 객체들간의 협력 구조를 도메인 개념 사이에 관계와 최대한 유사하게 만들 수 있다. 이에 따라 일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다.

객체 사이의 관계를 최대한 도메인 개념 사이의 맺어진 관계와 유사하게 만듦으로써 프로그램의 구조를 이해하고 예상하기 쉽게 만들어야 한다.


클래스 구현하기

원칙에 따라 도메인 구조를 반영하는 적절한 클래스 구조를 그렸다고 가정하자. 이제 남은 일은 적절한 프로그래밍 언어를 이용해 이 구조를 구현하는 것이다.

이 구조를 코드상의 클래스로 구현하기 위해 어떤 점들을 주의깊게 살펴봐야할까? 우리는 앞서 챕터1에서 자율적인 객체의 중요성을 강조해왔다. 자율적인 객체구체적인 세부 구현 내용은 숨기고 인터페이스만을 외부에 드러내는 캡슐화를 통해 얻을 수 있다.고 하였다. 이를 클래스 구현에 적용한다면 가장 중요한 것이 바로 클래스의 경계를 구분짓는 것임을 짐작할 수 있다. 클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정하는 것이다.

그렇다면 왜 클래스의 관점에서 클래스의 내부와 외부를 구분해야 하는지 짐작해보자.

첫 번째로 경계의 명확성이 객체의 자율성을 보장하기 때문이다. 즉, 자율적인 객체를 보장하기 때문이다. 그리고 또 다른 중요한 이유는 바로 프로그래머에게 구현의 자유를 제공하기 때문이다.

자율적인 객체

먼저 두 가지 중요한 사실을 알아야 한다. 첫 번째 사실은 객체가 상태(state)행동(behavior)을 함께 가지는 복합적인 존재라는 것이다. 두 번째는 객체가 스스로 판단하고 행동하는 자율적인 존재라는 것이다. 이 두 가지 사실은 서로 연관되어 있다.

대부분의 객체지향 프로그래밍 언어들은 외부에서의 접근을 통제할 수 있는 접근 제어(access control) 메커니즘을 제공한다. 많은 프로그래밍 언어들은 접근 제어를 위해 public, protected, private와 같은 접근 수정자(access modifier)를 제공한다.

객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서다. 여기서 객체 내부세부 구현을 의미한다. 객체지향의 핵심은 스스로 상태를 관리하고, 판단하고, 행동하는 자율적인 객체들의 공동체를 구성하는 것이다. 객체가 자율적인 존재로 우뚝 서기 위해서는 외부의 간섭을 최소화해야 한다. 즉, 외부에서는 객체에게 필요한 것을 요청만 하고 객체 스스로가 최선의 방법을 결정할 수 있을 것이라는 점을 믿고 기다려야 한다.

일반적으로 객체의 상태는 숨기고 행동만 외부에 공개해야 한다. 이 때 외부에 공개되는 퍼블릭 인터페이스public으로 지정된 메서드만을 포함한다. 그 밖의 private 메서드, protected 메서드, 속성세부 구현의 영역에 포함된다.

프로그래머의 자유

객체의 내부, 외부의 경계를 명확하게 하는 이유(인터페이스와 구현을 분리해야 하는 이유)중 하나가 프로그래머에게 구현의 자유를 제공하는 것이라고 하였다. 이것이 무엇을 의미하는지 살펴보자.

프로그래머의 역할을 클래스 작성자클라이언트 프로그래머로 구분하는 것으로 시작하자. 클래스 작성자는 새로운 데이터 타입을 프로그램에 추가하고 클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용한다.

클래스 작성자클라이언트 프로그래머에게 필요한 부분만을 공개하고 나머지는 꽁꽁 숨긴다. 클라이언트 프로그래머가 세부 구현에 접근할 수 없도록 방지함으로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다. 이를 구현 은닉이라고 한다.

접근 제어 메커니즘은 프로그래밍 언어 차원에서 클래스의 내부와 외부를 명확하게 경계 지을 수 있게 하는 동시에 클래스 작성자가 내부 구현을 은닉할 수 있게 해준다.

구현 은닉클래스 작성자클라이언트 프로그래머 모두에게 유용한 개념이다. 클라이언트 프로그래머는 내부의 구현은 무시한채 인터페이스만 알고 있어도 클래스를 사용할 수 있기 때문에 머릿속에 담아야 하는 지식의 양을 줄일 수 있다. 클래스 작성자인터페이스를 바꾸지 않는 한 외부에 미치는 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다. 다시말해 public 영역을 변경하지 않는 한 코드를 자유롭게 수정할 수 있다는 것이다.

객체의 외부외 내부를 구분하면 클라이언트 프로그래머가 알아야 할 지식의 양이 줄어들고 클래스 작성자가 자유롭게 구현을 변경할 수 있는 폭이 넓어진다. 따라서 클래스를 개발할 때 마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 한다.

설계가 필요한 이유는 변경을 위해서라는 점을 기억하자. 객체지향 언어는 객체 사이의 의존성을 적절히 관리함으로써 변경에 대한 파급효과를 제어할 수 있는 다양한 방법을 제공한다. 객체의 변경을 관리할 수 있는 기법 중에서 가장 대표적인 것이 바로 접근제어다.

우리는 변경될 가능성이 있는 세부적인 구현 내용을 private 영역 안에 감춤으로써 변경으로 인한 혼란을 최소화 할 수 있다.


협력에 관한 짧은 이야기

객체가 다른 객체와 상호작용 할 수 있는 유일한 방법은 메시지를 전송하는 것 뿐이다. 다른 객체애개 요청이 도착했을 때 해당 객체가 메시지를 수신했다고 표현한다.

메시지를 수신한 객체는 자신만의(자율적인) 방법으로 메시지를 처리한다. 이처럼 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드라고 한다.

메시지와 메서드를 명확하게 구분하는 것이 중요하다. 뒤에서 살펴보겠지만 메시지와 메서드의 구분으로부터 다형성의 개념이 출발한다.

메시지를 수신한 객체는 스스로 적절한 메서드를 선택한다. 자바와 같은 정적 타입 언어에서는 해당되지 않지만 루비스몰토크같은 동적 타입 언어에서는 다른 시그니처를 가진 메서드를 통해서도 메시지에 응답 가능하다. 결국 메시지를 처리하는 방법을 결정하는 것은 객체 스스로의 문제인 것이다. 이것이 객체가 메시지를 처리하는 방법을 자율적으로 결정할 수 있다고 말했던 이유다.


상속과 다형성

컴파일 타임 의존성과 런타임 의존성

먼저 의존성에 대해서 알아보자. 의존성이란 어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 객체의 메서드를 호출할 경우 두 클래스 사이의 의존성이 존재한다고 말한다. 여기서 주의해야 할 점은 컴파일 시점의 의존성실행 시점의 의존성이 서로 다를 수 있다는 것이다.

다시말해 클래스 사이의 의존성과 객체 사이의 의존성이 서로 다를 수 있다.

위 코드는 전형적인 의존성 주입의 모습을 보여준다. 여기서 주입되는 의존 객체들은 ChildA, ChildB이다. 즉, 컴파일 시점의 코드상에서 SomethingParent에 의존하고 있지만 실행 시점에서는 ChildA 또는 ChildB에 의존하게 된다.

바로 이것이 컴파일 시점 의존성과 실행 시점 의존성이 서로 다를 수 있다는 것의 의미이다. Somethingparent의 구체적인 타입이 무엇인지 신경쓰지 않는다. 다만 parent가 이해할 수 있다고 생각하는 메시지를 전송(요청)할 뿐이다. 그리고 메시지를 수신하는 객체가 무엇이냐(ChildA or ChildB)에 따라 실행되는 메서드가 달라진다.

이처럼 코드의 의존성과 실행 시점의 의존성이 서로 다른 설계가 가지는 특징은 유연하고 쉽게 재사용 가능하며, 확장 가능한 설계라는 것이다. 또한 코드를 이해하기 어려워진다. 그 이유는 코드를 이해하기 위해서 코드 뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다.

설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다는 사실을 기억하자. 반면 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉬워지지만 재사용성과 확장 가능성은 낮아진다는 사실도 기억하자.


상속과 인터페이스

상속이 가치있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.

인터페이스 상속으로 ‘설계의 재사용’이 가능하다. 좀 더 상위 수준의 재사용이다.

상속의 가치는 부모 클래스의 모든 인터페이스를 재사용할 수 있다는 점에 있다. 일반적으로 상속을 할 때 부모 클래스의 인스턴스 변수, 메서드와 같이 세부 구현을 재사용 하려는 경향이 강한데, 이는 좋지 못한 방법이다. 뒤에서 좀 더 다루겠지만 코드 재사용을 위해서는 상속 보다는 합성이 훨씬 더 유연하고 좋은 방법이다.

다시한번 강조하지만 상속이 가치있는 이유는 부모 클래스의 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. 인터페이스는 객체가 이해할 수 있는 메시지 목록을 정의한다는 것을 기억하자. 상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다. 결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스의 부모 클래스와 동일한 타입으로 간주할 수 있다.

위 문장을 다형성 측면에서 다시 설명한다면 (메시지를 전송하는)외부 객체는 메시지를 이해할 수만 있다면(동일한 인터페이스를 갖는 동일한 타입이라면) 그 객체가 어떤 클래스의 인스턴스인지(부모 클래스의 인스턴스인지, 자식 클래스의 인스턴스인지) 상관하지 않는다는 것이다. 즉, 동일한 메시지에 대해 다양한 메서드를 실행 가능한 것이다.

정리하자면 자식 클래스는 상속을 통해 부모 클래스의 ‘인터페이스’를 물려받기 때문에 부모 클래스 대신 사용될 수 있다. 컴파일러는 코드 상에서 부모 클래스가 나오는 모든 장소에서 자식 클래스를 사용하는 것을 허용한다.


다형성

다시한번 강조하지만 메시지와 메서드는 다른 개념이다. 코드 상에서 Parent 클래스에게 메시지를 전송(doing)하지만 실행 시점에 실제로 실행되는 메서드는 Something과 협력하는 객체의 구체 타입(클래스)가 무엇이냐(ChildA or ChildB)에 따라 달라진다. 다시말해서 Something은 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라 부른다.

다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다. 이처럼 다형성은 컴파일 시간 의존성과 실행 시간 의존성을 다르게 만들 수 있는 객체지향의 특성을 이용해 서로 다른 메서드를 실행할 수 있게 한다.

다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다. 따라서 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다. 다시 말해서 인터페이스가 동일해야 한다.

ChildAChildB가 다형적인 협력에 참여할 수 있는 이유는 이들이 Parent로 부터 동일한 인터페이스(doing)을 물려받았기 때문이다. 그리고 이 두 클래스의 인터페이스를 통일하기 위해 사용한 구현 방법이 상속인 것이다.

다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다. 다시 말해 메시지메서드실행 시점에 바인딩한다. 이를 지연 바인딩 또는 동적 바인딩이라고 부른다.


구현 상속과 인터페이스 상속

상속에 대해 조금 더 첨언해보겠다. 앞서 상속데이터메서드를 재사용하기 위해서가 아니라 인터페이스를 재사용할 때 그 가치가 드러난다고 하였다. 이를 구현 상속인터페이스 상속으로 분류할 수 있다.

흔히 구현 상속서브클래싱이라고 부르고 인터페이스 상속서브타이핑이라고 부른다. 순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것을 구현 상속이라고 부른다. 그리고 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 사용하는 것을 인터페이스 상속이라고 부른다.

상속은 구현 상속이 아니라 인터페이스 상속을 위해서 사용해야 한다. 인터페이스를 재사용할 목적이 아니라 구현을 재사용하기 위해 상속을 사용하게 되면 변경에 취약한 코드를 낳게 될 확률이 상당히 높아진다.


추상화와 유연성

추상화의 힘

추상적인 개념들은 구체적인 세부 사항이 아니라 인터페이스에 초점을 맞춘다. 즉, ‘HOW’(DB로 저장, 파일로 저장)와 같이 구체적인 방법이 아닌 ‘WHAT’(저장)과 같이 보다 추상적이고 상위 수준에서 정의한다. 이처럼 추상적인 개념은 같은 계층에 속하는 클래스들이 공통으로 가질 수 있는 인터페이스를 정의하며 구현의 일부(추상클래스의 경우) 또는 전체(자바 인터페이스의 경우)를 자식 클래스가 결정할 수 있도록 결정권을 위임한다.

추상화를 사용할 경우의 첫 번째 장점은 추상화의 계층만 따로 떼어놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다. 즉, 복잡한 세부 사항에 얽매이지 않고 상위 정책으로만 쉽고 간단하게 표현할 수 있다. 추상화의 이런 특징은 세부사항에 얽매이지 않고 상위 개념만으로도 도메인의 중요한 개념을 설명할 수 있게 한다.

추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미한다. 위의 간단한 코드에서 코드의 흐름은 Something에서 Parent를 향해 흐른다. 그리고 ChildAChildB와 같은 새로운 자식 클래스들은 추상화를 이용해서 정의한 상위의 협력 흐름을 그대로 따르게 된다. 이 개념은 매우 중요한데 재사용 가능한 설계의 기본을 이루는 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 메커니즘을 활용하고 있기 때문이다.

두 번째 장점은 첫 번째 장점으로부터 유추 가능한데, 바로 추상화를 이용하면 보다 유연한 설계가 가능하다는 것이다. 즉, 추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.


유연한 설계

책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 대부분의 경우 좋지 않은 선택이다. 항상 예외 케이스를 최소화 하고 일관성을 유지할 수 있는 방법을 선택하자.

위 코드에서 본래 요금 계산의 책임을 discountPolicy가 수행하던 것을 discountPolicy가 null인 경우에 요금 계산에 대한 책임을 Movie가 직접 처리한다. 이처럼 책임의 위치를 결정하기 위해 조건문을 사용하는 것은 옳지 못하다. 또한 책임MovieDiscountPolicy 양쪽으로 분산되어 있기 때문에 응집도 측면에서도 좋지 못한 선택이다.

이를 해결하기 위해서는 할인 조건이 존재하지 않음(NoneDiscountPolicy)을 표현하게끔 DiscountPolicy의 자식 클래스로 구현하여 해당 객체에 책임을 할당하는 것이 더 좋은 선택이다.

즉, 기존의 코드를 수정(if 조건으로 예외 처리)하지 않고 단순히 NoneDiscountPolicy만을 추가함으로써 변경의 파급효과를 제어한다.


코드 재사용

상속은 코드를 재사용하기 위해 널리 사용되는 방법이다. 그러나 널리 사용되는 방법이라고 해서 가장 좋은 방법인 것은 아니다. 코드 재사용을 위해서라면 상속보다는 합성이 더 좋은 방법이다. 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다. 그렇다면 상속보다 합성이 더 선호되는 이유는 무엇일까?


상속

코드 재사용을 위해서 상속을 사용할 경우 두 가지 관점에서 안좋은 영향을 미친다. 한 가지는 상속은 캡슐화를 위반한다는 것이고 또 다른 하나는 설계를 유연하지 못하게 한다는 것이다.

상속의 가장 큰 문제점은 캡슐화를 위반한다는 것이다. 상속을 이용하기 위해서는 부모 클래스의 구조를 잘 알고 있어야 한다. 부모 클래스의 세부 구현에 대해서 이해하고 있어야 한다. 부모 클래스의 private 메서드가 언제 호출되는지와 같은 유의 사항을 잘 알고 있어야 한다. 결과적으로 부모 클래스의 세부 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다. 캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높인다. 결과적으로 상속을 과도하게 사용한 코드는 변경하기도 어려워진다.

상속의 두 번째 단점은 설계가 유연하지 않다는 것이다. 상속은 부모 클래스와 자식 클래스의 관계를 컴파일 시점에 결정한다. 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.

상속은 컴파일타임에 부모 클래스의 코드와 자식 클래스의 코드를 하나로 ‘결합’시키는 메커니즘이다. 이에 따라 런타임에 부모 클래스의 코드와 자식 클래스의 코드를 분리할 수 없게되어 부모 클래스에 강하게 결합한다.


합성

상속이 부모 클래스의 코드와 자식 클래스의 코드컴파일 시점에 하나의 단위로 강하게 결합하는데 비해 합성을 이용하면 인터페이스를 통해 약하게 결합된다. 이에 따라 클라이언트 객체합성 객체의 세부 구현에는 전혀 알지 못한다. 이처럼 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.

합성상속이 가지는 두 가지 문제점을 모두 해결한다. 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화 할 수 있다. 또한 의존하는 인스턴스(합성 객체)를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.

상속은 클래스를 통해 강하게 결합되는데 비해 합성은 메시지를 통해 느슨하게 결합된다. 따라서 코드 재사용을 위해서는 상속보다는 합성을 선호하는 것이 더 좋은 방법이다.

그렇다고 해서 상속을 절대 사용하지 말라는 것은 아니다. 대부분의 설계에서는 상속과 합성을 ‘함께’ 사용해야 한다. 이처럼 코드 재사용의 경우에는 합성 > 상속이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용한다.

참고 및 출처

  • 오브젝트