본문 바로가기
📖 독서/오브젝트(Objects)

[ 오브젝트] 8장. 의존성 관리하기

by 말랑한곰탱이 2022. 4. 9.

- 협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다. 객체지향 설계의 핵심은 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거하는 데 있다. 이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술이라고 할 수 있다.

 

1. 의존성 이해하기

<변경과 의존성>

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

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

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

 

<의존성 전이>

- 의존성 전이 : 객체 A가 객체 B에 의존할 경우 A는 B가 의존하는 대상에 대해서도 자동적으로 의존하게 되는 것. 의존성은 전이될 수 있기 때문에 직접 의존성간접 의존성으로 나뉜다.

- 직접 의존성 : 말 그대로 한 요소가 다른 요소에 직접 의존하는 경우.

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

 

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

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

- 컴파일타임 : 작성된 코드를 컴파일하는 시점. 그러나 의존성에서는 코드 그 자체를 가리킨다.

- 객체지향 애플리케이션에서 런타임의 주인공은 객체다. 따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성이다. 반면 코드 관점에서 주인공은 클래스다. 따라서 컴파일타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.

- 어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 된다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 한다. 클래스가 협력할 객체의 클래스를 명시적으로 드러내고 있다면 다른 클래스의 인스턴스와 협력할 가능성 자체가 없어진다. 따라서 컴파일타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용 가능해진다.

 

<컨텍스트 독립성>

: 클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서는 사용하기 어려워진다. 다른 문맥에서 재사용하기 수월하기 위해 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄지게 하는 것.

- 설계가 유연해지기 위해서는 가능한 한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다. 컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용될 수 있기 때문이다.

 

<의존성 해결하기>

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

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

 

2. 유연한 설계

<의존성과 결합도>

- 일반적으로 의존성과 결합도를 동의어로 사용하지만 사실 두 용어는 서로 다른 관점에서 관계의 특성을 설명하는 용어다. 의존성은 두 요소 사이의 관계 유무를 설명한다. 따라서 의존성의 관점에서는 "의존성이 존재한다" 또는 "의존성이 존재하지 않는다"라고 표현해야 한다. 그에 반해 결합도는 두 요소 사이에 존재하는 의존성의 정도를 상대적으로 표현한다. 따라서 결합도의 관점에서는 "결합도가 강하다" 또는 "결합도가 느슨하다"라고 표현한다.

- 바람직한 의존성이란 설계를 재사용하기 쉽게 만드는 의존성이다. 바람직하지 못한 의존성이란 설계를 재사용하기 어렵게 만드는 의존성이다. 어떤 의존성이 재사용을 방해한다면 결합도가 강하다고 표현한다. 어떤 의존성이 재사용을 쉽게 허용한다면 결합도가 느슨하다고 표현한다.

 

<지식이 결합을 낳는다>

- 더 많이 알수록 더 많이 결합된다. 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미한다. 기존 지식에 어울리지 않는 컨텍스트에서 클래스의 인스턴스를 사용하기 위해서 할 수 있는 유일한 방법은 클래스를 수정하는 것뿐이다. 결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야 한다. 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하다. 이 목적을 달성할 수 있는 가장 효과적인 방법은 추상화다.

 

<추상화에 의존하라>

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

- 실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록 결합도가 낮아진다. 결합도를 느슨하게 만들기 위해서는 구체적인 클래스보다 추상 클래스에, 추상 클래스보다 인터페이스에 의존하도록 만드는 것이 더 효과적이다. 다시 말해 의존하는 대상이 더 추상적일수록 결합도는 더 낮아진다.

 

<명시적인 의존성>

- 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수밖에 없다. 커다란 클래스에 정의된 긴 메서드 내부 어딘가에서 인스턴스를 생성하는 코드를 파악하는 것은 쉽지 않을뿐더러 심지어 고통스러울 수도 있다.

- 의존성은 명시적으로 표현돼야 한다. 의존성을 구현 내부에 숨겨두지 마라. 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다. 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성으로 교체할 수 있다.

 

<new는 해롭다>

- 대부분의 언어에서는 클래스의 인스턴스를 생성할 수 있는 new 연산자를 제공한다. 하지만 new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다. 결합도 측면에서 new가 해로운 이유는 크게 두 가지다.

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

 

<조합 가능한 행동>

- 유연하고 재사용 가능한 설계는 객체가 어떻게(how) 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇(what)을  하는지를 표현하는 클래스들로 구성된다. 따라서 클래스의 인스턴스를 생성하는 코드를 보는 것만으로 객체가 어떤 일을 하는지를 쉽게 파악할 수 있다. 코드에 드러난 로직을 해석할 필요 없이 객체가 어떤 객체와 연결됐는지를 보는 것만으로도 객체의 행동을 쉽게 예상하고 이해할 수 있기 때문이다. 다시 말해 선언적으로 객체의 행동을 정의할 수 있는 것이다.

- 유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계다. 훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다. 그리고 이런 설계를 창조하는 데 있어서의 핵심은 의존성을 관리하는 것이다.