You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
잘 설계된 애플리케이션은 작고 응집도 높은 객체들로 구성된다. 작고 응집도가 높은 객체란 책임의 초점이 명확하고 한 가지 일만 잘하는 객체를 의미한다. 이런 작은 객체들은 단독으로 수행할 수 있는 작업이 거의 없기 때문에 다른 객체의 도움이 필요하다. 이런 과정에서 협력을 낳는다.
객체지향 세계에서 협력은 필수적이지만, 설계를 곤경에 빠뜨릴 수 있다. 협력은 객체가 다른 객체에 대해 알아야 함을 강조한다. 즉, 다른 객체와 협력하기 위해서는 그런 객체가 존재한다는 사실을 알아야 하며 이런 지식들이 객체 사이의 의존성을 만든다.
협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다. (버그 가능성도 높아짐) 객체지향 설계의 핵심은 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거하는데 있다.
쉽게 스파게티 코드가 가장 협력을 많이 하는 코드가 아닐까? 실제 개개인이 자율성을 가지고 하는 작업이 효율성이 제일 잘 나오는 것처럼 코드도 관리자 주도형과 같이 하나의 컨트롤러, 매니저가 모든 일을 담당하여 처리하면 효율성과 작업의 퀄리티가 떨어질 수 있다. 이런 부분에서는 객체지향이 일상세계와 많이 닮아있다는 생각이 든다.
의존성 이해하기
변경과 의존성
어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 생기게 된다. 의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가진다.
실행 시점: 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시에 의존 대상 객체가 반드시 존재해야 한다.
정리하면 실행 시점에 PeriodCondition의 인스턴스가 정상적으로 동작하기 위해서는 Screening의 인스턴스가 존재해야 한다. 만약 Screening의 인스턴스가 존재하지 않거나 GetStartTime메시지를 이해할 수 없다면 PeriodCondition의 IsSatisfiedBy메서드는 정상적으로 동작하지 않는다.
이처럼 어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 객체 사이에 의존성이 존재한다고 한다. 의존성은 방향성을 가지며 항상 단방향이다.
의존성은 존재해야 한다. 객체 사이의 메시지를 전달하기 위해서는 필연적이다. 하지만 그 의존성을 유연하게 관리한다는 것은 다른 이야기이다. 두 객체의 메시징을 상위 객체에서 델리게이트로 연결하거나 인터페이스에 DI를 통해 주입하여 사용하는 등 매우 느슨한 결합도 결국은 결합이다.
두 요소 사이의 의존성은 의존되는 요소가 변경될 수 있다는 것을 의미한다. 따라서 의존성 변경에 의한 영향의 전파 가능성을 암시한다.
의존성 전이
의존성은 전이될 수 있다. Screening의 코드를 살펴보면 Screening이 Movie, LocalDataTime, Customer에 의존한다는 사실을 알 수 있다. 의존성 전이(transitive dependency) 가 의미하는 것은 PeriodCondition이 의존하는 대상에 대해서도 자동적으로 의존하게 된다는 것이다. 다시 말해서 Sreening이 가지고 있는 의존성이 Screening에 의존하고 있는 PeriodCondition에 전이된다.
의존성은 함께 변경될 수 있는 가능성을 의미하기 때문에 모든 경우에 의존성이 전이되는 것은 아니다. (현재 코드와 같이 메서드 체이닝, 기차충돌의 예제를 생각해보면 된다.) 의존성이 실제로 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라진다.
Screening이 의존하고 있ㅈ는 어떤 요소의 구현이나 인터페이스가 변경되는 경우에 Screening이 내부 구현을 효과적으로 캡슐화하고 있다면 Screening에 의존하고 있는 객체들까지는 변경이 전파되지 않았을 것이다.
의존성은 전이될 수 있기 때문에 의존성의 종류를 **직접 의존성(direct dependency)**과 **간접 의존성(indirect dependency)**로 나누기도 한다. 직접 의존성이란 말 그대로 한 요소가 다른 요소에 직접 의존하는 경우를 가리킨다. 이 경우 코드에 명시적으로 드러난다. 간접 의존성이란 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우를 가리킨다.
여기서는 되게 미시적인 클래스를 예로 설명했지만 변경과 관련이 있는 어떤 것에도 의존성이라는 개념을 적용할 수 있다. 의존성의 대상은 객체일 수도 있고 모듈이나 더 큰 규모의 실행 시스템일 수도 있다. 하지만 의존성의 본질은 변하지 않는다. 의존성이란 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이다.
런타임 의존성과 컴파일타입 의존성
의존성과 관련해서 다뤄야 하는 또 다른 주제는 **런타임 의존성(run-time dependency)과 컴파일타임 의존성(compile-time dependency)**의 차이다.
런타임은 말 그대로 애플리케이션이 실행되는 시점을 의미하고 컴파일 타임은 약간 추상적으로 다룬다. 실제 작성된 코드를 컴파일하는 시간을 말하기도 하지만, 문맥에 따라서는 코드 그 자체를 가리키기도 한다. 컴파일타임 의존성은 시간이 아니라 작성한 코드의 구조이기 때문에 런타임 의존성과는 다르다.
실제로 컴파일 타임이라는 용어는 많이 혼합해서 사용하기에 문맥에서 말하고자 하는 것을 파악하는게 중요하다.
객체지향 애플리케이션에서 런타임의 주인공은 객체이다. 따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성이다. 반면 코드 관점에서 주인공은 클래스이다. 따라서 컴파일 타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.
동적 모델과 정적 모델의 차이라고 생각하면 될 것 같다. 동적 모델은 실행 시점에 객체들이 어떻게 동작하는지를 보여주고 정적 모델은 코드를 보고 어떻게 동작하는지를 보여준다.
앞서 다룬 DiscountPolicy라는 역할에 대해서 Movie는 해당 역할에만 의존성을 가지지 해당 추상클래스/인터페이스의 구현체에 대한 의존성을 가지지 않는다. 하지만 이는 컴파일 타임에 해당하는 이야기고 런타임에는 달라진다. 실제 런타임의 모델은 Movie마다 의존성 주입을 통해 넣어준 DiscountPolicy의 구현체에 의존하게 된다.
어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 된다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 한다. 클래스가 협력할 객체를 명시적으로 드러내고 있다면 다른 클래스의 인스턴스와 협력할 가능성 자체가 없어진다. 따라서 컴파일타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용 가능해진다.
객체지향의 자율적인 객체, 협력의 구조는 추상적일수록 강화가 되는데 그 구조는 재귀적일 수 있다. 해당 로직을 담고 있는 모듈이 있고 그 모듈끼리의 또 다른 협력과 그 협력 관계를 담고 있는 모듈로 계속해서..
컨텍스트 독립성
구체 클래스에 대해 의존하는 것은 클래스의 인스턴스가 어떤 문맥에서 사용될 것인지를 구체적으로 명시하는 것과 같다. 클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서 사용하기는 더 어려워진다. (해당 문맥에 강하게 결합되기 때문에) 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트 독립성이라고 한다.
설계가 유연해지기 위해서는 가능한 한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다. 컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용될 수 있기 때문이다.
의존성 해결하기
컴파일타임 의존성은 구체적인 런타임 의존성으로 대체돼야 한다. 의존성을 해결하기 위해서는 일반적으로 다음과 같은 세 가지 방법을 이용한다.
객체를 생성하는 시점에 생성자를 통해 의존성 해결 (기본적인 DI)
객체 생성 후 setter 메서드를 통해 의존성 해결
메서드 실행 시 인자를 이용해 의존성 해결
대부분은 생성자를 통해서 의존성을 주입하는데, 권장하는 방법은 생성자를 통해 기본적으로 사용할 객체를 주입하여 주고(Default) 이후에 setter를 통해 변경할 수 있도록 하는 것이다. 이렇게 하면 객체의 불변성을 유지하면서도 유연하게 변경할 수 있다.
이 말은 결국은 어디선가는 이 것을 담당하도록 해야한다는 것인데, 이를 DI프레임워크를 사용해 컨테이너에 위임할 수도 있고, 그냥 코드 처럼 상위 오브젝트가 생성하여 할당할 수도 있고,, 데이터 베이스나 서버로 처리할 수 있다. 중요한 것은 객체 끼리의 협력에서 의존성이 줄어든다는 것이다.
유연한 설계
설계를 유연하고 재사용 가능하게 만들기로 결정했다면 의존성을 관리하는 데 유용한 몇 가지 원칙과 시법을 익힐 필요가 있다.
의존성과 결합도
객체지향 패러다임은 근간은 협력이다. 객체들은 협력을 통해 애플리케이션에 생명력을 불어넣는다. 객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야 한다. 이런 자식들이 객체 사이의 의존성을 낳는다. 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서는 바람직한 것이다. 하지만 의존성이 과하면 문제가 될 수 있다.
의존성이 과하거나 너무 강하게 결합되어 있다면 이는 다른 객체와의 협력이 단절됨을 의미한다. 따라서 코드에서 다루든 추상화를 통해 다형성을 이용하여 의존성을 바람직하게 만들어야 한다. 의존성 자체는 나쁜 것이 아니며 협력하기 위헤선 반드시 필요하다. 바람직한 의존성은 결국 재사용성과 관련이 있다.
이런 의존성을 다루는 좀 더 세련된 용어로 결합도가 있다. 두 요소 사이에 존재하는 의존성이 바람직할 때 두 요소가 **느슨한 결합도(loose coupling)**를 가진다고 말한다. or 약한 결합도(weak coupling) 반대로 두 요소 사이에 의존성이 바람직하지 못할 때 단단한 결합도(strong coupling) 또는 **강한 결합도(tight coupling)**라고 한다.
지식이 결합을 낳는다
결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정된다. 한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다. 반대로 한 요소가 다른 요소에 대해 더 적은 정보를 알고 있을수록 두 요소는 느슨하게 결합된다.
서로에 대해 알고 있는 정도를 지식의 양이라고 표현할 수 있으며 결국 더 많이 알수록 더 많이 결합된다. 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용이 가능하다는 것을 의미한다. 기존 지식에 어울리지 않는 컨텍스트에서 클래스의 인스턴스를 사용하기 위해서 할 수 있는 유일한 방법은 클래스를 수정하는 것뿐이다.
결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야 하며, 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하다. 이에 대한 가장 효과적인 방법은 바로 추상화다.
추상화에 의존하라
추상화란 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다. 추상화를 사용하면 현재 다루고 있는 문제를 해결하는 데 불필요한 정보를 감출 수 있다. 따라서 대상에 대해 알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있다.
일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용하다.
구체 클래스 의존성
추상 클래스 의존성
인터페이스 의존성
아래로 갈수록 알아야 하는 지식의 양이 줄어들기 때문에 결합도 또한 느슨해진다.
구체 클래스에 비해 추상 클래스는 메서드의 내부 구현과 지식 클래스의 종류에 대한 지식을 클라이언트에게 숨길 수 있다. 따라서 클라이언트가 알아야 하는 지식의 양이 더 적기 때문에 구체 클래스보다 추상 클래스에 의존하는 것이 결합도가 더 낮다. 같은 이유로 인터페이스에 의존하는 것이 추상 클래스에 의존하는 것보다 결합도가 더 낮다.
여기서 중요한 것은 실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록 결합도가 낮아진다는 것이다. 결합도를 느슨하게 만들기 위해서는 구체적인 클래스보다 추상 클래스에, 추상 클래스보다 인터페이스에 의존하도록 만드는 것이 더 효과적이다.
더 효과적이긴 하나, 정답은 아니다. 경우에 따라 추상클래스로의 다형성을 보여줘야 할 때도 있다.
명시적인 의존성
결합도를 느슨하게 만들기 위해서는 인스턴스 변수의 타입을 추상 클래스나 인터페이스로 선언하는 것만으로는 부족하다. 클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야만 한다. 하지만 런타임에는 직접적으로 협력해야 하기 때문에 이를 해결할 방법이 필요하다.
의존성을 해결하는 방법 중 하나는 의존성 주입이다. 의존성 주입은 클래스가 사용할 의존성을 클래스 외부에서 제공하도록 만드는 것이다. 이를 통해 클래스는 자신이 사용할 의존성을 직접 선택할 수 있게 된다.
의존성의 대상을 생성자의 인자로 전달받는 방법과 생성자 안에서 직접 생성하는 방법 사이의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 다형성을 제공할 수 있는지의 여부이다. 퍼블릭 인터페이스를 통해 의존성을 명시적으로 노출시키는 것을 **명시적인 의존성(Explicit Dependency)**라고 한다.
반면 생성자 내부에서 new를 통해 직접 생성하는 방식은 Movie가 DiscountPolicy에 의존한다는 사실을 감춘다. 다시 말해 의존성이 퍼블릭 인터페이스에 표현되지 않는다. 이를 **숨겨진 의존성(Hidden Dependency)**라고 한다.
의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수밖에 없다. 커다란 클래스에 정의된 긴 메서드 내부 어딘가에서 인스턴스를 생성하는 코드를 파악하는 것은 쉽지 않을뿐더러 심지어 고통스러울 수도 있다.
더 커다란 문제는 의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다는 것이다. 코드 수정은 언제나 잠재적으로 버그의 발생 가능성을 내포한다. 의존성을 명시적으로 드러내면 코드를 직접 수정해야 하는 위험을 피할 수 있다. 실행 컨텍스트에 적절한 의존성을 선택할 수 있기 때문이다.
정리하면 의존성은 명시적으로 표현되어야 하며, 내부에 숨기면 안된다. 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다. 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성으로 교체할 수 있다.
의존성을 감추지 마라!
new는 해롭다
대부분의 언어에서는 클래스의 인스턴스를 생성할 수 있는 new연산자를 제공한다. 하지만 안타깝게도 new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다. 결합도 측면에서 new가 해로운 이유는 크게 두 가지다.
new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 한다. 따라서 new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존할 수밖에 없기 때문에 결합도가 높아진다.
new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 이용해 클래스의 생성자를 호출해야 하는지도 알아야 한다. 따라서 new를 사용하면 클라이언트가 알아야 하는 지식의 양이 늘어나기 때문에 결합도가 높아진다.
구체 클래스에 직접 의존하면 결합도가 높아진다는 사실을 기억하자. 결합도의 관점에서 구체 클래스는 협력자에게 너무 많은 지식을 알도록 강요한다. 여기에 new는 문제를 더 크게 만든다. 클라이언트는 구체 클래스를 생성하는 데 어떤 정보가 필요한지에 대해서도 알아야 하기 때문이다.
이에 대한 해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것이다.Movie는 단순하게 할인 조건을 받아서 사용하기만 해야 한다. (직접 생성 x) 이를 위해 외부에서 이미 생성된 인스턴스를 추상화를 통해 주입해주는 것이다.
사용과 생성의 책임을 분리해서 결합도를 낮추면 설계를 유연하게 만들 수 있다. 의존성을 명시적으로 드러내고, 구체 클래스가 아닌 추상 클래스에 의존하게 함으로 설계를 유연하게 만들자. 그리고 그 출발은 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮기는 것에서 시작했다는 점을 기억하자.
같은 맥락으로 모든 디커플리은 책임의 분리이다. 다른 아키텍처나 로직이 복잡해지고 클래스가 방대해진다면 책임을 분리하자.
간혹 DI에 대해서 너무 어렵게 생각하는 것 같은데,(나도 어려웠다. ㅎㅎ) 하지만 단순하게 매개변수에 구체화된 클래스를 전달 받아 사용하는 것으로 사용하는 객체는 실제 코드에서 추상화된 참조 변수를 통해 해당 메모리에 접근하여 사용하는 것이다. 이것만 알아도 충분하다.
가끔은 생성해도 무방하다
클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있다. 주로 협력하는 객체를 설정하고 싶을 경우가 여기에 속한다. 즉, 다형성이 발생하지 않는 부분에 대해서는 의존성을 가지기도 한다. 책에선 생성자를 체이닝하는 방법으로 나타내며 실제 C#에서는 명명된 매개변수의 형태로도 많이 나타낸다.
결국 트레이드오프 활동으로 이어지게 되고 오버로딩, 생성자 체이닝 등도 결국 결합도와 사용성을 고민해야 한다는 것이다. 그럼에도 가급적이면 구체 클래스에 대한 의존성을 제거할 수 있는 방법을 찾아야 한다. 종종 모든 결합도가 모이는 새로운 클래스를 추가함으로써 사용성과 유연성이라는 두 마리 토끼를 모두 잡을 수 있다.
표준 클래스에 대한 의존은 해롭지 않다
의존성이 불편한 이유는 그것이 항상 변경에 대한 영향을 암시하기 때문이다. 따라서 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다.
어떻게 생각해보면 언어끼리의 차이도 의존성이라고 할 수 있다.
컨텍스트 확장하기
Null 객체 패턴, 특정한 상황에 대한 처리를 위한 패턴이다. 이 패턴은 객체가 null을 반환하는 대신에 특별한 객체를 반환하도록 만들어서 클라이언트가 null을 반환하는 객체를 처리하는 코드를 작성하지 않도록 한다.
이런 유연한 설계가 가능한 이유도 조금만 생각해보면 변경에 용이할 수 있도록 직접 설계했기 때문이다. 다시 정리하자면 사용성과 유연성을 생각하고 설계하자. 시퀀스형태의 디자인도 마찬가지로 컴포지션 패턴처럼 유연하게 설계가 가능하다.
조합 가능한 행동
다양한 종류의 할인 정책이 필요한 컨텍스트에서 Movie를 재사용할 수 있었던 이유는 코드를 직접 수정하지 않고도 협력 대상인 DiscountPolicy인스턴스를 교체할 수 있었기 때문이다. 즉, Moive라는 클래스는 하나이지만, Movie라는 객체는 매우 다양한 객체로써 협력이 가능해진다. 이것이 의존성을 낮춰야 하는 이유이다.
인터페이스의 엄청난 장점이며, 사용해야 하는 이유이기도 하다. Movie자체가 DiscountPolicy라는 추상화에 의존하고 생성자를 통해 의존성을 명시적으로 드러내었으며 new와 같이 구체 클래스를 직접적으로 다뤄야 하는 책임을 외부로 옮겼기 때문이다.
어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징이다. 유연하고 재사용 가능한 설계는 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써 애플리케이션의 기능을 쉽게 확장할 수 있다.
유연하고 재사용 가능한 설계는 객체가 어떻게 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇을 하는지를 표현하는 클래스들로 구성된다. 따라서 클래스의 인스턴스를 생성하는 코드를 보는 것만으로 객체가 어떤 일을 하는지 쉽게 파악할 수 있다. 코드에 드러난 로직을 해석할 필요 없이 객체가 어떤 객체와 연결됐는지를 보는 것만으로 객체의 행동을 쉽게 예상하고 이해할 수 있기 때문이다.
유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계다. 훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다. 그리고 지금까지 설명한 것처럼 이런 설계를 창조하는 데 있어서의 핵심은 의존성을 관리하는 것이다.
이것이 객체지향이 매력적인 이유다. 주어진 도메인에 매번 다른 형태의 설계가 나오기 때문에 지속적으로 성장할 수 있는 것 같다.
느낀점
지속해서 내 생각을 글에 적긴했지만, 결국 How가 아니라 What에 집중해야 한다는 말로 귀결되는 것 같다. 앞 1~6장까지 말한 책임을 따라야 하는 이유와 왜 그래야 하는지를 설계에서 의존성이 가지는 중요성을 설명한 것 같다.
누구나 다른 코드를 짜고 만들어내지만, 객체지향은 정말 단순하고 명확하지만, 어렵다. 아마 기존의 코드 스타일이나 사고방식 자체가 달라져야 하기 때문이 아닐까?
애자일과 연관이 되어 있듯이 우리는 쉽게 실제 건축하는 것과 같은 설계를 하려고 한다. 하지만 소프트웨어 개발에선 불확실성과 변경사항이 너무나 많기 때문에 이런 의존성을 낮추는 개발 방법이 필요하다. 그래서 사고방식의 전환이나 지속적인 경험이 중요한 것 같다. (폭포수 모델 -> 애자일)
논의사항
구체 클래스 의존성
추상 클래스 의존성
인터페이스 의존성
아래로 갈수록 알아야 하는 지식의 양이 줄어들기 때문에 결합도 또한 느슨해진다.
인터페이스와 추상클래스에 대한 논의도 많이 일어나고 있는 것으로 알고 있습니다. 과거에 본 글중에 "데이터를 중심으로 상속하고 싶다면 추상 클래스, 행위에 중심으로 상속하고 싶다면 인터페이스"라는 말을 들은 적이 있는데, 이런 말처럼 실제 여러분들이 작성하신 코드에서 어떤 방식으로 사용할 수 있는지/하는지 에 대해 논의해보면 좋을 것 같습니다.
The text was updated successfully, but these errors were encountered:
8장 의존성 관리하기
잘 설계된 애플리케이션은 작고 응집도 높은 객체들로 구성된다. 작고 응집도가 높은 객체란 책임의 초점이 명확하고 한 가지 일만 잘하는 객체를 의미한다. 이런 작은 객체들은 단독으로 수행할 수 있는 작업이 거의 없기 때문에 다른 객체의 도움이 필요하다. 이런 과정에서 협력을 낳는다.
객체지향 세계에서 협력은 필수적이지만, 설계를 곤경에 빠뜨릴 수 있다. 협력은 객체가 다른 객체에 대해 알아야 함을 강조한다. 즉, 다른 객체와 협력하기 위해서는 그런 객체가 존재한다는 사실을 알아야 하며 이런 지식들이 객체 사이의 의존성을 만든다.
협력을 위해서는 의존성이 필요하지만 과도한 의존성은 애플리케이션을 수정하기 어렵게 만든다. (버그 가능성도 높아짐) 객체지향 설계의 핵심은 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거하는데 있다.
쉽게 스파게티 코드가 가장 협력을 많이 하는 코드가 아닐까? 실제 개개인이 자율성을 가지고 하는 작업이 효율성이 제일 잘 나오는 것처럼 코드도 관리자 주도형과 같이 하나의 컨트롤러, 매니저가 모든 일을 담당하여 처리하면 효율성과 작업의 퀄리티가 떨어질 수 있다. 이런 부분에서는 객체지향이 일상세계와 많이 닮아있다는 생각이 든다.
의존성 이해하기
변경과 의존성
어떤 객체가 협력하기 위해 다른 객체를 필요로 할 때 두 객체 사이에 의존성이 생기게 된다. 의존성은 실행 시점과 구현 시점에 서로 다른 의미를 가진다.
PeriodCondition
클래스의IsSatisfiedBy
메서드는Screening
인스턴스에게GetStartTime
메시지를 전송한다.정리하면 실행 시점에
PeriodCondition
의 인스턴스가 정상적으로 동작하기 위해서는Screening
의 인스턴스가 존재해야 한다. 만약 Screening의 인스턴스가 존재하지 않거나GetStartTime
메시지를 이해할 수 없다면PeriodCondition
의IsSatisfiedBy
메서드는 정상적으로 동작하지 않는다.이처럼 어떤 객체가 예정된 작업을 정상적으로 수행하기 위해 다른 객체를 필요로 하는 경우 객체 사이에 의존성이 존재한다고 한다. 의존성은 방향성을 가지며 항상 단방향이다.
의존성은 존재해야 한다. 객체 사이의 메시지를 전달하기 위해서는 필연적이다. 하지만 그 의존성을 유연하게 관리한다는 것은 다른 이야기이다. 두 객체의 메시징을 상위 객체에서 델리게이트로 연결하거나 인터페이스에 DI를 통해 주입하여 사용하는 등 매우 느슨한 결합도 결국은 결합이다.
두 요소 사이의 의존성은 의존되는 요소가 변경될 수 있다는 것을 의미한다. 따라서 의존성 변경에 의한 영향의 전파 가능성을 암시한다.
의존성 전이
의존성은 전이될 수 있다.
Screening
의 코드를 살펴보면Screening
이Movie
,LocalDataTime
,Customer
에 의존한다는 사실을 알 수 있다. 의존성 전이(transitive dependency) 가 의미하는 것은PeriodCondition
이 의존하는 대상에 대해서도 자동적으로 의존하게 된다는 것이다. 다시 말해서Sreening
이 가지고 있는 의존성이Screening
에 의존하고 있는PeriodCondition
에 전이된다.의존성은 함께 변경될 수 있는 가능성을 의미하기 때문에 모든 경우에 의존성이 전이되는 것은 아니다. (현재 코드와 같이 메서드 체이닝, 기차충돌의 예제를 생각해보면 된다.) 의존성이 실제로 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라진다.
Screening
이 의존하고 있ㅈ는 어떤 요소의 구현이나 인터페이스가 변경되는 경우에 Screening이 내부 구현을 효과적으로 캡슐화하고 있다면Screening
에 의존하고 있는 객체들까지는 변경이 전파되지 않았을 것이다.의존성은 전이될 수 있기 때문에 의존성의 종류를 **직접 의존성(direct dependency)**과 **간접 의존성(indirect dependency)**로 나누기도 한다. 직접 의존성이란 말 그대로 한 요소가 다른 요소에 직접 의존하는 경우를 가리킨다. 이 경우 코드에 명시적으로 드러난다. 간접 의존성이란 직접적인 관계는 존재하지 않지만 의존성 전이에 의해 영향이 전파되는 경우를 가리킨다.
여기서는 되게 미시적인 클래스를 예로 설명했지만 변경과 관련이 있는 어떤 것에도 의존성이라는 개념을 적용할 수 있다. 의존성의 대상은 객체일 수도 있고 모듈이나 더 큰 규모의 실행 시스템일 수도 있다. 하지만 의존성의 본질은 변하지 않는다. 의존성이란 의존하고 있는 대상의 변경에 영향을 받을 수 있는 가능성이다.
런타임 의존성과 컴파일타입 의존성
의존성과 관련해서 다뤄야 하는 또 다른 주제는 **런타임 의존성(run-time dependency)과 컴파일타임 의존성(compile-time dependency)**의 차이다.
런타임은 말 그대로 애플리케이션이 실행되는 시점을 의미하고 컴파일 타임은 약간 추상적으로 다룬다. 실제 작성된 코드를 컴파일하는 시간을 말하기도 하지만, 문맥에 따라서는 코드 그 자체를 가리키기도 한다. 컴파일타임 의존성은 시간이 아니라 작성한 코드의 구조이기 때문에 런타임 의존성과는 다르다.
실제로 컴파일 타임이라는 용어는 많이 혼합해서 사용하기에 문맥에서 말하고자 하는 것을 파악하는게 중요하다.
객체지향 애플리케이션에서 런타임의 주인공은 객체이다. 따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성이다. 반면 코드 관점에서 주인공은 클래스이다. 따라서 컴파일 타임 의존성이 다루는 주제는 클래스 사이의 의존성이다.
동적 모델과 정적 모델의 차이라고 생각하면 될 것 같다. 동적 모델은 실행 시점에 객체들이 어떻게 동작하는지를 보여주고 정적 모델은 코드를 보고 어떻게 동작하는지를 보여준다.
앞서 다룬
DiscountPolicy
라는 역할에 대해서Movie
는 해당 역할에만 의존성을 가지지 해당 추상클래스/인터페이스의 구현체에 대한 의존성을 가지지 않는다. 하지만 이는 컴파일 타임에 해당하는 이야기고 런타임에는 달라진다. 실제 런타임의 모델은Movie
마다 의존성 주입을 통해 넣어준DiscountPolicy
의 구현체에 의존하게 된다.어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안 된다. 실제로 협력할 객체가 어떤 것인지는 런타임에 해결해야 한다. 클래스가 협력할 객체를 명시적으로 드러내고 있다면 다른 클래스의 인스턴스와 협력할 가능성 자체가 없어진다. 따라서 컴파일타임 구조와 런타임 구조 사이의 거리가 멀면 멀수록 설계가 유연해지고 재사용 가능해진다.
객체지향의 자율적인 객체, 협력의 구조는 추상적일수록 강화가 되는데 그 구조는 재귀적일 수 있다. 해당 로직을 담고 있는 모듈이 있고 그 모듈끼리의 또 다른 협력과 그 협력 관계를 담고 있는 모듈로 계속해서..
컨텍스트 독립성
구체 클래스에 대해 의존하는 것은 클래스의 인스턴스가 어떤 문맥에서 사용될 것인지를 구체적으로 명시하는 것과 같다. 클래스가 특정한 문맥에 강하게 결합될수록 다른 문맥에서 사용하기는 더 어려워진다. (해당 문맥에 강하게 결합되기 때문에) 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트 독립성이라고 한다.
설계가 유연해지기 위해서는 가능한 한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 한다. 컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용될 수 있기 때문이다.
의존성 해결하기
컴파일타임 의존성은 구체적인 런타임 의존성으로 대체돼야 한다. 의존성을 해결하기 위해서는 일반적으로 다음과 같은 세 가지 방법을 이용한다.
대부분은 생성자를 통해서 의존성을 주입하는데, 권장하는 방법은 생성자를 통해 기본적으로 사용할 객체를 주입하여 주고(Default) 이후에 setter를 통해 변경할 수 있도록 하는 것이다. 이렇게 하면 객체의 불변성을 유지하면서도 유연하게 변경할 수 있다.
이 말은 결국은 어디선가는 이 것을 담당하도록 해야한다는 것인데, 이를 DI프레임워크를 사용해 컨테이너에 위임할 수도 있고, 그냥 코드 처럼 상위 오브젝트가 생성하여 할당할 수도 있고,, 데이터 베이스나 서버로 처리할 수 있다. 중요한 것은 객체 끼리의 협력에서 의존성이 줄어든다는 것이다.
유연한 설계
설계를 유연하고 재사용 가능하게 만들기로 결정했다면 의존성을 관리하는 데 유용한 몇 가지 원칙과 시법을 익힐 필요가 있다.
의존성과 결합도
객체지향 패러다임은 근간은 협력이다. 객체들은 협력을 통해 애플리케이션에 생명력을 불어넣는다. 객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야 한다. 이런 자식들이 객체 사이의 의존성을 낳는다. 의존성은 객체들의 협력을 가능하게 만드는 매개체라는 관점에서는 바람직한 것이다. 하지만 의존성이 과하면 문제가 될 수 있다.
의존성이 과하거나 너무 강하게 결합되어 있다면 이는 다른 객체와의 협력이 단절됨을 의미한다. 따라서 코드에서 다루든 추상화를 통해 다형성을 이용하여 의존성을 바람직하게 만들어야 한다. 의존성 자체는 나쁜 것이 아니며 협력하기 위헤선 반드시 필요하다. 바람직한 의존성은 결국 재사용성과 관련이 있다.
이런 의존성을 다루는 좀 더 세련된 용어로 결합도가 있다. 두 요소 사이에 존재하는 의존성이 바람직할 때 두 요소가 **느슨한 결합도(loose coupling)**를 가진다고 말한다. or 약한 결합도(weak coupling) 반대로 두 요소 사이에 의존성이 바람직하지 못할 때 단단한 결합도(strong coupling) 또는 **강한 결합도(tight coupling)**라고 한다.
지식이 결합을 낳는다
결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정된다. 한 요소가 다른 요소에 대해 더 많은 정보를 알고 있을수록 두 요소는 강하게 결합된다. 반대로 한 요소가 다른 요소에 대해 더 적은 정보를 알고 있을수록 두 요소는 느슨하게 결합된다.
서로에 대해 알고 있는 정도를 지식의 양이라고 표현할 수 있으며 결국 더 많이 알수록 더 많이 결합된다. 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용이 가능하다는 것을 의미한다. 기존 지식에 어울리지 않는 컨텍스트에서 클래스의 인스턴스를 사용하기 위해서 할 수 있는 유일한 방법은 클래스를 수정하는 것뿐이다.
결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야 하며, 결합도를 느슨하게 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에는 최대한 감추는 것이 중요하다. 이에 대한 가장 효과적인 방법은 바로 추상화다.
추상화에 의존하라
추상화란 어떤 양상, 세부사항, 구조를 좀 더 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다. 추상화를 사용하면 현재 다루고 있는 문제를 해결하는 데 불필요한 정보를 감출 수 있다. 따라서 대상에 대해 알아야 하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있다.
일반적으로 추상화와 결합도의 관점에서 의존 대상을 다음과 같이 구분하는 것이 유용하다.
아래로 갈수록 알아야 하는 지식의 양이 줄어들기 때문에 결합도 또한 느슨해진다.
구체 클래스에 비해 추상 클래스는 메서드의 내부 구현과 지식 클래스의 종류에 대한 지식을 클라이언트에게 숨길 수 있다. 따라서 클라이언트가 알아야 하는 지식의 양이 더 적기 때문에 구체 클래스보다 추상 클래스에 의존하는 것이 결합도가 더 낮다. 같은 이유로 인터페이스에 의존하는 것이 추상 클래스에 의존하는 것보다 결합도가 더 낮다.
여기서 중요한 것은 실행 컨텍스트에 대해 알아야 하는 정보를 줄일수록 결합도가 낮아진다는 것이다. 결합도를 느슨하게 만들기 위해서는 구체적인 클래스보다 추상 클래스에, 추상 클래스보다 인터페이스에 의존하도록 만드는 것이 더 효과적이다.
더 효과적이긴 하나, 정답은 아니다. 경우에 따라 추상클래스로의 다형성을 보여줘야 할 때도 있다.
명시적인 의존성
결합도를 느슨하게 만들기 위해서는 인스턴스 변수의 타입을 추상 클래스나 인터페이스로 선언하는 것만으로는 부족하다. 클래스 안에서 구체 클래스에 대한 모든 의존성을 제거해야만 한다. 하지만 런타임에는 직접적으로 협력해야 하기 때문에 이를 해결할 방법이 필요하다.
의존성을 해결하는 방법 중 하나는 의존성 주입이다. 의존성 주입은 클래스가 사용할 의존성을 클래스 외부에서 제공하도록 만드는 것이다. 이를 통해 클래스는 자신이 사용할 의존성을 직접 선택할 수 있게 된다.
의존성의 대상을 생성자의 인자로 전달받는 방법과 생성자 안에서 직접 생성하는 방법 사이의 가장 큰 차이점은 퍼블릭 인터페이스를 통해 다형성을 제공할 수 있는지의 여부이다. 퍼블릭 인터페이스를 통해 의존성을 명시적으로 노출시키는 것을 **명시적인 의존성(Explicit Dependency)**라고 한다.
반면 생성자 내부에서 new를 통해 직접 생성하는 방식은
Movie
가DiscountPolicy
에 의존한다는 사실을 감춘다. 다시 말해 의존성이 퍼블릭 인터페이스에 표현되지 않는다. 이를 **숨겨진 의존성(Hidden Dependency)**라고 한다.의존성이 명시적이지 않으면 의존성을 파악하기 위해 내부 구현을 직접 살펴볼 수밖에 없다. 커다란 클래스에 정의된 긴 메서드 내부 어딘가에서 인스턴스를 생성하는 코드를 파악하는 것은 쉽지 않을뿐더러 심지어 고통스러울 수도 있다.
더 커다란 문제는 의존성이 명시적이지 않으면 클래스를 다른 컨텍스트에서 재사용하기 위해 내부 구현을 직접 변경해야 한다는 것이다. 코드 수정은 언제나 잠재적으로 버그의 발생 가능성을 내포한다. 의존성을 명시적으로 드러내면 코드를 직접 수정해야 하는 위험을 피할 수 있다. 실행 컨텍스트에 적절한 의존성을 선택할 수 있기 때문이다.
정리하면 의존성은 명시적으로 표현되어야 하며, 내부에 숨기면 안된다. 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계다. 명시적인 의존성을 사용해야만 퍼블릭 인터페이스를 통해 컴파일타임 의존성을 적절한 런타임 의존성으로 교체할 수 있다.
new는 해롭다
대부분의 언어에서는 클래스의 인스턴스를 생성할 수 있는
new
연산자를 제공한다. 하지만 안타깝게도new
를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아진다. 결합도 측면에서new
가 해로운 이유는 크게 두 가지다.구체 클래스에 직접 의존하면 결합도가 높아진다는 사실을 기억하자. 결합도의 관점에서 구체 클래스는 협력자에게 너무 많은 지식을 알도록 강요한다. 여기에 new는 문제를 더 크게 만든다. 클라이언트는 구체 클래스를 생성하는 데 어떤 정보가 필요한지에 대해서도 알아야 하기 때문이다.
이에 대한 해결 방법은 인스턴스를 생성하는 로직과 생성된 인스턴스를 사용하는 로직을 분리하는 것이다.
Movie
는 단순하게 할인 조건을 받아서 사용하기만 해야 한다. (직접 생성 x) 이를 위해 외부에서 이미 생성된 인스턴스를 추상화를 통해 주입해주는 것이다.사용과 생성의 책임을 분리해서 결합도를 낮추면 설계를 유연하게 만들 수 있다. 의존성을 명시적으로 드러내고, 구체 클래스가 아닌 추상 클래스에 의존하게 함으로 설계를 유연하게 만들자. 그리고 그 출발은 객체를 생성하는 책임을 객체 내부가 아니라 클라이언트로 옮기는 것에서 시작했다는 점을 기억하자.
같은 맥락으로 모든 디커플리은 책임의 분리이다. 다른 아키텍처나 로직이 복잡해지고 클래스가 방대해진다면 책임을 분리하자.
간혹 DI에 대해서 너무 어렵게 생각하는 것 같은데,(나도 어려웠다. ㅎㅎ) 하지만 단순하게 매개변수에 구체화된 클래스를 전달 받아 사용하는 것으로 사용하는 객체는 실제 코드에서 추상화된 참조 변수를 통해 해당 메모리에 접근하여 사용하는 것이다. 이것만 알아도 충분하다.
가끔은 생성해도 무방하다
클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있다. 주로 협력하는 객체를 설정하고 싶을 경우가 여기에 속한다. 즉, 다형성이 발생하지 않는 부분에 대해서는 의존성을 가지기도 한다. 책에선 생성자를 체이닝하는 방법으로 나타내며 실제 C#에서는 명명된 매개변수의 형태로도 많이 나타낸다.
결국 트레이드오프 활동으로 이어지게 되고 오버로딩, 생성자 체이닝 등도 결국 결합도와 사용성을 고민해야 한다는 것이다. 그럼에도 가급적이면 구체 클래스에 대한 의존성을 제거할 수 있는 방법을 찾아야 한다. 종종 모든 결합도가 모이는 새로운 클래스를 추가함으로써 사용성과 유연성이라는 두 마리 토끼를 모두 잡을 수 있다.
표준 클래스에 대한 의존은 해롭지 않다
의존성이 불편한 이유는 그것이 항상 변경에 대한 영향을 암시하기 때문이다. 따라서 변경될 확률이 거의 없는 클래스라면 의존성이 문제가 되지 않는다.
어떻게 생각해보면 언어끼리의 차이도 의존성이라고 할 수 있다.
컨텍스트 확장하기
Null 객체 패턴, 특정한 상황에 대한 처리를 위한 패턴이다. 이 패턴은 객체가 null을 반환하는 대신에 특별한 객체를 반환하도록 만들어서 클라이언트가 null을 반환하는 객체를 처리하는 코드를 작성하지 않도록 한다.
이런 유연한 설계가 가능한 이유도 조금만 생각해보면 변경에 용이할 수 있도록 직접 설계했기 때문이다. 다시 정리하자면 사용성과 유연성을 생각하고 설계하자. 시퀀스형태의 디자인도 마찬가지로 컴포지션 패턴처럼 유연하게 설계가 가능하다.
조합 가능한 행동
다양한 종류의 할인 정책이 필요한 컨텍스트에서
Movie
를 재사용할 수 있었던 이유는 코드를 직접 수정하지 않고도 협력 대상인DiscountPolicy
인스턴스를 교체할 수 있었기 때문이다. 즉,Moive
라는 클래스는 하나이지만,Movie
라는 객체는 매우 다양한 객체로써 협력이 가능해진다. 이것이 의존성을 낮춰야 하는 이유이다.인터페이스의 엄청난 장점이며, 사용해야 하는 이유이기도 하다.
Movie
자체가DiscountPolicy
라는 추상화에 의존하고 생성자를 통해 의존성을 명시적으로 드러내었으며new
와 같이 구체 클래스를 직접적으로 다뤄야 하는 책임을 외부로 옮겼기 때문이다.어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징이다. 유연하고 재사용 가능한 설계는 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써 애플리케이션의 기능을 쉽게 확장할 수 있다.
유연하고 재사용 가능한 설계는 객체가 어떻게 하는지를 장황하게 나열하지 않고도 객체들의 조합을 통해 무엇을 하는지를 표현하는 클래스들로 구성된다. 따라서 클래스의 인스턴스를 생성하는 코드를 보는 것만으로 객체가 어떤 일을 하는지 쉽게 파악할 수 있다. 코드에 드러난 로직을 해석할 필요 없이 객체가 어떤 객체와 연결됐는지를 보는 것만으로 객체의 행동을 쉽게 예상하고 이해할 수 있기 때문이다.
유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계다. 훌륭한 객체지향 설계란 객체가 어떻게 하는지를 표현하는 것이 아니라 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지를 표현하는 설계다. 그리고 지금까지 설명한 것처럼 이런 설계를 창조하는 데 있어서의 핵심은 의존성을 관리하는 것이다.
이것이 객체지향이 매력적인 이유다. 주어진 도메인에 매번 다른 형태의 설계가 나오기 때문에 지속적으로 성장할 수 있는 것 같다.
느낀점
지속해서 내 생각을 글에 적긴했지만, 결국
How
가 아니라What
에 집중해야 한다는 말로 귀결되는 것 같다. 앞 1~6장까지 말한 책임을 따라야 하는 이유와 왜 그래야 하는지를 설계에서 의존성이 가지는 중요성을 설명한 것 같다.누구나 다른 코드를 짜고 만들어내지만, 객체지향은 정말 단순하고 명확하지만, 어렵다. 아마 기존의 코드 스타일이나 사고방식 자체가 달라져야 하기 때문이 아닐까?
애자일과 연관이 되어 있듯이 우리는 쉽게 실제 건축하는 것과 같은 설계를 하려고 한다. 하지만 소프트웨어 개발에선 불확실성과 변경사항이 너무나 많기 때문에 이런 의존성을 낮추는 개발 방법이 필요하다. 그래서 사고방식의 전환이나 지속적인 경험이 중요한 것 같다. (폭포수 모델 -> 애자일)
논의사항
The text was updated successfully, but these errors were encountered: