책 「오브젝트」 6장 메시지와 인터페이스를 읽고 작성한 내용입니다.

클래스라는 구현 도구에 지나치게 집착하면 경직되고 유연하지 못한 설계에 이를 확률이 높아진다.
훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아니라 객체를 지향해야 한다. 좀 더 정확하게 말해서 협력 안에서 객체가 수행하는 책임에 초점을 맞춰야 한다. 여기서 중요한 것은 책임이 객체가 수신할 수 있는 메시지의 기반이 된다는 것이다.
애플리케이션은 클래스로 구성되지만 메시지를 통해 정의된다는 사실을 기억하라[Metz12].
협력과 메시지
클라이언트-서버 모델
협력은 어떤 객체가 다른 객체에게 무언가를 요청할 때 시작된다[Wirfs-Brock03].
협력 안에서 메시지를 전송하는 객체를 클라이언트, 메시지를 수신하는 객체를 서버라고 부른다. 협력은 클라이언트가 서버의 서비스를 요청하는 단방향 상호작용이다.
메시지를 매개로 하는 요청과 응답의 조합이 두 객체 사이의 협력을 구성한다.
객체는 협력에 참여하는 동안 클라이언트와 서버의 역할을 동시에 수행하는 것이 일반적이다.
- 객체가 수신하는 메시지의 집합
- 외부의 객체에게 전송하는 메시지의 집합
혼란스러운 용어 정리
메시지(message) : 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단
- 오퍼레이션명(operation name)과 인자(argument)로 구성
- 메시지 전송은 여기에 메시지 수신자를 추가한 것
- 예시 : condition.isSatisfiedBy(screening);
메시지 전송(sending) / 메시지 패싱(passing) : 한 객체가 다른 객체에게 도움을 요청하는 것
메시지 전송자(sender): 메시지를 전송하는 객체
메시지 수신자(receiver): 메시지를 수신하는 객체
클라이언트-서버 모델 관점에서는 메시지 전송자는 클라이언트, 메시지 수신자는 서버라고 부르기도 한다.
오퍼레이션: 객체가 다른 객체에게 제공하는 추상적인 서비스
메서드: 메시지에 응답하기 위해 실행되는 코드 블록
메시지와 메서드
이처럼 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드라고 부른다. 중요한 것은 코드 상에서 동일한 이름의 변수(condition)에게 동일한 메시지를 전송하더라도 객체의 타입에 따라 실행되는 메서드가 달라질 수 있다는 것이다.
반면 객체는 메시지와 메서드라는 두 가지 서로 다른 개념을 실행 시점에 연결해야 하기 때문에 컴파일 시점과 실행 시점의 의미가 달라질 수 있다.
메시지를 수신했을 때 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제 타입이 무엇인가에 달려있다.
실행 시점에 메시지와 메서드를 바인딩하기 때문이다.
이러한 메커니즘은 두 객체 사이의 결합도를 낮춤으로써 유연하고 확장 가능한 코드를 작성할 수 있게 만든다.
메시지와 메서드의 구분은 메시지 전송자와 메시지 수신자가 느슨하게 결합될 수 있게 한다.
- 메시지 전송자는 자신이 어떤 메시지를 전송해야 하는지만 알면 된다.
- 메시지 수신자 역시 누가 메시지를 전송하는지 알 필요가 없다.
퍼블릭 인터페이스와 오퍼레이션, 시그니처
객체는 안과 밖을 구분하는 뚜렷한 경계를 가진다.
이처럼 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 퍼블릭 인터페이스라고 부른다.
프로그래밍 언어의 관점에서 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션(operation)이라고 부른다. 오퍼레이션은 수행 가능한 어떤 행동에 대한 추상화다.
흔히 오퍼레이션이라고 부를 때는 내부의 구현 코드는 제외하고 단순히 메시지와 관련된 시그니처를 가리키는 경우가 대부분이다.
그에 비해 메시지를 수신했을 때 실제로 실행되는 코드는 메서드라고 부른다.
따라서 퍼블릭 인터페이스와 메시지의 관점에서 보면 ‘메서드 호출’보다는 ‘오퍼레이션 호출’이라는 용어를 사용하는 것이 더 적절하다.
- 인터페이스의 각 요소는 오퍼레이션
- 메서드는 오퍼레이션에 대한 구현
C++에서 코드를 작성할 때를 생각해 보았을 때 .hpp에 작성하는 부분이 오퍼레이션, .cpp에 작성하는 부분이 메서드라고 나눌 수 있을 것 같다. 물론 이 이유만을 위해 작성하지는 않겠지만, .hpp 헤더 파일을 작성하는 이유 중 하나가 경계를 나눌 수 있는 점이 아닐까 생각해 볼 수 있었다.
오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합쳐 시그니처(signature)라고 부른다.
오퍼레이션 관점에서 다형성이란 동일한 오퍼레이션 호출에 대해 서로 다른 메서드들이 실행되는 것이라고 정의할 수 있다.
- 오퍼레이션은 실행 코드 없이 시그니처만을 정의한 것이다.
- 메서드는 이 시그니처에 구현을 더한 것이다.
- 일반적으로 메시지를 수신하면 오퍼레이션의 시그니처와 동일한 메서드가 실행된다.
인터페이스와 설계 품질
좋은 인터페이스
- 최소한의 인터페이스 : 꼭 필요한 오퍼레이션만을 인터페이스에 포함
- 추상적인 인터페이스 : 어떻게 수행하는지가 아니라 무엇을 하는지를 표현
책임 주도 설계 방법이 훌륭한 인터페이스를 얻을 수 있는 지침을 제공한다.
퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법
- 디미터 법칙
- 묻지 말고 시켜라
- 의도를 드러내는 인터페이스
- 명령-쿼리 분리
디미터 법칙
이처럼 협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙이 바로 디미터 법칙(Law of Demeter)이다.
내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것
“낯선 자에게 말하지 말라(don’t talk to strangers)[Larman04]”
“오직 인접한 이웃하고만 말하라(only talk to your immediate neighbors)[Metz12]”
“오직 하나의 도트만 사용하라(use only one dot)[Metz12]”
클래스가 특정한 조건을 만족하는 대상에게만 메시지를 전송하도록 프로그래밍
모든 클래스 C와 C에 구현된 모든 메서드 M에 대해서, M이 메시지를 전송할 수 있는 모든 객체는 다음에 서술된 클래스의 인스턴스여야 한다. 이때 M에 의해 생성된 객체나 M이 호출하는 메서드에 의해 생성된 객체, 전역 변수로 선언된 객체는 모두 M의 인자로 간주한다.
- M의 인자로 전달된 클래스 (C 자체를 포함)
- C의 인스턴스 변수의 클래스
디미터 법칙을 따르면 부끄럼 타는 코드(shy code)를 작성할 수 있다[Hunt99]. 부끄럼타는 코드란 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드를 말한다.
디미터 법칙을 따르는 코드는 메시지 수신자의 내부 구조가 전송자에게 노출되지 않으며, 메시지 전송자는 수신자의 내부 구현에 결합되지 않는다. 따라서 클라이언트와 서버 사이의 낮은 결합도를 유지할 수 있다.
하지만 무비판적으로 디미터 법칙을 수용하면 퍼블릭 인터페이스 관점에서 객체의 응집도가 낮아질 수도 있다.
묻지 말고 시켜라
디미터 법칙은 훌륭한 메시지는 객체의 상태에 관해 묻지 않고 원하는 것을 시켜야 한다는 사실을 강조한다.
절차적인 코드는 정보를 얻은 후에 결정한다. 객체지향 코드는 객체에게 그것을 하도록 시킨다[Sharp00].
묻지 말고 시켜라 원칙을 따르면 밀접하게 연관된 정보와 행동을 함께 가지는 객체를 만들 수 있다.
상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라. 협력을 설계하고 객체가 수신할 메시지를 결정하는 매 순간 묻지 말고 시켜라 원칙과 더미터 법칙을 머릿속에 떠올리는 것은 퍼블릭 인터페이스의 품질을 향상시킬 수 있는 좋은 습관이다.
메서드를 명명하는 두 가지 방법
의도를 드러내는 인터페이스를 만들자!
- 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는 것
- 책임을 수행하는 방법을 드러내는 메서드를 사용한 설계는 변경에 취약할 수밖에 없다.
- ‘어떻게’가 아니라 ‘무엇’을 하는지를 드러내는 것
- 무엇을 하는지 드러내는 이름은 코드를 읽고 이해하기 쉽게 만들 뿐만 아니라 유연한 코드를 낳는 지름길이다.
- 객체가 협력 안에서 수행해야 하는 책임에 관해 고민해야 한다.
- 외부의 객체가 메시지를 전송하는 목적을 먼저 생각하도록 만들며, 결과적으로 협력하는 클라이언트의 의도에 부합하도록 메서드의 이름을 짓게 된다.
방정식을 푸는 방법을 제시하지 말고 이를 공식으로 표현하라. 문제를 내라. 하지만 문제를 푸는 방법을 표현해서는 안 된다[Evans03].
객체에게 묻지 말고 시키되 구현 방법이 아닌 클라이언트의 의도를 드러내야 한다. 이것이 이해하기 쉽고 유연한 동시에 협력적인 객체를 만드는 가장 기본적인 요구사항이다.
원칙의 함정
초보자는 원칙을 맹목적으로 추종한다. 심지어 적용하려는 원칙들이 충돌하는 경우에도 원칙에 정당성을 부여하고 억지로 끼워 맞추려고 노력한다.
원칙이 현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시하라.
위의 2개가 절대적인 법칙이 아니다. 설계는 트레이드오프의 산물이다.
안타깝게도 묻지 말고 시켜라와 디미터 법칙을 준수하는 것이 항상 긍정적인 결과로만 귀결되는 것은 아니다. 모든 상황에서 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션들이 공존하게 된다. 결과적으로는 객체는 상관없는 책임들을 한꺼번에 떠안게 되기 때문에 결과적으로 응집도가 낮아진다.
객체에게 시키는 것이 항상 가능한 것은 아니다. 가끔씩은 물어야 한다.
소프트웨어 설계에 법칙이란 존재하지 않는다는 것이다. 원칙을 맹신하지 마라. 원칙이 적절한 상황과 부적절한 상황을 판단할 수 있는 안목을 길러라. 설계는 트레이드오프의 산물이다. 소프트웨어 설계에 존재하는 몇 안되는 법칙 중 하나는 “경우에 따라 다르다”라는 사실을 명심하라
이전에 책에서 결합도가 낮고 응집도가 높으면 좋은 설계라고 말했다. 그래서 결합도와 응집도가 항상 같이 엮여있다고 생각했던 것 같다.
디미터 법칙을 따르면 낮은 결합도를 가지게 되지만, 응집도도 낮아질 수 있다.
책에서 소개하는 설계 방향은 무조건 다 정답이지 않을까도 생각했는데, 그렇지 않다는 것을 느꼈다.
항상 트레이드오프를 생각하며 무엇이 더 중요한지를 판단하고 이에 따라 결정해야겠다.
명령-쿼리 분리 원칙
- Command-Query Separation 원칙
- 퍼블릭 인터페이스에 오퍼레이션을 정의할 때 참고할 수 있는 지침을 제공
용어 정리
루틴(routine): 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능
- 프로시저와 함수로 구분 가능 → 부수효과와 반환값의 유무라는 측면에서 명확하게 구분됨
- 프로시저(procedure) : 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류
- 프로시저는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
- 함수(function) : 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류
- 함수는 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
명령(Command)과 쿼리(Query)는 객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또 다른 이름
- 명령: 객체의 상태를 수정하는 오퍼레이션
- 쿼리: 객체와 관련된 정보를 반환하는 오퍼레이션
개념적으로 명령 == 프로시저, 쿼리 == 함수
명령-쿼리 분리 원칙의 요지는 오퍼레이션은 부수효과를 발생시키는 명령이거나 부수효과를 발생시키지 않는 쿼리 중 하나여야 한다는 것이다.
명령-쿼리 분리와 참조 투명성
명령과 쿼리를 뒤섞으면 실행 결과를 예측하기가 어려워질 수 있다.
퍼블릭 인터페이스를 설계할 때 부수효과를 가지는 대신 값을 반환하지 않는 명령과, 부수효과를 가지지 않는 대신 값을 반환하는 쿼리를 분리하기를 바란다. 그 결과, 코드는 예측 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해질 것이다.
명령과 쿼리를 분리함으로써 명령형 언어의 틀 안에서 참조 투명성(referential transparency)의 장점을 제한적이나마 누릴 수 있게 된다.
참조 투명성
- 어떤 표현식 e가 있을 때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성
- 식의 순서를 변경하더라도 결과가 달라지지 않는다는 것
참조 투명성의 장점
- 모든 함수를 이미 알고 있는 하나의 결괏값으로 대체할 수 있기 때문에 식을 쉽게 계산할 수 있다.
- 모든 곳에서 함수의 결괏값이 동일하기 때문에 식의 순서를 변경하더라도 각 식의 결과는 달라지지 않는다.
이 부분을 읽으면서 C++로 구현할 때 함수명 끝에 const를 붙이는 것이 생각났다. C++에서는 함수 뒤에 const를 붙이면 해당 함수가 객체의 멤버 변수를 변경하지 않음을 나타내며, 함수를 호출할 때 객체를 const로 전달할 수 있다. 이렇게 하면 코드를 읽을 때 어떤 함수가 객체의 상태를 변경하는지 명확하게 파악할 수 있어서 유용했다. 코드를 작성할 때 이런 부분도 디버깅이 용이한 장점이 될 수 있다는 것을 한번 더 상기할 수 있었다.
'study > CS' 카테고리의 다른 글
[CS/책] 「오브젝트」5장 - 책임 할당하기 (0) | 2024.03.26 |
---|---|
[CS/책] 「오브젝트」4장 - 설계 품질과 트레이드오프 (0) | 2024.03.21 |
[CS/책] 「오브젝트」3장 - 역할, 책임, 협력 (0) | 2024.03.10 |
[CS/책] 「오브젝트」2장 - 객체지향 프로그래밍 (0) | 2024.03.03 |
[CS/책] 「오브젝트」1장 - 객체, 설계 (0) | 2024.02.25 |