카테고리 없음

오브젝트 - 8. 의존성 관리하기

jimanfe 2025. 1. 4. 16:17

 

협력은 필수적이지만 과도한 협력은 설계를 변경을 방해할 수 있다. 

협력은 객체가 다른 객체의 존재와 객체가 수신할 수 있는메시지에 대해서도 알 것을 강요한다. 

협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거해야한다. 

 

1. 의존성 이해하기

index 

- 변경과 의존성

- 의존성 전이

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

- 컨텍스트 독립성

- 의존성 해결하기 

변경과 의존성

의존성은 실행시점과 구현시점에 서로 다른 의미를 가진다. 

실행시점: 의존하는 객체가 정상적으로 동작하기 위해 런타임에 의존 객체가 반드시 존재해야한다. 

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

 

어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재한다.

의존성은 단방향이다. 

 

두 요소 사이의 의존성은 의존되는 객체가 변경되었을때 의존하는 객체가 변경될 수 있음을 의미한다. 

 

의존성 전이

의존성 전이는 A -> B -> C 의 의존 관계에 대해서, A 가 C 에도 의존하게 된다는 것을 의미한다. 

다만, 함께 변경될 수 있는 가능성을 의미하는 것이며 항상 의존성이 전이되지는 않는다. 

전이의 여부는 변경의 방향과 캡슐화의 정도에 따라 달라진다. 

 

C의 구현이나 인터페이스가 변경되는 경우에 B 가 내부구현을 효과적으로 캡슐화했다면 A로까지 변경이 전파되지는 않는다. 

 

의존성의 종류

- 직접 의존성: 코드상에서 직접적으로 드러나는 의존관계 

- 간접 의존성: 코드상에서 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우 

 

의존성은 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성

 

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

런타임: 애플리케이션이 실행되는 시점

컴파일 타임: 작성된 코드를 컴파일하는 시점 or 코드 그자체(코드를 작성하는 시점)

 

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

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

 

런타임의존성과 컴파일타임의존성은 다를 수 있으며, 두 종류의 의존성을 서로 다르게  만들어야한다. 

 

 

런타임에서 Movie는 AmountDiscountPolicy 와 PercentDiscountPolicy 모두와 협력할 수 있어야한다. 

코드 작성 시점에는, 즉 컴파일 타임의존성 관점에서, Movie 클래스에서 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스로 향하는 어떤 의존성도 존재하지 않는다. 

추상클래스인 DiscountPolicy 에만 의존한다. 

DiscountPolicy 는 컴파일타임 의존성이며 런타임에 PercentDiscountPolicy 인스턴스나 AmountDiscountPolicy 인스턴스에 대한 런타임 의존성으로 대체해야한다. 

 

어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 된다. 

구체적인 클래스를 알면 알수록 클래스가 사용하는 문맥에 강하게 결합되기 때 3문이다. 

 

컴파일타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해진다. 

 

컨텍스트 독립성

클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서 사용하기 더 어렵다. 

클래스가 특정한 문맥에 대해 최소한의 가정만 가지고 있다면 다른 문맥에 대해 재사용하기가 더 수월하다. 

 

컨텍스트 독립적이다 = 객체가 객체를 실행하는 시스템에 대해 아무것도 알지 못한다. 

 

의존성 해결하기

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

- 객체를 생성하는 시점에 생성자를 통해 의존성 해결

- 객체 생성 후 setter 메서드를 통해 의존성 해결 

- 메서드 실행 시 인자를 이용해 의존성 해결 

 

 

객체를 생성하는 시점에 생성자를 통해 의존성 해결

 

 Movie avatar = new Movie("아바타", 
   Duration.ofMinuates(120),
   Money.wons(10000),
   new AmountDiscountPolicy(...) // or new PercenetDiscountPolicy(...)
 );

 

객체 생성 후 setter 메서드를 통해 의존성 해결 

Movie avatar = new Movie(...);

avatar.setDiscountPolicy(new AmountDiscountPolicy(...));

- 객체를 생성한 이후에도 의존하고 있는 대상을 변경할 수 있는 가능성을 열어두고 싶은 경우 

- 객체가 생성된 후 의존 대상을 설정하기 때문에  그 전까지는 객체 상태가 불완전하다. 

 

