06 메세지와 인터페이스

 

들어가며

 

클라이언트-서버 모델

 

두 객체 사이의 협력 관계를 설명하기 위해 사용되는 메타포는 클라이언트 - 서버(Client - Server) 모델이다.

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

 

 

Movie는 DiscountPilicy의 관점에서 메세지 전송자가 될 수 있다.

 

대부분의 사람들은 객체가 수신하는 메세지의 집합에만 초점을 맞추지만 협력에 적합한 객체를 설계하기 위해서는 외부에 전송하는

메세지의 집합도 함께 고려하는 것이 바람직하다.

 

 

객체가 독립적으로 수행할 수 있는 것보다 더 큰 책임을 수행하기 위해서는 다른 객체와 협력해야 한다. 두 객체 사이의 협력을 가능하게 해주는 매개체가 바로 메세지 이다.

 


메세지와 메세지 전송

 

메세지는 오퍼레이션명(operation name)과 인자(argument)로 구성되며 메세지 전송은 여기에 메세지 수신자를 추가한 것이다.

 

 


메세지와 메서드

 

메세지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드라고 부른다. 중요한 것은 코드 상에서 동일한 이름의 변수(condition)에게 동일한 메세지를 전송하더라도 객체의 타입에 따라 실행되는 메서드라 달라질 수 있다. (추상화를 통한 동적 바인딩)

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

 


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

 

객체가 의사소통을 위해 외부에 공개하는 메세지의 집합을 퍼블릭 인터페이스라고 부른다.

퍼블릭 인터페이스에 포함된 메세지는 오퍼레이션이라고 부른다.

오퍼레이션은 내부의 구현코드는 제외하고 단순히 메세지와 관련된 시그니처를 가리키는 경우가 대부분이며, 수행가능한 어떤 행동에 대한 추상화이다.  -> DiscountCondition인터페이스에 정의된 isSatisfiedBy가 오퍼레이션에 해당된다.

 

메세지를 수신했을 때 실제로 실핼되는 코드가 바로 메서드이다.

 

 

객체가 다른 객체에게 메세지를 전송하면 런타임 시스템은 메세지 전송을 오퍼레이션 호출로 해석하고 메세지를 수신한 객체의 실제 타입을

기반으로 메서드를 찾아 실행한다.

-> 퍼블릭 인터페이스와 메세지 관점에서보면 '메서드 호출'보다는 '오퍼레이션 호출'이 더 적절해 보인다.

 


시그니처

 

오퍼레이션의 이름과 파라미터 목록을 합쳐 시그니처(signature)라고 부른다. 오퍼레이션은 실행 코드 없이 시그니처만을 정의한 것이며, 메서드는 이 시그니처에 구현을 더한것이다.

 

결론적으로 메세지가 객체의 품질 즉, 퍼블릭 인터페이스와 그안에 포함되는 오퍼레이션을 결정한다.

 

 

02 인터페이스와 설계 품질

 

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

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

-> 메세지를 먼저 선택함으로써 협력과는 무관한 오퍼레이션이 인터페이스에 스며드는 것을 방지한다.

 

퍼블릭 인터페이스의 품질에 영향을 미치는 다음과 같은 기법이 존재한다.

 

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

 


디미터 법칙

 

public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
        Movie movie = screening.getMovie();

        boolean discountable = false;
        for (DiscountCondition condition : movie.getDiscountConditions()) {
            if (condition.getType() == DiscountConditionType.PERIOD) {
                discountable = screening.getWhenScreened().getDayOfWeek().equals(condition.getDayOfWeek())
                    && condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0
                    && condition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
            } else {
                discountable = condition.getSequence() == screening.getSequence();
            }
            if (discountable) { 
            	break; 
            }
        }
        ...

 

해당 코드의 큰 문제는 ReservationAgency와 인자로 전달된 Screening사이의 결합도가 너무 높아 Screening의 내부 구현이 변경될 때마다 ReservationAgency도 함께 변경된다.

 

객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 객체 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한 하는 원칙이 디미터 법칙(Law of Demeter)이다.

 

디미터 법칙을 따르기 위해서는 다음과 같은 조건을 막족하는 대상에게만 메세지를 전송해야한다.

 

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

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

  • C의 인스턴스 변수의 클래스

public Reservation reserve(Customer customer, int audienceCount) {
    Money fee = screening.calculateFee(audienceCount);
    return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
}

 

디미터 법칙을 따르면 불피요한 어떤것도 다른 객체에 보여주지 않는 부끄럼타는 코드를 작성할 수 있다.

 


묻지 말고 시켜라

 

