티스토리 뷰

이번 주 스터디는 8, 9장에 대해 나눈다. 7장은 쉬어가는 파트라고 해서 건너뛰었다. 8장에서는 의존성을 잘 관리하기 위해 의존성을 이해하고 의존성을 관리하는 원칙과 기법에 관해 학습한다.

의존성 이해하기


변경과 의존성

어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때, 두 객체 사이에서는 의존성이 존재하게 된다. 의존성은 실행 시점구현 시점에 따라 다른 의미를 가진다.

  • 실행 시점 : 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 한다.
  • 구현 시점 : 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다.
public class PeriodCondition impolements DiscountCondition {
    public boolean isSatisfiedBy(Screening screening) {
        return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
        startTime.compareTo(screening.getStarttime().toLocalTime()) <= 0 &&
        endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;    
    }
}

위 코드는 PeriodCondition이 Screening에 의존성을 가지는 예이다.

실행 시점에 PeriodCondition의 인스턴스가 정상적으로 동작하기 위해서는 Screening의 인스턴스가 존재해야 한다. 의존성은 항상 단방향을 가진다. Screening이 변경될 때, PeriodCondition이 영향을 받지만 그 역은 성립하지 않는다. 

의존성 전이

의존성은 전이될 수 있다. PeriodCondition이 Screening에 의존하는 경우에 Screening이 의존하는 다른 대상에 대해서도 자동적으로 의존하게 된다.

직접 의존성간접 의존성으로 나누기도 한다.

  • 직접 의존성 : 한 요소가 다른 요소에 직접 의존하는 경우, 코드에 명시적으로 드러남
  • 간접 의존성 : 직접적인 관계는 존재하지 않지만, 의존성 전이에 의해 영향이 전파되는 경우

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

  • 런타임 : 애플리케이션이 실행되는 시점
  • 컴파일 타임 : 작성된 코드를 컴파일하는 시점 또는 코드 그 자체

런타임의 주인공은 객체, 컴파일타임의 주인공은 클래스이다. 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 한다.

컨텍스트 독립성

클래스가 특정 문맥에 강하게 결합될수록 다른 문맥에서 사용하기 어렵다. 특정 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 쉬워진다. 이를 컨텍스트 독립성이라 한다.

의존성 해결하기

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

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

유연한 설계


객체지향의 패러다임의 근간은 협력이다. 협력하기 위해서는 객체들이 서로의 존재와 수행 가능한 책임을 알아야 한다. 이런 지식들이 객체 사이의 의존성을 만든다. 의존성이 존재하는 것은 바람직하다. 그 의존성의 정도가 문제다.

바람직한 의존성은 재사용성과 관련이 있다. 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못하다. 의존성이 다양한 환경에서 재사용할 수 있다면의존성은 바람직한 것이다. 즉, 컨텍스트 독립적인 의존성바람직한 의존성이다. 특정 컨텍스트에 강하게 결합된 의존성은 바람직하지 않다.

결합도란 의존성이 바람직하냐, 아니냐를 가리키는 용어다. 의존성이 바람직할 때 두 요소가 느슨한 결합도(loose coupling) 또는 약한 결합도(weak coupling)를 가진다고 말한다. 두 요소 사이의 의존성이 바람직하지 못할 때 단단한 결합도(tight coupling) 또는 강한 결합도(strong coupling)를 가진다고 말한다.

지식이 결합을 낳는다

서로에 대해 알고 있는 지식의 양결합도를 결정한다. 더 많이 알수록 더 많이 결합된다. 이는 더 적은 컨텍스트에서 재사용 가능하다는 의미다.

추상화에 의존하라

추상화란 어떤 양상, 세부사항, 구조를 더 명확하게 이해하기 위해 특정 절차나 물체를 생략하거나 감춤으로써 복잡도를 극복하는 방법이다. 추상화를 통해 대상에 대해 알아야 하는 지식의 양을 줄일 수 있다.

의존 대상을 아래와 같이 구분된다.

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

아래로 내려올수록 클라이언트가 알아야 하는 지식의 양이 적어지기 때문에 결합도가 느슨해진다.

명시적인 의존성

의존성 대상을 메서드 인자로 전달받는 방식은 어떤 객체에 의존하는지 퍼블릭 인터페이스에 드러낸다. 이를 명시적인 의존성(explicit dependency)이라 한다. 반면에 클래스 내부에서 인스턴스를 직접 생성하는 방식은 의존성을 감춘다. 이를 숨겨진 의존성이라 한다.

결론은, 의존성은 명시적으로 표현되어야 한다. 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴보아야 하기 때문이다. 또한, 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다. 유연하고 재사용 가능한 설계는 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러난다.

new는 해롭다

new를 잘못 사용하면 클래스 사이의 결합도가 높아진다. new 연산자 사용은 아래 두 가지의 단점이 있다.

  • 구체 클래스 이름을 직접 기술함. 즉, new를 사용하면 구체 클래스에 의존하게 됨.
  • 구체 클래스 이름뿐 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 함. 즉, 클라이언트가 알아야 하는 지식이 늘어남.

아래 예시를 보자.

public class Movie {
	
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningtime, Money fee) {
    	this.discountPolicy = new AmountDiscountPolicy(money.wons(800),
        					  new SequenceCondition(1),
                              new SequenceCondition(10),
                              new Periodcondition(DayOfWeek.MONDAY,
                                  LocalTime.of(10, 0), LocalTime.of(11, 59)),
                              new Periodcondition(DayOfWeek.THURSDAY,
                                  LocalTime.of(10, 0), LocalTime.of(20, 59))));
    }
}

Movie가 AmountDiscount 인스턴스를 생성하기 위해서, 생성자에 전달되는 인자를 알고 있어야 한다. 이것은 Movie 클래스가 알아야 하는 지식의 양을 늘리기 때문에 Movie가 AmountDiscountPolicy에게 더 강하게 결합되게 한다. 즉,  new는 결합도를 높이기 때문에 해롭다.

위 예시는 아래처럼 바뀔 수 있다.

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

생성자 인자로 DiscountPolicy를 받는다. 생성자 내부에서 AmountDiscountPolicy 인스턴스를 직접 생성하지 않는다. 대신에 클라이언트에게 AmountDiscountPolicy 인스턴스를 생성하는 책임이 옮겨졌다. 다시 말하면, Movie에게서 인스턴스를 생성하는 책임은 클라이언트에게 옮겨지고, 인스턴스를 사용하는 책임은 남는다.

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

하지만, 그럼에도 불구하고 사용해도 되는 경우가 있다. 

가끔은 생성해도 무방하다

협력하는 기본 객체를 설정하고 싶은 경우가 있다. Movie가 대부분의 경우에 AmountDiscountPolicy의 인스턴스와 협력하고 가끔씩은 PercentDiscountPolicy의 인스턴스와 협력할 수도 있다. 이경우에는 기본 객체를 생성하는 생성자를 추가하고, 이 생성자에서 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;
    }
}

첫 번째 생성자 내부에서 두 번째 생성자를 호출한다. 이 경우 평소에는 AmountDiscountPolicy의 인스턴스와 협력하게 하면서, 때에 따라 DiscountPolicy의 인스턴스로 교체할 수 있다.

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

변경될 확률이 거의 없는 클래스라면 의존성이 문제 되지 않는다. ArrayList와 같은 코드가 이 경우다.

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

new 연산자를 통해 객체를 생성하지만, 변경될 확률이 거의 없기에 문제가 되지 않는다.

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday