이 포스트의 주제는 이전의 S.O.L.I.D 의  LSP에 이어 의존 역전의 원칙 Dependency Inversion Principle (DIP) 입니다. 포스팅의 동기 또한 LSP의 동기와 같습니다. 개념이해를 돕기  위해 뽑아낸 간략화된 핵심 정의가, 오히려 상세 이해를 방해함을 느꼈습니다. 원칙의 정의만으로는 그 원칙이 의미하는 바를 정확히 이해하기 어렵습니다. 이 포스트는 DIP 원칙이 의미를 설명하는 것입니다. 원칙의 상세를 이해하고, 핵심 정의 구문은 기억을 끄집어 내기위한 실마리로 삼는 것이 가장 좋을 것입니다.

이제 의존 관계 역전의 원칙 DIP :  Dependency Inversion Principle 을 시작하도록 합시다.

먼저 원칙의 이름으로 부터 어떤 원칙인지 힌트를 얻어 보도록 하겠습니다. "의존 관계를 역전 하라는 이야기군, 가만 의존이 뭐지?"


의존이란 무엇인가?

객체-지향 프로그램은 관계를 가지고 있는 객체의 집합 그 자체 입니다. 그 관계를 UML의하면 Multiplicity, Aggregation, Composition, Dependency  4가지의 형태로 일반화 할 수 있습니다. Inheritance 는 종류가 다른 관계라고 생각하도록 하죠.  단순화 하면 객체A 가 객체B를 포함하고 있는데, 그 형태가 위에 언급한 형태 중 하나라는 것이며, 이때 객체A가 객체B를 의존하고 있다 라고 일반화 시켜 말합니다.



의존을 알았으니 추측을 넘어, DIP 의 정의를 알아 보도록 하겠습니다.

A. 고차원의 모듈은 저차원의 모듈에 의존하면 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존 해야 한다.

B. 추상화 된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.


더 쉽게 말할 수도 있다. 자주 변경되는 컨크리트 클래스에 의존하지 마라. 만약 어떤 클래스의 참조를 가져야 한다면, 참조 대상이 되는 클래스를 추상 클래스로 만들어라. 만약 어떤 함수를 호출해야 한다면, 호출 되는 함수를 추상 함수로 만들어라.

일반적으로, 추상 클래스와 인터페이스는 자신에게서 유도된 구체적인 클래스 보다 훨씬 덜 변한다.

Java 프로그래머를 위한 UML 실전에서는 이것만 쓴다!. 로버트 C.마틴 지음 / 이용원, 정지호 옮김, 인사이트 출판사, p140, 141 


음... 가능한 추상 클래스나 인터페이스를 참조 함으로써 의존을 좀더 느슨하게 만들어라는 얘기군. 알겠어, 앞으로는 최대한 추상 클래스를 만들고, 인터페이스를 끄집어 내서 DIP 를 지키는 개발자가 되겠어!  근데 관계의 역전은 뭘 의미하는 거지? 위의 정의에 역전이라는 말은 언급되지도 안잖아, 에이 머리 아파~ 이 정도만 이해하면 되겠지.

관계의 역전의 뜻에 대해 의문점을 남긴채 찜찜한 상태로 공부를 마무리 하게 됩니다.




자! 이제부터 시작입니다. 역전은 뭘 말하는 걸까요?. DIP 의 성배를 찾으러 갑시다.


DIP를 제시한 로버트 C. 마틴보다 더 잘 설명할 능력이 부족하기도 하고, 지금까지 읽은 문서중 가장 잘 설명하고 있다고 생각하는 마틴의 DIP 문서의 요약 및 해석으로 글을 쓰도록 하겠습니다.



의존 관계 역전의 원칙 The Dependency Inversion Principle 


이전 글에서 Liskov Substitution Principle LSP 에 대해 이야기 했습니다. LSP는 C++ 에서 상속을 사용할 때 일반적인 가이드를 제공하는 것입니다.  파생되는 클래스는 기반 클래스의 가상 멤버 함수의 약속이 파생 클래스에서도 제공되어 져야 한다는 것이고, 이를 위반할 경우 객체의 적절한 작동을 보장하기 위해서는  실제 객체의 타입을 확인해야 절차가 유발되며, 이 행위는 OCP를 위반하게 된다 였습니다.

우리는 OCP 와 LSP를 논했고, 이 원칙들이 엄격하게 지켜져서 얻어지는 구조를 일반화 시킬수가 있는데, 이 구조를 "의존 관계 역전의 원칙 The Dependency Inversion Principle" 이라고 이름 붙이겠습니다.


소프트웨어에 뭐가 잘못 되어가고 있는가? What goes wrong with software?

우리 대부분은 "나쁜 디자인"의 소프트웨어 조각으로 일을 해본 유쾌하지 않은 경험을 가지고 있습니다. 우리중 일부는 그 "나쁜 디자인"의 주인이 자기 자신이었다는 것을 발견하는 더욱 나쁜 경험을 가지고 있을지도 모릅니다. 도대체 디자인을 나쁘게 만드는 것은 무엇일까요?


"나쁜 디자인"의 정의 The Definition of a "Bad Design"

여러분은 다른 사람들에게 자신이 특별히 자랑스러워하는 소프트웨어 디자인을  발표해 본적이 있습니까?  그때 "왜 그런 방법으로 하셨나요?" 라는 말을 들어 본적이 있습니까? 이건 저에게 일어난 일이고, 또 많은 엔지니어들에도 벌이지고 있는 일입니다. 디자인에 동의하지 않는 엔니지어는 "나쁜 디자인"에 대한 정의에 대해 나와 동일한 기준을 가지고 있지 않았습니다. 엔지니어들에게서 발견한 가장 일반적인 기준은 "나는 그런 방법으로 하지 않아" 였습니다.

하지만, 모든 엔지니어들이 동의하는 나쁜 디자인에 대한 기준이 있었으니 다음과 같습니다.

1. 모든 변경마다 많은 다른 부분에 영향을 미쳐 변경 자체가 어렵다. (Rigidity)
2. 변경 작업을 할때 예상치 못한 다른 부분이 망가진다. (Fragility)
3. 현재의 어플리케이션에서 분리할 수 없기 때문에 다른 어플리케이션에서 재사용 하기가 매우 어렵다. (Immobility)

그 외에도 유연성, 견고성 과 같은 설명하기 어려운 요소들이 있었습니다. 따라서 위의 세가지 항목을 "좋고, 나쁨의" 기준으로 삼기로 결정 했습니다.


 "나쁜 디자인"의 원인 The Cause of "Bad Design"

무엇이 디자인을 딱딱하고 rigid, 깨지기 쉽고 fragile, 이동할 수 없게 immobile 만드는 것일까? 그건 모듈 디자인의 상호의존 interdependence 때문입니다. 

변경이 어려운 rigidity 디자인 원인은 강하게 상호의존하고 있는 부분의 단일 변경이 그가 의존하는 모듈에게 연쇄변경을 요구하기 때문입니다. 이러한 일련의 변경이 확장되면 설계자와 유지관리자는 그를 예측, 추정하지 못합니다. 이는 변경 승인을 주저하게 만들고, 디자인은 공식적으로 경화 되었다라고 선언 됩니다.

깨지기 쉬움 fragility 이란 단일 변경의 영향으로 매우 많은 부분이 고장남을 뜻합니다. 개념적으로 관련이 없는 영역에서도 종종 새로운 문제가 발생합니다. 이런 깨지기 쉬움은 설계와 관리조직의 신뢰성을 급격히  떨어트립니다. 사용자와 관리자는 상품의 품질을 예상할 수 없게 됩니다. 한 부분의 간단한 변경이 그와 명백히 관련이 없는 부분에서 실패를 만들어 냅니다. 이런 문제를 해결하는 일 자체는 더욱 문제로 부상합니다. 유지보수 프로세스가 자신의 꼬리를 쫒는 개와 닮았거든요.

이동 불가능한 immobile 디자인이란 이동을 희망하는 어떠한 부분이 원치 않는 다른 부분의 상세를 강력하게 의존하고  있는 것을 말합니다. 디자이너는 새로운 어플리이션에 재사용 될 수 있는지 기존 어플리케이션을 조사 합니다. 이동시키고 싶은 부분을 분리하기 위해 매우 많은 부분으로 부터 분리해야 한다면, 대부분 그러한 디자인은 새로 개발하는 것보다 비용이 높기 때문에 재사용될 수 없습니다.



