[오브젝트] chap04을 읽고

좋은 객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다.

위의 정의를 풀어 말하면 객체지향 설계의 핵심은 책임이라는 것이고, 둘째로는 책임을 할당하는 작업이 응집도와 결합도 같은 설계 품질과 깊이 연관되어 있다는 것이다.

설계는 변경을 위해 존재한다. 좋은 객체지향 설계가 되었을 때, 객체들은 결합도와 응집도를 합리적인 수준으로 유지할 수 있고 이에 따라 설계 변경 시에도 합리적인 비용 안에서 변경을 수용할 수 있게 된다.

그럼 결합도를 낮추고 응집도를 높일 수 있는 방법은 무엇일까? 3장에서도 보았듯이 객체의 상태에 집중하기 보단 객체의 행동과 책임에 초점을 맞추는 것이다.

이번 장에서는 데이터 중심의 설계와 책임 중심의 설계의 예시를 보며 좋은 객체지향 설계의 책임 할당 월칙을 이해해보자.

데이터 중심의 설계와 책임 중심의 설계

객체지향 설계에서는 두 가지방법을 이용해 시스템을 객체로 분할할 수 있다. 첫 번째는 상태(데이터)를 분할의 중심축으로 삼는 방법이고, 두 번째 방법은 책임을 분할의 중심 축으로 삼는 방법이다.

데이터 중심 관점에서는 객체는 자신이 포함하고 있는 데이터를 조작하는데 필요한 오퍼레이션을 정의한다. 책임 중심의 관점에서는 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관한다. 예를 들어 데이터 중심의 관점에서는 아래와 같은 객체를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class DiscountCondition {
    private DiscountConditionType type;

    private int sequence;

    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    public DiscountConditionType getType() {
        return type;
    }

    public void setType(DiscountConditionType type) {
        this.type = type;
    }

    public DayOfWeek getDayOfWeek() {
        return dayOfWeek;
    }

    public void setDayOfWeek(DayOfWeek dayOfWeek) {
        this.dayOfWeek = dayOfWeek;
    }

    public LocalTime getStartTime() {
        return startTime;
    }

    public void setStartTime(LocalTime startTime) {
        this.startTime = startTime;
    }

    public LocalTime getEndTime() {
        return endTime;
    }

    public void setEndTime(LocalTime endTime) {
        this.endTime = endTime;
    }

    public int getSequence() {
        return sequence;
    }

    public void setSequence(int sequence) {
        this.sequence = sequence;
    }
}

우리는 어떤 관점의 설계를 해야할까? 결론부터 말하자면 책임 중심의 설계를 해야한다. 객체의 상태는 구현에 속한다. 구현은 불안정하기 때문에 변하기 쉽다. 상태를 객체 분할의 중심축으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화의 원칙이 무너진다. 결과적으로 상태 변경은 인터페이스의 변경을 초래하며 이 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼지게 된다.

설계 품질 척도

객체지향 커뮤니티에서는 오랜 기간 동안 좋은 설계의 특징을 판단할 수 있는 기준에 대한 다양한 논의가 있어왔다. 데이터 중심 설계와 책임 중심 설게의 장단점을 비교하기 위해 캡슐화, 응집도, 결합도라는 품질 척도의 의미를 살펴보자.

결론은 이렇다. 캡슐화의 정도가 응집도와 결합도에 영향을 미친다. 따라서 응집도와 결합도를 고려하기 전에 먼저 캡슐화를 향상시키기 위해 노력하라.

캡슐화

상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다. 여기서 구현이란 나중에 변경될 가능성이 높은 어떤 것을 가리킨다. 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다.

객체를 사용하면 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다. 변경될 가능성이 높은 부분을 구현이라 부르고 상대적으로 안정적인 부분을 인터페이스라고 부른다. 객체를 설계하기 위한 가장 기본적인 아이디어는 변경에 정도에 따라 구현과 인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.

캡슐화란 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류다. 정리하면 캡슐화란 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다.

객체 내부에 무엇을 캡슐화해야 하는가? 변경될 수 있는 어떤 것이라도 캡슐화해야 한다. 이것이 바로 객체지향 설계의 핵심이다.

응집도와 결합도

응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다. 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다. 반대로 요소들이 서로 다른 목적을 추구한다면 그 모듈은 낮은 응집도를 가진다. 객체지향의 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.

