[오브젝트] chapter 06. 메시지와 인터페이스

6 minute read

들어가며

우리가 사용하는 언어에 존재하는 객체 지향 언어에서 클래스는 단순히 도구이다. 이 클래스가 중요한 것은 아니지만 클래스 중심으로 설계를 하고 개발하는 경우에 유연한 설계를 하기가 어렵다.

유연한 설계를 하기 위해서 협력 안에서 객체가 수행하는 책임에 초점을 맞춰야 한다, 중요한 것은 책임이 객체가 수행할 수 있는 메시지의 기반이 된다는 것이다.

이번 장에서는 객체 끼리 진행되는 의사소통 메커니즘인 메시지에 대해서 그리고 그것과 관련된 퍼블릭 인터페이스를 만드는데 도움이 되는 설계 원칙과 기법을 익혀보도록 하자.

1. 협력과 메시지

클라이언트-서버 모델

두 객체 사이의 협력 관계를 설명하기 위해 사용하는 전통적인 메타포.

협력 안에서 메시지를 전송하는 객체를 클라이언트, 메시지를 수신하는 객체를 서버.

협력은 클라이언트가 서버의 서비스를 요청하는 단방향 상호작용이다.

협력의 관점에서 객체는 두 가지 종류의 메시지 집합으로 구성된다. 하나는 객체가 수신하는 메시지의 집합, 하나는 외부의 객체에게 전송하는 메시지의 집합

단순히 특정 객체는 메시지를 전송만 하고 수신만 한다고 생각할 수 있는데 그렇지 않으며 많은 사람들이 수신하는 메시지의 집합에만 초점을 맞추지만 협력에 적합한 객체를 설계하기 위해서 외부에 전송하는 메시지의 집합도 함께 고려해야 한다.

메시지와 메시지 전송

  • 메시지: 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단
    • 메시지 전송: 한 객체가 다른 객체에게 도움을 요청하는 것
    • 메시지 전송자: 해당 메시지를 전송한 객체
    • 메시지 수신자: 해당 메시지를 수신한 객체
condition.isSatisfiedBy(screening)
[수신자].[오퍼레이션명]([인자])
  • 오퍼레이션명과 인자: isSatisfiedBy(screening)
  • 메시지 전송: condition.isSatisfiedBy(screening)

메시지와 메서드

메서드: 메시지를 수신할 때 실제로 실행되는 함수 또는 프로시저

메서드가 중요한 것은 동일한 이름의 변수에게 동일한 메시지를 전송하더라도 객체의 타입에 따라 실행되는 메서드가 달라질 수 있다는 것.

전통적인 경우에는 컴파일때 어떤 코드가 실행될지 알고 있다. 하지만 객체 지향에서는 메시지와 메서드라는 두 가지 서로 다른 개념을 실행 시점에 연결해야 하기 때문에 컴파일 시점과 실행 시점의 의미가 달라질 수 있다.

메시지와 메서드와의 분리

메시지 전송자는 자신이 어떤 메시지를 전송해야 하는지만 알면 되다. 수신자는 어떤 클래스의 인스턴스인지, 어떤 방식으로 요청을 처리하는지 모르더라도 월활한 협력이 가능하다.

실행 시점에 메시지와 메서드를 바인딩하는 메커니즘은 두 객체 사이의 결합도를 낮춤으로써 유연하고 확장 가능한 코드를 작성할 수 있게 만든다.

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

  • 퍼블릭 인터페이스: 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합
  • 오퍼레이션: 퍼블릭 인터페이스에 포함된 메시지. 수행 가능한 어떤 행동에 대한 추상화 이기 때문에 정의만 되어져 있는 경우가 대부분이다.

실제로 메시지를 수신했을 때 실제로 실행되는 코드는 메서드이다.

객체가 다른 객체에게 메시지를 전송하면 런타임 시스템은 메시지 전송을 오퍼레이션 호출로 해석하고 메시지를 수신한 객체의 실제 타입을 기반으로 적잘한 메서드를 찾아 실행.

client -------> server (operation) -> method
      메시지 전송
  1. client는 메시지 전송 -> 퍼블릭한 operation을 호출한다.
  2. 해당 operation을 가지고 있는 객체가 내부 구현 메소드를 실행

여기서 어떤 객체가 실행될지 런타임에서만 명확하게 알 수 있다.

시그니처

  • 시그니처: 오퍼레이션(또는 메소드)의 이름과 파라미터 목록을 합친 것

일반적으로 메시지를 수신하면 오퍼레이션의 시그니처와 동일한 메서드가 실행

