[오브젝트] 6장 메시지와 인터페이스 본문

백앤드 개발일지/웹, 백앤드

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

giron 2023. 12. 17. 14:55
728x90

스터디를 하면서 실무에 적용하기 좋은 챕터라고 생각되어 따로 정리해둡니다.


유연하고 재사용 가능한 퍼블릭 인터페이스를 만들기 위한 원칙과 기법을 살펴보는 것이 주제이다.

우선 협력메시지에 대해 알아보자.

협력 관계를 설명하는 전통적인 메타포는 클라이언트-서버 모델이다. 협력은 클라이언트가 서버의 서비스를 요청하는 단방향 상호작용이다. 그리고 객체는 클라이언트와 서버의 역할을 동시에 수행한다.

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

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

협력에 적합한 객체를 설계하기 위해서는 외부에 전

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

용어정리

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

한 객체가 다른 객체에게 도움을 요청하는 것을 메시지 전송 또는 메시지 패싱이라고 부른다.

  • 메시지를 전송하는 객체를 메시지 전송자라고 부르고
  • 메시지를 수신하는 객체를 메시지 수신자라고 부른다.

클라이언트 서버 관점에서는 전송자는 클라이언트, 수신자는 서버라고 부르기도 한다. 메시지는 오퍼레이션명과 인자로 구성되며 메시지 전송은 메시지 수신자를 추가한 것이다.

따라서 메시지 전송 = 메시지 수신자 + 오퍼레이션명 + 인자의 조합

 

메시지 전송 표기법 with Java

condition.isSatisfiedBy(screening);
  • condition: 수신자
  • isSatisfiedBy: 오퍼레이션명
  • screening: 인자

어떤 코드가 실행되는지는 메시지 수신자의 실제 타입에 달려있다. condition은 DiscountCondition이라는 인터페이스 타입으로 정의돼 있지만 실제 수행 코드는 인터페이스를 실체화한 클래스 종류에 따라서 달라진다.

 

이처럼 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드라고 부른다.

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

퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션이라고 부른다. 오퍼레이션은 수행가능한 어떤 행동에 대한 추상화다. 오퍼레이션을 부를때는 내부 코드는 제외하고 단순히 메시지와 관련된 시그니처를 가리키는 경우가 대부분이다. 그에 반해 메시지를 수신했을 때 실제로 실행되는 코드는 메서드라고 부른다..

예시 사진

하나의 오퍼레이션에 다양한 메서드를 구성해서 다형성을 활용하자.

2. 인터페이스와 설계 품질

좋은 인터페이스는 최소한의 꼭 필요한 오퍼레이션만을 인터페이스에 포함한다. 추상적인 인터페이스는 어떻게 수행하는지가 아니라 무엇을 하는지를 표현한다.

좋은 인터페이스를 설계하는 방법은 책임 주도 설계 방법을 따르는 것이다. 이 방법은 메시지를 먼저 선택함으로써 협력과 무관한 오퍼레이션이 인터페이스에 스며드는 것을 방지한다. (인터페이스 분리 원칙)

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

원칙과 기법 소개

  • 디미터 법칙
    • 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라
    • 이웃하고만 얘기해라, 낯선 자에게 말하지 말라
  • 묻지 말고 시켜라
  • 의도를 드러내는 인터페이스
  • 명령-쿼리 분리

디미터의 법칙

클래스가 특정 조건을 만족하는 대상에게만 메시지를 전송하도록 해야한다. 모든 클래스 C와 그의 메서드를 M으로 가정할때, M이 메시지를 전송할 수 있는 모든 객체는 다음 서술된 클래스의 인스턴스여야 한다. 이때 M에 의해 생성된 객체나 M이 호출하는 메서드에 의해 생성된 객체, 전역 변수로 선언된 객체는 모두 M의 인자로 간주한다.

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

쉬운 예시로 아래 조건을 만족하는 인스턴스에만 메시지를 전송하도록 프로그래밍 해야 한다.

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

기차충돌 : screening.getMovie().getDiscountConditions()와 같이 만들지 말자.