결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다. 어떤 모듈이 다른 모듈에 대애 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가진다. 어떤 모듈이 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면 두 모듈은 낮은 결합도를 가진다. 객체지향의 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만들 유지하고 있는지를 나타낸다.

그렇가면 응집도와 결합도는 어떤 요소에 의해 높고 낮음을 판단할 수 있을까? 그것은 바로 변경이다. 응집도와 결합도는 변경과 관련된 것이다.

간단히 말해 하나의 변경을 수용하기 위해 모듈 전체가 함께 변경된다면 응집도가 높은 것이고 모듈의 일부만 변경된다면 응집도가 낮은 것이다. 또한 하나의 변경에 대해 하나의 모듈만 변경된다면 응집도가 높지만 다수의 모듈이 함께 변경되야 한다면 응집도가 낮은 것이다.

결합도도 변경의 관점에서 설명할 수 있다. 결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다. 다시 말해 하나의 모듈을 수정할 때 얼마나 많은 모듈을 함께 수정해야 하는지를 나타낸다. 결합도가 높으면 높을수록 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경이 어려워진다. 영향을 받는 모듈의 수 외에도 변경의 원인을 이용해 결합도의 개념을 설명할 수도 있다. 내부 구현을 변경했을 때 이것이 다른 모듈에 영향을 미치는 경우에는 두 모듈 사이의 결합도가 높다고 표현한다.

결합도가 높아도 상관 없는 경우도 있다. 일반적으로 변경될확률이 매우 적은 안정적인 모듈(ex. 표준 라이브러리)에 의존하는 것은 아무런 문제도 되지 않는다. 하지만 직접 작성한 코드라면 언제나 변경될 위험에 노출되기 때문에 낮은 결합도를 유지하려고 노력해야한다.

데이터 중심 설계의 문제점

데이터 중심의 설계는 캡슐화 위반, 높은 결합도, 낮은 응집도라는 세가지 문제점을 가지고 있다. 각 문제점을 예시와 함께 살펴보자.

캡슐화 위반

데이터 중심의 설계에서 캡슐화를 강화 시키기 위해 선택하는 가장 쉬운 방법은 데이터를 private접근자로 선언해 외부에서 직접 접근할 수 없게 만들고, gettersetter를 사용하는 것이다.

위의 gettersetter는 직접 객체의 내부에 접근할 수 없기 때문에 캡슐화의 원칙을 지키고 있는 것처럼 보인다. 하지만 접근자(getter)와 수정자(setter)는 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다. gettersetter는 객체 내부에 있는 인스턴스 변수의 이름과 타입까지 퍼블릭 인터페이스에 노골적으로 드러낸다. 즉, 상태라는 구현을 외부에 노출한다.

구현을 캡슐화할 수 있는 적절한 책임은 협력이라는 문맥을 고려할 때문 얻을 수 있다. 설계 시, 협력에 대해 고민하지 않으면 캡슐화를 위반하는 과도한 접근자와 수정자를 가지게 되는 경향이 있다.

접근자와 수정자에 과도하게 의존하는 설계 방식을 추측에 의한 설계 전략이라고 부른다. 객체가 사용될 협력을 고려하지 않고 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행하기때문에 객체의 내부 구현이 퍼블릭 인터페이스에 그대로 노출될 수밖에 없는 것이다.

높은 결합도

데이터 중심의 설계는 접근자와 수정자를 통해 내부 구현을 인터페이스의 일부로 만들기 때문에 캡슐화를 위반한다. 이에 대한 결과로 클라이언트 객체가 구현에 강하게 결합된다. 이렇게 되면 객체의 내부 구현을 변경했을 때 이 인터페이스에 의존하는 모든 클라이언트 객체의 코드도 함께 변경해야 한다는 것이다.

1
movie.getFee().minus(discountedAmount).times(audienceCount);

클라이언트 객체가 Movie에 대해 의존하며 예매 가격을 계산하고 있다. 만약 여기서 fee의 타입을 변경해야한다고 하자. 이를 위해서는 getFee메서드의 반환 타입도 수정해야 하고, 클라이언트 객체에서도 변경된 타입에 맞게 수정이 일어나야 할 것이다.

fee의 타입 변경으로 인해 협력하는 클래스가 변경되기 때문에 getFee메서드는 fee를 정상적으로 캡슐화하지 못하고 있음을 알 수 있다. getFee메서드를 이용하는 것은 인스턴스 변수 fee의 가시성을 사실생 private에서 public으로 변경하는 것과 동일하다고 할 수 있다. 이처럼 데이터 중심 설계는 객체의 캡슐화를 약화시키기 때문에 클라이언트 객체가 객체의 구현에 강하게 결합된다.

