본문 바로가기
스터디/오브젝트

오브젝트 04 - 설계 품질과 트레이드오프

by 디토20 2024. 2. 19.
반응형

 

 

 

 

 

오브젝트 04 - 설계 품질과 트레이드오프

가끔 좋은 설계보다는 나쁜 설계를 통해 통찰을 얻기도 한다. 특히 좋은 설계와 나쁜 설계를 비교할 효과가 좋다.

이번 장에서는 영화 예매 시스템을 책임이 아닌 상태를 표현하는 데이터 중심의 설계를 살펴보고 객체 지향적으로 설계한 구조와 어떤 차이점이 있는지 살펴보자.

 

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

  • 데이터 중심 설계에서 객체는 자신이 포함하고 있는 데이터를 조작하는데 필요한 오퍼레이션을 정의한다.
  • 데이터 중심 설계에서는 객체를 독립된 데이터 덩어리로 바라본다.
  • 상태가 객체 분할의 중심이 되면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화가 무너진다.
  • 과적으로 상태 변경은 인터페이스의 변경을 초래하여 해당 인터페이스에 의존하는 모든 객체에게 영향을 준다.
  • 따라서 데이터에 초점을 맞추는 설계는 변경에 취약할 수밖없다.

 

4.1.1 데이터를 준비하자

  • 데이터 중심 설계는 '데이터가 무엇인가'를 묻는 것으로 시작한다.
  • 먼저 Movie저장될 데이터를 정하는 것으로 설계시작하자.
# 데이터 중심 설계
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 class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }
    
    public Money getFee() {
        return fee;
    }
    public Money calculateMovieFee(Screening screening) {
    	return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

 

  • 책임 중심 설계와 가장 다른 점은 할인 조건의 목록(discountConditions)이 Movie 안에 변수로 포함되어 있다는 것
  • 또한 할인 정책의 할인 금액(discountAmount)할인 비율 (discountPercent)Movie 안에서 정의
  • 영화는 하나의 할인 정책을 가질 수 있으므로 movieType으로 종류를 결정

 

데이터 중심의 설계를 하게 되는 과정
  • 데이터 중심의 설계에서는 객체의 책임보다 객체가 포함해야 하는 데이터에 집중
  • 객체는 어떤 데이터를 가져야하나? 객체의 책임을 결정하기 전에 이런 질문을 반복한다면 데이터에 몰되어 있을 것이다.
  • 특히 Movie 클래스의 경우처럼 객체의 종류를 저장하는 변수(movieType)와 인스턴스의 종류에 따라 각각 사용될 인스턴스 변수(discountAmount, discountPercent)하나의 클래스 안에 함께 포함시키는 방식은 데이터 중심의 설계 안에서 흔히 볼 수 있는패턴이다.

 

이제 할인 조건을 저장할 데이터를 구현해 보자.

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

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

 

  • movie와 마찬가지로 순번 조건에서만 사용되는 데이터와 기간 조건에서만 사용되는 데이터를 함께 포함한다.

 

마지막으로 screening 클래스와 Reservation 클래스를 구현하자

public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;
}

public class Reservation {
    private Customer customer;
    private Screening screening;
    private Money fee;
    private int audienceCount;
}

 

 

  • 그 후, 모든 클래스의 캡슐화를 위해 getter와 setter를 생성하자.
  • 영화 예매 시스템을 위해 필요한 모든 데이터를 클래스로 구현했다.

 

영화 예매 시스템 구현을 위한 데이터 클래스

 

 

 

4.1.2 영화를 예매하자

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

 

  • reserve 메서드는 크게 할인 가능 여부를 확인하는 for 문과, 적절한 할인 정책에 따라 예매 요금을 계산하는 if 문으로 구성되어있다.

 

 

4.2 설계 트레이드오프

4.2.1 캡슐화

  • 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 하기 때문이다.
  • 변경 가능성이 높은 부분은 내부에 숨기고 외부에안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다.

 

4.2.2 응집도와 결합도

  • 응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.
  • 결합도의존성의 정도나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타낸다.
  • 높은 응집도와 낮은 결합도를 가진 설계를 추구해야 하는 단 한가지의 이유는 그것이 설계를 변경하쉽게 만들기 때문이다. 

 

 

 

 

  • 일반적으로 변경될 확률이 매우 적은 안정적인 모듈과는 결합도가 높아도 상관이 없다.
    • 표준 라이브러리에 포함된 모듈이나 성숙 단계에 접어든 프레임워크 등
    • 예를 들어, 자바의 String이나 Arraylist는 변경될 확률이 매우 낮기 때문에 결합도에 대해 고민할 필요가 없다.
  • 그러나 직접 작성한 코드는 항상 불안정하며 언제라도 변경될 기능성이 높다.
  • 따라서 직접 작성한 코드의 경우에는 낮은 결합도를 유지하려고 노력해야 한다.
  • 마지막으로 캡슐화를 지키면 모듈 안의 응집도는 높아지고 모듈 사이의 결합도는 낮아진다.

 

 

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

4.3.1 캡슐화 위반

public class Movie {
    private Money fee;
    
    public Money getFee() {
    	return fee;
    }
    
