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

bien·2024년 9월 25일
0

오브젝트

목록 보기
4/13
  • 객체지향 설계의 핵심; 역할, 책임, 협력
    • 협력; 앱 기능을 구현하기 위해 메시지를 주고 받은 객체들 사이의 상호작용
    • 책임; 객체가 다른 객체와 협력하기 위해 수행하는 행동
    • 역할; 대체 가능한 책임의 집합
  • 책임 주도 설계
    • 이름에서 알 수 잇듯, 가장 중요한 것은 책임이다.
    • 책임이 객체지향 애플리케이션 전체의 품질을 결정한다.
  • 객체지향 설계; 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동.
    1. 시사점1: 객체지향 설계의 핵심은 책임
    2. 시사점2: 책임을 할당하는 작업이 응집도와 결합도 같은 설계 품질과 깊이 연관돼 있음
  • 설계는 변경을 위해 존재하며, 변경은 비용을 발생시킨다.
    • 훌륭한 설계; 합리적인 비용 안에서 변경을 수용할 수 있는 구조
    • 적절한 비용안에서 쉽게 변경할 수 있는 설계
      • 응집도가 높고 서로 느슨하게 결합되어있는 요소로 구성
  • 객체의 상태가 아니라 행동에 초점을 맞춰야 한다.

이번 장에서는 데이터 중심 설계를 살펴보고, 이후 객체지향 설계와 비교해본다.

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

  • 객체지향 설계에서 객체 분할 방법
    1. 상태 (=데이터)를 분할의 중심축으로 삼는 방법
      • 객체의 상태 = 객체가 저장해야 하는 데이터의 집합
    2. 책임을 분할의 중심축으로 삼는 방법

  • 훌륭한 객체지향 설계는 데이터가 아니라 책임에 초점을 맞춰야 한다.
    • 객체의 상태는 구현에 속한다.
      • 따라서, 상태에 초점을 맞추면, 객체의 구현에 관한 세부사항이 객체의 인터페이스에 스며들어 캡슐화의 원칙이 무너진다.
      • 즉, 상태 변경 > 인터페이스 변경 > 의존하는 모든 외부 객체로의 변경으로 변경이 전파되게 된다.
        • 결론; 데이터 중심 설계 = 변경에 취약
    • 객체의 책임은 인터페이스에 속한다.
      • 책임을 드러내는 안정적인 인터페이스 뒤로, 책임을 수행하는 데 필요한 상태를 캡슐화하여, 구현 변경의 파장이 외부로 퍼져나가는 것을 방지한다.
        • 결론; 책임 중심 설계 = 변경에 안정적

설계1; 데이터 준비

  • 데이터 중심 설계; 데이터를 기반으로 시스템을 분할
    • 객체가 내부에 저장해야 할 데이터가 무엇인가?

영화 설계

Movie.java

  • 할인의 조건 목록(discountCondition)이 인스턴스 변수로 Movie 안에 직접 포함되어 있다.
  • 할인의 정책을 discountPolciy로 분리했던 예전 예제와 달리 할인 금액(discountAmount)과 비율(discountPercent)을 Movie 안에서 직접 정의하고 있다.
  • 내부 데이터가 객체 밖으로 빠져나가 외부 객체를 오염시키는 것을 막기 위해, 접근자(accessor)수정자(mutator)를 사용할 수 있다.
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 List<DiscountCondition> getDiscountConditions() {
        return discountConditions;
    }

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

    public MovieType getMovieType() {
        return movieType;
    }

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

    public Money getDiscountAmount() {
        return discountAmount;
    }

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

    public double getDiscountPercent() {
        return discountPercent;
    }

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

}
  • 데이터 중심 설계에서 흔히 볼수 있는 방식;
    • 객체의 종류를 결정하는 인스턴스 변수(movieType)와 인스턴스 종류에 따라 배타적으로 사용될 인스턴스 변수(discountAmount, discountPercent)를 하나의 클래스 안에 포함시키는 방식

