Head First OOAD, Gof Design Pattern 같은 OOP 기본서에서 'Hollywood principle'이나 'Inversion of Control'이란 용어를 많이 들어봤을 것이다. 이런 디자인 관련 책들에서 굉장히 반복적으로 나오는 용어인 것을 보면 꽤 중요한 개념인 것 같은데, 설명은 아래와 같이 매우 간단한 문장으로 마치곤 한다.

 

'Don't call us, we'll call you' (우리한테 연락하지 마세요. 우리가 당신에게 연락할게요.)

 

  물론 기본서이기 때문에 해당 개념에 대해 자세히 설명하고자 하면 더 복잡하게 될 것 같기도 하다. 나 같은 경우에는 대학생 때 Spring으로 웹서버를 개발하면서 Spring IoC를 통해 IoC에 대한 개념을 자연스럽게 익힐 수 있었다. 백문불여일견이라고 직접 사용해봐야 쉽게 이해할 수 있는 것 같다.

 

  IoC란 용어가 처음 등장한 건 Gof Design Pattern 저자 Ralph E. Johnson이 1988년 저술한 논문 'Designing Reusable Classes'에 처음 등장한다. 10년 전에 읽었을 땐 잘 이해가 안 갔는데, 사실 지금 다시 봐도 잘 모르겠다. 역시 영어는 어렵다. 그리고 그 후에 Gof Design Pattern 책에 'Hollywood principle'이란 용어로 재등장한다.

 

  물론 용어가 정립되고 일반화된 건 1988년이 최초이지만 위 논문에서 유추해 볼 수 있듯이 IoC 개념은 1978년 팔로알토 연구소에서 현대의 GUI와 가장 유사한 컴퓨터를 발표할 때 함께 최초로 GUI를 제공하는 객체지향언어인 Smalltalk와 MVC를 적용한 프레임워크를 공개하면서부터 시작한 것으로 보인다.

 

  일단, IoC에 대해 이해하기 위해서는 Dependency와 Dependency Inversion 그리고 Depdendency Injection에 대한 개념을 이해하고 있어야 하므로 Dependency 개념부터 차근차근 이해해보도록 하자.

Dpendency

[그림 1] Dependency

  [그림 1]에서 Client A는 Service B를 의존한다. (class들의 관계에서 Service는 특정 기능을 API를 통해 제공해주는 class를 의미하고 Client는 그 Service를 이용하는 class라고 이해하면 된다.) 여기서 의존한다는 뜻은 A가 B를 멤버 변수나 로컬 변수로 가지고 있거나 혹은 파라미터로 전달되거나 B의 메소드를 호출하는 것들을 의미한다.

 

  만약 Service B가 변경되면 Client A는 B를 강하게 Dependency하고 있음으로 컴파일이 안되거나 예상치 못한 동작을 하는 등의 영향을 받게 된다. 그리고 이러한 의존성은 A를 재사용하기 어렵게 만들기 때문에 A는 Component/Service가 될 수가 없다. 여기서 Component란 소스 코드의 아무런 수정 없이 다른 프로젝트에서도 바로 재사용이 가능한 수준의 모듈을 말하는데, 만약 현재 상태에서 A만 다른 프로젝트에 가져와 재사용하기 위해서는 A에서 B를 사용하는 부분을 수정해야 한다.

 

[그림 2] 관리되지 않은 Dependency

  여기서 더 문제는 우리 프로젝트 내에서 대부분의 class들의 의존관계가 [그림 2]와 같다는 것이다. 결국 Leaf에 존재하는 Class 말고는 재사용 할 수 있는 코드가 전혀 없다는 뜻을 의미한다. 본인 프로젝트 코드를 한번 돌이켜 보자. 과연 컴포넌트가 될 수 있는 클래스가 몇 개나 되는지? 그리고 UI이나 Data Source 등이 변경되었을 때 각 class간의 영향도가 어느 정도인지 말이다.

 

  이렇게 고차원 모듈이 저차원 모듈을 의존하고 저차원 모듈이 다시 고차원 모듈을 의존하는 것을 의존성 부패(Dependency Rot)이라 한다. 아래는 이런 의존성 부패를 없애는 일반적인 디자인 방법인 DIP에 대해 설명한다.

Dependency Inversion Principle (DIP)

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

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

