본문 바로가기

『객체지향 사실과 오해(3)』협력 | 역할 | 책임

by Recstasy 2019. 8. 29.

 훌륭한 객체지향 설계란, 겉모습은 아름답지만 협력자들을 무시하는 오만한 객체를 창조하는 게 아니라 조화를 이루며 적극적으로 상호작용하는 협력적인 객체를 창조하는 것이다. 객체지향 설계의 아름다움은 객체들간의 협력에 있다. 객체들이 어떤 협력에 참여하는지에 따라 객체에 필요한 행동이 결정되고, 또 필요한 행동이 객체의 상태를 결정한다. 그러므로 객체지향 설계의 원리를 꿰뚫고 있는 개발자는 개별적인 객체들의 행동이나 상태에 집중하지 않는다. 객체들간의 협력에 집중한다.

 

 

 

책임

객체지향의 '협력'관점에서 택배를 생각해보자. 택배가 집앞까지 오는 과정은 철저하게 '협력'관계로 이뤄진다. 


[[고객]] => 주문한다(요청) => [[(응답:주문)플랫폼]] => 처리하라(요청) => [[(응답:처리)판매자]] => 배송하라(요청) => [[(응답:배송)택배사]] => 준비하라(요청) => [[(응답:준비)고객]]


 쇼핑몰에서 물건을 주문하는 객체지향 설계는 '고객', '플랫폼', '판매자', '택배사'의 4가지 도메인의 협력관계에 의해 이뤄진다. 설계를 시작하는 초반에는 어떤 객체가 어떤 책임을 갖고 어떤 방식으로 서로협력해야 하는지에 대한 개요를 아는 것에서부터 출발한다. 책임과 협력의 구조가 자리를 잡기 전까지는 책임을 어떻게 구현할지는 잠시 뒤로 미뤄둔다. 가령, 고객이 어떤 카드사에 결제를 하고, 쿠폰 사용, 포인트, 등... 세부적인 역할은 책임이 결정된 이후에 작성한다. 해당 부분을 책임 단계에 넣어버리면 일반적인 설계가 아닌 특수한 상황에만 적용할 수밖에 없는 어플리케이션이 탄생하고, 확장성이 떨어진다. 따라서 객체지향 설계를 처음 시작할 때는 협력관계와 협력에 따른 책임 그리고 어떤 객체로부터 어떠한 메시지를 수신하고(요청) 받을(응답) 것인지를 명확하게 한다. 이 과정에서는 클래스를 만들거나 메서드를 생각하지 않는다. 

 

 

 

 

역할

 쇼핑몰 시스템에서 '역할'을 생각해보자. 특정 고객만 주문할 수 있는가? 판매자는 단 한명만 존재하는가? 택배사는 특정 회사와 항상 거래를 해야 할까? 또, 고객은 특정 플랫폼에서만 물건을 구입해야 할까? 이와 같은 질문의 답은 모두 '아니요'일 것이다. 판매자의 자리는 언제나 새로운 판매자가 들어올 수 있고, 고객 역시 신규로 가입 및 탈퇴할 수 있다. 즉, '책임'은 언제나 유지되어야 하지만 '역할'의 주체는 언제든지 교체할 수 있다. 이는 객체지향 설계의 강점이다. 

 

 택배사는 '배송'의 책임이 있으며, 해당 책임을 다할 수 있는 역할을 할 수 있는 객체라면 누구든지 대환영이다. 고객 역시 배송이나 쇼핑에 관련된 정보를 제공(명세서)할 수 있는 책임을 질 수 있으면 어떤 고객이든 협력에 참여할 수 있다. 역할은 협력 내에서 다른 객체로 대체할 수 있음을 나타내는 일종의 표식이다. 객체지향 설계에서 협력관계가 정해지면, '역할'은 '이 자리는 해당 역할을 수행할 수 이는 어떤 객체라도 대신할 수 있습니다'라고 말하는 것과 같다. 단, 모든 객체가 협력관계에서 역할을 수행할 수 있는 것은 아니다. 역할을 수행하려면 메시지를 동일한 방식으로 이해해야 한다. 택배사는 '배송하라'는 메시지를 이해할 수 있고, 이를 위해 택배운송 시스템과 물류배송 인적관리 그리고 장비들을 갖추고 있는 객체여야만 한다. 결국 동일한 역할을 수행할 수 있다는 것은 해당 객체들이 협력 내에서 동일한 책임의 집합을 수행할 수 있다는 것을 의미한다. 여기서 동일한 역할을 수행하는 객체들이 동일한 메시지를 수신할 수 있기 때문에 동일한 책임을 수행할 수 있다는 것은 매우 중요한 개념이다. 이 개념을 제대로 이해해야만 객체지향이 제공하는 많은 이점을 활용할 수 있다.   

 

 

 

 

 

