반응형
오브젝트 04 - 설계 품질과 트레이드오프
가끔 좋은 설계보다는 나쁜 설계를 통해 통찰을 얻기도 한다. 특히 좋은 설계와 나쁜 설계를 비교할 때 효과가 좋다.
이번 장에서는 영화 예매 시스템을 책임이 아닌 상태를 표현하는 데이터 중심의 설계를 살펴보고 객체 지향적으로 설계한 구조와 어떤 차이점이 있는지 살펴보자.
4.1 데이터 중심의 영화 예매 시스템
- 데이터 중심 설계에서 객체는 자신이 포함하고 있는 데이터를 조작하는데 필요한 오퍼레이션을 정의한다.
- 데이터 중심 설계에서는 객체를 독립된 데이터 덩어리로 바라본다.
- 상태가 객체 분할의 중심이 되면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화가 무너진다.
- 결과적으로 상태 변경은 인터페이스의 변경을 초래하여 해당 인터페이스에 의존하는 모든 객체에게 영향을 준다.
- 따라서 데이터에 초점을 맞추는 설계는 변경에 취약할 수밖에 없다.
4.1.1 데이터를 준비하자
- 데이터 중심 설계는 '데이터가 무엇인가'를 묻는 것으로 시작한다.
- 먼저 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 class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = discountPolicy;
}
public Money getFee() {
return fee;
}
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
- 책임 중심 설계와 가장 다른 점은 할인 조건의 목록(discountConditions)이 Movie 안에 변수로 포함되어 있다는 것
- 또한 할인 정책의 할인 금액(discountAmount)과 할인 비율 (discountPercent)을 Movie 안에서 직접 정의
- 영화는 하나의 할인 정책을 가질 수 있으므로 movieType으로 종류를 결정
데이터 중심의 설계를 하게 되는 과정
- 데이터 중심의 설계에서는 객체의 책임보다 객체가 포함해야 하는 데이터에 집중
- 이 객체는 어떤 데이터를 가져야하나? 객체의 책임을 결정하기 전에 이런 질문을 반복한다면 데이터에 매몰되어 있을 것이다.
- 특히 Movie 클래스의 경우처럼 객체의 종류를 저장하는 변수(movieType)와 인스턴스의 종류에 따라 각각 사용될 인스턴스 변수(discountAmount, discountPercent)를 하나의 클래스 안에 함께 포함시키는 방식은 데이터 중심의 설계 안에서 흔히 볼 수 있는패턴이다.
이제 할인 조건을 저장할 데이터를 구현해 보자.
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
}
public enum DiscountConditionType {
SEQUENCE // 순번 조건
PERIOD // 기간 조건
}
- movie와 마찬가지로 순번 조건에서만 사용되는 데이터와 기간 조건에서만 사용되는 데이터를 함께 포함한다.
마지막으로 screening 클래스와 Reservation 클래스를 구현하자
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
}
public class Reservation {
private Customer customer;
private Screening screening;
private Money fee;
private int audienceCount;
}
- 그 후, 모든 클래스의 캡슐화를 위해 getter와 setter를 생성하자.
-
영화 예매 시스템을 위해 필요한 모든 데이터를 클래스로 구현했다.
4.1.2 영화를 예매하자
- ReservationAgency는 데이터 클래스들을 조합해서 영화 예매 절치를 구현하는 클래스다.
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 메서드는 크게 할인 가능 여부를 확인하는 for 문과, 적절한 할인 정책에 따라 예매 요금을 계산하는 if 문으로 구성되어있다.
4.2 설계 트레이드오프
4.2.1 캡슐화
- 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 하기 때문이다.
- 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다.
4.2.2 응집도와 결합도
- 응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.
- 결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타낸다.
- 높은 응집도와 낮은 결합도를 가진 설계를 추구해야 하는 단 한가지의 이유는 그것이 설계를 변경하기 쉽게 만들기 때문이다.
- 일반적으로 변경될 확률이 매우 적은 안정적인 모듈과는 결합도가 높아도 상관이 없다.
- 표준 라이브러리에 포함된 모듈이나 성숙 단계에 접어든 프레임워크 등
- 예를 들어, 자바의 String이나 Arraylist는 변경될 확률이 매우 낮기 때문에 결합도에 대해 고민할 필요가 없다.
- 그러나 직접 작성한 코드는 항상 불안정하며 언제라도 변경될 기능성이 높다.
- 따라서 직접 작성한 코드의 경우에는 낮은 결합도를 유지하려고 노력해야 한다.
-
마지막으로 캡슐화를 지키면 모듈 안의 응집도는 높아지고 모듈 사이의 결합도는 낮아진다.
4.3 데이터 중심의 영화 예매 시스템의 문제점
4.3.1 캡슐화 위반
public class Movie {
private Money fee;
public Money getFee() {
return fee;
}
public void setFee(Money fee) {
this.fee = fee;
}
}
- 위 코드는 객체의 내부에 직접 접근할 수 없어서 캡슐화의 원칙을 지키는 것 같다.
- 그러나 접근자와 수정자 메서드는 fee라는 변수의 존재를 공개적으로 드러내 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다.
- Movie가 캡슐화의 원칙을 어기게 된 근본적인 원인은 객체가 수행할 책임이 아니라 내부에 저장할 데이터에 초점을 맞췄기 때문이다.
- 객체의 협력을 고려하지 않고 객체가 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행한다.
4.3.2 높은 결합도
- 객체 내부의 구현이 객체의 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미한다.
- 이것은 단지 객체의 내부 구현을 변경했음에도 이 인터페이스에 의존하는 모든 클라이언트들도 함께 변경해야 한다는 것이다.
- 데이터 중심 설계에서는 여러 데이터 객체들을 사용하는 제어 로직이 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것이 다.
- 이 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수밖에 없다.
4.3.3 낮은 응집도
- 서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 말한다.
- 현재의 설계는 새로운 할인 정책을 추가하거나 새로운 할인 조건을 추가하기 위해 하나 이상의 클래스를 동시에 수정해야 한다.
4.4 자율적인 객체를 향해
4.4.1 캡슐화를 지켜라
- 객체에게 의미 있는 메서드는 객체가 책임져야 하는 무언가를 수행하는 메서드다.
- 속성의 가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다.
Rectangle을 통한 캡슐화 위반 예제
@Getter
@Setter
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;
}
}
class AnyClass {
void anyMethod(Rectangle rectangle, int multiple) {
rectangle.setRight(rectangle.getRight() * multiple);
rectangle.setBottom(rectangle.getBottom() * multiple);
}
}
- 이 코드의 첫번째 문제점은 ‘코드 중복’이 발생할 확률이 높다.
- 다른 곳에서도 사각형의 너비와 높이를 증가시키는 코드가 필요하다면 유사한 코드가 존재할 것이다.
- 두 번째 문제점은 변경에 취약하다는 점이다.
- Rectangle의 필드명을 변경한다면, 그것을 사용하고 있는 모든 클래스도 변경이 필요하다.
캡슐화를 통한 해결
class Rectangle {
public void enlarge(int multiple) {
right *= multiple;
bottom *= multiple;
}
}
- Rectangle 내부에 너비와 높이를 조절하는 로직을 캡슐화해서 두 가지 문제를 해결할 수 있다.
- Rectangle을 변경하는 주체를 외부의 객체에서 Rectangle로 이동시켰다.
4.4.2 스스로 자신의 데이터를 책임지는 객체
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
Money fee = screening.calculateFee(audienceCount);
return new Reservation(customer, screening, fee, audienceCount);
}
}
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public 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);
}
break;
case NONE_DISCOUNT:
return movie.calculateNoneDiscountedFee().times(audienceCount);
}
return movie.calculateNoneDiscountedFee().times(audienceCount);
}
}
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 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;
}
}
- 최소한 결합도 측면에서 ReservationAgency에 의존성이 몰려있던 첫 번째 설계보다는 개선되었다.
- 첫 번째 설계보다 내부 구현을 더 면밀하게 캡슐화하고 있다.
- 데이터를 처리하는 데 필요한 메서드를 데이터를 가지고 있는 객체 스스로 구현하고 있다.
4.5 하지만 여전히 부족하다
4.5.1 캡슐화 위반
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 time) { ... }
public boolean isDiscountable(int sequence) { ... }
}
- 기간 조건을 판단하는 isDiscountable은 인스턴스 변수를 인터페이스를 통해 외부에 노출하고 있다.
- DiscountCondition의 속성을 변경해야 한다면 해당 메서드를 사용하는 모든 클라이언트도 함께 수정해야 한다.
- 내부 구현의 변경이 외부로 퍼져나가는 파급 효과(ripple effect)는 캡슐화가 부족하다는 명백한 증거다.
- Movie 역시 세 개의 메서드를 통해 할인 정책에는 금액, 비율, 미적용의 세 가지가 존재한다는 사실을 노출하고 있다.
- 새로운 할인 정책이 추가되거나 제거된다면 이 메서드들에 의존하는 모든 클라이언트가 영향을 받는다.
- Movie는 세 가지 할인 정책을 포함하고 있다는 내부 구현을 성공적으로 캡슐화하지 못한다.
4.5.2 낮은 응집도
public class Screening {
public 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);
}
break;
case NONE_DISCOUNT:
return movie.calculateNoneDiscountedFee().times(audienceCount);
break;
}
return movie.calculateNoneDiscountedFee().times(audienceCount);
}
}
- DiscountCondition이 할인 여부를 판단하는데 필요한 정보가 변경된다면 Screening에서 Movie의 isDiscountable 메서드를 호출하는 부분도 함께 변경해야 한다.
- 할인 조건을 변경하기 위해서는 DiscountCondition, Movie, 그리고 Movie를 사용히는 Screening을 전부 수정해야 한다
- 하나의 기능을 변경하기 위해 여러 곳을 동시에 변경해야 한다는 것은 설계의 응집도가 낮다는 증거다.
4.6 데이터 중심 설계의 문제점
- 두 번째 설계가 변경에 유연하지 못한 이유는 캡슐화를 위반했기 때문이다.
- 캡슐화를 위반한 설계는 응집도가 낮고 결합도가 높아 변경에 취약하다.
- 데이터 중심의 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
- 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.
728x90
반응형
'스터디 > 오브젝트' 카테고리의 다른 글
오브젝트 06 - 메시지와 인터페이스 (0) | 2024.02.26 |
---|---|
오브젝트 05 - 책임 할당하기 (0) | 2024.02.20 |
오브젝트 03 - 역할, 책임, 협력 (0) | 2024.02.13 |
오브젝트 02 - 객체지향 프로그래밍 (0) | 2024.02.06 |
오브젝트 01 - 객체, 설계 (0) | 2024.02.06 |
댓글