본문 바로가기

IT Book Summary/Object: 객체지향설계

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

객체지향 설계 -> 올바른 객체에게 올바른 책임을 할당. 낮은 결합도 높은 응집도를 가진 구조를 창조

두가지 관점 - 설계의 핵심은 책임. 책임을 할당하는 작업이 응집도와 결합도 같은 설계품질과 연관.

 

훌륭한 설계란?  합리적인 비용 안에서 쉽게 변경할 수 있는 설계

객체의 상태가 아니라 행동, 책임에 초점.

 

이번장에서는 책임이 아닌 상태를 표현하는 데이터 중심 설계를 보고 차이점을 살펴보자.

 


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

 

객체의 상태 == 객체가 저장해야하는 데이터의 집합

 

두가지 방법 객체 분할

데이터(상태) 중심 책임 중심
자신이 가지는 데이터를 조작하는데 필요한 오퍼레이션을 정의 다른객체가 요청할 수 있는 오퍼레이션을 위해 상태를 보관
객체의 상태에 초점 객체의 행동에 초점
독립된 데이터 덩어리 협력하는 공동체의 일원

객체의 상태는 구현에 속함.

구현은 불안정하기 때문에 상태변경시 인터페이스 변경을 초래하며 그 파장이 거기에 의존하는 모든 객체에게 퍼지게 됨. 

책임은 인터페이스에 속함.

책임을 수행하는데 필요한 상태를 캡슐화. 구현변경시 파장이 외부로 퍼져나가는것을 막음.

 

데이터를 준비하자

 

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;
}

 

달라진점

- 할인조건의 목록(discountConditions) 이 인스턴스 변수로 포함

- DiscountPolicy 클래스로 분리했던 것과 달리 discountAmount, discountPercent 직접 정의.

- 할인 정책의 종류를 열거형 타입 MovieType 인스턴스

 

public enum MovieType {
	AMOUNT_DISCOUNT,	//금액 할인 정책
	PERCENT_DISCOUNT,	//비율 할인 정책
	NONE_DISCOUNT		//미적용
}

 

객체가 포함해야하는 데이터에 집중함.

 

내부의 데이터를 반환하는 접근자(accessor)와 데이터를 변경하는 수정자(mutator)를 추가하자.

 

 

public class Movie {
    public MovieType getMovieType(){
        return movieType;
    }
    
    public void setMovieType(MovieType movieType){
        this.movieType = movieType;
    }
    
    ......
}

 

 

할인조건 구현. 할인 조건의 타입을 저장할 DiscountConditionType 정의

 

 

public enum DiscountConditionType {
    SEQUENCE,	//순번 조건
    PERIOD	//기간 조건
}

 

 

할인조건을 구현하는 DiscountCondition 은 조건의 타입인 인스턴스 변수 type을 포함

또한 순번조건에 사용되는 순번과

기간조건에 사용되는 데이터들도 포함시키자.

메서드로 추가하자.

 

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;
    }
    
    ......
    
}

 

 

동일하게 Screening 클래스도 만들자.

 

 

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;
    }
    
    ......
    
}

 

 

 

예매를 위한 Reservation 클래스를 추가하자

 

 

public class Reservation {
    private Customer customer;
    private Screening screening;
    private Money fee;
    private int audienceCount;
    
    public Reservation(Customer costomer, Screening screening, Money fee, int audienceCount) {
        this.customer = customer;
        this.screening = screening;
        this.fee = fee;
        this.audienceCount = audienceCount;
    }
    
    public Customer getCustomer() {
        return customer;
    }
    
    public void setCustomer(Customer customer) {
        this.customer = customer;
    }
    
    ......
    
}

 

 

고객정보 Customer 클래스

 

 

public class Customer {
    private String name;
    private String id;
    
    public Customer(String name, String id) {
        this.id = id;
        this.name = name;
    }
}

 

 

 