디미터 법칙은 훌륭한 메세지는 객체의 상태에 대해 묻지 말고 정보전문가에게 원하는 것을 시켜야한다는 사실을 강조한다. 이로써 객체의 정보를 이용하는 행동을 객체의 외부가 아닌 내부에 위치시키기 때문에 자연스레 정보와 행동을 동일한 클래스 안에 두게 된다. 

 

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

 

public class PeriodCondition {
    public boolean isSatisfiedByPeriod(Screening screening) { ... }
}

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

 

메서드 명을 지을 때는 어떻게가 아닌 무엇을 수행하는지 드러내도록 지으되 클라이언트 관점에서 동일한 작업을 수행하는경우 추상화 하여 하나의 타입 계층으로 묶어 설계를 유연하게 만들어야 한다.

 

public class PeriodCondition implements DiscountCondition {
    public boolean isSatisfiedByPeriod(Screening screening) { ... }
}

public class SequenceCondition implements DiscountCondition {
    public boolean isSatisfiedBySequence(Screening screening) { ... }
}

public interface DiscountCondition {
    boolean isSatisfiedBy(Screening screening);
}

 

이처럼 어떻게 하느냐가 아닌 무엇을 하느냐에 따라 메서드의 이름을 짓는 패턴을 의도를 드러내는 선택자(Intention Revealing Selector)라고 부른다.

 


함께 모으기

 

public class Theater {
     private TicketSeller ticketSeller;
     
     public Theater(TicketSeller ticketSeller) {
          this.ticketSeller = ticketSeller;
     }
     
