객체지향 설계의 핵심은 협력, 역할, 책임이다.
협력 : 애플리케이션 기능 구현을 위해 메시지를 주고 받는 객체간의 상호작용
책임 : 객체가 다른 객체와 협력하기 위해 수행하는 행동
역할 : 대체 가능한 책임의 집합
객체 지향 설계란
: 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동
01. 데이터 중심의 영화 예매 시스템
: 자신이 포함하고 있는 데이터 조작에 필요한 오퍼레이션 정의
: 객체의 상태에 초점을 맞춤
: 객체를 독립된 데이터 덩어리로 바라봄
=> 객체의 상태에 초점을 맞추면 일어나는 일
- 상태는 내부 구현에 속한다 => 불안정하다
- 인터페이스에 내부 상태가 스며든다 => 캡슐화가 깨진다
- 상태변경이 인터페이스 변경을 초래하여 변경이 전파된다
데이터를 준비하자
데이터 중심 설계의 클래스들
> 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; // 할인 비율
}
> DiscountConditionType Enum
public enum DiscountConditionType {
SEQUENCE, // 순번조건
PERIOD // 기간 조건
}
> DiscountCondition
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
}
> Screening객체
public class Screening {
private Movie movie; // 영화
private int sequence; // 순번
private LocalDateTime whenScreened; // 상영일자
}
> Reservation 객체
public class Reservation {
private Customer customer;
private Screening screening;
private Money fee;
private int audienceCount;
}
> Customer 객체
public class Customer {
private String name;
private String id;
public Customer(String name, String id) {
this.id = id;
this.name = name;
}
}
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().times(audienceCount);
}
return new Reservation(customer, screening, fee, audienceCount);
}
}
2. 트레이드 오프
캡슐화
: 불안정한 부분을 감추고 안정적인 부분만을 노출하는 것
: 구현 => 불안정한 것 / 인터페이스 => 안정적인 것
: 불안정한 부분이 변경되더라도 객체 안으로 변경의 여파를 고립시킴
응집도와 결합도
응집도 | - 모듈에 포함된 내부 요소들이 연관되어 있는 정도 - 변경이 발생했을 때 모듈 내부에서 발생하는 변경의 정도(내부) - 변경 수용을 위해 오직 하나의 모듈만을 수정 => 응집도가 높음 - 변경해야 하는 부분이 분산되어 있음=> 응집도가 낮음 |
결합도 | - 다른 모듈에 대해 얼마나 많은 지식을 가지는지 나타냄 - 한 모듈이 변경되었을 때 다른 모듈의 변경을 요구하는 정도(외부) - 하나의 변경 -> 하나의 모듈만 영향 => 결합도가 낮음 - 하나의 변경 -> 다른 모듈에게 영향 => 결합도가 높음 [변경의 원인] - 내부 구현 변경 시 다른 모듈에게 영향 => 결합도가 높음 - 인터페이스 변경시에만 다른 모듈에게 영향 => 결합도가 낮음 |
=> 우리의 목적은 높은 응집도 + 낮은 결합도
03. 데이터 중심의 영화 예매 시스템의 문제점
문제1. 캡슐화 위반
public Money getFee() {
return fee;
}
public void setFee(Money fee) {
this.fee = fee;
}
- getter/setter는 Movie 내부의 인스턴스 변수에 대한 정보를 드러낸다
=> 내부에 저장할 데이터에 초점을 맞추면서 생기는 문제
[추측에 의한 설계] by Allen Holub
- 객체가 다양한 상황에서 사용될 것이라는 추측
=> 내부 상태를 드러내는 메서드를 최대한 많이 추가해야 한다는 압박에 시달림
=> 퍼블릭 인터페이스에 내부 구현이 그대로 스며듦
문제2. 높은 결합도
2- 1) 강한 결합
- 객체의 내부 구현이 인터페이스에 드러남 => 강하게 결합될 수밖에 없음
- getFee로 꺼내어와 연산 => fee 타입변경시, ReservatinAgency도 함께 수정 필요
2-2) 제어로직의 집중
- 데이터를 사용하는 제어로직이 특정 객체에 집중되게 됨
- 어떤 데이터 객체를 수정하던 제어로직을 함께 수정할 수 밖에 없음
문제3. 낮은 응집도
단일 책임 원칙(SRP)
클래스는 단 한가지의 변경 이유만을 가져야 함
=> 응집도가 낮은 클래스는 변경의 이유가 여러가지임
ReservationAgency의 변경 이유를 몇가지 생각해보자
- 할인 정책 추가 시
- 할인 정책 별로 요금 계산 방법 변경 시
- 할인 조건 추가 시
- 할인 조건 별로 할인 여부를 판단하는 방법이 변경될 경우
- 예매 요금을 계산하는 방법이 변경될 경우
[낮은 응집도의 2가지 문제 ]
>> 변경과 관련없는 코드가 영향을 받는다
ex) ReservationAgency에 할인 정책을 추가하면, 할인 조건에 영향을 미칠 수 있다
=> 할인 정책을 선택하는 코드와 할인 조건을 판단하는 코드가 함께 위치해 있기 때문이다.
>> 하나의 요구사항을 반영하기 위해 여러 모듈을 수정해야 한다
ex) 새로운 할인 정책 추가시
- MovieType에 할인 정책 추가
- ReservationAgency에서 새로운 분기문 추가
- Movie에 새로운 요금 게산 로직 추가
=> 하나의 변경을 반영하기 위해 여러 클래스 수정 필요
4. 자율적인 객체를 향해
캡슐화를 지켜라
- 속성의 가시성을 private으로 설정해도 접근자/수정자로 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다
ex) Rectangle의 너비와 높이를 증가시키는 코드
public class Rectangle {
private int left;
private int right;
private int up;
private int bottom;
public Rectangle(int left, int right, int up, int bottom) {
this.left = left;
this.right = right;
this.up = up;
this.bottom = bottom;
}
public int getLeft() {
return left;
}
public int getRight() {
return right;
}
public int getUp() {
return up;
}
public int getBottom() {
return bottom;
}
public void setRight(int right) {
this.right = right;
}
public void setBottom(int bottom) {
this.bottom = bottom;
}
}
public class AnyClass {
void anyMethod(Rectangle rectangle, int multiple) {
rectangle.setRight(rectangle.getRight() * multiple);
rectangle.setBottom(rectangle.getBottom() * multiple);
}
}
문제1. 코드 중복
- 다른 곳에서도 사각형의 너비와 높이를 증가시켜야 하다면 같은 코드가 반복됨
문제2. 변경 취약
- right,bottom 대신 length,height로 수정 => 내부 로직을 수정해야 함
=> 해결방안 : 캡슐화( left, right, up, bottom이라는 4가지 인스턴스 변수를 외부로 노출하지 말아야 함)
public class Rectangle {
public void enLarge(int multiple){
right*=multiple;
bottom*=multiple;
}
}
스스로 자신의 데이터를 책임지는 객체
- 각 데이터에 대해 스스로 책임 지도록 리팩터링을 진행해보자
리팩터링1. DiscountCondition이 할인 조건 만족 여부 판단 가능하도록
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
// 스스로 순번 조건 판단
public boolean isDiscountable(int sequence) {
if (type == DiscountConditionType.PERIOD) {
throw new IllegalArgumentException();
}
return this.sequence == sequence;
}
// 스스로 기간 조건 판단
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
if (type == DiscountConditionType.SEQUENCE) {
throw new IllegalArgumentException();
}
return this.dayOfWeek.equals(dayOfWeek) &&
this.startTime.compareTo(time) <=0 &&
this.endTime.compareTo(time) >=0;
}
}
리팩터링2. Movie가 스스로 요금 계산 by 할인 가능여부 판단
public Money calculateAmountDiscountedFee() {
if (this.movieType != MovieType.AMOUNT_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee.minus(this.discountAmount);
}
public Money calculatePercentDiscountedFee() {
if (this.movieType != MovieType.PERCENT_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee.minus(fee.times(this.discountPercent));
}
public Money calculateNoneDiscountedFee() {
if (this.movieType != MovieType.NONE_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee;
}
Movie가 가진 Condition을 순회하면서 만족 여부 판단
public boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
for (DiscountCondition condition : discountConditions) {
if (condition.getType() == DiscountConditionType.SEQUENCE) {
if (condition.isDiscountable(sequence)) {
return true;
}
} else {
if (condition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
return true;
}
}
}
return false;
}
리팩터링3. Screening에서 요금 계산
public Money calculateFee(){
switch(movie.getMovieType()){
case AMOUNT_DISCOUNT:
if(movie.isDiscountable(whenScreened, sequence)){
return movie.calculateAmountDiscountedFee();
}
break;
case PERCENT_DISCOUNT:
if(movie.isDiscountable(whenScreened, sequence)){
return movie.calculatePercentDiscountedFee();
}
break;
}
return movie.calculateNoneDiscountedFee();
}
최종 리팩터링 : ReservationAgency의 reserve 코드
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
return new Reservation(customer, screening, screening.calculateFee(), audienceCount);
}
}
=> 리팩터링을 통해 캡슐화가 조금이라도 된 코드를 얻을 수 있었다
=> 데이터를 처리하는데 필요한 메서드를 데이터를 가지고 있는 객체에서 스스로 처리한다.
5. 하지만 여전히 부족하다
캡슐화 위반
DiscountCondition의 isDiscountable 메서드들
public class DiscountCondition {
// 스스로 순번 조건 판단
public boolean isDiscountable(int sequence);
// 스스로 기간 조건 판단
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time);
}
- 객체 내부의 DayOfWeek 타입의 요일과 LocalTime 타입의 시간 정보가 인스턴스 변수로 있음을 인터페이스로 드러냄
=> 내부 구현 변경시 인터페이스에 바로 side effect
=> 내부 구현이 인터페이스에 스며듦
Movie의 calculateXXXDiscountFee() 메서드들
public Money calculateAmountDiscountedFee();
public Money calculatePercentDiscountedFee();
public Money calculateNoneDiscountedFee();
- 각각 어떤 할인 정책을 사용하고 있는지가 노출됨
- 할인 정책이 추가되거나 제거된다면 이 메서드들에 의존하는 모든 클라이언트가 영향을 받는다
=> 캡슐화는 불안정한 것을 감추는 것이다.
=> 그러나, 현재는 인터페이스에 불안정한 것(내부 구현)이 드러나있다
높은 결합도
public boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
for (DiscountCondition condition : discountConditions) {
if (condition.getType() == DiscountConditionType.SEQUENCE) {
if (condition.isDiscountable(sequence)) {
return true;
}
} else {
if (condition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
return true;
}
}
}
return false;
}
=> Movie에서 Discountcondition의 내부구현이 노출되었기에 강한 결합도를 가짐
=> 판별방법 : 인터페이스가 아닌 내부 구현 수정의 영향을 받는가?
실험 : 내부구현 변경시 영향을 받는다
변경1. 기간 할인 조건의 명칭이 PERIOD에서 다른 값으로 변경되면 Movie를 수정해야 한다
변경2. DiscountCondition 종류가 추가되면 if문의 분기를 추가해야 한다
변경3. DiscontCondition 만족 여부를 판단하는데 필요한 정보가 변경되면 isDiscountable의 파라미터가 변경됨
낮은 응집도
Screening의 상황을 살펴보자
public Money calculateFee(){
switch(movie.getMovieType()){
case AMOUNT_DISCOUNT:
if(movie.isDiscountable(whenScreened, sequence)){
return movie.calculateAmountDiscountedFee();
}
break;
case PERCENT_DISCOUNT:
if(movie.isDiscountable(whenScreened, sequence)){
return movie.calculatePercentDiscountedFee();
}
break;
}
return movie.calculateNoneDiscountedFee();
}
만약 DiscountCondition의 할인 여부를 판단하는데 필요한 정보가 달라진다면
=> Movie의 isDiscountable 메서드로 전달해야 하는 파라미터 종류가 변경된다
=> Screening에서 Movie의 isDiscountable 메서드로 전달하는 부분도 함께 변경된다
즉, 할인 조건의 종류 변경을 위해서 코드의 여러곳을 동시에 변경해야 한다
=> 이는 캡슐화가 위반되어 인터페이스에 내부 구현이 묻어있기 때문이다.
6. 데이터 중심 설계의 문제점
문제1. 객체의 행동보다 상태에 초점을 맞춘다
- 데이터 중심 설계는 너무 이른 시기에 내부 구현에 초점을 맞춘다
- 데이터 > 오퍼레이션을 결정하는 과정은 데이터에 관한 지식이 인터페이스에 스며들게 된다
=> 캡슐화에 실패한다
문제2. 협력이라는 문맥을 고려하지 못한다
- 올바른 객체지향의 무게중심은 항상 내부가 아니라 외부에 있어야 한다
- 그러나, 데이터 중심 설계의 초점은 항상 내부를 향한다 => 데이터의 세부정보를 먼저 결정하므로
- 객체의 인터페이스에 내부구현이 묻어있어 협력이 구현 세부사항에 종속되어버린다
'기술 서적' 카테고리의 다른 글
[오브젝트] ch5. 책임 할당하기 (0) | 2024.04.13 |
---|---|
[오브젝트] ch3. 역할, 책임, 협력 (0) | 2024.04.11 |
[오브젝트] ch2. 객체지향 프로그래밍 (0) | 2024.04.09 |
[오브젝트] ch1. 객체, 설계 (0) | 2024.04.07 |
[스터디] 이펙티브 자바 - item4. 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2024.03.12 |