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

  if문은 특정 조건에 따라 프로그램을 제어하기 위해 사용한다. 그렇기 때문에 요구사항(조건)이 변경되면 영향도가 가장 많이 발생하는 코드 중 하나이다. 일반적으로 우리는 서비스를 분기할때 다형성을 통해 클래스를 디자인하여 (ex. strategy, policy pattern 등) if문을 없애곤 한다. 하지만 이렇게 나뉘어진 서비스들도 각 함수내에 목적 코드를 실행하기 전 제어를 위한 중복된 if문들이 생기게 된다. 경험상 모두가 알다시피 이러한 중복된 if 조건들은 변경사항이 발생하면 프로젝트내에 모두 찾기하여 일일이 수정해야 하기 때문에 분명 좋은 코드가 아니다.

 

  아래 몇가지 패턴은 내가 자주 쓰는 패턴 중 하나다. if 조건들을 오브젝트화 하여 핵심관심(Core Concerns)과 횡단관심(Crosscutting Concerns)을 분리(Computer Science, SoC)하고 중복코드를 없애 재사용이 가능한 상태로 만드는 디자인이다. (물론, AOP를 적용하면 좀 더 우아하게 처리 할 수도 있다.) 

Interceptor Filter Pattern

  코드에서 대부분의 if문들은 아래와 같은 형태를 띄고 있다. [그림 1]은 함수내에서 원하는 목적 코드를 수행하기 전 인증, 권한, 데이터 유효성 확인 등과 같은 전처리를 수행하는 코드를 예로 든다. 보통 이러한 전처리 코드들은 경험상 중복되는 경우가 많으며 변경 가능성이 높은 코드들이다. 

[그림 1] if 조건에 따른 필터 처리

  [그림 1]에서 글쓰기 권한을 '준회원'에서 '일반회원'으로 변경되면 우리는 해당 if문들을 찾아 일일이 수정해야한다. 예시가 간단하기 때문에 쉽게 수정할 수 있다고 생각하겠지만, if문이 많은 그리고 복잡도가 높은 실제 프로젝트내에서 중복 코드의 변경은 개발자의 실수가 충분히 발생할 수 있다. 그리고 이러한 횡단관심 코드들은 해당 함수의 핵심관심을 읽기 어렵게 만든다. 즉, 한 마디로 코드가 더러워 진다. 

 

  위와 같은 경우에는 Interceptor Fileter Pattern을 사용하여 조건들을 오브젝트화 하여  중복코드를 없애면 코드를 좀 더 깔끔하게 만들 수 있다.

[그림 2] Interceptor filter class diagram

  Interfceptor Filter pattern은 조건들을 오브젝트화 하여 중복 코드를 없애고 여러 조건들을 조합하여 재사용할 수 있도록 한다. 또한, 조건 코드와 목적 코드의 디팬던시가 제거됨으로 변경에 따른 영향도를 최소화 할 수 있다.

[그림 3] Interceptor filter sequence diagram

  시퀀스는 간단하다. Client는 FilterChain에 target과 filter들을 추가하고 FilterChain.doFilter()를 호출한다. FilterChain은 첫 번째 Filter를 호출하고 호출된 해당 Filter는 주어진 조건을 확인하고 판단하고 다시 FilterChain..doFilter를 호출하면 다음 Filter를 호출하게 되고 FilterChain.stopFilter를 호출하면 중단되며 target은 실행되지 않는다. 모든 filter들이 실행되어 통과되면 target은 그 때 실행될 수 있다.  각 요구사항에 따라 디테일한 동작은  변형하여 디자인 하면 된다. 수도코드를 보면 아래와 같다.

 

1. FilterChain

2. Filter

3. Target

4. Client에서 사용할 때

 

Chain of Responcibility Pattern

  [그림 1]처럼 하나의 목적코드를 실행하기 위해 전처리 조건들을 사용할때도 있지만, [그림 4] 다음과 같이 조건에 따른 여러 분기를 하여 그에 알맞은 목적 코드를 실행 할 때도 있다. 이럴때는 Chain of Responsibility Pattern를 사용하여 조건들을 오브젝트화할 수 있다. 

[그림 4] if 조건에 따라 목적코드를 분기하는 경우

  [그림 4]는 특정 조건에 따라 목적 코드를 분기하는 경우다. Chain of Responsibility Pattern의 컨셉은 자신의 조건이 맞으면 수행하고 그렇지 않으면 패스한다. else if로 만들수도 있고 if, if, 조건으로 만드는 등 각 요구사항에 맞게  Varient하여 디자인 하면 된다. [그림 5] 아래는 log level에 따라 각 Logger들이 로깅을 할지 말지를 판단하는 체인을 구현한 예시다. 

