6장 - 메시지와 인터페이스

Date
2024/02/25 10:04
Tags

1. 협력과 메시지

1.1. 클라이언트-서버 모델

협력은 어떤 객체가 다른 객체에게 무언가를 요청할 때 시작된다.
메시지는 객체 사이의 협력을 가능케 하는 매개체다.
두 객체 사이의 협력 관계 설명을 위하 사용하는 전통적 메타포는 클라이언트-서버 모델이다.
협력 안에서 송신 객체는 클라이언트, 수신 객체는 서버라고 부른다.
협력에 참여하는 동안 객체는 서버이자 클라이언트이다.
메시지는 객체가 수신하는 메시지의 집합과 외부에 전송하는 집합으로 구성된다.

1.2. 메시지와 메시지 전송

메시지는 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단이다.
메시지 전송(메시지 패싱) = 다른 객체에거 도움 요청
메시지 전송자 = 메시지 전송 객체
메시지 수신자 = 메시지 수신객체
메시지는 오퍼레이션명과 인자로 구성되며 전송은 메시지 수신자를 추가한 것이다.

1.3. 메시지와 메서드

메시지를 수신했을 때 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제 타입이 무엇인가에 달려있다.
메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드라고 부른다.
코드 상 동일한 이름의 변수에게 동일한 메시지를 전송하더라도 객체의 타입에 따라 실행되는 메서드가 달라질 수 있다.
컴파일 시점과 런타임 시점의 객체가 다를 수 있기 때문
실행 시점에 따라 메시지 수신 객체가 달라지기 때문에 수신 객체가 적절한 응답을 할 것이라 믿을 수 밖에 없다.
메세지와 메서드의 구분은 메시지 전송자와 메시지 수신자가 느슨하게 결합될 수 있게 한다.
송신자는 어떤 메시지를 전송할지만 집중하고 수신자는 단지 도착했다는 사실에만 집중하면 된다.

1.4. 퍼블릭 인터페이스와 오퍼레이션

객체는 안과 밖을 구분하는 뚜렷한 경계를 가진다.
외부에서 볼 때 객체 안쪽은 검증 장막으로 가려진 미지의 영역
객체가 외부와 의사소통을 위해 공개하는 메시지의 집합을 퍼블릭 인터페이스라 함
프로그래밍 언어 과점에서 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션이라고 부른다.
오퍼레이션은 수행 가능한 행동에 대한 추상화다.
메시지를 수신했을 때 실제로 실행되는 코드는 메서드라 부른다.
프로그래밍 언어 과점에서 객체가 다른 객체에게 메시지를 전송하면 런타임 시스템은 메시지 전송을 오퍼레이션 호출로 해석하고 수신 객체의 실제 타입을 기반으로 적절한 메서드를 찾아 실행한다.

1.5. 시그니처

오퍼레이션의 이름과 파라미터 목록을 합쳐 시그니쳐라고 부른다.
오퍼레이션은 실행 코드 없이 시그니처만을 정의한 것이다.
메서드는 이 시그니처에 구현을 더한 것

1.6. 용어 정리

메시지: 객체가 다른 객체와 협력하기 위해 사용하는 의사소통 메커니즘.
일반적으로 객체의 오퍼레이션이 실행되도록 요 청하는 것을 "메시지 전송"이라고 부른다.
메시지는 협력에 참여하는 전송자와 수신자 양쪽 모두를 포함하는 개념이다.
오퍼레이션: 객체가 다른 객체에게 제공하는 추상적인 서비스다.
메시지가 전송자와 수신자 사이의 협력 관계를 강조 하는 데 비해 오퍼레이션은 메시지를 수신하는 객체의 인터페이스를 강조한다.
다시 말해서 메시지 전송자는 고려하지 않은 채 메시지 수신자의 관점만을 다룬다.
메시지 수신이란 메시지에 대응되는 객체의 오퍼레이션을 호출하는 것을 의미한다.
메서드: 메시지에 응답하기 위해 실행되는 코드 블록을 메서드라고 부른다.
메서드는 오퍼레이션의 구현이다.
동일한 오퍼레이션이라고 해도 메서드는 다를 수 있다.
오퍼레이션과 메서드의 구분은 다형성의 개념과 연결된다.
퍼블릭 인터페이스: 객체가 협력에 참여하기 위해 외부에서 수신할 수 있는 메시지의 묶음.
클래스의 퍼블릭 메서드들 의 집합이나 메시지의 집합을 가리키는 데 사용된다.
객체를 설계할 때 가장 중요한 것은 훌륭한 퍼블릭 인터페이스를 설계하는 것이다.
시그니처: 시그니처는 오퍼레이션이나 메서드의 명세를 나타낸 것으로, 이름과 인자의 목록을 포함한다.
대부분의 언어 는 시그니처의 일부로 반환 타입을 포함하지 않지만 반환 타입을 시그니처의 일부로 포함하는 언어도 존재한다.

