목차
- ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.
- 모든 이미지의 출처는 오브젝트(조용호 저) 책 입니다.
CHAPTER 04. 설계 품질과 트레이드오프
- 코드 (github)
⚈ ☆ (2장 관련한 내용) 예를들어 책에서 설명된 영화 할인 정책에서, 현재는 정책이 금액 정책 하나지만 차후 정책이 추가될게 분명해서 미리 추상화 해둔다면 이건 다형성일까? -> 다형성이 맞긴 하지만, 의미 없는 다형성 (YAGNI)
⚈ 객체지향 설계의 핵심은 책임
- 책임이 객체지향 애플리케이션 전체의 품질을 결정
- 객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동
⚈ 설계는 변경을 위해 존재하고 변경에는 어떤 식으로든 비용이 발생한다.
- 훌륭한 설계란 합리적인 비용 안에서 변경을 수용할 수 있는 구조를 만드는 것
⚈ 가끔씩은 좋은 설계보다는 나쁜 설계를 살펴보는 과정에서 통찰을 얻기도 한다. -> ☆ 챕터 4에서 하려고 하는 것!
01 데이터 중심의 영화 예매 시스템
⚈ 객체지향 설계에서 시스템을 객체로 분할하는 두 가지 방법
- 상태(데이터)를 분할의 중심축으로 : 객체는 자신이 포함하고 있는 데이터를 조작하는 데 필요한 오퍼레이션을 정의
- 책임을 분할의 중심축으로 : 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관
⚈ 객체의 상태 = 객체가 저장해야 하는 데이터의 집합 (상태 = 데이터). 객체의 상태는 구현에 속한다.
⚈ 객체의 상태는 구현에 속한다.
- 구현은 불안정하기 때문에 변화기 쉽다.
- 상태를 객체 분할의 중심축으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화의 원칙이 무너진다.
- 상태 변경 -> 인터페이스 변경을 초래 -> 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼짐
- 따라서 변경에 취약
⚈ 객체의 책임은 인터페이스에 속한다.
- 책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하는 데 필요한 상태를 캡슐화 -> 구현 변경에 대한 파장이 외부로 퍼져나가는 것을 방지
- 따라서 책임에 초점을 맞추면 상대적으로 변경에 안정적인 설계를 얻을 수 있다.
02 설계 트레이드오프
[ 캡슐화 ]
⚈ 상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서이다.
⚈ 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문
⚈외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제
⚈ 객체지향에서 가장 중요한 원리
⚈ 변경될 수 있는 어떤 것이라도 캡슐화해야 한다!
- ☆ 내부 필드 전부 private으로 두고, 외부에 모든 필드에 대해 getter-setter 썼다고 캡슐화한게 아님.
[ 응집도와 결합도 ]
⚈ 응집도
- 모듈에 포함된 내부 요소들이 연관돼 있는 정도
- 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈을 높은 응집도를 가짐
- 객체지향의 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.
- ☆ 행동들이 얼마나 상태를 응집도 있게 쓰는지
- 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도.
- 응집도 높음 = 변경의 대상과 범위가 명확해지므로 코드를 변경하기 쉬워짐 = 하나의 변경을 수용하기 위해 모듈 전체가 함께 변경됨, 하나의 변경에 대해 하나의 모듈만 변경됨
⚈ 결합도
- 의존성의 정도
- 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도
- 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가진다.
- 어떤 모듈이 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면 낮은 결합도를 가진다.
- 객체지향의 관점에서 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다.
- 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도 -> 하나의 모듈을 수정할 때 얼마나 많은 모듈을 함께 수정해야 하는지
- 내부 구현을 변경했을 때 이것이 다른 모듈에 영향을 미치는 경우 두 모듈 사이의 결합도가 높다고 표현한다.
- 퍼블릭 인터페이스를 수정했을 때만 다른 모듈에 영향을 미치는 경우에는 결합도가 낮다고 표현한다.
- 변경될 확률이 매우 적은 안정적인 모듈(자바의 String, ArrayList처럼)에 의존하는 것은 아무런 문제가 안된다. (결합도에 대해 고민할 필요가 없다.)
⚈ 좋은 설계
- 높은 응집도와 낮은 결합도를 가진 모듈로 구성된 설계
- 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계
03 데이터 중심의 영화 예매 시스템의 문제점
⚈ 데이터 중심의 설계가 가진 대표적인 문제점
- 캡슐화 위반
- 높은 결합도
- 낮은 응집도
[ 캡슐화 위반 ]
⚈ Movie 코드 -> 직접 객체의 내부에 접근할 수 없어서 캡슐화의 원칙을 지키고 있는 것 처럼 보인다.
public class Movie {
private Money fee;
public Money getFee() {
return fee;
}
public void setFee(Money fee) {
this.fee = fee;
}
}
- Movie 내부에 Movie 타입의 fee라는 이름의 인스턴스 변수가 존재한다는 사실을 퍼블릭 인터페이스에 드러낸다.
- 접근자와 수정자 메서드는 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다.
⚈ 추측에 의한 설계 전략(design-by-guessing strategy)
- 접근자와 수정자에 과도하게 의존하는 설계 방식
- 대부분의 내부 구현이 퍼블릭 인터페이스에 그대로 노출
- 객체가 사용될 문맥을 추측할 수밖에 없는 경우 개발자는 어떤 상황에서도 해당 객체가 사용될 수 있게 최대한 많은 접근자 메서드를 추가하게 되는 것이다.
[ 높은 결합도 ]
⚈ 구현이 객체의 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미
⚈ ReservationAgency 코드 -> 위의 Movie 클래스에서 fee의 타입이 바뀌면 ReservationAgency도 수정해야 한다.
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer,
int audienceCount) {
...
Money fee;
if (discountable) {
...
fee = movie.getFee().minus(discountAmount).times(audienceCount);
} else {
fee = movie.getFee().times(audienceCount);
}
return new Reservation(customer, screening, fee, audienceCount);
}
}
- 데이터 중심 설계 : 제어 객체가 다수의 데이터 객체에 강하게 결합된다. -> 전체 시스템을 하나의 거대한 의존성 덩어리로 만듬
- 이 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수 밖에 없다. -> 어떤 변경이라도 일단 발생하고 나면 시스템 전체가 요동친다.
⚈ ReservationAgency는 모든 의존성이 모이는 결합도의 집결지
[ 낮은 응집도 ]
⚈ 낮은 응집도의 문제
- 변경의 이유가 서로 다른 코드들을 하나의 모듈 안에 뭉쳐놓았기 때문에 변경과 상관이 없는 코드들이 영향을 받게된다.
- 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다.
⚈ 어떤 요구사항 변경을 수용하기 위해 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 증거
04 자율적인 객체를 향해
[ 캡슐화를 지켜라 ]
⚈ 캡슐화는 설계의 제1원리
- 데이터 중심의 설계가 낮은 응집도와 높은 결합도라는 문제로 몸살을 앓게 된 근본적 원인은 캡슐화의 원칙을 위반했기 때문
- 객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.
- 속성의 가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반한 것
[ 스스로 자신의 데이터를 책임지는 객체 ]
⚈ 우리가 상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서이다.
⚈ 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.
⚈ 코드 개선 (github의 step2)
- ReservationAgency로 새어나간 데이터에 대한 책임을 실제 데이터를 포함하고 있는 객체로 옮기는 과정 -> DiscountCondition에 isDiscountable 메서드가 필요. Screening을 통해 Reservation을 생성
- Movie -> getMovieType 메서드, isDiscountable 메서드.
- Screening -> Movie를 통해 영화 요금 계산
- 결합도 측면에서 ReservationAgency에 의존성이 몰려있던 첫 번째 설계보다 개선되었다. -> 두 번째 설계가 첫 번째 설계보다 내부 구현을 더 면밀하게 캡슐화하고 있다.
05 하지만 여전히 부족하다
⚈ 두 번째 설계 역시 데이터 중심의 설계 방식에 속한다
[ 캡슐화 위반 ]
⚈ DiscountCondition
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public DiscountConditionType getType() {
return type;
}
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
...
}
public boolean isDiscountable(int sequence) {
...
}
}
- isDiscountable(DayOfWeek dayOfWeek, LocalTime time) -> 객체 내부에 DayOfWeek, LocalTime이 인스턴스 변수로 포함돼 있다는 사실을 인터페이스를 통해 외부에 노출한다.
- DiscountCondition의 속성을 변경할 경우 -> 두 isDiscountable 메서드의 파라미터를 수정하고 해당 메서드를 사용하는 모든 클라이언트도 함께 수정해야 할 것이다. -> 내부 구현의 변경이 외부로 퍼져나가는 파급 효과(ripple effect)는 캡슐화가 부족하다는 명백한 증거
⚈ Movie
public class Movie {
public Money calculateAmountDiscountedFee() {
...
}
public Money calculatePercentDiscountedFee() {
...
}
public Money calculateNoneDiscountedFee() {
...
}
}
- 할인 정책의 종류를 인터페이스에 노출시킨다. (금액 할인 정책, 비율 할인 정책, 미적용)
- 만약 새로운 할인 정책이 추가되거나 제거될 경우 -> 이 메서드에 의존하는 모든 클라이언트가 영향을 받는다.
[ 높은 결합도 ]
⚈ Movie
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의 기간 할인 조건의 명칭이 PERIOD에서 다른 값으로 변경되면 Movie를 수정해야 한다.
- DiscountCondition의 종류가 추가되거나 삭제된다면 Movie안의 if - else 구문을 수정해야 한다.
- condition.isDiscountable 메소드의 인자가 변경될 경우, Movie의 isDiscountable 인자도 변경되어야 하고 결과적으로 Screening까지 변경을 초래하게 된다.
- 모든 문제의 원인은 캡슐화 원칙을 지키지 않았기 때문
[ 낮은 응집도 ]
⚈ Screening
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);
}
case NONE_DISCOUNT:
movie.calculateNoneDiscountedFee().times(audienceCount);
}
return movie.calculateNoneDiscountedFee().times(audienceCount);
}
}
- DiscountCondition의 isDiscountable의 인자가 바뀌면 Movie도 바뀌어야 하고 Screening도 변경되어야 한다. -> 즉, 하나의 변경을 수용하기 위해 코드의 여러 곳을 동시에 변경해야 한다 = 설계의 응집도가 낮다는 증거
06 데이터 중심 설계의 문제점
⚈ 데이터 중심 설계가 변경에 취약한 이유
- 데이터 중심 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
- 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.
⚈ 데이터 중심 설계는 객체의 행동보다는 상태에 초점을 맞춘다.
- 데이터 중심 설계의 첫 질문 : "이 객체가 포함해야 하는 데이터가 무엇인가?"
- 데이터 중심 설계에 익숙한 개발자 : 일반적으로 데이터와 기능을 분리하는 절차적 프로그래밍 방식을 따른다.
- 접근자와 수정자를 과도하게 추가하게 된다. -> 접근자와 수정자는 public 속성과 큰 차이가 없기 때문에 캡슐화가 무너진다.
- 개선된 step2 코드에서도 데이터를 먼저 결정하고 데이터를 처리하는데 필요한 오프레이션을 나중에 결정하므로 데이터에 관한 지식이 객체의 인터페이스에 고스란히 드러난다. -> 캡슐화에 실패하고 코드는 변경에 취약해진다.
- 결론적으로 데이터 중심의 설계는 너무 이른 시기에 데이터에 대해 고민하기 때문에 캡슐화에 실패하게 된다.
- 객체의 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 미치기 때문에 변경에 취약한 코드를 낳게 된다.
⚈ 데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다.
- 중요한 것은 객체가 다른 객체와 협력하는 방법이다.
- 데이터 중심 설계에서 초점은 객체의 외부가 아니라 내부로 향한다. -> 이미 구현된 객체의 인터페이스를 억지로 끼워맞출수밖에 없다.
- 따라서 변경에 유연하게 대처하지 못한다.
'Study > 오브젝트' 카테고리의 다른 글
[오브젝트] 6장. 메시지와 인터페이스 (2) | 2022.12.08 |
---|---|
[오브젝트] 5장. 책임 할당하기 (0) | 2022.12.02 |
[오브젝트] 3장. 역할, 책임, 협력 (0) | 2022.11.24 |
[오브젝트] 2장. 객체지향 프로그래밍 (2) | 2022.11.23 |
[오브젝트] 1장. 객체, 설계 (2) | 2022.11.16 |
댓글