생성 시점 의존성 해결 및 setter 를 통해서도 의존 대상을 변경할 수 있는 방법

Movie avatar = new Movie(..., new PercentDiscountPolicy(...));

avatar.setDiscountPolicy(new AmountDiscountPolicy(...));

 

메서드 실행 시 인자를 이용해 의존성 해결 

public class Movie {
  public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
    //...
  }
}

협력 대상에 대해 지속적으로 의존관계를 맺을 필요없이

- 메서드가 실행되는 동안만 일시적으로 의존 관계가 존재해도 무방한 경우 

- 메서드가 실행될때마다 의존대상이 매번 달라져야 하는 경우 

 

2. 유연한 설계

index

- 의존성과 결합도

- 지식의 결합을 낳는다. 

- 추상화에 의존하라

- 명시적인 의존성 

- new 는 해롭다 

- 가끔은 생성해도 무방하다. 

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

- 컨텍스트 확장하기 

- 조합 가능한 행동 

 

의존성과 결합도 

객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야한다. 

따라서 모든 의존성이 나쁜 것은 아니지만, 과하면 문제가 될 수 있다. 

 

public class Movie {
  private PercentDiscountPolicy percentDiscountPolicy;
  
  public Movie (String title, Duration runningTime, Money fee, PercentDiscountPolicy percentDiscountPolicy) {
    this.percentDiscountPolicy = percentDiscountPolicy;
  }
  
  public Money calculateMovieFee(Screening screening) {
    return fee.minus(percentDiscountPolicy.calculateDiscountAmount(screening));
  }
}

 

Movie 는 PercentDiscountPolicy 라는 구체적인 클래스에 의존한다. 다른 종류의 할인 정책이 필요한 문맥에서 Movie 를 재사용할 수 있는 가능성을 없앴다. 

 

Movie 가 협력하고 싶은 대상이 반드시 PercentDiscountPolicy 의 인스턴스일 필요는 없다. 

Movie 자신이 전송하는 calculateMovieFee메시지를 이해할 수 있고 할인된 요금을 계산할 수만 있다면 어떤 타입의 객체와 협력하더라도 상관없다. 

 

추상클래스인 DiscountPolicy 는 calculateDiscountAmount 메시지를 이해할 수 있는 타입을 정의하여 이 문제를 해결한다. 

 

바람직한 의존성은 재사용성과 관련있다. 

- X: 다양한 환경에서 클래스를 재사용할 수 없다면, 특정한 컨텍스트에 강결합되었다면, 강한 결합도

- O: 다양한 환경에서 재사용할 수 있다면, 컨텍스트 독립적이라면, 느슨한 결합도

 

특정한 컨텍스트에 강하게 의존하는 클래스를 다른 컨텍스트에서 재사용하는 방법은 구현을 변경하는 것 뿐이다. 

 

다른 환경에서 재사용하기 위해 내부 구현을 변경하게 만드는 모든 의존성은 바람직하지 않다. 

 

의존성

- 두 요소 사이의 관계 유무

 

결합도

-의존성의 정도 

 

지식이 결합을 낳는다. 

위 예제에서 Movie 는 협력할 객체가 비율 할인 정책에 따라 할인 요금을 계산할 것이라는 사실을 알고 있다. 

반면 추상클래스 DiscountPolicy 에 의존한다면 구체적인 계산방법을 알필요는 없다. 

 

결합도를 느슨하게 만들기 위해서는 협력 대상에 대한 필요한 정보 외에는 최대한 감추는 것이 중요하다. 

가장효과적인 방법은 추상화다.

 

추상화에 의존하라

추상화란 특정 구현을 의도적으로 생략하거나 감춤으로서 복잡도를 제어하는 방법이다. 문제 해결에 불필요한 정보를 감출 수 있고, 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있다. 

 

DiscountPolicy는 PercentDiscountPolicy의 추상화다. DiscountPolicy 는 PercentDiscountPolicy 가 비율 할인 정책에 따라 할인 요금을 계산한다는 사실을 숨겨준다. 

Movie는 구체적인 대상이 아닌 추상화에 의존하기 때문에 DiscountPolicy사이의 결합도는 더 느슨하다. 

 

