객체지향 프로그래밍(OOP) – 2 : Composition and Inheritance

This entry is part 2 of 3 in the series Python OOP

 

 

 

흔하디 흔한 제목이고 많은 소개 자료가 있어 굳이 블로그에 포스트할 필요가 있나 싶었지만, 최근 보게 된 책에서 내용을 잘 설명해 주어서 책의 흐름을 따라가면서 소개해 보고자 한다. 내가 읽은 책은 『Python3 Object Oriented Programming – Harness the power of Python 3 objects, Dusty Phillips, PACKT』 이다. 이 책은 객체지향 프로그래밍 (OOP) 관점도 훌륭하게 소개시켜 주고 있지만, Python 3 에 꼭 필요한 내용들을 다루고 있어서 정말 추천하고 싶은 책이다.

지난번 포스트에서 객체 (object) 와 객체를 구성하는 속성 (attribute) 과 행위 (behavior), 그리고 정보 은닉 (information encapsulation) 에 대해서 알아 보았다면 이번엔 합성 (Composition) 과 상속 (Inheritance) 에 대해서 알아보자.


<합성과 상속; Composition and inheritance>

지금까지 다룬 내용으로는 어떻게 추상화를 해야 하는지 알기 어렵다. 하지만 대부분의 디자인 패턴 (design pattern) 들은 두 가지 기본 방법들에 의존한다. 그것이 합성 (composition)상속 (inheritance) 이다. ( composition 의 경우 구성으로 번역할지, 합성으로 번역할지 애매하다. )

Composition 은 여러 객체를 합하여 다른 하나로 만드는 것을 말한다. 어떤 객체가 다른 객체의 일부분인 경우다. 자동차가 엔진, 트랜스미션, 헤드라이트 등으로 구성되는 것 같은 경우이다.

Composition 과 Inheritance 의 개념을 설명하기 위해서 체스 게임의 예로 들겠다. 체스 게임은 두 명의 플레이어 ( player ) 가 8 x 8 격자로 64개의 위치 ( position ) 을 갖는 체스 판 ( chess board ) 과 각자 16개의 체스 말 ( piece ) 을 한 세트 ( chess set )로 하여 시작한다. 각자 돌아가며 ( turn ) 정해진 규칙에 따라 체스 말을 움직여서 최종 승자를 가리는 게임이다. 게임으로 만들기 위해서는 체스 판이 컴퓨터 스크린에 ( screen ) 에 그려져야 한다.

위의 프로그램 상세 (description) 에서 이탤릭체로 쓴 부분이 프로그램에서 모델링이 필요한 객체들이다. ( 프로그램 상세를 먼저 글로 잘 옮겨 놓는 작업이야 말로 객체지향 프로그래밍의 시작이라고 할 수 있다! )

다른 고려할 점들은 집어 치우고, 먼저 제일 단순한 것을 생각해 보자. ‘체스는 두 명의 플레이어가 체스 판을 두고 체스 말을 움직여서 하는 게임이다.‘ 이 부분을 그림으로 나타내면 다음과 같을 것이다.

이것은 무엇인가? 이전에 살펴 보았던 클래스 다이어그램과는 조금 다르다. 이것은 객체 다이어그램 (object diagram) 이다. 또는 인스턴스 다이어그램이라고도 한다. 이것은 특정 상태에 있는 시스템을 기술하고, 특정 객체의 인스턴스를 표현한다. 기억할 점은 두 명의 플레이어 (player1, player2) 모두 동일한 클래스 (player class) 의 인스턴스라는 점이다. 따라서 이것을 클래스 다이어그램으로 나타내면 다른 형태가 된다.

