[오브젝트] 6장_메세지와 인터페이스

이번 장에서는, 유연하고 재사용 가능한 퍼블릭 인터페이스를 만드는데 도움이 되는 설계 원칙과 기법을 익힌다.

01 협력과 메세지

클라이언트-서버 모델
  1. 클라이언트 : 메세지를 전송하는 객체
  2. 서버 : 수신하는 객체

객체는 자신의 희망을 메세지 형태로 전송하고 메세지를 수신한 객체는 요청을 처리하고 응답한다.
객체는 독립적으로 수행할 수 있는 더 큰 책임을 수행하기 위해, 다른 객체와 협력해야 한다.

메세지와 메세지 전송

메세지는 객체들이 협력하기 위해 사용하는 유일한 의사소통 수단이다. 한 객체가 다른 객체에게 도움을 요청하는 것을 메세지 전송이라고 한다.

메세지와 메서드

메세지를 수신했을 때 실제로 수행되는 함수 또는 프로시저를 메서드라고 한다.
메세지 전송자와 메세지 수신자는 서로에 대한 상세한 정보를 알지 못하고 메세지라는 얅고 가는 끈을 통해 연결된다.

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

객체가 의사소통을 위해 외부에 공개하는 메세지의 집합을 퍼블릭 인터페이스라고 한다.
오퍼레이션은 내부의 구현 코드는 제외하고 단순히 메세지와 관련된 시그니처이다.
메세지를 수신했을 때 실제로 실행되는 코드는 메서드이다.
객체가 다른 객체에게 메세지를 전송하면 런타임 시스템은 메세지 전송을 오퍼레이션 호출로 해석하고 메세지를 수신한 객체의 실제 타입을 기반으로 적절한 메서드를 찾아 실행한다.

시그니처

오퍼레이션 관점에서 다형성이란, 동일한 오퍼레이션 호출에 대해 서로 다른 메서드들이 실행되는 것이다.

02 인터페이스와 설계 품질

퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법들

  1. 디미터 법칙
  2. 묻지 말고 시켜라
  3. 의도를 드러내는 인터페이스
  4. 명령-쿼리 분리
디미터 법칙

디미터 법칙은 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것이다. dot 를 이용해 메세지 전송을 표현하는 언어에서는 “오직 하나의 도트만 사용하라” 라는 말로 요약된다.
디미터 법칙을 따르면 shy code 를 작성할 수 있다. shy code 란 불필요한 다른 객체에게 어떤 것도 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드를 말한다.

1
2
// 디미터 법칙 위반
screening.getMovie().getDiscountCondition();

위 코드는, 메세지 전송자가 수신자의 내부 구조에 대해 물어보고 반환받은 요소에 대해 연쇄적으로 메세지를 전송한다. “기차 충돌” 이라고 한다.

1
2
// 디미터 법칙 준수
screening.calculateFee(audienceCount);

위 코드는, 객체의 내부 구조에 대해 묻지 않고 수신자에게 무언가를 시키는 메세지를 전송한다.

묻지 말고 시켜라

이 원칙을 지키다보면, 자연스럽게 정보 전문가에게 책임을 할당하고 높은 응집도를 가진 클래스를 얻게된다.
호출하는 객체는 이웃 객체가 수행하는 역할을 사용해 무언을 원하는지 서술하고 호출되는 객체가 어떻게 해야하는지를 스스로 결정하게 해야한다. 일반적으로 Tell, Don’t Ask 라고 한다. 공싱적으로는 디미터 법칙이라고 한다.

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

메서드 명명 방법 두가지

  1. 메서드가 작업을 어떻게 수행하는지 나타낸다.
    Bad. 메서드의 내부 구현을 설명하게 된다.

    1
    2
    3
    4
    5
    6
    7
    public class PeriodCondition{
    public boolean isSatisfedByPeriod(Screening screening) {...}
    }

    public class SequenceCondition{
    public boolean isSatisfedBySequence(Screening screening) {...}
    }

    클라이언트는 메서드의 이름이 다르기 때문에, 내구 구현일 정확히 이해하지 못하면 두 메서드가 동일한 작업을 한다는 것을 모른다.

  2. 무엇을 하는지 드러낸다.

    1
    2
    3
    4
    5
    6
    7
    public class PeriodCondition{
    public boolean isSatisfied(Screening screening) {...}
    }

    public class SequenceCondition{
    public boolean isSatisfied(Screening screening) {...}
    }

    두 메서드가 동일한 목적을 가진다는 것을 메서드 이름 통해서 명확하게 표현된다.

클라이언트가 두 메서드를 가진 객체를 동일한 타입드로 간주할 수 있도록 타입 계층으로 묶어야한다.

1
2
3
public interface DiscountCondition{
public boolean isSatisfied(Screening screening);
}

이처럼 어떻게 하느냐가 아니라 무엇을 하느냐에 따라 메서드 이름 짓는 패턴을 “의도를 드러내는 선택자” 라고 한다. 에릭 에반스는 도메인 주도 설계에서 “의도를 드러내는 선택자” 를 인터페이스 레벨로 확장한 “의도를 드러내는 인터페이스” 를 제시했다.