디미터의 법칙은 내부 구조를 묻는 메시지가 아니라 수신자에게 무언가를 시키는 메시지가 더 좋은 메시지라고 속삭인다.

책에서는 ReservationAgency가 Movie, DiscountCondition을 알고있어서 강한 결합 및 메시지를 보내지 않고 있었는데 디미터의 법칙을 적용해서 이런 결합도를 낮추고 유지보수하기 좋은 코드가 되었다.

묻지말고 시켜라

객체지향의 기본은 함께 변경될 확률이 높은 정보와 행동을 하나의 단위로 통합하는 것이다. 내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재한다면 객체가 책임져야하는 어떤 행동이 객체 외부로 누수된 것이다.

ex) if(screening.getPosition() ≠ “1st room”) ← 누수된 예시

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

메서드 이름 짓는 방법

  1. 메서드가 작업을 어떻게 수행하는지를 나타내도록 짓는 것 → X
  2. 어떻게가 아니라 무엇을 하는지를 드러내는 것이다. → O

안좋은 메서드 네이밍 예시

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

public class SequenceCondition {
	public boolean isSatisfiedBySequence(Screening screening) {...}
}
  1. 클라이언트 관점에선 모두 할인 조건을 판단하는 동일한 작업이지만 메서드 이름이 다르기 때문에 두 메서드의 내부 구현을 정확하게 이해하지 못한다면 두 메서드가 동일한 작업을 수행한다는 사실을 알아채기 어렵다.
  2. 메서드 수준의 캡슐화를 위반한다는 것이다. PeriodCondition을 사용하는 코드를 SequenceCondition으로 바꾸면 메서드또한 바꿔줘야한다.

따라서 2번의 방식인 무엇을 하는지를 드러내도록 작성하는 것이 좋다. 또한 1번 방식은 이른 시기부터 클래스 내부 구현에 관한 고민이 발생할수 있다. 반면에 2번은 외부의 객체가 메시지를 전송하는 목적을 먼저 생각하도록 하여 클라이언트의 의도에 부합하도록 메서드의 이름을 짓게 된다.

따라서 동일한 목적을 의미하도록 메서드 이름을 바꿔준다. 

 

* 처음엔 구체적인 네이밍이 이해하기 쉽다고 생각했다. 코드를 확인하지않고 네이밍으로만 어떤 코드인지 유추할수 있겠다고 생각했기 때문이다. 그런데 스터디를 통해 의견을 나누면서 생각이 바뀌었다.

 

결국엔 코드를 수정하려면 내부 로직을 읽어봐야하기 때문이다. 2번의 명확한 장점은 코드를 읽을때 어떻게 메시지를 보내는 것이 아닌 무엇을 보냈는지에만 집중하면 된다. 어떻게 메시지를 보내야할지 파악하는것은 실제 리펙토링할때 코드를 보고 확인하는게 맞다고 생각한다.

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

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

추가로 클라이언트가 두 메서드를 가진 객체를 동일한 타입으로 간주할 수 있도록 동일한 타입 계층으로 묶어야 한다. 인터페이스를 사용하여 적용할 수 있다.

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

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

무엇에 집중한 패턴을 의도를 드러내는 선택자라고 부른다.

원칙의 함정

원칙이 현재상황에 부적합하다고 판단되면 과감하게 원칙을 무시해라. 그것이 초보자와 고수의 차이이다.

 

고려할만한 이슈

IntStream.of(1, 15, 20, 3, 9).filter(x -> x > 10).distinct().count()

이 코드는 디미터의 법칙을 위반하지않는다. 디미터의 법칙은 결합도와 관련된 것이며, 결합도가 문제 되는 것은 객체의 내부 구조가 외부로 노출되는 경우로 한정된다. IntStream을 다른 IntStream으로 변환할 뿐, 객체를 둘러싸고 있는 캡슐은 그대로 유지된다. 즉, 디미터의 법칙은 객체의 내부 주고를 외부로 노출될때 문제가 되는 것이다.

 

결합도와 응집도의 충돌