MovieType.java

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

    public int getSequence() {
        return sequence;
    }

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

    public DayOfWeek getDayOfWeek() {
        return dayOfWeek;
    }

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

    public LocalTime getStartTime() {
        return startTime;
    }

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

    public LocalTime getEndTime() {
        return endTime;
    }

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

영화 예매 설계

Screening.java

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

    public int getSequence() {
        return sequence;
    }

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

    public LocalDateTime getWhenScreened() {
        return whenScreened;
    }

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

예약 설계

Reservation.java

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

    public Reservation(Customer customer, 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;
    }

    public Screening getScreening() {
        return screening;
    }

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

    public Money getFee() {
        return fee;
    }

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

    public int getAudienceCount() {
        return audienceCount;
    }

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

고객 정보 설계

Customer.java

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

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

설계2; 영화 예매

Reservation.java

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()
    1. DiscountCondition에 대해 루프를 돌며 할인 가능 여부를 판단하는 for
    2. discountable의 값을 체크하고, 적절한 할인 정책에 따라 예매 요금을 계산하는 if

  • 설계의 완성도를 평가할 수 있는 세 가지 평가 척도을 알아보자
    • 캡슐화, 응집도, 결합도

02. 설계 트레이드 오프

캡슐화

  • 객체를 사용하면 변경 가능성이 높은 부분은 내부에 숨기고, 외부에는 상대적으로 안정된 부분만 공개함으로써 변경의 여파를 통제할 수 있다.
    • 구현; 변경될 가능성이 높은 부분
    • 인터페이스; 상대적으로 안정적인 부분
  • 캡슐화; 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류
    • 객체지향에서 가장 중요한 부분
    • 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법

복잡성을 다루기 위한 가장 효과적인 도구는 추상화다. 다양한 추상화 유형을 사용할 수 있지만 객체지향 프로그래밍에서 복잡성을 취급하는 주요한 추상화 방법은 캡슐화다. 그러나 프로그래밍할 때 객체지향 언어를 사용한다고 해서 애플리케이션의 복잡성이 잘 캡슐화될 것이라고 보장할 수는 없다. 훌륭한 프로그래밍 기술을 적용해서 캡슐화를 향상시킬 수는 있겠지만, 객체지향 프로그램을 통해 전반적으로 얻을 수 있는 장점은 오직 설계 과정 동안 캡슐화를 목표로 인식할때만 작성될수 있다.[Wirfs-Brock89].

  • 설계가 필요한 이유 = 요구사항이 변경되기 때문
    • 캡슐화가 중요한 이유 = 불안정한 부분과 안정적인 부분을 분리해 변경의 영향을 통제할 수 있기 때문
    • 따라서, 변경의 관점에서, 설계의 품질을 판단하기 위해 캡슐화를 기준으로 삼을 수 있다.
  • 캡슐화: 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법
    • 변경될 수 있는 어떤 것이라도 캡슐화해야 한다.
      • 이 점이 객체지향 설계의 핵심

유지보수성이 목표다. 여기서 유지보수성이란 두려움 없이, 주저함 없이, 저항감 없이 코드를 변경할 수 있는 능력을 말한다. ... 가장 중요한 동료는 캡슐화다. 캡슐화란 어떤 것을 숨긴다는 것을 의미한다. 우리는 시스템의 한 부분을 다른 부분으로 감춤으로써 뜻밖의 피해가 발생할 수 있는 가능성을 사전에 방지할 수 있다. 만약 시스템이 완전히 캡슐화된다면 우리는 변경으로부터 완전히 자유로워질 것이다. 만약 시스템의 캡슐화가 크게 부족하다면 우리는 변경으로부터 자유로울 수 없고, 결과적으로 시스템은 진화할 수 없을 것이다. 응집도, 결합도, 중복 역시 훌륭한(변경 가능한) 코드를 규정하는데 핵심적인 품질인 것이 사실이지만 캡슐화는 우리를 좋은 코드로 안내하기 때문에 가장 중요한 제 1원리다.[Bain08].

  • 캡슐화가 응집도와 결합도에 영향을 미친다.
    • 캡슐화 준수; 응집도 ⬆️, 결합도 ⬇️

