본문 바로가기

카테고리 없음

[Python 33] 객체들 간의 관계-1 : 컴포지션(composition)

 

 

 

모든 프로그램은 "객체들"과 "객체들 간의 관계"로 만들어진다. 사실 모든 소프트웨어란 클래스를 정의하여 객체를 생성하고, 생성된 객체들 간에 관계(연결)을 만들어 주게 되는데, 그 관계를 통해 객체들이 서로 연관되어지고 서로 커뮤니케이션하고 상호작용하게 된다. 사람들이 서로 상호작용하면서 세상이 움직여 가듯, 객체들이 서로 상호작용하면서 소프트웨어가 움직이는(구동되는) 것이다.

 

우리는 여태껏 하나의 클래스 만을 사용해 왔다. 객체도 한두개 만들어 본 것이 전부이다. 그런데, 세상은 그렇게 단순하지가 않다. 세상에는 수없이 다양한 클래스들과 객체들이 존재한다. 단순히 존재하는 것에 그치는 것이 아니라, 객체들은 다른 객체들과 어떤 특별한 관계 (relationship)를 가지게 된다. 어느 세상의 한토막 스토리를 보자면 "홍길동이라는 이름의 직원이 오늘, 자동차(car) 생산라인에서, 자동차 프레임 위에다가 엔진(Engine) 을 조립하는 작업을 한다"는데, 이 스토리에는 홍길동 이라는 객체, 자동차라는 객체, 엔진이라는 객체가 등장하는데 서로 어떤 연관이 생기게 된다. 

 

이제 부터 객체들을 서로 연관짓는 관계(relationship)라는 키워드에 대해 살펴보도록 하자. 객체들 간의 관계는 다음과 같이 크게 3가지 타입으로 구분할 수 있다.

 

첫째는, 상속(Inheritance) 관계이다. 보통 IS-A relationship 이라고 부른다. 

둘째는, Composition 또는 Aggregation 관계이다. 구성, 포함 정도로 해석할 수 있을텐데, 보통 HAS-A relationship 이라고 부른다.

셋째는, Association 관계이다. 관계는 일종의 결합이어서 어떤 결합은 강하고(strong), 어떤 결합은 상대적으로 약하다(weak). 보통, 한번 관계가 만들어지면 오래 지속되는 (또는 영구히 존속하는) 관계는 강한 관계인데 비해, 관계가 만들어진 후에 쉽게 관계가 깨어지기도 하는 (또는 바뀌기도 하는) 약한 관계도 존재한다. Association은 다른 2개 관계 (Inheritance, Composition) 에 비해서 약한 관계이다. 구현되는 방식은 Composition 과 동일하다. 구별에 실익은 없을 수 있다. 따로 설명할 일은 없을 것 같고, Composition을 공부한 후에 그 차이를 살짝 알아보는 것으로 하자.

 

위의 3가지 관계 중, 이번 글에서는 먼저 Composition 관계에 대해서 알아보자.

 

○ Composition, Aggregation은 Whole-Part 관계를 나타냅니다

 

세상에는 자동차란 것들도 있고, 엔진이라는 것들도 존재한다. 그런데, 하나의 자동차는 그 내부에 하나의 엔진을 포함하게 됨으로써 두개의 객체 간에는 어떤 특별한 관계가 존재하게 된다. 자동차에 엔진이 빠지면 움직이지 못하니 자동차라고 부를 수가 없을테고, 엔진이 자동차에 조립되지 않는다면 그 쓰임새가 너무 제한적일 것이다.

 

물론 자동차에 엔진만 있는 것은 아니다. 엔진은 하나의 자동차를 만드는데 필요한 여러가지 부품(part, 엔진을 부품이라고 부를려니 조금 이상하긴 하지만...) 중의 하나이다. 자동차(Car)는 전체 (whole)에 해당이 되고, 엔진(Engine)은 그 전체를 구성하는 부품(part)에 해당하게 된다. 영어로 한번 표현해 보면 "A Car is composed of  an Engine, four Wheels, a Streering wheel, and so on." 이라고 표현할 수 있겠다. 조금 다르게 "An Engine, four Wheels, and a Streering wheel are aggregated into a Car." 라고 쓸 수도 있겠다. 그래서, 이러한 관계를 composition 또는 aggregation 이라고 부른다. 

 

