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

bien·2024년 10월 15일
0

오브젝트

목록 보기
6/13
  • 객체지향 프로그램의 흔한 오해; App은 클래스의 집합
    • 클래스에 집착 => 경직되고 비유연한 설계 유도
  • 훌륭향 객체지향 코드; 클래스❌ 객체(지향)⭕️
    • 정확히는, 객체가 수행하는 책임에 초점을 맞추는 것
      • 책임 = 객체가 수신할 메시지의 기반
  • App은 클래스로 구성되지만, 메시지를 통해 정의된다.
    • 객체지향 App의 주 재료; 클래스❌ 객체들간의 메시지⭕️
  • 객체가 수신하는 메시지 = 객체의 퍼블릭 인터페이스
    • 좋은, 유연하고 재설계 가능한 퍼블릭 인터페이스를 위한 원칙, 기법이 있음.

이번장에서 이 원칙, 기법들을 알아보자!


01. 협력과 메시지

클라이언트-서버 모델

  • 두 객체사이의 협럭; 클라이언트-서버(Client-Server) 모델에 비유 가능
    • 클라이언트; 협력 안에서 메시지를 전송하는 객체
    • 서버; 메시지를 수신하는 객체
    • 협력; 클라이언트가 서버의 서비스를 요청하는 단방향 상호작용

  • 객체는 협력에 참여하는 동안, 클라이언트와 서버 역할을 동시에 수행하는 것이 일반적이다.
    • 객체의 메시지 집합 = 2종류
      1. 객체가 수신하는 메시지 집합
      2. 외부의 객체에게 전송하는 메시지 집합.

  • 객체가 더 큰 책임을 수행하기 위해서는, 다른 객체와의 협력이 필요하다.
    • 이때, 두 객체 사이의 협력 매개체 = 메시지

메시지와 메시지 전송

  • 메시지(message); 객체들 간의 유일한 협력 수단
    • 메시지 전송(message sending) or 메시지 패싱(message passing)
      • 한 객체가 다른 객체에게 도움을 요청하는 것.
    • 메시지 전송자(message sender); 클라이언트
    • 메시지 수신자(message receiver); 서버
  • 메시지: 오퍼레이션명(operation name) + 인자(argument)
    • ex) isSatisfiedBy(screening)
  • 메시지 전송: 메시지 수신자 + 메시지(오퍼레이션명 + 인자)
    • ex) condition.isSatisfiedBy(screening)

메시지와 메서드

  • 메시지: 메서드를 수신했을 때 실제로 실행되는 함수 또는 프로시저
    • 메시지 수신 시, 실제로 실행되는 코드는, 메시지 수신자의 타입에 달려있다.
    • 객체는 메시지와 메서드를 실행 시점에 연결하므로, 컴팡리 시점과 의미가 달라질 수 있다.
  • 메시지와 메서드의 구분 = 메시지는 전송자와 수신자 간의 느슨한 결합 생성
    • 전송자와 수신자는 서로에 대한 어떤 정보도 알 필요가 없다. (단지, 메서드 송수신 여부만 알면 된다.)
      • 이 부분에서, 메시지 전송자와 수신자에게 스스로 결정할 자율권이 생기게 된다.

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

  • 퍼블릭 인터페이스
    • 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합
  • 오퍼레이션(Operation)
    1. 퍼블릭 인터페이스에 포함된 메시지
    2. 수행 가능한 어떤 행동에 대한 추상화
    3. (내부의 구현 코드는 제외한) 단순히 메시지와 관련된 시그니처
      • cf. 메서드; 메서디 수신 시 실제로 실행되는 코드

by. Larman04

  • 오퍼레이션
    • 실행하기 위해 객체가 호출될 수 있는 변환이나 정의에 관한 명세
    • 인터페이스의 각 요소
    • 구현이 아닌 추상화

  1. 객체가 다른 객체에게 메시지를 전송하면
  2. 런타임 시스템은 메세지 전송을 오퍼레이션 호출로 해석하고
  3. 메시지를 수신한 객체의 실제 타입을 기반으로 적절한 메서드를 찾아 실행한다.

※ 퍼블릭 인터페이스 관점에서 '메서드 호출'보다는 '오퍼레이션 호출'이라는 용어가 더 적절하다.