- Martin, Robert C. -

 

  Dependency Inversion Principle은 class들 간의 의존성 부패(Dependency Rot)를 제거하기 위한 일반적인 디자인 방법이며, Martin, Robert C.가 1996년 'The Dependency Inversion Principle'을 발표하면서 많이 알려지기 시작했다. 

  위 [그림 1] Client A와 Service B에 DIP를 적용하면 아래와 같은 순서로 적용된다.

[그림 3]

  1. '고차원 모듈은 저차원 모듈에 의존하면 안된다.' [그림 3]에서 A가 B를 바라보는 Dependency를 제거한다.

[그림 4]

  2. '이 모듈 모두 다른 추상화된 것에 의존해야 한다. 그리고 추상화 된것은 구체적인 것에 의존하면 안된다.' [그림 4]에서 A는 Abstract를 Reference하지만 Abstract는 B를 Dependency하면 안된다.

[그림 5]

  3. '구체적인 것이 추상화된 것에 의존해야 한다.'  [그림 5]에서 B가 Abstract를 Inherit하도록 하여 Dependency를 Inversion한다. 추상적인 그림만 봐서는 이해하기 어려울 수 있으니 아래 구체적인 예제 코드를 통해 보도록 하자.

 

[코드 1] Dependency 문제가 있는 SwitchButton

 

  [코드 1]은 SwitchButton을 toggle하면 Lamp가 on/off되는 간단한 예제 코드다. 위 class들의 관계를 그려보면 아래와 같다.

[그림 6] SwitchButton과 Lamp의 Class Diagram

  [그림 6] SwitchButton과 Lamp의 Class Diagram을 보면 [그림 1]과 동일한 의존관계인 것을 알 수 있다. 위에서 이미 설명했듯이 [그림 6]에서 재사용할 수 있는 건 Lamp밖에 없으며 SwitchButton은 재사용할 수 없다. 현재 상태에서 램프가 아닌 자동차 시동을 거는 버튼이 필요하다면 SwitchButton을 재사용하지 못하고 EnginSwitchButton을 또 구현해야 된다.

 

  [코드 2]는 [코드 1]의 의존성 문제를 DIP를 적용하여 해결한 코드이다.

[코드 2] DIP가 적용된 SwitchButton

 

[그림 7] Dependency Inversion이 적용된 모습

  [코드 1]에서는 SwitchButton이 Lamp를 직접적으로 Dependeny하고 있었다. 하지만 [코드 2]에서는 SwitchButton이 Lamp대신 SwitchButtonInterface를 참조하도록 하였고, 각 Lamp와 Engine이 SwitchButtonInterface를 Inherit하도록 하여 Dependency를 Inversion하였다. 따라서 이제 SwitchButton은 Lamp와 같은 구현체와 Dependency가 전혀 없음으로 재사용이 가능한 코드가 되었다.

 

  가끔 Class Diagram만 보고 Strategy Pattern과 혼동하는 사람들이 있는데, 아래 [그림 8]을 보면 개념적으로 전혀 다른것을 알 수 있다.

[그림 8] DIP와 Stractegy Pattern과의 차이점

  [그림 8]에서 DIP의 Interface는 A에서 정의하고 B에서 구현하고 있다. 하지만, Stractegy Pattern에서는 그 반대다. Interface를 B에서 정의하고 A에서 의존한다.

 

  Dependency Inversion이 적용된 디자인과 그렇지 않은 디자인을 Application Architecture 레벨에서 바라본 모습은 아래와 같다.

[그림 9] Dependency Rot이 발생한 프로젝트

  [그림 9]는 Dependency가 정리되지 않아 의존성 부패가 발생한 Application Architecture의 모습이다. class들이 전부 스파게티처럼 엮여 있어 모두 재사용을 할 수 없는 상태다. 가독성도 떨어지며 유지 보수성도 좋지 않다. Dependency가 매우 강하기 때문에 각 클래스들은  Mock으로 대체될 수 없어 Unit Test도 불가능한 코드가 대부분이다. 그리고 이러한 디자인 균열은 계속해서 더 큰 균열을 불러온다.