영화를 예매하자

 

데이터 클래스들은 조합해 영화예매를 구현하는 ReservationAgency 클래스

 

 

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.getDiacountAmount();
                    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();
        }
        
        return new Reservation(customer, screening, fee, audienceCount);
    }
}

 

 

reserve 메서드 

루프를 돌면서 할인가능 여부 확인 for문

할인정책에 따라 예매요금을 계산하는 if문

 


02. 설계 트레이드 오프

 

캡슐화, 응집도, 결합도를 바탕으로 한

데이터 중심, 책임 중심 설계의 장단점 비교

 

캡슐화

 

상태와 행동을 하나의 객체 안에 모으는 이유 - 객체 내부구현(나중에 변경될 가능성이 높은 어떤것)을 외부로부터 감추기 위해

 

불안정한 구현 세부사항을 안정적인 인터페이스 (상대적으로 안정적인 부분) 뒤로 캡슐화

 

 복잡성을 다루기 위한 가장 효과적인 도구는 추상화다. 다양한 추상화 유형을 사용할 수 있지만 객체지향 프로그래밍에서 복잡성을 취급하는 주요한 추상화 방법은 캡슐화다. ... 훌륭한 프로그래밍 기술을 적용해서 캡슐화를 향상시킬 수는 있겠지만 객체지향 프로그래밍을 통해 전반적으로 얻을 수 있는 장점은 오직 설계 과정 동안 캡슐화를 목표로 인식할 때만 달성될 수 있다.  - Wirfs-Brock

 

객체 내부에 무엇을 캡슐화해야 하는가? 변경되 수 있는 모든것.

 

 유지보수성이 목표다. 여기서 유지보수성이란 두려움 없이, 주저함 없이, 저항감 없이 코드를 변경할 수 있는 능력을 말한다. ... 응집도, 결합도, 중복 역시 훌륭한(변경 가능한) 코드를 규정하는 데 핵심적인 품질인 것이 사실이지만 캡슐화는 우리를 좋은 코드로 안내하기 때문에 가장 중요한 제 1원리다. - Bain

 

응집도와 결합도

 

응집도 란? 모듈에 포함된 내부 요소들이 연관되 있는 정도

모듈내 요소가 하나의 목적을 위해 긴밀히 협력한다면 높은 응집도

 

변경이 발생할 때 모듈 내부에 일부만, 다수의 모듈이 함께 변경되어야 한다면 낮은 응집도

변경이 발생할 때 하나의 모듈 전체만 변경하면 되는경우 높은 응집도. 변경의 대상 범위가 명확

 

객체지향관점 / 객체에 얼마나 관련 높은 책임을 할당하는지

 

결합도 란? 의존성의 정도. 다른 모듈에 대한 지식. 

변경의 관점에서 한 모듈이 변경되기 위해 다른 모듈의 변경을 요구하는 정도로 측정함.

 

다른 모듈에 대해  최소한의 필요한 지식만 있다면 낮은 결합도

결합도가 높을수록 변경해야 하는 모듈의 수가 늘어나 변경이 어려워짐.

 

좋은 설계란? 높은 응집도와 낮은 결합도를 가진 모듈로 구성된 설계

 

표준 라이브러리에 포함된 모듈이나 성숙된 프레임워크의 경우 변경이 적으므로 결합도가 높아도 상관 없음.

 

직접작성한 코드는 낮은 결합도를 유지하려고 노력해야함.

 

캡슐화를 향상시키면 모듈안 응집도는 높아지고 모듈사이 결합도는 낮아진다. 

 


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

 

데이터 설계가 가진 문제점

 

  • 캡슐화 위반

접근자와 수정자에 과도하게 의존하는 추측에 의한 설계 전략(design-by-guessing strategy) 

최대한 많은 접근자 메소드를 남발하여 내부 구현이 퍼블릭 인터페이스에 그대로 노출.

