Post

메시지와 인터페이스

6. 메시지와 인터페이스

훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아니라 객체를 지향해야 한다

객체가 수행하는 책임에 초점을 맞춰야 함

객체지향 애플리케이션의 가장 중요한 재료는 클래스가 아니라 객체들이 주고받는 메시지

애플리케이션은 클래스로 구성되지만 메시지를 통해 정의됨

객체가 수신하는 메세지들이 객체의 퍼블릭 인터페이스를 구성

6.1 협력과 메시지

클라이언트-서버 모델

협력은 어떤 객체가 다른 객체에게 무엇인가를 요청할 때 시작

메시지는 객체 사이의 협력을 가능하게 하는 매개체

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

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

협력의 관점에서 객체는 두 가지 종류의 메시지 집합으로 구성

하나는 객체가 수신하는 메시지의 집합이고 다른 하나는 외부의 객체에게 전송하는 메시지의 집합

객체가 독립적으로 수행할 수 있는 것보다 더 큰 책임을 수행하기 위해서는 다른 객체와 협력해야 한다는 것

두 객체 사이의 협력을 가능하게 해주는 매개체가 바로 메시지

메시지와 메시지 전송

메시지(message): 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단

메시지 전송(message sending) 또는 메시지 패싱(message passing): 한 객체가 다른 객체에 게 도움을 요청하는 것

메시지를 전송하는 객체를 메시지 전송자(message sender), 메시지를 수신하는 객체를 메시지 수신자(message receiver)

메시지는 오퍼레이션명(operation name)인자(argument)로 구성됨

메시지 전송은 메시지 수신자 + 오퍼레이션명 + 인자

메시지와 메서드

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

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

객체는 메시지와 메서드라는 두 가지 서로 다른 개념을 실행 시점에 연결해야 하기 때문에 컴파일 시점과 실행 시점의 의미가 달라질 수 있다.

객체지향이 메시지 전송과 메서드 호출을 명확하게 구분한다는 사실이 모호함의 덫으로 밀어 넣을 수도 있다.

메시지와 메서드의 구분은 메시지 전송자와 메시지 수신자가 느슨하게 결합될 수 있게 한다.

메시지 전송자와 메시지 수신자는 서로에 대한 상세한 정보를 알지 못한 채 단지 메시지라는 얇고 가는 끈을 통해 연결

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

객체는 안과 밖을 구분하는 뚜렷한 경계를 가짐

퍼블릭 인터페이스: 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합

오퍼레이션(operation): 프로그래밍 언어의 관점에서 퍼블릭 인터페이스에 포함된 메시지

오퍼레이션은 수행 가능한 어떤 행동에 대한 추상화

메시지를 수신했을 때 실행되는 코드는 메서드

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

시그니처

오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합쳐 시그니처(signature)

6.2 인터페이스와 설계 품질

좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족해야 함

최소한의 인터페이스는 꼭 필요한 오퍼레이션만을 인터페이스에 포함

최소주의를 따르면서도 추상적인 인터페이스를 설계할 수 있는 가장 좋은 방법은 책임 주도 설계 방법을 따르는 것

객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 함으로써 클라이언트의 의도를 메시지에 표현할 수 있게 함

디미터 법칙

협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙이 바로 디미터 법칙(Law of Demeter)

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

“낯선 자에게 말하지 말라”

“오직 인접한 이웃하고만 말하라”

디미터 법칙을 따르기 위해서는 클래스가 특정한 조건을 만족하는 대상에게만 메시지를 전송하도록 프로그래밍해야 한다.

모든 클래스 C와 C에 구현된 모든 메서드 M에 대해서, M이 메시지를 전송할 수 있는 모든 객체는 다음에 서술된 클래스의 인스턴스여야 한다.

이때 M에 의해 생성된 객체나 M이 호출하는 메서드에 의해 생성된 객체, 전역 변수로 선언된 객체는 모두 M의 인자로 간주한다.

  • M의 인자로 전달된 클래스(C 자체를 포함)
  • C의 인스턴스 클래스 변수

아래 설명과 동일

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