시그니처

  • 시그니처(signature) = 오퍼레이션(또는 메서드)의 이름 + 파라미터 목록
    • 오퍼레이션; 실행 코드 없이 시그니처만을 정의한 것
    • 메서드; 시그니처 + 구현
  • 하나의 오퍼레이션에 하나의 메서드 => 단순
    • 그러나 다형성을 위해서는, 오퍼레이션에 대해 다양한 메서드를 구현해야만 한다.

📌 용어정리

  • 메시지
    • 객체가 다른 객체와 협력하기 위해 사용하는 의사소통 메커니즘.
    • 일반적으로 객체의 오퍼레이션이 실행되도록 요청하는 것을 "메시지 전송"이라고 부른다.
    • 메시지는 협력에 참여하는 전송자와 수신자 양쪽 모두를 포함하는 개념이다.
  • 오퍼레이션
    • 객체가 다른 객체에게 제공하는 추상적인 서비스
    • 메시지를 수신하는 객체의 인터페이스를 강조
    • 메시지 전송자는 고려하지 않은 채, 메시지 수신자의 관점만을 다룬다.
    • 메시지 수신이란 메시지에 대응되는 객체의 오퍼레이션을 호출하는 것을 의미한다.
  • 메서드
    • 메시지에 응답하기 위해 실행되는 코드 블록
    • 오퍼레이션의 구현. 동일한 오퍼레이션이더라도 메서드는 다를 수 있다.
    • 오퍼레이션과 메서드의 구분은 다형성의 개념과 연결된다.
  • 퍼블릭 인터페이스
    • 객체가 협력에 참여하기 위해 외부에서 수신할 수 있는 메시지의 묶음
    • 클래스의 퍼블릭 메서드들의 집합이나 메시지의 집합을 기리키는 데 사용
    • 객체를 설계할 때 가장 중요한 것 = 훌륭한 퍼블릭 인터페이스를 설계하는 것.
  • 시그니처
    • 시그니처는 오퍼레이션이나 메서드의 명세를 나타낸 것.
    • 이름, 인자의 목록을 포함
    • 타입을 시그니처의 일부로 포함하는 언어도 존재.

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

  • 좋은 interface = 최소한의 인터페이스 + 추상적인 인터페이스
    • 최소한; 꼭 필요한 오퍼레이션만 인터페이스에 포함
    • 추상적; 어떻게❌ 무엇을 ⭕️ 표현
      • 책임 주도 설계를 통해 획득 가능

퍼블릭 인터페이스의 품질에 영향을 미치는 원칙(기법)

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

📗 디미터 법칙(Law of Demeter)

1. 의도

협력하는 객체의 내부 구조에 대한 결합으로 발생하는 설계 문제를 해결하기 위해 제안된 원칙