하나의 오퍼레이션에 오직 하나의 메서드만 존재한다면 매우 단순하고 사실 오퍼레이션을 정의해서 사용할 필요가 없음.

객체가 수신할 수 있는 메시지가 객체의 퍼블릭 인터페이스와 그 안에 포함될 오퍼레이션을 결정한다는 것. ???

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

좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족해야 한다. 기본적으로 그러기 위해서는 어떻게 수행하는지를 표현하기 보다 무엇을 하는지를 표현한다.

퍼블릭 인터페잇으의 품질에 영향을 미치는 조건에 대해서 배워보자.

디미터 법칙 (Law of Demeter)

객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라.

  • 낯선 자에게 말하지 말라
  • 오직 인접한 이웃하고만 말하라

오직 하나의 도트만 사용하라.

사용할 수 있는 친구들

  • this 객체
  • 메서드의 매개변수
  • this의 속성
  • this의 속성인 컬렉션의 요소
  • 메서드 내에서 생성된 지역 객체

디미터 법칙을 따르게 되면 부끄럼타는 코드를 작성할 수 있다. 이것을 통해서 다른 객체를 구체적으로 접근하지도 않고 내부 구현을 알지 못하더라도 사용할 수 있다.

묻지 말고 시켜라

우리가 원하는 것을 얻기 위해서 객체의 상태에 관해 묻지 말고 원하는 것을 시켜라 라는 원칙이다.

메시지 전송자는 메시지 수신자의 상태를 기반으로 결정을 내린 후 메시지 수신자의 상태를 바꿔서는 안된다.

해당 원칙을 따르면 객체의 정보를 이용하는 행동을 객체의 외부가 아닌 내부에 위치시키기 때문에 자연스럽게 정보와 행동을 동일한 클래스 안에 두게 된다.

자연스럽게 상태를 묻는 코드를 없애고 행동을 요청하는 오퍼레이션으로 대체한다면 자연스럽게 객체 밖에서 사용되었던 상태들과 로작들이 객체 내부로 들어가서 응집도가 높아질 확률이 높다.

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

메소드 이름을 만드는데는 두가지 방법이 있다고 한다.

  1. 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓기
    • 동일한 행동을 하는 메서드들에 대해서 알아차리기가 어렵다. (이름이 다르기 때문에) ex. isExistByLine, isExistByKakao
    • 메서드 수준에서 캡슐화를 위반. 협력하는 객체의 종류를 알도록 강요. 여기서 어떤 특정 메서드를 변경했을 경우 연쇄적인 코드 변경이 일어나는 구조가 될 수 있다. 책임을 수행하는 방법을 드러내는 경우 변경에 취약할 수 있다. (책에 예제 코드 참조)
  2. 무엇을 하는지에 대해서 메서드 이름 짓기

어떻게 수행하는지를 드러내는 이름은 협력을 설계하기 시작하는 이른 시기부터 클래스의 내부 구현에 관해서 고민하게 된다. why? 당연히 내부 구현에 대한 대표적인 이름을 만들어야되기 때문에

하지만 책임에 관해서 그리고 메시지에 관해서 집중하여서 한다면 클라이언트의 의도에 부합하도록 메소드 이름을 지을 수 있다. 클라이언트 입장이기 때문에 큰 요구사항이 변경되는 경우가 아닌 경우 인터페이스는 변경되지 않고 내부 로직만 변경되지 않을까나?

그 뿐만 아니라 무엇을 하느냐에 초점을 맞추면 클라이언트의 관점에서 동일한 작ㅇㅂ을 수행하는 메서드들을하나의 타입 계층으로 묶을 수도 있다. 그것을 통해서 유연한 설계가 가능하다.

3. 원칙의 함정

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

해당 법칙은 내부 구현에 대한 정보를 외부 노출하지 않기 위해서 지켜야되는 원칙이다.

예제에서 나오는 stream처럼 내부 구현과 별개로 동일한 인스턴스로 진행하는 경우 객체의 내부 구현에 대해서 보여주는 경우가 아니기에 괜찮다.

결합도와 응집도의 충돌

모든 상황에서 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스 안에 어울리지 않은 오퍼레이션들이 공존하게 된다.

예제가 이해가 안되는구만…

예외적으로 디미터 법칙의 위한 여부는 객체인지 자료구조인지에 따라 다르다. 객체는 내부를 숨겨야되지만 자료 구조는 노출되는게 당연한 것이기 때문에 적용하지 않는다.