의존 관계 역전의 원칙 The Dependency Inversion Principle


A. 하이 레벨 모듈은 로우레벨 모듈에 의존해서는 안된다. 둘다 추상에 의존해야 한다.

B. 추상은 상세를 의존해서는 안된다. 상세는 추상을 의존해야 한다.

A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.

B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.


Copy 모듈은 Keyboard 와 Printer 디바이스의 상세를 사용합니다. - 구조적 디자인의 예



내가 왜 "역전 inversion" 이라는 단어를 사용했는지 의문이 생길 겁니다. 바로 구조적 분석, 구조적 디자인으로 대표되는 전통적인 개발 방법 때문입니다.  하이 레벨 모듈이 로우 레벨 모듈을 의존하는 구조, 추상이 상세를 의존하는 구조적 디자인 말이죠. 전통적 설계의 목적은 하이 레벨 모듈이 로우 레벨 모듈을 어떻게 호출할 것인지의 서브프로그램 구조를 정의하는 것 입니다.  [그림 1 ]은 구조적 디자인 구조의 좋은 예 입니다. 그래서 잘 설계된 객체-지향 프로그램의 의존 구조는 전통적 절차적 메소드의 결과로 만들어지는 의존 구조를 "역전 invert"한 형태 입니다.


 하이 레벨 모듈이 로우 레벨 모듈을 의존하고 있는 경우를 고려해 봅시다. 하이 레벨 모듈은 어플리케이션의 중요한 정책 결정과 비즈니스 모델을 담고 있습니다. 모델은 어플리케이션의 정체성을 담고 있죠. 이 모듈들이 로우 레벨 모듈을 의존하고 있고, 로우 레벨 모듈에서 발생하는 변경은 하이 레벨 모듈들에 직접적인 영향을 미치며 마침내 변경을 강요합니다.


이건 완전히 말이 안되는 상황입니다. 변경을 강요해야 하는 것은 하이 레벨 모듈이어야 하며, 하이 레벨 모듈이 로우 레벨의 상급자가 되어야 합니다. 어떤 상황이건 하이 레벨 모듈이 로우 레벨 모듈을 의존 해서는 안됩니다.

더 나아가, 하이 레벨 모듈은 재사용 가능해야 합니다. 우리는 이미 서브루틴 라이브러리와 같은 형태로 로우 레벨 모듈을 재사용하는데 정통해 있습니다. 하이 레벨 모듈이 로우 레벨 모듈을 의존하고 있으면, 하이 레벨 모듈은 문맥이 다른 곳에 재사용되기 어려워 집니다. 그러나, 하이 레벨 모듈이 로우 레벨 모듈에 독립적 이라면, 하이 레벨 모듈은 매우 간단히 재사용 될 수 있습니다. 이것이 바로 프레임워크 디자인의 핵심 입니다.
 



레이어링 Layering

Booch 에 따르면 " 잘 구조화된 객체-지향 아키텍처는 명확하게 정의된 레이어를 가지고 있다. 개개의 레이어는 잘-정의 되고, 통제되는 인터페이스를 통해 응집성 있는 서비스의 집합을 제공한다." 이 문장을 순진하게 해석한 디자이너가 만들어내는 구조는 [그림 3] 과 같을 것입니다. 이 다이어그램에서 하이 레벨 정책 클래스는 로워 레벨 메커니즘을 사용합니다. 이는 상세 레벨의 유틸리티 클래스를 사용하는 형태로 변합니다.  정책 레이어가 변경에 민감해서 유틸리티 레이어에 변경이 전파되는 교활한 특성을 보이더라도 괜찮지만, 의존은 전이적 transitive 입니다. 정책 레이어는 유틸리티 레이어를 의존하는 그 어떤 것(여기서는 메커니즘 레이어)에 의존하고 있습니다.  그래서 정책 레이어는 유틸리티 레이어를 의존하는 불행한 형태가 됩니다.




