Published on

오브젝트 Chap7

Authors
  • avatar
    Name
    ywj9811
    Twitter

객체지향 설계의 핵심은 협력을 위해 필요한 의존성을 유지하면서도 변경을 방해하는 의존성은 제거해야 하는데 있다.

의존성 이해하기

변경과 의존성

어떤 객체가 협력하기 위해 다른 객체를 필요로 하게 될 때 두 객체 사이에 의존성이 존재하게 된다.

  • 실행 시점 : 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 존재해야 한다.
  • 구현 시점 : 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경되어야 한다.

예시를 살펴보자

public class PeriodCondition implements DiscountCondition {
	private DayOfWeek dayOfWeek;
	private LocalTime startTime;
	private LocalTime endTimel;

	...
	
	public boolean isSatisfiedBy(Screening screening) {
		return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
			startTime.compareTo(screeing.getStartTime().toLocalTime() <= 0 &&
			endTime.compareTo(screeing.getStartTime().toLocalTime() >= 0 &&
	}
}

이때 PeriodCondition의 인스턴스가 제대로 동작하기 위해서는 Screening의 인스턴스가 존재해야 한다.

이러한 모습에서 두 객체 사이에 의존성이 존재한다고 말한다.

의존성은 방향성을 가지며 단방향으로, Screening이 변경될 때 PeriodCondition이 영향을 받지만 역은 성립하지 않는다.

의존성은 변경에 의한 영향의 전파 가능성을 암시하는데, 다시 위의 코드를 살펴보면 PeriodCondtion은 DayOfWeek과 LocalTime의 인스턴스를 속성으로 포함하고 isSatisfiedBy 메소드의 인자로 Screening의 인스턴스를 받는다.

즉, PeriodCondtion은 DayOfWeek, LocalTime, Screening에 대한 의존성을 갖는다.

또한, DiscountCondition 인터페이스의 변경이 영향을 끼치기 때문에 이에 대해서도 의존성을 가진다.

이렇게 어떠한 형태로든 DayOfWeek, LocalTime, Screening, DiscountCondition 이 변경된다면 Periodcondtion도 함께 변경될 수 있다는 것이다.

이것이 근본적인 의존성의 특성이다. 자신이 의존하는 대상이 변경될 때 함께 변경될 수 있다는 것이다.

의존성 전이

의존성 전이가 의미하는 것은 의존하는 대상이 의존하는 대상 또한 의존하게 된다는 것이다.

즉, Screening이 의존하는 대상에 대해서도 PeriodCondtion이 의존하게 된다는 것이다.

다만 의존성 전이는 변경에 의해 영향이 널리 전파될 수도 있다는 경고일 뿐이다.

이러한 특성에 의해 의존성은 두가지 종류로 나뉜다.

  • 직접 의존성 : 다른 요소에 대해 직접 의존하는 경우 (PeriodCondtion은 Screening, LocalTime 등등)
  • 간접 의존성 : 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우

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

의존성은 시점에 따라 런타임 의존성과 컴파일타임 의존성으로 나뉜다.

  • 런타임 의존성 : 말 그대로 어플리케이션이 실행되는 시점의 의존성이다.
  • 컴파일타임 의존성 : 일반적으로 컴파일타임을 이야기하면 컴파일 되는 시점이라고 생각하지만, 다른 경우도 있다. 바로 코드 그 자체를 가리키는 경우이다. 컴파일타임 의존성이 그 경우이다.

객체지향 어플리케이션에서 런타임의 주인공은 객체이다. 따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성이다.

하지만 코드 관점에서 주인공은 클래스이다. 따라서 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.

이전에 살펴보았던 Movie클래스를 생각해보면 DiscountPolicy라는 추상 클래스에 의존하고 있으며 그에 대한 구현체에 대해서는 알지 못하지만, 실제 동작시에는 AmountDiscountPolicy혹은 PercentDiscountPolicy와 의존관계를 가지고 있다.

컴파일타임 의존성은 DiscountPolicy이지만, 런타임 의존성은 AmountDiscountPolicy혹은 PercentDiscountPolicy로 이루어져있다.

이처럼 유연하고 재사용 가능한 설계를 창조하기 위해서는 동일한 소스코드 구조(DiscountPolicy의존)를 가지고 다양한 실행 구조(두가지 구현체와 의존)를 가질 수 있도록 해야한다.

즉, 어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안된다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 한다.

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

컨텍스트 독립성

클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알아서는 안된다.

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

클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이루어져 있다면 다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트 독립성이라 부른다.

컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용될 수 있기 때문이다.

의존성 해결하기

컴파일타임 의존성은 구체적인 런타임 의존성으로 대체되어야 한다.

이처럼 컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부르며 일반적으로 세가지 방법을 사용한다.

  1. 객체를 생성하는 시점에 생성자를 통해 의존성 해결
  2. 객체 생성 후 setter를 통해 의존성 해결
    • 이 경우 객체가 생성된 후에 협력에 필요한 의존 대상을 설정하기 때문에 객체를 생성하고 의존 대상을 설정하기 전까지 객체의 상태가 불완전할 수 있다. NPE 위험
    • 그리고 어디서든 변경이 가능하기 때문에 오류를 유발할 수 있다.(책의 내용X)
  3. 메소드 실행 시 인자를 이용해 의존성 해결
    • 협력 대상에 대해 지속적으로 의존 관계를 맺을 필요 없이 메소드가 실행되는 동안에만 일시적으로 의존관계가 존재해도 무방하거나, 메소드 실행 시점에 따라 매번 달라져야 하는 경우에 사용

유연한 설계

의존성과 결합도

객체지향 패러다임의 근간은 협력이다. 객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야 한다. 이런 지식들이 객체 사이의 의존성을 낳는다. 따라서 모든 의존성이 나쁜 것이 아니다.

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

만약 Movie가 PercentDiscountPolicy라는 구체적인 클래스에 의존하게 만들었다면, 다른 종류의 할인 정책이 필요한 문맥에서 Movie를 재사용할 수 없을 것이다.

이는 바람직한 의존성이 아니다.

바람직한 의존성을 위해서는 Movie가 협력하고 싶은 대상이 반드시 PercentDiscountPolicy가 아닐 수 있다는 것에 집중해야 한다.

Movie의 입장에서 협력할 객체의 클래스를 고정할 필요가 없다. 단지 자신이 전송하는 메시지를 이해할 수 있고 응답만 할 수 있다면 어떤 타입의 객체와 협력하더라도 상관없다.

따라서 DiscountPolicy라는 추상 클래스를 통해 메시지를 이해할 수 있는 타입으로 정의하고 구체적인 클래스는 이를 구현하도록 한 이후 Movie가 DiscountPolicy와 의존성을 가진다면 컴파일타임 의존성을 구체적인 런타임 의존성으로 대체할 수 있을 것이다.

이처럼 의존성이 다양한 환경에서 재사용할 수 있다면 이는 바람직한 의존성이다.

반면에 다양한 환경에서 클래스를 재사용할 수 없다면 이는 바람직하지 못한 의존성이다.

이러한 바람직한 의존성을 느슨한 결합도, 약한 결합도를 가진다고 말하고

바람직하지 못한 의존성을 단단한 결합도, 강한 결합도를 가진다고 말한다.

지식이 결합을 낳는다

서로에 대해 알고 있는 지식의 양이 결합도를 결정한다.

더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미하는데, 기존 지식에 어울리지 않는 컨텍스트에서 클래스의 인스턴스를 사용하기 위해서 할 수 있는 유일한 방법은 클래스를 수정하는 것 뿐이다.

추상화에 의존하라

추상화를 사용하면 현재 다루고 있는 문제를 해결하는데 불필요한 정보를 감출 수 있기에 결합도를 느슨하게 유지할 수 있다.

  • 구체 클래스 의존성
  • 추상 클래스 의존성
  • 인터페이스 의존성

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

추상 클래스는 메소드의 내부 구현과 자식 클래스의 종류에 대한 지식을 클라이언트에게 숨길 수 있다.

따라서 결합도가 낮아지게 된다. 하지만 추상 클래스의 클라이언트는 여전히 협력하는 대상이 속한 클래스 상속 계층이 무엇인지 알아야 한다.

하지만 인터페이스에 의존하게 되면 상속 계층을 모르더라도 단순히 어떤 메시지를 수신할 수 있는지만 알 수 있다면 가능하기 때문에 결합도가 더욱 낮아지게 된다.

명시적인 의존성

public class Movie {
	...
	private DiscountPolicy discountPolicy;

	public Movie(String title, ...) {
		...
		this.discountPolicy = new AmountDiscountPolicy(...);
	}
}

이 코드는 한가지 실수로 인해 결합도가 불필요하게 높아졌다. 바로 생성자에서 구체 클래스인 AmountDiscountPolicy 의 인스턴스를 생성해서 대입하고 있다.

따라서 Movie는 추상 클래스인 DiscountPolicy뿐만 아니라 구체 클래스인 AmountDiscountPolicy의 인스턴스를 직접 생성해서 대입하고 있기 때문에 Movie는 추상 클래스 뿐만 아니라 구체 클래스에도 의존하게 된다.

우리는 클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야 한다.

이를 해결하는 방법에는 앞서 설명한 바와 같이 생성자, setter, 메소드 인자를 사용하는 방식이 존재한다.

public class Movie {
	...
	private DiscountPolicy discountPolicy;

	public Movie(String title, ..., DiscountPolicy discountPolicy) {
		...
		this.discountPolicy = discountPolicy;
	}
}

이렇게 생성자의 인자를 통해 전달을 하게 되면 런타임에 구체 클래스의 인스턴스를 선택적으로 전달할 수 있다.

이 두가지 방식의 큰 차이점은 퍼블릭 인터페이스를 통해 할인 정책을 설정할 수 있는 방법을 제공하는지 여부이다.

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

이렇게 모든 경우에 의존성은 명시적으로 퍼블릭 인터페이스에 노출되게 된다.

이것을 명시적인 의존성 이라고 부른다.

반대로 의존성이 퍼블릭 인터페이스에 표현되지 않는 것을 숨겨진 의존성 이라고 부른다.

유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계이다.

new는 해롭다

new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다. 결합도 측면에서 new가 해로운 이유는 크게 두가지이다.

  1. new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 한다 → 구체 클래스에 직접 의존해야 하기 때문에 결합도가 높아진다.
  2. new 연산자는 생성하려는 구체 클래스 뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다 → 지식의 양이 늘어나기 때문에 결합도가 높아진다.

예를 들어서 이전에 Movie 생성자 내부에서 직접 new를 통해 AmountDiscountPolicy를 생성하던 모습을 생각해보자.

이를 생성하기 위해서는 AmountDiscountPolicy 생성자의 전달될 인자를 알고 있어야 하고 Movie는 이로 인해 더 많은 것에 의존하게 된다.

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

Movie의 생성자에 인자로 받아서 DiscountPolicy를 런타임 시점에 구체화 하는 코드를 생각해보자.

이 경우 Movie는 단지 메시지를 전송하는 단 하나의 일만 수행하게 된다.

그렇다면 AmountDiscountPolicy는 누가 생성하는가. Movie의 클라이언트가 처리한다.

이렇게 생성의 책임을 클라이언트로 옮김으로써 Movie는 DiscountPolicy의 모든 자식 클래스와 협력할 수 있게 된다.

사용과 생성의 책임을 분리하고, 명시적으로 의존성을 생성자에 드러내고, 구체 클래스가 아닌 추상 클래스 혹은 인터페이스에 의존하게 해서 설계를 유연하게 만들 수 있는데, 그 출발은 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트에게 옮기는 것에서 시작했다는 점을 기억하자.

가끔은 생성해도 무방하다

만약 Movie가 거의 항상 AmountDiscountPolicy만 사용한다고 가정해보자.

그렇다면 생성하는 책임을 클라이언트로 옮기는 것이 오히려 중복 코드가 늘어나고 사용성도 나빠질 것이다.

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

public class Movie {
	...
	private DiscountPolicy discountPolicy;

	public Movie(String title, ...) {
		...
		this(title, ..., new AmountDiscountPolicy(...));
	}

	public Movie(String title, ..., DiscountPolicy discountPolicy) {
		...
		this.discountPolicy = discountPolicy;
	}
}

이제 클라이언트는 대부분의 경우에 추가된 간략한 생성자를 통해 AmountDiscountPolicy와 협력하게 하면서도 컨텍스트에 적절한 DiscountPolicy의 인스턴스로 의존성을 교체할 수 있다.

이러한 예시는 설계가 트레이드 오프 활동이라는 사실을 다시 한번 상기시킨다.

여기서 트레이드 오프의 대상은 결합도와 사용성이다.

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

조합 가능한 행동

어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징이다.

유연하고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇(what)을 하는지 표현하는 클래스들로 구성된다.

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

그리고 이런 설계를 창조하는 데 있어서 핵심은 의존성에 대한 관리이다.