개발자는 기록이 답이다

오브젝트 4장 - 설계 품질과 트레이드오프(1) 본문

기술 서적/OOP

오브젝트 4장 - 설계 품질과 트레이드오프(1)

slow-walker 2023. 12. 2. 15:03

 

객체지향 설계의 핵심은 역할,책임,협력이다.

협력은 애플리케이션의 기능을 구현하기 위해 메시지를 주고받는 객체들 사이의 상호작용이고,

책임은 객체가 다른 객체와 협력하기 위해 수행하는 행동이고, 역할은 대체 가능한 책임의 집합이다.

 

책임 주도 설계라는 이름에서 알 수 있는 것처럼 가장 중요한 것은 '책임'이다.

객체들이 수행할 책임이 적절하게 할당되지 못한 상황에서는 원활한 협력도 기대할 수 없을 것이다.

역할도 책임의 집합이기 때문에 책임이 적절하지 못하면 역할 역시 협력과 조화를 이루지 못한다.

결국 책임이 객체 지향 애플리케이션의 전체 품질을 결정한다.

 

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

 

책임을 할당하는 작업은 응집도와 결합도 같은 설계 품질과 깊이 연관돼 있다.

설계는 변경을 위해 존재하고 변경에는 어떤 식으로든 비용이 발생한다.

훌륭한 설계란 합리적인 비용 안에서 변경을 수용할 수 있는 구조를 만드는 것이다.

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

 

결합도와 응집도를 합리적으로 유지할 수 있는 원칙이 있다.

객체의 상태가 아닌 행동에 초첨을 맞추는것이다.

객체를 단순 데이터 집합으로 바라보는 시각은 객체의 내부 구현을 퍼블릭 인터페이스에 노출시켜 설계가 변경에 취약해지기 때문이다.

책임은 객체의 상태에서 행동으로, 나아가 객체와 객체 사이의 상호작용으로 설계 중심을 이동시키고, 결합도가 낮고 응집도가 높으며 구현을 효과적으로 캡슐화하는 객체들을 창조할 수 있는 기반을 제공한다.

 

이번 장은 상태 중심(데이터 중심)의 설계를 살펴보면서 객체 지향 설계가 왜 훌륭한 설계인지, 첵임 할당 원칙에 대해 쉽게 이해해보자

 

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

객체지향 설계에서는 두 가지 방법을 이용해 시스템을 객체로 분할할 수 있다.

  1. 상태를 분할의 중심축으로 삼는 방법
  2. 책임을 분할의 중심축으로 삼는 방법

일반적으로 객체의 상태는 객체가 저장해야 하는 데이터의 집합을 의미하기 때문에 '상태'와 '데이터'를 동일한 의미로 사용하겠다.

 

데이터 중심의 관점

  • 객체의 상태에 초점을 맞춘다
  • 객체를 독립된 데이터 덩어리로 바라본다
  • 객체는 자신이 포함하고 있는 데이터를 조작하는데 필요한 오퍼레이션을 정의한다.

 

책임 중심의 관점

  • 객체의 행동에 초점을 맞춘다.
  • 객체를 협력하는 공동체의 일원으로 바라본다
  • 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관한다.

시스템을 분할하기 위해 훌륭햔 객체지향 설계는 데이터보다 책임에 초점을 맞춰야 한다. 이유는 변경과 관련된다.

 

객체의 상태 구현에 속한다. 구현은 불안정하기 때문에 변하기 쉽다. 

그러므로 상태를 객체 분할의 중심축으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화의 원칙이 무너진다.

결과적으로 상태 변경은 인터페이스의 변경을 초래하며 이 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼진다.

 

그에 비해 객체의 책임 인터페이스에 속한다.

객체는 책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하는 데 필요한 상태를 캡슐화함으로써 구현 변경에 대한 파장이 외부로 퍼져나가는것을 방지한다.

 

 

데이터를 준비하자

 

책임 중심의 설계가 '책임이란 무엇인가'를 묻는 것으로 시작한다면,

데이터 중심의 설계는 객체가 내부에 저장해야 하는 '데이터가 무엇인가'를 묻는 것으로 시작한다.

 

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

 

 

데이터 중심으로 만든 Movie클래스가 기존 설계와의 차이점은 할인 조건 목록(discountConditions)이 인스턴스 변수로 Movie안에 직접 포함돼 있다는 것이다. 또한 할인 정책을 DiscountPolicy라는 별도의 클래스로 분리했던 이전 예제와 달리 금액 할인 정책에 사용된 할인 금액과 비율 할인 정책에 사용되는 할인 비율을 Movie안에서 직접 정의하고 있다.

 

할인 정책은 영화별로 오직 하나만 지정할 수 있기 때문에 한 시점에 할인 금액과 할인 비율 중 하나의 값만 사용될 수 있다. 할인 정책의 종류를 결정하기 위해 열거형 타입인 movieType의 인스턴스를 활용할 수 있다.