여기서 UML 을 설명하려는 것은 아니기 때문에 다시 Composition 에 집중해 보자. Chess Set 의 구성에 대해 생각해 보자. Player 에 대해서 추가적으로 모델링 할 수도 있지만 우리 시스템이 플레이어의 속성이나 행동에 대해서 모델링해야 할 부분은 여기서는 더는 없다. 체스 세트 (Chess Set) 은 체스 보드 (chess board) 와 32개 체스 말 (chess pieces) 로 구성된다. 체스 보드는 64개의 격자로 구성된다. 누군가는 체스 말들은 다른 말들로 교체 가능하기 때문에 체스 세트에 일부분이다 라고 말하기 힘들다고 주장 할지도 모르겠다. 여기서 새로운 개념이 등장하는데 그것이 집합 (Aggregation) 이다. Aggregation 은 Composition 과 거의 같지만 객체들이 독립적으로 존재가능하다는 점에서 Composition 과 다르다. 따라서, 엄밀히 정의하자면 체스 말 (chess piece) 와 체스 세트 (chess set) 는 aggregation 관계라고 할 수 있다.

이 개념이 조금 헷갈리는 경우라면 객체의 생애주기 관점에서 접근해 보는 것도 좋은 방법이겠다. Aggregation 의 경우 집합을 구성하는 상위 개념의 객체가 사라지더라도 하위 객체가 독립적으로 존재할 수 있다. 반면에 Composition 의 경우는 집합 (합성)을 구성하는 상위 존재가 사라지면 하위 존재도 생명을 다하게 된다. 클래스 다이어그램 상에서는 Composition 은 속이 찬 마름모로 표현하고, Aggregation 은 속이 빈 마름모로 표시한다.

설계 단계에서 Aggregation 과 Composition 을 구분할 수는 있겠으나 큰 의미를 부여하지는 말자. 구현 단계를 지나면 거의 유사하게 행동하기 때문이다. ( 또 다른 이유로는, 쓸데없이 클래스 다이어그램의 해석을 두고 아까운 시간을 낭비할 필요가 없기 때문이다. ) 하지만 알고 있는것이 나쁠 이유는 없다.

상속 (Inheritance)

지금까지 연관 (Association), 합성 (Composition), 집합 (Aggregation) 에 대해서 알아 보았다. 하지만 이들이 우리가 사용할 전부는 아니다. 플레이어는 사람이거나 어떤 인공지능이라고 간주할 수 있다.  예를 들어서 딥 블루가 플레이어 라거나 게리 가스파로프가 플레이어라고 할 수 있겠다. ( Deep Blue is a player or Gary Kasparov is a player ). 영어 문장의 is a 에 해당되는 관계가 바로 상속 (Inheritance) 이다. 상속은 객체지향에서 가장 잘 알려져 있으면서도 때론 자주 남용되고 있다. 할아버지로부터 이름과 유전적 특징이 상속되듯이, 객체지향 프로그래밍에서 하나의 클래스는 다른 클래스로부터 속성 (attribute) 과 메쏘드 (method) 를 상속받는다.

예를들어, 체스 세트 (chess set) 에 32개의 체스 말 (chess piece) 이 있지만, 말들은 모두 여섯 종류로 구성된다. (pawns, rooks, bishops, knights, king, and queen) 이들 말들은 다른 움직임을 갖는다. 이들 체스 말 클래스는 색깔 (color), 말이 속한 체스 세트 (chess set), 고유의 모양 (컴퓨터 화면에 그려질) 과 정해진 움직임 (move) 을 갖는다. 이들 여섯 종류의 말들이 공통적인 체스 말 (chess piece) 로 부터 어떻게 상속 (inheritance) 받는지 아래 그림을 통해서 확인해 보자.

빈 삼각형으로 구성된 화살표가 상속 (inheritance) 를 가리킨다. 화살표 끝이 닿은 클래스가 상속의 부모 클래스이고 선이 출발하는 클래스가 자식 클래스이다. 여섯 종류의 클래스들은 모두 Piece 클래스를 상속받는다. Piece 클래스는 체스 세트 (chess set) 와 색깔 (color) 을 가지고 있고, 이들 속성은 자식 클래스인 여섯개의 말 클래스들에 상속된다. 한편, 각각의 자식 클래스들은 고유의 모양 (shape) 을 가지고 있고, 자신만의 움직임 (move) 을 메쏘드로 갖고 있다.