응집도

  • 응집도
    • 모듈에 포함된 내부 요소들이 연관돼 있는 정도
    • 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 갖는 것.
    • 객체나 클래스에 얼마나 관련이 높은 책임들을 할당했는가?
  • 좋은 설계; 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계
    • 높은 응집도 && 낮은 결합도
      • 변경을 수용하는것에 유리하다.
    • 응집도가 높음 >> 변경의 대상과 범위가 명확해짐 >> 코드 변경이 쉬워진다.

결합도

  • 결합도
    • 의존성의 정도.
      • 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도
    • 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도
    • 낮은 결합도; 타 모듈에 대해 필수적인 의존만 가지는 경우
    1. 결합도 판단기준1: 영향을 받는 모듈의 수
    2. 결합도 판단기준2: 변경의 원인
      • 내부 구현 변경 시 타 모듈에 영향을 미치는 경우 => 결합도 높음
      • 퍼블릭 인터페이스 수정 시에만 다른 모듈에 영향을 미치는 경우 => 결합도 낮음
        • 따라서 클래스 구현이 아닌 인터페이스에 의존하도록 코드를 작성해야 낮은 결합도를 얻을 수 있다.
  • 좋은 설계; 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계
    • 높은 응집도 && 낮은 결합도
      • 결합도가 높음 >> 변경해야 할 모듈이 많음 >> 변경이 어려워짐
  • 결합도가 높아도 상관없는 경우
    • 변경될 확률이 매우 적은 안정적인 모듈에 의존하는 것은 문제가 되지 않음.
      • ex) String, ArrayList
    • 직접 작성한 코드는 불안정하며 변경 가능성이 높으므로, 수정을 고려해야 함.

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

  • 데이터 중심 설계; 캡슐화 위반
    • 객체 내부 구현을 인터페이스의 일부로 만든다.
  • 책임 중심 설계
    • 객체의 내부 구현을 안정적인 인터페이스의 뒤로 캡슐화한다.
  • 데이터 중심 설계가 가진 문제점
    1. 캡슐화 위반
    2. 높은 결합도
    3. 낮은 응집도

캡슐화 위반

Movie.java

public class Movie {
	private Money fee;
    
    public Money getFee() {
    	return fee;
    }
    
    public void setFee(Money fee) {
    	this.fee = fee;
    }
}
  • 직접 내부 필드에 접근할 수 없으므로,내부를 캡슐화하고 있는 것으로 보인다.
    • 그러나 접근자와 수정자 메서드가 내부 구현을 노골적으로 외부 인터페이스에 드러낸다.
      • 이는 객체의 책임이 아니라, 내부 저장할 데이터에 초점을 맞췄기 때문이다.
  • 추측에 의한 설계 전략(design-by-guessing strategy)
    • 접근자와 수정자에 과도하게 의존하는 설계 방식
    • 객체가 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행
      • 접근자와 수정자에 과도하게 의존하게 된다.
    • 내부의 구현이 퍼블릭 인터페이스에 노출될 수 밖에 없음.
      • 캡슐화를 위반하는, 변경에 취약한 설계 유도.

높은 결합도

ReservationAgency.java

