본문 바로가기

BackEnd

오브젝트 4장 설계 품질과 트레이드오프 정리

반응형

오브젝트 - YES24

 

오브젝트 - YES24

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음은 객체를

www.yes24.com

가장 중요한 것은 책임이다.

  • 훌륭한 설계란 합리적인 비용 안에서 변경을 수용할 수 있는 구조를 만드는 것이다.
  • 훌륭한 설계의 결과로 탄생하는 객체는 결합도가 낮고 응집도가 높다.
  • 훌륭한 설계를 위해선 객체의 행동에 초점을 맞춰야 한다. (내부 구현(필드 및 필드 수정 피함)
    • 객체를 단순한 데이터 집합으로 바라보는 시각은 객체의 내부 구현을 퍼블릭 인터페이스에 노출시키는 결과를 낳는다.
    • 즉 내부 변수를 다른 메서드의 인자로 노출시키거나, 다른 객체의 지역 변수로 노출시킨다.
      • 무지성 getter, setter는 지양해야 한다.
  • 이는 의존성을 만든다.

책임을 중심으로 시스템을 후보(역할,객체,클래스)로 분할한다.

캡슐화 : 유연한 설계의 첫번째 목표!

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

  • 데이터 중심의 관점에서 객체는 자신이 포함하고 있는 데이터를 조작하는데 필요한 오퍼레이션을 정의한다. (상태 중심)
  • 책임 중심의 관점에서 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관한다. (행동 중심)
  • 객체의 상태는 구현에 속한다. 구현은 불안정하기 때문에 변하기 쉽다.
  • 객체의 책임은 인터페이스에 속한다.
    • 객체는 책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하는데 필요한 상태를 캡슐화함으로써 구현 변경에 대한 파장이 외부로 퍼져나가는 것을 막는다.
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 Movie(String title, Duration runningTime, Money fee, double discountPercent, DiscountCondition... discountConditions) {
        this(MovieType.PERCENT_DISCOUNT, title, runningTime, fee, Money.ZERO, discountPercent, discountConditions);
    }

    public Movie(String title, Duration runningTime, Money fee, Money discountAmount, DiscountCondition... discountConditions) {
        this(MovieType.AMOUNT_DISCOUNT, title, runningTime, fee, discountAmount, 0, discountConditions);
    }

    public Movie(String title, Duration runningTime, Money fee) {
        this(MovieType.NONE_DISCOUNT, title, runningTime, fee, Money.ZERO, 0);
    }

    private Movie(MovieType movieType, String title, Duration runningTime, Money fee, Money discountAmount, double discountPercent,
                  DiscountCondition... discountConditions) {
        this.movieType = movieType;
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountAmount = discountAmount;
        this.discountPercent = discountPercent;
        this.discountConditions = Arrays.asList(discountConditions);
    }

    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 Collections.unmodifiableList(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;
    }
}

public enum MovieType {
    AMOUNT_DISCOUNT,    // 금액 할인 정책
    PERCENT_DISCOUNT,   // 비율 할인 정책
    NONE_DISCOUNT       // 미적용
}

객체 안에 객체의 종류를 저장하는 인스턴스 변수 movieType이나

배타적으로 사용되는 인스턴스 변수(discountAmont, discountPercent)를 하나의 클래스 안에 포함시키는 방식은

데이터 중심 설계 안에서 흔히 보인다.

Java Enum 활용기 | 우아한형제들 기술블로그 (woowahan.com)

 

Java Enum 활용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E

techblog.woowahan.com

거의 순수하게 데이터와 accessor, mutator만 존재하는 클래스들

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().times(audienceCount);
        }

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

Discountcondition에 대해 루프를 돌면서 할인 가능 여부를 확인한다.

discountable 변수의 값을 체크하고 적절한 할인 정책에 따라 예매 요금을 계산한다.

 

설계 트레이드오프

캡슐화

상태와 행동을 하나의 객체 안에 모으는 이유는 내부 구현을 감추기 위해서다 (필드가 안드러나게 하기 위함)

오직 인터페이스만 밖으로 드러난다. 변경될수 있는 어떤 것이라도 캡슐화해야 한다.

캡슐화가 가장 중요한 이유

응집도와 결합도

구조적 설계 방법이 주도하던 시대의 기준이나 객체지향의 시대에도 여전히 유효하다.

응집도 

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

결합도

  • 객체지향의 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다.
    • 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다.
    • 즉 독립적 변경이 유연함의 정도다.

 

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

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

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

 

음영으로 칠해진 부분이 변경 발생 시 수정되는 모듈의 영역이다

응집도가 높으면 오직 하나만 수정하면 된다. 여러 모듈을 수정해야 하면 응집도나 낮다.

음영으로 칠해진 부분이 변경 발생 시 수정되는 모듈이다

결합도가 낮으면 하나의 모듈만 수정하면 된다. 한 모듈의 내부 구현 변경이 다른 모듈에 미치면 결합도가 높다고 표현한다.

표준 라이브러리, 프레임웍에 의존하는건 상관없다. (변경 확률 매우 적음)

직접 작성한 코드는 항상 불안정하며 변경될 가능성이 크다.

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

내부 구현(속성)을 인터페이스의 일부로 만든다.

  • 파라미터
  • 지역 변수

캡슐화 위반

게터, 세터를 통해 인스턴스 변수를 노골적으로 드러냄

높은 결합도

다른 객체의 인스턴스 변수를 지역 변수로 저장

다른 객체의 구현이 바뀌면 이것도 바꿔줘야 함...

다른 데이터 객체들을 사용하는 제어 로직이 특정 객체 안에 강하게 결합됨

(ex) getter + switch)

사용 데이터 객체들이 바뀌면 제어 로직을 바꿔야할 가능성이 큼