    public void setFee(Money fee) {
    	this.fee = fee;
    }
}

 

  • 위 코드는 객체의 내부에 직접 접근할 수 없어서 캡슐화의 원칙을 지키는 것 같다.
  • 그러나 접근자와 수정자 메서드는 fee라는 변수의 존재를 공개적으로 드러내 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다.
  • Movie가 캡슐화의 원칙을 어기게 근본적인 원인은 객체가 수행할 책임이 아니라 내부에 저장할 데이터에 초점을 맞췄기 때문이다.
  • 객체의 협력을 고려하지 않고 객체가 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행한다.

 

 

4.3.2 높은 결합도

  • 객체 내부의 구현이 객체의 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미한다.
  • 이것은 단지 객체의 내부 구현을 변경했음에도 이 인터페이스에 의존하는 모든 클라이언트들도 함께 변경해야 한다는 것이다.
  • 데이터 중심 설계에서는 여러 데이터 객체들을 사용하는 제어 로직특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것이 다.
  • 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수밖에 없다.

너무 많은 대상에 의존하기 때문에 변경에 취약한 Reservation Agency

 

4.3.3 낮은 응집도

  • 서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 말한다.
  • 현재의 설계는 새로운 할인 정책을 추가하거나 새로운 할인 조건을 추가하기 위해 하나 이상의 클래스를 동시에 수정해야 한다.

 

 

4.4 자율적인 객체를 향해

4.4.1 캡슐화를 지켜라

  • 객체에게 의미 있는 메서드는 객체가 책임져야 하는 무언가를 수행하는 메서드다.
  • 속성의 가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다.

 

Rectangle을 통한 캡슐화 위반 예제
@Getter
@Setter
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;
    }
}


class AnyClass {
    void anyMethod(Rectangle rectangle, int multiple) {
        rectangle.setRight(rectangle.getRight() * multiple); 
        rectangle.setBottom(rectangle.getBottom() * multiple);
    }
}

 

  • 이 코드의 첫번째 문제점은 ‘코드 중복’이 발생할 확률이 높다.
  • 다른 곳에서도 사각형의 너비와 높이를 증가시키는 코드가 필요하다면 유사한 코드가 존재할 것이다.
  • 번째 문제점은 변경에 취약하다는 점이다.
  • Rectangle의 필드명을 변경한다면, 그것을 사용하고 있는 모든 클래스도 변경이 필요하다.

 

 캡슐화를 통한 해결
class Rectangle {
    public void enlarge(int multiple) {
    right *= multiple;
    bottom *= multiple;
    }
}

 

  • Rectangle 내부에 너비와 높이를 조절하는 로직을 캡슐화해서 두 가지 문제를 해결할 수 있다.
  • Rectangle을 변경하는 주체를 외부의 객체에서 Rectangle이동시켰다.

 

 

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

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

public class Screening {
    private Movie movie;
    private int sequence;
    private LocalDateTime 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);
                }
                break;
            case NONE_DISCOUNT:
            	return movie.calculateNoneDiscountedFee().times(audienceCount);
        }
        return movie.calculateNoneDiscountedFee().times(audienceCount);
    }
}


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

 

  • 최소한 결합도 측면에서 ReservationAgency에 의존성이 몰려있던 첫 번째 설계보다는 개선되었다.
  • 첫 번째 설계보다 내부 구현면밀하게 캡슐화하고 있다.
  • 데이터처리하는 필요한 메서드를 데이터가지고 있는 객체 스스로 구현하고 있다.

 

 

 

 

4.5 하지만 여전히 부족하다

4.5.1 캡슐화 위반

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) { ... }
}

 

  • 기간 조건을 판단하는 isDiscountable은 인스턴스 변수를 인터페이스를 통해 외부에 노출하고 있다.
  • DiscountCondition속성을 변경해야 한다면 해당 메서드를 사용하는 모든 클라이언트도 함께 수정해야 한다.
  • 내부 구현의 변경이 외부로 퍼져나가는 파급 효과(ripple effect)캡슐화가 부족하다는 명백한 증거다.
  • Movie 역시 개의 메서드를 통해 할인 정책에는 금액, 비율, 미적용의 가지가 존재한다는 사실을 노출하고 있다.
    • 새로운 할인 정책이 추가되거나 제거된다면 메서드들에 의존하는 모든 클라이언트가 영향을 받는다.
    • Movie가지 할인 정책을 포함하고 있다는 내부 구현을 성공적으로 캡슐화하지 못한다.

 

4.5.2 낮은 응집도

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);
                }
                break;
            case NONE_DISCOUNT:
            	return movie.calculateNoneDiscountedFee().times(audienceCount);
                break;
        }
    	return movie.calculateNoneDiscountedFee().times(audienceCount);
    }
}

 

 

  •  DiscountCondition이 할인 여부를 판단하는데 필요한 정보가 변경된다면 Screening에서 Movie의 isDiscountable 메서드를 호출하는 부분도 함께 변경해야 한다.
  • 인 조건을 변경하기 위해서DiscountCondition, Movie, 리고 Movie사용히­ Screening을 전부 수정해야 한다
  • 하나의 기능을 변경하기 위해 여러 을 동시에 해야 한다는 것은 계의 응집도가 낮다는 증거다.

 

 

4.6 데이터 중심 설계의 문제점

  • 두 번째 설계가 변경에 유연하지 못한 이유는 캡슐화를 위반했기 때문이다.
  • 캡슐화를 위반한 설계는 응집도가 낮고 결합도가 높아 변경에 취약하다.
  • 데이터 중심의 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
  • 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.
     

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
반응형

댓글