public enum MovieType {
     /** 
      * 가격 할인 
      */
    AMOUNT_DISCOUNT,
    
    /** 
     * 비율 할인 
     */
    PERCENT_DISCOUNT,
    
    /** 
     * 할인 사용 안함 
     */
    NONE_DISCOUNT
}

 

객체가 포함해야 할 데이터는 무엇인가?

객체의 책임을 결정하기 전에 이런 질문의 반복에 휩쓸려 있다면 데이터 중심의 설계에 매몰돼 있을 확률이 높다.

특히 객체의 종류를 저장하는 인스턴스 변수(MovieType)와 인스턴스 종류에 따라 배타적으로 사용될 인스턴스변수(discountAmount, discountPercent)를 하나의 클래스 안에 함께 포함시키는 방식은 데이터 중심의 설계 안에서 흔히 볼 수 있는 패턴이다.

 

객체지향의 가장 중요한 원칙은 캡슐화이므로 내부 데이터가 바깥으로 빠져나가 외부의 다른 객체들을 오염시키는 것을 막아야 한다

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

 

package chapter4.movie;

public class Movie {
	...
    public MovieType getMovieType() {
        return movieType;
    }

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

    public Money getFee() {
        return fee;
    }

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

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

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

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

 

이제 할인 조건을 구현하면, 순번 조건과 기간 조건 2가지 종류가 있기 때문에 '할인 조건을 구현하는데 필요한 데이터는 무엇인가?'라는 질문에 답하기 위해 현재 할인 조건의 종류를 저장할 데이터가 필요하다.

public enum DiscountConditionType {
    /** 
     * 상영 순번 조건 
     */
    SEQUENCE,
    
    /** 
     * 상영 시간 조건 
     */
    PERIOD
}

 

package chapter4.movie;

import java.time.DayOfWeek;
import java.time.LocalTime;

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

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

    public int getSequence() {
        return sequence;
    }
}

 

이어서 Screening 클래스도 지금까지 했던 것과 동일하게 어떤 데이터를 포함해야 하는지 결정하고, 데이터르 ㄹ캡슐화하기 위해 메서드를 추가하자.

 

package chapter4.movie;

import java.time.LocalDateTime;

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 LocalDateTime getWhenScreened() {
        return whenScreened;
    }

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

    public int getSequence() {
        return sequence;
    }

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

 

영화 예매 시스템의 목적은 영화를 예매하는 것이다. Reservation클래스를 추가하자

package chapter4.movie;

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은 고객의 정보를 보관하는 간단한 클래스이다.

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

 

영화를 예매하자

 

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

 

reserve메서드

  • DiscountCondition에 대한 루프를 돌면서 할인 가능 여부 확인하는 for문
  • discountable 변수의 값을 체크하고 적절한 할인 정책에 따라 예매 요금을 계산하는 if문
package chapter4.movie;

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

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

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

 

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

 

위의 그림으로 표현된 데이터 중심 설계 방법과 책임 중심 설계 방법을 비교하기 전에 비교하기 위한 기준을 알아본다.

 

2. 설계 트레이드오프

 

캡슐화 : 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법

 

외부에서 알 필요가 없는 부분은 감춤으로써 대상을 단순화하는 추상화의 한 종류다. 

상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다.

여기서 구현이란 나중에 변경될 가능성이 높은 어떤 것을 가리킨다.

 

객체 지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다. 객체를 사용하면 변경 가능성이 높은 부분(구현)은 내부에 숨기고 외부에는 상대적으로 안정적인 부분(인터페이스)만 공개함으로써 변경의 여파를 통제할 수 있다.

 

변경될 가능성이 높은 부분을 구현이라고 부르고 상대적으로 안정적인 부분은 인터페이스라고 부른다.

변경 정도에 따라 구현과 인터페이스를 분리하고, 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.

 

복잡성을 다루기 위한 가장 효과적인 도구는 추상화이다.
객체지향 프로그래밍에서 복잡성을 취급하는 주요한 추상화방법은 캡슐화이다.

유지보수성이 목표다. 
요지보수성이란, 두려움과 주저함없이 코드를 변경할 수 있는 능력을 말한다.
캡슐화를 통해 시스템의 한 부분을 다른부분으로부터 감춤으로써
뜻밖의 피해가 발생할 수 있는 가능성을 사전에 방지할 수 있다.

 

응집도와 결합도

 

🚩 응집도란?

  • 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.
  • 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다.
  • 모듈 내의 요소들이 서로 다른 목적을 추구한다면 그 모듈은 낮은 응집도를 가진다.
  • 객체지향의 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.