데이터 중심 설계가 가지는 또 다른 문제는 여러 데이터 객체를을 사용하는 제어 로직이 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것이다. 즉 제어 객체가 다수의 데이터 객체에 의존하게 된다. 이 때, 데이터 객체에서 변경이 일어나면 제어 객체에 까지 변경의 여파가 퍼지게 된다.

데이터 중심 설계는 전체 시스템을 하나의 거대한 의존성 덩어리로 만들기 때문에 어떤 변경이라도 일단 발생하고 나면 시스템 전체가 요동칠 수 밖에 없다.

낮은 응집도

서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 말한다. 따라서 각 모듈의 응집도를 살펴보기 위해서는 코드를 수정하는 이유가 무엇인지 살펴봐야 한다.

데이터 중심 설계에서 영화 예매의 책임을 가진 제어 객체 ReservationAgency 클래스가 아래와 같이 존재한다고 가정하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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);
    }
}

위의 클래스를 예로 들어 변경과 응집도 사이의 관계를 살펴보자.

  • 할인 정책이 추가되는 경우
  • 할인 정책별로 할인 요금을 계산하는 방법이 변경될 경우
  • 할인 조건이 추가되는 경우
  • 할인 조건별로 할인 여부를 판단하는 방법이 변경될 경우
  • 에매 요금을 계산하는 방법이 변경될 경우

위와 같은 수정사항이 발생하는 경우에 코드를 수정해야 할 것이다. 낮은 응집도는 두 가지 측면에서 설계에 문제를 일으킨다.

변경의 이유가 서로 다른 코드들을 하나의 모듈 안에 뭉쳐놓았기 때문에 변경과 아무 상관이 없는 코드들이 영향을 받게 된다. 위의 ReservationAgency에서는 할인 정책을 선택하는 코드와 할인 조건을 판단하는 코드가 함께 존재하기 때문에 새로운 할인 정책을 추가하는 작업이 할인 조건에도 영향을 미칠 수 있다.

하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다. 응집도가 낮을 경우 다른 모듈에 위치해야 할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다. 새로운 할인 정책을 추가해야 한다고 가정해 보자. 이를 위해서는 MovieType에 새로운 할인 정책을 포함하는 열거형 값을 추가하고 ReservationAgencyreserve메서드의 switch 구문에 새로운 case절을 추가해야 한다. 또한 새로운 할인 정책에 따라 할인 요금을 계산하기 위해 필요한 데이터도 Movie에 추가해야 한다. 하나의 요구사항 변화를 위해 3개의 클래스를 함께 수정해야 하는 것이다.

어떤 요구사항의 변경을 수용하기 위해 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 증거이다. 결론은 이렇다. 높은 응집도를 위해서는 클래스를 단 한가지의 변경 이유만을 가지게 설계해야한다.

개선하기

캡슐화를 지켜라

객체는 자신이 어떤 데이터를 가지고 있는지를 내부에 캡슐화하고 외부에 공개해서는 안된다. 객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.

하지만 여기서 말하는 메서드는 단순히 gettersetter를 말하는 것이 아니다. 객체에게 의미있는 메서드는 객체가 책임져야 하는 무언가를 수행하는 메서드다. 속성의 가시성을 private으로 설정했다고 해도 속성을 외부로 제동하고 있다면 캡슐화를 위반하는 것이다.

스스로 자신의 데이터를 책임지는 객체

우리가 상태와 행동을 객체라는 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위함이다. 객체는 단순한 데이터 제공자가 아니다. 따라서 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.

따라서 객체를 설계할 때 ‘어떤 데이터를 포함해아 하는가’라는 질문에서 끝나지 않고 ‘데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가’라는 질문까지 해야한다. 자신의 데이터가 밖으로 세어나가지 않도록 행동을 함께 정의하자.

하지만 여전히 부족하다.

단순히 데이터를 자신이 책임지도록 메서드를 통해 데이터를 조작하도록 객체를 변경했다고 해서 캡슐화가 잘 지켜지고 결합도가 낮아지고 응집도가 높아지지는 않는다. 예시들을 통해 어떤 문제점이 있는지 살펴보자.