[그림 4] 는 보다 좋은 모델입니다. 각각의 로워 레벨 레이어들이 추상 클래스로 표현되고 있습니다. 실체 레이어는 추상레이어로 부터 파생 됩니다. 각각의 하이 레벨 레이어는 추상 인터페이스를 이용해 다음 단계의 로워 레벨 레이어를 사용합니다. 따라서 레이어들은 다른 레이어들을 의존하는 대신에 추상 클래스를 의존합니다. 정책 레이어의 의존이 전이되면 유틸리티 레이어만을 망가뜨리는 것이 아닙니다. 정책 레이어가  직접적으로 의존하고 있는 메커니즘 레이어 조차도 망가뜨립니다. 


이 모델을 사용하면 정책 레이어는 메커니즘 레이어나 유틸리티 레이어의 변경으로 부터 영향을 받지   않습니다. 더 나아가,  정책 레이어는 메커니즘 레이어의 인터페이스를 준수하는 로워 레벨 모듈의 정의를 가진 다른 문맥에서 재사용 될 수 있습니다. 의존을 역전시킴으로써 우리는 좀더 유연하고, 견고하고, 이동성 있는 구조를 만들 수 있습니다.


단순한 예제 Simple Example

의존 역전은 한 클래스가 다른 클래스에 메시지를 보낼때 적용할 수 있습니다. button 객체와 lamp 객체를 생각해 보세요. 

button 객체는 외부 환경을 감지하고, 사용자가 버튼을 눌렀는지 여부를 결정합니다. lamp 객체는 외부 환경으로 부터 영향을 받습니다. TurnOn 메시지를 받으면 불을 밝히고, TurnOff 메시지를 받으면 불을 끕니다.

 button 객체가 lamp 객체를 제어하는 시스템을 어떻게 설계할까요?

Figure 5는 이를 구현한 순진한 모델입니다. 

button 객체는 lamp 객체에게 TurnOn, TurnOff 메시지를 보냅니다.  button 클래스는 lamp 클래스 인스턴스와 관계를 가지기 위해서 lamp를 "포함" 합니다. 

 


위 모델의 구현은 아래와 같습니다.

Figure 5 는 의존 관계 역전의 원칙을 위반합니다. 어플리케이션의 하이-레벨 정책이 로우-레벨 모듈과 분리되지 않았습니다. : 추상과 상세가 분리되지 않았다는 말입니다. 분리하지 않으면 하이-레벨 정책이 자동적으로 로우-레벨 모듈을 의존합니다. 추상이 상세를 자동적으로 의존하는 것입니다.


[전통적인 button/lamp model 구현 코드]

namespace TradButtonModel
{
    class Lamp{
        public void TurnOn()
        {
            Console.WriteLine("Trad Lamp 램프를 켭니다.");
        }
        public void TurnOff()
        {
            Console.WriteLine("Trad Lamp 램프를 끕니다.");
        }
    }

    class Button {
        private Lamp itsLamp;

        public Button(Lamp lamp)
        {
            this.itsLamp = lamp;
        }

        public void Detect()
        {
            bool buttonOn = GetPhysicalState();
            if (buttonOn)
            {
                itsLamp.TurnOn();
            }
            else
            {
                itsLamp.TurnOff();
            }
        }

        private bool GetPhysicalState()
        {
            bool isPressed = false;
            //물리적 장치가 눌려저 있으면 true 를 반환
            return isPressed;
        }
    }



}


의존 하는 추상 찾기 Finding the Underlying Abstraction

하이 레벨 정책이란 무엇일까요? 이것은 어플리케이션이 의존하는 추상입니다. 상세가 변한다 해도 변경되지 않는 것을 의미하죠. Button/Lamp 예제에서 의존하는 추상이란 사용자의 on/off 제스처를 감지하고 타겟 객체에게 전달하는 것을 의미합니다. 말도 안됩니다!, 타겟 객체는 또 뭐죠? 타겟 객체는 추상에 영향을 주지 않는 상세 입니다.

의존 관계 역전의 원칙을 적용하려면 문제의 상세로부터 추상을 분리해야 합니다. 그런 다음, 추상이 상세를 의존하는 Figure 5 를 Figure 6 와 같이 변경할 수 있습니다.



