[Object] 6장
6. 메시지와 인터페이스
클래스라는 구현 도구에 지나치게 집착하면 경직되고 유연하지 못한 설계에 이를 확률이 높아진다. 훌량한 객체지향 코드를 얻기 위해서는 객체를 지향해야 한다. 협력 안에서 객체가 수행하는 책임에 초점을 맞춰야 한다. 중요한 것은 책임이 객체가 수신할 수 있는 메시지의 기반이 된다는 것이다.
객체지향 애플리케이션의 가장 중요한 재료는 클래스가 아니라 객체들이 주고받는 메시지다. 애플리케이션은 클래스로 구성되지만 메시지를 통해 정의된다는 사실을 기억해라. 객체가 수신하는 메시지들이 객체의 퍼블릭 인터페이스를 구성한다.
협력과 메시지
클라이언트-서버 모델
협력은 어떤 객체가 다른 객체에게 무언가를 요청할 때 시작된다. 메시지는 객체 사이의 협력을 가능하게 하는 매개체다. 두 객체 사이의 협력 관계를 설명하기 위해 사용되는 전통적인 메타포는 클라이언트-서버(Client-Server) 모델이다. 협력 안에서 메시지를 전송하는 객체를 클라이언트, 메시지를 수신하는 객체를 서버라고 부른다.
객체는 협력에 참여하는 동안 클라이언트와 서버의 역할을 동시에 수행하는 것이 일반적이다. 협력에 적합한 객체를 설계하기 위해서는 외부에 전송하는 메시지의 집합도 함께 고려하는 것이 바람직하다. 더 큰 책임을 수행하기 위해서는 다른 객체와 협력해야 한다.
메시지와 메시지 전송
메시지(message) 는 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단이다. 메시지는 오퍼레이션명(operation name) 과 인자(argument) 로 구성되며 메시지 전송은 여기에 메시지 수신자를 추가한 것이다.
메시지와 메서드
메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드라고 부른다. 코드 상에서 동일한 이름의 변수에게 동일한 메시지를 전송하더라도 객체의 타입에 따라 실행되는 메서드가 달라질 수 있다. 전통적인 방식은 코드의 의미가 컴파일 시점과 실행 시점에 동일하다. 반면 객체는 메시지와 메서드라는 두 가지 서로 다른 개념을 실행 시점을 연결해야 하기 때문에 컴파일 시점과 실행 시점의 의미가 달라질 수 있다.
메시지에 응답할 수 있는 객체가 존재하고 그 객체가 적절한 메서드를 선택해서 응답할 것이라고 믿을 수밖에 없다. 메시지와 메서드의 구분은 메시지 전송자와 메시지 수신자가 느슨하게 결합될 수 있게 한다. 메시지 전송자는 자신이 어떤 메시지를 전송해야 하는지만 알면 된다. 실행 시점에 메시지와 메서드를 바인딩하는 메커니즘은 두 객체의 결합도를 낮춤으로써 유연하고 확장 가능한 코드를 작성할 수 있게 만든다.
퍼블릭 인터페이스와 오퍼레이션
객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 퍼블릭 인터페이스라고 부른다. 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션(operation) 이라고 부른다. 오퍼레이션은 수행 가능한 어떤 행동에 대한 추상화다.
퍼블릭 인터페이스와 메시지의 관점에서 보면 ‘메서드 호출’보다는 ‘오퍼레이션 호출’ 이라는 용어를 사용하는 것이 더 적절하다.
시그니처
오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합쳐 시그니쳐(signature) 라고 부른다. 메서드는 이 시그니처에 구현을 더한 것이다. 다형성의 축복을 받기 위해서는 하나의 오퍼레이션에 대한 다양한 메서드를 구현해야만 한다. 객체의 퍼블릭 인터페이스가 객체의 품질을 결정하기 때문에 결국 메시지가 객체의 품질을 결정한다고 할 수 있다.
인터페이스와 설계 품질
좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족해야한다. 최소한의 인터페이스는 꼭 필요한 오퍼레이션만을 인터페이스에 포함한다. 추상적인 인터페이스는 어떻게 수행하는지가 아니라 무엇을 하는지 표현한다.
최소주의를 따르면서도 추상적인 인터페이스를 설계할 수 있는 가장 좋은 방법은 책임 주도 설계 방법을 따르는 것이다. 메시지를 먼저 선택함으로써 협력과는 무관한 오퍼레이션이 인터페이스에 스며드는 것을 방지한다.
디미터 법칙
협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙이 바로 디미터 법칙(Law of Demeter) 이다. 디미터 법칙을 간단하게 요약하면 객체 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것이다.
디미터 법칙은 "낯선 자에게 말하지 말라(don't talk to strangers)" 또는 `“오직 인접한 이웃하고만 말하라(only talk to your immediate neighbors)”로 요약할 수 있다.
디미터 법칙을 따르면 부끄럼타는 코드(shy code) 를 작성할 수 있다. 부끄럼타는 코드란 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드를 말한다. 디미터 법칙을 따르는 코드는 메시지 수신자의 내부 구조가 전송자에게 노출되지 않으며, 메시지 전송자는 수신자의 내부 구현에 결합되지 않는다.
screening.getMovie().getDiscountConditions();
이와 같은 코드를 기차 충돌(train wreck) 이라고 부른다. 기차 충돌은 클래스의 내부 구현이 외부로 노출됐을 때 나타나는 전형적인 형태로 메시지 전송자는 메시지 수신자의 내부 정보를 자세히 알게 된다.
디미터 법칙은 객체가 자기 자신을 책임지는 자율적인 존재여야 한다는 사실을 강조한다. 또한, 객체의 내부 구조를 묻는 메시지가 아니라 수신자에게 무언가를 시키는 메시지가 더 좋은 메시지라고 속삭인다.
묻지말고 시켜라
묻지말고 시켜라(Tell, Don’t Ask) 는 객체의 상태에 관해 묻지 말고 원하는 것을 시켜야한다는 스타일의 메시지 작성을 장려하는 원칙을 가리키는 용어다.
객체지향의 기본은 함께 변경될 확률이 높은 정보와 행동을 하나의 단위로 통합하는 것이다. 묻지 말고 시켜라 원칙에 따르도록 메시지를 결장하다보면 자연스럽게 정보 전문가에게 책임을 할당하게 되고 높은 응집도를 가진 클래스를 얻을 확률이 높아진다.
상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라. 협력을 설계하고 객체가 수신할 메시지를 결정하는 매 순간 묻지 말고 시켜라 원칙과 디미터 법칙을 머리속에 떠올리는 것은 퍼블릭 인터페이스의 품질을 향상시킬 수 있는 좋은 습관이다.
의도를 드러내는 인터페이스
메서드를 명명하는 두 가지 방법이 있다.
- 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는 것이다.
- ‘어떻게’가 아니라 ‘무엇’을 하는지를 드러내는 것이다.
무엇을 하는지 드로내도록 메서드의 이름을 짓기 위해서는 객체가 협력 안에서 수행해야 하는 책임에 관해 고민해야 한다. 이것은 외부의 객체가 메시지를 전송하는 목적을 먼저 생각하도록 만든다.
어떻게 하느냐가 아니라 무엇을 하느냐에 따라 메서드의 이름을 짓는 패턴을 의도를 드러내는 선택자(Intention Revealing Selector) 라고 부른다.
에릭 에반스는 켄트 백의 의도를 드러내는 선택자를 인터페이스 레벨로 확장한 의도를 드러내는 인터페이스(Intention Revealing Interface) 를 제시했다. 의도를 드러내는 인터페이스는 구현과 관련된 모든 정보를 캡슐화하고 객체의 퍼블릭 인터페이스에는 협력과 관계된 의도만을 표현해야 한다.
원칙의 함정
원칙이 현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시하라.
디미터 법칙은 하나의 도트(.)를 강제하는 규칙이 아니다.
IntStream.of(1, 15, 20, 3, 9).filter(x -> x > 10).distinct().count();
이 코드는 디미터 법칙을 위반하지 않는다. 디미터 법칙은 결합도와 관련된 것이며, 이 결합도가 문제가 되는 것은 객체의 내부 구조가 외부로 노출되는 경우로 한정된다. 기차 충돌처럼 보이는 코드라도 객체의 내부 구현에 대한 어떤 정보도 외부로 노출하지 않는다면 그것은 디미터 법칙을 준수한 것이다.
명령-쿼리 분리 원칙
명령-쿼리 분리(Command-Query Separation) 원칙은 퍼블릭 인터페이스에 오퍼레이션을 정의할 때 참고할 수 있는 지침을 제공한다.
어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴(routine) 이라고 부른다. 루틴은 다시 프로시저(procedure) 와 함수(function) 로 구분할 수 있다.
- 프로시저는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
- 함수는 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
명령(Command) 과 쿼리(Query) 는 객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또 다른 이름이다. 명령-쿼리 분리 원칙의 요지는 오퍼레이션은 부수효과를 발생시키는 명령이거나 부수효과를 발생시키지 않는 쿼리 중 하나여야 한다는 것이다. 어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안된다.
명령-쿼리 분리 원칙을 한 문장으로 표현하면 "질문이 답변을 수정해서는 안 된다."는 것이다. 명령은 상태를 변경할 수 있지만 상태를 반환해서는 안 된다. 쿼리는 객체의 상태를 반환할 수 있지만 상태를 변경해서는 안된다.
마틴 파울러(Martin Fowler)는 명령-쿼리 분리 원칙에 따라 작성된 객체의 인터페이스를 명령-쿼리 인터페이스(Command-Query Interface) 라고 부른다.
명령-쿼리 분리와 참조 투명성
명령과 쿼리를 엄격하게 분류하면 객체의 부수효과를 제어하기 수월해진다. 참조 투명성(referential transparency) 이라는 특성을 잘 활용하면 버그가 적고, 디버깅이 용이하며, 쿼리의 순서에 따라 실행 결과가 변하지 않는 코드를 작성할 수 있다. 참조 투명성이란 “어떤 표현식 e가 있을 때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성”을 의미한다.
명령-쿼리 분리 원칙은 부수효과를 가지는 명령으로부터 부수효과를 가지지 않는 쿼리를 명백하게 분리함으로써 제한적이나마 참조 투명성의 혜택을 누릴 수 있게 된다.
책임에 초점을 맞춰라
책임 주도 설계 방법에 따라 메시지가 객체를 결정하게 하라.
Leave a comment