[그림 5] Logger class diagram

 

[그림 6] Logger sequence diagram

1. CommonLogger

2. concrete Logger 

3. Client 사용할 때

5. 호출 결과

consoleLogger.logMessage(logLevel: .info, message: "info message!!")

 -> console logger info message!!

 

consoleLogger.logMessage(logLevel: .error, message: "error message!!")

 -> console logger error message!! 

      file logger error message!! 

      error logger error message!!

 

  Application에서 Persistent Data를 다루는 방법엔 여러가지가 있다. RDBMS를 사용하거나 단순히 txt로 저장할 수도 있고 네트워크를 통해 외부 시스템을 사용할 수도 있다. 그리고 각 저장 방식에는 메커니즘이 다르기 때문에 구현방법도 모두 다르다. 그렇기 때문에 Persistent Logic은 외부 환경과 Dependency가 매우 강하다. 따라서 Application Logic과 Persistent Logic은 섞이면 안되며 서로 독립된 레이어에 존재해야 한다. 만약 그렇지 않고 로직이 섞인 상태에서 Data Source가 바뀌게 된다면 Application Logic까지 영향도(Force)가 전달되어 변경 전파(Change Propagation)가 발생하게 된다. 

 

[그림 1] DAO Class Diagram

  [그림 1] Data Access Object Pattern은 특정 Data Source에 접근하는 로직을 추상화하고 캡슐화 한다. DAO는 DataSource의 메커니즘을 구현하고 단순한 API만을 Client에게 제공한다. (이때, DataSource에서 사용하는 ErrorCode나 Exception같은 것도 역시 상위 레이어로 전달하면 안되며, 모두 wrapping하여 상위로 전달 해야한다.) 이제 Client에서는 DAO를 생성하고 저장해야할 Value Object를 생성하여 DAO에 전달하기만 하면 된다. 이렇게 Layer를 분리하면 DataSource가 Oracle에서 외부 네트워크 시스템으로 변경되어도 Client의 수정없이 DAO만 변경하면 된다.

 

[그림 2] DAO Sequence Diagram

  [그림 2]는 Client가 DAO를 통해 데이터를 읽을때의 Sequence Diagram을 나타낸다. DAO는 Data Source를 통해 데이터를 읽어와서 Value Object로 변환하여 반환하고 Client를 Value Object를 통해 실제 데이터를 사용한다.

 

  수도 코드로 예를 들면, 회원 정보를 저장하는 DAO는 아래와 같이 구현될 수 있다. 

  만약 Data Source와 관련된 로직을 추상화 할 수 있다면, 아래와 같이 Generic으로 추상화하여 CommonDAO를 만들 수 있다. 그렇게 되면 앞으로 생성되는 DAO class들은 정의만 하면 되고 중복 구현은 추상화 되어 없앨 수 있다. 그리고 특정 서비스에서만 사용되는 로직들만 확장하여 사용하면 된다.

 

 

[그림 1. 우리가 기대하는 Singleton]

  Runtime에 유일한 상태를 공유하기 위해서 singleton을 많이 사용하는데, 우리가 singleton class를 만들때 기대하는 바는 위 [그림 1]과 같다.  Singleton은 다른 object들과의 dependency 관계가 쉽게 맺어지고 사용될 수 있도록 구현되어 있다. 이게 singleton의 장점이자 단점으로 다가오는데, 시간이 지나면서 개발자들은 singleton이 존재해야 할 레이어에 대한 인지가 점점 떨어진다. 특히 급하게 처리되어야 할 문제들이 쉽게 해결할 수 있는 singleton으로 모이게 되는데, 이런식으로 처음 의도와는 다르게 여러 service들이 하나의 singleton을 의존하게 된다. 그리고 여러 service들에 의해 많은 기능들이 추가 되면서 점점 large class가 되어 간다. 심지어 callback을 주기위해 역참조하는 경우도 발생한다.

 