캡슐화 위반

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DiscountCondition {

    private DiscountConditionType type;
    private int sequence;
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    // ...

    public DiscountConditionType getType() {
        // ...
    }

    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
        // ...
    }

    public boolean isDiscountable(int sequence) {
        // ...
    }

    // ...
}

위의 DiscountCondition은 자기 자신의 데이터를 이용해 할인 가능 여부를 스스로 판단한다. 하지만 캡슐화를 위반한다.

기간 조건을 판단하는 isDiscountable(DayOfWeek dayOfWeek, LocalTime time)메서드의 시그니처를 보면 DiscountCondition이 속성으로 포함하고 있는 정보를 파라미터로 받는 것을 알 수 있다. 즉, 이 메서드는 객체 내부에 어떤 인스턴수 변수가 포함되어 있는지에 대한 정보를 인터페이스를 통해 외부에 노출하고 있는 것이다. 또한 비록 setType메서느는 없지만 getType을 통해 내부에 DiscountConditionType을 포함하고 있다는 정보 역시 노출하고 있다.

만약 DiscountCondition의 속성을 변경해야 한다고 하면 아마도 두 isDiscountable메서드의 파라미터를 수정하고 해당 메서드를 사용하는 모든 클라이언트 객체의 코드도 수정해야 할 것이다.

즉 내부구현의 변경이 외부로 퍼져나가는 파급효과(ripple effect)가 발생하고 있다. 이는 캡슐화가 부족하다는 명백한 증거이다. 따라서 변경 후의 설계는 자기 자신을 스스로 처리한다는 점에서 분명히 개선되었지만 여전히 내부의 구현을 캡슐화하는 데는 실패한 것이다.

인터페이스를 통해 내부 구현이 노출되는 또 다른 사례를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 MovieType getMovieType() {  
        //...
    }  
  
    public Money calculateAmountDiscountedFee() {
        //...
    }
  
    public Money calculatePercentDiscountedFee() {  
        //...
    }  
  
    public Money calculateNoneDiscountedFee() {  
        //...
    }  
}

위의 Movie클래스의 메서드들은 파라미터나 반환 값으로 내부에 포함된 어떤 정보도 노출하지 않는다. 하지만 여기서 노출시키는 것은 할인 정책의 종류이다. 할인 요금을 계산하는 세 가지 메서드의 이름은 금액 할인 정책, 비율 할인 정책, 미적용의 세 가지가 존재한다는 사실을 드러내고 있다.

만약 새로운 할인 정책이 추가되거나 제거된다면 이 메서드들에 의존하는 모든 클라이언트 객체가 영향을 받게 될 것이다. 따라서 위의 Movie클래스는 내부 구현을 성공적으로 캡슐화하지 못한다.

캡슐화는 단순히 객체 내부의 데이터를 외부로부터 감추는 것만을 의미하지는 않는다. 사실 캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다. 설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화 해야한다. 이것이 캡슐화라는 용어를 통해 말하고자 하는 진정한 의미이다.

높은 결합도

위의 예시에서 캡슐화의 위반으로 DiscountCondition의 내부 구현이 외부로 노출되었다. 따라서 해당 메서드에 의존하는 Movie클래스와 DiscountCondition의 결합도는 높을 수 밖에 없다. 두 객체 사이의 결합도가 높을 경우 한 객체의 변경할 때 다른 객체에게 변경의 영향이 전파될 확률이 증가하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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구문을 수정해야 한다.
  • DiscountCondition의 만족 여부를 판단하는 데 필요한 정보가 변경된다면 MovieisDiscountable 메서드로 전달된 파라미터를 변경해야 한다. 이로 인해 MovieisDiscountable의 메서드 시그니처도 변경될 것이고 이에 의존하는 클라이언트 객체에도 영향을 미칠 것이다.

이 요소들이 DiscountCondition구현에 속한다는 사실을 주목하라. DiscountCondition의 인터페이스가 아니라 구현을 변경하는 경우에도 해당 객체에 의존하는 클라이언트 객체도 변경해야 한다면 두 객체 사이의 결합도가 높다는 것을 의미한다.

모든 문제의 원인은 캡슐화를 지키지 못했기 때문이다. 따라서 변경에 유연한 낮은 결합도를 가진 설계를 위해서는 캡슐화를 첫번째 목표로 삼아야 한다.

낮은 응집도