의존성의 결합도는 다음순으로 높다.

1. 구체 클래스 의존성

2. 추상 클래스 의존성

3. 인터페이스 의존성 

 

추상 클래스는 메서드의 내부 구현과 자식클래스의 종류를 숨긴다. 

하지만 클래스 상속계층이 무엇인지 알고 있어야한다. 

 

인터페이스는 상속 계층을 모르더라도 협력이 가능해진다. 

의존 대상 객체가 어떤 메시지를 수신할 수 있는지에 대한 지식만 남긴다. 

다양한 클래스 상속 계층에 속한 객체들이 동일한 메시지를 수신할 수 있다.

 

명시적인 의존성

public class Movie {

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

// AmountDiscountPolicy 라는 구체적인 클래스에 의존하고 있다. 해당 클래스를 직접 생성하여 대입한다.  
// 1. Movie 클래스는 금액할인 정책을 사용하지 않는 컨텍스트에서는 재사용이 불가하다. 
// 2. 다른 컨텍스트에서 재사용하기 위해서는 구현을변경해야한다.

 

결합도를 느슨하게 만들기 위해서는 단순히 타입을 추상클래스나 인터페이스로 선언하는 것 뿐만 아니라 클래스 내부에서 모든 구체 클래스에 대한 의존성을 제거해야한다. 

 

런타임에 Movie는 구체 클래스의 인스턴스와 협력해야하기 때문에 Movie의 인스턴스가 AmountDiscountPolicy의 인스턴스인지 PercentDiscountPolicy의 인스턴스인지를 알려줄 수 있는 방법이 필요하다. = 의존성 해결

- 생성자

- setter 메서드

- 메서드 인자 

인스턴스 변수 타입은 추상 클래스나 인터페이스로 정의, 의존성 해결시에는 구체클래스를 전달

 

의존성의 대상을 생성자의 인자로 전달받는 방법과 생성자 안에서 직접 생성하는 방법 사이의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 할인 정책을 설정할 수 있는 방법을 제공하는 지 여부다. 

생성자의 인자로 선언하는 방법은 Movie가 DiscountPolicy에 의존한다는 사실을 Movie의 퍼블릭 인터페이스에 드러낸다. 

 

이를 명시적인 의존성이라고 한다. 

클래스 내부에서 의존 대상의 인스턴스를 직접 생성하는 방식은 의존성이 퍼블릭 인터페이스에 노출하지 않는다. 이를 숨겨진 의존성이라고 한다. 

 

의존성이 명시적이지 않으면

- 의존성 파악을 위해 내부 구현을 직접 살펴봐야한다. 인스턴스 생성 코드를 파악하는 것은 쉽지 않다.  

-  다른 컨텍스트에서 재사용하기 위해 내부 구현을 변경해야한다. 코드 수정은 잠재적인 버그가능성을 내포한다. 

 

의존성을 구현 내부에 숨겨두면 안되고, 퍼블릭 인터페이스를 통해 명시적으로 드러내야한다. 

컴파일 타임 의존성을 적절한 런타임 의존성으로 교체할 수 있다. 

 

new 는 해롭다. 

new를 잘못 사용하면 결합도가 매우 높아진다. 

- 클라이언트에서 구체클래스의 이름을 기술해야한다. 추상화가 아닌 구체클래스에 의존하게된다. 

- 구체 클래스가 어떤 인자를 사용해 클래스 생성자를 호출해야하는지도 알아야한다. 클라이언트가 알아야하는 지식의 양이 늘어난다. 

 

 

해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것이다. 

 

Movie는 인스턴스를 생성해서는 안된다. 사용하기만 해야한다. 

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

 

Movie에는 메세지를 전송하는 코드만 남아있어야한다. 

 

생성 책임은 Movie의 클라이언트가 가져야한다. 

 

사용과 생성 책임 분리, 의존성을 생성자에 명시적으로 드러내고, 구체클래스가 아닌 추상클래스에 의존하게 함으로서 설계를 유연하게 만들 수 있다. 

 

객체 생성 책임을 객체 내부가 아니라 클라이언트로 옮기는 것에서 시작이다. 

 

가끔은 생성해도 무방하다. 

주로 협력하는 기본 객체를 설정하고 싶은 경우이다. 

예를 들어 Movie 가 10 번중 9 번은 AmountDiscountPolicy와 협력하고, 1번은 PercentDiscountPolicy와 협력한다. 인스턴스 생성 책임을  오직  Movie의 클라이언트가  가진다면 중복 코드가 늘어나고 Movie 사용성이 나빠질 것이다. 

 

Movie 에 기본 객체를 생성하는 생성자를 추가하고, 이 생성자에서 DiscountPolicy의 인스턴스를 인자로 받는 생성자를 체이닝 하는 것이다. 

 

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

 

생성자가 아닌 메서드 오버로딩 시에도 사용할 수 있다. 

 

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

 

설계는 트레이드오프활동이다. 트레이드오프 대상은 결합도와 사용성이다. 

구체 클래스에 의존하게 되더라도 클래스의 사용성이 더 중요하다면 결합도를 높이는 방향으로 설계할 수도 있다. 

 

모든 결합도가 모이는 새로운 클래스를 추가할 수도 있다(Factory)

 

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

의존성이 불편한 이유는 그것이 항상 변경에 대한 영향을 암시하기 때문이다. 

따라서 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다. 

 

단 클래스를 직접 생성하더라도 추상적인 타입을 사용하는 것이 확장성 측면에서 유리하다. 

 

컨텍스트 확장하기

Movie 가 새로운 컨텍스트에서 재활용되는 케이스

 

할인 혜택을 제공하지 않는 영화의 예매요금 계산

1. discountPolicy를 null 이 가능하도록 만들어 null인 경우에는 할인 정책을 미적용하는 방법

 

문제점

- Movie 와 DiscountPolicy와의 협력 방법에 어긋나는 엣지케이스 추가

- 예외 케이스 처리를 위한 Movie 코드 수정 -> 버그 가능성 내포

 

해결책 

- 할인 정책이 존재하지 않음을 예외케이스로 처리하지 않고, DiscountPolicy 와의 협력 방식에 맞추기

 

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

- Movie 클래스에 if 예외처리 없이도 할인혜택이 없는 영화 구현  가능

 

중복 적용이 가능한 할인정책 구현

 

1. Movie 가 DiscountPolicy의 인스턴스로 구성된 List를 인스턴스 변수로 갖게 하기

 

문제점

- 기존의 할인정책의 협력방식과는 다른 예외케이스를 추가하게 만든다. 

 

해결책

- 중복 할인 정책을 구현하는 OverlappedDiscountPolicy 를 DiscountPolicy의 자식클래스로 만든다. 

 

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



// 사용시
Movie avatar = new Movie("아바타", 
  Duration.ofMinutes(120),
  Money.wons(10000),
  new OverlappedDiscountPolicy(
    new AmountDiscountPolicy(...),
    new PercentDiscountPolicy(...)
  )
)

 

유연한 설계를 만들 수 있었던 이유

- Movie 가 DiscountPolicy라는 추상화에 의존

- 생성자를 통해 DiscountPolicy에 대한 의존성을 명시적으로 드러냄

- 구체 클래스의 인스턴스 생성 책임을 외부로 옮김

 

Movie 가 의존하는 추상화인 DiscountPolicy클래스에 자식클래스를 추가하여 Movie가 사용될 컨텍스트를 확장할 수 있었다. 

 

조합 가능한 행동 

컨텍스트 확장이 가능한 이유는 코드를 직접 수정하지 않고도 협력대상인 DiscountPolicy인스턴스를 교체할 수 있었기 때문이다. 

어떤 DiscountPolicy의 인스턴스를 Movie에 연결하느냐에 따라 행동이 달라진다.

유연하고 재사용가능한 설계는 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결하여 애플리케이션 기능을 쉽게 확장할 수 있다. 

 

객체들의 조합을 통해 무엇을 하는지를 표현하는 클래스들로 구성된다. 

클래스의 인스턴스를 생성하는 코드를 보는 것만으로 객체가 어떤일을 하는지 쉽게 파악 가능하다. 

 

훌륭한 객체지향 설계 : 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현하여 객체들이 무엇을 하는지 표현하는 설계

 

핵심은 의존성을 관리하는 것이다.