설계 품질과 트레이드오프

2023. 10. 1. 15:52북리뷰/오브젝트

728x90

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

적절한 비용 안에서 쉽게 변경할 수 있는 설계는 응집도가 높고 서로 느슨하게 결합되어 있다.

중요한 것은 객체의 상태가 아닌, 객체의 행동에 초점을 맞추는 것이다. 

 

객체 지향 설계는 두가지 방법이 있다.

첫 번째 방법은 상태(데이터)를 분할의 중심축으로 삼는 방법이고,

두번째 방법은 책임을 분할의 중심축으로 삼는 방법이다.

 

객체지향 설계는 상태(데이터)가 아니라 책임에 초점을 맞춰야 한다. 

객체의 상태는 구현에 속하기 때문이다.

허나 책임은 인터페이스에 속한다.

인터페이스(책임) 뒤로 구현(상태)을 갭출화해야 안정적인 설계를 얻을 수 있다.

 

즉, 데이터를 중심으로 설계하면 안 된다.

이 객체가 포함해야 하는 데이터는 무엇인가? 라는 질문의 반복에 휩쓸려있다면, 데이터 중심의 설계에 매몰돼 있을 확률이 높다.

 

# 데이터  기반 설계 예시 (안좋은 예)

 

### 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 Money getFee() {
        return fee;
    }

    public void setFee(final Money fee) {
        this.fee = fee;
    }

    public List<DiscountCondition> getDiscountConditions() {
        return Collections.unmodifiableList(discountConditions);
    }

    public void setDiscountConditions(List<DiscountCondition> discountConditions) {
        this.discountConditions = discountConditions;
    }


    public MovieType getMovieType() {
        return movieType;
    }

    public void setMovieType(final MovieType movieType) {
        this.movieType = movieType;
    }

    public Money getDiscountAmount() {
        return discountAmount;
    }

    public void setDiscountAmount(final Money discountAmount) {
        this.discountAmount = discountAmount;
    }

    public double getDiscountPercent() {
        return discountPercent;
    }

    public void setDiscountPercent(final double discountPercent) {
        this.discountPercent = discountPercent;
    }
}

### MovieType

public enum MovieType {
    AMOUNT_DISCOUNT,
    PERCENT_DISCOUNT,
    NONE_DISCOUNT,
}

### DiscountConditionType

public enum DiscountConditionType {
    SEQUENCE,
    PERIOD,
}

### 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 void setType(final DiscountConditionType type) {
        this.type = type;
    }

    public int getSequence() {
        return sequence;
    }

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

    public DayOfWeek getDayOfWeek() {
        return dayOfWeek;
    }

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

    public LocalTime getStartTime() {
        return startTime;
    }

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

    public LocalTime getEndTime() {
        return endTime;
    }

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

 

### Screening

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

    public Movie getMovie() {
        return movie;
    }

    public void setMovie(final Movie movie) {
        this.movie = movie;
    }

    public int getSequence() {
        return sequence;
    }

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

    public LocalDateTime getWhenScreened() {
        return whenScreened;
    }

    public void setWhenScreened(final LocalDateTime whenScreened) {
        this.whenScreened = whenScreened;
    }
}

### Reservation

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

    public Reservation(final Customer customer, final Screening screening, final Money fee, final int audienceCount) {
        this.customer = customer;
        this.screening = screening;
        this.fee = fee;
        this.audienceCount = audienceCount;
    }

    public Customer getCustomer() {
        return customer;
    }

    public void setCustomer(final Customer customer) {
        this.customer = customer;
    }

    public Screening getScreening() {
        return screening;
    }

    public void setScreening(final Screening screening) {
        this.screening = screening;
    }

    public Money getFee() {
        return fee;
    }

    public void setFee(final Money fee) {
        this.fee = fee;
    }

    public int getAudienceCount() {
        return audienceCount;
    }

    public void setAudienceCount(final int audienceCount) {
        this.audienceCount = audienceCount;
    }
}

### Customer

public class Customer {
    private String name;
    private String id;

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

위 클래스드를 조합해서 영화 예매 절차를 구현해보자

 

### 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);
        }else {
            fee = movie.getFee();
        }

        return new Reservation(customer, screening, fee, audienceCount);
    }
}

# 설계 트레이드 오프

확실히 이전 설계와 비교했을 때 코드퀄리티가 떨어지는 것을 볼 수 있다. 

이전 설계외 장단점을 캡슐화, 응집도, 결합도 측면에서 볼 수 있다.

 

캡슐화를 통해 변경 가능성이 높은 부분은 내부로 숨기고, 외부에는 안정적인 부분만 공개하여 변경의 여파를 통제할 수 있다. 

응집도는 객체가 얼마나 관련 높은 책임을 할당했는지, 결합도는 객체가 협력에 필요한 적절 수준의 관계를 유지하고 있는지를 나타낸다.

변경의 관점에서 응집도는 변경 발생 시 모듈에서 발생하는 변경의 정도로 측정 가능하다.

즉, 하나의 변경에 하나의 모듈만 변경한다면 응집도가 높으나, 다수 모듈이 함께 변경돼야 한다면 응집도가 낮은 것이다.

결합도 또한 한 모듈이 변경되기 위해 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다.

즉, 캡슐화의 정도가 응집도와 결합도에 영향을 미치고,

응집과 결합을 고려하기 전에 먼저 캡슐화를 중시해야한다.

 

그럼 위 코드의 문제점을 다음과 같이 요약할 수 있다.

1. 캡슐화 위반

public class Movie {
    private Money fee;

    public Money getFee() {
        return fee;
    }

    public void setFee(final Money fee) {
        this.fee = fee;
    }
}

fee를 읽거나 수정하기 위해서는 getter와 setter를 사용해야하만 한다.

설계할 때 협력에 관해 고민하지 않으면 캡슐화를 위반하는 과도한 접근자와 수정자를 가지는 경향이 있다.

2. 높은 결합도

객체 내부 구현이 객체의 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미한다.

public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
       ...

        Money fee;
        if (discountable) {
            ...

            fee = movie.getFee().minus(discountAmount);
        }else {
            fee = movie.getFee();
        }

       ...
    }
}

데이터 객체들을 사용하는 제어 로직(ReservationAgency)이 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다. 즉, 다른 데이터 객체들이 변경되면 ReservationAgency도 변경을 유발한다.

 

3. 낮은 응집도

위 코드에서 할인 정책을 추가하면? 할인 조건이 추가되면? ReservationAgency도 수정이 필요하다.

 

캡슐화를 지키기 위해 다음과 같이 수정할 수 있다.

 

### DiscountCondition

 

### Movie

### Screening

 

### ReservationAgency

ReservationAgency 안에 있던 로직들을 각 객체로 분산하여 넣은 것을 볼 수 있다.

하지만, 여전히 부족하다. 아직도 Movie나 Screening에는 높은 결합도를 보여 변경에 취약한 것을 볼 수 있다.

 

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

1. 데이터 중심 설계는 본질적으로 너무 이른 시기에 데이터를 결정하도록 강요한다.

2. 데이터 중심 설계는 협력을 고려하지 않는다.

올바른 객체지향 설계는 항상 객체 내부가 아니라 외부에 있어야 한다.

 

728x90

'북리뷰 > 오브젝트' 카테고리의 다른 글

역할, 책임, 협력  (1) 2023.09.30
객체지향 프로그래밍  (0) 2023.09.30
객체, 설계  (0) 2023.09.29