본문 바로가기

기술서적 정리/오브젝트 - 코드로 이해하는 객체지향 설계

08 CHAPTER 의존성 관리하기

https://wikibook.co.kr/object/

 

오브젝트: 코드로 이해하는 객체지향 설계

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라! 객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두 번째 걸음은 객체를

wikibook.co.kr

 

 

객체지향 설계의 핵심은 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거하는 데 있다.

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




◈ 의존성 이해하기

변경과 의존성

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

1) 실행시점에는 실행 시에 의존 대상 객체가 반드시 존재해야한다.

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

 

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

의존성은 방향성을 가지며 항상 단방향이다.

설계와 관련된 대부분의 용어들이 변경과 관련이 있다.
의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미한다.

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



의존성 전이

의존하는 대상에 대해서도 자동적으로 의존하게 되는 것.

 

모든 경우에 의존성이 전이되는 것은 아니다.

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



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

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

런타임 의존성이 다루는 주제는 객체 사이의 의존성이다.

 

반면 코드 관점에서 주인공은 클래스다.

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

 

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

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



비슷한 맥락의 일을 하는 클래스 둘 모두에 의존하도록 만드는 것은 좋은 방법이 아니다.

이것은 전체적인 결합도를 높일뿐만 아니라 비슷한 맥락의 새로운 일을 추가하기 어렵게 만들기 때문이다.

 

이를 해결할 방법은 두 클래스 모두를 포괄하는 추상 클래스에 의존하도록 만들어 이 컴파일타임 의존성을 실행시 인스턴스에 대한 런타임 의존성으로 대체하는 방법이다.

 

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

협력할 객체의 클래스를 명시적으로 드러내고 있다면 다른 클래스의 인스턴스와 협력할 가능 자체가 없어진다.

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



컨텍스트 독립성

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

 

클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다.

이를 컨텍스트 독립성이라고 부른다.

 

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

 

클래스가 실행 컨텍스트에 독립적인데도 어떻게 런타임에 실행 컨텍스트에 적절한 객체들과 협력할 수 있을까?



의존성 해결하기

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

의존성을 해결하기 위한 일반적인 3가지 방법.

1) 객체를 생성하는 시점생성자를 통해 의존성을 해결.

2) 객체 생성 후 setter 메서를 통해 의존성 해결.

3) 메서드 실행 시 파라미터를 이용해 의존성 해결.

 

setter 메서드를 이용하는 방식은 객체를 생성한 이후에도 의존하고 있는 대상을 변경할 수 있는 가능성을 열어 놓고 싶은 경우에 유용하다.

 

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

단점은 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할 수 있다. 이럴 경우 널포인터익셉션이 발생할 가능성이 존재한다.

 

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

1) 항상 객체를 생성할 때 의존성을 해결해서 완전한 상태의 객체를 생성한 후.

2) 필요에 따라 setter 메서드를 이용해 의존 대상을 변경할 수 있게 할 수 있다.



메서드 인자를 사용한 방식은. 1)메서드가 실행디는 동안만 일시적으로 의존 관계가 존재해도 무방하거나, 2)메서드가 실행될 때마다 의존 대상이 매번 달라져야 하는 경우유용하다.

 

하지만 클래스의 메서드를 호출하는 대부분의 경우에 매번 동일한 객체를 인자로 전달하고 있다면 생성자를 이용하는 방식이나 setter 메서드를 이용해 의존성을 지속적으로 유지하는 방식으로 변경하는 것이 좋다.




 

◈ 유연한 설계

의존성과 결합도

의존성은 협력을 위해 반드시 필요한 것이다.

 

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

 

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

 

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



자식이 결합을 낳는다.

더 많이 알수록 더 많이 결합된다.

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

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

이 목적을 달성할 수 있는 가장 효과적인 방법은 ‘추상화’이다.



추상화에 의존하라.

구체적인 대상이 아닌 추상화에 의존하면 결합도가 너 느슨해진다.

 

구체 클래스의 의존성, 추상 클래스의 의존성, 인터페이스 의존성 순으로 클라이언트가 알아야하는 지식의 양이 적어진다.

 

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



명시적인 의존성

런타임에 구체 클래스의 인스턴스와 협력해야 하는데 어떤 인스턴스인지 알려줄 수 있는 방법이 필요하다.

 

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

 

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

 

인자로 전달받는 방법명시적인 의존성이라고 하고 내부에서 인스턴스를 직접 생성하는 방식을 숨겨진 의존성이라고 부른다.

 

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

 

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



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

의존성을 내부에 숨겨두지 마라.

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

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

 

경계해야 할 것은 의존성 자체가 아니라 의존성을 감추는 것이다.

숨겨져 있는 의존성을 밝은 곳으로 드러내서 널리 알려라.

그렇게 하면 설계가 유연하고 재사용 가능해질 것이다.



new는 해롭다.

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

 

결합도 측면에서 new가 해로운 이유는 크게 두 가지다.

1) new연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야하기에 추상화가 아닌 구체 클래스에 의존할 수밖에 없어진다. 결국 결합도가 높아지는 결과를 초래한다.

2) 생성하는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다. 따라서 클라이언트가 알아야하는 지식의 양이 늘어나기 때문에 결합도가 높아진다.

 

결론적으로 new를 사용하면.

1) 구체 클래스에 결합.

2) 인스턴스를 생성하기 위해 어떤 인자들이 필요하고 그 인자들을 어떤 순서로 사용해야 하는지에 대한 정보도 노출.

3) 인자로 사용되는 구체 클래스에 대한 의존성을 추가.



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

 

인스턴스를 외부에서 생성하고 사용하려면 인스턴스를 전달받아야한다.

외부에서 인스턴스를 전달받는 방법은 앞에서 살펴본 의존성을 해결 방법과 동일하다.

1) 생성자의 인자로 전달.

2) setter메서드를 사용.

3) 실행 시에 메서드의 인자로 전달.



설계를 유연하게 만들 수 있는 방법.

1) 사용과 생성의 책임을 분리.

2) 의존성을 생성자에 명시적으로 드러낸다.

3) 구체 클래스가 아닌 추상 클래스에 의존한다.

 

그리고 이러한 방법의 출발은 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮기는 것에서 시작했다.



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

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

주로 협력하는 기본 객체를 설정하고 싶은 경우가 여기에 속한다.

 

기본 객체를 생성하는 생성자를 추가하고 이 생성자에서 특정 인스턴스를 인자로 받는 생성자를 체이닝한다.

 

첫 번째 생성자의 내부에서 두 번째 생성자를 호출한다.

생성자가 체인처럼 연결되는 것이다.

이렇게 되면 클라이언트는 대부분의 경우에 추가된 간략한 생성자를 통해 특정 인스턴스와 협력하게 하면서도 컨텍스트에 적절한 인스턴스로 의존성을 교체할 수도 있다.



조합 가능한 행동.

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

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

 

유연하고 재사용 가능한 설계는 객체들의 조합을 통해 무엇을 하는지를 표현하는 클래스들로 구성된다.

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

코드에 드러난 로직을 해석할 필요 없이 객체가 어떤 객체와 연결됐는지를 보는 것만으로도 객체의 행동을 쉽게 예상하고 이해할 수 있기 때문이다.



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

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

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