[그림 2. Large class가 되어버린 Singleton과 Dependeny rot]

  [그림 2]처럼 service A를 위해 만들어졌던 func1이 다른 service들이 사용하기 시작한다. service A의 요구 사항이 변경되어 func1이 변경된다면 이를 사용하는 모든 service들에게 변경 전파(Shotgun Surgery)가 이루어진다. 그리고 이러한 coupling 문제를 개선하기 위해 singleton을 리팩토링 할 때의 영향도(Force)는 어플리케이션 전체가 된다.

 

  Singleton은 class dependency라서 mock으로 대체 될 수 없다. 이는 singleton을 사용하는 service들의 unit test를 작성하는데 어렵게 만든다. singleton의 변경에 따른 영향도는 굉장히 높지만 그 변경에 대한 검증이 매우 부족한 상태라는 의미다.

 

  Singleton에 대한 class dependency 문제를 해결하기 위해서는 의존 관계가 외부로부터 injection 되어야 한다. 그리고 client는 이렇게 주입받은 object가 singleton instance인지 일반 object intacne인지 모르는 상태에서 사용되어야 한다. 또한 이 object는 interface에 의존하도록 디자인 되어야 한다. 이는 통제된 인터페이스를 통해 정의된 서비스를 제공하도록 디자인됨을 의미한다. 그리고 해당 object의 유일성은 Service Locator같은 녀석이 책임지고 dependency wiring은 IoC Container로부터 injection 받도록 디자인 해볼 수 있다.

iOS Clean Architecture를 디자인하였고 해당 architecture를 통해 TODO list를 저장하는 샘플앱을 만들어 보았다.

github : https://github.com/ezero9/iOSCleanArchitecture

1. Architecture 

[그림 1. Architecture]

  Architecture는 일반적으로 많이 사용되고 있는 3 Layer Architecture로 구성한다. 본 Architecture[그림 1]는 명확하게 정의된 레이어를 가지고 있으며, 각각의 레이어는 잘 정의되고, 통제되는 인터페이스를 통해 응집성 있는 서비스의 집합을 제공한다. 

 

  Object간의 dependency 관계는 IoC Container를 통해 injection받는다. 각 class들의 동작에대한 구성은 아래와 같다. [그림 2] 하위 레이어에서 상위 레이어로의 change propagation은 RxSwift나 delegation pattern, callback등을 통해 notify하는 방식으로 active하게 디자인한다.

 

[그림 2. 동작 구성]

2. IoC Container

[그림 3. IoC Container Class Diagram]

  일반적인 IoC Container 라이브러리들의 무거운 기능들은 전부 제외하고 DI, Singleton지원과 같은 필수적인 기능들만 구현하여 심플하게 제공한다. (Swift IoC 라이브러리가 없어서 직접 구현함)

 

Swift에서의 DI(Dependency Injection): https://develogs.tistory.com/21

2.1 Dependency Injection

  Object를 생성하고 각 object들간의 관계를 wiring하는 책임은 모두 IoC가 갖는다. client는 자신이 사용할 object instance를 IoC를 통해 DI받는다. 따라서 client에서는 IoC가 주는 object가 어떤 concrete class로 부터 생성이 되었는지 알필요 없으며, 정의된 interface를 통해 동작하기만 하면된다. 이는 object들간의 관계가 변경 전파 없이 같은 interface로 구현된 mock나 다른 class의 object instance로 언제든 대체될 수 있음을 의미한다. IoC를 사용하여 object들과의 관계를 설정할때 되도록이면 constructor injection을 사용하는게 좋다.

 

[Post기능과 관련된 object들의 의존성을 구성하는 부분]

 

  IoC(Inversion of Control, 제어의 역전)과 DI(Dependency Injection, 의존성 주입)에 대한 자세한 설명은 아래 링크에서 확인 할 수 있다.

제어의 역전(Inversion of Control, IoC) 이란?: https://develogs.tistory.com/19

2.2 Singleton 지원

  Singleton이 필요할때는 Singleton Pattern으로 class로 디자인하지 않고 IoC에 singleton으로 register한다. clinet에서는 IoC를 통해 필요한 object가 injection되면 해당 object가 singleton인지 일반 object인지 모르는 상태에서 사용되어야 한다. 

 

Singleton Pattern으로 디자인 하지 않은 이유: https://develogs.tistory.com/8

3.  Presentation Layer

[그림 4. BaseViewController와 BaseViewModel Class Diagram]

  Presentation Layer는 [그림 2]에서 보았듯이 MVVM Design Pattern을 사용한다. BaseViewController는 toast, popup, progress같은 ViewController의 공통 로직이 구현되어 있으며, BaseViewModel도 해당 ViewController와 interaction을 위한 공통 로직이 구현되어 있다. ViewController는 controller의 역할보다는 하나의 View로 보고 있으며 거기에 더해 View와 ViewModel과의 glue code가 존재하는 곳이다.

