6. 메시지와 인터페이스
00. 선요약
- SW 설계에서 절대적인 것은 없다. 모든 결정은 경우에 따라 이루어져야 한다.
01. 협력과 메시지
클라이언트-서버 모델
클라이언트-서버 모델은 두 객체 사이의 협력관계를 설명하기 위해 사용되는 전통적인 메타포 이다. 메시지를 전송하는 객체가 클라이언트 역할, 메시지를 수신하는 객체가 서버 역할을 한다. 그리고 협력 안에서 객체는 클라이언트 역할과 서버 역할을 동시에 수행한다.
메시지와 메시지 전송
메시지는 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단이다. 한 객체가 다른 객체에게 도움을 요청하는 것을 메시지 전송(message sending) 또는 메시지 패싱(message passing)이라 부른다.
메시지는 오퍼레이션명과 인자로 구성되며 메시지 전송은 여기에 메시지 전송자를 추가한 것이다. 자바를 예로 들면 오퍼레이션명과 인자를 조합한 isSatisfiedBy(screening)이 메시지 이고 여기에 수신자인 condition을 추가한 condition.isSatisfiedBy(screening)이 '메시지 전송' 이다.
메시지와 메서드
메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드 라고 부르는데, 메시지를 수신했을 때 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제타입이 뭔가에 달려있다. 이 말은 실행 시간에 메시지와 메서드가 바인딩 된다는 말이고 실행 시간에 메서드가 바인딩 되는 메커니즘은 지금까지 계속 반복해 왔던 것처럼 두 객체 사이의 결합도를 낮춤으로써 유연하고 확장 가능한 코드를 작성할 수 있게 만든다.
퍼블릭 인터페이스와 오퍼레이션
(캡슐화가 잘 된) 객체의 안쪽은 검은 장막으로 가려진 미지의 영역이다. 외부에서 알 수 있는 것은 오직 객체가 공개해 놓은 메시지 이다. 이렇게 객체가 의사소통을 위해 외부에 공개해놓은 메시지의 집합을 퍼블릭 인터페이스라 한다.
프로그래밍 언어의 관점에서 퍼블릭인터페이스에 포함된 메시지를 오퍼레이션이라 부른다. 오퍼레이션은 수행 가능한 어떤 행동에 대한 추상화 이다. 그리고 메서드는 메시지를 수신했을 때 실행되는 코드이다.
프로그래밍 언어의 관점에서 객체가 다른 객체에게 메시지를 전송하면 런타임 시스템은 메시지 전송을 오퍼레이션 호출로 해석하고 메시지를 수신한 객체의 실제 타입을 기반으로 적절한 메서드를 찾아 실행한다. 따라서 퍼블릭 인터페이스와 메시지의 관점에서 '메서드 호출' 보다 '오퍼레이션 호출'이라는 용어를 사용하는 것이 더 적합하다.
오퍼레이션 관점에서 다형성이란 동일한 오퍼레이션 호출에 대해 다른 메서드들이 실행되는 것이라고 정의할 수 있다.
시그니처
오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합쳐 시그니처라고 부른다. 오퍼레이션은 실행 코드 없이 시그니처만을 정의한 것이다.
02. 인터페이스와 설계품질
좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족해야 한다. 최소한의 인터페이스는 꼭 필요한 오퍼레이션만 인터페이스에 추가하고 추상적인 인터페이스는 어떻게 수행하는지가 아니라 무엇을 하는지를 표현한다.
좋은 설계를 위해 이 두가지 조건을 만족시키기 위한 가장 좋은 방법인 책임 주도 설계방법을 사용하면 되지만 훌륭한 인터페이스가 가지는 공통적인 특징을 알아보는 것도 우리의 안목을 넓히고 올바른 설계에 도달할 수 있는 지름길을 제공한다. 원칙은 아래와 같다.
- 디미터법칙
- 묻지 말고 시켜라
- 의도를 드러내는 인터페이스
- 명령-쿼리 분리
디미터법칙
디미터 법칙은 객체의 내부구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것이다. 이는 "낯선 자에게 말하지 말라" 또는 "오직 인접한 이웃하고만 말하라" 또는 "오직 하나의 도트만 사용하라" 라는 말로 요약할 수 있다.
디미터법칙 아래서 메시지를 보낼 수 있는 객체는 아래와 같이 요약된다.
- this 객체
- 메서드의 매개변수
- this의 속성
- this의 속성인 컬렉션 요소
- 매서드 내에서 생성된 지역객체
디미터법칙을 지킴으로서 우리는 필요한 어떤것도 다른 객체에 보여주지 않고 다른 객체의 구현에 의존하지 않는 부끄럼 타는 코드를 작성할 수 있다.
묻지 말고 시켜라
디미터법칙은 훌륭한 메시지는 객체의 상태에 관해 묻지 않고 원하는 것을 시켜야 한다는 사실을 강조한다. 메시지 전송자는 메시지 수신자의 상태를 기반으로 결정 내린 후 메시지 수신자의 상태를 바꿔서는 안된다. 이 로직은 메시지 수신자가 담당해야 할 책임이다! 해당 객체가 담당해야 할 책임이 외부로 누수된 것이다.
의도를 드러내는 인터페이스
인터페이스는 객체가 어떻게 하는 지가 아니라 무엇을 하는지를 서술해야 한다. 무엇을 하는지 잘 서술하기 위해 메서드를 명명하는 방법이 두 가지가 있는데 하나는 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름을 짓는 것이고 다른 하나는 무엇을 하는지 나타나도록 이름을 짓는 것이다.
메서드가 어떻게 수행하느냐가 아니라 무엇을 하느냐에 초첨을 맞추면 클라이언트 입장에서 동일한 작업을 수행하는 메서드들을 하나의 타입 계층으로 묶을 수 있는 가능성이 커지고 그 결과 다양한 타입이 객체가 협력할 수 있는 유연한 협력을 얻게 된다.
이런 방식을 의도를 드러낸 선택자 패턴이라 부른다.
03. 원칙의 함정
이세상엔 100% 통용되는 법칙이 없듯(있나..?) 설계 원칙 역시 100% 지켜야 하는 것이 아니다.(100% 지킬 수 없다가 맞는 표현일수도..) 여러 대안들 중 한 가지를 선택해야 하며 다른 것들을 포기해야 한다. 앞서 알아본 원칙들도 마찬가지 이다. 원칙이 현재 상황에 부적합하다고 생각되면 과감하게 원칙을 무시하라. 원칙을 아는것보다 중요한 것은 언제 원칙이 유용하고 언제 유용하지 않은지 판단할 수 있는 능력으 기르는 것이다.
디미터법칙은 하나의 도트(.)를 강제하는 규칙이 아니다.
예를 들어 아래와 같은 코드는 디미터 법칙을 위반하는 것이 아니다.
IntStream.of(1, 15, 20, 3, 9).filter(x -> x > 10).distinct().count();
도트가 여러 번 쓰여서 디미터법칙을 위반하는 것이 아닌가 싶을 수도 있지만 위 메서드에서 of, filter, distinct메서드는 모두 동일한 IntStream 메서드를 반환한다. 따라서 이 코드는 디미터법칙을 위반하는 것이 아니다.
디미터법칙은 결합도와 관련된 것이며 이 결합도가 문제가 되는 것은 객체의 내부 구조가 외부로 노출되는 경우로 한정된다.
결합도와 응집도의 충돌
좋은 설계를 위해 '묻지 말고 시켜라'와 '디미터 법칙'을 준수하는 것이 항상 긍정적인 결과로만 귀결되지는 않는다. 모든 상황에서 맹목적으로 위임메서드를 추가하면 같은 퍼블릭 메서드 안에 어울리지 않는 메서드들이 공존하게 된다. 결과적으로 객체는 상관 없는 책임들을 떠안게 되기 때문에 응집도가 떨어지게 된다. 클래스는 하나의 변경요인만 가져야 하는데 응집도가 낮은 상태(서로 상관없는 책임들이 모여 있는 경우)에서는 작은 변경으로도 쉽게 무너질 수 있다.
객체에게 시키는 것이 항상 가능한 것이 아니다. 가끔은 물어야 한다.(객체가 데이터인 경우) 한 객체의 입장만 따지지 말고 여러 객체를 함께 보며 캡슐화, 응집도, 결합도를 고려하여 전체적인 관점에서 좋은 방향으로 설계를 해야 한다.
04. 명령-쿼리 분리 원칙
명령-쿼리 분리 원칙은 퍼블릭 인터페이스에서 오퍼레이션을 정의할 때 참고할 수 있는 지침을 제공한다. 우선 이해를 돕기 위해 몇가지 용어들을 먼저 살펴보자.
용어 | 설명 |
---|---|
루틴 | 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능모듈을 루틴이라 부른다. 루틴은 다시 프로시저와 함수로 구분할 수 있다. |
프로시저 명령 | 정해진 절차에 따라 내부의 상태를 변경하는 루틴. 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다. |
함수 쿼리 | 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다. |
어떤 오퍼레이션도 명령인 동시에 쿼리 여서는 안된다. 명령-쿼리 분리 원칙을 한 문장으로 요약하면 "질문이 답변을 수정해서는 안 된다"는 것이다.
명령과 쿼리를 분리하면 로직이 더 길어질 수도 있다. 하지만 그렇게 함으로서 코드는 예측 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해질 것이다.
명령-쿼리 분리와 참조 투명성
참조투명성이란 "어떤 표현식 e가 있을 때 e의 값으로 e가 나타내는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성"을 말한다. 부수효과를 발생시키지 않는다는 말이다. 수학은 참조투명성을 엄격하게 준수하는 가장 유명한 체계이다. 참조투명성이라는 특성을 잘 이용하면 버그가 적고, 디버깅이 용이하며, 쿼리 순서에 따라 실행 결과가 변하지 않는 코드를 작성할 수 있다.
우리가 공부하고 있는 객체지향 패러다임은 객체의 상태변화라는 부수효과를 기반으로 하기 때문에 참조투명성은 기대하기 힘들지만 명령-쿼리 분리를 통해 참조 투명성의 장점을 조금이나마 누릴 수 있다.
책임에 초점을 맞춰라
책임 주도 설계방법을 따르면 우리의 설계가 아름답고 깔끔해지며 심지어 우아해질 수 있다!
메시지를 선택하고 그 후에 메시지를 처리 할 객체를 선택함으로서 디미터법칙을 준수하고 묻지말고 시켜라 스타일을 따르며 의도를 드러내는 설계를 할 수 있다. 객체의 구현 이전에 객체 사이의 협력에 초점을 맞추고 협력 방식을 단순하고 유연하게 만듦으로서 명령과 쿼리를 분리하고 계약에 의한 설계(부록 A) 개념을 통해 객체의 협력 방식을 명시적으로 드러낼 수 있다. 이 모든 방식의 중심에는 객체가 수행해야 할 책임이 있다.
← 5. 책임 할당하기 7. 객체 분해 →