한국어 | English | 日本語
8.8년차 Web Application Developer 웹 개발자
기술·개발
engineering
웹 프론트엔드 및 백엔드 개발에 관련된 내용들을 주로 다룹니다

0. 객체지향 프로그래밍에서의 제 1, 2 원칙

Composition vs Inheritance
필자의 대학교 시절, 객체지향 프로그램은 다형성과 상속뿐이었다. 기업에서 개발하며 깊게 체감하고 배운것은 객체지향 프로그램은, 당연하지만, 학문이 아닌 실전이라는 것이다. 디자인 패턴을 학습하기에 있어 왜, OOP 의 제 1, 2 원칙을 다루는지 의아할 수 있다. 디자인 패턴은 '객체지향 패러다임에서 더 좋은 코드란 무엇인가에 대한 고민의 결과'이다. 이를 학습하기에 앞서 객체지향 프로그래밍의 원칙을 알아야지만, 다양한 디자인 패턴의 의의를 제대로 체득할 수 있다.
학교에서 OOP 를 처음 배울때 접하는 '상속'은 학문상의 순서에 따라 가장 처음 배우는것일뿐 실제 산업에서는 가장 사용하지 말아야할 안티패턴이다. 본 글에선 '상속'을 어떻게 버리는지, 왜 버리는지에 대해 제 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 원칙

위에서 서술한 내용은 결국 아래 두 원칙으로 짧게 정리될 수 있다.

‘클래스 - 상속’보다 ‘인터페이스 - 구성’을 사용하라

‘상속’은 부모 클래스가 가진 모든것을 필요에 상관없이 갖게된다.
마치 레고처럼, 원하는 구현들을 선택적으로 갖는 ‘구성’이 더 확장성이 높다.

‘구현 클래스’보다 ‘인터페이스’로 구성하라

구현은 언제든지 바뀔 수 있다. 클래스 내 로직 구성 시 구현 클래스가 아닌 인터페이스를 통해 유연하게 구성하자.

class RedHeadDuck {
	class SuperFlyable; <-- 구현 클래스 
	class ShoutQuackable; <-- 구현 클래스 
	doFly() { SuperFlyable.fly() }
	doQuack(){ ShoutQuackable.quack() }
}
class RedHeadDuck {
	interface Flyable = new SuperFlyable(); <-- 인터페이스 (다양한 구현 낄수있음)  
	interface Quackable = new ShoutQuackable(); <-- 인터페이스 (다양한 구현 낄수있음)  
	doFly() { Flyable.fly() }
	doQuack(){ Quackable.quack() }
}

0. 객체지향 프로그래밍에서의 제 1, 2 원칙
Author
Aaron
Posted on
Licensed Under
CC BY-NC-SA 4.0
CC BY-NC-SA 4.0
같은 카테고리 내 다른 글들
최근에 게시된 글들
토스트 예시 메세지