본문 바로가기
Study/오브젝트

[오브젝트] 4장. 설계 품질과 트레이드오프

by Nahwasa 2022. 12. 2.

스터디 메인 페이지

목차

    - ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.

    - 모든 이미지의 출처는 오브젝트(조용호 저) 책 입니다.

     


     

    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 코드에서도 데이터를 먼저 결정하고 데이터를 처리하는데 필요한 오프레이션을 나중에 결정하므로 데이터에 관한 지식이 객체의 인터페이스에 고스란히 드러난다. -> 캡슐화에 실패하고 코드는 변경에 취약해진다.
    • 결론적으로 데이터 중심의 설계는 너무 이른 시기에 데이터에 대해 고민하기 때문에 캡슐화에 실패하게 된다.
    • 객체의 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 미치기 때문에 변경에 취약한 코드를 낳게 된다.

     

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

    • 중요한 것은 객체가 다른 객체와 협력하는 방법이다.
    • 데이터 중심 설계에서 초점은 객체의 외부가 아니라 내부로 향한다. -> 이미 구현된 객체의 인터페이스를 억지로 끼워맞출수밖에 없다.
    • 따라서 변경에 유연하게 대처하지 못한다.

    댓글