|협력

 협력은 최종적으로 객체의 모양을 결정한다. 가령, 쇼핑몰 시스템에서 택배사는 협력에 따라 여러가지 책임과 역할을 수행한다. '배송 책임'과 '제품파손 책임'을 동시에 가질 수 있고, 책임에 따라 안전테그를 부착하거나 포장법을 새롭게하는 역할을 맡을 수 있다. 객체설계에 있어, 상태와 데이터는 객체구성에 중요한 요소이지만 데이터는 단지 객체가 행위를 수행하는 활동에 필요한 재료일 뿐이다. 객체가 존재하는 이유는 행위를 수행하며 협력에 참여하기 위해서다. 객체지향 설계에서 가장 중요한 것은 객체의 행동 그리고 책임이다. 

 

 객체지향 설계를 제대로 이해하지 못한 프로그래머나 비즈니스 결정자가 있는 회사에서는 '아이디어'를 구체화한 이후부터 즉시 '클래스'나 '데이터' 중심으로 프로그램을 설계하기 시작한다. 이는 협력이라는 문맥을 고려하지 않고 각 객체를 독립적으로 바라보기 때문에 발생하는 참사다. 가령, 쇼핑몰 시스템에서 '택배원'이라는 객체를 떠올리면 유니폼과 차량 그리고 택배차량에 가득찬 박스부터 떠올린다. 이렇게 임시방편으로 취합한 데이터를 기반으로 클래스를 만들고, 택배원 이름부터 차량 속성과 경로, 위치, 시스템을 구현하다보면, 결국 '우리는 무엇을 위해서 이러한 내용을 만들고 있는 것일까?'란 암울한 상황을 맞이하게 된다.

 

 정적모델인 클래스부터 만들어나가면, 처음에는 속도가 빠르고 뭔가 제대로 굴러가는 것처럼 보여진다. 그러나 정작 중요한 것은 택배원의 이름, 나이, 유니폼, 차량, 등...과 같은 데이터가 아니다. 쇼핑몰이란 시스템에서 택배원이란 객체가 참여하는 '역할과 책임 관계'이다. 다시 강조하지만 객체지향 설계는 항상 문맥을 고려해야하며, 각 객체를 독립적으로 이해해서는 안 된다. 독립적인 객체부터 시작하여 이들 객체를 협력관계에 밀어넣다보면, 모두 각자의 독립된 공간을 확보하게 된다. 결론적으로 이러한 시스템 설계는 협력이 물흐르듯이 이뤄지지 않는 복잡한 코드를 만들어낸다. 객체들은 항상 협력을 따라 물 흐르듯 흘러야 한다. 객체지향 설계를 올바르게 작성한 개발자들은 협력을 따라 자연스럽게 흐르는 객체의 책임에 아름다움을 느낀다. 여기서 '흐름'은 설계에 참여하는 객체들이 주고받을 요청과 응답의 흐름을 결정한다는 것을 의미한다. 또, 요청과 응답의 흐름은 객체가 협력에 참여하기 위해 수행될 책임이 된다.

 

 설계에 앞서 막막하다면, 일단 객체에게 책임을 할당하는 일부터 시작하자. 객체에게 책임을 할당하고 나면 저절로 책임완수를 위해 객체가 외부에 제공하게 될 행동 목록들이 떠오른다. 이렇게 객체가 문맥에서 수행하게 될 적절한 책임, 즉 행동을 결정한 후에 그 행동을 수행하는 필요한 데이터를 고민한다. 그리고 객체가 협력에 참여하기 위해 필요한 데이터와 행동이 어느 정도 결정됐다면, 클래스의 구현 방법을 결정한다. 클래스와 데이터는 협력과 책임의 집합이 결정된 후에야 무대 위에 등장할 수 있다.

 