2. 정의

  1. 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하는 것
  2. 낯선 자에게 말하지 말라 (don't talk to strangers)[Larman04]
  3. 오직 인접한 이웃하고만 말하라 (only talk to your immediate neighbors)[Metz12]
  4. 오직 하나의 도트만 사용하라 (use only one dot)[Metz12]
    • 자바, C#와 같이 '도트(.)'를 이용해 메시지 전송을 표현하는 언어에서

3. 메시지 전송 조건

  1. 모든 클래스 C와 C에 구현된 모든 메서드 M에 대해, M이 메시지 전송할 수 있는 모든 객체는, 다음에 서술된 클래스의 인스턴스여야 한다.
    • M의 인자로 전달된 클래스 (C 자체를 포함)
      • M의 인자;
        • M에 의해 생성된 객체
        • M이 호출하는 메서드에 의해 생성된 객체
        • 전역 변수로 선언된 객체
    • C의 인스턴스 변수의 클래스
  2. 클래스 내부의 메서드가 아래 조건을 만족하는 인스턴스에만 메시지를 전송하도록 프로그래밍해야 한다.
    • this 객체
    • 메서드의 매개변수
    • this의 속성
    • this의 속성인 컬렉션의 요소
    • 메서드 내에서 생성된 지역 객체

4. 이점

  • 부끄럼타는 코드(shy code)
    • 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드.
    • 메시지 수신자의 내부 구조가 전송자에게 노출되지 않으며, 메시지 전송자는 수신자의 내부 구현에 결합되지 않는다.
      • 따라서 클라이언트와 서버 사이에 낮은 결합도를 유지할 수 있다.

5. 디미터 법칙을 준수한 예시

public class ReservationAgency {
	public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
   		Money fee = screening.calculateFee(audienceCount);
        return new Reservation(customer, screening, fee, audienceCount);
      }
    
  • (내부의 다른 인스턴스가 아니라) Screening 인스턴스에게만 메시지 전송
    • 내부 구조와 결합되지 않아 변경의 여파가 외부로 전파되지 않음

디미터 법칙과 캡슐화

  • 디미터 법칙; 캡슐화의 또 다른 관점
  • 캡술화와 디미터 법칙; 클래스의 캡슐화를 위한 구체적 지침 제공
    • 캡슐화; 내부 구현의 은폐
    • 디미터의 법칙; (협력간의 캡슐화 보존을 위해) 외부에서 접근하는 요소를 제한
  • 디미터의 법칙; 협력과 구현의 유기적 통합
    • 내부구현을 채우는 동시에,
    • 현재 협력하는 클래스에 관해서도 고민하도록 주의를 환기.

묻지말고 시켜라

  • 디미터의 법칙에 의해 강조되는 개념
    • 훌륭한 메시지는 "객체의 상태를 묻지말고""원하는 것을 시켜야 한다."⭕️

상태를 묻지말고 시켜라

  • (자신 내부의) 상태 변경은 수신자의 책임이다.
    • 외부에서 메서드를 통해 상태를 묻고 이 값을 기반으로 결정한다면, 객체의 책임이 외부로 누수된 것이다.
  • 객체의 정보를 이용하는 행동을 객체 내부에 위치시킨다.
    • 정보 전문가(연관된 정보와 행동을 같이 가지는 객체)에게 책임이 할당되어 응집도를 높일 수 있다.
  • 오퍼레이션에 대한 힌트
    • 상태를 묻는 오퍼레이션을 행동을 묻는 오퍼레이션으로 대체해라.

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

  • 2가지 메서드 명명규칙 (by. 켄트 벡(kent beck))
    1. 메서드가 어떻게 작업을 수행하는지 나타내도록 명명
    2. 무엇을 할지 드러나게 메서드 이름 결정

1. 메서드가 어떻게 작업을 수행하는지 나타나도록 명명

  • 이름에 내부 구현이 노출된다.
  • 단점1) 두 메서드의 이름이 달라, 내부 구현을 이해하지 못하면 두 메서드가 같은 기능을 수행한다는 점을 파악하기 어렵다.
  • 단점2) 메서드 수준에서의 캡슐화 위반
    • 클라이언트가 협력하는 객체의 종류를 알아야 한다.
    • 책임을 수행하는 방법이 메서드명에 들어나고, 메서드 이름은 클라이언트에게 노출되므로, 로직변화 시 모든 메서드 호출부에 변경이 있어야 한다.
publc class PeriodCondition {
	public boolean isSatisfiedByPeriod(Screening screening) {...}
}

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

2. 무엇을 할지 드러나게 메서드 이름 결정

  • cf. 어떻게 수행하는지를 보여주는 메서드 명명 방식; 메서드의 내부 구현을 설명
    • 내부 구현현을 협력 설계 초기에 너무 이르게 고민하게 한다.
  • 무엇을 하는지를 드러내는 메서드 명명 방식; 객체가 협력안에서 수행해야 할 책임을 설명
    • 메서드 전송 목적을 먼저 고려한다.
    • 따라서, 클라이언트의 의도에 부합하는 메서드명을 선정할 수 있다.
  • 무엇을 하는지에 초점을 맞추면, 클라이언트의 관점에서 동일한 작업을 수행하는 메서드들을 하나의 타입 계층으로 묶을 수 있는 가능성이 커진다.
    • 이를 기반으로, 다양한 객체가 참여하는 유연한 협력을 얻을 수 있다.
public interface DiscountCondition {
	boolean isSatisfiedBy(Screening screening);
}

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

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

의도를 드러내는 선택자(Intention Revealing Selector)

  • 무엇을 하느냐에 따라 메서드의 이름을 짓는 패턴
  • 메서드의 의도를 드러내는 명명을 위한 조언
    • 하나의 구현을 가진 메서드의 이름을 일반화하는 방법;
      • 매우 다른 두번째 구현을 상상하고, 두 메서드에 동일한 이름을 부여해보는 것.
      • 가장 추상적인 이름을 획득할 수 있다.

