Chapter 4. 설계 품질과 트레이드오프

Posted by yunki kim on January 10, 2022

  객체지향 설계의 핵심은 역할, 책임, 협력이다.

    1. 협력: 애플리케이션 기능 구현을 위한 객체들 간의 상호작용.

    2. 책임: 객체가 다른 객체와의 협력을 위해 수행하는 행동.

    3. 역할: 대체 가능한 책임의 집합.

  객체지향 설계에서 가장 중요한 것은 적절한 책임 할당이다. 적절한 책임 할당이 이루어 져야 제대로된 협력이 가능하고 제대로된 역할의 배분이 이루어진다.

  객체지향 설계는 올바른 책임을 할당해 낮은 결합도와 높은 응집도를 가진 구조를 만드는 것이다. 이 말에는 다음과 같은 두 가지 관점이 섞여있다.

    1. 객체지향 설계의 핵심이 책임이다.

    2. 책임을 할당하는 작업이 응집도와 결합도 같은 설계 품질과 깊이 연관되 있다.

  설계를 변경한다는 것은 비용 발생과 직결되 있다. 따라서 합리적인 비용 내에서 변경을 수용할 수 있는 구조를 만들어야 한다. 이는 높은 응집도와 느슨한 결합을 가진 구조에서 가능하다.

  합리적인 비용 내에서 변경을 수용하기 위해서는 퍼블릭 인터페이스에 구현을 노출시키지 않아야 한다. 이를 위해서는 설계 단계에서 객체에 초점을 맞추어 설계를 해야 한다.

  때로는 나쁜 설계와 좋은 설계를 대조해 보는 것이 좋은 설계에 대한 통찰을 가져다 준다. 따라서 이번 장에서는 chapter2chapter3에서 객체를 중심으로 설계한 영화 예매 시스템을 데이터 중심의 설계로 설계해 차이점을 비교해 보려 한다.

 

객체지향 설계의 중점

  객체지향 설계 방법은 상태 분할을 중심으로 한 설계와 책임 분할을 중심으로한 설계 총 두 가지가 존재한다. 훌륭한 객체지향 설계는 상태 분할 중심이 아닌 책임 분할을 중심으로 설계해야 한다.

  이 둘의 차이는 다음과 같다.

상태 분할을 중심으로 한 설계 책임 분할을 중심으로 한 설계
객체는 자신이 포함하고 있는 데이터를 조작하는 데 필요한 오퍼레이션을 정의한다. 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관한다.
객체 상태에 초점을 맞춘다. 행동에 초점을 맞춘다.
객체는 동립된 데이터 덩어리다 객체는 협력하는 공동체의 일원이다.

  좋은 객체 설계는 구현을 퍼블릭 인터페이스에 들어내지 않는다. 구현은 불안정하기 때문에 수시로 바뀐다. 그런데 상태 분할을 중심으로 객체를 설계하게 되면 구현이 퍼블릭 인터페이스에 포함되게 된다. 만약 이 상황에서 수정이 가해진다면 해당 퍼블릭 인터페이스를 사용하는 모든 객체에 수정을 가해야 하고 이는 버그로 이어진다.

  지금부터 데이터 중심의 영화 예매 시스템을 살펴보자.

 

데이터 중심의 영화 예매 시스템

  데이터 중심 설계는 객체 내부에 저장되는 데이터를 기반으로 시스템을 분할한다. 따라서 설계를 시작할때 데이터가 무엇인지를 파악한다. Movie에 저장될 데이터는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
public class Movie {
    // 기존 코드에서도 존재했던 부분
    private String title;
    private Duration runningTime;
    private Moeny fee;
    // 데이터 중심 설계로 인해 추가된 부분
    private List<DiscountCondition> discountConditions;
    private MovieType movieType;
    private Money discountAmount;
    private double discountPercent;
}
cs

  할인 정책은 하나만 선택할 수 있다. 따라서 discountAmount와 discountPercent 중 사용할 것을 선택하는 상태가 movieType이다.