(재사용 되는 뷰가 없으면 오히려 mvvm의 기반코드를 작성하는게 이점대비 비용이 더 클수도 있다. View들의 도메인 요구사항에 따라 MVVM, MVP, MVC, VIPER등 무엇을  쓸지 design을 잘 결정하자.)

3.1 Data Binding

[그림 5. DataBinding]

  View와 ViewModel은 RxSwift, RxCocoa 라이브러리를 사용하여 two way binding을 한다. ViewModel은 binding해야 될 데이터를 정의해야하며, DataBinding Interface를 통해 바인딩 전략을 구현해야한다.

 

[AllPostViewModel에서 DataBinding 전략을 구현한 부분]

 

[AllPostViewController에서 AllPostViewModel과 binding하는 부분]

3.2 Routing Mechanism

[그림 6. Navigator Class Diagram]

  MVVM에서는 controller가 없기 때문에 Routing Mechanism이 필요한데, 그 역할은 Navigator가 담당하게 된다. Navigator는 자신이 이동해야 될 ViewController 혹은 Navigator를 IoC를 통해 전달 받아 정해진 interface를 통해 동작하게 된다. 

 

[BaseNavigator에서 View를 받아서 present하는 부분]

4. Domain Layer

[그림 7. 상위 레이어에서 requirement를 정의하고 하위 레이어에서 concrete를 구현]

  Application Logic을 담당하는 Layer임으로 Domain Layer는 반드시 다른 레이어들과 독립성을 유지해야한다. 다른 레이어들의 dependency가 존재하면 안되기 때문에 DIP(Dependency Inversion Principle)가 지켜져야 하는 레이어이다. Domain Layer에서는 필요한 requirements를 정의하고 Psersistent Layer에서는 이 정의된 requirement interface를 이용해 concrete class를 구현한다.

 

  여기서 다루는 내용중에 Domain Layer의 isolation이 가장 중요하다고 할 수 있다. 위에서 말했듯이 Presentation Layer에서의 view design pattern은 View 요구사항에 따라 바뀌겠지만 domain layer의 isolation은 반드시 지키는게 좋다.

5. Persistent Layer

[그림 8. Dependency Inversion 되어 구현되고 있는 Persistent Layer]

  Persistent Layer는 data source에 대한 decoupling을 위한 layer이며 Domain Layer에서 정의된 requirement interface를 통해 concrete class를 구현한다. Persistent Layer의 object들도 마찬가지로 IoC를 통해 Domain Layer에 injection되며, 여기서 injection된 object는 다른 구현체로 변경된다 하더라도 절대로 상위 레이어로 변경 전파가 이루어지지 않아야 한다.

6. Error Handling

[그림 8. TraceError Class Diagram]

  error stack trace를 확인하기 위해 ErrorTrace라는 최상위 error를 정의했다. ErrorTrace를 상속받아 명확한 이름을 가진 Error를 정의하고 예외는 do try catch 구문으로 처리한다. 

 

[그림 9. Error Wrapping]

  error는 하위 레이어에서 상위 레이어로 thorw될 때, 상위 레이어에서 requirement로 정의된 좀 더 구체적인 이름을 가진 error로 wrapping하여 올려 준다.

 

getStackTrace()을 찍어보면 아래와 같이 layer별로 명확한 이름으로 정의된 error의 stack trace를 확인 할 수 있다.

🔶[ERROR TRACE]🔶

🔶 iOSCleanArchitecture/Scenes/AllPost/AllPostViewModel.swift 50: viewDidLoad() - PostInterface.LoadFailError: not found Post Data. 

    🔶iOSCleanArchitecture/Domain/Model/AllPostModel.swift 42: parsing() - PostInterface.ParseFailError: parsing failed. 

        🔶 iOSCleanArchitecture/Repository/PostNetworkDAO.swift 35: network() - PostNetworkDAO.NetworkError: 404 error   

 

Error handling - 우아하게 실패하는 방법 : https://develogs.tistory.com/9

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

제어의 역전(Inversion of Control, IoC) 이란?  (16) 2019.12.08
if문을 없애는 디자인  (0) 2019.11.17
Data Access Object Pattern  (0) 2019.09.07
Singleton Pattern의 함정  (0) 2019.07.14

+ Recent posts