오브젝트 Ch.8

Manx·2022년 5월 18일
0
post-thumbnail

'오브젝트: 코드로 이해하는 객체지향 설계' 2주차
분량 : Ch.6 ~ Ch.10
기간 : 22.5.15 ~ 22.5.21

Ch.8 의존성 관리하기

잘 설계된 객체지향 애플리케이션은 작고 응집도 높은 객체들로 구성된다.
(책임의 초점이 명확하고 한 가지 일만 잘하는 객체들)
단독으로 수행할 수 있는 작업은 거의 없기 때문에 다른 객체에게 도움을 요청해야하고, 객체 사이의 협력을 낳는다.
협력은 객체가 다른 객체게에 대해 알 것을 강요한다. 다른 객체와 협력하기 위해서는 그런 객체가 존재한다는 사실을 알고 있어야 한다. 객체가 수신할 수 있는 메시지에 대해서도 알고 있어야 한다. 이런 지식이 객체 사이의 의존성을 낳는다.

과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다.
객체 지향 설계의 핵심 : 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거해야 한다.


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

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

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

Ex) A가 B에 의존할 때

public class A {
	private B b;
}

B의 변경은 A도 영향을 받게 되지만, 그 역은 성립하지 않는다.

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

  • 런타임 : 애플리케이션이 실행되는 시점
  • 컴파일타임 : 일반적으로 컴파일하는 시점을 기리키지만 문맥에 따라서는 코드 그 자체를 가리키기도 한다.

이 예제는 전 장에서도 많이 다룬 예제이다.
인터페이스를 상속받아 구현하는 클래스를 생각해보자.
코드 상에서는 Movie클래스가 DiscountPolicy를 의존하는 것 처럼 보이지만, 런타임 상에서는 DiscountPolicy를 구현한 클래스인 AmountDiscountPolicy, PercentDiscountPolicy를 의존한다.
이처럼 컴파일타임 의존성이 런타임 의존성과 다를 수 있다.

동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 한다.

의존성 해결하기

인터페이스에 의존하는 클래스의 컴파일타임의 의존성을 어떻게 적절한 런타임 의존성으로 교체할 수 있을까?

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

Movie starWars = new Movie("스타워즈", new PercentDiscountPolicy(...));

Movie객체가 생설될 때 생성자를 통해 PercentDiscountPolicy의 인스턴스를 전달한다.


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

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

public class Movie {
	public void setDiscountPolicy (DiscountPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy
    }
}

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

  • 단점 : 객체가 생성된 후에 협력에 필요한 의존 대상을 설정하기 때문에 객체를 생성하고 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할 수 있다.

더 좋은 방법은 생성자 방식과 setter 방식을 혼합하는 것이다.

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

3. 메서드 실행 시 인자를 이용해 의존성 해결
특정 행동을 할 때만 일시적으로 알아도 무방하다면 메서드의 인자를 이용해 의존성을 해결할 수 있다.

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

유연한 설계

객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야 한다. 이런 지식들이 객체 사이의 의존성을 낳는다. 따라서 모든 의존성이 나쁜 것은 아니다.
의존성은 협력을 위해 반드시 필요한 것이다. 단지 바람직하지 못한 의존성이 문제일 뿐이다.

바람직한 의존성이란 무엇일까?
바람직한 의존성은 재사용성과 관련이 있다. 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것이다.
다른 환경에서 재사용하기 위해 내부 구현을 변경하게 만드는 모든 의존성은 바람직하지 않은 의존성이다.

바람직한 의존성이란 컨텍스트에 독립적인 의존성을 의미하며 다양한 환경에서 재사용될 수 있는 가능성을 열어놓는 의존성을 의미한다.


추상화에 의존하라

한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다. 반대로 다른 요소에 대해 더 적은 정보를 알고 있을수록 두 요소는 약하게 결합된다.
Movie가 PercentDiscountPolicy에 의존하는 것보다 DiscountPolicy에 의존하는 경우 알아야 하는 지식의 양이 적기 때문에 결합도가 느슨해지는 것이다.
결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하고, 이 목적을 달성할 수 있는 가장 효과적인 방법이 추상화이다.

추상화 : 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법

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

아래로 갈 수록 알아야 하는 정보가 적어진다.

의존하는 대상이 더 추상적일수록 결합도는 더 낮아진다.


new의 해로움

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

해결방법 : 인스턴스를 생성하는 로직과 사용하는 로직 분리
생성자의 인자로 전달하거나, setter 메서드를 사용하거나, 실행 시에 메서드의 인자로 전달하면 된다.

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

profile
백엔드 개발자

0개의 댓글