6장 메시지와 인터페이스

좋은 인터페이스를 설계하기 위해서 어떻게 해야할까? 책임 주도 설계와 함께 적용되면 좋을 법한 4가지 원칙이 있다.

훌륭한 퍼블릭 인터페이스를 얻기 위해서는 책임 주도 설계 방법을 따르는 것만으로는 부족하다. 유연하고 재사용 가능한 퍼블릭 인터페이스를 만드는 데 도움이 되는 설계 원칙과 기법을 익히고 적용해야 한다.

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

  • 객체가 수신하는 메시지의 집합

  • 외부의 객체에 전송하는 메시지의 집합

두 집합을 모두 고려해서 객체를 설계해야 한다.

객체지향은 메시지 전송과 메서드 호출을 명확하게 구분한다!

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

isSatisfiedBy(screening) // 메시지
condition.isSatisfiedBy(screening) // 메시지 전송

한편 메시지는 메서드와 구분되는 개념이다. 객체지향에서는 메시지를 전송 받은 객체의 타입에 따라 호출되는 메서드가 달라진다.

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

메시지와 메서드의 느슨한 결합이 객체지향의 유연함을 가능케 한다.

용어 정리

  • 오퍼레이션: 객체가 다른 객체에게 제공하는 추상적인 서비스. 메시지를 수신하는 객체의 인터페이스를 강조한다. 오퍼레이션은 실행 코드 없이 시그니처만을 정의한 것이다.

  • 메서드: 메시지에 응답하기 위해 실행되는 코드 블록. 메서드는 오퍼레이션의 구현이다.

  • 퍼블릭 인터페이스: 객체가 협력에 참여하기 위해 외부에서 수신할 수 있는 메시지의 묶음.

  • 시그니처: 시그니처는 오퍼레이션이나 메서드의 명세를 나타낸 것이다. 함수 이름과 인자의 목록을 포함한다. 일반적이진 않지만 반환 타입도 시그니처의 일부로 여기는 언어도 존재한다.

다형성은 하나의 오퍼레이션에 여러 메서드가 맵핑되어 실행되는 것이라고 정의할 수 있다.

그래서 좋은 인터페이스는?

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

이를 위한 노하우들이 녹아있는 4가지 원칙이 있다.

1) 디미터 원칙

  • 낯선 자에게 말하지 말라, 오직 인접한 이웃하고만 말하라, 오직 하나의 도트만 사용하라.

  • 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 의미. 이렇게 하면 결합도를 효과적으로 낮출 수 있다.

  • 클래스를 캡슐화하는 데 유용한 구체적인 지침을 제공한다.

2) 묻지 말고 시켜라

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

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

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

  • 메서드 명에 협력하는 객체의 종류를 알도록 강조하진 말자.

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

    이렇게 하지 말자. 메시지 전송하는 클라이언트 입장에서 메시지 수신 객체의 내부구현이 드러난다.

    public class PeriodCondition {
      public boolean isSatisfiedBy(Screening screening) {...}
    }
    
    public class SequenceCondition {
      public boolean isSatisfiedBy(Screening screening) {...}
    }

    이렇게 하자!

    클라이언트의 관점에서 협력을 바라봐야 한다. 클라이언트의 관점에서는 두 메서드는 할인 여부를 판단하기 위한 작업을 수행하므로, 두 메서드 모두 클라이언트의 의도를 담을 수 있도록 isSatisfiedBy로 변경하는 것이 적절한 것이다. 가장 간단한건 아래 인터페이스를 정의하는 것이다.

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

4) 명령-쿼리 분리

프로시저(procedure)와 함수(function)을 구분하자.

  • 프로시저: 부수효과(side effect)를 발생시킬 수 있지만 값을 반환할 수 없다. 이 것이 명령(command)에 해당한다.

  • 함수: 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다. 이 것이 쿼리(query)에 해당한다.

이를 구분해야 예측 가한 협력을 만들기 용이하다.

주의사항: 원칙의 함정에 빠지지 말자

설계는 트레이드오프의 산물이다. 그런데 초보자는 원칙을 맹목적으로 추종하고, 적용하려는 원칙들이 서로 충돌하는 경우에도 원칙에 정당성을 부여하고 억지로 끼워 맞추려고 노력한다.

원칙이 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시하자. 원칙을 아는 것보다 더 중요한 것은 원칙이 언제 유용하고 언제 유용하지 않은지 판단할 수 있는 능력을 기르는 것이다.

그래서!

1) 디미터 원칙이 하나의 도트(.)를 강제하는 규칙이라고 오해하지 말자.

예를 들어 InStream.of(1, 15, 20, 3, 9).filter(x -> x > 10).distinct().count(); 는 디미터 원칙을 위반하지 않는다. 도트(.)가 많지만 IntStream을 다른 IntStream으로 변경할 뿐 객체의 내부 구조를 외부에 노출하지 않기 때문이다.

2) '묻지 말고 시켜라' 스타일로 코드를 바꿔서 캡슐화하려다가 결합도를 높이고 응집도를 낮추는 경우도 있다.

Last updated