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