🚩 결합도란?

  • 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다.
  • 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가진다.
  • 어떤 모듈이 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면 두 모듈은 낮은 결합도를 가진다.
  • 객체지향의 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준이 관계만을 유지하고 있는지를 나타낸다.

문제는 대부분의 사람들이 이런 애매한 설명만으로 응집도와 결합도의 의미를 명확하게 이해하기 어렵다는 것이다.

  • 모듈 내의 요소가 얼마나 강하게 연관돼 있어야 응집도가 높을까?
  • 모듈 사이에 어느 정도의 의존성만 남겨야 결합도가 낮을까?

응집도와 결합도의 의미를 이해하기 위한 첫걸음은 두 개념 모두 설계와 관련이 있다는 것이다.

좋은 설계란 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계이다.

높은 응집도와 낮은 결합도를 가진 설계를 추구해야 하는 이유는 설계를 변경하기 쉽게 만들기 때문이다.

 

 

💡 변경의 관점에서 응집도란 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다.

즉, 하나의 변경을 수용하기 위해 모듈 전체가 함께 변경된다면 응집도가 높은것이고, 모듈의 일부만 변경된다면 응집도가 낮은것이다.

또한 하나의 변경에 대해 하나의 모듈만 변경된다면 응집도가 높지만, 다수의 모듈이 함께 변경된다면 응집도가 낮은 것이다.

 

음영으로 칠해진 부분은 변경이 발생했을 때 수정되는 영역을 표현한 것이다.

응집도가 높은 설계에서는 하나의 요구사항 변경을 반영하기 위해 오직 하나의 모듈만 수정하면 된다.

반면 응집도가 낮은 설계에서는 하나의 원인에 의해 변경해야 하는 부분이 다수의 모듈에 분산돼 있기 때문에 여러 모듈을 동시에 수정해야 한다.

 

응집도가 높을 수록 변경의 대상과 범위가 명확해지기 때문에 코드를 변경하기 쉬워진다. 변경으로 인해 수정되는 부분을 파악하기 위해 헤매고 다니거나 여러 모듈을 동시에 수정할 필요가 없으며 변경을 반영하기 위해 오직 하나의 모듈만 수정하면 된다.

 

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

즉, 하나의 모듈을 수정할때 얼마나 많은 모듈을 함께 수정해야 하는지를 나타낸다. 따라서 결합도가 높으면 높을 수록 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경하기 어려워 진다.

 

내부 구현을 변경했을때 이것이 다른 모듈에 영향을 미치는 경우에는 두 모듈 사이의 결합도가 높다고 표현한다.

반면 퍼블릭 인터페이스를 수정했을 때만 다른 모듈에 영향을 미치는 경우에는 결합도가 낮다고 한다.

따라서 클래스의 구현이 아닌 인터페이스에 의존하도록 코드를 작성해야 낮은 결합도를 가질 수 있는데, 이것은 "인터페이스에 대해 프로그래밍하라[GOF94]"로도 알려져 있다.

 

💡  결합도가 높아도 상관없는 경우

일반적으로 변경될 확률이 매우 적은 안정적인 모듈은 상관없다

표준 라이브러리에 포함된 모듈이나 성숙 단계에 접어든 프레임워크

e.g. 자바의 String이나 ArrayList

 

캡슐화를 지키면 모듈안의 응집도는 높아지고 모듈사이의 결합도는 낮아진다.
 응집도와 결합도를 고려하기 전에 먼저 캡슐화를 향상시켜야한다.

 

 

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

데이터 중심의 설계는 캡슐화를 위반하고, 객체의 내부 구현을 인터페이스의 이룹로 만든다.

반면, 책임 중심의 설계는 객체의 내부 구현을 안정적인 인터페이스 뒤로 캡슐화한다.

 

데이터 중심 설계가 가진 문제점

  • 캡슐화 위반
  • 높은 결합도
  • 낮은 응집도

 

캡슐화 위반

 

데이터 중심으로 설계한 Movie클래스를 보면 오직 메서드를 통해서만 객체의 내부 상태에 접근할 수 있다는 것을 알 수 있다.

fee의 값을 읽거나 수정하기 위해서는 getFee 메서드와 setFee 메서드를 사용해야 한다.

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

 

위 코드는 직접 객체의 내부에 접근할 수 없기 때문에 캡슐화의 원칙을 지키고 있는것 처럼 보인다. 정말 그럴까?

 

 

외부에서 직접 인스턴스변수에 접근할 수 없지만 캡슐화는 보장되지 못한다.

getter, setter는 fee 인스턴스 변수가 Movie 내부에 존재한다는 사실을 퍼블릭 인터페이스에 노골적으로 드러내기 때문이다.

캡슐화를 어긴 근본적인 원인은 객체가 수행할 책임이 아닌 내부에 저장할 데이터에 초점을 맞췄기 때문이다.

 