의도를 드러내는 인터페이스(Intention Revealing Interface)

  • 의도를 드러내는 선택자를 인터페이스 레벨로 확장한 것.
  • 구현과 관련된 모든 정보를 캡슐화하고, 객체의 퍼블릭 인터페이스에는 협력과 관련된 의도만을 표현하는 것.

켄트 벡은 메서드의 목적을 효과적으로 전달하고자 의도를 드러내는 선택자를 사용해 메서드의 이름을 짓는 것에 관해 글을 쓴 적이 있다. 설계에 포함된 모든 공개 요소가 조화를 이뤄 인터페이스를 구성하고, 인터페이스를 구성하는 각 요소의 이름을 토대로 설계 의도를 드러낼 수 있는 기회를 얻게 된다. 타입 이름, 메서드 이름, 인자 이름이 모두 결합되어 의도를 드러내는 인터페이스를 형성한다. 그러므로 수행 방법에 관해서는 언급하지 말고 결과와 목적만을 포함하도록 클래스와 오퍼레이션의 이름을 부여하라. 이렇게 하면 클라이언트 개발자가 내부를 이해해야 할 필요성이 줄어든다.

방법이 아닌 의도를 표현하는 추상적인 인터페이스 뒤로 모든 까다로운 매커니즘을 캡슐화해야 한다. 도메인의 퍼블릭 인터페이스에서는 관계와 규칙을 시행하는 방법이 아닌 이벤트와 규칙 그 자체만을 명시한다.

방정식을 푸는 방법을 제시하지말고 이를 공식으로 표현해라. 문제를 내라. 하지만 문제를 푸는 방법을 표현해서는 안된다.[Evans03].

📌 결론

  • 객체에게 묻지말고 시키되, 구현 방법이 아닌 클라이언트의 의도를 드러내야 한다.
    • 이해하기 쉽고 유연하며, 동시에 협력적인 객체를 만드는 초석

💻 코드 예시

디미터 법칙을 위반하는 코드 예시

  • Theater가 인자로 전달된 audience와 인스턴스 변수인 ticketSeller외에도, 내부에 포함된 객체에게 직접 접근하고 있다.
  • 근본적으로 디미터의 법칙을 위반하는 설계는 인터페이스와 구현의 분리 원칙을 위반한다.
    • 객체의 내부 구조는 구현에 해당한다.
Ticket ticket = ticketSeller.getTicketOffice().getTicet();
audience.getBag().minusAmount(ticket.getFee());

디미터 법칙을 위반한 코드를 수정하는 일반적인 방법은, 내부 구조를 묻는 것이 아니라, 직접 자신의 책임을 수행하도록 시키는 것이다.

Theater.java; 디미터 법칙 위반 예시 전체코드

public lass 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 {
        	Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().tetTicket(ticket);
        }
    }
}

묻지 말고 시켜라 적용

내부 구조를 묻는 것이 아니라 원하는 작업을 시켜야 한다.
AudienceTicketSeller가 내부 구조에 관해 묻지 말고 시켜라 스타일을 따르는 퍼블릭 인터페이스를 갖도록 수정하자.

Theater.java

  • Theater(클라이언트)가 TicketSeller에게 시키고 싶은 일
    • AudienceTicket을 가지도록 만드는 것.
public class TicketSeller {
	public void setTicket(Audience audience) {
    	if (audience.getBag().hasInvitation()) {
        	Ticket ticket = ticketOffice.getTicket();
            audience.getBag().setTicket(ticket);
        } else {
        	Ticket ticket = ticketOffice.getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketOffice.plusAmount(ticket.getFee());
        }
	}
}
// Theater가 디미터의 법칙을 준수하도록 수정
public class Theater {
	public void enter(Audience audience) {
    	ticketSeller.setTicket(audience);
    }
}

Audience.java

  • TicketSeller(클라이언트)가 Audience에게 시키고 싶은 일
    • 스스로 Ticket을 보유하는 것.
public class Audience {
	public Long setTicket(Ticket ticket) {
    	if (bag.hasInvitation()) {
        	bag.setTicket(ticket);
            return 0L;
        } else {
        	bag.setTicket(ticket);
            bag.minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }
}
// TicketSeller가 디미터의 법칙을 준수하도록 수정
public class TicketSeller {
	public void setTicket(Audience audience) {
    	ticketOffice.plusAmount {
        	audience.setTicket(ticketOffice.getTicket());
        }
    }
}

Bag.java

