6장 : 메시지와 인터페이스
💡 실행 가능한 코드를 통해 개념을 이해하는 것이 가장 좋은 방법이다.
훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아니라 객체를 지향해야 한다.
1. 협력과 메시지
✔️ 클라이언트-서버 모델
협력은 어떤 객체가 다른 객체에게 무언가를 요청할 때 시작된다. 메시지는 객체 사이의 협력을 가능하게 하는 매개체다.
두 객체 사이의 협력 관계를 설명하기 위해 사용하는 전통적인 메타포는 클라이언트-서버(Client-Server) 모델
이다. 협력 안에서 메시지를 전송하는 객체를 클라이언트, 메시지를 수신하는 객체를 서버라고 부른다.
✔️ 메시지와 메시지 전송
메시지(message)
는 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단이다. 한 객체가 다른 객체에게 도움을 요청하는 것을 메시지 전송(message sending) 또는 메시지 패싱(message passing)이라고 부른다. 이때 메시지를 전송하는 객체를 메시지 전송자(message sender)라고 부르고 메시지를 수신하는 객체를 메시지 수신자(message receiver)라고 부른다. 클라이언트-서버 모델 관점에서는 메시지 전송자는 클라이언트, 메시지 수신자는 서버라고 부르기도 한다.
메시지는 오퍼레이션명(operation name)과 인자(argument)로 구성되며 메시지 전송은 여기에 메시지 수신자를 추가한 것이다. 따라서 메시지 전송은 메시지 수신자, 오퍼레이션명, 인자의 조합이다.
메시지 : isSatisfiedBy(screening)
메시지 전송 : condition.isSatisfiedBy(screening)
✔️ 메시지와 메서드
메시지를 수신했을 때 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제 타입이 무엇인가에 달려 있다.
메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드
라고 부른다. 중요한 것은 코드 상에서 동일한 이름의 변수(condition)에게 동일한 메시지를 전송하더라도 객체의 타입에 따라 실행되는 메서드가 달라질 수 있다는 것이다. 전통적인 방식의 개발은 코드의 의미가 컴파일 시점과 실행 시점에 동일한 반면, 객체는 메시지와 메서드라는 두 가지 서로 다른 개념을 실행 시점에 연결해야 하기 때문에 컴파일 시점과 실행 시점의 의미가 달라질 수 있다.
메시지 전송자와 메시지 수신자는 서로에 대한 상세한 정보를 알지 못한 채 단지 메시지라는 얇고 가는 끈을 통해 연결된다. 실행 시점에 메시지와 메서드를 바인딩
하는 메커니즘은 두 객체 사이의 결합도를 낮춤으로써 유연하고 확장 가능한 코드를 작성할 수 있게 만든다.
✔️ 퍼블릭 인터페이스와 오퍼레이션
객체는 안과 밖을 구분하는 뚜렷한 경계를 가진다. 외부의 객체는 오직 객체가 공개하는 메시지를 통해서만 객체와 상호작용할 수 있는데, 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 퍼블릭 인터페이스
라고 부른다.
프로그래밍 언어의 관점에서 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션(operarion)
이라고 부른다.
오퍼레이션이라 부를 때는 내부 구현 코드는 제외하고 단순히 메시지와 관련된 시그니처를 가리키는 경우가 대부분이다.
DiscountCondition 인터페이스에 정의된 isSatisfiedBy
메시지를 수신했을 때 실제로 실행되는 코드는 메서드라고 부른다.
SequenceCondition과 PeriodCondition에 정의된 각각의 isSatisfiedBy
SequenceCondition과 PeriodCondition의 두 메서드는 DiscountCondition 인터페이스에 정의된 isSatisfiedBy 오퍼레이션의 여러 가능한 구현 중 하나다.
프로그래밍 언어의 관점에서 객체가 다른 객체에게 메시지를 전송하면 런타임 시스템은 메시지 전공을 오퍼레이션 호출로 해석하고 메시지를 수신한 객체의 실제 타입을 기반으로 적절한 메서드를 찾아 실행한다. 따라서 퍼블릭 인터페이스와 메시지의 관점에서 보면 ‘메서드 호출’보다는 ‘오퍼레이션 호출’이라는 용어를 사용하는 것이 더 적절하다.
✔️ 시그니처
오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합쳐 시그니처(signature)
라고 부른다.
메시지
객체가 다른 객체와 협력하기 위해 사용하는 의사소통 매커니즘. 일반적으로 객체의 오퍼레이션이 실행되도록 요청하는 것을 “메시지 전송”이라고 부른다. 메시지는 협력에 참여하는 전송자와 수신자 양쪽 모두를 포함하는 개념이다.
오퍼레이션
객체가 다른 객체에게 제공하는 추상적인 서비스다. 메시지가 전송자와 수신자 사이의 협력 관계를 강조하는 데 비해 오퍼레이션은 메시지를 수신하는 객체의 인터페이스를 강조한다.
메서드
메시지에 응답하기 위해 실행되는 코드 블록을 메서드라고 부른다. 메서드는 오퍼레이션의 구현이다. 동일한 오퍼레이션이라고 해도 메서드는 다를 수 있다. 오퍼레이션과 메서드의 구분은 다형성의 개념과 연결된다.
퍼블릭 인터페이스
객체가 협력에 참여하기 위해 외부에서 수신할 수 있는 메시지의 묶음. 클래스의 퍼블릭 메서드들의 집합이나 메시지의 집합을 가리키는 데 사용된다. 객체를 설계할 때 가장 중요한 것은 훌륭한 퍼블릭 인터페이스를 설계하는 것이다.
시그니처
시그니처는 오퍼레이션이나 메서드의 명세를 나타낸 것으로, 이름과 인자 목록을 포함한다. 대부분의 언어는 시그니처의 일부로 반환 타입을 포함하지 않지만 반환 타입을 시그니처의 일부로 포함하는 언어도 존재한다.
중요한 것은 객체가 수신할 수 있는 메시지가 객체의 퍼블릭 인터페이스와 그 안에 포함될 오퍼레이션을 결정한다는 것이다. 객체의 퍼블릭 인터페이스가 객체의 품질을 결정하기 때문에 결국 메시지가 객체의 품질을 결정한다고 할 수 있다.
2. 인터페이스와 설계 품질
좋은 인터페이스는 최소한의 인터페이스
와 추상적인 인터페이스
라는 조건을 만족해야 한다.
최소주의를 따르면서도 추상적인 인터페이스를 설계할 수 있는 가장 좋은 방법은 책임 주도 설계 방법을 따르는 것이다. 책임 주도 설계 방법은 메시지를 먼저 선택함으로써 협력과는 무관한 오퍼레이션이 인터페이스에 스며드는 것을 방지한다. 따라서 인터페이스는 최소의 오퍼레이션만 포함하게 된다. 또한 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 함으로써 클라이언트의 의도를 메시지에 표현할 수 있게 한다.
여기서는 퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법에 관해 살펴보겠다.
디미터 법칙
묻지 말고 시켜라
의도를 드러내는 인터페이스
명령-쿼리 분리
✔️ 디미터 법칙
협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙이 바로 디미터 법칙(Law of Demeter)
이다.
객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것이다.
“낯선 자에게 말하지 말라(don’t talk to strangers)”
“오직 인접한 이웃하고만 말하라(only talk to your immediate neighbors)”
“오직 하나의 도트만 사용하라(use only one dot)”
디미터 법칙을 따르기 위해서는 클래스 내부의 메서드가 아래 조건을 만족하는 인스턴스에만 메시지를 전송하도록 프로그래밍 해야한다.
this 객체
메서드의 매개변수
this의 속성
this의 속성인 컬렉션의 요소
메서드 내에서 생성된 지역 객체
디미터 법칙과 캡슐화
디미터 법칙은 캡슐화를 다른 관점에서 표현한 것이다. 디미터 법칙이 가치 있는 이유는 클래스를 캡슐화하기 위해 따라야 하는 구체적인 지침을 제공하기 때문이다.
메시지 전송자가 수신자의 내부 구조에 대해 물어보고 반환받은 요소에 대해 연쇄적으로 메시지를 전송한다. 흔히 이와 같은 코드를
기차 충돌(train wreck)
이라고 부른다.기차 충돌은 클래스의 내부 구현이 외부로 노출됐을 때 나타나는 전형적인 형태로 메시지 전송자는 메시지 수신자의 내부 정보를 자세히 알게 된다.
디미터 법칙은 객체가 자신을 책임지는 자율적인 존재여야 한다는 사실을 강조한다. 정보를 처리하는 데 필요한 책임을 정보를 알고 있는 객체에게 할당하기 때문에 응집도가 높은 객체가 만들어진다.
✔️ 묻지 말고 시켜라
내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재하고 있으면 해당 객체가 책임져야 하는 어떤 행동이 객체 외부로 누수된 것이다.
상태를 묻는 오퍼레이션을 행동을 오평하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라.
✔️ 의도를 드러내는 인터페이스
켄트 벡(Kent Beck)은 <Smalltalk Best Practice Patterns>에서 메서드를 명명하는 두 가지 방법을 설명했다.
첫 번째 방법은 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는 것이다. (지양)
두 번째 방법은 ‘어떻게’가 아니라 ‘무엇’을 하는지를 드러내는 것이다.
무엇을 하는지를 드러내도록 메서드의 이름을 짓기 위해서는 객체가 협력 안에서 수행해야 하는 책임에 관해 고민해야 한다. 이것은 외부의 객체가 메시지를 전송하는 목적을 먼저 생각하도록 만들며, 결과적으로 협력하는 클라이언트의 의도에 부합하도록 메서드의 이름을 짓게 된다.
이처럼 어떻게 하느냐가 아니라 무엇을 하느냐에 따라 메서드의 이름을 짓는 패턴을 **
의도를 드러내는 선택자(Intention Revelaing Selector)
**라고 부른다.
객체에게 붇지 말고 시키되 구현 방법이 아닌 클라이언트의 의도를 드러내야 한다.
✔️ 함께 모으기
디미터 법칙, 묻지 말고 시켜라 스타일, 의도를 드러내는 인터페이스를 이해할 수 있는 좋은 방법 중 하나는 이럼 원칙을 위반하는 코드의 모습을 살펴보는 것이다.
😱 디미터 법칙을 위반하는 티켓 판매 도메인
Theater의 enter 메서드는 디미터 법칙을 위반한 코드의 전형적인 모습을 잘 보여준다.
근본적으로 디미터 법칙을 위반하는 설계는 인터페이스와 구현의 분리 원칙을 위반한다.
😱 묻지 말고 시켜라
먼저 Theater가 TicketSeller에게 자신이 원하는 일을 시키도록 수정하자. Theater가 TicketSeller에게 시키고 싶은 일은 Audience가 Ticket을 가지도록 만드는 것이다. TicketSeller에 setTicket 메서드를 추가하고 enter 메서드의 로직을 setTIcket 메서드 안으로 옮기자.
Theater는 자신의 속성으로 포함하고 있는 TicketSeller의 인스턴스에게만 메시지를 전송하게 됐다. 디미터 법칙을 준수하도록 Theater를 수정한 것이다.
이제 TicketSeller에 초점을 맞춰보자. TicketSeller가 원하는 것은 Audience가 Ticket을 보유하도록 만드는 것이다. 따라서 Audience에게 setTicket 메서드를 추가하고 스스로 티켓을 가지도록 만들자
이제 TicketSeller는 속성으로 포함하고 있는 TicketOffice의 인스턴스와 인자로 전달된 Audience에게만 메시지를 전송한다. 따라서 TicketSeller 역시 디미터 법칙을 준수한다.
Audience의 setTIcket 메서드를 자세히 살펴보면 Audience가 Bag에게 원하는 일을 시키기 전에 hasInvitation 메서드를 이용해 초대권을 가지고 있는지를 묻는다는 사실을 알 수 있다. 따라서 Audience는 디미터 법칙을 위반한다.
Audience의 setTicket메서드 구현을 Bag의 setTicket 메서드로 이동시키자.
Audience의 setTicket 메서드가 Bag의 setTicket 메서드를 호출하도록 수정하면 묻지 말고 시켜라 스타일을 따르고 디미터 법칙을 준수하는 Audience를 얻을 수 있다.
디미터 법칙과 묻지 말고 시켜라 원칙에 따라 코드를 리팩터링한 후에 Audience 스스로 자신의 상태를 제어하게 됐다는 점에 주목하자.
😱 인터페이스에 의도를 드러내자
TicketSeller의 setTicket, Audience의 setTIcket, Bag의 setTIcket 메서드는 미묘하게 다른 의미를 가지고 있음에도 불구하고 같은 이름을 가지고 있다. 이는 클라이언트 개발자를 혼란스럽게 만들 확률이 높다. 문제는 TicketSeller의 입장에서, Audience의 입장에서, Bag의 입장에서 setRicket이라는 이름이 협렵하는 클라이언트의 의도를 명확하게 드러내지 못한다는 것이다. 따라서 클라이언트의 의도가 분명하게 드러나도록 객체의 퍼블릭 인터페이스를 개선해야 한다.
오퍼레이션의 이름은 협력이라는 문맥을 반영해야 한다. 오퍼레이션은 클라이언트가 객체에게 무엇을 원하는지를 표현해야 한다.
우리는 결합도가 낮으면서도 의도를 명확히 드러내는 간결한 협력을 원한다. 디미터 법칙과 묻지 말고 시켜라 스타일, 의도를 드러내는 인터페이스가 우리르 도울 것이다.
3. 원칙의 함정
디미터 법칙과 묻지 말고 시켜라 스타일은 객체의 퍼블릭 인터페이스를 깔끔하고 유연하게 만들 수 있는 훌륭한 설계 원칙이지만 절대적인 법칙은 아니다.
원칙이 현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시하라. 원칙을 아는 것보다 더 중요한 것은 언제 원칙이 유용하고 언제 유용하지 않은지를 판단할 수 있는 능력을 기르는 것이다.
✔️ 디미터 법칙은 하나의 도트(.)를 강제하는 규칙이 아니다
기차 충돌처럼 보이는 코드라도 객체의 내부 구현에 대한 어떤 정보도 외부로 토출하지 않는다면 그것은 디미터 법칙을 준수한 것이다.
✔️ 결합도와 응집도의 충돌
일반적으로 어떤 객체의 상태를 물어본 후 반환된 상태를 기반으로 결정을 내리고 그 결정에 따라 객체의 상태를 변경하는 코드는 묻지 말고 시켜라 스타일로 변경해야 한다.
안타깝게도 묻지 말고 시켜라와 디미터 법칙을 준수하는 것이 항상 긍정적인 결과로 귀결되는 것은 아닌데, 모든 상황에서 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션들이 공존하게 되고, 결과적으로 객체는 상관 없는 책임들을 한꺼번에 떠안게 되기 때문에 결과적으로 응집도가 낮아진다.
클래스는 하나의 변경 원인만을 가져야 한다. 서로 상관없는 책임들이 함께 뭉쳐있는 클래스는 응집도가 낮으며 작은 변경으로도 쉽게 무너질 수 있다.
가끔씩 묻는 것 외에는 다른 방법이 존재하지 않는 경우도 존재한다. 컬렉션에 포함된 객체들을 처리하는 유일한 방법은 객체에게 물어보는 것이다.
로버트 마틴은 <클린 코드>에서 디미터 법칙의 위반 여부는 묻는 대상이 객체인지, 자료 구조인지에 달려있다고 설명한다. 객체는 내부 구조를 숨겨야 하므로 디미터 법칙을 따르는 것이 좋지만 자료 구조라면 당연히 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다.
원칙을 맹신하지 마라. 원칙이 적절한 상황과 부적절한 상황을 찬단할 수 있는 안목을 길러라. 설계는 트레이드오프의 산물이다. 소프트웨어 설계에 존재하는 몇 안 되는 법칙 중 하나는 “경우에 따라 다르다”라는 사실을 명심하라.
4. 명령-쿼리 분리 원칙
가끔씩 필요에 따라 물어야 한다는 사실에 납득했다면 명령-쿼리 분리(Command-Query Separation) 원칙
을 알아두면 도움이 될 것이다. 명령-쿼리 분리 원칙은 퍼블릭 인터페이스에 오퍼레이션을 정의할 때 참고할 수 있는 지침을 제공한다.
어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴(routine)
이라고 부른다. 루틴은 다시 프로시저(procedure)
와 함수(function)
로 구분할 수 있다.
프로시저는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다
함수는 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다
명령(Command)
과 쿼리(Query)
는 객체의 인터페이스 특면에서 프로시저와 함수를 부르는 또 다른 이름이다. 객체의 상태를 수정하는 오퍼레이션을 명령이라고 부르고 객체와 관련된 정보를 반환하는 오퍼레이션을 쿼리라고 부른다. 따라서 개념적으로 명령은 프로시저와 동일하고 쿼리는 함수와 동일하다.
명령-쿼리 분리 원칙의 요지는 오퍼레이션은 부수효과를 발생시키는 명령이거나 부수 효과를 발생시키지 않는 쿼리 중 하나여야 한다는 것이다. 어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안 된다. 따라서 명령과 쿼리를 분리하기 위해서는 다음의 두 가지 규칙을 준수해야 한다.
객체의 상태를 변경하는 명령은 반환값을 가질 수 없다
객체의 정보를 반환하는 쿼리는 상태를 변경할 수 없다
마틴 파울러(Martin Fowler)는 명령-쿼리 분리 원칙에 따라 작성된 객체의 인터페이스를 명령-쿼리 인터페이스(Command-Auery Interface)라고 부른다.
✔️ 반복 일정의 명령과 쿼리 분리하기
명령과 쿼리를 뒤섞으면 실행 결과를 예측하기각 어려워질 수 있다. isSatisfied 메서드처럼 겉으로 보기에는 쿼리처럼 보이지만 내부적으로 부수효과를 가지는 메서드는 이해하기 어렵고, 잘못 사용하기 쉬우며, 버그를 양산하는 경향이 있다. 가장 깔끔한 해결책은 명령과 쿼리를 명확하게 분리하는 것이다.
명령과 쿼리를 분리하면서 reschedule 메서드의 가시성이 private에서 public으로 변경됐다는 점을 눈여겨 봐라.
✔️ 명령-쿼리 분리와 참조 투명성
명령과 쿼리를 분리함으로써 명령형 언어의 틀 안에서 참조 투명성(referential transparency)
의 장점을 제한적이나마 누릴 수 있게 된다.
참조 투명성 → “어떤 표현식 e가 있을 때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성”
컴퓨터의 세계와 수학의 세계를 나누는 가장 큰 특징은 부수효과(side effect)의 존재 유무다.
✔️ 책임에 초점을 맞춰라
책임 주도 설계 방법에 따라 메시지가 객체를 결정하게 하라.
Last updated