Figure 6, 우리는 button 클래스의 상세 구현으로 부터 추상을 격리하였습니다. 


상위 레벨 정책이 완전히 추상 button 클래스만을 가지고 있음을 주목하세요. Button 클래스는 사용자   제스처를 감지하는 물리적 메커니즘을 전혀 모릅니다. 그리고, Lamp 에 대해서도 전혀 모릅니다. 

이 상세들은 추상을 구현한 ButtonImplementation, Lamp 클래스에 의해 추상으로 부터 격리됩니다. 


[DIP를 적용한 button/lamp model 구현 코드] 

namespace InvertedButtonModel
{
    abstract class ButtonClient
    {
        public abstract void TurnOn();
        public abstract void TurnOff();
    }


    class Lamp : ButtonClient
    {
        public override void TurnOn()
        {
            Console.WriteLine("Lamp 램프를 켭니다.");
        }

        public override void TurnOff()
        {
            Console.WriteLine("Lamp 램프를 끕니다.");
        }
    }


    abstract class Button
    {
        private ButtonClient buttonClient;

        public Button(ButtonClient client)
        {
            this.buttonClient = client;
        }
        public void Detect()
        {
            bool buttonOn = GetState();
            if (buttonOn)
            {
                buttonClient.TurnOn();
            }
            else
            {
                buttonClient.TurnOff();
            }
        }
        public abstract bool GetState();


    }

    class ButtonImplementation : Button
    {
        public ButtonImplementation(ButtonClient client):base(client){}

        public override bool GetState(){
            bool isPressed = false;
            //물리적 장치를 감지하여 반환
            return isPressed;
        }

    }
}


결론 Conclusion

의존 관계 역전의 원칙은 객체-지향 기술이 주장하는 장점의 근원입니다. 좋은 어플리케이션은 재사용 가능한 프레임워크를 만들길 요구합니다. 변경에 강한 코드의 구조를 만드는 것은 매우 중요 합니다. 그리고, 추상과 상세를 서로 격리시키기 때문에 코드 관리는 더욱 쉬워 집니다.

의존 관계의 역전 dependency inversion  이란 구조적 디자인에서 발생하던 로워 레벨 모듈의 변경이 하이 레벨 모듈의 변경을 요구는 위계의 관계를 끊자 라는 의미로 쓰여진 역전입니다. 실제의 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고 받음으로써 관계를 최대한 느슨하게 만들어야 한다는 원칙입니다.

구현 관점에서 추상 클래스, 인터페이스를 사용하는 것부터 시작하여, 아키텍처 관점까지 확장할 수 있습니다. 예를 들어,  데이터베이스를 연결하는 레이어로 OLEDB를 사용한다면, OLEDB의 인터페이스를 제공하는 물리 DB가  MSSQL 에서 ORACLE 로의 변경되더라도 그 변경이 상위로 전파되지 않는다 라고 이해 할 수 있습니다.
 


하이 레벨과 로워레벨 사이에 추상을 삽입함으로써 로워 레벨의 변경이 하이 레벨로 전파되는 것을 막는다.



끝으로, 


"A. 하이 레벨 모듈은 로우레벨 모듈에 의존해서는 안된다. 둘다 추상에 의존해야 한다." 라는 정의는  "A. 하이 레벨 모듈은 로우레벨 모듈에 의존해서는 안된다 의존 하되, 둘다 추상에 의존해야 한다." 로 해석하는 것이 혼란을 피하는 것이라 생각됩니다.


### 끝.


참고자료 : 
로버트 C. 마틴 The Dependency Inversion Principle.pdf [Martin96] Robert C. Martin, Engineering Notebook, C++ Report
 


책 : java 프로그래머를 위한 UML 실전에서는 이것만 쓴다!. 로버트 C.마틴 지음 / 이용원, 정지호 옮김, 인사이트 출판사, p140,p141 

 




저작자 표시
신고

이 글을 Twitter / Facebook 에 공유하기
이 글이 유익하다면 아래의 트위터 버튼을 눌러 공유해 주시거나, 페이스북 "좋아요" 버튼을 눌러 주세요.

   


Posted by 반더빌트


티스토리 툴바