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

ppparkta·2025년 4월 2일
0

오브젝트

목록 보기
6/6

학습 목표

  • 객체지향에서 가장 중요한 것은 책임, 협력, 역할
    • 그 중에서도 가장 중요한 것은 책임이다
    • 왜냐하면 역할은 책임의 집합이고, 올바른 책임을 설정해야 올바른 협력이 가능하기 때문이다
  • 객체지향 설계란 올바른 객체가 올바른 책임을 할당받으면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다
  1. 올바른 객체가 올바른 책임을 한다
  2. 책임은 낮은 결합도와 높은 응집도, 즉 설계 품질과 관련되어 있다

⇒ 이번 장에서는 데이터 중심의 설계를 통해 왜 상태 중심의 설계가 문제되는지 확인한다.

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

  1. 상태를 분할의 중심축으로 두기
    • 구현 위주 → 변경 가능성 높음 (캡슐화 깨짐)
  2. 책임을 분할의 중심축으로 두기
    • 인터페이스 → 변경에 안정적 (캡슐화)

데이터를 중심으로 Movie 객체를 다시 작성한다.

이번에는 DiscountPolicy가 빠지고 그 자리를 MovieType이 대체했다.

데이터로 놓고 본다면 DiscountPolicy는 객체에 딱 하나만 필요하기 때문이다.

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 enum MovieType {
    AMOUNT_DISCOUNT,    // 금액 할인 정책
    PERCENT_DISCOUNT,   // 비율 할인 정책
    NONE_DISCOUNT       // 미적용
}

위와 같이 한 객체 내에

(1)객체의 종류를 저장하는 데이터(movieType)와

(2)인스턴스에 따라 배타적으로 사용될 변수(discountAmount, discountPercent)를 함께 저장하는 것

데이터 중심 설계에서 흔히 볼 수 있는 사례다.

정의한 데이터를 캡슐화하기 위해서 접근자와 제어자 (getter & setter)를 추가한다.

이제 할인 조건을 구한다. 할인 조건을 구하는 로직은 크게 두 부분으로 나뉜다 (ReservationAgency)

  • DiscountCondition에 대해 루프 돌며 할인 가능 여부 확인
  • discountable 변수의 값 체크하고 적절한 할인 정책에 따라 예매 요금 계산
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.getWhenScreend().getDayOfWeek().equals(condition.getDayOfWeek()) &&
                        condition.getStartTime().compareTo(screening.getWhenScreend().toLocalTime()) <= 0 &&
                        condition.getEndTime().compareTo(screening.getWhenScreend().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. 캡슐화
  2. 응집도와 결합도

캡슐화

  • 변경될 가능성이 높은 부분(구현)
  • 상대적으로 안정적인 부분(인터페이스)
  • 기본적인 아이디어는 변경 가능성이 높은 부분은 구현으로 두고 외부에서는 인터페이스에만 접근 가능하도록 제어한다. ⇒ 캡슐화를 지킬 수 있다
    • 캡슐화의 정도가 응집도와 결합도를 결정한다

응집도와 결합도

여기서 말하는 응집도란 변경의 관점에서 어떤 변경에 대해 얼마나 유연하게 처리할 수 있느냐임. 하나의 변경에 대해 하나의 모듈만 수정하면 된다면 응집도가 높은 것임.

변경의 관점

  • 응집도
    • 변경이 발생할 때 모듈 내부에서만 변경이 발생하는 정도
      • 만약 변경이 외부 모듈에도 영향을 준다면 응집도가 낮음
    • 하나의 변경에 대해 하나의 모듈만 변경됨 → 응집도가 높음
    • 다수의 모듈이 함께 변경돼야 함 → 응집도가 낮음

높은 응집도

낮은 응집도

  • 결합도
    • 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도
      • 응집도와 반대되는 단어임. 한 모듈에 변경에 대해 외부 모듈이 영향을 받는 정도를 의미함
    • 하나의 모듈을 수정할 때 얼마나 많은 모듈을 함께 수정해야 하는지

낮은 결합도

높은 결합도

참고 결합도가 높아도 상관 없는 경우도 있다. 일반적으로 변경될 확률이 매우 적은 안정적인 모듈에 의존하는 것은 아무런 문제도 되지 않는다.
ex) 자바의 String, ArrayList

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

책임 중심의 영화 예매 시스템에서 다루게 될 구현은 같지만 캡슐화를 다루는 방식에 큰 차이가 있다.

책임 중심 설계는 자주 변경되는 구현부를 객체 내부에 숨기고 인터페이스만 공개함으로서 캡슐화한다.

캡슐화는 객체의 응집도와 결합도를 결정한다. 데이터 중심 설계의 문제점은 아래와 같다.

  1. 캡슐화 위반
  2. 높은 결합도
  3. 낮은 응집도

캡슐화 위반

  • 접근자와 제어자를 사용하는 것은 사실상 상태를 public 상태로 바꾸는 것이나 다름없다. → 캡슐화를 위반한다
  • 추측에 의한 설계 전략(getter/setter 의존)을 피해야 한다.
    • 추측에 의한 설계 전략이란? 협력을 고려하지 않고 객체가 다양한 상황에서 아용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행한다.

높은 결합도

  • 접근자와 제어자 인터페이스에 의존하는 모든 클라이언트가 함께 변경되므로 결합도가 높아진다
  • 데이터 중심의 설계는 객체의 캡슐화를 약화시키므로 클라이언트가 객체의 구현에 강하게 결합한다

낮은 응집도

단일 책임 원칙 (Single Responsibility Principle)