이와 비슷한 사례는 수도 없이 많다. 대학교(university) 안에는 여러개의 학과(department) 가 포함된다. 사람이 거주하는 공간에는 여러 종류의 가구가 배치된다. 하나의 부서에는 (역할이 다른) 여러 직원으로 구성된다. 우리나라에는 여러개의 지방자치단체가 있다. 3개 이상의 도로가 모이면 교차로가 된다. 시냇물이 모여서 큰 강물을 이룬다 (어디선가 들어본 듯한 ... ) 사람의 몸에는 (피가 도는) 순환계통과 (음식이 영양소로 바뀌는) 소화계통이 있다. 우리 아파트는 4개 라인으로 구성되어 있다. 우리 아파트는 총 14개 층으로 만들어져 있다. (뭐, 너무 뻔한 얘기라서 더 이상의 예제는 의미가 없어 보인다) *객체지향은 이처럼 뻔~한 개념이다. 어렵게 생각하지 말기 *

 

첫번째 예를 다시 한번 살펴보자. "대학교(university) 에는 여러개의 학과(department) 가 포함된다" 고 했는데, 여기서 대학교는 전체(whole)에 해당하고 학과는 부분(part)에 해당한다. 부언하자면, 어떤 2개의 객체 간에 composition 관계인지 아닌지 검증이 필요한 경우에는 한쪽이 전체에 해당하는지 그리고 나머지 한쪽은 부분에 해당하는지를 살펴보는 것이 쉽다. 어쨌거나, 이를 (말이나 글이 아닌) 그림(diagram)으로 표현하면 다음과 같이 나타낸다. 아래의 그림은 그냥 "내 마음대로" 그린 것이 아니고, 준 표준(de facto standard) 또는 관례(convention)적으로 사용되는 UML (Unified Modeling Language)의 표기법(notation)이다.

 

 

박스는 클래스를 표현한다. 2개 클래스 간에 일정한 관계가 존재하면 선으로 연결한다. 위에서 보았듯이, 클래스 간에 존재하는 관계가 3가지 종류이다 보니 모양이 다르게 표기한다. Composition 관계의 경우 전체(whole) 쪽에 다이어몬드 심볼을 조그맣게 붙인다. Composition과 aggregation을 세분해서 다루는 경우에는 속이 채원진 다이어몬드와 속이 비워진 다이어몬드로 나누기도 하는데 별 실익이 없다. (그런 것이 있구나 하고 넘어가면 되겠습니다)

사람들은 정보를 가지고 있다. 어디에? 본인의 머리속에. 그래서 밖에서는 그 정보를 알 수가 없다. 본인이 그 정보를 표현하기 전 까지는 절대로 밖에서 알 수가 없다. 정보의 표현에 사용되는 수단은 크게 말, 글, 그림이 있다. 여기서, 정보를 표현하는 그림을 보통 다이어그램이라고 부르는데, 사전에 정해진 일정한 규칙에 따라 그려지는 그림을 말한다.  일반적으로 여러개의 심볼(symbol)과 선(line)으로 표현된다. 그림은 정보를 표현하는 정말 좋은 수단이다. 다이어그램에 대해서는 다음에 한번 더 자세하게 살펴보도록 하겠다.

 

 

  객체의 참조자를 알아야 메쏘드를 호출할 수 있습니다

 

어떤 객체 A가 다른 객체 B의 메쏘드를 호출하기 위해서는, 객체 A가 객체 B의 참조자를 가지고 (또는, 알고) 있어야 한다. 예를 들어, 병원진료를 가려는 사람에게 “이 의사 선생님에게 물어봐”라고 한다거나, 도서관에 가서 책을 찾으려고 하는 친구에게 “이 사서 선생님에게 문의해 봐”라고 하는 것이 좋은 예가 되겠다.

 

코드의 예를 보자. 아래의 Point는 2차원 좌표를 나타내는 클래스이다. 좌표를 나타내는 2개의 인스턴스 변수 x, y를 속성으로 가지고 있고, 게터 메쏘드로 get_x( )와 get_y( ), 좌표 정보를 출력하는 show( ) 메쏘드를 가지고 있는 클래스이다. 

 

 

 

아래의 Geometry 클래스는 여러 기하학적 연산을 수행하는 클래스이다. 현재는 2개 좌표 간의 직선 최단거리를 계산하는 distance( ) 메쏘드만 가지고 있다 (아래의 코드를 참고하기 바랍니다).

 

 

 