결국, 객체지향 설계의 품질은 협력에 필요한 책임을 결정하고, 객체에게 책임을 할당하는 과정이 얼마나 합리적이고 적절하게 수행했는지에 달려있다. 객체지향 시스템에서 가장 중요한 것은 충분히 자율적인 동시에 충분히 협력적인 객체를 창조하는 것이다. 이 목표를 달성할 수 있는 가장 쉬운 방법은 객체를 충분히 협력적으로 만든 후에 협력이라는 문맥 안에서 객체를 충분히 자율적으로 만드는 것이다.

 

 

 

 

책임과 메시지

 객체지향 설계에서 적절한 책임이 엄청나게 중요한 사실을 알았지만 이를 어떻게 구현해야 할까? 책임의 범위와 방법을 어떻게 설정해야만 적절한 책임이 될까? 여기서 '적절하다'와 같은 말은 그 자체로 애매할 수 있다. 위의 쇼핑몰 시스템에서 판매자의 상황을 생각해보자. 쇼핑몰 플랫폼은 고객에게 '처리하라'는 요청을 받고 판매자에게 '처리해라'고 요청을 한다. 그리고 판매자는 구매자의 요청을 처리해야 할 책임을 갖고, 또 책임을 위한 여러가지 행동(메서드)을 실행한다. 

 

만일 쇼핑몰이 독재자처럼 군림하며, 'xx택배사'만을 이용해야 하고, 계좌는 'xx은행'만을 이용하며, 특정 시간대에만 접속할 수 있다는 요청을 판매자에게 강요한다면, 해당 시스템은 어떻게 될까? 아마도 판매자의 역할을 수행할 수 있는 객체는 급격하게 줄어들 것이다. 그 결과 책임을 완수할 객체가 줄어들면서, 쇼핑몰 객체 생태계는 급격하게 굳어질 수밖에 없다. 그러므로 판매자는 '처리하라'는 요청을 받은 뒤, 자율적으로 택배사를 정할 수 있으며 거래계좌도 스스로 처리할 수 있고, 제품 발송시간 역시 쇼핑몰이 정한 기한 내에서 자율적으로 진행할 수 있다. '처리하라'는 메시지에는 반드시 응답할 책임이 있지만 나머지 역할과 관련된 행동은 자율적인 것이다. 즉, '적절한 책임'은 '적절한 메시지'에서 나온다. 메시지에는 'how(어떻게)'가 자세하게 들어가면 안 된다. 메시지는 'what(무엇)'에 관련된 내용이 있어야 할뿐 '어떻게 할 것인지'는 응답하는 객체가 독립적으로 진행해야 할 사안이다.

 

 메시지는 충분히 '무엇'에 관한 범위를 설정하며 객체는 '무엇'을 '어떻게' 수행할 것인지를 갖췄다면, 이를 적절한 책임이 주어진 객체지향 설계라 할 수 있다. 그러므로 객체지향 설계에서 메시지는 아주 중요하다. 객체가 다른 객체에게 접근할 수 있는 유일한 방법은 요청을 전송하는 것뿐이며, 이 요청이 바로 메시지다. 메시지는 객체로 하여금 자신의 책임을 완수하고, 행동을 수행하게 만드는 유일한 방법이다. 

 

 

 

 

객체지향의 핵심, 메시지

 객체지향의 세계에서 객체들이 서로 협력하기 위해 사용할 수 있는 유일한 방법은 메시지를 전송하는 것이며, 메시지를 전송받은 객체는 'how(어떻게)'를 이용해서 메시지를 메서드로 구현한다. 그리고 메시지를 수신한 객체는 메서드를 실행하는 중에 다른 객체의 도움이 필요하다고 판단하고, 적합한 객체에게 메시지를 전송한다.  

 

 대부분의 객체지향 입문자들은 클래스 간의 상속 관계가 객체지향 설계를 가치 있게 만드는 핵심적인 매커니즘이라 배운다. 객체지향 설계와 관련된 많은 논의가 클래스에 어떤 책임을 할당하고 클래스 간의 의존성을 어떻게 관리할 것인가에 집중된다. 하지만 객체지향 설계의 강력함은 클래스가 아니다. 물론 클래스라는 정적 모델을 통해 시스템 전체구조를 볼 수 있다는 측면에서 클래스도 중요한 부분이다. 그러나 적어도 '설계'에 있어서 클래스 중심으로 해석하려는 시도는 '유연하지 못한' 소프트웨어를 만든다. 가령, 택배시스템을 생각해보자. 택배박스는 상황과 기타 상태에 따라 바뀔 수도 있다. 그런데 사전에 '택배박스는 이렇다 저렇다' 정해놓고, 각종 클래스를 만들어 설계를 시작해버리면 새로운 협력관계와 역할에 에러만 발생하는 최악의 시스템을 양산할 것이다. 

 

