CH6. 메시지와 인터페이스

의진·2024년 7월 17일

[Book] 오브젝트

목록 보기
3/4


앞서 객체지향 코드를 얻기 위해서는 객체가 협력 안에서 수행하는 책임에 초점 맞춰야 된다고 이야기 했다.
그리고 책임은 객체가 수신할 수 있는 메시지에 기반이 된다.


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

  • 협력은 어떤 객체(클라이언트)가 다른 객체(서버)에게 무언가를 요청할 때 시작된다
  • 협력은 클라이언트가 서버의 서비스를 요청하는 단방향 상호작용이다
  • 객체는 협력에 참여하는 동안 클라이언트와 서버의 역할을 동시에 수행하는 것이 일반적이다
  • 여기서 메시지는 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단이다

❓ 메시지

✔️ 메시지: 오퍼레이션명 + 인자
✔️ 메시지 전송: 메시지(오퍼레이션명+인자) + 메시지 수신자
✔️ 오퍼레이션: 객체가 다른 객체에게 제공하는 추상적인 서비스 (퍼블릭 인터페이스에 포함된 메시지)
✔️ 메서드: 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저 (오퍼레이션에 대한 구현)
✔️ 시그니처: 오퍼레이션명 + 파라미터 목록

🌟 코드 상에서 동일한 이름의 변수에게 동일한 메시지를 전송하더라도 객체의 타입에 따라 실행되는 메서드가 달라질 수 있다 🌟


좋은 인터페이스를 만드는 방법

1️⃣ 디미터 법칙: 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라

  • "오직 하나의 도트만 사용하라"라는 말로 요약되기도 한다
  • 객체의 상태에 관해 묻지 말고 원하는 것을 시켜야 한다는 사실을 강조하는 법칙
  • this 객체 / this 속성 / this 속성의 컬렉션 요소 / 메서드의 매개변수 / 메서드 내에서 생성된 지역 객체에게만 메시지를 전송하라!
  • 디미터 법칙 ❌
    screeing.getMovie().getDiscountConditions(); // 기차 충돌
  • 디미터 법칙 ⭕️
    screeing.calculateFee(audienceCount); // 객체의 응집도 ↑

✋🏻 But 무비판적으로 디미터 법칙을 수용하면 오히려 객체의 응집도가 낮아질 수 있으니 주의!

  • 기차 충돌처럼 보이더라도 객체의 내부 구현에 대한 어떤 정보도 외부로 노출하지 않는다면 그것은 디미터 법칙을 준수한 것이다
  • 객체는 내부 구조를 숨겨야 하므로 디미터 법칙을 따르는 것이 좋지만, 자료 구조라면 당연히 내부 구조를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다
    IntStream.of(1, 15, 20, 3, 9).filter(x -> x >. 0).distinct().count();

2️⃣ 묻지 말고 시켜라

  • 상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라
  • 디미터 법칙이 공식적인 방법론이라면, "묻지 말고 시켜라"는일반적인 스타일이다 (비슷)

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

public class PeriodCondition implements DiscountCondition {
	public boolean isSatisfiedBy(Screening screening) {
    	// getter()로 인해 "묻지 말고 시켜라" 법칙을 위반한 것처럼 보이는 expression 
    	return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
        	startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
            endTime.compareTo(screening.getEndTime().toLocalTime()); 
    }
}

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

// Screening이 할인 조건을 판단하는 책임을 떠안게 됨!
// PeriodCondition의 내부 상태(ex. dayOfWeek)가 Screening에게 노출됨!

3️⃣ '어떻게'가 아닌 '무엇'을 하는지 드러내는 메서드명을 지어라

  • "수행 방법에 관해서는 언급하지 말고 목적만을 포함하도록 클래스와 인터페이스의 이름을 부여하라"

  • 오퍼레이션의 이름은 협력이라는 문맥을 반영해야 한다 (객체 자신이 아닌 클라이언트의 의도를 표현하는 이름을 가져야 한다)

    setTicket(Ticket ticket)sellTo(Audience audience) / buy(Ticket ticket) / hold(Ticket ticket)
  • 오퍼레이션의 시그니처에는 어떤 조건이 만족되어야만 오퍼레이션을 호출할 수 있고, 어떤 경우에 결과를 반환받을 수 없는지를 표현할 수 없다!

  • 의도를 들어내는 인터페이스 ❌

    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) { ... }
    }
  • 메서드가 무엇을 하는지에 초점을 맞추면 클라이언트의 관점에서 동일한 작업을 수행하는 메서드들을 하나의 타입 계층으로 묶을 수 있는 가능성이 커진다 (가독성 ↑ 유연성 ↑)


4️⃣ 명령과 쿼리를 분리해라

📖 잠깐! 용어 정리
✔️ 루틴: 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈
✔️ 프로시저: 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류 (부수효과 ⭕️ / 반환 값 ❌)
✔️ 함수: 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류 (부수효과 ❌ / 반환 값 ⭕️)
✔️ 참조 투명성: 어떤 표현식 e(f(1) = 3)가 있을 때, 모든 e(f(1))를 e의 값(3)으로 바꾸더라도 결과가 달라지지 않는 특성 (불변성)

  • 명령은 객체의 상태를 수정하는 오퍼레이션 (객체의 인터페이스 측면에서의 프로시저)
  • 쿼리는 객체와 관련된 정보를 반환하는 오퍼레이션 (객체의 인터페이스 측면에서의 함수)
  • 명령과 쿼리를 분리하면 코드가 예측 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해진다👍🏻
  • 명령과 쿼리를 분리하면 제한적으로나마 참조 투명성의 혜택을 누릴 수 있게 된다👍🏻

💬 원칙이 현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시하라

  • 소프트웨어 설계에서 법칙이란 존재하지 않는다
  • 원칙을 아는 것보다 더 중요한 것은 언제 원칙이 유용하고 언제 유용하지 않은지를 판단할 수 있는 능력을 기르는 것이다
  • ✋🏻 But 부분을 잘 보자!

💬 훌륭한 메시지를 얻기 위한 출발점은 책임 주도 설계를 따르는 것이다!

  • 책임 주도 설계는 객체가 메시지를 결정하는 것이 아니라 메시지가 객체를 선택하기 때문에 협력에 적합한 메시지를 결정할 수 있는 확률이 높아진다!
profile
📖 오브젝트

0개의 댓글