public class ReservationAgency {
	public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
    	...
        Money fee;
        if (discountable) {
        	...
            fee = movie.getFee().minus(discountedAmount).times(audienceCount);
        } else {
        	fee = movie.getFee();
        }
        ...
    }
}
  • 만약 fee의 타입이 변경되는 경우,
    • getFee() 메서드의 반환 타입이 수정되어야 한다.
    • getFee()를 호출하는 ReservationAgency의 구현도 변경된 타입에 맞게 함께 수정되어야 한다.
  • 사실상 getFee()메서드의 사용은, 변수 fee의 가시성을 private에서 public으로 변경하는 것과 거의 동일하다.
    • 데이터 중심 설계는 객체의 캡슐화를 약화시켜 클라이언트가 객체의 구현에 강하게 결합되게 한다.

  • 대부분의 제어 로직을 가지고 있는 ReservationAgency가 모든 데이터 객체에 의존하고 있다.
    • DiscountCondition, Screening츼 데이터 변경 시, ReservationAgency도 함께 수정되어야 한다.
    • 모든 의존성이 모이는 결합도의 집결지로, 시스템 안의 어떤 변경도 RservationAgency의 변경을 유발한다.

📌 결론

  • 데이터 중심 설계; 결합도와 관련해서 치명적인 문제점을 가진다.
    • 전체 시스템을 하나의 거대한 의존성 덩어리로 만든다.
    • 변경 발생 시, 시스템 전체가 영향을 받게 된다.

낮은 응집도

  • 변경해야 하는 2개 이상의 이유가 하나의 코듈 안에서 공존하는 경우
    • = 모듈의 응집도가 낮다
  • 다음의 수정사항은 ReservationAgency 코드를 수정하게 한다.
- 할인 정책이 추가되는 경우
- 할인 정책별로 할인 요금을 계산하는 방법이 변경될 경우
- 할인 조건이 추가되는 경우
- 할인 조건별로 할인 여부를 판단하는 방법이 변경될 경우
- 예매 요금을 계산하는 방법이 변경될 경우

낮은 응집도가 유발하는 설계 문제

  1. 변경의 이유가 다른 코드가 하나의 모듈에 뭉쳐있으면, 변경과 상관없는 코드가 영향을 받는다.
    • ex) ReservationAgency할인 정책을 선택하는 코드할인 조건을 판단하는 코드가 함께 위치하기 때문에, 새로운 할인 정책을 추가하는 작업이 할인 조건에도 영향을 미칠 수 있다.
    • 코드를 수정한 후에, 아무런 상관이 없는 코드에 문제가 발생하는 것은 응집도가 낮을 때 발생하는 대표적인 증상이다.
  2. 하나의 요구사항 변경을 반영하기 위해, 동시에 여러 모듈을 수정해야 한다.
    • 응집도가 낮을 경우, 다른 모듈에 위치해야 할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다.
    • ex) 새로운 할인 정책을 추가하기 위해서는 MovieType에 새로운 할인 정책을 표현하는 열거형을 추가하고, ReservationAgencyreserveswitch문에 새로운 case절을 추가해야 한다. 또한 새로운 할인 정책에 따른 요금 계산 로직을 Movie에 추가해야 한다.
      • 결과적으로, 하나의 요구사항 변화를 수용하기 위해 3개의 클래스가 수정되어야 한다.
    • 요구사항 변경을 위해 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 증거다.

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

  • 로버트 마틴(Robert C. Martin); 모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위해 단일 책임 원칙이라는 설계 원칙을 제시.
  • 클래스는 단 한가지의 변경 이유만 가져야 한다.
  • '책임'이라는 말이 '변경의 이유'라는 의미로 사용된다.
    • 책에서의 책임과 달리, 변경과 관련된 더 큰 개념을 가리킨다.

04. 자율적인 객체를 향해

캡슐화를 지켜라

  • 데이터 중심 설계의 문제점; 캡슐화 위반
    • 객체는 자신의 데이터를 내부에 캡슐화하고, 외부에 공개해서는 안된다.
    • 객체는 스스로의 상태를 책임져야 하며, 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.
  • 객체의 메서드; 객체가 책임져야 하는 무언가를 수행하는 메서드.
    • 속성의 가시성을 private로 설정했더라도, 접근자와 수정자를 통해 속성이 외부로 공개된다면, 캡슐화를 위반하는 것이다.