Geometry 클래스의 distance( ) 메쏘드는 매개변수 p1과 p2를 통해, 거리값을 계산하고자 하는 2개의 Point 객체의 참조자를 받고 있다. 예를 들어, Geometry 객체 g에게 g.distance(a, b)를 호출한다면, 이는 “여기 a와 b, 2개의 점(좌표)이 있는데, 이 두 점 사이의 거리를 좀 구해달라”는 요청이 되는 셈이다. 실제로 이렇게 전달된 p1과 p2 참조자는 distance 메쏘드 내에서 거리값을 구하는데 사용되고 있다.

 

실제 실행 예를 보이면 아래와 같다.

 

 

 

우리가 지금 얘기하고 있는 문제는, 어떤 객체가 다른 객체의 메쏘드를 호출하기 위해서는 그 다른 객체의 참조자를 알고 있어야 하는데, 이를 어떻게 가능하게 만들것인가 하는 문제이다. 하나의 방법으로, 함수의 호출에서 인자로 전달하는 방법을 살펴보았다.

 

두 번째로 생각할 수 있는 방법은, A 객체가 자신과 연관되어있는 B 객체의 참조자를 인스턴스 변수에 저장하여 계속 유지할 수 있도록 하는 것이다 (이것이 바로 컴포지션입니다)

 

예를 들어, 2개의 Point 좌표로 만들어지는 Line 객체를 생각해 볼 수 있겠다. 아래는 2개의 Point 객체를 각각 self.st와 self.end로 참조하는 Line 클래스의 정의를 보이고 있다. 실제로 2개의 Point 객체를 이용해서 Line 객체를 생성하는 예는 아래의 In[36] 코드에서 볼 수 있다.

 

 

 

In[36]에서 생성된 Line 객체에게 show() 메쏘드를 호출하면 아래와 같은 결과를 보게 되는데, 이는 인스턴스 변수 self.st와 self.end에 저장되어 있는 2개 Point 객체의 참조자를 통해 Point 클래스의 show() 메쏘드를 호출함으로써 이루어지고 있다. 

 

 

 

정리하자면, 하나의 Line 객체는 2개의 Point 객체를 가지고 있다 (실제로, 하나의 선은 2개의 점으로 정의됩니다). 즉, Line객체가 whole(전체)이 되고, 2개의 Point 객체는 "그" whole을 구성하는 부분(part)이 된다. 이 관계가 컴포지션 관계이며, "참조자"에 의해서 만들어진다는 것이 핵심이다. 

 

Line 객체의 show() 메쏘드를 살펴보면, Point 클래스의 show() 메쏘드 호출이 이루어지고 있는데, 이는 결국 Line 객체가 어떤 기능을 수행하기 위해서, 컴포지션하고 있는 객체(들)의 기능을 이용하는 것이 된다. 마치 Line 객체에게 "화면에다 너의 좌표 좀 찍어봐"했더니, 자기(Line객체)가 데리고 있는(즉, 가지고 있는) 애들(Point 객체들)에게 "야, 니들이 찍어"라고 위임(delegation)하는 셈이 된다. 세상의 일을 모두 내가 할 수는 없다. 어떤 일들이야 내가 "직접" 하겠지만, 보통의 일들은 다른 객체들의 도움을 받는다. 이를 위임이라고 한다. 보통 "시킨다"라고 해석되는 위임은 시키는 자와 시킴을 받는 자 간의 관계로 부터 만들어진다. 아무한테나 "시킬" 수는 없는 노릇이다. 결국, 위임의 바탕이 되는 관계가 컴포지션이 되는 셈이다. 

 

  컴포지션 : 어떤 객체가 다른 객체의 부분이 됩니다

 

위에서 잠시 소개했지만, 컴포지션은 다음 그림과 같이 나타낼 수 있다. 객체지향적 설계에 필요한 여러가지 개념을 그림으로 묘사하는데에 UML (Unified Modeling Language) 이라는 도구가 사실상의 표준으로 사용되고 있다. UML에서 컴포지션은 아래와 같이 다이어몬드 심볼로 표현된다.

 

 

 