1
2
3
4
5
6
public enum MovieType {
    AMOUNT_DISCOUNT, // 금액 할인 정책
    PERCENT_DISCOUNT, // 비율 할인 정책
    NONE_DISCOUNT, // 미적용
}
 
cs

  이것이 데이터 중심 설계이다. 위 예시는 Movie가 할인 금액을 계산하는 데 필요한 테이터가 무엇인지를 중점적으로 고려해 상태를 추가했다.

  데이터 중심 설계는 데이터를 중점으로 두기 때문에 필요한 데이터에 대한 고려만 한다. 따라서 Movie처럼 종류에 따라 배타적으로 사용될 discountAmount와 discountPercent가 하나의 클래스에 포함되는 설계를 자주 볼 수 있다.

  이제 내부 데이터가 외부로 빠져 나가는 것을 방지하기 위해 캡슐화를 해보자. 캡슐화를 위해 accessor와 mutator를 추가한다.

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
51
52
public class Movie {
    // 기존 코드에서도 존재했던 부분
    private String title;
    private Duration runningTime;
    private Moeny fee;
    // 데이터 중심 설계로 인해 추가된 부분
    private List<DiscountCondition> discountConditions;
    private MovieType movieType;
    private Money discountAmount;
    private double discountPercent;
 
    public Moeny getFee() {
        return fee;
    }
 
    public List<DiscountCondition> getDiscountConditions() {
        return discountConditions;
    }
 
    public MovieType getMovieType() {
        return movieType;
    }
 
    public Money getDiscountAmount() {
        return discountAmount;
    }
 
    public double getDiscountPercent() {
        return discountPercent;
    }
 
    public void setFee(Moeny fee) {
        this.fee = fee;
    }
 
    public void setDiscountConditions(List<DiscountCondition> discountConditions) {
        this.discountConditions = discountConditions;
    }
 
    public void setMovieType(MovieType movieType) {
        this.movieType = movieType;
    }
 
    public void setDiscountAmount(Money discountAmount) {
        this.discountAmount = discountAmount;
    }
 
    public void setDiscountPercent(double discountPercent) {
        this.discountPercent = discountPercent;
    }
}
 
cs

    이제 위에서 했던 방식 처럼 할인 조건을 설계하기 위해 필요한 데이터가 무엇인지를 고려하며 할인 조건을 구현해 보자.

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;
    
    // 순번 조건(type이 SEQUENCE)에만 사용되는 상태들
    private int sequence;
    private DayOfWeek dayOfWeek;
    private LocalTime localTime;
    private LocalTime endTime;
 
    public DiscountConditionType getType() {
        return type;
    }
 
    public void setType(DiscountConditionType type) {
        this.type = type;
    }
 
    public int getSequence() {
        return sequence;
    }
 
    public void setSequence(int sequence) {
        this.sequence = sequence;
    }
 
    public DayOfWeek getDayOfWeek() {
        return dayOfWeek;
    }
 
    public void setDayOfWeek(DayOfWeek dayOfWeek) {
        this.dayOfWeek = dayOfWeek;
    }
 
    public LocalTime getLocalTime() {
        return localTime;
    }
 
    public void setLocalTime(LocalTime localTime) {
        this.localTime = localTime;
    }
 
    public LocalTime getEndTime() {
        return endTime;
    }
 
    public void setEndTime(LocalTime endTime) {
        this.endTime = endTime;
    }
}
 
cs

  같은 과정을 통해 Screeing, Reservation도 구현하자.

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 Movie getMovie() {
        return movie;
    }
 
    public void setMovie(Movie movie) {
        this.movie = movie;
    }
 
    public int getSequence() {
        return sequence;
    }
 
    public void setSequence(int sequence) {
        this.sequence = sequence;
    }
 
    public LocalDateTime getWhenScreened() {
        return whenScreened;
    }
 
    public void setWhenScreened(LocalDateTime whenScreened) {
        this.whenScreened = whenScreened;
    }
}
 
cs

 

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
public class Reservation {
    private Customer customer;
    private Screening screening;
    private Money money;
    private int audienceCount;
 