사실 모든 체스 말 (chess piece) 클래스의 종속 클래스 (subclass) 들은 움직임 (move) 을 갖고 있어야 한다. 그렇지 않으면 체스 보드에서 체스 말을 움직일 때 혼란스러울 것이다. 현재 설계에서는 만약 새로운 체스 말이 추가되더라도 움직임 (move) 메쏘드를 갖지 않아도 된다. 이 경우 체스 보드가 체스 말을 스스로 옮기려 한다면 아마 기권하고 말 것이다. (새로운 체스 말의 움직임을 체스 보드도 모르기 때문이다.)

위와 같은 상황을 고려하여, 체스 말 (Piece) 에 더미 (dummy) 움직임 (move) 메쏘드를 추가 할 수 있다. 그렇게 되면 Piece 를 상속받는 종속 클래스들 (subclasses) 들은 모두 움직임 (move) 메쏘드를 재정의 (override) 할 수 있게 된다. Piece 클래스의 구현은 큰 의미가 없고 Piece 자체가 움직일 일은 없다. 이런 재정의 (또는 오버라이딩 이라고도 한다.) 방식은 객체지향을 구현하는데 강력한 무기가 된다.

체스 말 (chess piece) 클래스 자체는 사실 움직임 (move) 을 직접 구현할 필요가 전혀 없다. 왜냐하면 이 클래스는 움직임 (move) 메쏘드를 자식 클래스에 상속해 주는 역할만 가지고 있기 때문이다. 따라서 우리는 Piece 클래스를 추상 (abstract) 클래스로 만들 수 있다. 추상 클래스 (abstract class) 는 말 그대로 ‘자식 클래스를 가질 수 있으나 실재적인 구현은 정하지 않는다.’ 라고 할 수 있겠다. 하나의 클래스가 아무런 메쏘드도 구현하지 않는 것이 가능하다. 그런 클래스는 단지 클래스가 어떤 것을 해야 하는지 알려주기만 하고 어떻게 하는 것인지는 전혀 알려 주지 않는다. 객체지향에서 사용하는 말로는, 그런 클래스들을 인터페이스 (interfaces) 라고 한다.

상속 (Inheritance) 이 추상화 (abstract) 을 제공한다.

자, 이제 또 다른 buzzword 인 Polymorphism 에 대해서 얘기할 시점이 왔다. 다형성 (Polymorphism) 은 어떤 클래스의 종속 클래스가 다른 형태로 구현될 수 있는 것을 말한다. 이미 상속에서 살펴봤듯이 체스 보드 (chess board) 는 플레이어가 내린 지시를 수행한다. 보드 (board) 자체는 체스 말 (chess piece) 클래스의 움직임 (move) 을 호출한다. 단지 움직임 (move) 메쏘드를 수행할 뿐이지 실재 어떤 말 (종속 클래스;  pawns, rooks, bishops, knights, king, and queen) 들이 움직임(move) 을 수행하는지는 모른다. 그저 체스 말 (Chess Piece) 중 하나를 움직일 (move) 뿐이다.