디미터 법칙을 따르면 부끄럼타는 코드(shy code)를 작성 가능

부끄럼타는 코드란 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드를 말한다.

디미터 법칙과 캡슐화

디미터 법칙은 캡슐화를 다른 관점에서 표현한 것

디미터 법칙이 가치 있는 이유는 클래스를 캡슐화하기 위해 따라야하는 구체적인 지침을 제공하기 때문

디미터 법칙은 협력하는 클래스의 캡슐화를 지키기 위해 접근해야 하는 요소를 제한

디미터 법칙을 위반하는 코드의 전형적인 모습

1
screening.getMovie().getDiscountConditions()

메시지 전송자가 수신자의 내부 구조에 대해 물어보고 반환받은 요소에 대해 연쇄적으로 메시지를 전송 -> 기차 충돌(train wreck)

기차 충돌을 클래스의 내부 구현이 외부로 노출됐을 때 나타나는 전형적인 형태로 메시지 전송자는 메시지 수신자의 내부 정보를 자세히 알게 된다

디미터 법칙을 따르도록 코드를 개선하면 메시지 전송자는 더 이상 메시지 수신자의 내부 구조에 관해 묻지 않게 된다.

디미터 법칙은 객체가 자기 자신을 책임지는 자율적인 존재여야 한다는 사실을 강조

정보를 처리하는 데 필요한 책임을 정보를 알고 있는 객체에게 할당하기 떄문에 응집도가 높은 객체가 만들어짐

묻지 말고 시켜라

훌륭한 메시지는 객체의 상태에 관해 묻지 말고 원하는 것을 시켜야 한다는 사실을 강조

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

객체의 외부에서 해당 객체의 상태를 기반으로 결정을 내리는 것은 객체의 캡슐화를 위반

절차적인 코드는 정보를 얻은 후에 결정한다. 객체지향 코드는 객체에게 그것을 하도록 시킨다.

내부의 상태를 묻는 오퍼레이션을 인터페이스에 포함시키고 있다면 더 나은 방법은 없는지 고민해보라

내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재 -> 해당 객체가 책임져야 하는 어떤 행동이 객체 외부로 누수된 것

상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라

인터페이스는 객체가 어떻게 하는지가 아니라 무엇을 하는지를 서술해야 한다.

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

메서드 명명법

  1. 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는 것
1
2
 public boolean isSatisfiedByPeriod(Screening screening) { ...}
 public boolean isSatisfiedBySequence(Screening screening) { ...}

이러한 스타일이 좋지 않은 이유

  • 메서드에 대해 제대로 커뮤니케이션하지 못한다. 동일한 작업을 수행하지만 메서드의 이름이 다르기 때문에 두 메서드의 내부 구현을 정확하게 이해하지 못한다면 두 메서드가 동일한 작업을 수행한다는 사실을 알아채기 어렵다

  • 메서드 수준에서 캡슐화를 위반한다. 클라이언트로 하여금 협력하는 객체의 종류를 알도록 강요함

    만약 할인 방법이 변경된다면 메서드의 이름이 변경되어야 함 -> 클라이언트의 코드도 함께 변경 -> 변경 취약

  1. ‘어떻게’가 아니라 ‘무엇’을 하는지를 드러내는 것
1
 public boolean isSatisfiedBy(Screening screening) { ...}

클라이언트의 관점에서 동일한 작업을 수행하는 메서드들을 하나의 타입 계층으로 묶을 수 있는 가능성이 커짐

다양한 타입의 객체가 참여할 수 있는 유연한 협력을 얻게 됨

무엇을 하느냐에 따라 메서드의 이름을 짓는 패턴 -> 의도를 드러내는 선택자(Intention Revealing Selector)

객체에게 묻지 말고 시키되 구현 방법이 아닌 클라이언트의 의도를 드러내야 한다.

함께 모으기

디미터 법칙을 위반한 사례

근본적으로 디미터 법칙을 위반하는 설계는 인터페이스와 구현의 분리 원칙을 위반

프로그램에 노출되는 객체 사이의 관계가 많아질수록 결합도가 높아지기 때문에 프로그램은 불안정해짐