    public Reservation(Customer customer, Screening screening, Money money, int audienceCount) {
        this.customer = customer;
        this.screening = screening;
        this.money = money;
        this.audienceCount = audienceCount;
    }
 
    public Customer getCustomer() {
        return customer;
    }
 
    public void setCustomer(Customer customer) {
        this.customer = customer;
    }
 
    public Screening getScreening() {
        return screening;
    }
 
    public void setScreening(Screening screening) {
        this.screening = screening;
    }
 
    public Money getMoney() {
        return money;
    }
 
    public void setMoney(Money money) {
        this.money = money;
    }
 
    public int getAudienceCount() {
        return audienceCount;
    }
 
    public void setAudienceCount(int audienceCount) {
        this.audienceCount = audienceCount;
    }
}
 
cs

  Customer 클래스는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
public class Customer {
    private String name;
    private String id;
 
    public Customer(String name, String id) {
        this.name = name;
        this.id = id;
    }
}
 
cs

  위 코드는 다음과 같은 구조를 가지고 있다.(메서드는 생성자, accessor, mutator만 있으므로 다이어그램에서 생략)

데이터 중심 설계 영화 예매 시스템

  이제 위의 데이터 클래스들을 조합해 영화 예매 절차를 구현해 보자.

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
/** 영화 예메 절차 */
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);
        } else {
            fee = movie.getFee();
        }
 
        return new Reservation(customer, screening, fee, audienceCount);
    }
}
 
cs

 

설계 트레이드 오프

  캡슐화

    객체지향은 내부 구현을 외부에 노출시키지 않기 위해 상태와 행동을 객체 안에 모은다. 객체지향은 한 곳에서 발생된 수      정이 외부로 파급되는 것을 조절하는 장치를 제공하기 때문에 강력하다. 

    객체 설계는 기본적으로 변경되기 쉬운 구현 부분을 내부에 감추고 외부 객체와의 상호작용은 오직 퍼블릭 인터페이스에    만 의존하게 한다. 

    객체지향에서 캡슐화가 가장 중요하다. 캡슐화는 구현을 객체 내부에 감추고 퍼블릭 인터페이스만 노출시켜 대상을 단순    화하는 추상화의 한 종류다. 이를 통해 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 감춘다.

    객체지향 설계는 유지보수성을 목표로 한다. 여기서의 유지보수성은 두려움, 주저함, 저항감 없이 코드를 변경할 수 있는    능력이다. 캡슐화를 통해 객체에게 자율성을 부여하지 않으면 코드의 수정이 어려워지고 결과적으로 시스템이 진화할 수      없다.

  응집도와 결합도

    응집도와 결합도는 구조적 설계 방법이 주도하던 시대에 소프트웨어 품질을 측정하기 위해 소개된 기준이지만 객체지향      에서도 유효하다.

    응집도: 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.

     결합도: 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타낸다.

    사실 이런 추상적인 설명으로는 응집도와 결합도를 제대로 이해하기 어렵다. 위 문장으로는 모듈 내의 요소가 얼마나 강      하게 연돤왜 있는 것이 높은 응집도인지, 모듈 사이에 얼마 정도의 의존성만 있어야 하는지를 알기 어렵다.

    응집도와 결합도를 이해하기 위해서는 이것들이 설계와 관련되 있다는 사실을 알아야 한다. 좋은 설계는 높은 응집도와      낮은 결합도를 가진 모듈로 구성되 있다.

    좋은 설계는 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계다. 그리고 좋은 설계를 위해서는 높은 응집도    와 낮은 결합도를 추구해야 한다. 따라서 응집도와 결합도는 변경과 관련되 있다.

    높은 응집도, 낮은 결합도를 가진 설계는 변경을 용이하게 한다. 변경 관점에서 응집도는 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도이다.

  낮은 응집도와 높은 응집도는 다음과 같이 구별할 수 있다.