[그림 10] Dependency Inversion이 적용된 프로젝트

  [그림 10]은 Dependency Inversion을 적용하여 의존성이 잘 정리된 Application Architecture이다. 상위 레이어와 하위 레이어의 높은 의존관계가 제거 되었고, 추상 레이어를 추가하여 두 레이어 모두 필요한 서비스를 추상 레이어를 의존하도록 하였다. 레이어가 모두 명확하게 정의 되었으며, 각 레이어는 통제된 인터페이스를 통해 응집성 있는 서비스만을 제공 되도록 디자인 되었다. 

 

  우리가 많이 알고 있는 MVC에 DIP가 적용된 모습은 아래 [그림 11]과 같다. 

[그림 11] MVC에서의 DIP

  요즘 MVC를 사용하면 Controller가 비대해진다느니 UI와 Business logic이 섞인다느니 하면서 MVC 자체에 문제가 있는 것처럼 말하는 사람이 많은데, 사실 내가 본 프로젝트들은 대부분 MVC 때문이 아닌 이런 의존성을 관리하지 못한 문제였다. 아마도 Dependency에 대한 이해가 부족한 상태면 MVC가 아닌 그 어떤 디자인 패턴을 적용해도 문제가 발생할 것이다.

  (예로 들면, 안드로이드에서 Activity를 Controller처럼 사용한다든지, 마치 대학교 1학년 때 절차 지향 언어인 C언어로 main함수 안에서 CUI 프로그램 개발하듯 모든 제어를 하나의 컨트롤러에서 중앙 집중 제어 방식으로 디자인하고 있었다. 혹은 dynamic behavior에 대한 고민 없이 물리적인 logic만 class로 나눈 static structure만 보고 만족해하는 경우도 있다. (MVC에 대한 자세한 이야기는 다른 글에서 설명하겠다.))

 

  다시 돌아가 DIP가 적용된 [코드 2] 예제에 대해 이야기해보자. SwitchButton과 Lamp에 Dependency만 Inversion한다고 의존성 문제가 해결될까?

 

[코드 3] Class Dependency

 

  위 [코드 3]에서 2line을 보면 SwitchButton이 concrete class인 Lamp를 직접 생성하고 있다. 의존성을 뒤집어 Interface를 참조하도록 하였지만, 아직 class dependency가 남아 있다. Factory Pattern을 적용하면 아래와 같이 Lamp와의 class dependency를 제거할 수 있다.

 

[코드 4] Factory Pattern 적용

 

  위 [코드 4]에서 Factory Pattern을 적용하여 SwitchButton의 Lamp class dependeny를 제거 하였다. Factory는  SwitchButton이 알지 못하게 SwitchButtonInterface를 구현한 어떤 concrete class를 반환할 것 이다.

[그림 12] Factory Pattern을 적용한 SwitchButton

  하지만, [그림 12] Factory Pattern을 적용한 SwitchButton을 보면 이제 Lamp 대신 Fatory를 강하게 Dependency 하고 있다. 이제 SwitchButton은 Factory와 한 몸이 되었다. 다른 프로젝트에서 재사용 하려면 결국 Factory를 수정해서 적용해야한다. 따라서 SwitchButton은 아직도 component가 되지 못한다. 또한 decoupling을 이런식으로 적용하게 되면 프로젝트 내의 모든 클래스들이 각각 자신만의 factory를 갖게 될지도 모른다. 

Dependency Injection (DI)

  의존성 주입(Dependency Injectioin, DI)라는 말을 들어 봤을 것이다. 용어만 보고 어려워하는 사람이 간혹 있는데, 사실 우리는 이미 프로그래밍을 처음 배울 때부터 사용하고 있었다.

 

'int main(int argc, char *argv[]) {}' 

 

  위 코드는 c언어에서의 entry point인 main 함수다. argument로 argc와 argv를 주입받고 있다. 이렇게 외부로부터 전달받는 것을 의존성 주입(Dependency Injection, DI)이라 한다. 그렇다면 이제 SwitchButton에 DI를 적용해 보자.

 

[코드 5] DI를 적용한 SwitchButton

 

  [코드 5]는 Constructor Inecjtion을 적용한 SwitchButton이다. (DI는 크게 Constructor Injection, Interface Injection, Method Injection으로 사용되지만, 좀 더 명확한 Constructor Injection을 선호한다.) 이제 SwitchButton은 Factory도 Lamp도 의존하지 않은 독립적인 존재가 되었다. 어떤한 concrete에 대한 dependency가 없으니 외부로부터의 변경사항에 대한 영향도가 매우 적어졌다. 따라서 SwitchButton은 어디에서도 수정 없이 곧바로 재사용 가능한 Component가 되었다.

 