다시 강조하지만 클래스는 코드를 구현하기 위해 사용할 수 있는 중요한 추상화 도구일 뿐이다. 객체지향의 진정한 강력함은 클래스가 아닌 객체들이 주고받는 '메시지'에 있다. 객체지향 어플리케이션은 클래스를 이용해 만들어지지만 메시지를 통해 정의된다. 실제로 어플리케이션이 살아있도록 만드는 것은 정적인 클래스가 아니라 동적인 객체들간의 관계다. 클래스를 정의하는 것이 먼저가 아니라 객체들의 속성과 행위를 식별하는 것이 항상 먼저가 되어야 한다. 클래스는 객체의 속성과 행위를 담는 틀일 뿐이다. 심지어 클래스를 사용하지 않고, 객체의 속성과 행위를 표현한 것만으로도 프로그램을 구현할 수 있다. 

 

꽉막힌 코드를 구현하는 대부분의 이유는 클래스 정의에 지나치게 매몰된 나머지 협력이라는 문맥을 배제한 채, 객체 내부의 데이터 구조만을 먼저 생각하고 또, 이와 관련된(데이터 조작에 필요한) 오퍼레이션을 나중에 고려한 경우다. 객체지향 설계를 통해 코딩의 즐거움을 만끽하려면 데이터 중심으로 객체를 설계하는 방식과 습관부터 당장 버려야 한다. 이러한 방식은 객체의 내부 구조를 객체 정의의 일부로 만들기 때문에 객체의 자율성이 사라진다. 객체의 내부구조는 외부에 감춰져 있어야 하고, "협력 관계 속에서 다른 객체에게 무엇을 제공하고 다른 객체로부터 무엇을 얻어야 하는가"라는 관점이 훌륭한 책임을 수행하는 기반이 된다. 

 

 객체지향 설계의 중심에는 메시지가 위치해야만 한다. 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택해야 한다. 이를 극대화하기 위해서 '책임주도 설계'방식이 탄생했다.

(책임주도 설계에 자주 사용되는 방식은 'What/Who'사이클이다)

 

 

 

 

 

What/Who사이클

 책임-주도 설계의 핵심은 어떤 행위가 필요한지를 먼저 결정한 후에 이 행위를 수행할 객체를 결정하는 것이다. 이 과정을 'What/Who'사이클[Budd 2001]이라고 한다. What/Who사이클이라는 용어가 의미하는 거은 객체 사이의 협력 관계를 설계하기 위해서는 먼저 '어떤 행위(What)'를 수행할 것인지를 결정한 후에 '누가(Who)' 그 행위를 수행할 것인지를 결정해야 한다는 것이다. 여기서 '어떤 행위'가 바로 메시지다. 즉, 협력이라는 문맥 안에서 필요한 메시지를 먼저 결정한 후에 메시지를 수신하기에 적합한 객체를 선택한다는 의미다. 여기서 수신된 메시지가 객체의 책임을 결정한다. 이것은 객체를 고립된 상태로 놓고 어떤 책임이 적절한지를 결정하는 것과는 근본적으로 다른 접근 방법이다. 어떤 객체도 섬이 아니라는 말은 협력이라는 문맥 안에서 객체의 책임과 역할을 결정하라는 의미를 내포하고 있다.

 

그리고 수신 가능한 메시지가 모여 객체의 인터페이스를 구성한다. 메시지를 먼저 결정하고 메시지를 수신할 객체를 선택하는 과정은 객체의 인터페이스가 어떤 방식으로 결정되는지를 명확하게 보여준다. 

 

 '책임-주도 설계'는 객체가 아니라 객체들이 주고받는 메시지에 초점을 맞추게 함으로써 객체지향의 장점을 극대화한다. What/Who 사이클은 어떤 객체가 필요한지를 생각하지 말고 어떤 메시지가 필요한지를 먼저 고민하라고 조언한다. 즉, 메시지를 결정하기 전까지는 객체에 관해 고민하지 않는다. 일단 메시지가 결정된 후에야 이 메시지를 처리할 객체를 선택한다. 즉, '책임-주도 설계'는 메시지를 먼저 처리함으로써 '캡슐화'라는 또다른 장점을 갖는 셈이다. 객체부터 설계하지 않고, 메시지를 정한 뒤에 객체를 선정하는 과정에서 메시지 송신자는 메시지를 수신할 객체의 내부 상태를 볼 수 없기 때문이다. 따라서 메시지 중심의 설계는 메시지 수신자의 캡슐화를 증진시킨다. 또한 송신자와 수신자의 내부 상태를 미리 알 수 없기 때문에 송신자와 수신자가 느슨하게 결합된다.

 