낮은 응집도 높은 응집도
변경을 수용하기 위해 모듈 일부만 변경된다. 변경을 수용하기 위해 모듈 전체가 함께 변경된다.
하나의 변경에 대해 다수의 모듈이 변경된다. 하나의 변경에 대해 하나의 모듈만 변경된다.

    결합도는 한 모듈이 변경되기 위해 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다. 따라서 결합도가 높으면 변경해야 하는 모듈의      수가 늘어난다. 또 한 구현의 수정으로 인해 다른 모듈에서 수정 사항이 발생한다면 그 역시 결합도가 높은거다. 오직 퍼블릭 인터페이스를    수정했을 때에만 다른 클래스에서 수정이 발생해야 한다. 따라서 객체 간의 상호작용이 오직 인터페이스에만 의존하게 코드를 작성해야 한    다.

    물론 변경될 확률이 매우 적고, 적은 양의 코드에만 의존할 경우에는 결합도가 높아도 상관 없다. 표준 라이브러리에 포함된 모듈이나 성    숙 단계에 접어든 프레임워크에 의존하는 경우가 그 예시다(String, ArrryaList 등). 하지만 직접 작성한 코드는 불안정하며 언제든지 변      경될 수 있다.

 

데이터 중심의 영화 예매 시스템의 문제

  데이터 중심 설계와 책임 중심 설계를 활용한 영화 예매 시스템의 기능은 완전히 같다. 하지만 설계 관점에서는 완전히 다르다. 이 둘의 근본적인 차이점은 캡슐화를 보는 관점이다. 데이터 중심 설계는 캡슐화를 위반하고 구현을 인터페이스의 일부로 사용한다. 반면에 책임 중심 설계는 구현을 인터페이스 뒤로 감추어 캡슐화한다. 따라서 데이터 중심 설계는 캡슐화를 위반해서 높은 결합도와 낮은 응집도를 갖게 한다.

  캡슐화 위반

1
2
3
4
5
6
7
8
9
10
11
public class Movie {
    private Money fee;
 
    public Money getFee() {
        return this.fee;
    }
 
    public void setFee(Money fee) {
        this.fee = fee;
    }
}
cs

    위 코드를 Movie 클래스는 오직 메서드를 통해서만 객체의 내부 상태에 접근할 수 있다. 따라서 겉보기에는 캡슐화의 원    칙을 지키고 있는거 같다. 하지만 접근자와 수정자 메서드는 객체 내부의 상태에 대한 어떤 정보도 캡슐화 하지 않는다. 이    런 현상이 발생하는 이유는 객체가 수행할 책임이 아니라 내부 데이터에 초점을 맞추었기 때문이다. 

    제대로된 캡슐화는 오직 적절한 책임은 협력이라는 것을 고려할때만 얻을 수 있다. 객체가 사용될 문맥을 고려하지 못하      면 개발자는 최대한 많은 접근자와 수정자를 추가하게 된다.

    앨런 홀럽(Allen Holub)은 이런 접근자와 수정자에 과도한 의존을 하는 설계 방식을 추측에 의한 설계 전략(design-by-    guessing strategy)이라 했다.

  높은 결합도

    데이터 중심 설계는 구현을 외부로 노출시켜 인터페이스로서 사용한다. 이는 구현이 강하게 결합되 있다는 것을 의미한      다. 따라서 이런 코드는 수정에 취약하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 영화 예메 절차 */
public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
        ...
        // 적절한 할인 정책에 따라 예매 요금 계산
        Money fee;
        if (discountable) {
            ...
        } else {
            fee = movie.getFee();
        }
        ...
    }
}
 
