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

조재훈·2024년 6월 10일

8장. 의존성 관리하기

잘 설계된 객체지향 애플리케이션은 초점이 명확하고 한 가지 일만 잘하는 객체들의 모임으로 구성된다. 시스템에 요구하는 기능들을 구현하기 위해 이 객체들은 서로 도움을 요청하고 응답하며 협력한다

협력은 객체가 다른 객체를 아는 것을 강요하며 객체 사이 의존성을 강화시킨다. 협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 변경하기 어렵게 만든다. 객체지향 설계의 핵심은 최소한의 의존성을 유지하여 변경을 쉽게 수용할 수 있는 애플리케이션을 만드는 것이다

01. 의존성 이해하기

변경과 의존성

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

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

어떤 객체가 작업을 정상적으로 수행하기 위해 다른 객체가 필요할 경우 두 객체 사이 의존성이 있다 말하고 의존성은 방향성을 가지며 항상 단방향이다

두 요소 사이 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미하며 의존성은 변경에 의한 영향의 전파 가능성을 암시한다

의존성 전이

의존성 전이가 의미하는 것은 클래스 A가 클래스 B에 의존할 경우 클래스 A는 클래스 B가 의존하는 대상에 대해서도 자동적으로 의존하게 된다는 것이다

의존성은 함께 변경될 가능성을 의미하기에 모든 경우 의존성이 전이 되는 것이 아니라 실제로 전이될 지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라진다

  • 의존성의 종류
    • 직접 의존성 : 한 요소가 다른 요소에 직접 의존하는 경우
    • 간접 의존성 : 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우

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

런타임과 컴파일 타임의 용어 설명은 넘어가겠다.

객체지향 애플리케이션에서 런타임의 주인공은 객체이고 코드 관점에서 주인공은 클래스이다. 런타임 의존성은 객체 간 의존성이고 컴파일타임 의존성은 클래스 간 의존성임

중요한 것은 런타임 의존성과 컴파일타임 의존성이 서로 다를 수 있다는 것. 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 의존성을 다르게 만들어야 한다(상속, 추상 클래스)

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

컨텍스트 독립성

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

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

의존성 해결하기

컴파일타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체해야 한다. 이를 의존성 해결이라고 부른다. 해결을 위해 세 가지 방법을 사용한다

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

setter 메서드를 이용하는 방식은 객체를 생성한 이후 의존하고 있는 대상을 변경할 수 있는 가능성을 열어 놓고 싶은 경우 유용함. 단점은 객체가 생성된 후 협력에 필요한 의존 대상을 설정하기 때문에 객체를 생성하고 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할 수 있다는 점임

좋은 방법으로 생성자 방식과 setter 방식을 혼합하는 것. 항상 객체를 생성할 때 의존성을 해결해서 완전한 상태 객체 생성 후, 필요에 따라 setter 메서드를 사용해 의존 대상을 변경하는 것

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

02. 유연한 설계

의존성과 결합도

의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서 바람직하지만 과도한 의존성은 문제가 될 수 있다

변경이 쉽지 않은 코드는 의존하는 것이 문제가 아니라 의존성의 정도가 문제가 된다. 클래스의 입장에서 협력할 객체의 클래스를 고정할 필요가 없다. 자신이 전송하는 메시지를 이해할 수만 있다면 어떤 타입의 객체와 협력하더라도 상관이 없다

바람직한 의존성이란 재사용성과 관련이 있으며 다양한 환경에서 의존성을 재사용할 수 있다면 그 의존성은 바람직한 것이다. 다시 말해 컨텍스트에 독립적인 의존성이 바람직한 의존성이다

다른 환경에서 재사용하기 위해 내부 구현을 변경하게 만드는 모든 의존서은 바람직하지 않은 의존성이다. 어떤 두 요소 사이 존재하는 의존성이 바람직할 때 두 요소가 느슨한(약한) 결합도를 가진다고 말하며 반대는 단단한(강한) 결합도를 가진다고 말한다

