Skip to main content
soso01 blog
  1. Posts/

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

·5 mins

서론 #

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

01. 의존성 이해하기 #

변경과 의존성 #

  • 어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 생긴다.
  • 의존성은 실행 시점과 구현 시점에 다른 의미를 가진다.
    • 실행 시점 : 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 한다.
    • 구현 시점 : 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경된다.
  • 의존성은 단방향이다.
  • 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미한다.

의존성 전이 #

  • 의존성은 전이 될 수 있다.
  • 의존성은 함께 변경될 수 있는 가능성을 의미하며, 실제로 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라진다.

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

  • 런타임 : 실행되는 시점

  • 컴파일 : 코드를 컴파일하는 시점이거나 문맥에 따라서는 코드 그 자체를 가리킴

  • 런타임 의존성이 다루는 주제는 객체사이의 의존성이며, 코드 관점에서 주인공은 클래스이다.

  • 여기서 중요한 것은 런타임 의존성과 컴파일 의존성이 다를 수 있다는 것이다.

  • 앞선 예제에서 DiscountPolicy와 같은 추상클래스와의 의존성이 컴파일 의존성이고, PercentDiscountPolicy, AmountDiscountPolicy와 같은 클래스 인스턴스와의 의존성은 런타임임.

  • 어떤 클래스의 인스턴스가 다양한 클래스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안된다.

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

컨텍스트 독립성 #

  • 클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서는 사용하기 어려워진다.
  • 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트 독립성이라고 부른다.
  • 설계가 유연해지기 위해서는 가능한 한 자신이 실행될 컨텍스트에 대해 구체적인 정보를 최대한 적게 알아야 한다.
    • 컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용될 수 있기 때문이다.
    • 결과적으로 설계는 더 유연해지고 변경에 탄력적으로 대응할 수 있게 될 것이다.

의존성 해결하기 #

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

02. 유연한 설계 #

의존성과 결합도 #

  • 의존성은 객체들의 협력을 가능하게 만들지만, 과하면 문제가 될 수 있다.
    • 예시 - 객체 인스턴스에 직접 의존
    • 이 경우 의존성의 정도가 문제다. 꼭 특정 객체가 아니라 어떤 타입과의 객체와도 협력할 수 있도록 만들어야 함.
  • 바람직한 의존성이란? 재사용과 관련이 있다.
    • 어떤 의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성을 바람직하지 못한 것이다.
    • 컨텍스트에 독립적인 의존성이 바람직하며, 특정한 컨텍스트에 강하게 결합된 의존성은 바람직 하지 않다.
    • 특정 컨텍스트에 강하게 의존하는 클래스를 다른 컨텍스트에서 재사용할 수 있는 유일한 방법은 구현을 변경하는 것 뿐이다.
  • 개발 커뮤니티에서 바람직 하지 않은 의존성을 가리키는 다른 용어는 결합도이다.
    • 두 요소 사이에 의존성이 바람직하다면 ‘느슨한 결합도’ 또는 ‘약한 결합도’라 부르고, 바람직 하지 못한다면 ‘단단한 결합도’, ‘강한 결합도’라고 부른다.

지식이 결합을 낳는다. #

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

추상화에 의존하라 #

  • 추상화란 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다.
  • 추상화를 사용하면 현재 다루고 있는 문제를 해결하는 데 불필요한 정보를 감출 수 있다.
    • 따라서 대상에 대해 알아야 하는 지식의 양이 줄어들고 결합도는 느슨하게 유지된다.
  • 추상화 결합도 관점에서 의존 대상을 다음과 같이 구분하면 유용하다
    • 구체 클래스 의존성
    • 추상 클래스 의존성
    • 인터페이스 의존성
  • 아래로 갈 수 록 알아야 하는 지식이 줄어든다.
  • 의존하는 대상이 더 추상적일수록 결합도는 더 낮아진다.

명시적인 의존성 #

  • 느슨한 결합도를 위해서는 클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야 한다.
  • 어떻게? 의존성을 해결하기 위해 추상클래스를 상속받거나 인터페이스를 실체화한 구체 클래스를 생성자, setter, 메서드 인자를 이용하여 전달하는 것이다.
  • 의존성의 대상을 생성자의 인자로 전달받는 방법과 생성자 안에서 직접 생성하는 방법 사이의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 설정할 수 있는 방법을 제공하는지 여부이다.
    • 의존성을 명시적으로 퍼블릭 인터페이스에 노출하면 이를 명시적 의존성이라 부른다.
    • 클래스 생성자 내에서 다른 객체를 직접 생성하는 경우 처럼 인터페이스에 드러나지 않는다면 숨겨진 의존성이라고 부른다.
  • 의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수 밖에 없다.
    • 커다란 클래스에 정의된 긴 메서드 내부 어딘가에서 인스턴스를 생성하는 코드를 파악하는 것은 쉽지 않다.
    • 더 큰 문제는 다른 컨텍스트에서 재사용하기 위해서는 내부 구현을 직접 변경해야 한다는 것이다.
  • 의존성은 명시적으로 표현돼야 한다.
    • 의존성을 구현 내부에 숨겨두지 마라.
    • 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 선택할 수 있다.

new는 해롭다. #

  • new를 잘못쓰면 클래스 사이의 결합도가 극단적으로 높아진다.

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

  • 사용과 생성의 책임을 분리하면 설계를 유연하게 만들 수 있다.

  • 구 출발은 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮기는 것에서 시작한다.

가끔은 생성해도 무방하다 #

  • js로 치면 defaultValue 설정과 같은 내용

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

  • 의존성이 불편한 이유는 그것이 항상 변경에 대한 영향을 암시하기 때문이다.
  • 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다.

컨텍스트 확장하기 #

  • DiscountPolicy 추상클래스의 구체클래스로 NoneDiscountPolicy, OverlappedDiscountPolicy 를 구현하여 할인 정책이 없을 때, 중복 적용이 가능할 때 처리가능하도록 할인정책 컨텍스트 확장

조합 가능한 행동 #

  • 다양한 종류의 할인 정책이 필요한 컨텍스트에서 Movie를 재사용할 수 있는 이유는 코드를 직접 수정하지 않고도 협력 대상인 DiscountPolicy 인스턴스를 교체할 수 있기 때문이다.
  • 어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징이다.
    • 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써 애플리케이션의 기능을 쉽게 확장할 수 있다.
  • 유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계다.
    • 훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다.
    • 이의 핵심은 의존성을 관리하는 것