아키텍처는 시스템의 동작 원리를 나타내는 것으로, 소프트웨어를 구성할 때 가장 밑바닥에 전제되는 구성 방법이자 프로젝트에 참여하는 개발자간의 약속입니다. 어떤 식으로든 합의된 구조로 코드가 올라가게 됩니다. 가장 기본적인 아키텍처는 MVC 입니다. 구글이나 애플같은 퍼스트파티에서 제안하는 가장 기본적인 구성입니다. 자료제공은 Model, 화면 구성은 View, 처리 방법은 Controller 등으로 구분해서 작성하게 됩니다. 단순히 코드가 길어져서 파일을 나누는 것이 아니라 역할에 따라 특정 영역을 구분하는 것이죠.

핵심은 유지보수

우리는 프로젝트가 시작할 때 아키텍처를 합의하고 갑니다. 어떤 개발자에게는 이 틀이 불편할 수도 있습니다. 그럼에도 불구하고 사용하는 이유는 딱 하나 “유지보수” 때문입니다. 이 시스템이 무슨 일을 하는지, 문제가 발생하면 어디부터 살펴야 하는지, 제거를 위해서는 어디를 손봐야 하는지 등에 대한 형식적인 틀을 제공합니다.

대개 아키텍처를 찾아보는 개발자들은 자신이 맡은 프로젝트가 망가지는 경험을 한 번쯤 했을 것입니다. 땜빵에 땜빵을 거듭하다가 “이거 왜 이렇게 개판이지?”하면서 좌절하고, 뭘 좀 바꾸자고 하면 “그건 안돼요.” 라고 하면서 파트 교체 시 발생할 수많은 의존성 때문에 착수 전부터 피로감이 몰려오는 그런 경험 말이죠.

유지보수는 첫 릴리즈보다도 훨씬 더 긴 호흡으로 우리를 압박합니다. 다른 개발자가 작성한 코드를 당신이 수정할 수도 있습니다. 이 과정에서 방향을 잡지 못하면 큰 혼란이 야기됩니다. 유지보수가 용이하다는 말은 테스트가 쉽다는 말과 같고, 파트 교체 시 후유증이 덜하다는 의미입니다. 코드를 수정하거나 덜어내는 것이 문제 없어야 새 기능도 추가할 수 있고 프로젝트가 덩치를 키워 나갈 수 있는 것이죠.

유지보수는 그만큼 중요합니다.

MVC 는 나쁜 패턴인가?

MVC는 대단히 직관적입니다. 그러나 MVC는 매번 새로운 아키텍처가 소개될 때마다 두들겨 맞습니다. 그 이유로 M-V-C 간 의존성이 높아서 테스트하기 어렵고 프로젝트가 커질수록 복잡해진다는 것입니다.

과연 그럴까요? 사실은 그렇지 않습니다. MVC가 그렇다면 다른 아키텍처에서도 동일한 문제가 발생합니다. 우리 선배들이 만든 수많은 소프트웨어가 이 기반으로 작성되었고, 매우 훌륭하게 유지보수를 해왔던 걸 기억해야 합니다. 그렇다면 혹시 우리가 MVC 를 잘못 사용해서 발생한 문제는 아닐까요?

퍼스트에서 제공하는 샘플코드는 구현 동작을 집약적으로 보여주는 데 목적이 있습니다. 우리는 이것을 보고 오해를 합니다. 마치 이대로 작성하라는 것처럼 보이거든요. Android 는 아무렇지도 않게 Fragment 에서 Room 을 호출하고, iOS 역시 ViewController 에서 CoreData 에 별 생각 없이 접근합니다. 웹이라고 다를 게 없습니다. JSP 에서 JDBC 를 바로 연결하면서 복잡하다고 투덜거리죠. 이건 사실 개발자 스스로 MVC 를 깨버리는 행위입니다.

저는 이것이 Controller 라는 이름에서 비롯된 오해라고 생각합니다. 이름만으로는 무엇을 컨트롤하는지 알 수 없습니다. 어디든 갖다 붙일 수 있는 단어니까요. 이런 오해는 Model 도 마찬가지입니다. 어떤 모델을 말하는 것인지 불분명합니다. 그래서 조금 더 구체적으로 그려보겠습니다.

Label  ─┐
Button ─┼─ UI Controller ─ Logic Controller ─ Data Controller ─ Storage
Field  ─┘
========================   ================   =========================
         View                  Controller               Model