  • Audience(클라이언트)가 hasInvitation을 이용해 Bag의 상태를 확인하고 있다.
    • Bag이 스스로 상태를 확인하고 행동하도록, 관련 로직을 Bag 내부로 이동시키자.
public class Bag {
	public Long setTicket(Ticket ticket) {
    	if (hasInvation()) {
        	this.ticket = ticket;
            return 0L;
        } else {
        	this.ticket = ticket;
            minsuAmount(ticket.getFee());
            return ticket.getFee();
        }
    }

    private boolean hasInvation() {
        return invation != null;
    }

    private void minusAmount(Long amount) {
        this.amoun -= amount;
    }
}
// Audience가 디미터의 법칙을 준수하도록 수정
public class Audience {
	public Long setTicket(Ticket ticket) {
    	return bag.setTicket(ticket);
    }
}

디미터의 법칙 + 묻지말고 시켜라 스타일 준수

  • 자율적인 객체로 구성된 유연한 협력을 얻을 수 있다.
  • 구현이 객체의 퍼블릭 인터페이스에 노출되지 않는다.
    • 객체 사이의 결합도가 낮아진다.
  • 책임이 잘못된 곳에 할당될 가능성이 낮아진다.
    • 객체의 응집도가 높아진다.

📕 인터페이스에 의도를 드러내자 적용

응용이 어렵게 느껴지는 부분. 어떤식으로 의도를 드러내야 하는지, 예시를 통한 학습 필요.

  • 인터페이스가 클라이언트의 의도를 명확하게 드러내는가?
    • TicketSellersetTicket은 클라이언트의 의도를 명확하게 전달하는가?
    • AudiencesetTicket 메서드의 의도는 무엇인가?
    • BagsetTicket메서드는, 앞의 두 메서드와 동일한 의도인가?
  • 메서드를 직접 개발한 개발자는 이 새 메서드의 미묘한 차이점을 정확하게 인지한다.
    • 그러나 클라이언트 개발자는 이를 파악하기 쉽지 않다.
      • 미묘하게 다른 의미를 가진 3개의 메서드가 같은 이름을 가지고 있어 클라이언트 개발자를 혼란스럽게 만들 확률이 높다.
  • 클라이언트의 의도가 명확히 드러나도록 객체의 인터페이스를 개선해야 한다.
    • TicketSellersetTicket();
      • Theater가 TicketSeller에게 요청하고자 하는 것; Audience에게 티켓 판매
        • => sellTo()로 변경
    • AudiencsetTicket();
      • TicketSeller가 Audience에게 요청하고자 하는 것; Audience가 티켓을 사는 것.
        • => buy()로 변경
    • BagsetTicket();
      • Audience가 Bag에게 요청하고자 하는 것; Bag이 티켓을 보관하는 것.
        • => hold()로 변경
public class TicketSeller {
	public void sellTo(Audience audience) {...}
}

public class Audience {
	public Long buy(Ticket ticket) {...}
}

public class Bag {
	public Long hold(Ticket ticket) {...}
}
  • 오퍼레이션의 이름을 짓는 법
    • 오퍼레이션의 이름은 협력이라는 문맥을 반영해야 한다.
    • 오퍼레이션은 클라이언트가 객체에게 무엇을 원하는지를 표현해야 한다.

📌 결론