로버트 마틴이 모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위해서 제시한 설계 원칙

→ 클래스는 단 한 가지의 변경 이유만 가져야 한다

여기서 책임은 변경의 이유라는 의미로 사용된다. 단일 책임 원칙에서의 책임은 지금까지의 역할, 책임, 협력과는 다르며 변경과 관련된 더 큰 개념이다.

자율적인 객체를 향해

책임의 주체를 자기 자신으로 이동시킨다. 즉, 책임을 이동시킨다. 객체가 자기 스스로를 책임진다는 말의 의미는 이와 같다.

아래와 같이 사각형을 나타내는 클래스가 있을 때, 너비와 높이를 증가시키는 코드가 필요하다면 getter와 setter를 사용해서 꺼내쓰는 것이 아니라 객체 내부에서 계산한다.

public 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 void enlarge(int multiple) {
    right *= multiple;
    bottom *= multiple;
}

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

이제 객체를 설계할 때 이 객체에 어떤 데이터를 포함해야 하는가? 에 대한 질문은 두개로 나눠서 생각해야 한다.

  1. 이 객체에 포함될 데이터는 무엇인가?
  2. 데이터를 사용해서 수행할 오퍼레이션은 무엇인가?

이 문제를 고려하며 최종적으로 위에서 정의한 ReservationAgency를 다시 설계해본다.

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

첫번째 설계보다 두번째 설계가 내부 구현을 더 면밀하게 캡슐화한다. 객체 스스로 구현을 하고 있다. 따라서 이 객체들은 스스로를 책임진다고 말할 수 있다.

더 개선하기

코드를 더 개선할 수 있다. 사실, 첫번째 코드에서 발생했던 문제(캡슐화 위반, 높은 결합도, 낮은 응집도)는 두번째 코드에서도 똑같이 드러난다.

캡슐화 위반

DiscountCondition의 메서드를 보자.

매개변수를 통해 객체 내부에 DayOfWeek 타입 요일과 LocalTime 타입 시간 정보가 인스턴스 변수로 포함된 사실을 알 수 있다.

public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {...}
public boolean isDiscountable(int sequence) {...}

만약 DiscountCondition의 속성을 변경해야 한다면? isDiscountable 메서드는 파라미터를 수정하고 이를 사용하는 모든 클라이언트도 함께 수정해야 한다.

→ 내부 구현의 변경이 외부로 퍼져나가는 파급 효과는 캡슐화가 부족하다는 명백한 증거다.

첫번째 설계에 비해 자기 자신을 스스로 처리한다는 점에서 개선되었으나, 여전히 캡슐화에는 실패했다.

캡슐화의 진정한 의미

캡슐화는 단순히 객체 내부의 데이터를 외부로부터 감추는 것 이상의 의미를 가진다. 캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다.

속성의 타입, 할인 정책의 종류 상관 없이 내부 구현 변경으로 인해 외부 객체가 영향을 받는다면 캡슐화 위반이다.

높은 결합도

캡슐화 위반은 결합도 상승과 응집도 하락을 초래한다.

DiscountCondition 내부 구현이 외부로 노출되었으므로 Movie와 DiscountCondition 사이 결합도는 높을 수밖에 없다.

Movie의 isDiscountable() 메서드를 보면 캡슐화 위반으로 인해 높은 결합도가 생긴 것을 확인할 수 있다.

  • DiscountCondition 기간 할인 명칭이 변경되면 Movie도 수정해야 함
  • DiscountCondition 종류 추가 혹은 삭제 시 Movie도 수정해야 함
  • DiscountCondition의 만족 여부 판단하는 데 필요한 정보가 변경된다면 Movie의 isDiscountable 메서드로 전달된 파라미터를 변경해야 함. 이는 결과적으로 Screening에 대한 변경을 초래함

위에서 언급한 부분은 DiscountCondition의 구현에 속한다. 인터페이스가 아니라 구현을 변경할 때에도 이에 의존하는 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;
}

이러한 문제의 근본적인 원인은 캡슐화 위반이다.

낮은 응집도

이번에는 Screening이다.

DiscountCondition이 할인 여부 판단하는 데 필요한 정보가 변경된다면 Movie의 isDiscountable 메서드로 전달해야 하는 파라미터의 종류를 변경해야 한다.

→ 이로 인해 Screening에서 Movie에 의존하는 부분도 함께 변경해야 한다.

public Money calculateFee(int audienceCount) {
    switch (movie.getMovieType()) {
        case AMOUNT_DISCOUNT:
            if (movie.isDiscountable(whenScreend, sequence)) {
                return movie.calculateAmountDiscountedFee().times(audienceCount);
            }
            break;
        case PERCENT_DISCOUNT:
            if (movie.isDiscountable(whenScreend, sequence)) {
                return movie.calculatePercentDiscountedFee().times(audienceCount);
            }
    }
    return movie.calculateNoneDiscountedFee().times(audienceCount);
}

하나의 변경을 수용하기 위해서 코드 여러 곳을 동시에 변경해야 한다는 것은 설계 응집도가 낮다는 증거다.

→ 서로 다른 책임을 동일한 인터페이스에서 수행하고 있을 확률이 높다

응집도가 낮은 이유 역시 캡슐화 위반이다.

데이터 중심 설계의 문제점

두번째 설계가 변경에 유연하지 못한 이유는 결국 캡슐화를 위반했기 때문이다.

  • 데이터 중심 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요함
  • 데이터 중심 설계에서 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정함

데이터도 구현의 일부일 뿐이다.

profile
겉촉속촉

0개의 댓글