역할, 책임, 협력
책임
이다.이번 장에서는 데이터 중심 설계를 살펴보고, 이후 객체지향 설계와 비교해본다.
상태
(=데이터)를 분할의 중심축으로 삼는 방법책임
을 분할의 중심축으로 삼는 방법상태 변경 > 인터페이스 변경 > 의존하는 모든 외부 객체로의 변경
으로 변경이 전파되게 된다.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
, ArrayList
public 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
속성과 큰 차이가 없으므로, 객체의 캡슐화가 무너지기 쉽다.