Inversion of Control (IoC)

 DIP와 DI가 적용된 SwitchButton은 어떻게 사용될까? 아래는 SwitchButton을 사용하는 client 코드다.

 

[코드 6] SwitchButton을 사용하는 Client

 

  [코드 6]에서 Client가 Lamp를 생성해 SwitchButton에 주입하고 있다. Switchbutton은 Lamp를 모르게 됐지만, Client가 Lamp를 생성하고 SwitchButton과의 관계를 설정하고 있는 오히려 더 이상 해진 상황이 됐다. Client는 Lamp를 알 이유도 없으며 알아서도 안되는데 말이다.

[그림 12] 일반적인 프로그램에서 제어의 방향

  IoC가 적용되지 않은 일반적인 프로그램의 흐름은 [그림 12]처럼 entry point에서 다음에 사용할 오브젝트를 결정하고, 생성하고, 생성된 오브젝트의 메서드를 호출하고, 그 오브젝트 메서드 안에서는 또다시 다음에 사용할 것을 결정하고 호출하는 식의 작업이 반복된다. 각각의 오브젝트는 프로그램 흐름을 결정하거나 사용할 오브젝트를 구성하는 작업에 능동적으로 참여한다. 즉, Service를 사용하는 Client쪽에서 모든 걸 제어하고 있는 구조이다. 제어의 역전(Inverson of Control, IoC)이란 이러한 제어의 흐름을 Inversion하는 것을 의미한다.

 

[그림 13] 역전된 제어의 흐름

  [그림 13]은 역전된 제어의 흐름을 보여준다. entry point에서 IoC Container에게 모든 관계 설정에 대한 책임을 위임한다. 따라서 컴파일 타임의 static한 class dependency가 런타임의 dynamic한 object dependency로 변경된 것을 볼 수 있다. [그림 14]는 SwitchButton에 IoC개념이 적용한 모습이다.

 

[그림 14] IoC가 적용된 SwitchButton

 client가 IoC Container에게 필요한 Object를 요청하면 IoC Container는 SwitchButton이 필요한 object를 생성하고 관계를 wiring 하여 전달한다. 각 class들이 다른 class에 대한 dependency가 모두 사라졌으니 이제 모든 class들은 component가 될 수 있다. 그리고 분리된 모든 class들은 전부 mock으로 대체될 수 있어 testability도 높아졌다.  IoC만 바뀌면 dynamic 하게 전혀 다른 동작을 하는 프로그램이 될 수도 있다.

 

  가끔 IoC Container를 Factory Pattern과 혼동하여 IoC 관련 라이브러리를 그냥 Factory처럼 사용하는 경우도 많이 보았는데, Factory는 단순히 object를 생성하는 assembler에 가깝고 IoC Container는 거기에 제어의 역전 개념이 적용되어야 한다. IoC Container를 그냥 사용한다고 제어가 역전 되는 게 아니다.

Conclusion

  지금까지 의존성의 방향과 제어에 대한 설명을 하였다. 애플리케이션 개발에 있어서 기초적인 것은 아니지만 반드시 필요한 기본적인 개념이다. 모든 class에 대해 해당 개념이 적용되어야 한다는 뜻은 아니다. 분명히 재사용성과 유지 보수성이 좋아지겠지만 코드의 복잡도는 증가할 것이다. 하지만, 지속 가능한 소프트웨어를 개발하기 위해서는 의존성이 완전히 부패되기 전에 기본적인 관리가 필요하다. 기본적인 관리라 함은 Layer를 명확히 정의해야 하며, 각 Layer는 통제되어야 하고 약속된 인터페이스를 통해서만 커뮤니케이션을 해야 하는 것을 말한다. 즉, 적어도 각 Layer들끼리의 Dependency 관리는 반드시 필요하다 게 개인적인 생각이다. 물론, 간결한 코드와 우아한 코드의 밸런스를 적절히 맞추는 게 쉽진 않겠지만 의존성이 망가져버린 프로젝트를 유지 보수하는 것보단 나을 것이다.

'Architecture & Design Pattern' 카테고리의 다른 글

if문을 없애는 디자인  (0) 2019.11.17
Data Access Object Pattern  (0) 2019.09.07
Singleton Pattern의 함정  (0) 2019.07.14
iOS Clean Architecture with Swift  (1) 2019.07.13

+ Recent posts