본문 바로가기

IT Book Summary/Object: 객체지향설계

Chapter 08 의존성 관리하기

 협력은 객체가 다른 객체에 대해 알것을 강요함. 

- 협력할 객체가 존재한다는 사실

- 객체가 수신할 수 있는 메세지에 대한 사실

-> 이런 지식이 객체 사이의 의존성을 낳음.

 

객체 지향 설계의 핵심 - 협력을 위해 필요한 의존성은 유지, 변경을 방해하는 의존성은 제거

 

유연한 객체를 만들기 위해 의존성을 관리하는 방법? 

 


01 의존성 이해하기

 

변경과 의존성

 

의존성은 실행 시점과 구현 시점에 다른의미를 가짐

 

실행시점 : 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상객체가 반드시 존재해야 함.

구현시점 : 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경됨.

 

ex) 영화예매 시스템의 PeriodCondition 클래스의 의존성 개념

 

public class PeriodCondition implements DiscountCondition {
    private DayOfWeek dayofWeek;
    private LocalTime startTime;
    private LocalTime endTime;
    
    ...
    
    public boolean isSatisfiedBy(Screening screening) {
        return screenging.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
        startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
        endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
    }
}

 

 의존성은 방향성을 가지며 항상 단방향

Screening 이 변경될 때 PeriodCondition이 영향을 받게 되지만 그 역은 성립하지 않음.

 

의존성은 변경에 의한 영향의 전파 가능성을 암시

 

속성으로 포함하고 있는 DayOfWeek, LocalTime 인스턴스, 메서드의 인자로 받는 Screening 인스턴스 에 대해 의존성을 가짐.

 

DiscountConditon 에도 의존하고 있음- 인터페이스에 정의된 오퍼레이션들을 퍼블릭 인터페이스 일부로 포함시키기 위해.

 

각기 다른 방식으로 의존하고 있지만 의존성이 가지는 근본적인 특성은 동일 

-> PeriodCondition은 자신이 의존하는 대상이 변경될 때 함께 변경될 수 있다는 것.

 

의존성 전이(transitive dependency)

 

PeriodCondition 이 Screening에 의존할 경우

PeriodCondition은 Screening이 의존하는 대상에 대해서도 자동적으로 의존하게 된다는 것.

 

But 의존성이 실제도 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라짐.

 

직접의존성- 명시적으로 직접 의존.

간접의존성 - 명시적으로 드러나지않지만 의존성 전이에 의해 영향이 전파되는 경우.

 

 

런타임 의존성과 컴파일타임 의존성

 

런타임 - 어플리케이션이 실행되는 시점

 

컴파일타임 - 작성된 코드를 컴파일하는 시점을 가리키나 문맥에 따라 코드 그 자체를 가리키기도 함(동적타입 언어의 경우)

-> 컴파일이 진행되는 시점 or 코드를 작성하는 시점

 

런타임 의존성이 다루는 주제를 객체사이의 의존성

 

컴파일 타임 의존성이 다루는 주제는 클래스 사이의 의존성.

 

ex) 추상클래스인 DiscountPolicy 와 Movie 클래스가 의존하는 방식.

 

두 클래스를 모두 포괄하는  DiscountPolicy를 만들고 이 컴파일 타임 의존성을

실행 시에  PercentDiscountPolicy 인스턴스나 AmountDiscountPolicy인스턴스에 대한

런타임 의존성으로 대체해야 함.

 

실제로 협력할 객체가 어떤 것인지는 런타임에 해결하도록 하자.

 

컨텍스트 독립성

 

 

클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기 수월해짐.

 

설계가 유연해지기 위해서는 가능한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 함.

 

클래스가 실행 컨텍스트 독립적이라면 어떻게 런타임에 실행 컨텍스트에 적절한 객체들과 협력할 수 있을까?

 

 

의존성 해결하기

 

의존성 해결 - 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것

  • 객체를 생성하는 시점에 생성자를 통해 의존성 해결
  • 객체 생성 후 setter 메서드를 통해 의존성 해결
  • 메서드 실행 시 인자를 이용해 의존성 해결

Movie 생성자에 PercentDiscountPolicy 인스턴스 전달

Movie 인스턴스를 생성한 후에 set메서드를 이용해 의존성 해결

(객체를 생성한 이후에도 의존하고 있는 대상을 변경할 수 있는 가능성을 열어 놓고 싶은 경우 유용하나, 객체를 먼저 생성하고 의존대상을 설정하기에 객체의 상태가 불안정하다.)

->생성자 방식과 setter 방식의 혼용 : 가장 선호되는 방식. 상태를 안정적으로 유지하면서 유연성 향상

 

 


02 유연한 설계

 

 

 

 

의존성과 결합도

 

문제는 의존성의 존재가 아니라 의존성의 정도

 

Movie를 PercentDiscountPolicy에 직접 의존하게 만들면

다른종류의 할인 정책이 필요한 문맥에서 Movie의 재사용 가능성을 없애버리는 것이 된다.

-> 런타임 의존성으로 대체

 

바람직한 의존성은 재사용과 관련이 있다 : 컨텍스트에 독립적인 의존성

 

느슨한 결합도 (loose coupling)

 

 

지식이 결합을 낳는다

 

결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정

 

서로에 대해 알고 있는 지식의 양이 많을수록 강하게 결합된다.

-> 협력하는 대상에 대해 필요한 정보외에 감춰야 함. 추상화

 

 

추상화에 의존하라

 

불필요한 정보를 감춰 결합도를 느슨하게 유지

 

