Search

객체지향의 사실과 오해 - 4, 5장

“객체지향의 사실과 오해” 책을 바탕으로 정리를 진행했습니다. 제가 이해한 바를 정리하고 싶어서 적은 포스팅이라 책에 있는 내용과는 맞지 않거나 오류가 있을 수 있습니다. 더 많은 내용을 보고 싶으신 분들은 책을 직접 읽으시는걸 추천드립니다.
객체지향의 사실과 오해 - 1, 2, 3장 에서 이전 내용을 보실 수 있습니다.

 역할, 책임, 협력

인간은 본질적으로 어떤 특성을 지니고 있느냐가 아니라 어떤 상황에 처해 있느냐가 행동을 결정하게 됩니다. 각 개인이 처해 있는 정황, 문맥이 행동방식을 결정한다는 겁니다.
객체 세계에서는 협력이라는 문맥이 객체의 행동방식을 결정합니다. 객체 세계에서 중요한 건 개별 객체가 아닌 객체들 사이에 이뤄지는 협력입니다. 협력이 자리를 잡으면 저절로 객체의 행동이 드러나게 됩니다.
훌륭한 객체지향 설계는 조화를 이루며 적극적으로 상호작용하는 협력적인 객체를 창조하는 겁니다. 객체의 모양을 빚는 것은 결국 객체가 참여하는 협력입니다. 협력이 객체에 필요한 행동을 결정하고, 이는 객체의 상태를 결정하게 됩니다.
협력은 한 사람이 다른 사람에게 도움을 요청할 때 시작합니다. 요청을 받은 사람은 요청을 한 사람에게 필요한 지식이나 서비스를 제공함으로써 요청에 응답합니다. 즉, 협력은 다수의 연쇄적인 요청과 응답의 흐름으로 구성됩니다.
요청을 받은 객체(사람)가 특정 요청을 받아들일 수 있는 이유는 그 요청에 대해 적절한 방식으로 응답하는데 필요한 지식과 행동 방식을 가지고 있기 때문입니다. 어떤 요청에 대한 응답은 협력에 참여하는 객체가 수행할 책임을 정의합니다. 어떤 객체가 어떤 요청에 대해 대답해줄 수 있거나, 적절한 행동할 의무가 있는 경우 해당 객체가 요청에 대한 응답을 할 책임을 가졌다고 보는겁니다.
즉, 어떤 대상에 대한 요청은 그 대상이 요청을 처리할 책임이 있음을 암시합니다.
객체지향에서 가장 중요한 능력은 책임을 능숙하게 소프트웨어 객체에게 할당하는 것입니다. 제대로 할당하지 않고 성급하게 구현에 뛰어드는 것은 변경에 취약하고, 다양한 협력이 어려운 비자율적인 객체를 만들어 냅니다.
책임이 정확히 뭔가요?
책임은 객체가 알아야 하는 정보, 객체가 수행할 수 있는 행위에 대해 개략적으로 서술한 문장입니다. 즉, 객체가 무엇을 알고 있고(knowing), 무엇을 할 수 있는지(doing)에 대한 겁니다.
책임은 객체지향 세계에서 객체의 외부에 제공해 줄 수 있는 정보(knowing), 외부에 제공해줄 수 있는 서비스(doing)의 목록입니다.
즉, 책임을 통해서 객체의 공용 인터페이스를 구성할 수 있게 됩니다.
하지만, 책임은 스스로 다할 수 없습니다. 즉, 스스로에 의해서 책임을 수행할 수 없습니다. 책임을 다하게 만들 누군가의 요청이 필요합니다. 요청은 메세지 전송이라는 방식으로 진행됩니다. 객체가 다른 객체로 접근할 수 있는 유일한 방법이기 때문이죠.
다른 객체가 이해할 수 있는 메시지를 전송하면 메시지를 받은 객체는 적절한 책임을 수행하게 됩니다.
메세지를 기반으로 상호 협력이 이뤄지는겁니다.
객체지향은 협력에 참여하기 위해 어떤 객체가 어떤 책임을 수행하고 어떤 객체로부터 메세지를 수신받을 것인지 결정하는 것으로부터 시작합니다. 어떤 클래스, 어떤 메서드를 포함할 지 결정하는 것은 책임과 메세지를 정한 후에 결정해도 괜찮습니다.
책임은 객체지향에서 중요한 의미를 가집니다. 그리고 어떤 객체가 수행하는 책임의 집합은 곧, 객체가 협력 안에서 수행하는 역할을 암시하게 됩니다. 역할은 재사용 가능하고 유연한 객체지향 설계를 낳는 매우 중요한 구성요소 입니다.
역할이 있기에 비슷한 협력을 모두 포괄할 수 있는 하나의 협력으로 추상화할 수 있습니다. 역할은 협력 내에서 다른 객체로 대체할 수 있기 때문이죠. 역할은 “이 자리는 해당 역할을 수행할 수 있는 어떤 객체라도 대신할 수 있습니다.” 라는 뜻을 내포합니다.
역할이 대체 가능한 이유는 동일한 메세지를 이해할 수 있기 때문입니다. 동일한 역할을 수행하는 객체들은 동일한 메세지를 수신할 수 있고 이는 동일한 책임을 수행할 수 있다는 뜻이 됩니다.
역할을 사용하면 유사한 협력을 추상화하여 인지 과부하를 줄일 수 있습니다. 또한, 다양한 객체들이 협력에 참여할 수 있어서 협력이 유연해집니다. 동일한 협력에 객체들이 참여할 수 있기 때문에 재사용성이 높아집니다.
이는, 역할이 대체 가능하기 때문입니다.
객체지향 설계에서 중요한 건 결국, 협력입니다. 먼저 견고하고 깔끔한 협력을 설계해야지만 객체들이 주고 받을 요청과 응답의 흐름이 결정되면서 객체의 책임을 결정할 수 있습니다. 객체가 외부에 제공하게 될 행동을 결정한 후에 그 행동에 필요한 데이터를 고민하게 됩니다. 그러고서, 클래스 구현 방법을 결정하게 되겠지요.
협력이라는 실행 문맥 안에서 책임을 분배해야 합니다. 중요한 건 충분히 자율적인 동시에 충분히 협력적인 객체를 창조해야 한다는 겁니다. 그러기 위해선, 객체를 충분히 협력적으로 만든 후에 협력이라는 문맥 안에서 객체를 충분히 자율적으로 만들면 됩니다.

 책임과 메세지