묻지말고 시켜라와 디미터의 법칙을 준수하더라도 좋지않는 결과가 나올수 있다. 클래스는 하나의 변경 원인만을 가져야 한다.

Theater클래스에서 Audience객체의 getBag()을 통해서 구현되어있어, Theater가 Audienece의 내부 구조에 강하게 결합되어 있다. 이를 해결하기위해 질문하고, 판단하고, 상태를 변경하는 코드를 Audience로 옮기는 것이다. 이러면 Audience가 상태와 행동을 조작하므로 응집도가 높아졌다.

하지만 모든 상황에 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션들이 공존하게 된다.

 

Before

public class PeriodCondition implements DiscountCondition {
	public boolean isSatisifiedBy(Screening screening){
		return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
			startTime.compareTo(screening.getStartTime().toLocalTime()) ...;
}

 

After

public class Screening {
    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime startTime,LocalTime endTime){
        return whenScreened.getDayOfWeek().equals(dayOfWeek) &&
                startTime.compareTo(whenScreened.toLocalTime()) <= 0 ...
    }
}

public class PeriodCondition implements DiscountCondition {
    public boolean isSatisfiedBy(Screening screening){
        return screening.isDiscountable(dayOfWeek, startTime, endTime);
    }
}

screening이 기간에 따른 할인 조건을 판단하는 책임을 떠안게 된다. 이것은 책임의 본질이 아니다. Screening이 직접 할인 조건을 판단하게 되면 객체의 응집도가 낮아진다.

명령-쿼리 분리 원칙

절차를 묶어 호출 가능하도록 이름을 부여하는 기능 모듈을 루틴이라고 부른다. 루틴은 프로시저와 함수로 구분된다.

프로시저는 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류이다.(반환 x)

함수는 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류이다.(상태 변경 x)

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

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

명령과 쿼리를 분리해야 하는 이유는 실행 결과를 예측하기 쉽게 하기 위함이다.

도메인의 두 가지 중요한 용어인 이벤트와 반복 일정이 있다.

이벤트는 특정 일자에 실제로 발생하는 사건을 의미한다.

반복 일정은 일주일 단위로 돌아오는 특정 시간 간격에 발생하는 사건 전체를 포괄적으로 지칭하는 용어

이벤트가 발생하고 반복 일정에 포함되는지 확인하는 메서드가 있다고 가정하자. 이벤트에서 isSatisfiedBy 메서드를 호출하고 만족하면 스케줄을 변경하는 로직(reschedule)이 내부에 있다고 가정하면, 메서드를 두번 실행하면 값이 매번 바뀔 것이다.

만족하는지만 확인할 것같은 로직에서 정보 수정이 있다면 예측하기 어려워진 로직이 될것이다.

명령-쿼리 분리 원칙에 따라 수정하면 isSatisfied메서드는 부수효과를 가지지 않는 반환만 하는 메서드가 되고 reschedule 메서드는 반환 값을 돌려주지 않고 상태만 변경한다.

명령과 쿼리를 분리함으로써 명령형 언어의 틀 안에서 참조 투명성의 장점을 제한적으로나마 누릴 수 있다. 참조 투명성이란 “어떤 표현식 e가 있을때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성”

→ 어떤 표현식 e가 있을때 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성

ex) f(1) + f(1) = 6, f(1) * 2 = 6일때, f(1)을 3으로 교체하더라도 결과가 같아지는 특성

이러한 이유는 불변성때문이다.

객체지향프로그래밍은 객체의 상태 변경이라는 부수효과를 기반으로 하기 때문에 참조 투명성은 예외에 가깝다. 명령-쿼리 분리 원칙으로 부수효과를 가지는 명령으로부터 부수효과가 없는 쿼리를 분리함으로써 제한적이나마 참조 투명성의 혜택을 누릴수 있다.

책임 주도 설계에서는 객체가 메시지를 선택하는 것이 아닌 메시지가 객체를 선택하기 때문에 협력에 적합한 메시지를 결정할 확률이 높아진다. 협력에 적합한 객체가 아닌 메시지이다.

728x90
Comments