추상화와 결합도의 관점에서 의존대상 구분

  • 구체 클래스 의존성(concrete class dependency)
  • 추상 클래스 의존성(abstract class dependency)
  • 인터페이스 의존성(interface dependency)

-> 의존하는 대상이 더 추상적일수록 결합도는 더 낮아짐.

 

명시적인 의존성

 

클래스 안에 구체 클래스에 대한 모든 의존성을 제거해야 함.

 

의존성의 대상을 생성자의 인자로 전달받는 방법 vs 생성자 안에서 직접 생성하는 방법 (숨겨진 의존성)

setter메서드, 메서드 인자 사용

(명시적 의존성)

 

의존성은 명시적으로 표현되야 한다. 의존성을 감추는것을 경계하라.

 

그래야 내부 구현을 직접 변경하지 않고도 실행 컨텍스트에 적절한 의존성을 선택할 수 있다.

 

 

new는 해롭다

 

  • new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 함. new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수 밖에 없기 때문에 결합도가 높아짐.
  • new 연산자는 생성하려는 구체 클래스뿐 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 함. 클라이언트가 알아야 하는 지식의 양도 늘어나기 때문에 결합도가 높아짐

new는 불필요한 결합도를 높인다.

생성시 필요한 인자의 구성에 대한 정보와 인자로 사용되는 구체클래스에 대한 의존성까지 추가

 

-> 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것

직접 인스턴스를 생성하지 말고, 이미 클라이언트에서 생성된 인스턴스를 전달받아야 함

 

public class Movie {
    ...
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        ...
        this.discountPolicy = discountPolicy;
    }
}

Movie는 인스턴스를 생산하지 않고 전달 받는다. 전달받은 인스턴스를 사용하고 메시지를 전송하는 일만 수행함.

 

  • 사용과 생성의 책임을 분리.
  • 의존성을 생성자에 명시적으로 드러냄
  • 구체클래스가 아닌 추상클래스에 의존하게 함
  • 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮김.

 

가끔은 생성해도 무방하다

 

주로 협력하는 기본 객체를 설정하고 싶은 경우 클래스 안에서 객체의 인스턴스를 직접 생성해도 무방하다.

 

->기본객체를 생성하는 생성자를 추가하고 이 생성자에서 추상클래스 인스턴스를 인자로 받는 생성자를 체이닝 하는것

 

public class Movie {
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duratioin runningTime) {
        this(title, runningTime, new AmountDiscountPolicy(...));
    }
    
    public Movie(String title, Duratioin runningTime,Money fee, DiscountPolicy discountPolicy) {
        ...
        this.discountPolicy = discountPolicy;
    }
}

 

첫번째 생성자 내부에서 두번째 생성자를 호출함.

 

추가된 간략한 생성자를 통해 의존성을 교체할 수 있다.

 

같은 방법으로 메서드 오버로딩도 가능

 

 

public class Movie {
    public Money calculateMovieFee(Screening screening) {
        return calculateMovieFee(screening, new AmountDiscountPolicy(...));
    }
    
    public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

 

이 예는 설계가 트레이드오프 활동임을 상기시킨다.

 

 

 

표준 클래스에 대한 의존은 해롭지 않다

 

코드가 수정될 확률은 거의 없기때문에 생성하더라도 문제 없음.

그래도 가능한 추상 타입을 사용하는것이 확장성 측면에서 유리하다

다양한 타입의 객체로 대체할 수 있게 유연성을 높이기 때문.

 

public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();
    
    public void switchCondition(List<DiscountCondition> conditions) {
        this.conditions = conditions;
    }
}

 

 

컨텍스트 확장하기

 

할인 혜택을 제공하지 않는 영화의 경우와

다수의 할인 정책을 중복해서 적용하는 영화를 처리하는 경우

 

public class NoneDiscountPolicy extends DiscountPolicy {
    @Override
    protected Money getDiscountAmount(Screening screening) {
    return Money.ZERO;
    }
}


public class OverlappedDiscountPolicy extends DiscountPolicy {
    private List<DiscountPolicy> discountPolicies = new ArrayList<>();
    
    public OverlappedDiscountPolicy(DiscountPolicy ... discountPolicies) {
        this.discountPolicies = Arrays.asList(discountPolicies);
    }
    
    @Override
    protected Money getDiscountAmount(Screening screening){
        Money result = Money.ZERO;
        for(DiscountPolicy each : discountPolicies) {
            result = result.plus(each.calculateDiscountAmount(screening));
        }
        return result;
    }
}

 

 

조합가능한 행동

 

유연하고 재사용 가능한 설계는 객체들의 조합을 통해 무엇을 하는지를 표현하는 클래스들로 구성된다

 

객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을하는지를 표현하는가 것이 훌륭한 객체지향 설계

 

핵심은 의존성 관리.

 

 절차적인 코드를 작성하기보다 인스턴스 추가나 제거 또는 조합을 달리해서 구성을 변경해 시스템 작동방식을 바꿀수 있다. 이러한 객체 구성을 관리할 목적으로 작성하는 코드를 객체 네트워크의 행위에 대한 선언적인 정의하고 한다. 방법이 아니라 목적에 지중할 수 있어 시스템의 행위(객체의 조합으로 나타나는 특성)를 변경하기 더 쉽다.

 

new Movie("아바타",
    Duration.ofMinutes(120),
    Money.wons(10000),
    new AmountDiscountPolicy(Money.wons(800)),
        new SequenceCondition(1),
        new SequenceCondition(10),
        new PeriodCondition(DayOfWee.MONDAY, LocalTime.of(10,0), LocalTime.of(12,0)),
        new PeriodCondition(DayOfWee.THURSDAY, LocalTime.of(10,0), LocalTime.of(21,0))));