객체지향 공동체를 구성하는 기본 단위는 바로, 자율적인 객체입니다.
자율적인 객체는 스스로 정한 원칙에 따라 판단하고 스스로의 의지를 기반으로 행동하는 객체입니다.
객체의 행동은 다른 객체로부터 요청을 받았기 때문에 수행됩니다. 즉, 다른 객체로부터 온 메세지에 대한 책임을 다하기 위함이죠. 적절한 책임이 자율적인 객체를 낳고, 자율적인 객체들이 모여 유연하고 단순한 협력을 낳게 됩니다. 자율적인 객체가 얼마나 자율적인지가 전체 애플리케이션 품질을 결정하게 됩니다.
객체가 책임을 자율적으로 수행하기 위해서는 객체에게 할당되는 책임도 자율적이어야 합니다.
예를 들어서, 화가는 그림을 그릴 책임이 있지만 그림을 그리기 위한 구체적인 방법이나 절차에 대해서는 최대한 자유를 누립니다. 화가에게 그림을 요청하고 그림을 받는 구매자에게는 화가가 어떤 방법으로 그림을 그리는지가 중요하지 않습니다.
오히려, 구매자의 요청이 구체적일 경우에 화가의 자유를 훼손할 수 있습니다.
구매자가 만약 화가에게 “아크릴 물감을 사용해서 그려라”, “빨간색을 많이 사용해서 그려라” 라고 한다면 그림을 그리기 위한 자유의 범위를 지나치게 제한해버리게 됩니다. 화가의 행동이 구매자의 요청에 의존할 수 밖에 없어집니다.
따라서, 화가가 자율적이기 위해서는 화가에게 할당되는 책임의 수준도 자율적이어야 합니다.
자율적인 책임은 어떻게(how) 해야 하는가가 아니라 무엇을(what) 해야 하는가를 설명합니다. 어떻게 할 지는 책임을 할당 받은 객체가 결정할 일입니다. 어떻게 할 지를 메시지를 받은 객체가 선택하지 못한다면 자율적이지 못한 객체가 됩니다.
하지만, 협력의 의도를 명확하게 표현하지 못할 정도로 추상적인 것 역시 문제가 됩니다. 협력에 참여하는 의도를 명확하게 설명할 수 있는 수준에서 추상적이어야 합니다.
화가에게 구매자가 “만들어라”라고 한다면, 화가는 그 책임이 너무 추상적이라서 어떻게 해야할 지 모를겁니다. 물론, 판단하는 기준은 문맥에 따라 다르기 때문에, 어떠한 문맥에서는 “만들어라”라는 책임이 적절할 지도 모릅니다. 어떤 책임이 적절한지는 설계 중인 협력이 무엇이었는지에 따라 달라집니다.
객체는 메세지로 다른 객체에게 접근할 수 있습니다. 메세지가 다른 객체로 접근할 수 있는 유일한 방법이기도 합니다. 송신자가 수신자에게 메세지를 보낼 때, 수신자.메시지이름(인자) 의 형식으로 보내게 됩니다. 구매자는 화가에게 화가.그려라() 의 형식으로 요청을 보내는거죠.
메시지를 수신받은 객체는 자신이 해당 메시지를 처리 가능한지 확인합니다. 수신받은 객체가 해당 메시지를 처리 가능하다면 메시지에 해당하는 행동을 수행할 책임이 있음을 의미합니다. 따라서, 수신받은 객체는 요청에 대한 응답을 하게 됩니다.
메시지는 다른 객체로부터 전달되기 때문에 외부의 다른 객체가 볼 수 있는 공개된 영역에 속해야 합니다. 하지만, 책임을 수행하는 방법은 외부 객체가 볼 수 없는 사적인 영역에 속합니다. 사적인 영역에 속해야 다른 객체의 간섭없이 메시지를 처리하는 방법을 자율적으로 선택할 수 있기 때문이죠.
이렇게, 사적인 영역에서 실행되는 것을 “메서드”라고 합니다. 어떤 객체에 메세지를 전달하면 특정 메서드가 실행됩니다.
무엇(what)이 실행되길 바라는지 명시한 메세지를 받아서 메서드를 실행할 때에 어떤 메서드를 선택할 지는 수신자의 결정에 좌우됩니다. 메서드는 요청된 메시지에 대한 응답을 만족시키기 위해서 실행될 겁니다.
이전에 서로 다른 유형의 객체가 동일한 메시지를 받을 수 있다고 했습니다. 즉, 동일한 메시지에 대해서 서로 다르게 반응을 할 수 있습니다. 서로 다르게 반응을 하는 방식이 바로, “메서드”인거죠.
송신자 관점에서는 다형적인 수신자들을 구별할 필요가 없습니다. 자신의 요청을 수행할 책임을 지닌다는 점에서 모두 동일한 겁니다. 그렇기 때문에, 동일한 역할을 수행하는 다양한 타입의 객체와 협력할 수 있는겁니다.
이는 곧, 대체 가능성을 의미합니다. 송신자의 요청에 대해서 책임을 다할 수 있다면 누구든 괜찮다는겁니다.
구매자는 화가가 아니더라도 “그려라”를 수행해줄 수 있는 다른 사람이 있다면 그 사람으로 화가의 역할을 대체할 겁니다. 물론, 그 사람은 화가와 다르게 “그려라”를 수행할 수 있습니다. 화가가 물감으로 그림을 그렸다면, 그 사람은 아이패드로 그림을 그릴 수도 있는거죠. 아무튼, “그려라”만 수행할 수 있으면 됩니다.
구매자는 사실 자신의 그림을 그려줄 사람이 누군지 모르더라도 메시지를 전송할 수 있습니다. “그려라”를 수행할 수만 있다면 인스타그램 너머의 얼굴모를 누군가라도 괜찮다는 겁니다.
이는 송신자(구매자)에게 영향을 주지 않고도 메세지를 수신할 객체 타입을 자유롭게 추가 가능하다는걸 뜻합니다. “그려라”만 할 수 있다면 누구든 그림을 그려줄 수신자로 추가할 수 있으니깐요.
구매자와 그림을 그려줄 누군가는 메시지라는 아주 얇은 연결고리로 묶여 있습니다. 결합도가 낮다는 겁니다. 그렇기 때문에, 협력을 유연하게 만들 수 있습니다. 협력이 유연해지면 송신자에 대한 파급 효과없이 유연하게 협력을 변경할 수 있습니다.
또한, 협력이 수행되는 방식을 확장할 수 있습니다. 메시지를 기반으로 한 느슨한 관계만 존재하기 때문에 협력의 세부적인 수행 방식을 쉽게 수정할 수 있습니다. 협력에 영향을 미치지 않고 다양한 객체들이 수신자 자리를 대체할 수 있기 때문에 협력이 수행되는 방식도 재사용할 수 있게 됩니다.
객체가 어떻게 할 지보다는 무엇을 할 것인지에 초점을 맞추기 때문에 시스템의 행위를 변경하기 쉽습니다.
이는 다형성을 지탱하는 메시지가 존재하기 때문에 가능합니다. 메시지가 송신자, 수신자 사이의 결합도를 낮춰주니깐요.
메시지가 있기에 송신자는 오직 메시지만 바라볼 수 있습니다. 수신자는 메시지 처리를 위한 메서드를 자율적으로 선택할 수 있구요. 수신자가 자율적으로 선택한 메서드는 송신자에게 노출되지 않습니다.
메시지 기반의 낮은 결합도가 설계를 유연하고 확장 가능하며 재사용 가능하게 만들어 준겁니다. 즉, 우리는 훌륭한 메시지를 선택해야 합니다. 진정한 객체지향 패러다임으로의 도약은 개별적인 객체가 아니라 메시지를 주고 받는 객체들 사이의 커뮤니케이션에 초점을 맞출 때 일어납니다.
객체의 행위를 고려하기 위해서는 협력이라는 문맥에서 객체를 바라보아야 합니다. 결국, 객체를 이용하는 중요한 이유는 객체가 다른 객체가 필요로 하는 행위를 제공해주기 때문이니깐요. 다른 객체에게 무엇을 제공해야 하고, 다른 객체로부터 무엇을 얻어야 하는가의 관점에서 접근해야 합니다. 즉, 어떤 메시지를 전송할 수 있고, 어떤 메시지를 이해할 수 있는지의 관점에서 보아야 한다는 뜻입니다.
객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 합니다. 메시지 중심으로 협력을 설계합시다.
우리는 앞서 객체들 간에 주고 받는 메시지를 기반으로 적절한 역할과 책임, 협력을 발견했습니다.
객체의 책임을 할당하고 다른 객체의 도움이 필요하다고 판단되면 요청을 위한 메시지를 결정했습니다. 그리고 메시지를 수신하기에 적합한 객체를 선택했습니다. 수신자는 송신자가 기대한대로 메시지를 처리할 책임이 있습니다. 그리고 메시지가 이 수신자의 책임을 결정했습니다.
이런 설계 방식은 객체가 외부에 제공하는 인터페이스가 독특한 스타일을 따르게 합니다.
“묻지 말고 시켜라”
이는 메시지가 어떻게 해야하는지 지시하지 말고 무엇을 해야 하는지 요청해야 한다는걸 의미합니다.
무엇을 해야 하는지 요청하기 때문에 객체의 인터페이스 크기를 줄이고 설계를 유연하게 만듭니다. 또한, 의도도 명확해집니다.
그럼 “묻지 말고 시켜라” 방식을 따르는 인터페이스는 뭔가요?
인터페이스는 어떤 두 사물이 마주치는 경계 지점에서 서로 상호작용할 수 있게 이어주는 방법이나 장치를 의미합니다. 인터페이스만 알면 객체의 내부 구조나 작동 방식을 몰라도 객체와 상호작용할 수 있게 되죠.
우리는 리모컨 전원 버튼을 눌러서 TV를 켭니다. 전원 버튼을 눌렀을 때 리모컨이 어떤 식으로 TV를 켰는지에 대해서 우리는 몰라도 됩니다. 방식을 몰라도 TV를 켤 수 있기 때문입니다.
인터페이스는 객체가 수신할 수 있는 메시지 목록으로 구성되어 있습니다.
메시지 목록은 두 가지 형식으로 나뉩니다. 공개된 것과 감춰진 것으로요.
다른 객체로부터 수신할 수 있어야 하는데 감춰져 있으면 어떡하죠?
다른 객체뿐만 아니라 자기 스스로에게 뭔가를 요청하는 경우에도 메시지를 전송해야 합니다. 자신과의 상호작용이죠. 스스로에게 요청을 하기 때문에 외부 객체들은 해당 메시지를 몰라도 됩니다.
우리가 주로 얘기하게 되는 인터페이스는 바로, “공개된 인터페이스” 입니다.
객체가 협력에 참여하기 위해 수행하는 메시지는 공용 인터페이스를 뜻합니다. 협력에 쓰이기 때문에 객체가 외부로부터 수신할 수 있어야 합니다.
인터페이스를 공개된 인터페이스와 감춰진 인터페이스로 나누듯, 객체의 외부와 내부를 구분하는 것은 중요합니다.
외부와 내부를 구분하게 되면, 객체의 자율성을 저해하지 않도록 좀 더 추상적인 인터페이스를 만들 수 있습니다. 또한, 외부에서 사용할 필요가 없는 인터페이스는 최대한 노출을 하지 않도록 인터페이스를 구성하기 때문에 인터페이스가 최소화됩니다.
가장 중요한 것은 인터페이스와 구현간에 차이가 있음을 인식할 수 있다는 겁니다.
구현은 내부 구조와 작동 방식을 가리킵니다. 객체를 구성하지만 공용 인터페이스에 포함되지 않는 모든 것을 구현이라고 보면 됩니다.
우리가 위에서 객체의 외부와 내부를 구분하는 것은 중요하다고 했습니다. 이는 객체의 공용 인터페이스와 구현을 명확하게 분리하라는 말과 동일합니다. 훌륭한 객체는 구현을 모른 채 인터페이스만 알면 쉽게 상호작용할 수 있습니다. 즉, 우리가 리모컨의 원리를 몰라도 원하는대로 TV를 켜고 끌 수 있는 거처럼요.
객체 설계의 핵심은 객체를 두 개의 분리된 요소로 분할해서 설계하는 겁니다. 외부에 공개되는 인터페이스와 내부에 감춰지는 구현으로 분할해서 설계하는거죠.
분할해서 설계하는 것이 왜 중요할까요?
소프트웨어는 항상 변경됩니다. 어떤 객체를 수정했을 때 어떤 객체가 영향을 받는지 판단하는 것은 어렵습니다. 따라서, 변경 안전 지대를 만들어 주어야 합니다. 적절한 구현을 선택하고 이를 인터페이스 뒤로 감춰서 객체의 자율성을 향상시켜주는거죠.
변경 안전 지대에 있는 상태, 메서드 구현이 객체 외부에 영향을 미쳐서는 안됩니다. 객체 외부에 영향을 미치는 변경은 객체의 공용 인터페이스를 수정할 때 뿐입니다.
결국, 변경을 관리하기 위해서 우리는 분할해서 설계해야 합니다. 변경될만한 부분은 객체 내부에 숨기는거죠.
분할 설계는 자율성 보존을 위해서 구현을 외부로부터 감추게 됩니다.
객체의 상태와 행위를 캡슐화하여 협력적이고 자율적인 존재로 만들 수 있게 되는 겁니다.
상태와 행위를 캡슐화한 것을 “데이터 캡슐화” 라고 합니다. 상태와 행위를 한 데 묶은 후에 외부에서 반드시 접근해야 하는 행위만을 골라서 공용 인터페이스로 노출하는거죠. 객체는 외부에서 객체와 의사소통할 수 있는 고정된 경로인 공용 인터페이스를 경계로 최대한의 자율성을 보장받을 수 있습니다.
구현 변경 시 외부에 대한 파급 효과를 최소화하기 위해서는 외부의 객체는 공용 인터페이스에 의존해야 합니다. 구현 세부 사항에 의존해서는 안됩니다. 또한, 어떤 것도 동시에 객체 내부와 외부에 포함될 수 없습니다. 내부, 외부 중 하나에만 포함되어야 합니다.
내부와 외부를 명확하게 구분하면 설계가 단순하고 유연하고 변경하기 쉬워집니다.
내부와 외부를 명확하게 구분하게 되면 객체의 책임이 자율적이게 됩니다.
자율적인 객체는 협력을 단순하게 만듭니다. 책임이 적절하게 추상화되었기 때문이죠. 자율적인 책임은 객체 외부와 내부를 명확하게 분리하게 됩니다. 요청하는 객체가 몰라는 되는 사적인 부분이 객체 내부로 캡슐화되기 때문에 인터페이스와 구현이 분리되게 됩니다.
책임을 수행하는 내부적인 방법인 구현은 변경을 해도 외부에 영향을 미치지 않습니다. 구현이 외부에 영향을 미치지 않으면 변경에 의해서 수정되어야 하는 범위가 좁아지고 명확해집니다. 또한, 변경의 파급효과가 객체 내부로 캡슐화되기 때문에 객체의 결합도도 낮아지게 됩니다.
자율적인 책임을 가진 객체는 협력의 대상을 다양하게 선택할 수 있는 유연성이 제공됩니다. 협력이 유연해지고 다양한 문맥에서 재활용 가능해지기 때문이죠. 책임이 자율적이라는건 객체의 역할을 이해하기 쉬워졌음을 뜻하기도 합니다.
객체지향의 강력함을 누리기 위한 출발점은 책임을 자율적으로 만드는 것입니다. 그리고 이것은 선택하는 메시지에 따라서 달라지게 됩니다.