다형성 (Polymorphism) 은 아주 멋지다. 하지만 파이썬에서는 잘 사용되지 않는다. (다른 프로그래밍 언어인 자바나 C# 처럼 엄밀한 다형성을 포기한다는 의미로 받아들이면 되겠다.) 파이썬은 한발 더 나아가 움직임 (move) 메쏘드가 있는 어느 객체든지 움직임 (move) 메쏘드를 호출할 수 있다. (bishop 이 됐든, 자동차가 됐든 오리가 됐든지 말이다. 움직임 (move) 메쏘드가 호출되면 bishop 클래스는 대각선으로 움직일 것이고 자동차 클래스는 어딘가로 운전할 것이고, 오리는 수영하거나 날아갈 것이다. 파이썬의 이런 형태의 다형성을 덕 타이핑 (duck typing) 이라고 한다. 이 말은 “오리 같이 걷고, 오리 같이 헤엄치면 오리로 간주할 수 있다.” 라는 의미이다. 우리는 실제로 이것이 오리인지 (오리 클래스로 부터 상속 받은 종속 클래스인지) 신경쓰지 않는다. 단지 수영할 수 있고 걸을 수 있으면 된다. 아마도 거위나 백조는 우리가 생각하는 오리-같은 행동을 할 가능성이 높다. 이것은 미래 설계자들에게 새로운 형태의 조류(birds) 를 탄생시킬지도 모른다. 또 그들은 초기 설계자들이 생각했던 방향과 전혀 다른 방식으로 설계를 진행 시킬지 모르겠다. 어쩌면 미래의 설계자들은 한번도 오리를 떠올리지 않고도, 걷고 수영하는 펭귄을 만들어 낼지도 모른다.

다중 상속 (multiple inheritance)

흔히 ‘족보’ 라고 하는 가계도를 살펴보면 우리는 하나 이상의 부모로부터 태어났다. 어떤 사람이 당신의 어머니에게 ‘이 아이의 눈은 아버지를 쏙 빼 닮았군요.’ 라고 말하면 어머니는 ‘그렇지만 코는 저를 닮았답니다.’ 라고 말할지 모르겠다.

객체지향에서 어떤 클래스는 다중 상속 (multiple inheritance) 이 가능하다. 실재에서는 다중 상속은 좀 이상한 짓이다. 자바 같은 프로그래밍 언어에서는 이것을 엄격하게 금지한다. 하지만 다중상속 자체도 쓰임새가 있다. 가장 빈번하게 사용되는 것은 어떤 클래스가 두 개의 구분되는 행위를 가지고 있을 때이다. 예를들면, 어떤 객체가 스캐닝 (scanning) 과 팩스를 보내는 (sending fax) 일을 해야 한다고 하면 이 객체를 스캐너 (scanner) 객체와 팩스 (fax) 객체로부터 다중상속하여 생성할 수 있다.

두개 클래스들이 구분되는 인터페이스들을 가지고 있는한은 이 둘을 다중 상속하는 것은 해가 되진 않는다. 하지만 두 개의 클래스로부터 중복(중첩) 되는 인터페이스를 상속받는 것은 설계를 망치는 일이다. 예를 들면, 모토사이클 클래스는 움직임 (move) 메쏘드를 갖고 있고, 보트 클래스도 움직임 (move) 메쏘드를 갖고 있다고 해 보자. 만약 궁극의 수륙양용 탈것을 개발해 이 두개의 클래스를 다중 상속해 버리고 움직임 (move) 메쏘드를 호출한다면 어떻게 될까? 설계 단계에서 설명이 필요할 것이다. 구현단계에서는 프로그래밍 언어마다 각자의 방식으로 이 문제를 해결하려 할 것이다.

이 문제를 해결하는 방법 중의 하나는 이런 설계를 피하는 것이다. 당신이 설계한 어떤 방식이 이런 다중상속을 하고 있다면 아마도 잘못 설계되었을 가능성이 높다. 다시 돌아가서 시스템을 재 분석해 보고, 이런 다중상속을 합성 (composition) 이나 집합 (aggregation) 등으로 대체 할 수 없는지 확인해 봐야 한다.

상속 (inheritance) 은 행위 (behavior) 를 확장하는 강력한 방법이다. 또한 그것은 초기 객체지향 패러다임에서 가장 짜릿한 성취였다. 그러므로 상속은 객체지향 프로그래머들에게 가장 먼저 접근 할 수 있는 방법론이었다. 하지만 망치로 나사를 못으로 바꿀 수 없다는 것을 명심해야 한다. 상속은 is a 관계에서 명확한 해답이지만, 너무 남용되고 있다. 프로그래머들은 상속을 is a 관계가 없는 동떨어진 다른 두 클래스들의 코드를 공유하는데 종종 사용하고 있다. 이걸 정말로 나쁜 설계라고 말할 수는 없지만, 다른 관계나 디자인 패턴이 더 적합할 것이다.

Series Navigation<< 객체지향 프로그래밍(OOP) – 1 : Data and Behavior객체지향 프로그래밍(OOP) – 3 : Case Study “Library Catalog” >>

Leave a Comment