2. 인터페이스와 설계 품질

좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족해야 한다.
최소주의를 따르며 추상적인 인터페이스를 설계할 수 있는 가장 좋은 방법은 책임 주도 설계 방법을 따르는 것
메시지를 먼저 선택해 협력과는 무관한 오퍼레이션이 스며드는걸 방지함

2.1. 디미터 법칙

협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙이 바로 디미터 법칙이다.
객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하는 것
낯선 자에게 말하지 말라 or 오직 인접한 이웃하고만 말하라로 요약 가능하다.
이해하기 어렵다면 클래스 내부에 메서드가 아닌 아래 조건을 만족하는 인스턴스에만 메시지를 전송하도록 프로그래밍 해야한다라고 이해해도 무방하다.
this 객체
메서드의 매개변수
this의 속성
this의 속성인 컬렉션의 요소
메서드 내에서 생성된 지역 객체
디미터 법칙을 따르면 부끄럼 타는 코드를 작성할 수 있다.
불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드

2.2. 묻지 말고 시켜라

훌륭한 메시지는 객체의 상태에 관해 묻지 말고 원하는 것을 시켜야한다는 사실을 강조한다.
메시지 전송자는 메시지 수신자의 상태를 기반으로 결정을 내린 후 수신자의 상태를 바꿔서는 안된다.
객체 외부에서 상태가 변경되면 캡슐화 위반이다.
이 원칙을 따르면 밀접하게 연관된 정보와 행동을 함께 가지는 객체를 만들 수 있다.
원칙을 따르다보면 자연스럽게 정보 전문가에게 책임을 할당하고 높은 응집도를 가진 클래스를 얻을 확률이 높아진다.
상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로 인터페이스를 향상시켜라
앞선 두 원칙을 따라다보면 퍼블릭 인터페이스 품질을 향상시키는 좋은 습관을 가질 수 있다.
But, 단순히 객체에게 묻지 않고 시킨다고 모든 문제가 해결되는 것은 아니며 객체가 어떻게 작업을 수행하는지 노출해서는 안된다.
객체가 무엇인지가 아닌 무엇을 해야하는지 서술해야 한다.

2.3. 의도를 드러내는 인터페이스

첫 번째 방법은 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는 것이다.
메서드의 이름을 짓는 두 번째 방법은 '어떻게'가 아니라 무엇을 하는지를 드러내는 것이다.
메서드의 구현이 한 가지인 경우에는 무엇을 하는지를 드러내는 이름을 짓는 것이 어려울 수도 있다.
하지만 무엇을 하는지를 드러내는 이름은 코드를 읽고 이해하기 쉽게 만들뿐만 아니라 유연한 코드를 낳는 지름길이다.

3. 원칙의 함정

디미터 법칙과 묻지 말고 시켜라 스타일은 객체의 퍼블릭 인터페이스를 깔끔하고 유연하게 만들 수 있는 훌륭한 설계원칙이다.
But. 절대적인건 아니고 예외는 넘쳐난다.
원칙이 현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시하자
언제 어떻게 원칙을 쓰는게 유용한지 그렇지 않은지 판단하는 능력을 기르는게 더 중요하다.

3.1. 디미터 법칙은 도트를 강제하는 규칙이 아니다.

디미터 법칙의 흔한 오해는 오직 하나의 도트만을 사용하라는 말로 인해 아래 코드가 법칙을 위반한다고 생각할 것 이다.
IntStream.of(1, 15, 20, 3, 9).filter(x -> x -> 10).distinct().count();
Java
복사
하지만 이는 법칙을 제대로 이해 못한 것으로 of, filter, distinct 메서드는 모두 IntStream이라는 동일 클래스의 인스턴스를 반환한다.
따라서 해당 코드는 법칙을 위반하지 않는다.
법칙은 결합도와 관련된 것으로 결합도가 문제가 되는 것은 객체 내부 구조가 외부로 노출되는 경우로 한정된다.
IntStream의 구조가 외부로 노출된 것이 아니기 때문에 캡슐화는 유지된다.
기차 충돌처럼 보이는 코드라도 객체 내부 구현에 대한 어떤 정보도 외부로 노출하지 않는다면 그것은 디미터 법칙을 준수한 것이다.

3.2. 결합도와 응집도 충돌