💻 캡슐화 전후 예시; Rectangle

Rectangle.java

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 int getLeft() {
        return left;
    }

    public int getTop() {
        return top;
    }

    public int getRight() {
        return right;
    }

    public int getBottom() {
        return bottom;
    }
}

너비 & 높이 증가 코드 추가

❌ AnyClass.java; 내부 구현 외부 노출 예시

class AnyClass {
	void anyMethod(Rectagle rectangle, int multiple) {
    	rectangle.setRight(rectangle.getRight() * multiple);
		rectangle.setBottom(rectangle.getRight() * multiple);        
    }
}
  • 문제점1; 코드 중복 발생 확률이 높음
    • 사각형의 너비와 높이를 증가시키는 코드가 필요할 때 마다 getter를 호출해 조작할 것.
      • 코드의 중복은 반드시 제거되어야 한다.
  • 문제점2; 변경에 취약
    • 접근자(getter)와 수정자(setter)는 내부 구현을 인터페이스의 일부로 만든다.
      • 현재 접근자가 4개의 인스턴스의 존재 여부를 외부에 노출하고 있다.
      • 현재 접근자가 4개의 인스턴스 변수가 int타입이라는 사실을 외부에 노출시키고 있다.
      • 결과적으로, 내부 구현의 변경접근자와 수정자의 사용 부분 모두의 변경을 초래한다.

⭕️ Rectangle.java; 응집도 높은 내부구현 예시

class Rectangle {
	public void enlarge(int multiple) {
    	right += multiple;
        bottom += multiple;
    }
}
  • Rectangle을 변경하는 주체를 외부에서 내부 객체로 이동시켰다.
    • 자신의 크기를 스스로 증가시키도록 책임을 이동시킨 것이다.
      • 즉, 객체가 자기 스스로를 책임진다.

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

  • 객체; 상태 + 행동
    • 객체 스스로 자신의 상태를 처리할 수 있도록 하기 위함.
    • 객체는 단순한 데이터 제공자가 아니다.
      • 객체가 협력에 참여하며 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.
  • 객체 설계시 고민해야 할 주요 논점
    • 이 객체가 어떤 데이터를 포함해야 하는가?
      • 하위질문1. 이 객체가 어떤 데이터를 포함해야 하는가?
        • 객체 내부 상태를 저장하는 방식
      • 하위질문2. 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?
        • 저장된 상태에 대해 호출할 수 있는 오퍼레이션

💻 코드 리팩토링

DiscountCondition.java

  • 포함해야 하는 데이터 목록
public class DiscountCondition {
    private DiscountConditionType type;
    private int sequence;
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;
}
  • 데이터에 대해 수행할 수 있는 오퍼레이션
    • 할인 조건을 판단.
      • 할인조건; 순번, 기간
public class DiscountCondition {
    public DiscountConditionType getType() {
        return type;
    }

    public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime localTime) {
        if (type != DiscountConditionType.PERIOD) {
            throw new IllegalArgumentException();
        }

        return this.dayOfWeek.equals(dayOfWeek) &&
                this.dayOfWeek.compareTo(time) <= 0 &&
                this.dayOfWeek.compareTo(time) >= 0;
    }

    public boolean isDiscountable(int sequence) {
        if (type != DiscountConditionType.SEQUENCE) {
            throw new IllegalArgumentException();
        }

        return this.sequence == sequence;
    }
}

Movie.java

  • 포함해야 하는 데이터 목록