캡슐화 위반. 변경에 취약.

 

 

  • 높은 결합도

ReservationAgency 클래스를 살펴보자.

fee의 타입을 변경한다고 가정하면, 연쇄적으로 반환 타입 변경 시켜야함.

getFee 메서드를 사용하는것은 인스턴스 변수 fee의  private 은 사실상 의미가 없다!! 

(이미 알고 있었다...)

 

영화예매시스템에서 제어로직을 가지고 있는 제어 객체인 ReservationAgency 가 모든 데이터 객체에 의존함.

모든 의존성이 모이는 결합도의 집결지.

하나라도 변경이 생긴다면 ReservationAgency 의 변경은 막을 수 없다.

 

  • 낮은 응집도

모듈의 응집도가 낮을 때 발생하는 대표적인 증상

- 어떤 코드를 수정한 후에 아무런 상관도 없던 코드에 문제가 발생하는 것 

- 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 하는 것

 

ex) 새로운 할인조건을 추가한다고 할때

 

단일 책임 원책(Single Responsibility Principle, SRP)

- 클래스는 단 한개의 변경 이유(변경과 관련된 더 큰 개념으로서의 책임)만 가져야한다.

 


04. 자율적인 객체를 향해

 

캡슐화를 지켜라

 

객체에게 의미있는, 객체가 책임져야 하는 무언가를 수행하는 메서드를 통해서만 상태에 접근할 수 있어야 한다.

 ( 접근자,수정자 X )

 

class Rectangle {
    private int left;
    private int top;
    private int right;
    private int bottom;
    
    public Rectangle (int left, int top, int right, int bottom) {
        this.left = left;
        this.top = top;
        this.right = right;
        this.bottom = bottom;
    }
    
    public int getLeft() { return left; }
    public void setLeft(int left) { this.left = left; }
    
    public int getTop() { return top;}
    public void setTop(int top) { this.top = top; }
    
    ......
   
}

 

 

첫번째 문제점

수정자 메소드를 이용해 값을 설정하는 유사한 코드가 많을 수 있으므로 중복 코드가 많을 수 있다.

코드 중복은 악의 근원이다!

 

두번째 문제점

변경할 경우 기존 접근자 메서드를 사용하는 모든 코드에 영향을 준다.

 

해결책 - 캡슐화를 강화

Rectangle 내부에 너비와 높이를 조절하는 로직을 캡슐화

자신의 크기를 스스로 증가시키도록 '책임을 이동'

 

class Rectangle {
    public void enlarge(int multiple) {
        right *= multiple;
        bottom *= multiple;
    }
}

 

 

 

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

 

객체는 단순한 데이터 제공자가 아니다.

수행할 책임을 정의하는 오퍼레이션이 중요.

 

  • 이 객체가 어떤 데이터를 포함해야 하는가?
  • 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?

-> 새로운 데이터 타입을 만들자

 

 

DiscountCondition이 관리해야 하는 테이터는?

 

 

public class DiscountCondition {
    private DiscountConditionType type;
    private int sequence;
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;
}

 

 

수행할 수 있는 오퍼레이션은?

할인조건에 순번 / 기간 조건 필요 -> getType() 과 할인 조건을 판단할 isDiscountable 메서드

 

 

public class DiscountCondition {
    public DiscountConditionType getType() {
        return type;
    }
    
    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
        if (type != DiscountConditionType.PERIOD) {
            throw new IllegalArgumentException();
        }
        
        return this.dayOfWeek.equals(dayOfWeek) &&
        	this.startTime.compareTo(time) <= 0 &&
            this.endTime.compareTo(time) >= 0;
    }
    
    public boolean isDiscountable(int sequence) {
        if(type != DiscountConditionType.SEQUENCE) {
            throw new IllegalArgumentException();
        }
        
        return this.sequence == sequence;
    }
}

 

 

Movie는 어떤 데이터를 포함해야 하는가?

 