cs

    위 코드는 한 명의 예매 요금을 계산하기 위한 코드다. 만약 여기서 fee의 타입이 변경된다면 getFee 메서드의 반환 타입 역시 바꿔야한다. 즉, 여러 데이터 객채들을 사용하는 제어 로직이 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다.

  낮은 응집도

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

    낮은 응집도는 다음과 같은 문제점을 가지고 있다.

    1. 변경의 이유가 서로 다른 코드들을 하나의 모듈에 뭉쳐놓았기 때문에 변경과 관련 없는 코드들이 영향을 받는다.

      ReservationAgency의 경우 안에 할인 정책을 선택하는 코드와 할인 조건을 판단하는 코드가 같이 존재한다. 따라서 새로운 할인 정책을 추가하는 작업이 할인 조건에도 영향을 미칠 수 있다.

    2. 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다.

      응집도가 낮으면 책임이 엉뚱한 곳에 존재하게 된다. 새로운 할인 정책을 추가할 경우 이를 위해 MovieType, ReservationAgency, Movie를 수정해야 한다.

  단일 책임 원칙(Single Responsibility Principle)

    단일 책임 원칙은 클래스는 단 한 가지의 변경 이유만 가져야 한다는 것이다. 여기서 책임 이라는 말이 변경의 이유라는 의미로 사용된다는 것을 주의해야 한다. 단일 책임 원칙에서의 책임은 역할, 책임, 협력에서 말하는 책임과는 다르며 변경과 관련된 더 큰 개념을 의미한다.

 

자율적인 객체를 향해

 

캡슐화를 지켜라

  객체는 상태, 구현을 내부에 캡슐화 해서 외부에 공해하지 말아야 한다. 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근해야 한다. 여기서 말하는 메서드는 단순히 속성 하나를 변경 또는 반환하는 메서드가 아니다. 객체가 책임져야 하는 무언가를 수행하는 메서드다. 

 

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

  개발자가 객체라는 단위로 상태와 행동을 묶는 것은 자신의 상태를 자신이 처리할 수 있게 하기 위해서이다. 객체는 객체의 상태 보다 객체가 협력에 참여하며 수행하는 책임을 정의하는 것이 더 중요하다.

  따라서 객체를 설계할때는 "이 객체가 어떤 데이터를 포함해야 하는가?"라는 질문을 다음과 같은 두 개의 질문으로 분리해야 한다.

    1. 이 객체가 어떤 데이터를 포함해야 하는가?

    2. 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?

 

캡슐화의 진정한 의미

  캡슐화는 변경될 수 있는 어떤것이라고 감추는 것을 의미한다. 내부 속성을 외부로부터 감추는 것은 '데이터 캡슐화'라고 불리는 캡슐화의 한 종류다.

 

데이터 중심 설계의 문제점

  데이터 중심 설계는 다음과 같은 이유로 변경에 취약하다.

    1. 데이터 중심의 설계는 본질적으로 너무 이름 시기에 데이터에 관해 결정하게 강요한다.

    2. 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.

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

    데이터 중심 설계는 설계를 시작하는 처음부터 데이터에 관해 결정하게 강요한다. 따라서 너무 이른 시기에 내부 구현에      초점을 맞춘다.

    데이터 중심 설계는 그저 데이터와 기능을 분리하는 절차지향 프로그래밍이다. 이는 상태와 행동을 하나의 단위로 캡슐화    하는 객체지향 프로그래밍에 반한다. 데이터 중심 설계는 수정자와 접근자를 과도하게 사용해 객체의 상태를 그대로 들어      낸다. 이는 인터페이스에 구현을 들어내서 캡슐화를 실패하게 만들고 변경에 취약한 코드를 만든다.

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

    객체지향 설계는 협력하는 객체들의 공동체를 구축하는 것이다. 따라서 협력이라는 문맥 안에서 필요한 책임을 결정하고    이를 수행할 적절한 객체를 결정하는 것이 중요하다. 따라서 객체지향은 퍼블릭 인터페이스에 중점을 두어야 한다. 내부 구    현은 부가적인 문제다.

    이 관점에서 데이터 중심 설계는 초점이 내부 구현을 향한다. 내부 구현인 상태를 우선적으로 고려하기 때문에 이미 구현    된 객체의 인터페이스를 억지로 끼워맞출 수밖에 없다. 이렇게 협력이 구현 세부사항에 종속되 있으면 그에 따라 객체의 내    부 구현이 변경됐을 때 협력하는 객체 모두가 영향을 받는다.

 

출처 - 오브젝트