객체지향 설계의 핵심
- 협력 : 애플리케이션의 기능을 구현하기 위해 메시지를 주고받는 객체들 사이의 상호작용
- 책임 : 객체가 다른 객체와 협력하기 위해 수행하는 행동
- 역할 : 대체 가능한 책임의 집합
객체지향 설계
- 객체에 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동
- 객체지향의 설계의 핵심은 책임이다.
- 책임을 할당하는 작업이 응집도와 결합도 같은 설계 품질과 깊이 연관되어 있다.
설계
- 설계는 변경을 위해 존재한다.
- 훌륭한 설계한 합리적인 비용안에서 변경을 수용할 수 있는 구조를 만드는 것이다.
- 변경 가능한 설계는 응집도가 높고 결합도가 낮은 요소로 구성된다.
결합도와 응집도의 원칙
- 객체의 상태가 아니라 객체의 행동에 초점을 맞춘다.
- 상태만으로 객체를 바라보면 퍼블릭 인터페이스에 객체를 노출하게 되고, 변경에 취약해진다.
- 책임은 객체의 상태에서 행동으로, 객체와 객체 사이의 상호작용으로 설계 중심을 이동시킨다.
데이터 중심의 영화 예매 시스템
데이터를 준비하자
객체의 상태는 구현에 속한다
- 구현은 불안정하기 때문에 변하기 쉽다.
- 상태를 객체 분할의 중심축으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화의 원칙이 무너진다.
- 상태변경은 인터페이스의 변경을 초래한다. 그리고 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼진다.
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 getter, setter...
}
public enum MovieType {
AMOUNT_DISCOUNT,
PERCENT_DISCOUNT,
NONE_DISCOUNT
}
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
// public getter, setter...
}
public enum DiscountConditionType {
SEQUENCE,
PERIOD
}
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
// public getter, setter...
}
public class Reservation {
private Customer customer;
private Screening screening;
private Money fee;
private int audience;
//public getter, setter...
}
public class Customer {
private String name;
private String id;
// constructor...
}
- 할인조건의 목록이 맴버로 포함되어 있다.
- 할인 금액이 맴버로 포함되어 있다.
- 할인률이 맴버로 포함되어 있다.
데이터 중심의 접근 방법이다. 데이터 중심의 설계에서는 객체가 포함해야 하는 데이터에 집중한다.
영화를 예매하자
public class ReservationAgency {
public Reservation reverse(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();
breal;
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);
}
}
설계 트레이드오프
캡슐화
- 상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위한 것이다.
- 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 잇다.
- 변경될 가능성이 높은 부분을 구현 이라고 한다. 상대적으로 안정적인 부분을 인터페이스 라고 한다.
응집도와 결합도
응집도
- 모듈에 포함된 내부 요소들이 연관되어 있는 정도
- 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다
- 객체지향 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.
변경 관점에서 응집도란 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다. 하나의 변경을 수용하기 위해 모듈 전체가 함께 변경된다면 응집도가 높은 것이고, 모듈의 일부만 변경된다면 응집도가 낮은 것이다. 또한 하나의 변경에 대해 하나의 모듈만 변경된다면 응집도가 높지만, 다수의 모듈이 함께 변경되야 한다면 응집도가 낮은 것이다.
결합도
- 의존성의 정도
- 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도.
변경 관점에서 결합도란 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다. 하나의 모듈을 수정할 때 얼마나 많은 모듈을 함께 수정해야 하는지를 나타낸다. 결합도가 높을수록 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경하기가 어려워진다.
좋은 설계란 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계다. 좋은 설계를 만들기 위해서는 높은 응집도와 낮은 결합도를 추구해야 한다. 좋은 설계가 변경경과 관련되 것이고 응집도와 결홉다의 정도가 설계의 품질을 결정한다면 자연스럽게 다음과 같은 결론에 도달하게 된다. 응집도와 결합도는 변경과 관련된 것이다.
일반적으로 변경될 확률이 매우 낮은 라이브러리, 내장함수, 프레임워크 같이 성숙도가 높은 곳에 의존하는 경우는 결합도를 따지지 않는다.
데이터 중심의 영화 예매 시스템의 문제점
캡슐화 위반
getter
,setter
: 이름부터 내부 변수를 공개한다.
구현을 캡슐화할 수 있는 적절한 책임은 협력이라는 문맥을 고려할 때만 얻을 수 있다.
추측에 의한 설계 전략 Design-by-guessing strategy
- 접근자와 수정자에 과도하게 의존하는 설계 방식
- 객체가 사용될 문맥을 추측할 수 밖에 없는 경우에 개발자가 최대한 많은 접근자 메서드를 추가하는 경우
높은 결합도
- 데이터 중심 설계는 결국 접근자와 수정자를 통해 내부 구현을 인터페이스의 일부로 만들기 때문에 캡슐화를 위반한다.
- 영화를 예매하는
ReservationAgency
에서 호출하는 구현들이 변경되면ReservationAgency
의 코드도 변경되어야 한다. - 데이터중심의 설계는 전체 시스템을 하나의 거대한 의존성 덩어리로 만들어 버리기 때문에 어떤 변경이라도 일단 발생하고 나면 시스템 전체가 요동칠 수 밖에 없다.
낮은 응집도
낮은 응집도는 두가지 측면에서 설계에 문제를 일으킨다.
- 변경의 이유가 서로 다른 코드들을 하나의 모듈안에 뭉쳐놨기 때문에 변경과 아무 상관이 없는 코드들이 영향을 받게 된다. 어떤 코드를 수정한 후에 아무런 상관도 없던 코드에 문제가 발생하는 것은 모듈의 응집도가 낮을 때 발생하는 대표적인 증상이다.
- 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다. 응집도가 낮을 경우 다른 모듈에 위치해야 할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다.
단일 책임 원칙 Single Responsibility Principle
모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위해 제시한 원칙. 클래스는 단 한 가지의 변경 이유만 가져야 한다는 것이다.