역할, 책임, 협력책임이다.이번 장에서는 데이터 중심 설계를 살펴보고, 이후 객체지향 설계와 비교해본다.
상태 (=데이터)를 분할의 중심축으로 삼는 방법책임을 분할의 중심축으로 삼는 방법
상태 변경 > 인터페이스 변경 > 의존하는 모든 외부 객체로의 변경으로 변경이 전파되게 된다.discountCondition)이 인스턴스 변수로 Movie 안에 직접 포함되어 있다.discountPolciy로 분리했던 예전 예제와 달리 할인 금액(discountAmount)과 비율(discountPercent)을 Movie 안에서 직접 정의하고 있다.public class Movie {
// 영화 제목
private String title;
// 상영시간
private Duration runningTime;
// 기본 요금
private Money fee;
// 할인 조건 목록
private List<DiscountCondition> discountConditions;
// 할인 정책의 종류
private MovieType movieType;
// 할인 금액
private Money discountAmount;
// 할인 비율
private double discountPercent;
public List<DiscountCondition> getDiscountConditions() {
return discountConditions;
}
public void setDiscountConditions(List<DiscountCondition> discountConditions) {
this.discountConditions = discountConditions;
}
public MovieType getMovieType() {
return movieType;
}
public void setMovieType(MovieType movieType) {
this.movieType = movieType;
}
public Money getDiscountAmount() {
return discountAmount;
}
public void setDiscountAmount(Money discountAmount) {
this.discountAmount = discountAmount;
}
public double getDiscountPercent() {
return discountPercent;
}
public void setDiscountPercent(double discountPercent) {
this.discountPercent = discountPercent;
}
}
movieType)와 인스턴스 종류에 따라 배타적으로 사용될 인스턴스 변수(discountAmount, discountPercent)를 하나의 클래스 안에 포함시키는 방식public enum MovieType {
AMOUNT_DISCOUNT, // 금액 할인 정책
PERCENT_DISCOUNT, // 비율 할인 정책
NONE_DISCOUNT; // 미적용
}
public enum DiscountConditionType {
SEQUENCE, // 순번 조건
PERIOD; // 기간 조건
}
public class DiscountCondition {
// 할인 조건의 타입
private DiscountConditionType type;
// 상영 순번
private int sequence;
// 기간 조건에서만 사용; 요일
private DayOfWeek dayOfWeek;
// 기간 조건에서만 사용; 시작 시간
private LocalTime startTime;
// 기간 조건에서만 사용; 종료 시간
private LocalTime endTime;
public DiscountConditionType getType() {
return type;
}
public void setType(DiscountConditionType type) {
this.type = type;
}
public int getSequence() {
return sequence;
}
public void setSequence(int sequence) {
this.sequence = sequence;
}
public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
public void setDayOfWeek(DayOfWeek dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
public LocalTime getStartTime() {
return startTime;
}
public void setStartTime(LocalTime startTime) {
this.startTime = startTime;
}
public LocalTime getEndTime() {
return endTime;
}
public void setEndTime(LocalTime endTime) {
this.endTime = endTime;
}
}
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Movie getMovie() {
return movie;
}
public void setMovie(Movie movie) {
this.movie = movie;
}
public int getSequence() {
return sequence;
}
public void setSequence(int sequence) {
this.sequence = sequence;
}
public LocalDateTime getWhenScreened() {
return whenScreened;
}
public void setWhenScreened(LocalDateTime whenScreened) {
this.whenScreened = whenScreened;
}
}
public class Reservation {
private Customer customer;
private Screening screening;
private Money fee;
private int audienceCount;
public Reservation(Customer customer, Screening screening, Money fee, int audienceCount)
{
this.customer = customer;
this.screening = screening;
this.fee = fee;
this.audienceCount = audienceCount;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public Screening getScreening() {
return screening;
}
public void setScreening(Screening screening) {
this.screening = screening;
}
public Money getFee() {
return fee;
}
public void setFee(Money fee) {
this.fee = fee;
}
public int getAudienceCount() {
return audienceCount;
}
public void setAudienceCount(int audienceCount) {
this.audienceCount = audienceCount;
}
}
public class Customer {
private String name;
private String id;
public Customer(String name, String id) {
this.name = name;
this.id = id;
}
}

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())
&& condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0
&& condition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
} else {
discountable = condition.getSequence() == screening.getSequence();
}
if(discountable) {
break;
}
}
Money fee;
if (discountable) {
Money discountAmount = Money.ZERO;
switch(movie.getMovieType()) {
case AMOUNT_DISCOUNT:
discountAmount = movie.getDiscountAmount();
break;
case PERCENT_DISCOUNT:
discountAmount = movie.getFee().times(movie.getDiscountPercent());
break;
case NONE_DISCOUNT:
discountAmount = Money.ZERO;
break;
}
fee = movie.getFee().minus(discountAMount).times(AudienceCount);
} else {
fee = movie.getFee();
}
return new Reservation(customer, screening, fee, audienceCount);
}
}
reserve()DiscountCondition에 대해 루프를 돌며 할인 가능 여부를 판단하는 for문discountable의 값을 체크하고, 적절한 할인 정책에 따라 예매 요금을 계산하는 if문
- 설계의 완성도를 평가할 수 있는 세 가지 평가 척도을 알아보자
- 캡슐화, 응집도, 결합도
복잡성을 다루기 위한 가장 효과적인 도구는 추상화다. 다양한 추상화 유형을 사용할 수 있지만 객체지향 프로그래밍에서 복잡성을 취급하는 주요한 추상화 방법은 캡슐화다. 그러나 프로그래밍할 때 객체지향 언어를 사용한다고 해서 애플리케이션의 복잡성이 잘 캡슐화될 것이라고 보장할 수는 없다. 훌륭한 프로그래밍 기술을 적용해서 캡슐화를 향상시킬 수는 있겠지만, 객체지향 프로그램을 통해 전반적으로 얻을 수 있는 장점은 오직 설계 과정 동안 캡슐화를 목표로 인식할때만 작성될수 있다.[Wirfs-Brock89].
유지보수성이 목표다. 여기서 유지보수성이란 두려움 없이, 주저함 없이, 저항감 없이 코드를 변경할 수 있는 능력을 말한다. ... 가장 중요한 동료는 캡슐화다. 캡슐화란 어떤 것을 숨긴다는 것을 의미한다. 우리는 시스템의 한 부분을 다른 부분으로 감춤으로써 뜻밖의 피해가 발생할 수 있는 가능성을 사전에 방지할 수 있다. 만약 시스템이 완전히 캡슐화된다면 우리는 변경으로부터 완전히 자유로워질 것이다. 만약 시스템의 캡슐화가 크게 부족하다면 우리는 변경으로부터 자유로울 수 없고, 결과적으로 시스템은 진화할 수 없을 것이다. 응집도, 결합도, 중복 역시 훌륭한(변경 가능한) 코드를 규정하는데 핵심적인 품질인 것이 사실이지만 캡슐화는 우리를 좋은 코드로 안내하기 때문에 가장 중요한 제 1원리다.[Bain08].


