[자라기] - 책임 주도 설계를 따르는 방법

김주형·2022년 11월 14일
0

자라기

목록 보기
6/22
post-thumbnail

🙇🏻‍♂️ Reference


좋은 인터페이스 - 최소한, 추상적인 인터페이스

좋은 인터페이스란 무엇일까?

  • "최소한의 인터페이스""와 "추상적인 인터페이스"라는 조건을 만족해야 한다고 한다.
  • 최소한의 인터페이스 : 꼭 필요한 오퍼레이션만을 인터페이스에 포함한다.
  • 추상적인 인터페이스 : 어떻게 수행하는지가 아니라 무엇을 하는지를 표현한다.

최소주의를 따르면서도 추상적인 인터페이스를 설계할 수 있는 가장 좋은 방법은 책임 주도 설계를 따르는 것이라고 한다.

메시지를 먼저 선택 -> 협력과는 무관한 오퍼레이션이 인터페이스에 스며 드는 것을 방지 -> 인터페이스가 최소한의 오퍼레이션만을 포함, 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선태감으로써 클라이언트의 의도를 메시지에 표현

메시지 : 객체가 다른 객체와 협력하기 위해 사용하는 의사소통 개념
오퍼레이션 : 메시지를 수신하는 객체 위주의 인터페이스 개념


디미터 법칙

Low of Demeter

  • 협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙이다.
  • 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 의미
  • "낯선 자에게 말하지 마라"
  • "오직 인적한 이웃하고만 말하라."
  • "오직 하나의 도트만 사용하라"

객체들의 협력 경로를 제한하면 결합도를 효과적으로 낮출 수 있다!
클래스가 특정한 조건만을 만족하는 대상에게만 메시지를 전송하도록 프로그래밍해야 한다.

클래스 내부의 메서드가 아래 조건을 만족하는 인스턴스에만 메시지를 전송하도록 프로그래밍해야 한다라고 이해해도 무방하다고 하다.

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

무슨 뜻일까?

예시 - 오브젝트 4장
영화 예매 시스템 중
할인 가능 여부 체크하는 코드

public class ReservationAgency {

	public Reservation reserve(Screening screening,
    Customer customer, int audienceCount) {
    
    Movie movie = screening.getMovie();
    
    boolean discountable = false;
    for(DiscountCondition condition : movie.getDiscountConditions()) {
    	if (condition.getType() == DiscountConditionType.PERIOD) {
        discountable = screening.getWhenScreened().getDayOfWeek(). equals(condition.getDayOfWeek()) && condotion.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
        condition.getEndTime().compareTo(screening,getWhenScreened().toLocalTime()) >= 0;
        } else {
        discountable = condition.getSequence() == screening.getSequence();
        }
        if (discountable) {
        break;
        }
	}
    ..
}

문제점 : ReservationAgency와 인자로 전달된 Screening 사이 결합도

  • Screening의 내부 구현을 변경할 때마다 ReservationAgency도 함께 변경된다.
  • ReservationAgency가 Screening뿐만 아니라 Movie와 DiscountCondition에도 직접 접근하기 때문이다.
  • Screening이 Movie를 포함하지 않는다면?
  • Movie가 DiscountCondition을 포함하지 않게 된다면?

수정 - 부끄럼타는 코드 (shy code)

  • 디미터 법칙을 따르면 부끄럼타는 코드를 작성할 수 있다.
  • 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드를 말한다.
  • 디미터 법칙을 따르는 코드는 메시지 수신자의 내부 구조가 전송자에게 노출되지 않으며, 메시지 전송자는 수신자의 내부 구현에 결합되지 않는다.
  • 클라이언트 / 서버 사이에 낮은 결합도를 유지할 수 있다.
public class ReservationAgency {
	public Reservation reserve(Screening screening,
    Customer customer, int audienceCount) {
    
    Money fee = screening.calculateFee(audienceCount);
    
    return new Reservation(customer, screening, fee, audienceCount);
    }
}
  • ReservationAgency는 메서드의 인자로 전달된 Screening 인스턴스에게만 메시지를 전송한다.
  • ReservationAgency는 Screening 내부에 대한 어떤 정보도 알지 못한다.
  • ReservationAgency가 Screening의 내부 구조에 결합돼 있지 않기 때문에 Screening의 내부 구현을 변경할 때 ReservationAgency를 함께 변경할 필요가 없다.

기차 충돌

디미터 법칙이 가치 있는 이유는 클래스를 캡슐화하기 위해 따라야하는 구체적인 지침을 제공하기 때문이다.
협력과 구현이라는 두 가지 문맥을 하나의 유기적인 개념으로 통합한다.
클래스의 내부 구현을 채워가는 동시에 현재 협력하고 있는 클래스에 관해서도 고민하도록 주의를 환기시키기 때문이다.


screening.getMovie().getDiscountConditions(); // 디미터 법칙 위반
screening.calculateFee(audienceCount); // 개선
  • 메시지 전송자가 수신자의 내부 구조에 대해 물어보고 반환받은 요소에 대해 연쇄적으로 메시지를 전송하는 것을 여러 대의 기차가 한 줄로 늘어서 충돌한 것처럼 보이는 이유로 "기차 충돌"이라고 부른다.
  • 메시지 전송자가 메시지 수진자의 내부 구조에 관해 묻지 않고, 단지 자신이 원하는 것이 무엇인지를 명시하고 단순히 수행하도록 요청함으로 코드를 개선할 수 있다.

디미터 법칙