public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;
    
    private MovieType movieType;
    private Money money;
    private double discountPercent;
}
  • 데이터에 대해 수행할 수 있는 오퍼레이션
    • 영화 요금을 계산
    • 할인 여부를 판단
      • 금액, 비율, 미적용
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.PERCNET_DISCOUNT) {
            throw new IllegalArgumentException();
        }

        return fee.minus(fee.times(discountAmount));
    }

    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.java

  • 데이터에 대해 수행할 수 있는 오퍼레이션
    • Movie의 할인 여부를 판단하고, 적절한 메서드를 호출해 요금을 계산한다.
package screen;

import java.time.LocalDateTime;

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

    private 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:
                return movie.calculateNoneDiscountedFee().times(audienceCount);
        }
        return movie.calculateNoneDiscountedFee(this).times(audienceCount);
    }


}

ReservationAgency.java

  • ReservationAgency에 몰려 있던 의존성이 개선되었다.
    • 이는 수정된 설계가 내부 구현을 더 면밀하게 캡슐화하고 있기 때문이다.
  • 두 번째 설계는 데이터를 처리하는데 필요한 메서드를 데이터를 가지고 있는 객체 스스로 구현하고 있다.
    • 따라서 이 객체들은 스스로를 책임진다고 말할 수 있다.
public class ReservationAgency {
	public Reservation reserve(Screening screening, Customer customer, int audience) {
    	Money fee = screening.calculate(audienceCount);
        return new Reservation(customer, screening, fee, audienceCount);
    }
}

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

  • 수정된 설계 역시 데이터 중심 설계 방식에 속한다.
    • 캡슐화 측면에서 크게 향상되었지만, 만족할 수준은 아니다.
    • 캡슐화의 부족으로, 첫 번째 설계에서 발생했던 문제가 여전히 발생하고 있다.

캡슐화 위반

DiscountCondition.java

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

    public boolean isDiscountable(int sequence) { ... }

}
  • 메서드 시그니처가 외부에 내부 구현을 노출하고 있다.
    • public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime localTime)
      • 내부에 DayOfWeek 타입, LocalTime 타입의 변수가 있음을 알 수 있다.
    • public boolean isDiscountable(int sequence) { ... }
      • 내부에 int 타입의 변수가 있음을 알 수 있다.
    • public DiscountConditionType getType()
      • 내부에 DiscountConditionType 타입이 있음을 노출하고 있다.
  • 따라서, 내부 구현이 변경되는 경우,
    1. isDiscountable 메서드들의 파라미터가 변경되어야 한다.
    2. 해당 메서드를 사용하는 클라이언트들의 코드도 변경되어야 한다.
  • 파급효과(ripple effect); 내부 구현의 변경이 외부로 퍼져나가는 현상
    • 캡슐화가 부족하다는 증거

Movie.java

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

    public MovieType getMovieType() {...}
    public Money calculateAmountDiscountedFee() {...}
    public Money calculatePercentDiscountedFee() {...}
    public Money calculateNoneDiscountedFee() {...}

}
  • 메서드가 할인 종류의 정책을 외부에 노출하고 있다.
    • calculateAmountDiscountedFee, calculatePercentDiscountedFee, calculateNoneDiscountedFee 메서드
      • 할인 정책이 구체적으로 어떤 종류가 있는지를 외부에 공개하고 있다.
  • 새로운 할인 정책이 추가되거나 제거되면, 이 메서드들에 의존하는 모든 클라이언트가 영향을 받게 된다.
    • Movie는 세부 할인 정책을 성공적으로 캡슐화하지 못했다.

📌 캡슐화의 진정한 의미

  • 캡슐화; 변경될 수 있는 어떤 것이라도 감추는 것
    • cf. 캡슐화는 단순히 객체 내부의 데이터를 외부로부터 감추는 것 이상의 의미를 가진다.
  • 종류를 불문하고, 내부 구현의 변경으로 인해 외부가 영향을 받는다면, 캡슐화에 실패한 것이다.

높은 결합도

Movie.java

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이 과하게 내부 구현을 외부에 노출했기 때문에, Movie와 강하게 결합하고 있다.
    • DiscountCondition의 구현 변경 시 Movie에 미치는 영향