ReservationAgency가 사용하는 데이터 클래스의 내부 구현이 바뀌면...

낮은 응집도

서로 다른 이유로 변경되는 코드가 모듈 안에 공존하면 응집도가 낮다.

코드를 수정하는 이유를 찾아보자

ReservationAgency가 변경되는 이유

  • 할인정책 추가
  • 할인 정책별 계산 로직 변경
  • 할인 조건 추가
  • 예매 요금 계산 방식 변경
  • 할인 조건별 할인 여부 판단 방법 변경

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

  1. 변경의 원인이 다른 코드들이 하나의 모듈 안에 뭉쳐있어 변경과 아무 상관 없는 코드들이 영향을 받는다.
    1. 할인 정책을 추가하는 코드가 할인 조건을 판단하는 코드에 영향을 미칠 수 있다.
  2. 하나의 요구사항 변경을 위해 여러 모듈을 동시에 수정해야 한다.
    1. 할인 정책이 추가되면 3개의 모듈이 동시에 변경됨...
      1. MoneyType enum 열거형 값 추가
      2. ReservationAgency Switch에 case 추가
      3. Movie에 새로운 할인 정책 위해 필요한 데이터 추가

자율적인 객체를 향해

  • 캡슐화를 지켜라
  • 게터 세터 등으로 자신의 필드 타입을 노출시키는 짓을 막아라
  • 객체는 자신이 어떤 데이터를 가지고 있는지를 내부에 캡슐화하고 외부에 공개해서는 안된다.

예제 : 사각형의 너비의 높이를 증가시키는 코드

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

}

 

해당 코드의 문제점 :

  • 중복 : 다른 곳에서 비슷한 코드를 잔뜩 구현할수 있음.
    • (날짜구하는 코드를 여러곳에서 구현하는걸 실제로 프로덕션에서 본 적도 있음...)
  • 변경에 취약 : rectangle 가로세로 => 4개 좌표로 내부 구현 바뀌면 위 코드는 전면적으로 바뀌어야 함.

해결 방법 : 캡슐화

class Rectangle{
   public void enlarge(int multiple){
   		right *= multiple;
        bottom *= multiple; 
   }
}

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

  • 객체 내부 데이터보다 객체가 협력하며 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.
  • 아래 두 질문을 조합하면 새로운 데이터 타입을 만들 수 있다.
    • 이 객체가 어떤 데이터를 포함해야 하는가
    • 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가

각 데이터의 수정에 대한 책임을 각 객체에 할당하였다.

하지만 여전히 부족하다.

아직도 데이터 중심의 설계이다.

  • DiscountCondition은 내부 인스턴스 변수의 타입을 인터페이스로 외부에 노출함
    • sequence, dayOfWeek, sequence
  • Movie는 할인 정책의 종류(case, if)를 인터페이스에 노출시키고 있음
    • screnning에서 movie의 getType이용해서 switch... 
    • 새로운 할인 정책이 추가되거나 제거되면 screnning 변경해야 함.
캡슐화는 속성 타입이건, switch문의 case이건 관계없이 구현과 관련된 것은 모두 감추는 것이다.

높은 결합도 : 하나 바꾸면 여러개를 바꿔아햠

  • 사용 객체 내부의 type을 꺼내서 switch하는 경우, 내부 type의 케이스가 변경되면 사용처를 바꿔줘야 함
    • DiscountCondition 변경 => Movie 변경
  • DiscountCondition의 만족 여부 판단 정보가 변경되면 isDiscountable의 메서드 파라미터도 변경햐여 함 (whenScreened,sequence 둘 다 쓰고 있음)
    • 이 정보는 Screening에서 Movie를 통해 전달됨
  • DiscountCondition 변경 => Movie 변경 => Screening 변경

하나의 변경을 수용하기 위해 여러 모듈을 동시에 변경해야 함

낮은 응집도

위랑 같은 이야기

DiscountCondition이 할인 여부 판단에 필요한 정보가 변경되면

Movie의 isDiscountable 파라미터 종류 변경

Screening의 Movie 호출 변경

하나의 변경을 수용하기 위해 여러 곳(내부의 여러 메소드, 다른 모듈)을 동시에 변경해야 함

 

다시 말하지만 내부 구현(DiscountCondition의 필드)이 노출되었기 때문임

데이터 중심 설계의 문제점

너무 이른 시기에 데이터에 관해 결정하도록 강요한다.

협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.

 

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

첫번째 질문이 이 객체가 포함하는 데이터가 무엇인가였음.

데이터는 구현의 일부다

너무 이른 시간에 내부 구현에 초점을 맞추게 한다.

내부구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 미친다.

 

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

올바른 객체지항 설계의 무게중심은 항상 객체의 내부가 아니라 외부에 맞춰져 있어야 한다.

상태 관리는 부가적인 문제다.

데이터 중심 설계는 초점이 외부가 아니라 내부로 향한다.

 

여담 :

내가 생각해도 읽기도 싫은 코드라 이번장은 대부분 클래스 다이어그램만 보고 넘어갔다.

특히 코드성 데이터 같은 경우 대부분 이런 데이터 중심 사고가 드러나는데,

아래 글은 코드성 데이터를 말 그대로 우아하게 객체지향적으로 처리하고 있다.

Java Enum 활용기 | 우아한형제들 기술블로그 (woowahan.com)

 

Java Enum 활용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한 형제들에서 결제/정산 시스템을 개발하고 있는 이동욱입니다. 이번 사내 블로그 포스팅 주제로 저는 Java Enum 활용 경험을 선택하였습니다. 이전에 개인 블로그에 E

techblog.woowahan.com

인프런은 과제 받아놓고 사내 전배가 확정되어서 채용 전형을 포기했었는데, 이런 멋진 글을 보면 약간 후회

 

반응형