지식이 결합을 낳는다

결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정된다. 많이 알수록 결합도가 강해진다

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

추상화에 의존하라

추상화란 어떤 대상을 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다

일반적으로 추상화와 결합도 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용하다. 다음 목록에서 아래로 갈수록 클라이언트가 알아야 하는 지식의 양이 적어져 결합도가 느슨해진다

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

추상 클래스는 내부 구현과 자식 클래스 종류를 클라이언트에게 숨길 수 있지만 추상 클래스의 클라이언트는 여전히 협력하는 대상이 속한 클래스 상속 계층이 무엇인지에 대해선 알고 있어야 한다

인터페이스에 의존하면 상속 계층을 모르더라도 협력이 가능해지며 협력하는 객체가 어떤 메시지를 수신할 수 있는지에 대한 지식만을 남기기 때문에 추상 클래스 의존성보다 결합도가 낮다

명시적인 의존성

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

방법으로 인스턴스 변수의 타입은 추상 클래스나 인터페이스로 정의하고 생성자, setter, 메서드 인자로 의존성을 해결할 대는 추상 클래스를 상속받거나 인터페이스를 실체화한 구체 클래스를 전달하는 것이다

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

인자에 전달하는 것은 의존성이 명시적으로 퍼블릭 인터페이스에 노출되는 것이며 이를 명시적인 의존성이라고 부른다

반면 내부에서 직접 구체적인 타입을 생성하는 방식은 클래스가 다른 클래스에 의존한다는 사실을 감추며 숨겨진 의존성이라고 부른다

의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴봐야 한다. 그리고 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다. 이는 버그로 이어질 수 있다

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

new는 해롭다

언어에서 클래스의 인스턴스를 생성할 수 있는 new 연산자를 제공하는데 new를 잘못 사용하면 클래스 간 결합도가 극단적으로 높아진다. new가 해로운 이유는

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

new는 결합도를 높이기 때문에 해롭다. 클래스를 구체 클래스에 결합시키는 것만으로 끝이 아니라 협력할 클래스의 인스턴스를 생성하기 위해 어떤 인자들이 필요하고 그 인자들을 어떤 순서로 사용해야 하는지에 대한 정보도 노출시킬뿐만 아니라 인자로 사용되는 구체 클래스에 대한 의존성을 추가한다

해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것이다. 클래스 B를 사용하는 클래스 A를 생성하는 것이 아니라 단지 해당하는 인스턴스를 사용하기만 해야 한다. 이를 위해 클래스 A는 외부로부터 이미 생성된 B의 인스턴스를 전달받아야 한다

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

가끔은 생성해도 무방하다

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

대부분 클래스 A가 클래스 B의 인스턴스와 협력하고 가끔만 C의 인스턴스와 협력한다고 가정해보면 모든 경우에 인스턴스를 생성하는 책임을 클라이언트로 옮기면 클라이언트 사이 중복 코드가 늘어나고 사용성이 나빠질 것임

이를 해결하기 위해 기본 객체를 생성하는 생성자를 추가하고 생성자에서 추상 클래스의 인스턴스를 인자로 받는 생성자를 체이닝하는 것임(p275)

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

의존성이 불편한 이유는 변경에 대한 영향을 암시하기 때문인데 따라서 변경될 확률이 매우 적은 클래스라면 의존성이 문제 되지 않는다(표준 라이브러리)

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

컨텍스트 확장하기

예외 케이스를 처리하기 위해 코드 내부를 직접 수정하는 것은 버그의 발생 가능성을 높이는 것임(if로 처리)

자식 클래스를 추가해 그 인스턴스를 추상 클래스로 전달한다

조합 가능한 행동

유연하고 재사용 가능한 설계는 객체가 어떻게 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇을 하는지를 표현하는 클래스들로 구성된다. 따라서 클래스의 인스턴스를 생성하는 코드를 보는 것만으로 객체가 어떤 일을 하는지를 쉽게 파악할 수 있다

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

profile
나태지옥

0개의 댓글