이번엔 Movie의 클라이언트 객체인 Screening을 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;

    public Screening(Movie movie, int sequence, LocalDateTime whenScreened) {
        this.movie = movie;
        this.sequence = sequence;
        this.whenScreened = 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:
                movie.calculateNoneDiscountedFee().times(audienceCount);
        }

        return movie.calculateNoneDiscountedFee().times(audienceCount);
    }
}

DiscountCondition의 할인 여부를 판단하는 데 필요한 정보가 변경된다면 MovieisDiscountable의 시그니처를 변경해야하고 이로 인해 Screening에서 MovieisDiscountable을 호출하는 부분도 함께 변경해야 한다.

결과적으로 할인 조건의 종류를 변경하기 위해서는 DiscountCondition, Movie, Screening을 함께 변경해야한다. 하나의 변경을 수용하기 위해서 코드의 여러곳을 동시에 변경해야 한다는 것은 설게의 응집도가 낮다는 증거이다.

응집도가 낮은 이유는 캡슐화를 위반했기 때문이다. 즉, 원래 DiscountCondition이나 Movie에 위치해야 하는 로직이 Screening으로 새어나왔기 떄문이다.

개선의 한계

객체가 자신의 데이터를 책임지게 개선했다고 하더라도 기존에 가지고 있었던 문제가 반복되었다. 캡슐화가 위반되었고 변경의 여파가 객체 외부로 퍼져나갔다.

데이터 중심의 설계가 변경에 취약한 근본적인 이유는 다음과 같다.

  • 데이터 중심의 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
  • 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.

데이터 중심의 설계는 객체의 행동보다는 상태에 초점을 맞춘다.

데이터 중심의 설계에서의 첫 질문은 ‘이 객체가 포함해야 하는 데이터가 무엇인가?’이다. 데이터는 구현의 일부이기 때문에 데이터 중심의 설계는 구현을 이른 시기에 결정하도록 강요한다.

데이터 중심 설계에 익숙한 개발자들은 일반적으로 데이터와 기능을 분리하는 절차적 프로그래밍 방식을 따른다. 이는 상태와 행동을 하나의 단위로 캡슐화하는 객체지향 패러다임에 반하는 것이다. 이로 인해 접근자와 수정자를 과도하게 추가하게 된고 이 데이터 객체를 사용하는 절차를 분리된 별도의 객체 안에 구현하게 된다. 접근자와 수정자는 public속성과 큰 차이가 없기 때문에 캡슐화는 완전이 무너진다. 이것이 개전 전의 설계가 실패한 이유이다.

비록 데이터와 처리하는 작업을 같은 객체 안에 두더라도 데이터에 초점이 맞춰져 있다면 만족스러운 캡슐화를 얻이는 어렵다. 데이터를 먼저 결정하고 이를 처리하는 오퍼레이션을 나중에 결정하는 방식은 데이터에 관한 정보가 객체의 인터페이스에 고스란히 드러나게 된다. 결과적으로 객체의 인터페이스는 구현을 캡슐화 하는데 실패하고 변경에 취약해진다.

즉, 데이터 중심의 설계는 너무 이른 시기에 데이터를 결정하도록 강요하기 때문에 이들이 인터페이스에 고스란히 드러나게 된다. 캡슐화는 무너지고, 결합도와 응집도에 나쁜 영향을 미치기 때문에 변경에 취약한 코드를 낳게 된다.

데이터 중심의 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다.

객체지향 설계란 협력하는 객체들의 공동체를 만드는 작업이다. 따라서 협력이라는 문맥 아래서 필요한 책임을 결정하고 이를 수행할 적절한 객체를 결정하는 것이 가장 중요하다. 따라서 객체지향 설계의 무게중심은 객체의 내부가 아니라 외부에 맞춰져 있어야 한다. 객체 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는 가는 부가적인 문제이다.

데이터 중심의 설계의 초점은 객체 외부가 아니라 내부로 향한다. 협력에 대한 문맥 없이 객체가 관리할 데이터의 세부 정보를 먼저 결정한다. 구현이 이미 결정된 상태이기 때문에 구현을 인터페이스에 맞춰 억지로 끼워 맞출 수 밖에 없다.

객체의 인터페이스에 구현이 노출되어 있었기 때문에 협력이 구현 세부사항에 종속되고, 구현이 변경되면 협력하는 모든 객체가 영향을 받을 수 밖에 없는 것이다.

태그:

카테고리:

업데이트:

댓글남기기