  • 디미터의 법칙 ; 객체간의 협력 시 캡슐화를 위반하는 메시지가 인터페이스에 포함되지 않도록 제한한다.
  • 묻지말고 시켜라 원칙; 디미터 법칙을 준수하는 협력을 위한 스타일을 제시
  • 의도를 드러내는 인터페이스; 코드의 목적을 명확하게 커뮤니케이션 할 수 있게 한다.
    • 결과; 낮은 결합도 + 의도가 명확한 간결한 협력

03. 원칙의 함정

소프트웨어 설계에 절대적인 법칙은 없다.
설계는 트레이드 오프의 산물이므로, 유연한 적용에 대한 판단력이 필요하다.

소프트웨어 개발자는 원칙을 이해하고 따르되, 상황에 맞게 유연하게 적용할 줄 알아야한다.
원칙이 현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시해라.
원칙을 아는 것보다 더 중요한 것은 언제 원칙이 유용하고 언제 유용하지 않은지 판단할 수 있는 능력을 기르는 것이다.

원칙 적용 시 고려해야 할 예외 상황들을 알아보자!

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

IntStream.of(1, 15, 20, 3, 9).filter(x -> x > 10).distinct().count();
  • 이 코드는 기차 충돌이 아니고, 디미터의 법칙을 어기는 코드도 아니다.
    • of, filter, distinct 메서드는 객체를 다른 객체로 변환시키면서 IntStream이라는 동일한 인스턴스를 반환하고 있다.
      • 따라서, 객체의 내부 구조가 외부로 노출되지 않고 있다.
  • 디미터의 법칙결합도와 관련된 것으로, 객체의 내부 구조 노출 여부가 핵심이다.
    • 객체의 내부 구현이 외부로 노출되지 않는다면, 그것은 디미터 법칙을 준수한 것이다.

⭐️ 결합도와 응집도의 충돌