객체의 구조는 다양한 요구사항에 의해 변경되기 쉽기 때문에 디미터 법칙을 위반한 설계는 요구사항 변경에 취약해짐

디미터 법칙을 위반한 코드는 사용하기도 어렵다.

클라이언트 객체의 개발자는 내부구조까지 속속들이 알고 있어야 하기 때문

묻지 말고 시켜라

인터페이스에 의도를 드러내자

미묘하게 다른 의미를 가진 메서드가 같은 이름을 가지고 있다는 사실은 클라이언트 개발자를 혼란스럽게 만들 가능성이 높다

setTicket-> sellTo, buy, hold

오퍼레이션의 이름은 협력이라는 문맥을 반영해야 한다

객체 자신이 아닌 클라이언트의 의도를 표현하는 이름을 가져야 한다.

6.3 원칙의 함정

설계는 사실 트레이드오프의 산물

원칙이 현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시

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

하나 이상의 도트(.)를 사용하는 모든 케이스가 디미터 법칙 위반인 것은 아니다

결합도와 응집도의 충돌

객체는 내부 구조를 숨겨야 하므로 디미터 법칙을 따르는 것이 좋지만 자료 구조라면 당연히 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다

6.4 명령-쿼리 분리(Command-Query Separation) 원칙

루틴(routine): 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능

루틴은 프로시저(procedure)함수(function)로 구분

프로시저는 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류

함수는 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류

  • 프로시저는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다
  • 함수는 값을 반환할 수 있지만 부수효고를 발생시킬 수 없다

명령(command)쿼리(query)는 객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또 다른 이름

객체의 상태를 수정하는 오퍼레이션을 명령이라고 부르고, 객체와 관련된 정보를 반환하는 오퍼레이션을 쿼리라고 부른다.

명령-쿼리 분리 원칙의 요지는 오퍼레이션은 부수효과를 발생시키는 명령이거나 부수효과를 발생시키지 않는 쿼리 중 하나여야 함

어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안 됨

  • 객체의 상태를 변경하는 명령은 반환값을 가질 수 없다
  • 객체의 정보를 반환하는 쿼리는 상태를 변경할 수 없다

반복 일정의 명령과 쿼리 분리하기

일정 관리 예시를 통해 확인

이벤트: 특정 일정에 실제로 발생하는 사건 의미

반복 일정: 특정 시간 간격에 발생하는 사건 전체를 포괄적으로 지칭하는 용어

2개가 같이 있어서 에러 발생 -> 분리하여 해결

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

명령과 쿼리를 엄격하게 분류하면 객체의 부수효과를 제어하기가 수월해진다

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

명령과 쿼리를 분리함으로써 명령형 언어의 틀 안에서 참조 투명성(referential transparency)의 장점을 제한적으로 누릴 수 있다.

컴푸터의 세계와 수학의 세계를 나누는 가장 큰 특징은 부수효과(side effect)의 존재 유무

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

불변성(immutability): 어떤 값이 변하지 않는 성질

부수효과 -> 참조 투명성 -> 불변성

참조 투명성을 만족하는 식의 장점

  • 모든 함수를 이미 알고 있는 하나의 결괏값으로 대체할 수 있기 때문에 식을 쉽게 계산 가능
  • 모든 곳에서 함수의 결괏값이 동일하기 때문에 식의 순서를 변경하더라도 각 식의 결과는 달라지지 않는다

책임에 초점을 맞춰라

메시지를 먼저 선택하는 방식이 미치는 긍정적인 영향

  • 디미터 법칙: 두 객체 사이의 구조적인 결합도를 낮출 수 있다.
  • 묻지 말고 시켜라: 묻지 말고 시켜라 원칙에 따라 협력을 구조화하게 됨
  • 의도를 드러내는 인터페이스: 당연히 의도가 더 잘 드러남
  • 명령-쿼리 분리 원칙: 협력이라는 문맥안에서 객체의 인터페이스에 관해 고민하게 함

참조

  1. 오브젝트
This post is licensed under CC BY 4.0 by the author.