'묻지 말고 시켜라'

 

 객체는 다른 객체의 상태를 묻지 말아야 한다. 그래야만 자율성이 확보된다. 객체가 다른 객체의 상태를 묻는다는 것은 메시지를 전송하기 전에 객체가 가져야 하는 상태에 관해 너무 많이 고민하고 있었다는 증거다. 고민이 많으면 시스템의 자율성이 떨어진다. 그냥 메시지를 믿고 전송하자. 단지 필요한 메시지를 전송하기만 하고 메시지를 수신하는 객체가 스스로 메시지의 처리 방법을 결정하게 할수록 시스템의 유연성과 자율성이 높아진다. 메시지를 중심으로 설계된 구조는 유연하고 확장 가능하며 재사용 가능하다. 메시지를 믿으면 자율적인 책임은 저절로 따라온다.

 

 

 

 

 

인터페이스 구현 & 분리

 객체지향 설계에서 메시지는 두 가지로 나뉜다. 메시지가 객체에 전달되면 외부적으로 공개되는 부분과 내부적으로 구현되는 부분이 발생하는데, 여기서 외부적으로 공개되는 메시지는 '인터페이스'라 할 수 있다. 인터페이스란 용어는 일반인도 별로 생소하지 않은 단어다. 사용자가 이용하는 기계는 대부분 인터페이스를 갖고 있으며, 사용자는 인터페이스를 통해 원하는 목적을 달성한다. 사용자는 인터페이스가 있기 때문에 메시지를 전달할 수 있다.

 

 자동차를 생각해보자. 자동차를 운전할 수 있는 사람은 차량의 종류에 상관없이 운전이 가능하다. 경차건 중형차건 자동차는 공통적으로 '핸들','브레이크', '액셀레이터' 등...의 요소를 갖추고 있기 때문이다. 사용자는 핸들을 좌우로 돌리면 차가 움직인다는 사실을 알고 있으며, 핸들이 돌아가면 차가 해당 방향으로 움직이는 내부구조에 관해서는 알 필요가 없다. 여기서 인터페이스의 특징이 등장한다. 인터페이스는 외부로 노출된 객체이며, 반드시 특정 행위를 요청한다. 설사 내부의 행위나 상태가 변하더라도 인터페이스는 영향을 받지 않는다. 자동차의 엔진구조가 변하고 제동장치가 변경되더라도 브레이크와 액셀레이터는 똑같다. 만일 내부구조가 변하면서 인터페이스까지 변한다면 이는 유지보수에 심각한 에러를 초래한다. 반대로 인터페이스를 조작했는데 엔진구조가 변한다면 큰 사고로 이어질 수도 있다. 따라서 인터페이스는 반드시 구현과 분리해야 한다.

 

 구현이란, 객체가 스스로 내부에 전달하는 메시지다. 쉽게 생각해서 메서드가 구현이라고 이해하면 된다. 객체는 외부 메시지(인터페이스)를 받고 내부 동작을 수행하는데, 이를 구현이라고 한다. 내부 메시지(구현)는 오픈된 외부 메시지, 인터페이스와 철저하게 분리함으로써 캡슐화가 진행된다. 외부 메시지가 내부를 오염시키지 않을수록(접근 불가) 객체의 자율성이 높아지고, 확장성 역시 증가한다. 

 

 객체들은 인터페이스를 통해 전달받은 메시지(목적)를 달성하기 위해 각자의 방법대로 협력하고 구현하면 된다. 객체의 목표는 오직 책임을 완수하는 것이며, 이 과정에서 협력이 적절하게 이뤄질수록 전체 설계가 유연해진다.

 

댓글

최신글 전체

이미지
제목
글쓴이
등록일