public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;
    
    private MovieType movieType;
    private Money discountAmount;
    private douvle discountPercent;
}

 

 

요금을 계산하는 오퍼레이션

할인 정책 - 금액/ 비율/ 미적용 할인타입

 

getMovieType() 과

 

정책별 요금계산 메서드

 

할인여부를 판단하는 isDiscountable 메서드 (sequence)

 

 

public class Movie {
    public MovieType getMovieType() {
        return movieType;
    }
    
    public Money calculateAmountDiscountedFee() {
        if (movieType != MovieType.AMOUNT_DISCOUNT) {
            throw new IllegalArgumentException();
        }
        
        return fee.minus(discountAmount);
    }
    
    public Money calculatePercentDiscountedFee() {
        if(movieType != MovieType.PERCENT_DISCOUNT) {
            throw new IllegalArgumentException();
        }
        
        return fee.minus(fee.times(discountPercent));
    }
    
    public Money calculateNoneDiscountedFee() {
        if(movieType != MovieType.NONE_DISCOUNT) {
            throw new IllegalArgumentException();
        }
        
        return fee;
    }
    
    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;
    }
}

 

 

Screening 이 관리하는 데이터와 메서드

 

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 PERCEN_DISCOUNT:
                if(movie.isDiscountable(whenScreened, sequence)){
                    return movie.calculatePercentDiscountedFee().times(audienceCount);
                }
                break;
            
            case NONE_DISCOUNT:
                return movie.calculateNoneDiscountedFee().times(audienceCount);
        }
        
        return movie.calculateNoneDiscountedFee().times(audienceCount);
    }

}

 

 

 

ReservationAgency는 Screening 메서드 호출해

예매요금을 계산 후 Reservation 생성

 

public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
        Money fee = screening.calculateFee(audienceCount);
        return new Reservation(customer, screening, fee, audienceCount);
    }
}

 

필요한 메서드를 데이터를 가지고 있는 객체 스스로 구현했다.

 


05. 하지만 여전히 부족하다

 

여전히 어느정도의 문제가 발행한다. ㅠㅠ

 

캡슐화 위반

 

DiscountCondition의 메소드 isDiscountable 의 경우 

요일정보와 시간정보를 파라미터 타입으로 받는것은 인스턴스 정보를 포함하고 있다는 것을 인터페이스를 통해 노출시키는것 

내부구현의 캡슐화는 실패!

 

Movie 의 경우

세개의 메서드는 할인정책이 세가지 존재한다는것을 노출. 실패!

 

변할 수 있는 어떤것도 감추어야한다.

 

높은 결합도

 

Movie와 DiscountCondition사이의 높은 결합도

요소들이 DiscountCondition의 구현에 속하는 것으로, 변경시 의존하는 Movie의 변경이 불가피.

 

낮은 응집도

 

할인 조건을 변경하기 위해 Screening 도 변경해야함. 

하나의 변경때문에 코드 여러곳을 동시에 변경해야한다는 것은 응집도가 낮은것.

 

 


06. 데이터 중심 설계의 문제점

 

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

 

데이터는 구현의 일부다. 하지만 너무 이른시기에 데이터에 관해 결정하도록 강요한다.

 

데이터와 기능을 분리하는 절차적 프로그래밍으로 인해 객체를 사용하는 절차를 분리된 별도의 객체에서 구현한다.

 

남발되는 접근자와 수정자는 객체의 캡슐화를 무력화한다.

 

 

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

 

협력이라는 문맥에서 적절한 책임을 결정하고 분배해야한다.

 

객체가 다른 객체와 협력하는 방식이 중요하다.

 

객체의 구현이 이미 결정된 상태에서 다른객체와 협력방법을 고민하기때문에

이미 구현된 객체의 인터페이스를 억지로 끼워맞추므로 협력이 내부 구현에 종속되어 있고 변경에 취약한 것이다.