예외가 존재하고 어떨때는 객체 상태에 대해서 물어봐야되기도 한다. 설계에 대해서 항상 정답이 없고 트페이드 오프의 산물이다. 항상 경우에 따라 다르다 라는 것을 명심하라.

4. 명령-쿼리 분리 법칙

  • 명령(프로시저): 객체의 상태를 수정하는 오퍼레이션
  • 쿼리(함수): 객체와 관련돈 정보를 반환하는 오퍼레이션

명령-쿼리 분리 원칙에 따라 작성된 객체의 인터페이스를 명령-쿼리 인터페이스 라고 한다.

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

쿼리는 객체의 상태를 변경하지 않기 때문에 몇 번이고 반복적으로 호출하더라고 상관이 없다.

명령이 개입하지 않는 한 쿼리의 값은 변경되지 않기 때문에 쿼리의 결과를 예측하기 쉬워진다.

그뿐만 아니라 순서와 횟수 상관없이 호출할 수 있다.

컴퓨터의 세계와 수학의 세계를 나누는 특징으로 부수효과의 존재 유무가 있다. 프로그래밍에서 대입문과 프로시저라고 불리는 함수로 인해서 부수효과가 발생하고 결과값이 매번 달라질 수 있다.

참조 투명성: 어떤 표현식 e가 있을 대 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성

f(1) = 3 이라고 할때,

f(1) + f(1) = 6
f(1) - 2 = 2

f(1)에다가 해당 값인 3을 넣더라도 아무 문제가 없이 명확한 경우 참조 투명성을 만족한다고 한다. 수학에서는 매우 명확하게 해당 성질을 가지고 있다.

여기서 f(1)은 어떠한 경우에도 바뀌지 않는다. 이런 성질을 불변성이라고 하며 이 성질을 가지고 있다면 어떠한 부수 효과도 발생하지 않는다는 말과 같다.

수학에서는 불변성을 가지고 있기 때문에 참조 투명성을 만족시킨다. 그러기 때문에 특정한 장점을 가지게 된다.

  • 오직 하나의 결과값만을 가지기 때문에 식을 쉽게 계산
  • 함수의 결과값이 동일하기 때문에 식의 순서를 변경하더라도 결과는 달라지지 않음.

객체 지향에서는 변경을 베이스로 하고 있기 때문에 당연히 참조 무결성을 가질 수 없지만 명령-쿼리 분리 원칙을 통해서 특정한 경우에 참조 무결성의 특징을 가질 수 있게 만들 수 있다.

책임에 초점을 맞춰라

지금까지 나온 내용에 대해서 쉽게 설계 하는 방법은 메시지를 먼저 선택하고 그 이후에 메시지를 처리할 객체를 선택하는 것이다.

  • 디미터 법칙: 수신할 객체를 알지 못한 상태에서 메시지를 먼저 선택하기 때문에 객체의 내부 구조에 대해서 고민할 필요가 없다.
  • 묻지 말고 시켜라: 클라이언트 관점에서 메시지를 선택하기 때문에 필요한 정보를 물을 필요가 없다.
  • 의도를 드러내는 인터페이스: 메시지를 먼저 선택하기 땜누에 클라이언트 관점에 만들어진 메소드 이름일 것이고 그것은 명확하게 그 의도를 표현한다.
  • 명령-쿼리 분리 법칙: 메시지를 선택하는 과정에서 협력이라는 관점에서 고민하는 과정이 있을 것이다. 그 과정에서 협력 속에서 객체의 상태를 예측하고 이해하기 쉽게 만들기 위한 고민이 있을 것이다. 그 속에서 명령과 쿼리는 분리 될 것이다.

위에 말한 것처럼 우리가 고민했던 것들이 모두 책임을 기반으로 설계하는 경우 많은 부분 해결된다는 것을 알 수 있다.

가장 중요한 것은 협력에 적합한 객체가 아니라 협력에 적합한 메시지이다.

마무리

지금까지 살펴본 원칙들은 설계에서 좋은 방법이지만 실제로 실행 시점에 필요한 구체적인 제약이나 조건을 명확하게 표현하지는 못한다.

협력을 위해 두 객체가 보장해야하는 실행 시점의 제약을 인터페이스에 명시할 수 있는 방법이 존재하지 않는다.

이런 문제를 해결하기 위해서 계약에 의한 설계 라는 개념을 제안했다. 협럭을 위해 클라이언트와 서버가 준비해야 하는 제약을 코드 상에 명시적으로 표현하고 강제하는 방법이다.

Leave a comment