(객체지향을 공부하다 보면 많이 보게 되는 그림이어서 조금 부연해서 설명하도록 하겠습니다) UML에서 클래스는 박스(사각형)로 표현된다. 클래스 간에 관계가 있으면, 이를 선으로 연결하는데 컴포지션 관계는 한쪽에 다이어몬드 심볼이 있는 실선으로 표현된다. 전체(whole)에 해당하는 클래스 쪽에 다이어몬드 심볼이 위치하도록 그리면 되겠다. 하나의 차(Car) 객체에 4개의 바퀴(Wheel) 객체가 컴포지션되어 있는 것처럼, 하나의 whole에 여러 개의 부분(part)이 연관되는 경우, 그 관계에 참여하는 part의 개수 (보통, 카디널리티(cardinality)라고 부릅니다)를 선위에 표시하기도 하는데, 예를 들어 “여러”의 의미로 * 가 사용되고, 특정한 개수가 있는 경우 (예를 들어, 1개의 엔진 등)에는 그 수를 나타내 주기도 한다.

 

컴포지션은 전체(whole)에 해당하는 객체가 부분(part)에 해당하는 객체의 참조자를 인스턴스 변수로 저장하는 형태로 구현되며, 일반적으로 전체에 해당하는 객체의 생성시에 관계가 이루어지도록 만든다 (물론, 한번 만들어졌다고 해서 그 관계가 "영원히" 계속되어야 하는 것은 아닙니다. 당연히 필요할 때 마다 바꿀 수 있습니다).

 

부분에 해당하는 객체를 전체에 해당하는 객체의 내부에서 자체적으로 만드는지 외부에서 만들어 제공되는지에 따라 조금 의미가 다르게 쓰일 수 있다.  먼저, 부분에 해당하는 객체를 외부에서 만들어 전체에 해당하는 객체에게 전달하는 경우이다. 아래의 In[43] 코드를 보면, Engine 객체 e를 만든 후에 그 객체의 참조자를 Car 객체를 생성하는데 인자로 전달하고 있다.

 

 

 

두 번째의 예는 아래의 In[44] 코드에서 볼 수 있는데, [라인번호 6]에서 Car 객체가 자신이 가지고 있어야 하는 Engine 객체를 스스로 만들어 참조자를 인스턴스 변수에 저장해 두고 있다.

 

 

 

시작은 다를 수 있지만, 결국은 Engine 객체의 참조자가 Car 객체 내에 저장되게 되고, 이후에 Car 객체가 Engine 객체와 커뮤니케이션하기 위한 용도로 사용되게 된다. 이를 개념적으로 나타내 보면 아래와 같다. Car 객체 내에 Engine 객체가 통째로 들어와 있는 이미지가 아니라는 것은 꼭 이해하길 바란다.

 

 

 

컴포지션의 의의는 위임(delegation)에 있습니다

 

보통 위임이라고 하면, 어떤 행위와 책임을 다른 사람에게 넘겨주는 것을 말한다. 실제로 객체 간에도 컴포지션을 통해 위임관계가 만들어지게 된다. 실제로, 우리가 Car 객체에게 움직이라는 메시지 (move 메쏘드의 호출)를 보내게 되면, 그 요청을 받은 Car 객체는 자신이 컴포지션하고 있는 엔진 객체를 구동하게 된다. 아래의 코드를 보자.

 

 

 

In[60]에서 Car 객체 c에게 move( ) 하라는 요청이 이루어지고 있다. 실제 Car 클래스의 move( ) 메쏘드를 보면 Engine 객체의 run( ) 메쏘드의 호출을 포함하고 있는 것을 확인할 수 있다. 이것은 결국 Car 객체가 수행해야 할 일의 일부를 Engine 객체에게 위임한 형태가 되며, 이것이 가능한 것은 결국 Car 객체가 Engine 객체를 컴포지션하고 있기 때문이다. 실제 메시지의 전달을 그림으로 나타내 보면 아래와 같다.

 

 

 

어떤 클래스가 제공해야 하는 기능이 많아질수록 그 클래스를 정의하는 코드는 관리하기 어려울 정도로 복잡하게 된다. 복잡한 것은 항상 나쁜 것이다. 이를 단순하게 만드는 방법은 기능의 분할이다 (단순한 것이 나쁜 경우는 절대 없습니다) 가장 좋은 해결방법은 해당 클래스가 제공하고 있는 기능의 일부를 떼어내어 그 기능을 전담하는 클래스를 새로 하나 정의하는 것이다. 자연스럽게 컴포지션 관계가 성립하고 이를 기반으로 위임이 이루어지게 된다. 문제해결 기법으로 자주 소개되는 Divide and Conquer가 떠오른다 (복잡한 문제는 나누어서(divide) 푼다(conquer)). 컴포지션도 같은 맥락이다.