객체에게 중요한건 → 책임

구현을 캡슐화할 수 있는 적절한 책임은 협력이라는 문맥을 고려할때만 얻을 수 있다.

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

객체가 사용될 문맥을 추측할 수 밖에 없는 경우 어떤 상황에서도 해당 객체가 사용될 수 있게 최대한 많은 접근자 메서드를 추가하게 되는 문제가 있다. 앨런 홀럽은 이처럼 접근자와 수정자에 과도하게 의존하는 설계 방식추측에 의한 설계 전략(desgin-by-guessing strartegy)라고 부른다.


높은 결합도

 

데이터 중심의 설계는 접근자와 수정자를 통해 내부 구현을 인터페이스의 일부로 만들기 때문에 캡슐화를 위반한다.

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

단지 객체의 내부 구현을 변경했음에도 이 인터페이스에 의존하는 모든 클라이언트들도 함께 변경해야 한다.

public class ReservationAgency {
    public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
        ...
        Movie fee;
        if (discountable) {
        	...
        	fee = movie.getFee().minus(discountedAmount).times(audienceCount);
        } else {
        	fee = movie.getFee();
        }
    ...
 }
}

 

fee의 타입을 변경하면 getFee 메서드의 반환 타입도 함께 수정해야하며 getFee 메서드를 호출하는 ReservationAgency의 구현도 변경된 타입에 맞게 수정되어야한다.

 

fee의 타입 변경으로 인해 협력하는 클래스가 변경되기 때문에 getFee메서드는 fee를 정상적으로 캡슐화하지 못한다.

사실 getFee메서드를 사용하는 것은 인스턴스 변수 fee의 가시성을 private에서 public으로 변경하는 것과 거의 동일하다.

이처럼 데이터 중심 설계는 캡슐화를 약화시켜 클라이언트가 객체의 구현에 강하게 결합된다.

 

또 다른 단점으로, 여러 데이터 객체들을 사용하는 제어 로직이 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것이다. 이 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수 밖에 없다.

 

ReservationAgency클래스가 대부분의 제어 로직을 가지고 있고, 모든 데이터 객체에 의존하기 때문에 모든 의존성이 모이는 결합도의 집결지가 된다. 시스템안의 어떤 변경도 ReservationAgency의 변경을 유발한다.

 

 

낮은 응집도

 

서로 다른 이유로 변경되는 코드가 하나의 모듈안에 공존할 때 모듈의 응집도가 낮다고 말한다.

따라서 모듈의 응집도를 살펴보기 위해서는 코드를 수정하는 이유가 무엇인지 살펴봐야 한다.

 

아래와 같은 수정상황이 발생할 경우 ReservationAgency의 코드를 수정해야 한다.

  • 할인 정책이 추가될 경우
  • 할인 정책별로 할인 요금을 계산하는 방법이 변경될 경우
  • 할인 조건이 추가되는 경우
  • 할인 조건별로 할인 여부를 판단하는 방법이 변경될 경우
  • 예매 요금을 계산하는 방법이 변경될 경우

낮은 응집도는 2가지 측면에서 설계에 문제를 일으킨다

 

  • 변경의 이유가 서로 다른 코드들을 하나의 모듈 안에 뭉쳐놓았기 때문에 변경과 아무 상관이 없는 코드들이 함께 영향 받게 된다.
    • ReservationAgency안에 할인 정책을 선택하는 코드와 할인 조건을 판단하는 코드가 함께 존재하기 때문에 새로운 할인 정책을 추가하면 할인 조건에도영향을 미칠 수 있다.
  • 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다. 응집도가 낮을 경우 다른 모듈에 위치해야할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다.
    • 새로운 할인 정책을 추가할 경우, MovieType에 열거형 값을 추가하고, ReservationAgency의 reserve메서드의 switch 구문에 새로운 case절을 추가해야하고, 새로운 할인 정책에 따른 할인요금을 계산하기 위해 Movie에 데이터도 추가해야 한다.

이처럼 어떤 요구사항 변경을 수용하기 위해 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 증거이다.

 

🚩 단일 책임의 원칙

로버트 마틴은 모듈의 응집도가 변경과 연관성이 있다는 사실을 강조하기 위해 단일 책임 원칙을 제시했다. 클래스는 단 한 가지의 변경 이유만 가져야 한다는 것이다. '책임'이라는 말이 '변경의 이유'라는 의미로 사용되기 때문에, 객체의 역할, 책임, 협력에서 얘기하는 책임과는 다르다.

 

Github 코드 링크

 

GitHub - codesejin/OOP-Object: [오브젝트] - 코드로 이해하는 객체지향 설계

[오브젝트] - 코드로 이해하는 객체지향 설계. Contribute to codesejin/OOP-Object development by creating an account on GitHub.

github.com