들어가며
객체지향 설계의 핵심은 역할, 책임, 협력이다.
협력 - 애플리케이션의 기능을 구현하기 위해 메세지를 주고받는 객체들 사이의 상호작용.
책임 - 객체가 다른 객체와 협력하기 위해 수행하는 행동.
역할 - 대체 가능한 책임의 집합.
역할, 책임, 협력중에서 가장 중요한 것은 책임이다.
객체지향 설계란? 객체에게 올바른 책임을 할당하며, 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다.
결합도와 응집도를 합리적으로 유지할 수 있는 원칙이 있다.
객체의 상태가 아닌 행동에 초첨을 맞추는것이다.
객체를 단순 데이터 집합으로 바라보는 시각은 객체의 내부 구현을 퍼블릭 인터페이스에 노출시켜 설계가 변경에 취약해지기 때문이다.
행동 즉, 책임 주도 설계가 데이터 중심 설계보다 어떤 면이 좋은지 살펴보기로 한다.
이에 앞서 데이터 중심 설계를 구성해본다.
01 데이터 중심의 영화 예매 시스템
객체의 상태는 구현에 속한다. 구현은 불안정하기 때문에 변하기 쉽다.
그러므로 상태를 객체 분할의 중심축으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화의 원칙이 무너진다.
결과적으로 상태 변경은 인터페이스의 변경을 초래하며 이 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼진다.
그에 비해 객체의 책임은 인터페이스에 속한다.
객체는 책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하는 데 필요한 상태를 캡슐화함으로써 구현 변경에 대한 파장이 외부로 퍼져나가는것을 방지한다.
데이터 중심 설계 예제
@Getter
@Setter
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 enum MovieType {
/**
* 가격 할인
*/
AMOUNT_DISCOUNT,
/**
* 비율 할인
*/
PERCENT_DISCOUNT,
/**
* 할인 사용 안함
*/
NONE_DISCOUNT
}
public enum DiscountConditionType {
/**
* 상영 순번 조건
*/
SEQUENCE,
/**
* 상영 시간 조건
*/
PERIOD
}
@Getter
@Setter
public class DiscountCondition {
private DiscountConditionType type;
/**
* 할인 대상 상영 회차
*/
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
}
@Getter
@Setter
public class Screening {
/**
* 상영될 영화 정보
*/
private Movie movie;
/**
* 상영 회차 정보
*/
private int sequence;
/**
* 상영 시작 시간
*/
private LocalDateTime whenScreened;
}
@Getter
@Setter
public class Reservation {
/**
* 예약 고객정보
*/
private Customer customer;
/**
* 예약 상영 정보
*/
private Screening screening;
/**
* 예약 금액
*/
private Money fee;
/**
* 예약 인원 수
*/
private int audienceCount;
}
public class Customer {
private String name;
private String id;
public Customer(String name, String id) {
this.id = id;
this.name = name;
}
}
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;
default:
}
fee = movie.getFee().minus(discountAmount);
} else {
fee = movie.getFee();
}
return new Reservation(customer, screening, fee.times(audienceCount), audienceCount);
}
}
위의 그림으로 표현된 데이터 중심 설계 방법과 책임 중심 설계 방법을 비교하기에 앞서
비교하기 위해 사용할 수 있는 기준을 알아본다.
02 설계 트레이드오프
캡슐화
캡슐화란 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다.
변경될 가능성이 높은 부분인 구현은 숨기고, 상대적으로 안정적인 부분인 인터페이스는 공개함으로써 변경의 여파를 통제할 수 있다.
즉, 변경의 정도에 따라 구현과 인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.
응집도와 결합도
응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다. 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다.
결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다. 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분보다는 꼭 필요한 지식만 알고 있다면 두 모듈은 낮은 결합도를 가진다.
높은 응집도와 낮은 결합도를 가진 설계를 추구해야 하는 이유는 설계를 변경하기 쉽게 만들기 때문이다.
변경의 관점에서 응집도란 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도를 측정할 수 있다.
※ 자바의 String이나 ArrayList는 변경될 확률이 매우 낮으므로 결합도가 높아도 상관없다.
캡슐화를 지키면 모듈안의 응집도는 높아지고 모듈사이의 결합도는 낮아진다.
-> 응집도와 결합도를 고려하기 전에 먼저 캡슐화를 향상시켜야한다.
03 데이터 중심의 영화 예매 시스템의 문제점
데이터 중심 설계가 가진 문제점
- 캡슐화 위반
- 높은 결합도
- 낮은 응집도
캡슐화 위반
public class Movie {
private Money fee;
public Money getFee() {
return fee;
}
public void setFee(Money fee) {
this.fee = fee
}
}
fee의 값을 읽거나 수정하기 위해서는 getFee 메서드와 setFee 메서드를 사용해야 한다.
이를 통해 외부에서 직접 인스턴스변수에 접근할 수 없지만 캡슐화는 보장되지 못한다.
getter, setter는 fee 인스턴스 변수가 Movie 내부에 존재한다는 사실을 퍼블릭 인터페이스에 노골적으로 드러내기 때문이다.
캡슐화를 어긴 근본적인 원인은 객체가 수행할 책임이 아닌 내부에 저장할 데이터에 초점을 맞췄기 때문이다.
높은 결합도
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
...
Movie fee;
if (discountable) {
...
fee = movie.getFee().minus(discountedAmount).times(audienceCount);
} else {
fee = movie.getFee();
}
...
}
}
fee의 타입을 변경하면 getFee 메서드의 반환 타입도 함께 수정해야하며 getFee 메서드를 호출하는 ReservationAgency의 구현도 변경된 타입에 맞게 수정되어야한다.
데이터 중심 설계는 캡슐화를 약화시켜 클라이언트가 객체의 구현에 강하게 결합된다.
낮은 응집도
서로 다른 이유로 변경되는 코드가 하나의 모듈안에 공존할 때 모듈의 응집도가 낮다고 말한다.
현재 예로든 데이터 중심 설계는 새로운 할인 정책, 할인 조건을 추가하기 위해 하나 이상의 클래스를 동시에 수정해야 한다. (=ReservationAgency)
결론
04 자율적인 객체를 향해...😒
...
05 하지만 여전히 부족하다...😴
...
책에서는 데이터 중심 설계를 예로 들면서 점차 낮은 응집도에서 높은 응집도로
높은 결합도에서 낮은결합도로 또, 캡슐화를 만족 시키는 과정이 설명되지만 애초에 데이터 중심 설계가 아닌 행위 중심설계로 구현한다면 개선할 필요가 사라진다. 😡
결론적으로
객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.
또한 단순히 인스턴스 멤버변수를 직접적으로 접근하지 못하도록 private로 선언하고 getter, setter로 접근 하게끔 하더라도 캡슐화가 이루어졌다고 말할 수 없다. 이는 객체 설계가 행위보다 데이터에 맞춰져있다면 캡슐화를 위반할 가능성이 크다.
협력에 참여하기 위해 어떤 역할의 책임들이 있는지 파악하고 필요로하는 메시지는 내부구현을 통해 외부로 부터 꽁꽁 숨긴채 퍼블릭 인터페이스를 통해 제공하면 설계의 가장 큰 이유인 변경의 비용이 최대로 줄기 때문에 캡슐화가 이루어졌다고 말할 수 있으며 이는 곧 응집도는 높아지고 결합도는 낮아지게 됨을 의미한다.
데이터가 아닌 책임에 초점을 맞추자!
'📚 Book > Object' 카테고리의 다른 글
08 의존성 관리하기 (0) | 2020.03.08 |
---|---|
07 객체 분해 (0) | 2020.02.29 |
06 메세지와 인터페이스 (0) | 2020.02.19 |
05 책임 할당하기 (1) | 2020.02.16 |
03 역할, 책임, 협력 (0) | 2020.02.02 |