오브젝트 독서회 7회차 - 8장 의존성 관리하기

이번 독서회는 @vincent @freebear @은딩 님께서 함께해 주셨습니다.

이번 장은 오래 전에 정리까지 해두고, 앞선 6,7장을 정리하지 못해서 못올린거라 내용이 저도 잘 기억이 안나는데 한번 쭉 훑어보고 독후감 남겨야할 것 같습니다.


P253

잘 설계된 객체지향 어플리케이션은 작고 응집도 높은 객체들로 구성된다.

@은딩

작고 응집도 높은 객체란 책임의 초점이 명확하고 한 가지 일만 잘 하는 객체를 의미한다. 이런 작은 객체들이 단독으로 수행할 수 있는 작업은 거의 없기 때문에 일반적인 어플리케이션의 기능을 구현하기 위해서는 다른 객체에게 도움을 요청해야 한다. 이런 요청이 객체 사이의 협력을 낳는다.

@vincent

협력은 필수적이지만 과도한 협력은 설계를 곤경에 빠트릴 수 있다.

@은딩

협력은 객체가 다른 객체에 대해 알 것을 강요한다.

@vincent

다른 객체와 협력하기 위해서는 그런 객체가 존재한다는 사실을 알고 있어야 한다. 객체가 수신할 수 있는 메시지에 대해서도 알고 있어야 한다. 이런 지식이 객체 사이의 의존성을 낳는다.

@freebear

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

@은딩

이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술이라고 할 수 있다.[Metz12].

@freebear @vincent

P254

이처럼 어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재한다고 말한다. 의존성은 방향성을 가지며 항상 단방향이다.

@은딩 @freebear @vincent

P255

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

@freebear

따라서 의존성은 변경에 의한 영향의 전파 가능성을 암시한다.

@은딩

P256

의존성은 전이될 수 있다. Screening의 코드를 살펴보면 Screening이 Movie, LocalDateTime, Customer에 의존한다는 사실을 알 수 있다.

@freebear

P258

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

@은딩

여기서 중요한 것은 런타임 의존성과 컴파일타임 의존성이 다를 수 있다는 것이다. 사실 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 서로 다르게 만들어야 한다.

@은딩

P259

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

@freebear

P260

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

@은딩 @vincent

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

@freebear @은딩

객체지향 프로그램의 실행 구조는 소스코드 구조와 일치하지 않는 경우가 종종 있다. 코드 구조는 컴파일 시점에 확정되는 것이고 이 구조에는 고정된 상속 클래스 관계들이 포함된다. 그러나 프로그램의 실행 시점 구조는 협력하는 객체에 따라서 달라질 수 있다. 즉, 두 구조는 전혀 다른 별개의 독립성을 갖는다.

@은딩

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

@freebear

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

@은딩 @vincent

P261

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

@freebear

컨텍스트 독립성을 따르면 다양한 컨텍스트에 적용할 수 있는 응집력 있는 객체를 만들 수 있고 객체 구성 방법을 재설정해서 변경 가능한 시스템으로 나아갈 수 있다[Freeman09].

@freebear @은딩

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

@은딩

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

@freebear @vincent

P263

setter 메서드를 이용하는 방법은 실행 시점에 의존 대상을 변경할 수 있기 때문에 설계를 좀 더 유연하게 만들 수 있다. 단점은 객체가 생성된 후에 협력에 필요한 의존 대상을 설정하기 때문에 객체를 생성하고 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할 수 있다는 점이다.

@vincent

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

@freebear @vincent

P264

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

@vincent

설계를 유연하고 재사용 가능하게 만들기로 결정했다면 의존성을 관리하는 데 유용한 몇 가지 원칙과 기법을 익힐 필요가 있다.

@freebear

객체지향 패러다임의 근간은 협력이다.

@은딩

따라서 모든 의존성이 나쁜 것은 아니다. 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서는 바람직한 것이다. 하지만 의존성이 과하면 문제가 될 수 있다.

@freebear

P265

의존성은 협력을 위해 반드시 필요한 것이다. 단지 바람직하지 못한 의존성이 문제일 뿐이다.
바람직한 의존성은 재사용성 과 관련이 있다. 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것이다. 어떤 의존성이 다양한 환경에서 재사용할 수 있다면 그 의존성은 바람직한 것이다. 다시 말해 컨텍스트에 독립적인 의존성은 바람직한 의존성이고 특정한 컨텍스트에 강하게 결합된 의존성은 바람직하지 않은 의존성이다.

@freebear @은딩 @vincent

P266

