기술 서적 정리/오브젝트
4장 설계 품질과 트레이드 오프
wampy
2023. 2. 15. 22:15
- 책임이 객체지향 애플리케이션 전체의 품질을 결정한다
- 객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동
- 객체의 상태가 아니라 객체의 행동에 초점을 맞추자
데이터 중심의 영화 예매 시스템
- 객체지향 설계에서의 객체 분할
- 상태를 분할의 중심축으로 삼는다
- 책임을 분할의 중심축으로 삼는다
- 데이터 중심의 관점에서 객체는 자신이 포함하고 있는 데이터를 조작하는 데 필요한 오퍼레이션을 정의
- 훌륭한 객체지향 설계는 데이터가 아니라 책임에 초점을 맞춰야한다
- 상태를 객체 분할의 중심축으로 삼으면
- 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어
- 캡슐화의 원칙이 무너짐
- 상태의 변경 -> 인터페이스의 변경을 초래
- 이 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼지게 됨
- -> 데이터에 초점을 맞추는 설계는 변경에 취약할 수밖엥 ㅓㅄ음
- 객체의 책임은 인터페이스에 속한다
- 책임에 초점을 맞추면 상대적으로 변경에 안정적인 설계를 얻을 수 있게 됨
데이터를 기준으로 분할한 영화 예매 시스템의 설계 ㄱㄱ
데이터를 준비
- 데이터 중심의 설계란
- 객체 내부에 저장되는 데이터를 기반으로 시스템을 분할하는 방법
- 객체가 내부에 저장해야 하는 데이터가 무엇인가 를 묻는 것으로 시작
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;
}
- 할인 조건의 목록 (discountConditions), 할인 정책 (discountAmount, discountPercent) -> Movie 안에 직접 포함돼 있음
- 할인 정책은 영화별로 오직 하나만 지정할 수 있다
- 할인 정책의 종류 아는 법 :
MovieType
- 데이터 중심의 접근 방법..
- Movie가 할인 금액 계산하는 데 필요한 데이터는 무엇인지 알아야함
- 어떤 데이터가 필요한가
- 데이터 중심의 접근 방법..
public enum MovieType { AMOUNT_DISCOUNT, // 금액 할인 정책 PERCENT_DISCOUNT, // 비율 할인 정책 NONE_DISCOUNT // 미적용 }
- 할인 정책의 종류 아는 법 :
- 데이터 중심의 설계에서는 객체가 포함해야 하는 데이터에 집중한다
- 객체지향의 가장 중요한 원칙 -> 캡슐화
- 내부 데이터가 객체의 엷은 막을 빠져나가 외부의 다르 객체들을 오염시키는 것을 막아야 함
- 이를 달성할 수 있는 가장ㄱ ㅏㄴ단한 방법
- 접근자와 수정자를 추가
// getter setter 추가...
- 할인 조건 구현
- 할인 조건을 구현하는 데 필요한 데이터는 무엇인가?
- 할인 조건의 타입을 저장할
DiscountConditionType
을 정의 public enum DiscountConditionType { SEQUENCE, // 순번 조건 PERIOD // 기간 조건 }
- 할인 조건을 구현하는
DiscountCondition
public class DiscountCondition { private DiscountConditionType type; private int sequence; private DayOfWeek dayOfWeek; private LocalTime startTime; private LcoalTime endTime; }
- Screening 클래스 구현
public class Screening { private Movie movie; priavte int sequence; private LocalDateTime whenScreened; }
- Reservation (예매) 클래스 구현
public class Reservation { private Customer customer; private Screening screening; private Money fee; private int audienceCount; }
- Customer : 고객 정보 보관 클래스 구현
private class Customer { private String name; private String id; }
영화를 예매하자
- ReservationAgency : 데이터 클래스들을 조합해서 영화 예매 절차를 구현하는 클래스
public class ReservationAgency { public Reservation reserve(Screening screening, Customer customer, int audienceCount) { Movie movie = screening.getMovie(); boolean discountable = false; // DiscountCondition에 대해 루프를 돌면서 할인 가능 여부를 확인 for (DiscountCondition condition : movie.getDisocuntConditions()) { 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; // discountable 변수의 값을 체크하고 적절한 할인 정책에 따라 요금을 계산 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); } else { fee = movie.getFee(); } return new Reservation(customer, screening, fee, audienceCount); } }
설계 트레이드 오프
캡슐화
- 상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다..
- 변경될 가능성이 높은 부분을 구현 이라고 부르고
- 상대적으로 안정적인 부분을 인터페이스 라고 부른다
- 객체를 설계하기 위한 가장 기본적인 아이디어는 변경의 정도에 따라 구현과 인터페이스를 구현하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다
- 객체지향에서 가장 중요한 원리는 캡슐화
- 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류
- 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화하자
- 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다
- 변경될 수 있는 어떤 것이라도 캡슐화 해야함
응집도와 결합도
- 응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다
- 결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도
- 좋은 설계란 높은 응집도와 낮은 결합도를 가진 모듈로 구성된 설계를 의미
- 애플리케이션을 구성하는 각 요소의 응집도가 높고
- 서로 느슨하게 결합돼 있다면
- 그 애플리케이션은 좋은 설계를 가졌다고 볼 수 있음
데이터 중심의 영화 예매 시스템의 문제점
- 데이터 중심의 설계는 캡슐화를 위반하고 객체의 내부 구현을 인터페이스의 일부로 만든다
- 캡슐화 위반
- 높은 결합도
- 낮은 응집도
- 책임 중심의 설계는 객체의 내부 구현을 안정적인 인터페이스 뒤로 캡슐화한다
캡슐화 위반
- Moive 클래스
- 오직 메서드를 통해서만 객체의 내부 상태에 접근할 수 있다
getFee
메서드와setFee
메서드는 Movie 내부에 Money 타입의 fee라는 이름의 인스턴스 변수가 존재한다는 사실을 퍼블릭 인터페이스에 드러냄- 접근자와 수정자에 과도하게 의존...
- 내부 구현을 인터페이스의 일부로 만들기 때문에 캡슐화를 위반한다......
public class Movie { private Money fee; public Money getFee(){ return fee; } public void setFee(Money fee) { this.fee =fee; } }
높은 결합도
- ReservationAgency
- 한 명의 예매 요금을 계산하기 위해
getFee
메서드를 호 출하며 계산된 결과를Money fee
에 저장 - 이때 fee의 타입을 변경한다면........................................?
getFee
메서드의 반환타입도 함께 수정해야 할 것..
- 대부분의 제어 로직을 가지고 있는 제어 객체인 ReservationAgency가 모든 데이터 객체에 의존하고 있다...
- 하나 변경하면 다 변경해야함........
- 한 명의 예매 요금을 계산하기 위해
public class ReservationAgency { public Reservation reserve(Screening screening, Customer customer, int audienceCount) { ... Money fee; // discountable 변수의 값을 체크하고 적절한 할인 정책에 따라 요금을 계산 if (discountable) { ... fee = movie.getFee().minus(discountAmount); } else { fee = movie.getFee(); } ... } }
낮은 응집도
- 다음과 같은 수정사항이 발생하는 경우...ReservationAgency의 코드를 수정해야 함..
- 할인 정책이 추가될 경우
- 할인 정책별로 할인 요금을 계산하는 방법이 변경될 경우
- 할인 조건이 추가되는 경우
- 할인 조건별로 할인 여부를 판단하는 방법이 변경될 경우
- 예매 요금을 계산하는 방법이 변경될 경우
- 모듈의 응집도가 낮을 때 발생하는 대표적인 증상
- 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다......
단일책임원칙 SRP
- 모듈의 응집도가 변경과 연관이 있음
- 클래스는 단 한가지의 변경 이유만 가져야 한다
- 클래스의 응집도를 높일 수 있는 설계 원칙
자율적인 객체를 향해
캡슐화를 지켜라
- 객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.
- 객체에게 의미 있는 메서드는 객체가 책임져야 하는 무언가를 수행하는 메서드
스스로 자신의 데이터를 책임지는 객체
- 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요
- 이 객체는 어떤 데이터를 포함해야 하는가?
- 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?
- ReservationAgency 할인 조건 표현
- 어떤 데이터를 관리해야 하는가?
public class DiscountCondition { private DiscountConditionType type; private int sequence; private DayOfWeek dayOfWeek; private LocalTime startTime; private LcoalTime endTime; }
- 이 데이터에 대해 수행할 수 있는 오퍼레이션이 무엇인가?
- 할인 조건 : 순번 조건과 기간 조건의 두 가지 종류가 존재
- 두 가지 할인 조건을 판단할 수 있게 두 개의
isDiscountalbe
메서드가 필요
public class DiscountCondition { public DiscountConditionType getType() { return type; } // 기간 조건일 경우 public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) { if (type != DiscountConditionType.PERIOD) { throw new IllegalArgumentException(); } return this.dayOfWeek.equals(dayOfWeek) && this.startTime.compareTo(time) <= 0 && this.endTime.compareTo(time) >= 0; }
// 순번 조건일 경우
public boolean isDiscountable(int sequence) {
if (type != DiscountConditionType.SEQUENCE) {
throw new IllegalArgumentException();
}
return this.sequence == sequence;
}
}
```
- Movie
- 어떤 데이터를 포함해야 하는가?
public class Movie { private String title; private Duratin runningTime; private Money fee; private List<DiscountCondition> discountConditions; private MovieType movieType; private Money discountAmount; private double discountPercent; }
- 이 데이터를 처리하기 위해 어떤 오퍼레이션이 필요한가?
- 영화 요금을 계산하는 오퍼레이션
- 할인 정책을 염두에 둬야 함
- 금액 할인, 비율 할인, 할인 미적용
public class Movie { public MovieType getMovieType() { return movieType; } // 금액 할인 public Money calculateAmountDiscountedFee() { if (movieType != MovieType.AMOUNT_DISCOUNT) { throw new IllegalArgumnetException(); } return fee.minus(discountAMount); } // 비율 할인 public Money calculatePercentDiscountedFee() { if (movieType != MovieType.PERCENT_DISCOUNT) { throw new IllegalArgumentException(); } return fee.minus(fee.times(discountPercent)); } // 할인 미적용 public Money calculateNoneDiscountedFee() { if (movieType != MovieType.NONE_DISCOUNT) { throw new IllegalArgumentException(); } return fee; } }
- 할인 여부를 판단하는 오퍼레이션
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; } }
- 영화 요금을 계산하는 오퍼레이션
- Screening
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); } case NONE_DISCOUNT: return movie.calculateNoneDiscountedFee().times(audienceCount); } return movie.calculateNoneDiscountedFee().times(audienceCount); } }
보완
- 두 번째 설계 역시 데이터 중심의 설계 방식에 속하고 있음
캡슐화 위반
DiscountCondition
은 자기 자신의 데이터를 이용해 할인 가능 여부를 스스로 판단..하고 있다public class DiscountCondition { public DiscountConditionType getType() { return type; } // 기간 조건일 경우 public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) { if (type != DiscountConditionType.PERIOD) { throw new IllegalArgumentException(); } return this.dayOfWeek.equals(dayOfWeek) && this.startTime.compareTo(time) <= 0 && this.endTime.compareTo(time) >= 0; }
// 순번 조건일 경우
public boolean isDiscountable(int sequence) {
if (type != DiscountConditionType.SEQUENCE) {
throw new IllegalArgumentException();
}
return this.sequence == sequence;
}
}
- 여기서 `isDiscountable(DayOfWeek dayOfWeek, LocalTime time)`
- DayOfWeek 타입의 요일 정보와 LocalTime 타입의 시간 정보를 파라미터로 받고 있음
- 이 메서드는 객체 내부에 DayOfWeek 타입의 요일과 LocalTime 타입의 시간 정보가 인스턴스 변수로 포함돼 있따는 사실을 인터페이스를 통해 외부에 노출하고 있다
- `isDiscountalbe(int sequence)` 역시
- int 타입의 순번 정보를 포함하고 있음을 외부에 노출함
- `Movie` 역시
```java
public class Movie {
public MovieType getMovieType() {
return movieType;
}
// 금액 할인
public Money calculateAmountDiscountedFee() {
if (movieType != MovieType.AMOUNT_DISCOUNT) {
throw new IllegalArgumnetException();
}
return fee.minus(discountAMount);
}
// 비율 할인
public Money calculatePercentDiscountedFee() {
if (movieType != MovieType.PERCENT_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee.minus(fee.times(discountPercent));
}
// 할인 미적용
public Money calculateNoneDiscountedFee() {
if (movieType != MovieType.NONE_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee;
}
}
- 3가지 할인 정책에 관한 메서드는 할인 정책에는 금액 항린 정책, 비율 할인 정책, 미적용의 세 가지가 존재한다는 사실을 드러내고 있다. . . .
- 새로운 할인 정책이 축되거나 제거된다면?
- 이 메서드들에 의존하는 모든클라이언트가 영향을 받을 것...
캡슐화의 진정한 의미
- 캡슐화란 변할 수 있는 어떤 것이라도 감추는 것
- 설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화 해야 함
높은 결합도
- Movie와 DiscountCondition 사이의 결합도는 높을 수밖에 없다
- DiscountCondition의 기간 할인 조건의 명칭이 PERIOD에서 다른 값으로 변경된다면 Movie를 수정해야 한다
- DiscountCondition의 종류가 추가되거나 삭제된다면 Movie 안의 if~else 구문을 수정해야 함
- 각 DiscountCondition의 만족 여부를 판단하는 데 필요한 정보가 변경된다면 Movie의 isDiscountable 메서드로 전달 된 파라미터를 변경해야 함
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; } }
데이터 중심 설계의문제점
- 두 번째 설계가 변경에 유연하지 못한 이유는 캡슐화를 위반했기 때문이다
- 데이터 중심의 설계가 변경에 취약한 이유는
- 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요함
- 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정