  • 객체가 자기 자신을 책임지는 자율적인 존재여야 한다는 사실을 강조한다.
    -> 단일 책임 원칙, 의인화
  • 정보를 처리하는데 필요한 책임을 정보를 알고 있는 객체에게 할당한다.
    -> 응집도 높은 객체 생성
  • 무비판적으로 디미터 법칙을 수용하면 퍼블릭 인터페이스(객체가 협력에 참여하기 위해 외부에서 수신할 수 있는 메시지의 묶음) 관점에서 객체의 응집도가 낮아질 수도 있다.
  • 자연스럽게 객체의 내부 구조를 묻는 메시지가 아니라 수신자에게 무언가를 시키는 메시지가 더 좋은 메시지라고 속삭인다.

묻지 말고 시켜라 (Tell, Don't Ask.)

Tell, Don't Ask
디미터 법칙은 "훌륭한 메시지는 객체의 상태에 관해 묻지 말고, 원하는 것을 시켜야 한다"는 것을 강조한다.

  • 이런 스타일의 메시지 작성을 장려하는 원칙을 가리키는 용어다.

묻지 말고 시켜라 원칙을 따르면 밀접하게 연관된 정보와 행동을 함께 가지는 객체를 만들 수 있다.
객체지향의 기본은 함께 변경될 확률이 높은 정보와 행동을 하나의 단위로 통합하는 것이다.
객체의 정보를 이용하는 행동을 외부가 아닌 내부에 위치 -> 자연스럽게 정보와 행동을 동일한 클래스 안에 두게 된다.

우리는 메시지를 전송하는 개별적인 객체들을 보유하고 잇는데 그렇다면 객체들은 무엇을 말해야 하는가? 호출하는 객체는 이웃 객체가 수행하는 역할을 사용해 무엇을 원하는지를 서술해야 하고, 호출되는 객체가 어떻게 해야 하는지를 스스로 결정하게 해야한다. 이것은 일반적으로 묻지 말고 시켜라 스타일, 좀 더 공식적으로는 디미터 법칙으로 알려져있다.

  • 객체는 자신이 내부적으로 보유하고 있는 정보나 메시지 전송의 결과로 얻게 되는 정보만 사용해서 의사결정을 내리게 된다.
    -> 객체는 다른 객체의 내부를 탐색하지 않아도 된다.
  • 동일한 역할을 수행하는 객체로 교체하는 것이 쉬워진다.
  • 호출 객체는 역할 인터페이스 배후의 내부 구조나 시스템의 나머지 구조에 대해서는 어떤 것도 보지 못한다.
  • 이 스타일을 따르지 않을 경우, 일련의 getter들이 기차의 객차처럼 상호 연결디어 보이는 코드가 만들어지고 만다.

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

켄트 벡 선생님의 메서드를 명명하는 두 가지 방법

1. 메서드가 작업을 어떻게 수행하는지를 나타내도록 이름 짓는다. (메서드의 이름이 내부 구현 방법을 드러낸다.)

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

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

위 예시 코드의 문제점

  • 메서드에 대해 제대로 커뮤니케이션 하지 못한다.
    • 할인 조건을 판단하는 동일한 작업 수행이지만, 메서드의 이름이 다르기 때문에 두 메서드의 내부 구현을 정확하게 이해하지 못한다면 동일한 작업 수행이라는 사실을 알기 어렵다.
  • 메서드 수준에서 캡슐화를 위반한다.
    • 협력하는 객체의 종류를 알도록 강요한다.
    • 책임을 수행하는 방법을 드러내는 메서드를 사용한 설계는 변경에 취약할 수 밖에 없다.

2. '어떻게'가 아니라 '무엇을' 하는지를 드러낸다.

무엇을 하는지를 드러내는 이름은 코드를 읽고 이해하기 쉽게 만들뿐만 아니라 유연한 코드를 낳는 지름길이라고 한다. 무엇을 하는지를 드러내도록 메서드의 이름을 짓는다는건 무슨 의미일까?

'어떻게'와 '무엇을'의 차이

  • '어떻게' : 메서드의 내부 구현을 설명하는 이름. 결과적으로 협력을 설계하기 시작하는 이른 시기부터 클래스의 내부 구현에 관해 고민할 수 밖에 없다.
  • '무엇을' : 객체가 협력 안에서 수행해야 하는 책임에 관해 고민해야 한다. 외부의 객체가 메시지를 전송하는 목적을 먼저 생각하도록 만들며, 결과적으로 협력하는 클라이언트의 의도에 부합하도록 메서드의 이름을 짓게 된다.
pubic class PeriodCondition {
	public boolean isSatisfiedBy(Screening screening) { ... }

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

동일한 목적을 가진다는 것을 명확히 표현
동일한 메시지를 서로 다른 방법으로 처리 -> 서로 대체 가능

정적 타이핑 언어(java)에서 단순히 메서드의 이름이 같다고 해서 동일한 메시지를 처리할 수 있는 것은 아니기 때문에 동일한 타입 계층으로 묶어야 한다.

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

PeriodCondition, SequenceCondition을 동일한 타입으로 선언하기 위해 DiscountCondition를 실체화하게 하면 두 메서드를 동일한 방식으로 사용할 수 있게 된다.

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

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

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

  • 무엇을 하느냐에 따라 메서드의 이름을 짓는 것이 설계를 유연하게 만든다.
  • 클라이언트의 관점에서 동일한 작업을 수행하는 메서드들을 하나의 타입 계층으로 묶을 수 있는 가능성이 커진다.
  • 다양한 타입의 객체가 참여할 수 잇는 유연한 협력을 얻게 되기 때문이다.

어떻게 하느냐가 아니라 무엇을 하느냐에 따라 메서드의 이름을 짓는 패턴을 의도를 드러내는 선택자라고 부른다.

켄트 벡 선생님은 메서드에 의도를 드러낼 수 있는 이름을 붙이기 위해 다음과 같은 조언을 한다.

하나의 구현을 가진 메시지의 이름을 일반화하도록 도와주는 방법

  • " 매우 다른 두번째 구현을 상상하라. 그러고는 해당 메서드에 동일한 이름을 붙인다고 상상해보라. 그 순간에 여러분이 할 수 있는 한 가장 추상적인 이름을 메서드에 붙일 것이다. "

의도를 드러내는 인터페이스를 한 마디로 요약하면 "구현과 관련된 모든 정보를 캡슐화하고, 객체의 퍼블릭 인터페이스에는 협력과 관련된 의도만을 표현해야 한다는 것"이라고 한다.

메서드의 목적을 효과적으로 전달하는 방법 - 의도를 드러내는 선택자를 사용해 메서드의 이름을 짓는 방법

  • "수행 방법에 관해서는 언급하지 말고 결과와 목적만을 포함하도록 클래스와 오퍼레이션의 이름을 부여하라. 이렇게 하면 클라이언트 개발자가 내부를 이해해야 할 필요성이 줄어든다."
  • "방정식을 푸는 방법을 제시하지 말고 이를 공식으로 표현하라. 문제를 내라. 하지만 문제를 푸는 방법을 표현해서는 안된다."
  • 객체에게 묻지 말고 시키되 구현 방법이 아닌 클라이언트의 의도를 드러내야 한다.
  • 이해하기 쉽고 유연한 동시에 협력적인 객체를 만드는 가장 기본적인 요구사항이다.

인터페이스와 구현의 분리 원칙

디미터 법칙, 묻지 말고 시켜라, 의도를 드러내는 인터페이스를 위반하는 코드의 모습을 살펴보자.

public class 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 {
    ...
    
  • Theater는 Audience 뿐만 아니라내부 포함 객체에 직접 접근하고 있다.
    • 묻지 말고 시켜라 스타일을 따르는 퍼블릭 인터페이스를 가져야 한다.
  • enter 메서드에서 기차 충돌 스타일
  • Audience뿐만 아니라 내부에 포함된 Bag에게도 메시지를 전송
    • 퍼블릭 인터페이스 뿐만 아니라 내부 구조에 대해서도 결합된다.

디미터의 법칙 위반 -> 인터페이스와 구현의 분리 원칙을 위반하는 것으로 이어진다.

묻지 말고 시켜라

  • Theater는 TicketSeller와 Audience의 내부 구조에 관해 묻지 말고 원하는 작업을 시켜야 한다.
  • Theater가 TicketSeller에게 자신이 원하는 일을 시키도록 수정하자.
    • Audience가 Ticket을 가지도록 만드는 것이다.
public class TicketSeller {
	public void setTicket(Audience audience) {
    if (audience.getBag().hasInvitation()) {
    Ticket ticket = ticketSeller.getTicketOffice().getTicket();
    audience.getBag().setTicket(ticket);
    } else {
    ...
}
public class Theater {
	public void enter(Audience audience) {
    	tickeSeller.setTicket(audience);
        }
    }
}

setTicket : enter 메서드의 로직을 setTicket메서드 안으로 메서드 추출

  • TicketSeller가 원하는 것은 Audience가 Ticket을 보유하도록 만드는 것이다.
    • Audience에게 setTicket 메서드를 추가하고 스스로 티켓을 가지도록 만들기
public class Audience {
	public Long setTicket(Ticket ticket) {
    if (bag.hasInvitation()) {
    	bag.setTicket(ticket);
        return 0L;
        
    else {
    bag.setTicket(ticket);
    ...
	}
  }
}

TicketSeller는 속성으로 포함하고 있는 TicketOffice의 인스턴스와 인자로 전달된 Audience에게만 메시지를 전송한다. 따라서 TicketSeller 역시 디미터 법칙을 준수한다.

pubic class TicketSeller {
	pubic void setTicket(Audience audience) {
    ticketOffice.plusAmount(
    	audience.setTicket(ticketOffice.getTicket()));
        }
}
  • Audience의 setTicket을 살펴보면 Bag에게 원하는 일을 시키기 전에 hasInvitation 메서드를 이용해 초대권을 가지고 있는지를 묻는다.
  • 따라서 디미터 법칙을 위반한다.
    -> Audience의 setTicket 메서드 구현을 Bag의 setTicket 메서드로 이동
public class Bag {
	public Long setTicket(Ticket ticket) {
    	if (hasInvitation()) {
    	this.ticket = ticket;
        return 0L;
        
    	else {
    	this.sticket = ticket;
    	...
	}
}

private boolean hasInvitation() {
	return invitation != null;
}

Audience의 setTicket 메서드가 Bag의 setTicket 메서드를 호출하도록 수정하면 묻지 말고 시켜라 스타일을 따르고, 디미터 법칙을 준수하는 Audience를 얻을 수 있다.

  • 자신의 상태를 스스로 관리하고 제어하게된 Audience
public class Audience {
	public Long setTicket(Ticket ticket) {
    	return bag.setTicket(ticket);
    }
}

디미터 법칙과 묻지 말고 시켜라 스타일을 따르면 자연스럽게 자율적인 객체로 구성된 유연한 협력을 얻게 된다.
인터페이스가 클라이언트의 의도를 올바르게 반영햇느지를 확인해보자.


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

안타깝게도 미묘하게 다른 의미를 가진 세 메서드가 같은 이름을 가지고 있다는 사실은 클라이언트 개발자를 혼란스럽게 만들 확률이 높다.
클라이언트의 의도가 분명하게 드러나도록 객체의 퍼블릭 인터페이스를 개선해야 한다.

  • Theater가 TicketSeller에게 setTicket 메시지를 전송해서 얻고 싶었던 결과는 무엇일까? -> Audience가 티켓을 사도록 만드는 것
  • TicketSeller가 Audience에게 setTicket 메시지를 전송하는 이유는 무엇일까?

이 의도를 각 객체의 퍼블릭 인터페이스를 통해 명확하게 드러내자.

public class TicketSeller {
	public void sellTo(Audience audience) { ... }
}
public class Audience {
	public void buy(Ticket ticket) { ... }
}
public class Bag {
	public void hold(Ticket ticket) { ... }
}
  • 오퍼레이션의 이름은 협력이라는 문맥을 반영해야 한다.
  • 클라이언트가 객체에게 무엇을 원하는지를 표현해야 한다.
  • 객체 자신이 아닌 클라이언트의 의도를 표현하는 이름을 가져야 한다.
  • 객체에게 '무엇을' 원하는지를 명확하게 표현한다. setTicket은 그렇지 않다.

우리는 결합도가 낮으면서도 의도를 명확히 드러내는 간결한 협력을 원한다. 디미터 법칙과 묻지 말고 시켜라 스타일, 의도를 드러내는 인터페이스가 우리를 도울 것이라고 한다.

  • '묻지 말고 시켜라' 스타일에는 정보 은닉 말고도 객체 간의 상호작용을 getter의 체인 속에 암시적으로 두지 않고 좀 더 명시적으로 만들고 이름을 가지도록 강요한다.
profile
왔을때보다좋은곳으로

0개의 댓글