좀 더 엄격하게 구분해보면 MVCController 는 로직파트가 됩니다. 이 컨트롤러가 다듬어주는 자료를 뷰가 표현하는 것이죠. View 는 화면에 보이는 라벨, 버튼, 필드같은 컴포넌트 그 자체를 말하기도 하지만, 그 생사를 결정하고 유저 리쿼스트를 직접 받는 파트까지 뷰의 영역입니다. 아키텍처에서 칭하는 Model은 데이터 컨트롤러를 말합니다. 보통은 데이터베이스 같은 스토리지에 연결되어 자료를 CRUD 하죠. 그러나 프로젝트에서 모델은 데이터 타입을 가리킵니다. 예를 들어 PersonRepository 가 데이터베이스를 핸들링 하여 최종적으로 Person 객체를 반환할 때, 아키텍처 관점에서 모델은 PersonRepository 지만, 프로젝트에서 모델은 Person 클래스입니다.

이렇게 보면 흔히 MVC 라고 믿었던 패턴이 실제로는 VM 이라는 것을 깨닫게 됩니다. 그렇다면 Controller 는 어디 있는 걸까요? 바로 델리게이트입니다.

user
  │
  └──▶ -[onTouch] ──────────▶ -[execute] ────────▶ -[search]
            ┌──┐                 ┌──┐                 │
            │  └──◀──────────────┘  └──◀──────────────┘
            ▼           Human(name)          Person(first, last)
  ◀─── -[refresh]
========================   ================   ================
      HumanBoard             FindDelegate     PersonRepository
========================   ================   ================
         View                 Controller            Model

HumanBoard 는 Label, Button, Field 등 뷰 컴포넌트를 관리합니다. 그리고 터치가 떨어지면 FindDelegate 에게 작업을 위임합니다. FindDelegatePersonRepository 에게 이를 찾아달라고 하고, 이리하여 반환된 Person 데이터를 가공하여 Human 데이터를 만들게 됩니다. HumanBoard 는 바뀐 Human 의 값에 따라 자신이 소유한 컴포넌트를 고치게 되죠. 이런 구조에서 View 는 그래픽 표현에, Controller 는 데이터 가공에, Model 은 데이터 제공에 집중할 수 있습니다. 각자의 영역에 맞는 동작을 하게 되는 것이죠.

저는 MVC 만 되어도 매우 훌륭한 아키텍처라고 생각합니다. MVC 때문에 의존성이 높아지고 복잡도가 증가하는 것이 아닙니다. 의존성이 높아지는 것은 캡슐화를 어겼기 때문이고, 복잡도가 증가하는 것은 역할을 중복으로 맡았기 때문입니다.

격리의 원칙

MVC는 관심사가 엄격하게 분리된 Layered 이며 내부 동작은 그 계층에 격리되기 때문에 인접한 인터페이스를 통해서만 소통할 수 있습니다. 캡슐화라고 합니다. 쉽게 말해 View 에서 Model 에 직접 접근할 수 없으며, 반드시 Controller 를 통해서만 작업을 완료할 수 있다는 뜻입니다. 이것은 뷰와 모델 사이를 완충해줄 수 있는 추상계층을 하나 만드는 것과 같습니다.

격리된다는 것은 함수의 가시성뿐만 아니라 데이터 타입의 가시성도 포함됩니다. 예시에서 보듯이 Model 이 반환하는 Person 타입을 View 에서는 볼 수 없습니다. 뷰는 Human 만 알고 있어야 합니다. 그래서 Controller 는 자신이 리턴받은 Person 을 Human 으로 변환하여 뷰에 제공하게 됩니다. 심지어 Person 과 Human 의 내부 프로퍼티가 완전히 동일한 경우에도 재포장됩니다.

    Human(name)
 ┌──────────────┐
 ▼              │
View ───╫─── Controller ───╫─── Model
                    ▲            │
                    └────────────┘
            Person(first, last)

흔히들 안드로이드라면 Room 에서 정의한 Entity 객체를 LiveData 에 담아 처리하고 싶을 것이고, iOS라면 CoreData 의 FetchedResults 를 데이터소스로 삼아 NSManagedObject 를 바로 사용하고 싶을 겁니다. 그러나 이렇게 하면 격리를 깨버리게 됩니다. 교체할 일이 절대 없다고 하더라도 그렇게 해서는 안 됩니다. 가능하다는 것과 해도 된다는 것은 동일하지 않습니다. 아키텍처는 약속입니다. 우리는 아주 약간의 오버헤드를 감수하고서라도 이 약속을 지키고자 하는 것이고, 그렇게 원칙이 세워져 있어야만 유지보수 시 혼란을 줄일 수 있습니다.

