2. 디자인 패턴의 제 1, 2 원칙

설명에 사용할 코드는 Java-like Pseudo Code 입니다.


내게 대학교 시절에 객체지향 프로그램은 다형성과 상속뿐이었지만 책이 아닌 실제 프로그래밍으로 접한 객체지향 프로그램은 학문이 아니라 실전이었습니다. 왜 이걸 배웠고 이게 사실 어떤 의미를 갖는건지 그제서야 깨달을 수 있었습니다. 여기서는 짧게 우리가 생각했던 상속을 살펴보고 디자인 패턴으로 도약하기 위해 상속을 버리는 두 개의 원칙을 익히려고 합니다.

상속 = 객체지향 프로그램?

처음 엔터프라이즈 객체지향 프로그램을 작성한다고 가정해봅시다. 학교에서 공부한대로라면 객체지향 프로그램은 상속이라고 배웠습니다. 그래서 우리는 과감히 상위 클래스를 만들고 이를 상속하여 하위 개념에 해당하는 클래스를 활용할 것입니다. 그리고 객체지향 프로그래밍 프로젝트 #1 이라고 이름 짓겠죠. 코드는 아래 헤드퍼스트 책의 예제와 같을 것입니다.

처음 배웠던 상속

  • 상위 클래스 + 상위 행위
    1
    2
    class Duck
    { swim(), display(), fly(), quack() }
  • 하위 클래스 + 상위 행위 확장
    1
    2
    3
    4
    class RedHeadDuck extends Duck 
    { swim(), display(), fly(), quack() }
    class RubberDuck extends Duck
    { swim(), display(), fly(){ null }, quack() }
    상속을 사용하였더니 상위 Duck 클래스에 있는 모든 상위 행위들을 하위 Duck 클래스들이 모두 갖게됩니다. 어떤 하위 Duck 클래스는 의지와 상관없이 갖고싶지 않지만 무조건 모든 상위 행위를 갖고 확장해야합니다. 개발에 있어 불필요한 제약을 갖게되는 것입니다. 그럼 아래와 같이 선택적으로 행위를 가져갈 수 있도록 행위를 인터페이스로 분리해 가져보겠습니다.

상속 대신 인터페이스

  • 상위 클래스
    1
    2
    class Duck 
    { swim(), display() }
  • 행위 인터페이스
    1
    2
    interface Flyable { fly() }
    interface Quackable { quack() }
  • 하위 클래스 + 행위 인터페이스 구현
    1
    2
    3
    4
    class RedHeadDuck extends Duck implements Flyable, Quackable
    { swim(), display(), fly(), quack() }
    class RubberDuck extends Duck implements Quackable
    { swim(), display(), quack() }
    드디어 상위 클래스에 속하던 행위를 인터페이스로 분리하여 하위 클래스에 원하는 행위들만 붙일 수 있게 되었습니다. 하지만 두 개의 오리가 같은 소리를 갖는다면 Quackable 오리 각각에 quack() 을 똑같이 구현해주어야 합니다. 두 개의 오리면 괜찮겠지만 100 중 70 종의 오리가 같은 소리를 낸다면 70 개의 같은 quack() 구현 코드를 작성해야합니다. 그 중 몇 개의 quack() 소리를 다른 타입으로 바꾸려해도 같은 반복작업이 생기게 됩니다. 그럼 행위 인터페이스 구현을 따로 만들어서 원하는 구현을 선택적으로 가져보면 더 좋지 않을까요?

인터페이스 ‘구현’이 아닌 ‘구성’

  • 상위 클래스
    1
    2
    3
    4
    class Duck 
    { interface Flyable;
    interface Quackable;
    swim(), display(), doFly(){ Flyable.fly() }, doQuack(){ Quackable.quack() } }
  • 행위 인터페이스
    1
    2
    interface Flyable { fly() }
    interface Quackable { quack() }
  • 행위 인터페이스 구현
    1
    2
    3
    4
    class NotFlyable implements Flyable { fly(){...} }
    class SuperFlyable implements Flyable { fly(){...} }
    class ShoutQuackable implements Quackable { quack(){...} }
    class QuiteQuackable implements Quackable { quack(){...} }
  • 하위 클래스 + 행위 인터페이스 구현
    1
    2
    3
    4
    5
    6
    7
    8
    class RedHeadDuck extends Duck
    { interface Flyable = class SuperFlyable();
    interface Quackable = class ShoutQuackable();
    swim(), display(), doFly(){ Flyable.fly() }, doQuack(){ Quackable.quack() } }
    class RubberDuck extends Duck
    { interface Flyable = class NotFlyable();
    interface Quackable = class QuiteQuackable();
    swim(), display(), doFly(){ Flyable.fly() }, doQuack(){ Quackable.quack() } }
    인터페이스를 구현하는것이 아닌 구성을 통해 클래스 내부 변수로 갖게되면서 원하는 행위 인터페이스 구현을 마음껏 갖고 바꿀 수 있게 되었습니다. 이로써 인터페이스를 대학교때 배웠듯이 클래스의 템플릿이다.라는 이해에서 조금 더 나아가 클래스가 갖는 행위나 특성을 담을 수 있는 하나의 ‘변수’**로 생각할 수 있으면 좋을것 같습니다. 이것이 우리가 **다형성을 배운 이유이기도 합니다.

디자인 패턴의 제 1, 2 원칙

위에서 배운 내용은 사실 아래 두 원칙에 해당합니다.
복습 겸 한번 더 복기하고 다음 챕터로 넘어가도록 하겠습니다.

구현보다 인터페이스에 맞춰서 코딩한다.

구현은 언제나 바뀔 수 있다. 인터페이스를 통해 유연하게 구현하자

fly(), quack() 같은 행위를 클래스 내부에서 구현하지 않고 인터페이스로 대체함으로써 필요한 것과 필요하지 않은 것들을 분리할 수 있습니다.

인터페이스는 ‘상속’보다는 ‘구성’으로 사용하자

인터페이스를 ‘상속’이 아닌 ‘구성’ 시 원하는 구현을 붙였다 떼었다 할 수 있다.

인터페이스를 ‘상속’하면 인터페이스의 모든 함수들을 그를 상속하는 클래스 안에서 구현해야합니다. 구현이 클래스 안에 갖혀버림과 동시에 구현과 클래스 내부간의 강결합이 생깁니다.

반면 ‘구성’을 사용하면 인터페이스를 구현한 구현 클래스 단 하나로 어느곳에서든지 사용 가능합니다. 레고처럼 붙였다 떼었다 할 수 있어 쉽게 바꿀 수 있고, 구현 클래스 로직과 그걸 사용하는 클래스 간 결합이 풀리게 됩니다.

Author

Aaron Ryu

Posted on

2019-02-21

Updated on

2020-07-04

Licensed under

Comments