일반적으로 어떤 객체의 상태를 물어본 후 반환된 상태를 기반으로 결정을 내리고 그 결정에 따라 객체의 상태를 변경하는 코드는 묻지 말고 시켜라 스타일로 변경해야 한다.
위임 메서드를 통해 객체 내부 구조를 감주는 것은 협력에 참여하는 객체들의 겹합도를 낮출 수 있는 동시에 객체의 응집도를 높일 수 있는 가장 효과적인 방법이다.
But. 법칙을 준수하는게 항상 긍정적 결과로 귀결되는 것은 아니다.
맹목적으로 위임 메서드를 추가할 경우 어울리지 않는 오퍼레이션이 공존하게 된다.
결과적으로 객체가 상관없는 책임을 떠안아 응집도가 낮아진다.
클래스는 하나의 변경 원인만 가져야 한다.
법칙에 너무 맹목적으로 따르면 작은 변경으로도 쉽게 무너질 것이고 애플리케이션은 응집도 낮은 객체로 넘쳐날 것이다.

4. 명령-쿼리 분리 원칙

가끔씩 필요에 따라 물어야 한다는 사실에 납득했다면 명령-쿼리 원칙을 알아두면 도움이 된다.
루틴 = 어떤 절차를 호출하도록 이름을 부여한 기능 모듈
프로시저 = 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
함수 = 함수는 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
명령과 쿼리는 객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또 다른 이름이다.
명령-쿼리 분리 원칙의 요지는 오퍼레이션은 부수효과를 발생시키는 명령 or 발생시키지 않는 쿼리 중 하나여야 한다는 것이다.
어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안된다.
객체의 상태를 변경하는 명령은 반환값을 가질 수 없다.
객체의 정보를 반환하는 쿼리는 상태를 변경할 수 없다.
한 문장으로 요약하면 “질문이 답변을 수정해서는 안 된다”는 것이다.
해당 원칙에 따라 작성된 객체의 인터페이스를 명령-쿼리 인터페이스라고 부른다.
명령을 누르면 기계 상태가 변경되고, 쿼리 버튼을 이용해 상태를 확인할 수 있다.

4.1. 명령-쿼리 분리와 참조 투명성

지금까지 살펴본 것처럼 명령과 귀리를 엄격하게 분류하면 객체의 부수효괴를 제어하기가 수월해진다.
쿼리는 객체의 상태를 변경하지 않기 때문에 몇 번이고 반복적으로 호출하더라도 상관이 없다.
명령이 개입하지 않는 한 쿼리의 값은 변경되지 않기 때문에 쿼리의 결과를 예측하기 쉬워진다.
자연은 주기적으로 반복되는 다양한 현상으로 이뤄져 있다.
계절은 1년 주기로 순환하고, 월급날은 한 달 주기로 돌아오며, 월요병은 일주일 단위로 반복된다.
명령과 쿼리를 분리함으로써 명령형 언어의 들 안에서 참조 투명성의 이점을 제한적이나마 누릴 수 있게 된다.
참조 투명성이라는 특성을 잘 활용하면 버그가 적고, 디버깅이 용이하며, 쿼리의 순서에 따라 실행 결과가 변하지 않는 코드를 작성할 수 있다.
수학과 컴퓨터 세계를 나누는 큰 특징으로 부수효과의 존재 유무를 꼽을 수 있다.
함수는 내부에 부수효과를 포함할 경우 동일 인자더라도 부수효과에 의해 결괏값이 매번 달라질 수 있다.
참조 투명성이란 “어떤 표현식 e가 있을 때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성”을 의미한다.
수식
f(n)의 수를 변환 시켜도 결과는 달라지지 않는다.
이것이 바로 참조 투명성이다.
f(1)의 값이 변하지 않기에 항상 값을 3이라 말할 수 있다.
이 처럼 어떤 값이 변하지 않는 성질을 불변성(immutability)이라 부른다.
참조 투명성을 만족하는 식은 우리에게 두 가지 장점을 제공한다.
모든 함수를 이미 알고 있는 하나의 결괏값으로 대체할 수 있기 때문에 식을 쉽게 계산할 수 있다.
모든 곳에서 함수의 결괏값이 동일하기 때문에 식의 순서를 변경하더라도 각 식의 결과는 달라지지 않는다.
객체지향은 객체의 상태 변경이라는 부수효과를 기반으로 하기에 견고하다 생각했던 바닥에 심각한 균열이 생기기 시작한다.
명령-쿼리 분리 원칙을 사용하면 이 균열을 조금이나마 줄일 수 있다.

4.2. 책임에 초점을 맞춰라

앞서 설명한 모든 원칙의 중심에는 객체가 수행할 책임에 위치한다.
그러니까 네 가지 원칙을 잘 버무려서 책임 중심의 훌륭한 메세지와 퍼블릭 인터페이스를 설계하자~
계약에 의한 설계 = 클라이언트와 서버가 준수해야하는 제약을 코드 상에 명시적으로 표현하고 강제할 수 있는 방법