  • 묻지말고 시켜라 & 디미터의 법칙의 무조건적인 준수
    • => 모든 상황에서의 맹목적인 위임 메서드 추가를 야기
    • => 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션이 공존
      • => 객체는 상관없는 책임을 한꺼번에 떠안게 되어, 응집도가 낮아진다.
  • 클래스는 하나의 변경 원인만을 가져야 한다.
    • 상관없는 책임이 함께 뭉쳐있는 클래스는 응집도가 낮으며, 작은 변경에도 위태롭다.
    • 과잉된 디미터의 법칙묻지말고 시켜라 원칙의 준수는 낮은 응집도를 초래한다.

PeriodCondition.java; 수정 전 ⭕️

public class PeriodCondition implements DisocuntCondition {
	public boolean isStatisfiedBy(Screeing screening) {
    	return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
        	startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
            endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
  • Screening의 내부 상태를 가져와 사용; 캡슐화 위반으로 보일 수 있다.
    • 이 부분을 개선하기 위해, 할인 여부 판단 로직을 Screening으로 이동 & PeriodConditino에서 호출하도록 변경해보자!
      • 이는 묻지말고 시켜라 법칙의 준수로 여겨질 수 있다.

Screening.java; 수정 후 ❌

public class Screening {
	public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
    	return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
        	startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
            endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
    
  • Screening이 기간에 따른 할인 조건을 판단하는 책임을 떠안게 된다.
    • 본질적인 책임인 영화 예매가 아닌 다른 책임을 담당응집도가 낮아진다.
    • 본래 할인 조건 판단 책임은 PeriodCondition의 것이다.
  • 동시에, ScreeningPeriodCondition 인스턴스 변수를 인자로 받아, 둘 사이의 결합도는 높아진다.

개발하다 보면 원칙 주순에 대한 딜레마를 경험할 때가 많다.
중요한 건 원칙을 맹목적으로 따르는 것이 아니라, 클래스의 응집도에 초점을 맞춰야 한다는 조언을 얻을 수 있었다.

묻는 것이 유일한 방법인 경우가 있다.

  • 컬렉션에 포함된 객체들을 처리하는 유일한 방법은 객체에게 물어보는 것이다.
    • 컬렉션; 여러 객체를 모아서 하나의 그룹으로 관리할 수 있는 데이터 구조.
  • 아래 코드에서 Movie에게 묻지 않고 movies 컬렉션의 전체 영화 가격을 계산할 방법이 있을까?
for (Movie each: movies) {
	total += each.getFee();
}
  • 물으려는 대상이 자료구조인 경우, 당연히 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다.

04. 명령-쿼리 분리 원칙

  • 루틴(routine); 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈
    • 프로시저(procedure); 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류
      • 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
      • 명령(Command); 객체의 상태를 수정하는 오퍼레이션 (개념적으로 프로시저와 동일)
    • 함수(function); 어떤 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류
      • 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
      • 쿼리(Quenry); 객체와 관련된 정보를 반환하는 오퍼레이션 (개념적으로 함수와 동일)

명령-쿼리 분리(Command-Query Separation) 원칙

  • 명령 & 쿼리; 객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또 다른 이름
  • 오퍼레이션은 부수효과를 발생시키는 명령이거나 부수효과를 발생시키지 않는 쿼리여야 한다.
    • 규칙1. 객체의 상태를 변경하는 명령은 반환값을 가질 수 없다.
    • 규칙2. 객체의 정보를 반환하는 쿼리는 상태를 변경할 수 없다.
  • "질문이 답변을 수정해서는 안 된다."
  • 부수효과를 발생시키지 않는 것 만을 함수(쿼리)로 제한
    • 객체의 캡슐화와 다양한 문맥에서의 재사용성을 보장.
    • 이처럼 명령-쿼리 분리원칙에 따라 작성된 객체의 인터페이스를 명령-쿼리 인터페이스(Command-Query Inteface)라 부른다.

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

Event.java

public class Event {
	public boolean isStaitsfied(RecurringSchedule schedule) {
    	if (from.getDayOfWeek() != schedule.getDayOfWeek() ||
        		!from.toLocalTime().equals(schjedule.getFrom()) ||
            	!duration.equals(schedule.getDuration())) {
           reschedule(schedule);
           return false;
        }
        
        return true;
    }
    
    private void reschedule(RecurringSchedule schedule) {
    	from = LocalDateTime.of(from.toLocalDate().plusDays(dayDistance(schedule)),
        	schedule.getFrom());
        duration = schedule.getDuration();
    }
    
    private long dayDistance(RecurringSchedule schedule) {
    	return schedule.getDayOfWeek().getValue() - from.getDayOfWeek().getValue();
    }
}
  • 예시에서 reschedule 메서드는 Event의 일정을 인자로 전달된 RecurringSchedule의 조건에 맞게 변경한다.
    • 따라서 rechedule를 호출하는 isSatisfied메서드는 Event가 조건을 만족하지 못하는 경우, Event상태를 조건을 만족시키도록 변경한 후 false를 반환한다.
  • 예시에서 isSatisfied는 명령과 쿼리, 2가지 역할을 동시에 수행하고 있다.
    • isSatisfied는 조건의 부합 여부를 판단한 후 부합할 경우 true를, 부합하지 않는 경우 flase를 반환한다.
      • 따라서 개념적으로 쿼리다.
    • isSatisfied는 조건에 부합하지 않을 경우 Event의 상태를 조건에 부합하도록 변경한다.
      • 실제로는 부수효과를 가지는 명령이다.

💣 발생 가능한 버그 상황

  • 대부분의 사람들은 isSatisfied부수효과를 가질 것으로 예상하지 못할 것이다.
  • isSatisfied가 처음 구현되던 당시, reschedule의 호출 부분이 빠져 있었다.
    • 기능 추가 과정에서 Event가 조건이 맞지 않는 경우 Event의 상태를 수정해야 한다는 요구사항이 있었고, 개발자는 별다른 고민 없이 기존의 isSatisfiedreschedule를 호출하는 코드를 추가한 것이다.
  • 명령과 쿼리를 뒤섞으면 실행결과를 예측하기 어렵다.
    • isSatisfied; 겉으로 보기에 쿼리처럼 보이지만, 내부적으로 부수효과를 가지는 메서드
      • 이해하기 어렵고, 잘못사용하기 쉬우며, 버그를 양산하는 경향이 있다.

💡개선된 코드; 명령-쿼리 분리

Event.java

public class Event {
    public boolean isSatisfied(RecurringSchedule schedule) {
        if (from.getDayOfWeek() != schedule.getDayOfWeek() ||
                !from.toLocalTime().equals(schjedule.getFrom()) ||
                !duration.equals(schedule.getDuration())) {
            return false;
        }

        return true;
    }
    
    public void reschedule(RecurringSchedule schedule) {
    	from = LocalDateTime.of(from.toLocalDate().plusDays(dayDistance(schedule)),
        	schedule.getFrom());
        duration = schedule.getDuration();
    }
}
public class Event {
	public boolean isSatisfied(RecurringSchedule schedule) {...}
    public void reschedule(RecurringSchedule schedule) {...}
}
  • 반환값의 여부명령인지 쿼리인지 한눈에 파악할 수 있다.
    • isSatisfied는 리턴값을 가지는 쿼리이므로, 호출 시 부수효과를 걱정하지 않아도 된다.
    • reschedule은 리턴값을 가지지 않는 명령이므로, 부수효과에 주의해야 한다.
  • reschedule의 가시성이 private에서 public으로 변경됐다.
    • isSatisfied 안에서 수행했던 명령을 클라이언트가 직접 실행할 수 있게 하기 위해서다.
    • Event가 조건을 만족시키지 않을 경우, reschedule메서드 호출 여부를 Event를 사용하는 곳에서 결정할 수 있다.
if (!event.isSatisfied(schedule)) {
	event.reschedule(schedule);
}

📌 결과

  • 퍼블릭 인터페이스가 복잡해진 것 처럼 보이지만, 명령-쿼리로 얻을 수 있는 이점이 더 크다.
    • 코드가 예측 가능하고, 이해하기 쉬우며, 디버깅이 용이한 동시에, 유지보수가 수월해진다.

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

  • 쿼리; 객체의 상태를 변경하지 않음.
    • 호출의 순서, 횟수에 제약이 없어진다.
  • 컴퓨터의 세계의 큰 특징은 부수효과(side effect)를 가진다는 점이다.
    • 부수효과를 발생시키는 2가지 대표적인 문법
      • 대입문; =를 통한 값 변경
      • 함수; 동일한 인자를 전달하더라도, 부수효과에 의해 그 값이 매변 변경될 수 있다.
  • 참조 투명성
    • 어떤 표현식 e가 있을 때 모든 e를 e의 값으로 바꾸더라도 결과가 달라지지 않는 특성
      • ex) f(1) + f(1) = 6 -> f(1) = 3
    • 불변성(immutability); 어떤 값이 변하지 않는 성질
      • = 부수효과가 발생하지 않는다.
  • 참조 투명성이 제공하는 장점
    • 모든 함수를 이미 알고 있는 하나의 결괏값으로 대체할 수 있기 때문에 식을 쉽게 계산할 수 있다.
    • 모든 곳에서 함수의 결괏값이 동일하기 때문에 식의 순서를 변경하더라도 각 식의 결과는 달라지지 않는다.
  • 객체지향 패러다임은 부수효과를 기반으로 하므로 참조 투명성을 접하기 어렵다.
    • 그러나 명령-쿼리 원칙의 준수는, 부수효과를 가지지 않는 쿼리와 그 반대의 명령을 명백하게 분리함으로써, 제한적으로나마 참조 투명성의 혜택을 누릴 수 있게 돕는다.

명령형 프로그램과 함수형 프로그래밍

  • 명령형 프로그래밍(imperative programming)
    • 부수효과를 기반으로 하는 프로그래밍 방식
    • 상태를 변경시키는 연산들을 적절한 순서대로 나열함으로써 프로그램을 작성들을 적절한 순서대로 나열함으로써 프로그램을 작성
  • 함수형 프로그래밍(functional programming)
    • 부수효과가 존재하지 않는 수학적인 함수에 기반
    • 참조 투명성의 장점을 극대화 + 실행 결과 이해 및 예측이 쉽다.
    • 하드웨어의 발달로 병렬 처리가 중요해져, 최근에는 수요가 증가 중.

책임에 초점을 맞춰라

  • 책임 주도 설계
    • 메시지를 선택하고, 그 후에 메시지를 처리할 객체를 선택하는 방식
    • 앞서 언급한 원칙들의 근본적인 기틀이자 핵심 요인
      • 디미터 법칙; 객체보다 메시지를 먼저 고민하고 이후에 적절한 객체의 선정을 결정하므로, 의도적으로 디미터 법칙 위반을 최소화한다.
      • 묻지말고 시켜라; 클라이언트 관점에서 메시지를 선택하므로, 필요한 정보를 물을 필요 없이 원하는 것을 표현한 메시지를 전송할 수 있다.
      • 의도를 드러내는 인터페이스; 메시지를 먼저 선택하는 것은 클라이언트 관점에서 메시지 이름을 정하는 것으로, 그 의도가 분명히 드러날 수 밖에 없다.
      • 명령-쿼리 분리 원칙; 예측 가능한 협력을 만들기 위해 명려과 쿼리 분리를 시도할 확률이 높다.
  • 시그니처는 실행 시점에 보장되어야 하는 제약을 명시할 방법이 존재하지 않는다.
    • 이 경우 계약에 의한 설계(Design By Contract)를 사용 할 수 있다. (이후 장에서 다룸)

Reference

  • 오브젝트 | 조영호
profile
Good Luck!

0개의 댓글