String, ArrayListpublic class Movie {
private Money fee;
public Money getFee() {
return fee;
}
public void setFee(Money fee) {
this.fee = fee;
}
}
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
...
Money fee;
if (discountable) {
...
fee = movie.getFee().minus(discountedAmount).times(audienceCount);
} else {
fee = movie.getFee();
}
...
}
}
fee의 타입이 변경되는 경우,getFee() 메서드의 반환 타입이 수정되어야 한다.getFee()를 호출하는 ReservationAgency의 구현도 변경된 타입에 맞게 함께 수정되어야 한다.getFee()메서드의 사용은, 변수 fee의 가시성을 private에서 public으로 변경하는 것과 거의 동일하다.
ReservationAgency가 모든 데이터 객체에 의존하고 있다.DiscountCondition, Screening츼 데이터 변경 시, ReservationAgency도 함께 수정되어야 한다.RservationAgency의 변경을 유발한다.ReservationAgency 코드를 수정하게 한다.- 할인 정책이 추가되는 경우
- 할인 정책별로 할인 요금을 계산하는 방법이 변경될 경우
- 할인 조건이 추가되는 경우
- 할인 조건별로 할인 여부를 판단하는 방법이 변경될 경우
- 예매 요금을 계산하는 방법이 변경될 경우
ReservationAgency에 할인 정책을 선택하는 코드와 할인 조건을 판단하는 코드가 함께 위치하기 때문에, 새로운 할인 정책을 추가하는 작업이 할인 조건에도 영향을 미칠 수 있다.MovieType에 새로운 할인 정책을 표현하는 열거형을 추가하고, ReservationAgency의 reserve의 switch문에 새로운 case절을 추가해야 한다. 또한 새로운 할인 정책에 따른 요금 계산 로직을 Movie에 추가해야 한다.단일 책임 원칙(SRP, Single Responsibility Principle)
- 로버트 마틴(Robert C. Martin); 모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위해 단일 책임 원칙이라는 설계 원칙을 제시.
- 클래스는 단 한가지의 변경 이유만 가져야 한다.
- '책임'이라는 말이 '변경의 이유'라는 의미로 사용된다.
- 책에서의 책임과 달리, 변경과 관련된 더 큰 개념을 가리킨다.
private로 설정했더라도, 접근자와 수정자를 통해 속성이 외부로 공개된다면, 캡슐화를 위반하는 것이다.public class Rectangle {
private int left;
private int top;
private int right;
private int bottom;
public Rectangle(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public int getLeft() {
return left;
}
public int getTop() {
return top;
}
public int getRight() {
return right;
}
public int getBottom() {
return bottom;
}
}
class AnyClass {
void anyMethod(Rectagle rectangle, int multiple) {
rectangle.setRight(rectangle.getRight() * multiple);
rectangle.setBottom(rectangle.getRight() * multiple);
}
}
getter를 호출해 조작할 것.getter)와 수정자(setter)는 내부 구현을 인터페이스의 일부로 만든다.int타입이라는 사실을 외부에 노출시키고 있다.class Rectangle {
public void enlarge(int multiple) {
right += multiple;
bottom += multiple;
}
}
Rectangle을 변경하는 주체를 외부에서 내부 객체로 이동시켰다.이 객체가 어떤 데이터를 포함해야 하는가?이 객체가 어떤 데이터를 포함해야 하는가?이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
}
public class DiscountCondition {
public DiscountConditionType getType() {
return type;
}
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime localTime) {
if (type != DiscountConditionType.PERIOD) {
throw new IllegalArgumentException();
}
return this.dayOfWeek.equals(dayOfWeek) &&
this.dayOfWeek.compareTo(time) <= 0 &&
this.dayOfWeek.compareTo(time) >= 0;
}
public boolean isDiscountable(int sequence) {
if (type != DiscountConditionType.SEQUENCE) {
throw new IllegalArgumentException();
}
return this.sequence == sequence;
}
}
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money money;
private double discountPercent;
}
public class Movie {
// ------------------------- 영화 요금 계산 -------------------------
public MovieType getMovieType() {
return movieType;
}
public Money calculateAmountDiscountedFee() {
if (movieType != MovieType.AMOUNT_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee.minus(discountAmount);
}
public Money calculatePercentDiscountedFee() {
if (movieType != MovieType.PERCNET_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee.minus(fee.times(discountAmount));
}
public Money calculateNoneDiscountedFee() {
if (movieType != MovieType.NONE_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee;
}
// ------------------------- 할인 여부 판단 -------------------------
public boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
for(DiscountCondition condition : discountConditions) {
if(condition.getType() == DiscountConditionType.PERIOD) {
if(condition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
return true;
}
} else {
if(condition.isDiscountable(sequence)) {
return true;
}
}
}
return false;
}
}
Movie의 할인 여부를 판단하고, 적절한 메서드를 호출해 요금을 계산한다.package screen;
import java.time.LocalDateTime;
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Screening(Movie movie, int sequence, LocalDateTime whenScreened) {
this.movie = movie;
this.sequence = sequence;
this.whenScreened = whenScreened;
}
private Money calculateFee(int audienceCount) {
switch (movie.getMovieType()) {
case AMOUNT_DISCOUNT:
if(movie.isDiscountable(whenScreened, sequence)) {
return movie.calculateAmountDiscountedFee().times(audienceCount);
}
break;
case PERCENT_DISCOUNT:
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculatePercentDiscountedFee().times(audienceCount);
}
case NONE_DISCOUNT:
return movie.calculateNoneDiscountedFee().times(audienceCount);
}
return movie.calculateNoneDiscountedFee(this).times(audienceCount);
}
}
ReservationAgency에 몰려 있던 의존성이 개선되었다.public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audience) {
Money fee = screening.calculate(audienceCount);
return new Reservation(customer, screening, fee, audienceCount);
}
}
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public DiscountConditionType getType() { ... }
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime localTime) { ... }
public boolean isDiscountable(int sequence) { ... }
}
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime localTime)DayOfWeek 타입, LocalTime 타입의 변수가 있음을 알 수 있다.public boolean isDiscountable(int sequence) { ... }int 타입의 변수가 있음을 알 수 있다.public DiscountConditionType getType()DiscountConditionType 타입이 있음을 노출하고 있다.isDiscountable 메서드들의 파라미터가 변경되어야 한다.public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money money;
private double discountPercent;
public MovieType getMovieType() {...}
public Money calculateAmountDiscountedFee() {...}
public Money calculatePercentDiscountedFee() {...}
public Money calculateNoneDiscountedFee() {...}
}
calculateAmountDiscountedFee, calculatePercentDiscountedFee, calculateNoneDiscountedFee 메서드 Movie는 세부 할인 정책을 성공적으로 캡슐화하지 못했다.📌 캡슐화의 진정한 의미
- 캡슐화; 변경될 수 있는 어떤 것이라도 감추는 것
- cf. 캡슐화는 단순히 객체 내부의 데이터를 외부로부터 감추는 것 이상의 의미를 가진다.
- 종류를 불문하고, 내부 구현의 변경으로 인해 외부가 영향을 받는다면, 캡슐화에 실패한 것이다.
public class Movie {
public boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
for(DiscountCondition condition : discountConditions) {
if(condition.getType() == DiscountConditionType.PERIOD) {
if(condition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
return true;
}
} else {
if(condition.isDiscountable(sequence)) {
return true;
}
}
}
return false;
}
}
DiscountCondition이 과하게 내부 구현을 외부에 노출했기 때문에, Movie와 강하게 결합하고 있다.DiscountCondition의 구현 변경 시 Movie에 미치는 영향1. `DiscountCondition`의 기간할인조건 명칭이 `PERIOD`에서 다른 값으로 변경된다면 `Movie`를 변경해야 한다.
2. `DiscountCondition`의 종류가 추가되거나 삭제된다면 `Movie`의 `if ~ else` 구문을 수정해야 한다.
3. 각 `DiscountCondition`의 맍고 여부를 판단하는데 필요한 정보가 변경된다면,
`Movie`의 `isDiscountable` 메서드로 전달된 파라미터를 변경해야 한다.
이로인해 `Movie`의 `isDiscountable` 메서드의 시그니처가 변경될 것이고,
결과적으로 이 메서드에 의존하는 `Screening`에 대한 변경을 초래한다.
public class Screening {
private Money calculateFee(int audienceCount) {
switch (movie.getMovieType()) {
case AMOUNT_DISCOUNT:
if(movie.isDiscountable(whenScreened, sequence)) {
return movie.calculateAmountDiscountedFee().times(audienceCount);
}
break;
case PERCENT_DISCOUNT:
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculatePercentDiscountedFee().times(audienceCount);
}
case NONE_DISCOUNT:
return movie.calculateNoneDiscountedFee().times(audienceCount);
}
return movie.calculateNoneDiscountedFee(this).times(audienceCount);
}
}
DiscountCondition의 응집력이 약해, 내부 구현의 변화로 코드 여러 곳을 동시에 변경해야 한다.1. `Movie`의 `isDiscountable`로 전달하는 파라미터의 종류가 변경되어야 한다.
2. `Screening`에서 `Movie`의 `isDiscountable`를 호출하는 부분이 변경되어야 한다.
객체가 가져야 하는 데이터가 무엇인지 묻는다.public 속성과 큰 차이가 없으므로, 객체의 캡슐화가 무너지기 쉽다.