0. 객체지향 프로그래밍에서의 제 1, 2 원칙
상속 = 객체지향 프로그램?
개발자가 처음 엔터프라이즈 객체지향 프로그램을 작성한다고 가정해보자. 학교에서 공부한대로라면 객체지향 프로그램은 무릇 상속이라고 배웠듯이, 과감히 부모 클래스를 만들고 이를 상속한 자식 클래스들을 활용할 것이다. 코드는 아래 Head-first 책의 예제와 같을것이다.
처음 배운 상속
- 부모 클래스 + 부모 함수
class Duck
{ swim(), display(), fly(), quack() }
- 부모 클래스 + 부모 함수 확장 = 자식 클래스
class RedHeadDuck extends Duck
{ swim(), display(), fly(), quack() }
class RubberDuck extends Duck
{ swim(), display(), fly(){ null }, quack() }
상속을 사용하면 부모 Duck 클래스에 있는 모든 함수들을 자식 Duck 클래스들이 모두 갖게된다. 문제점은 부모에게 상속을 받으면 재산과 빚을 같이 받는것처럼 자식 Duck 클래스는 의지와 상관없이 갖고싶지 않은 모든 부모 함수들을 가진채 확장해야한다는 점이다. 개발에 있어 불필요한 제약을 갖게되는 것이다.
이것을 해결하기 위해 ‘분리 가능한 가장 작은 단위의 함수’들을 인터페이스로 정의하여, 개발하려는 클래스에 필요한 함수들을 가진 Interface 를 골라담아 확장하면 된다.
상속 대신 인터페이스
- ‘행위 단위’ 인터페이스
interface Flyable { fly() }
interface Quackable { quack() }
- ‘행위 단위’ 인터페이스 조합 및 구현
class RedHeadDuck implements Flyable, Quackable {
fly() { ... }
quack() { ... }
}
class RubberDuck implements Quackable {
quack() { ... }
}
이로써 부모 클래스에 속하던 행위들을 인터페이스로 세분하여 구현 클래스에 원하는 행위들만 붙일 수 있게 되었다. 하지만 ‘행위 단위’ 인터페이스를 골라담아 확장할때 매번 구현해줘야한다는 문제가 있다. 개발자는 귀찮음의 민족 아니겠는가, 매번 구현이 귀찮으니 ‘행위 단위’ 인터페이스를 ‘행위 단위’ 클래스로 미리 다 구현을 해놓은채 해당 ‘행위’들을 선택적으로 가져다 사용하도록 하는 경지에 오른다.
인터페이스 ‘구현’이 아닌 ‘구성’
- ‘행위 단위’ 인터페이스
interface Flyable { fly() }
interface Quackable { quack() }
- ‘행위 단위’ 클래스 (인터페이스 구현)
class NotFlyable implements Flyable { fly() { ... } }
class SuperFlyable implements Flyable { fly() { ... } }
class ShoutQuackable implements Quackable { quack() { ... } }
class QuiteQuackable implements Quackable { quack() { ... } }
- ‘행위 단위’ 클래스 조합
class RedHeadDuck {
interface Flyable = new SuperFlyable();
interface Quackable = new ShoutQuackable();
doFly() { Flyable.fly() }
doQuack(){ Quackable.quack() }
}
class RubberDuck {
interface Flyable = new NotFlyable();
interface Quackable = new QuiteQuackable();
doFly(){ Flyable.fly() }
doQuack(){ Quackable.quack() }
}
매번 인터페이스를 구현하는것이 아닌 미리 구현되어있는 인터페이스의 구현체들을 선택적으로 구성하게된다. 원하는 행위 인터페이스 구현을 마음껏 붙였다 떼었다 바꿀 수 있게 되었다. 이로써 인터페이스는 대학교때 배운대로 클래스의 템플릿이다.라는 개념에서 더 나아가 **행위나 특성을 구현한 클래스를 담을 수 있는 하나의 ‘변수’**로 생각하면 좋을것 같다. 이것이 우리가 다형성을 배운 이유이기도 하다.
객체지향 프로그래밍에서의 제 1, 2 원칙
위에서 서술한 내용은 결국 아래 두 원칙으로 짧게 정리될 수 있다.
‘클래스 - 상속’보다 ‘인터페이스 - 구성’을 사용하라
‘상속’은 부모 클래스가 가진 모든것을 필요에 상관없이 갖게된다.
마치 레고처럼, 원하는 구현들을 선택적으로 갖는 ‘구성’이 더 확장성이 높다.
‘구현 클래스’보다 ‘인터페이스’로 구성하라
구현은 언제든지 바뀔 수 있다. 클래스 내 로직 구성 시 구현 클래스가 아닌 인터페이스를 통해 유연하게 구성하자.
- X 지양: ‘구현 클래스’로 구성
class RedHeadDuck {
class SuperFlyable; <-- 구현 클래스
class ShoutQuackable; <-- 구현 클래스
doFly() { SuperFlyable.fly() }
doQuack(){ ShoutQuackable.quack() }
}
- O 지향: ‘인터페이스’로 구성 - 언제든 ‘구현 클래스’를 바꿔낄 수 있다
class RedHeadDuck {
interface Flyable = new SuperFlyable(); <-- 인터페이스 (다양한 구현 낄수있음)
interface Quackable = new ShoutQuackable(); <-- 인터페이스 (다양한 구현 낄수있음)
doFly() { Flyable.fly() }
doQuack(){ Quackable.quack() }
}