우리가 어떤 종류의 컨트롤러를 만들면 거의 대부분 private 로 선언합니다. 자신이 하는 세부 작업은 숨기죠. 자신이 품고 있는 도우미 객체를 숨기고, 자신이 취급하는 자료타입도 숨깁니다. 상위 레이어에서 요구하는 인터페이스만 구현하여 공개합니다. 또 상위 레이어와 합의된 데이터 타입으로 반환하게 됩니다. 우리는 경험을 통해 알고 있습니다. 뭐든 다 교체될 수 있다는 것을요.

MVC 의 진화 MVVM

MVVMMVC 의 연장선에 있으며 역시나 매우 직관적인 구조입니다. ControllerViewModel 로 이름만 바뀐 듯이 보입니다. 그러나 중요한 차이점이 있습니다. 컨트롤러가 반환하는 값을 뷰가 취급하는 것이 아니라 컨트롤러의 상태에 따라 뷰가 반응하게 만든 것이죠.

   View        Controller        Model
==========     ==========     ==========
   View        ViewModel         Model
----------     ----------     ----------
- onTouch  ──▶ - execute  ──▶  - search
                   ┌──────◀─────────┘
- refresh  ◀── @ state

뷰모델은 자신의 작업 결과를 반환하지 않고 자신의 State 에 담게 됩니다. 뷰는 이를 바인딩하여 감시하다가 상태가 바뀌면 자신도 refresh 할 겁니다. 이는 약간의 오버헤드가 더 발생하는 구조입니다. 그럼에도 왜 이렇게 했을까요?

일반적으로 델리게이트 컨트롤러는 자료를 반환하면서도 그것이 뷰에서 어떻게 표현될지 알지 못합니다. Ui Controller 를 테스트하면 될 것 같지만 그 자신이 뷰 컴포넌트를 포함하기 때문에 Unit Test 가 조금 곤란합니다. 그렇다고 Ui Test 를 하기에는 부담이 큽니다. Ui Test 코드는 작성하는 게 정말 짜증나고 별 실효성도 없습니다. 저는 화면을 띄워보고 만저볼 뿐이지 따로 Ui Test 를 하지는 않습니다.

@stateA       ◀─┐
@stateB    ◀─┐◀─┤
- exec1()  ──┘  │
- exec2()  ─────┘

뷰모델은 Ui 에 표현될 값을 State 에 담기 때문에 테스트하기 더욱 쉽습니다. 특히 하나의 작업 결과에 대해 여러 상태가 바뀔 때 유용합니다. 즉 뷰모델의 어떤 함수처리 결과로 Ui가 어떻게 변할지 알기 쉽다는 뜻입니다. 그래서 뷰모델까지만 Unit Test 해도 충분해집니다.

뷰모델은 뷰의 그림자이자 원형입니다. 뷰에서 그래픽으로 꾸며진 옷을 싹 벗겨버리면 뷰모델이 됩니다. 이와 같은 특성으로 인해 MVVM 에서는 뷰와 뷰모델을 1:1 로 붙이는 것이 목적에 맞아 보입니다. 그러나 이렇게 되면 MVC 에서 델리게이트를 1:N 으로 붙여 로직 복잡도를 줄이고자 했던 것에 비해 나아보이지 않습니다. 뷰모델이 비대해지는 것은 시간문제죠. 그냥 뷰모델을 여러 개 만들어서 1:N 으로 붙여버릴까요? 이걸 어떻게 해결할 수 있을까요?

View - ViewModel
        - exe1 ───────────┐
        - exe2 ───────┐   ├──▶ Action1 ────▶
                      └──────▶ Action2 ────▶
View - ViewModel          │ ┌▶ Action3 ────▶
        - exe1 ───────────┘ │
        - exe3 ─────────────┘

그렇습니다. 우리가 MVC에서 했던 것처럼 뷰모델에 목적별로 개별 델리게이트를 붙이면 됩니다. 이와 관련해서는 다음 포스트에 쓰겠습니다. 오늘은 여기까지만 하겠습니다.