내용 요약
객체가 수신하는 메시지들이 객체의 퍼블릭 인터페이스를 구성한다.
훌륭한 퍼블릭 인터페이스를 얻기 위해서는 책임 주도 설계 바업을 따르는 것만으로는 부족하다.
유연하고 재사용 가능한 퍼블릭 인터페이스를 만드는 데 도움이 되는 설계 원칙과 기법을 익히고 적용해야 한다.
이런 원칙과 기법들을 살펴보는 거이 이번 장의 주제다.
클라이언트-서버 모델
두 객체 사이의 협력 관계를 설명하기 위해 사용하는 전통적인 메타포는 클라이언트-서버(Client-Server)모델이다.
협력 안에서 메시지를 전송하는 객체를 클라이언트, 메시지를 수신하는 객체를 서버라고 부른다. 협력은 클라이언트가 서버의 서비스를 요청하는 단방향 상호작용이다.
메시지와 메시지 전송
메시지(message)는 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단이다.
한 객체가 다른 객체에게 도움을 요청하는 것을 메시지 전송(message sending) 또는 메시지 패싱(message passing)이라고 부른다. 이때 메시지를 전송하는 객체를 메시지 전송자(message sender)라고 부르고 메시지를 수신하는 객체를 메시지 수신자(message receiver)라고 부른다.
메시지는 오퍼레이션명(operation name)과 인자(argument)로 구성되며 메시지 전송은 여기에 메시지 수신자를 추가한 것이다. 따라서 메시지 전송은 메시지 수신자, 오퍼레이션명, 인자의 조합이다.
메시지와 메서드
메시지를 수신했을 때 실행되는 함수 또는 프로시저를 메서드라고 부른다.
중요한 것은 코드 상에서 동일한 이름의 변수(condition)에게 동일한 메시지를 전송하더라도 객체의 타입에 따라 실행되는 메서드가 달라질 수 있다는 것이다. 기술적인 관점에서 객체 사이의 메시지 전송은 전통적인 방식의 함수 호출이나 프로시저 호출과는 다르다.
전통적인 방식의 개발자는 어떤 코드가 실행될지를 정확하게 알고 있는 상황에서 함수 호출이나 프로시저 호출 구문을 작성한다.
다시 말해 코드의 의미가 컴파일 시점과 실행 시점에 동일하다는 것이다. 반면 객체는 메시지와 메서드라는 두 까지 서로 다른 개념을 실행 시점에 연결해야 하기 때문에 컴파일 시점과 실행 시점의 의미가 달라질 수 있다.
메시지 전송자와 메시지 수신자는 서로에 대한 상세한 정보를 알지 못한 채 단지 메시지라는 얆고 가는 끈을 통해 연결된다. 실행 시점에 메시지와 메서드를 바인딩하는 메커니즘은 두 객체 사이의 결함도를 낮춤으로써 유연하고 확장 가능한 코드를 작성할 수 있게 만든다.
퍼블릭 인터페이스와 오퍼레이션
객체는 안과 밖을 구분하는 뚜렷한 경계를 가진다. 외부에서 볼 때 객체의 안쪽은 검은 장막으로 가려진 미지의 영역이다.
외부의 객체는 오직 객체가 공개하는 메시지를 통해서만 객체와 상호작용할 수 있다. 이처럼 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 퍼블릭 인터페이스라고 부른다.
프로그래밍 언어의 관점에서 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션(operation)이라고 부른다.
오퍼레이션은 수행 가능한 어떤 행동에 대한 추상화다. 흔히 오퍼레이션이라고 부를 때는 내부의 구현 코드는 제외하고 단순히 메시지와 관련된 시그니처를 가리키는 경우가 대부분이다.
시그니처
오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합쳐 시그니처(signature)라고 부른다.
오퍼레이션은 실행 코드 없이 시그니처만을 정의한 것이다. 메서드는 이 시그니처에 구현을 더한 것이다.
일반적으로 메시지를 수신하면 오퍼레이션의 시그니처와 동일한 메서드가 실행된다.
디미터 법칙
협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙이 바로 디미터 법칙(Law Of Demeter)이다.
디미터 법칙을 간단하게 요약하면 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것이다.
디미터 법칙은 “낯선 자에게 말하지 말아라“ 또는 “오직 인접한 이웃하고만 말하라”로 요약할 수 있다.
자바나 C#과 같이 ‘도트(.)’를 이용해 메시지 전송을 표현하는 언어에서는 “오직 하나의 도트만 사용하라”라는 말로 요약되기도 한다.
디미터 법칙을 따르기 위해서는 클래스가 특정한 조건을 만족하는 대상에게만 메시지를 전송하도록 프로그래밍해야 한다.
모든 클래스 C와 C에 구현된 모든 메서드 M에 대해서, M이 메시지를 전송할 수 있는 모든 객체는 다음에 서술된 클래스의 인스턴스여야 한다. 이때 M에 의해 생성된 객체나 M이 호출하는 메서드에 의해 성성된 객체, 전역 변수로 선언된 객체는 모두 M의 인자로 간주한다.
- M의 인자로 전달된 클래스(C 자체를 포함)
- C의 인스턴스 변수의 클래스
위 설명이 이해하기 어렵다면 클래스 내부의 메서드가 아래 조건을 만족하는 인스턴스에만 메시지를 전송하도록 프로그래밍해야 한다고 이해해도 무방하다.
- this 객체
- 메서드의 매개변수
- this의 속성
- this의 속성인 컬렉션의 요소
- 메서드 내에서 생성된 지역 객체
디미터 법칙을 따르면 부끄럼타는 코드(shy code)를 작성할 수 있다.
부끄럼 타는 코드란 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드를 말한다.
디미터 법칙을 따르는 코드는 메시지 수신자의 내부 구조가 전송자에게 노출되지 않으며, 메시지 전송자는 수신자의 내부 구현에 결합되지 않는다. 따라서 서버 사이의 낮은 결합도를 유지할 수 있다.
메시지 전송자가 수신자의 내부 구조에 대해 물어보고 반환반은 요소에 대해 연쇄적으로 메시지를 전송한다.
흔히 이와 같은 코드를 기차 충돌(train wreck)이라고 부르는데 여러 대의 기차가 한줄로 늘어서 충돌한 것처럼 보이기 때문이다. 기차 충돌은 클래스의 내부 구현이 외부로 노출됐을때 나타나는 전형적인 형태로 메시지 전송자는 메시지 수신자의 내부 정보를 자세히 알게 된다. 따라서 메시지 수신자의 캡슐화는 무너지고, 메시지 전송자가 메시지 수신자의 내부 구현에 강하게 결합된다.
묻지말고 시켜라
디미터 법칙은 훌륭한 메시지는 객체의 상태에 관해 묻지 말고 원한느 것을 시켜야 한다는 사실을 강조한다.
묻지말고 시켜라(Tell, Don’t Ask)는 이런 스타일의 메시지 작성을 장려하는 원칙을 가리키는 용어다.
상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라.
협력을 설계하고 객체가 수신할 메시지를 결정하는 매 순간 묻지 말고 시켜라 원칙과 디미터 법칙을 머릿속에 떠올리는 것은 퍼블릭 인터페이스의 품질을 향상시킬 수 있는 좋은 습관이다.
하지만 단순하게 객체에게 묻지 않고 시킨다고 해서 모든 문제가 해결되는 것은 아니다. 훌륭한 인터페이스를 수확하기 위해서는 객체가 어떻게 작업을 수행하는지를 노출해서는 안 된다. 인터페이스는 객체가 어떻게 하는지가 아니라 무엇을 하는지를 서술해야 한다.
의도를 드러내는 인터페이스
켄트 벡(Kent Beck)은 그의 기념비적인 책인 Smalltalk Best Practice Patterns에서 메서드를 명명하는 두 가지 방법을 설명했다.
첫 번째 방법은 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는 것이다.
메서드의 이름을 짓는 두 번째 방법은 ‘어떻게’가 아니라 ‘무엇’을 하는지를 드러내는 것이다.
메서드의 구현이 한 가지인 경우에는 무엇을 하는지를 드러내는 이름을 짓는 것이 어려울 수도 있다.
하지만 무엇을 하는지를 드러내는 이름은 코드를 읽고 이해하기 쉽게 만들뿐만 아니라 유연한 코드를 낳는 지름길이다.
이처럼 어떻게 하느냐가 아니라 무엇을 하느냐에 따라 메서드의 이름을 짓는 패턴을 의도를 드러내는 선택자(Intention Revealing Selector)라고 부른다.
명령-쿼리 분리 원칙
가끔은 필요에 따라 물어야 한다는 사실에 납득했다면 명령-쿼리 분리(Command-Qeury Separation) 원칙을 알아두면 도움이 될 것이다. 명령-쿼리 분리 원칙은 퍼블릭 인터페이스에 오퍼레이션을 정의할 때 참고할 수 있는 지침을 제공한다.
어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴(routine)이라고 부른다. 루틴은 다시 프로시저(procedure)와 함수(function)로 구분할 수 있다. 프로시저와 함수를 같은 의미로 혼용하는 경우가 많지만 사실 프로시저와 함수는 부수효과와 반환관 유무라는 측면에서 명확하게 구분된다.
프로시저와 함수를 명확하게 구분하기 위해 루틴을 작성할 때 다음과 같은 제약을 따라야 한다.
- 프로시저는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
- 함수는 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
명령(Command)와 쿼리(Query)는 객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또 다른 이름이다.
객체의 상태를 수정하는 오퍼레이션을 명령이라고 부르고 객체와 관련된 정보를 반환하는 오퍼레이션을 쿼리라고 부른다. 따라서 개념적으로 명령은 프로시저와 동일하고 쿼리는 함수와 동일하다.
따라서 명령과 쿼리를 분리하기 위해서는 다음의 두 가지 규칙을 준수해야 한다.
- 객체의 상태를 변경하는 명령은 반환값을 가질 수 없다.
- 객체의 정보를 반환하는 쿼리는 상태를 변경할 수없다.
느낀점
신입 개발자에서 부터 동료들과 지속적으로 객체지향에 대한 고민과 공부를 하면서 디미터 법칙과 TDA법칙은 익숙했다. 하지만 여태껏 메서드의 이름을 ‘무엇을’ 하는지가 아니라 ‘어떻게’했는지 구현에 의존한 메서드를 많이 작성했던 것 같아서 반성이 되는 부분이였다.
또한 MSA 서비스를 개발하면서 아키텍처 레벨의 CQRS 패턴을 공부하면서도 객체의서의 명령-쿼리 분리 원칙을 생각하지 못했다는 것을 반성하게 되는 부분이였다.
DDD를 통해 개발하면서 도메인의 부수적인 효과(side effect)를 방지하기 위해 도메인은 되도록 불변으로 작성하도록 했던 부분이 생각나는 부분이였다.
