한국어 | 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
같은 카테고리 내 다른 글들
최근에 게시된 글들
Giscus CSS 커스터마이징으로 알아보는 브라우저의 보안 정책들
블로그를 새로 만들면서 기존 Hexo 블로그에서 사용하던 Discus 를 더 이상 사용하지 않고 현재 가장 편리하게 쓸 수 있는 Giscus 를 사용하게되었다. Giscus 댓글 컴포넌트를 나의 블로그 CSS 테마에 맞추기 위해 커스터마이즈한 CSS 만들어 외부에서 Giscus 에 주입하려하였다. 어째서인지 마음만큼 잘 되지 않았고, 헤매던 와중에 프론트엔드 개발자라면 질리도록 보는 CORS 뿐만 아니라 Mixed Content 와 PNA (Private Network Access) 에 대한 보안 정책들이 엉켜서 동작되지 않았던 이유들을 설명하고, Giscus 내 CSS 테마를 올바르게 적용하는 법에 대해서도 안내한다.
LLM 필터가 앗아가는 대화의 근육과 소통의 양식
대화의 무례함을 거르고 정제된 답변을 내놓는 LLM 도구가 일상화된 시대, 우리는 더 사려 깊은 대화를 하고 있는 것일까? 실시간 소통에서 오는 수많은 실패를 통해 다듬어져야 할 대화의 능력이 외부 도구에 의존하며 퇴화하고 있는 현상과, 그것이 가져올 사회적 불안 및 세대적 행동 양식의 변화를 고찰한다.
시니어 채용 시 유리한 연봉협상 시점과 전략
연봉협상은 단순히 숫자를 주고받는 과정이 아니라 심리적인 타이밍의 싸움이다. 사측의 입장에서 후보자가 계산적인 태도로 변하기 쉬운 최종 합격 이후보다, 채용 절차의 초기 단계부터 차근차근 협상을 진행하는 것이 왜 더 효율적이고 솔직한 자원 공유를 이끌어낼 수 있는지 분석한다.
토스트 예시 메세지