함께 모으기
  1. 디미터 법칙을 위반하는 티켓 판매 도메인

    1
    audience.getBag().minusAmount(ticket.getFee());

    Audience 의 퍼블릭 인터페이스 뿐만 아니라, 내부 구조에 대해서도 결합된다.

  2. 묻지 말고 시켜라

  3. 인터페이스에 의도를 드러내자
    TicketSeller 의 setTicket(), Audience 의 setTicekt(), Bag 의 setTicket() 명확하게 클라이언트의 의도를 드러내는 메서드 명인가 ?
    Theater 가 TicketSeller 에게 setTicket 메서드를 전송해서 얻고 싶었던 결과는 ? Audience 에게 티켓을 판매하는 것이다. 따라서 setTicket 보다 sellTo 가 의도를 명확하게 드러내는 메세지이다.
    TicketSeller 가 Audience 에게 setTicekt 메세지를 전송하는 이유는 ? Audience 가 티켓을 사도록 만드는 것이 목적이다. 따라서 클라이언트가 원하는 것은 buy 라는 메세지이다.
    Audience 가 Bag 에게 setTicekt 메세지를 전송하는 이유는 ? 티켓을 보관하도록 하기 위함이다. 따라서, hold 메세지가 명확하다.

03 원칙의 함정

디미터 법칙은 하나의 도트를 강제하는 규칙이 아니다
1
IntStream.of(1,15,20,3,9).filter(x-> x>10).distinct().count();

위 코드는, 디미터 법칙을 위반하지 않는다.
of, filter, distinct 메서드는 모두 IntStream 이라는 동일한 클래스 인스턴스를 반환한다. 즉, 이들은 IntStream 인스턴스를 또 다른 IntStream 의 인스턴스로 변환한다.
단지 IntStream 을 다른 IntStream 변환할 뿐, 객체를 둘러싸고 있는 캡슐은 그대로 유지된다.
기차 충돌처럼 보이는 코드라도 객체의 내부 구현에 대해 어떤 정보도 외부로 노출하고 있지 않으면 그것은 디미터 법칙을 준수한 것이다.
객체는 내부 구조를 숨겨야 하므로 디미터 법칙을 따라야하고, 자료 구조라면 내부를 노출해야하므로 적용할 필요가 없다.

결합도와 응집도의 충돌
1
2
3
4
5
6
public class PeriodCondition implements DiscountConditiono {
public boolean isSatisfied(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
.... ;
}
}

이 코드는 얼핏 보기에, Screening 의 내부 상태를 가져와 사용하기 때문에 캡슐화를 위반한 것 처럼 보인다.
그래서, 할인 여부 판단 로직을 Screening 의 isDiscountable 메서드로 옮기고 PeriodCondition 이 이 메서드를 호출하도록 변경하면 묻지 말고 시켜라 스타일을 준수하는 인터페이스를 얻는다고 생각할 것이다.
하지만, 이렇게 하면 Screening 이 기간에 따른 할인 조건을 판단하는 책임을 떠안게 된다. Screening 의 본직적인 책임은 영화를 예매하는 것이다. 반면, PeriodCondition 은 할인 조건을 판단하는 것이 본질적인 책임이다.
따라서, Screening 의 캡슐화를 향상시키는 것보다, Screening 의 응집도를 높이고 Screening 과 PeriodCondition 사이의 결합도를 낮추는 것이 전체적인 관점에 더 좋은 방법이다.

04 명령-쿼리 분리 원칙

  1. 명령 : 객체의 상태를 수정하는 오퍼레이션 == 프로시저
  2. 쿼리 : 객체와 관련된 정보를 반환하는 오퍼레이션 == 함수
반복 일정의 명령과 쿼리 분리하기

명령과 쿼리를 섞으면 실행 결과를 예측하기 힘들다. 명령과 쿼리를 명확하게 분리해야한다.

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

참조 투명성이란, 표현식 e를 e 의 값으로 e 의 위치 모두에 교체하더라도 결과가 달라지지 않는 특성이다.

f(1) = 3 이라고 하면,

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

f(1) 이 항상 3인 이유는, f(1) 의 값이 변하지 않기 때문이다. 어떤 값이 변하지 않는 성질을 “불변성” 이라고 한다. 어떤 값이 불변하다는 것은 부수효과가 발생하지 않는 다는 것이다.
객체지향 패러다임이 객체의 상태 변경이라는 부수 효과를 기반으로 하기 때문에, 참조 투명성은 예외이다.
하지만, 명령 쿼리 분리 원칙을 사용하면 균열을 줄일 수 있다. 즉, 부수 효과를 가지는 “명령” 으로부터 부수효과를 가지지 않는 “쿼리” 를 명백하게 분리함으로써 제한적으로 참조 투명성의 혜택을 얻을 수 있다.

책임에 초점을 맞춰라
  1. 디미터 법칙을을 준수하고, 묻지 말고 시켜라 스타일을 따르며, 의도를 드러내는 인터페이스를 설계하는 방법은
    메세지를 선택하고 그 후에 메세지를 처리할 객체를 선택하는 것이다.

  2. 명령과 쿼리를 분리하고, 계약에 의한 설계 개념을 통해 객체의 협력 방식을 명시적으로 드러내는 방법은
    객체 구현 이전에 객체 사이의 협력에 초점을 맞추는 것이다.

이 모든 방식의 중심에는 객체가 수행할 책임이 있다.


오브젝트 <조영호>

Comments