Post

의존성 관리하기

8. 의존성 관리하기

8.1 의존성 이해하기

변경과 의존성

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

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

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

의존성은 단방향

두 요소 사이의 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미

의존성 전이

의존성 전이(transitive dependency)가 의존하는 대상이 의존하는 것들에도 의존하게 됨

의존성은 함께 변경될 수 있는 가능성을 의미하기 때문에 모든 경우에 의존성이 전이되는 것은 아니다.

의존성이 실제로 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라진다

내부 구현을 효과적으로 캡슐화하고 있다면 변경이 전파되지 않음

의존성은 전이될 수 있기 때문에 의존성의 종류를 직접 의존성(direct dependency)간접 의존성(indirect dependency)으로 나누기도 함

직접 의존성이란 한 요소가 다른 요소에 직접 의존하는 경우

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

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

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

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

일반적으로 컴파일타임이란 작성된 코드를 컴파일하는 시점을 가리키지만 문맥에 따라서는 코드 그 자체를 가르키기도 함

컴파일타임 의존성이라는 용어를 중요하게 생각하는 것은 시간이 아니라 우리가 작성한 코드의 구조이기 때문

동적 타입 언어의 경우에는 컴파일타임이 존재하지 않기 때문에 컴파일타임 의존성이라는 용어를 실제로 컴파일이 수행되는 시점으로 이해하면 의미가 모호해짐

객체지향 애플리케이션에서 런타임의 주인공은 객체

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

코드 관점에서 주인공은 클래스

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

런타임 의존성과 컴파일타임 의존성이 다를 수 있음

유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 함

유연하고 재사용 가능한 설계를 창조하기 위해서는 동일한 소스코드 구조를 가지고 다양한 실행 구조를 만들 수 있어야 함

객체와 타입 간의 관계를 잘 정의해야 좋은 실행 구조를 만들어낼 수 있다.

컨텍스트 독립성

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

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

구체 클래스에 대해 의존하는 것은 클래스의 인스턴스가 어떤 문맥에서 사용될 것인지를 구체적으로 명시하는 것과 같다

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

클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 쉬워짐 -> 컨텍스트 독립성

설계가 유연해지기 위해서는 가능한 한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 함

컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용 가능

의존성 해결하기

컴파일타임 의존성을 구체적인 런타임 의존성으로 대체돼야 한다. -> 의존성 해결

의존성 해결 방법

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

setter 메서드를 이용하면 실행 시점에 의존 대상을 변경할 수 있기 때문에 설계를 좀 더 유연하게 만들 수 있다

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

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

항상 객체를 생성할 때 의존성을 해결해서 완전한 상태의 객체를 생성한 후, 필요에 따라 setter 메서드를 이용해 의존 대상을 변경할 수 있게 할 수 있다.

시스템의 상태를 안정적으로 유지하면서도 유연성을 향상시킬 수 있기 때문에 의존성 해결을 위해 가장 선호되는 방법

메서드 인자를 사용하는 방식은 협력 대상에 대해 지속적으로 의존 관계를 맺을 필요 없이 메서드가 실행되는 동안만 일시적으로 의존 관계가 존재해도 무방하거나, 메서드가 실행될 때마다 의존 대상이 매번 달라져야 하는 경우에 유용

8.2 유연한 설계

의존성과 결합도

의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서 바람직한 것

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

바람직한 의존성 필요

추상 클래스를 통해 해결

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

컨텍스트에 독립적인 의존성은 바람직한 의존성이고 특정한 컨텍스트에 강하게 결합된 의존성은 바람직하지 않은 의존성

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

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

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

어떤 두 요소 사이에 존재하는 의존성이 바람직할 때 두 요소가 느슨한 결합도(loose coupling) 또는 약한 결합도(weak coupling)을 가진다고 한다

두 요소 사이의 의존성이 바람직하지 못할 때 단단한 결합도(tight coupling) 또는 강한 결합도(strong coupling)를 가진다고 한다

지식이 결합을 낳는다

한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합됨

반대로 한 요소가 다른 요소에 대해 더 적은 정보를 알고 있을수록 두 요소는 약하게 결합됨

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

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

-> 추상화

추상화에 의존하라

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

일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분

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

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

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

하지만 추상 클래스의 클라이언트는 여전히 협락하는 대상이 속한 클래스 상속 계층이 무엇인지에 대해서는 알고 있어야 한다.

인터페이스에 의존하면 상속 계층을 모르더라도 협력이 가능해짐

인터페이스 의존성은 협력하는 객체가 어떤 메시지를 수신할 수 있는지에 대한 지식만을 남기기 때문에 추상 클래스 의존성보다 결합도가 낮다

실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록 결합도가 낮아짐

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

명시적인 의존성

인스턴스 변수의 타입을 추상 클래스나 인터페이스로 선언하는 것만으로는 부족함

클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야만 함

의존성의 대상을 생성자의 인자로 받음

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

모든 경우에 의존성은 명시적으로 퍼블릭 인터페이스로 노출됨 -> 명시적인 의존성(explicit dependency)

의존성이 퍼블릭 인터페이스에 표현되지 않음 -> 숨겨진 의존성(hidden dependency)

의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수밖에 없다.

더 큰 문제는 의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다는 것이다.

코드 수정은 언제나 잠재적으로 버그의 발생 가능성을 내포

의존성은 명시적으로 표현돼야 한다.

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

명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성으로 교체 가능

new는 해롭다

결합도 측면에서 new가 해로운 이유

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

구체 클래스에 직접 의존하면 결합도가 높아진다

결합도가 높으면 변경에 영향을 받기 쉬워짐 -> 변경에 취약

new는 결합도를 높이기 때문에 해롭다

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

필요한 인스턴스를 인자로 전달받아 내부의 인스턴스 변수에 설정

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

가끔은 생성해도 무방하다

주로 협력하는 기본 객체를 설정하고 싶은 경우 클래스 안에서 객체의 인스턴스르 직접 생성하는 방식이 유용할 수 있다.

이 문제를 해결하기 위해 기본 객체를 생성하는 생성자를 추가하고 이 생성자에서 인스턴스를 인자로 받는 생성자를 체이닝

메서드도 오버로딩으로 해결 가능

이러한 설계는 트레이드오프(결합도와 사용성)

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

변경될 확률이 거의 없는 클래스라면 의존성이 문제 되지 않음(ex.JDK)

의존성이 영향이 적은 경우데오 추상화에 의존하고 의존성을 명시적으로 드러내는 것은 좋은 습관이다.

컨텍스트 확장하기

결합도를 낮춤으로써 얻게 되는 컨텍스트의 확장이라는 개념이 유연하고 재사용 가능한 설계를 만드는 핵심이다.

조합 가능한 행동

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

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

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

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

선언적으로 객체의 행동을 정의 가능

유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계

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

이런 설계의 핵심은 의존성 관리!

참조

  1. 오브젝트
This post is licensed under CC BY 4.0 by the author.