1. `DiscountCondition`의 기간할인조건 명칭이 `PERIOD`에서 다른 값으로 변경된다면 `Movie`를 변경해야 한다.
2. `DiscountCondition`의 종류가 추가되거나 삭제된다면 `Movie`의 `if ~ else` 구문을 수정해야 한다.
3. 각 `DiscountCondition`의 맍고 여부를 판단하는데 필요한 정보가 변경된다면, 
	`Movie`의 `isDiscountable` 메서드로 전달된 파라미터를 변경해야 한다. 
	이로인해 `Movie`의 `isDiscountable` 메서드의 시그니처가 변경될 것이고, 
    결과적으로 이 메서드에 의존하는 `Screening`에 대한 변경을 초래한다.
  • 결합도가 높은 이유; 캡슐화 원칙을 지키지 않음.
    • 유연한 설계를 위해서는 캡슐화를 설계의 첫 번째 목표로 삼아야 한다.

낮은 응집도

Screening.java

public class Screening {
    private 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:
                return movie.calculateNoneDiscountedFee().times(audienceCount);
        }
        return movie.calculateNoneDiscountedFee(this).times(audienceCount);
    }
}
  • DiscountCondition의 응집력이 약해, 내부 구현의 변화로 코드 여러 곳을 동시에 변경해야 한다.
1. `Movie`의 `isDiscountable`로 전달하는 파라미터의 종류가 변경되어야 한다.
2. `Screening`에서 `Movie`의 `isDiscountable`를 호출하는 부분이 변경되어야 한다.
  • 응집력이 낮은 이유; 캡슐화 원칙을 지키지 않음.

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

  • 데이터 중심 설계가 변경에 취약한 이유
    • 데이터 중심의 설계는 본질적으로 너무 이른 시기에 데이터에 대해 결정하도록 강요한다.
    • 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.

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

  • 데이터 중심 설계는, 가장 우선적으로 객체가 가져야 하는 데이터가 무엇인지 묻는다.
    • 데이터는 구현의 일부로, 설계 시작부터 데이터에 관해 결정하도록 강요하므로, 너무 이른 시기에 내부 구현에 초점을 맞추게 한다.
  • 데이터 중심 설계; 데이터와 기능을 분리하는 절차적 프로그래밍 방식을 유발
    • 데이터 중심 설계의 객체; 단순한 데이터의 집합체
      • 과도한 접근자와 수정자의 추가를 부름
      • 접근자와 수정자는 public 속성과 큰 차이가 없으므로, 객체의 캡슐화가 무너지기 쉽다.
  • 데이터에 초점을 맞춘 설계
    • 설령 데이터와 처리 작업이 하나의 객체 안에 있더라도, 만족스러운 캡슐화를 얻기 힘들다.
    • 데이터에 관한 지식이 객체의 인터페이스에 고스란히 드러나기 때문

📌 결론

  • 데이터 중심 설계; 너무 이른시기에 데이터에 대해 고민하기 때문에, 캡슐화에 실패하게 된다.
    • 객체 내부 구현이 인터페이스를 오염시키고
    • 객체의 응집도와 결합도에 나쁜 영향을 미쳐
      • 변경에 취약한 코드를 만든다.

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

  • 올바른 객체지향 설계
    • 무게중심이 외부에 있어야 함.
      • 객체 내부의 상태와 관리는 부가적인 문제.
  • 데이터 중심 설계의 초점; 외부가 아닌 내부
    • 실행 문맥에 대한 고민 없이, 객체가 관리할 데이터를 먼저 선정
      • 내부(구현)를 먼저 결정하고 외부(타 객체와의 협력)를 고민한다.
      • 이로 인해 협력하는 모든 객체들이, 내부의 변경에 영향을 받게 된다.

Reference

  • 오브젝트 | 조영호
profile
Good Luck!

0개의 댓글