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

인터페이스와 설계 품질

최소 주의를 따르면서 추상적인 인터페이스를 설계하는 가장 좋은 방법은 책임 주도 설계를 따라가는 것 이다. 책임 주도 설계는 좋은 품질의 인터페이스를 얻는 지침을 제공하고, 다음은 좋은 품질의 인터페이스가 가지는 공통적 특징이다.

디미터 법칙

디미터 법칙은 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하는 법칙이다. 디미터 법칙은 낯선 자에게 말하지 말라 혹은 오직 인접한 이웃하고만 말하라로 귀결된다. Java나 C#과 같은 언어에서는 오직 하나의 dot만 사용하라와 같은 의미가 된다. 그리고 이를 통해 결합도를 효과적으로 낮춘다. 그리고 이를 따르기 위해서는 클래스 내부의 메서드가 다음과 같은 조건을 만족하는 인스턴스에만 메세지를 전송하도록 프로그래밍 해야 한다.

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

다음은 흔히 디미터 법칙을 위반했을 때 나타나는 코드의 양상이다.

1
coffeeShop.getCoffee().addExtraShot(2);

이와 같은 코드를 기차 충돌이라 부르는데 이는 내부 구현이 외부로 노출됐을때 나타나는 코드의 형태이다. 이를 통해 캡슐화가 무너지고 강하게 결합된다.

하지만 무비판적으로 디미터 법칙을 수용해서는 안된다. 가령 Streams API를 봐보자.

IntStream.of(1, 2, 3, 4, 5).filter(x -> x > 3).distinct().count()

이런 코드는 디미터 법칙을 위반하는 코드가 아니다. 디미터 법칙은 결합도와 관련된 것이며 객체 내부의 구조가 외부로 노출되지 않도록 하는 것이 목적이다. 위 코드는 IntStream을 IntStream으로 변환해가며 결과를 도출할 뿐 캡슐화를 저해하지 않는다.

또한 어떤 경우에서는 설계를 하다 보면 트레이드 오프를 해야 할 상황에 처한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Order {
private int sequence;
private DayOfWeek dayOfWeek;
private long fee;
private DiscountEvent discountEvent;


public int getSequence() {
return sequence;
}

public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}

public void setFee(Coffee coffee) {
this.fee = coffee.getFee() - this.discountEvent.discountFee(this);
}
}
1
2
3
4
5
public class DiscountEvent {
public long discountFee(Order order) {
return order.getDayOfWeek() == DayOfWeek.MONDAY && order.getSequence() % 10 == 0 ? 1000 : 0;
}
}

위와 같은 코드를 보자. DiscountEvent에서는 Order의 상태에 접근하고 있다. 이는 캡슐화를 위반한 것 처럼 보인다. 그렇다면 할인에 대한 로직을 Order안에서 호출하게 된다면 어떻게 될까? 과연 Order는 할인에 대한 책임을 가질 수 있을까? 디미터 법칙의 위반 여부는 물어야 하는 대상이 객체인지, 자료구조인지에 따라 달라진다. 객체는 내부 구조를 숨겨야함으로 디미터 법칙을 따르는 것이 좋지만 자료구조라면 당연히 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다.

묻지 말고 시켜라

묻지 말고 시켜라는 것은 메세지 수신자의 상태를 기반으로 송신자가 결정을 내린 후 메세지 수신자의 상태를 바꾸어선 안된다는 말이다. 객체의 외부에서 해당 객체의 상태를 기반으로 결정을 내리는 것은 캡슐화를 위반한다. 이를 점검하기 위해서는 내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재하는지를 점검하면 된다. 또한 이와 더불어 휼륭한 인터페이스를 위해서는 객체가 하는 작업을 외부로 드러내서는 안되고 무엇을 하는지 알려야 한다.

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

메서드의 이름을 짓는 방법은 두 가지가 있다. 첫째는 메서드가 작업을 어떻게 수행하는지에 대해 서술하는 것이다. 하지만 이는 큰 맥락에서 캡슐화를 위배하는 행동이다. 다음과 같은 코드를 보자.

1
2
3
4
5
6
7
public class SequentialDiscountEvent {
public boolean isSatisfiedBySequence(Order order) { ... }
}

public class DailyDiscountEvent {
public boolean isSatisfiedByDate(Order order) { ... }
}

메서드의 이름을 짓는 두번째 방법은 어떻게가 아닌 무엇을 하는지를 드러내는 것이다. 무엇을 하는지를 드러내는 코드는 코드를 읽기 쉽고 유연하게 만든다. 객체가 협력안에서 수행해야 하는 책임을 보여주기 때문이다.

1
2
3
public interface DiscountEvent {
public boolean isSatisfiedBy(Order order) { ... }
}

위 코드와 같이 의도를 드러내는 인터페이스를 설계해야 한다. 의도를 드러내는 인터페이스는 구현과 관련된 모든 정보를 캡슐화하고 객체의 퍼블릭 인터페이스에는 협력과 관련된 의도만을 표현할 수 있게 된다.

명령 - 쿼리 분리 원칙

명령 - 쿼리 분리 원칙은 인터페이스에 오퍼레이션을 정의할 때 도움을 주는 지침이다. 어떤 절차를 묶어 호출 가능하도록 기능을 부여한 것을 루틴이라 하는데, 루틴은 프로시저함수로 구분이 가능하다. 프로시저는 객체 내부의 상태를 변화시키고 반환값이 존재하지 않는다. 함수는 객체 내부의 상태를 변화시키지 않고 반환값이 존재 한다. 명령과 쿼리(질의)는 이런 시각에서 프로시저와 함수에 귀결한다.

명령과 쿼리를 분리하게 되면 부수효과(내부 상태 변경)를 제어하기 수월해진다. 또한 분리를 통해 참조 투명성의 장점을 범위 제한적으로 누릴 수 있다. 이러한 패러다임이 최근들어 다시 주목받는 함수형 프로그래밍에 부합하는 개념이다. 함수(쿼리 혹은 질의)를 통해서 반환값을 받고 내부 상태 변경에는 프로시저(명령)을 사용한다. 이를 통해 사이트 이펙트를 줄일 수 있다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2021/01/12/Object/object6/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.