다른 환경에서 재사용하기 위해 내부 구현을 변경하게 만드는 모든 의존성은 바람직하지 않은 의존성이다. 바람직한 의존성이란 컨텍스트에 독립적인 의존성을 의미하며 다양한 환경에서 재사용될 수 있는 가능성을 열어놓는 의존성을 의미한다.

@은딩

어떤 두 요소 사이에 존재하는 의존성이 바람직할 때 두 요소가 느슨한 결합도(loose coupling) 또는 약한 결합도(weak coupling) 를 가진다고 말한다. 반대로 두 요소 사이의 의존성이 바람직하지 못할 때 단단한 결합도(tight coupling) 또는 강한 결합도(strong coupling) 를 가진다고 말한다.

@은딩 @vincent

P267

따라서 Movie가 PercentDiscountPolicy에 의존하는 것보다 DiscountPolicy에 의존하는 경우 알아야 하는 지식의 양이 적기 때문에 결합도가 느슨해지는 것이다.

@은딩

더 많이 알수록 더 많이 결합된다. 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용이 가능하다는 것을 의미한다.

@은딩

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

@freebear

P268

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

@vincent

추상화를 사용하면 현재 다루고 있는 문제를 해결하는 데 불필요한 정보를 감출 수 있다. 따라서 대상에 대해 알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있다.

@freebear

따라서 클라이언트가 알아야 하는 지식의양이 더 적기 때문에 구체 클래스보다 추상 클래스에 의존하는 것이 결합도가 더 낮다.

@은딩

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

@은딩 @vincent

다시 말해 의존하는 대상이 더 추상적일수록 결합도는 더 낮아진다는 것이다. 이것이 핵심이다.

@은딩

P269

이 예제에서 알 수 있는 것처럼 결합도를 느슨하게 만들기 위해서는 인스턴스 변수의 타입을 추상 클래스나 인터페이스로 선언하는 것만드로는 부족하다. 클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야만 한다.

@은딩

P270

이것은 setter 메서드를 사용하는 방식과 메서드 인자를 사용하는 방식의 경우에도 동일하다. 모든 경우에 의존성은 명시적으로 퍼블릭 인터페이스에 노출된다. 이를 명시적인 의존성(explicit dependency) 이라고 부른다.

@은딩

P271

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

@은딩

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

@은딩

의존성은 명시적으로 표현돼야 한다. 의존성을 구현 내부에 숨겨두지 마라. 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러다는 설계다.

@freebear @은딩 @vincent

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

@vincent

하지만 안타깝게도 new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다.

@은딩

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

@은딩

P272

결합도가 높으면 변경에 의해 영향을 받기 쉬워진다.

@은딩

P273

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

@은딩 @vincent

이를 위해 Movie는 외부로부터 이미 생성된 AmountDiscountPolicy의 인스턴스를 전달받아야 한다.

@은딩

P274

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

@은딩 @vincent

클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있다.

@은딩

P275

이 예는 설계가 트레이드오프 활동이라는 사실을 다시 한번 상기시킨다. 여기서 트레이드오프의 대상은 결합도와 사용성이다.

@freebear @은딩

P276

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

@은딩

따라서 의존성에 의한 영향이 적은 경우에도 추상화에 의존하고 의존성을 명시적으로 드러내는 것은 좋은 설계 습관이다.

@freebear

P279

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

@freebear

설계를 유연하게 만들 수 있었던 이유는 Movie가 DiscountPolicy라는 추상화에 의존하고, 생성자를 통해 DiscountPolicy에 대한 의존성을 명시적으로 드러냈으며, new와 같이 구체 클래스를 직접적으로 다뤄야 하는 책임을 Movie 외부로 옮겼기 때문이다.

@은딩

P281

그리고 지금까지 설명한 것처럼 이런 설계를 창조하는 데 있어서의 핵심은 의존성을 관리하는 것이다.

@freebear

따라서 시스템에 포함된 객체의 구성을 변경해(절차적인 코드를 작성하기보다는 인스턴스 추가나 제거 또는 조합을 달리해서) 시스템의 작동 방식을 바꿀 수 있다. 이러한 객체 구성을 관리할 목적으로 작성하는 코드를 객체 네트워크의 행위에 대한 선언적인 정의라고 한다. 시스템을 이런 방식으로 구축하면 방법(how)이 아니라 목적(what)에 집중할 수 있어 시스템의 행위를 변경하기가 더 쉽다[Freeman09].

@vincent

6개의 좋아요

좋은 글 감사합니다. :+1:
이해하기 쉽고 다양한 의견이 있으며, 설명하거나 면접에 도움이 많이 될 것 같네요
문서는 문체가 딱딱하고 이해하는데 시간이 오래 걸려요. :face_with_spiral_eyes:

3개의 좋아요