     public void enter(Audience audience) {
         if (audience.getBag().hasInvitation()) {
             Ticket ticket = ticketSeller.getTicketOffice().getTicket();
             audience.getBag().setTicket(ticket);
         } else {
         
         ...
         

 

디미터 법칙에 따르면 Theater가 인자로 전달된 audience와 인스턴스 변수인 ticketSeller에게 메세지를 전송하는 것은 문제가 없다. 그러나 Theater가  audience와 ticketSeller 내부에 포함된 객체에도 직접 접근하는 것이 문제가 된다.

이로 인해 디미터 법칙을 위반하게 된다.

 

디미터 법칙을 위반하는 설계는 인터페이스와 구현의 분리 원칙을 위반한다.

Audience가 Bag을 포함한다는 사실은 Audience의 내부 구현에 속하며 Audience는 자신의 내부 구현을 자유롭게 변경할 수 있어야 하지만 퍼블릭 인터페이스에 getBag을 포함시키는 순간 객체의 구현이 퍼블릭 인터페이스를 통해 외부로 새어나가버린다.

 

디미터 법칙을 위반하는 코드를 수정하는 일반적인 방법은 Audience와 TicketSeller의 내부 구조를 묻는 대신 두 객체가 직접 자신의 책임을 수행하도록 시키는 것이다.

 

결합도가 낮으면서 의도를 명확히 드러내는 간결한 협력을 원한다면

 

묻지 말고 시켜라 원칙을 통해 모든 객체 스스로 자신의 상태를 제어하게끔 리팩토링을 진행해야한다.

디미터 법칙을 준수시켰다면 인터페이스에 의도를 드러내자 원칙을 통해

각 메서드들이 무엇을 하는지 그 의도를 드러내도록 리네이밍을 진행해야한다.

 


03 원칙의 함정

 

디미터 법칙, 묻지 말고 시켜라 원칙은 훌륭한 설계원칙이다. 하지만 이는 절대적인 법칙이 아니다. 법칙에는 예외가 없지만 원칙에는 예외가 넘쳐난다.

설계시 원칙을 추종하지말고 현재 상황에 부적합하다고 느껴진다면 과감하게 원칙을 무시해야한다.

 


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

 

Stream api 사용시 보통 기차 충돌과 같은 코드를 작성하게 되지만 이는 절대 디미터 법칙을 위반한것이 아니다. 디미터 법칙은 결합도와 관련된것이므로 객체의 내부 구조가 외부에 노출될 때 비로소 디미터 법칙을 위반한것으로 간주된다.

 


결합도와 응집도의 충돌

 

묻지않고 시켜야하지만 때로는 물으려는 객체가 데이터인 경우가 있다. 바로 자료구조인 경우이다. 자료구조라면 당연히 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다.

앞서 말했지만 원칙을 맹신하지 않고, 적용하기 전 적절한 상황과 적절한 상황을 판단하는 안목을 길러야 한다.

 


04 명령-쿼리 분리 원칙

 

명령-쿼리 분리 원칙이란? 퍼블릭 인터페이스에 오퍼레이션을 정의할 때 참고할 수 있는 지침이다.

 

이해를 위해 몇가지 용어를 살펴보자.

 

루틴 - 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈

프로시저 - 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류 (부수효과 발생시킴)

함수 - 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류 (부수효과 발생시키지 않음)

 

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

 

명령 - 객체의 상태를 수정하는 오퍼레이션 (프로시저)

쿼리 - 객체와 관련된 정보를 반환하는 오퍼레이션 (함수)

 

단, 명령-쿼리 분리 원칙의 요지는 오퍼레이션은 명령이거나 쿼리 중 하나여야 한다. 어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안 된다.

 

명령쿼리를 분리하기 위해서는 다음 규칙을 준수해야 한다.

 

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

 


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

 

명령과 쿼리를 메서드로 본다면 

public void setSomeStatus() { ... }

public Status getSomeStatus() { ... }

 

만약 명령과 쿼리를 분리하지 않는경우

 

public Status getSomeStatus() {
    setSomeStatus(); //getSomeStatus를 여러번 호출하면 반환값이 달라질 수 있음. 
}

private void setSomeStatus() { ... }

 

부수효과 즉, 사이트 이펙트를 발생시킬 수 있음.

 

명령과 쿼리 분리를 통해 가독성이 높은 코드작성과 유지보수의 효율을 높일 수 있음.

 


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

 

명령과 쿼리를 분리함으로써 명령형 언어의 틀 안에서 버그가 적고, 디버깅이 용이하며 쿼리의 순서에 따라 실행 결과가 변하지 않는 코드를 작성할 수 있는 이점을 주는 참조 투명성(referencetial transparency)을 제한적이나마 누릴 수 있게 된다.

 

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

 

수학에서 함수는 동일한 입력에 대해 항상 동일한 값을 반환한다. 그러므로 참조 투명성을 만족시키는 이상적인 예이다.

즉, 부수효과를 발생시키지 않는다.

 

f(1) = 3 이라고 정의 했을 때

f(1) * 2 = 6 //결괏값

f(1)을 3으로 바꾸더라도

3 * 2 = 6 

결괏값은 바뀌지 않는다.

 

즉, f(1)의 값은 변하지 않는다. -> 불변성

 

또 식의 순서가 변경되더라도 결괏값이 달리지지 않는다.

 

따라서 참조 투명성은 우리에게 두 가지 장점을 제공한다.

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

결론적으로 객체지향 패러다임이 객체의 상태 변경이라는 부수효과를 기반으로 하여 참조 투명성은 예외에 가까우나

명령-쿼리 원칙을 사용하면 참조 투명성에 조금이나마 다가갈 수 있다.

 


책임에 초점을 맞춰라

 

책임 주도 설계 방법에 따라 메세지가 객체를 결정하게 하면 앞서 설명한 디미터 법칙, 묻지 말고 시켜라 스타일, 의도를 드러내는 인터페이스, 명령-쿼리 분리 원칙에 긍정적인 영향을 미칠 수 있다.

 

디미터 법칙: 협력이라는 컨텍스트안에서 객체보다 메세지를 먼저 결정하면 수신할 객체를 알지 못한 상태에서 메세지를 먼저 선택하기 때문에 객체의 내부 구조에 대해 고민할 필요가 없어져 구조적인 결합도를 낮출 수 있다.

 

묻지 말고 시켜라: 클라이언트 관점에서 메세지를 먼저 선택하기 때문에 필요한 정보를 물을 필요없이 원하는 것을 표현한 메세지만 전송하면 된다.

 

의도를 드러내는 인터페이스: 메세지를 먼저 선택한다는 것은 메세지를 전송하는 클라이언트 관점에서 메세지의 이름을 정한다는 것이기 때문에 클라이언트가 무엇을 원하는지 그 의도가 분명히 드러날 수 밖에 없다.

 

명령-쿼리 분리 원칙: 메세지를 먼저 선택한다는 것은 협력이라는 컨텍스트안에서 객체의 인터페이스에 관해 고민하기 한다는 것이기 때문에 따라서 예측 가능한 협력을 만들기 위해 명령과 쿼리를 분리하게 될것이다.

 

 

'📚 Book > Object' 카테고리의 다른 글

08 의존성 관리하기  (0) 2020.03.08
07 객체 분해  (0) 2020.02.29
05 책임 할당하기  (1) 2020.02.16
04 설계 품질과 트레이드 오프  (0) 2020.02.10
03 역할, 책임, 협력  (0) 2020.02.02