Krzysztof zablocki architecture header

iOS 애플리케이션 아키텍처 : MVVM, MVC, VIPER 전격 비교

MVVM, MVC, VIPER 등 다양한 아키텍처 디자인 방법 중 어떤 것이 가장 좋을까요? 이 강연에서는 좋은 iOS 앱 아키텍처를 구성하기 위해 필요한 요소가 무엇인지 알아봅니다.


소개(0:00)

New York Times에서 일하고 있는 Krzysztof Zabłocki입니다. 오픈 소스 또는 제가 만든 Foldify 앱을 통해 제 이름을 들어보신 분들도 있을 겁니다. 어쩌면 Objective-C Playgrounds 프로젝트에서 제 이름을 만나셨을지도 모르겠습니다. Apple은 Objective-C용 Playgrounds는 불가능하다고 말했지만 저는 Objective-C Playgrounds 프로젝트에서 이것이 가능하다는 사실을 증명했습니다.

오늘 저는 좋은 아키텍처란 무엇인지에 대해 이야기하고자 합니다. 이것은 매우 중요한 주제이면서도 많은 논란이 되는 주제이기도 하지요. 좋은 아키텍처란 무엇인지 어떻게 정의할 수 있을까요? 이 주제는 항상 과열되기도 쉽고 다루기도 어렵다는 점에서 탭과 스페이스에 대한 논쟁과 유사한 측면이 있습니다.

이 주제에 대한 강연을 준비하면서 수많은 디자인 패턴 중 어떤 이야기를 해야 할 지 많이 고민했습니다. SOLID나 Uncle Bob의 The Clean Architecture 등 다양한 개념들이 있는데, 이들에 대한 정보는 전문 서적에서 찾을 수 있으며, 저는 The Gang of Fours처럼 그렇게 똑똑한 사람이 아니므로 이번 강연에서 이런 개념들에 관해 설명하지 않을 예정입니다.

대신 이 주제를 iOS 개발과정에서 겪을 수 있는 좀 더 실용적인 측면에서 살펴보고자 합니다. 저는 전체 프로젝트에 대한 코드 리뷰를 담당하는 컨설턴트 업무를 여러 차례 수행하면서, 아키텍처를 향상할 수 있는 방법과 올바른 개발 방향, 개선 방법 등에 대해 프로젝트팀에 조언했습니다.

제가 중요하다고 생각하는 것을 중심으로 이야기할 예정인데, 어떤 분들에게는 저의 의견이 편향적으로 느껴질지도 모르겠습니다. 이번 강연에서는 Apple의 샘플 코드가 왜 나쁜지, 그들은 왜 그렇게 하는지에 대해서도 알아보겠습니다. WWDC 2016에서 Apple은 매우 멋진 디자인 패턴 두 개를 선보였지만, 이 패턴들을 잘못된 형태로 사용했습니다. 이 부분은 잠시 후 살펴보겠습니다. 제가 자주 사용하는 아키텍처인 MVVM과 관련해서는, 사람들이 MVVM을 사용할 때 자주 실수하는 이유와 개선 방법에 대해 알아보고, 다른 아키텍처들도 잠깐 살펴보도록 하겠습니다.

좋은 아키텍처란?(2:26)

좋은 아키텍처란 무엇일까요? 특정 애플리케이션의 아키텍처에서 제가 항상 원하는 몇 가지 특성들이 있습니다.

각각의 객체들은 구체적이고, 명확한 역할을 가지고 있어야 합니다. 그것은 쉽게 이해할 수 있고, 쉽게 변경할 수 있어야 하며, 소스코드를 읽었을 때 이것이 하나의 역할을 실제로 충족하고 있는지 또는 작성하려고 하는 로직이 그것을 위반했는지 즉시 알 수 있는 구조여야 합니다.

또한, 단순한 데이터 흐름을 가져야 합니다. 이해하기 쉽고, 크래쉬 또는 에러가 발생했을 때 쉽게 디버그할 수 있어야 합니다. 여러 개의 다른 객체들 사이를 왔다 갔다 해야 하는 구조는 피하는 게 좋을 겁니다. 같은 공유 자원(shared resource)을 조작해서 사용할 경우 오류 원인을 찾기 힘들므로 이런 구조도 좋지 않습니다. 데이터는 단방향 흐름(unidirectional data flow) 형태가 가장 좋습니다. 특정 지점에 브레이크를 설정하고 데이터에 어떤 변화가 일어나는지 확인할 수 있기 때문입니다. 이러한 기능을 지원하는 아키텍처가 실재합니다.

아키텍처는 특정 프레임워크나 서비스에 종속되지 않아야 합니다. 왜냐하면, 프로젝트에서 사용한 특정 프레임워크 또는 서비스와 제가 작성한 코드 사이에 간단한 추상화 계층이 없는 경우 이러한 종속성이 얼마나 큰 어려움을 초래하는지 20년 이상의 프로그래밍 경험을 통해 잘 알고 있기 때문입니다. 특정 프레임워크 또는 서비스가 종료되거나 유사한 일이 발생할 때마다 저는 이 사실을 언급하곤 합니다.

아키텍처는 쉽게 이해할 수 있고 쉽게 바꿀 수 있는 유연함을 갖춰야 합니다. 여기서 말하는 유연함은 필요 이상으로 복잡한 것이 아니라 단순한 것을 말합니다. 200개의 추상 클래스가 존재하고 모든 것이 추상화되며 프로젝트에 참여하는 누구도 이해하지 못하는 구조이거나 새로운 기능 하나만 추가하는데도 많은 고생을 해야 하는 구조는 제가 생각하는 유연함이 아닙니다.

테스트 용이성(testability)도 중요한 고려사항입니다. 테스트 용이성은 테스트 자체만을 의미하지 않습니다. 이 부분에 대해서는 잠시 후에 살펴보겠습니다.

관심사의 분리(separation of concern)도 중요합니다. 이것은 하나의 책임만 담당하는 매우 명확한 디자인 패턴 중 하나입니다. 관심사의 분리에서는 명확한 경계를 정의합니다. 객체는 대부분의 경우에 하나의 역할만을 담당해야 합니다. 대부분의 경우라고 표현한 이유는 때로는 이러한 규칙을 어겨야 할 상황도 발생하기 때문입니다. 규칙을 어기더라도 이것은 여전히 괜찮은 선택인데, 그런 구조가 아니라면 클래스가 너무 많아지게 되기 때문입니다. 때로는 객체들 사이의 관계를 설정하기 위해 coordinator 역할을 수행하는 클래스가 필요할 때가 있는데, 어떤 사람들은 이것이 SRP(single responsibility principle)개념을 위반했다고 말하기도 합니다. 하지만 이들은 여전히 서브 시스템간의 통신을 수행하는 하나의 책임만 맡고 있습니다.

테스팅

애플리케이션을 개발할 때, 그것을 테스트하고 그 과정을 이해하는 것이 굉장히 도움이 됩니다. 테스트 용이성은 아주 중요한 요소지만, 테스트 코드를 작성하는 사람은 드뭅니다. 에이전시의 경우 클라이언트는 테스트의 중요성을 이해하지 못하기 때문에 테스트 코드를 작성하는 일에 대해 돈을 지급하고 싶어 하지 않습니다. 제 경우, 전체 앱을 작성해달라고 요청하는 클라이언트에게 테스트 코드를 작성하는 것은 선택사항이라고 이야기해줍니다. 어떤 사람이 집을 짓기 위해 건축업자를 고용한다면, 그들에게 집을 짓는 방법에 관해서 설명하지는 않겠지요. 그들이 전문가라고 생각하기 때문에 고용한 것이니까요.

만약 어떤 사람이 앱을 만들기 위해 저를 고용하면, 저는 제 능력을 최대한 발휘해서 앱을 만들 것이며 여기에는 테스트 코드 작성도 포함됩니다. 테스트 코드를 작성하는 것은 커다란 차이를 가져옵니다. 저는 아키텍처에 관해 이야기할 때마다 테스트가 가져다주는 장점에 대해서 항상 강조합니다.

우선, TDD 방식으로 테스트 코드를 작성하면 테스트 코드를 먼저 작성하게 됩니다. 이렇게 하면, 아키텍처, 클래스, 매니저 등을 구현하기 전에 API를 디자인할 수 있습니다. 새로운 기능을 추가하는 경우 테스트 파일에서 클래스를 생성할 수 있지만, 첫 번째 테스트 코드를 작성하기 전에는 인터페이스가 없을 것입니다.

첫 번째 테스트 코드를 작성하면서 어떻게 사용할 것인지 생각하며 함수를 작성합니다. 즉, API 사용자 관점에서 코드를 작성합니다. TDD 방식으로 개발하지 않는 경우, public 인터페이스의 프로토타입을 작성할 때, Objective-C의 헤더 파일에서 void 함수 또는 Swift에서 empty 함수를 이용해 API를 작성한 다음, 실제 코드에서 이들을 사용해보고 적절하지 않은 인터페이스라면 수정하는 과정을 거쳐야만 했습니다. 한편, TDD 방식에서 맨 처음 할 일은 인터페이스를 실제로 호출하는 것입니다. 이런 관점의 전환은 매우 작은 변화로 보일 수도 있지만 API 설계 측면에서는 매우 큰 변화가 발생합니다. API를 작성할 때는 테스트하기 쉬운 형태로 만들고 싶을 것입니다. 그렇지 않으면 테스트 작업이 힘들어지니까요.

이런 개발 뉴스를 더 만나보세요

많은 사람이 테스트 코드를 작성하지 않는 이유 중 하나는 테스트 코드 작성이 어렵기 때문입니다. 하지만, 테스트 코드를 작성하기 어렵다면 아키텍처에 문제가 있을 가능성이 높습니다. 테스트 코드를 처음부터 작성하면 인터페이스가 더욱 단순해지고, 유연해질 것입니다. 쉽게 테스트할 수 있는 환경을 만들면 시스템 안에 있는 요소들을 쉽게 교체할 수 있습니다.

TDD에서 사용되는 red-green-refactor 방식을 따라, 우선 실패하는 테스트 코드를 작성하고 가장 간단한 방법으로 그것을 컴파일합니다. 그런 다음, 컴파일이 성공하도록 코드를 간단히 수정하고 리팩토링을 수행합니다. 이 과정을 통해 여러분은 테스트뿐만 아니라 구현 측면에서도 좀 더 깔끔한 코드를 얻을 수 있습니다.

제가 맡은 모든 프로젝트는 이런 방식을 적용해 진행합니다. 만약, 특정 기능을 테스트하기 위해 수많은 페이크 오브젝트를 만든다면 하나 이상의 책임을 맡는 객체, 의존성 증가 등의 문제가 발생할 수 있습니다.

의존성 줄이기(8:41)

Foldify는 Parse 서비스를 기반으로 작성했으며 Parse 서비스 중단에 따른 마이그레이션 작업이 진행 중입니다. 애플리케이션에서 모델 객체를 다루는 추가적인 계층이 존재한다면, Parse에서 다른 백엔드 서비스로 마이그레이션하는 편이 SDK를 직접 사용한 경우보다 좀 더 쉬울 것입니다.

Parse를 처음 도입할 때만 해도 Parse의 서비스 품질은 매우 낮았습니다. Parse에서는 Parse 프레임워크 내부에 정의된 assertion으로 인해 이미지를 다운로드하는 것조차 할 수 없었습니다. 프로젝트와 써드 파티 서비스 사이에 계층을 하나 추가하면 나중에 발생할 수 있는 문제를 예방할 수 있습니다.

금전적인 수익을 가져다주는 프로젝트라면 현재 적극적으로 개발하고 있지 않더라도 몇 년간은 프로젝트를 유지하는 것이 좋습니다. 저 역시 Foldify에 새로운 기능을 추가하거나 적극적으로 프로젝트를 관리하고 있지는 않지만, 유지보수는 꾸준히 하고 있습니다.

에이전시처럼 프로젝트를 수행하고 제품을 넘겨주면 끝나버리는 구조가 아니라 몇 년 동안 살아남을 수 있는 아키텍처를 만드는 것이 중요합니다.

저는 항상 문제를 해결해야 하는 사람이 저라는 사실을 염두에 두고 프로젝트를 수행합니다. 모든 문제들을 고쳐야 할 사람이 결국 자신이 된다는 생각으로 프로젝트를 수행한다면 문제 발생 원인을 훨씬 줄일 수 있을 것입니다.

잘못된 아키텍처(10:07)

잘못된 아키텍처임을 보여주는 신호들은 무엇이며, 잠재적인 문제들을 쉽게 확인할 방법은 무엇일까요?

아래의 명령어를 이용해 파일의 코드 라인 수를 확인하는 것도 한 방법입니다.

find . -type f -exec wc -l {} + | sort -n

예제에서는 Swift로 작성한 ViewController 코드가 3천 라인 정도, AppDelegate 코드는 약 4천 라인으로 나타나고 있습니다. 정상적인 코드 길이로 보입니다. 한 파일 안에 여러 개의 클래스가 존재하는 경우도 있고 편의를 위해 하나의 파일로 관리하는 경우도 있으므로 한 파일의 코드 길이가 길다고 해서 잘못된 아키텍처라는 의미는 아니지만, 코드를 점검해야 한다는 신호는 될 수 있습니다.

저는 GitHub에서 운영 중인 모든 Objective-C 프로젝트에 대해 특정 파일이 너무 길면 경고 메시지를 제공하는 스크립트를 작성해서 사용 중입니다. 이 스크립트는 긴 코드 라인을 보유한 파일이 있으면 “원칙에 맞게 코드를 제대로 작성했는지 확인하세요” 와 같은 메시지를 제공합니다.

또 다른 방법은 전역 상태(global state)와 AppDelegate를 프로젝트에 사용했는지 확인하는 것입니다. 여러 개의 클래스가 있고 전역 속성(global property)을 저장하기 위해 AppDelegate를 사용한다면 이것은 의존성 주입도 아니고, 싱글톤을 올바르게 사용하지 않은 상황에 해당합니다. 이는 단지 전역 상태이며 나중에 문제를 일으킬 가능성이 큽니다. Dependency visualizer를 실행해보면 모든 것이 AppDelegate를 가리키고 있는 것을 확인할 수 있으며, 많은 클래스가 서로 연결되어 있어서 쉽게 바꿀 수 없고 모듈화도 되어 있지 않은 구조라는 사실을 알 수 있습니다.

이러한 상황을 피하고 싶다면 Dependency visualizer와 관련된 추가정보를 GitHub repo에서 확인세요.

디자인 패턴(12:00)

디자인 패턴에 대해 간략하게 살펴보겠습니다. 이미 수많은 글이 있으므로 모든 SOLID 개념을 언급하지는 않겠습니다.

사람들은 디자인 패턴을 마치 종교처럼 생각하는 경우가 있으며, 특정 패턴에 너무 집착한 나머지 모든 곳에 그것을 적용할 수 있다고 믿는 것 같습니다. 디자인 패턴은 일종의 도구이며 특정 상황에 알맞은 패턴을 적용할 경우 매우 좋은 결과를 보여줍니다.

싱글톤은 태생적으로 나쁜 디자인 패턴이 아닙니다. 사람들이 잘못 사용해서 나쁘게 보일 뿐이죠. 좋은 디자인 패턴일지라도 적절하게 사용하지 않으면 아키텍처는 엉망이 될 것입니다.

어떤 상황에서 특정 디자인 패턴을 사용해야 하는지 알기 위해서는 충분한 개발 경험과 시간이 필요합니다. 디자인 패턴에 대한 저의 접근법은 항상 실용성에 중점을 두고 있습니다. 특정 아키텍처 패턴을 따라야 하는 경우라도 부족한 점이 발견된다면, 테스트하기 쉽고 좀 더 멋진 코드를 만들 수 있는 계기가 될 수 있고, 다른 패턴을 적용할 수 있는지 고민할 수 있는 시작점이 될 것입니다.

여기 있는 사람들에게 싱글톤에 관해 묻는다면 아마도 싱글톤은 나쁘므로 사용하지 않는다고 하거나, 다른 대안이 없으므로 어쩔 수 없이 사용하고 있다고 답할 것입니다. 싱글톤을 사용하는 수많은 이유가 있지만, 대부분의 경우 적절하게 사용하고 있지 않습니다. 물론 싱글톤 패턴 자체가 나쁜 것은 아닙니다. 만약 데이터베이스에 접근하거나 하나의 인터페이스만 허락되는 상황이라면 싱글톤을 사용할 수 있습니다. 하지만 iOS에서는 대부분의 전역 상태를 동기화(global state sync)하기 위한 용도로 싱글톤을 사용하고 있으며, 모든 것을 싱글톤으로 보냅니다.

앱에서 logger는 대부분 싱글톤으로 구현하지만, Swift에서는 싱글톤을 사용하고 있다는 사실을 노출하지 않아도 됩니다. 싱글톤을 노출하지 않으면 아키텍처에서 깔끔한 구조를 유지하기가 수월합니다.

싱글톤을 사용하는 이유는 쉽게 액세스할 수 있기 때문입니다. 특정 시점이나 클래스에서 로그 메시지를 출력하기 위해 모든 클래스에 logger를 전달하는 형태가 아니라, 크래쉬가 발생한 상황일 때만 로그 메시지를 출력하거나, logger에 액세스할 수 있으며, 서버 또는 파일에 로그 메시지를 기록하는 등 디버그를 위한 로그 정보를 얻을 수 있는 logger가 필요합니다.

싱글톤을 가지고 있더라도 외부에 노출하지 않아도 됩니다. 특히 Swift에서는 이러한 형태로 구성하는 것이 가능합니다. 내부적으로 싱글톤을 사용하는 기본 구현(default implementation)을 가진 Loggable이라는 프로토콜을 작성하면, 싱글톤을 사용했다는 정보가 외부로 노출되지 않으며, 초기 설정(initial configuration)을 제외한 외부에서는 logger에 접근할 수 없습니다.

struct MyType: Loggable {
    func someFunction() {
        log(.Info, "log function is instance method provided by default implementation of Loggable")
        log(.Warning, "Nothing in the app can access Logger directly, outside of it's initial configuration")
    }
}

Loggable 프로토콜을 준수하는 클래스를 생성하면 log 함수에 바로 접근할 수 있으며, log 함수 내부에서는 싱글톤을 사용하고 있지만, 앱에서는 그 누구도 이 사실을 알지 못합니다. 테스트 코드 작성 시 default implementation을 변경하여 Loggable의 구현방법을 변경할 수 있으며 오버라이드도 할 수 있습니다. 정확하게 로그가 동작하는지 테스트할 수도 있고 관심 있는 클래스의 로그도 확인할 수 있습니다.

싱글톤의 외부 노출 여부에 상관없이 싱글톤이 잘못 사용된 예는 수없이 많습니다. 예를 들면, 네트워크 매니저로 싱글톤을 많이 사용하고 있습니다. image viewer의 URL을 설정하기 위해 보통 싱글톤을 사용합니다. 이것은 의존성 주입 이 결여된 이슈이기 때문에 의존성 주입으로 문제를 해결할 수 있습니다.

Composition(16:34)

composition은 누구에게나 추천할 만한 패턴입니다. 이 패턴을 좋아하는 이유는 간단합니다. composition을 제대로 사용하면 다른 좋은 선택들과 자연스럽게 연결되고, single responsibility와 DRY(Don’t repeat yourself)를 준수하는 코드를 얻게 됩니다. single responsibility를 갖게 되는 순간이 특별한 객체(particular object)를 갖는 시점이며, 이것은 이미 DRY입니다. DRY 코드를 갖게 되면 시간을 절약할 수 있으며 완벽하게 재사용한 가능한 코드를 보유하게 되고, 같은 코드를 중복하지 않는 구조를 갖게 되는 것입니다.

composition 패턴은 여러 가지 장점이 있습니다. composition은 자연스럽게 SRP 와 DRY 개념을 준수합니다. 작은 객체로 구성하면 다른 객체에 이것을 inject 할 수도 있고, fake로 대체할 수도 있으므로 테스트가 쉬워집니다. 객체에 대한 테스트에서도 매우 작은 단위의 인터페이스에 대해서만 테스트를 수행할 수 있는 장점이 있습니다.

또한, 작은 객체들로 나눌 때도 가변성이 적기 때문에 테스트할 코드가 줄어들게 됩니다.

composition을 유용하고 사용하게 있는 분야는 바로 게임입니다. Unity를 사용한 수많은 3D 그래픽 게임들을 많이 볼 수 있습니다. 앵그리 버드 스페이스 게임뿐만 아니라 전혀 다른 수많은 게임이 동일한 Unity 엔진을 사용하고 있습니다.

전체 Unity 시스템은 composition 기반이며 composition을 활용한 엔티티 시스템 기반입니다. 객체를 만들고 그 객체에 행동을 추가할 수 있습니다. 예를 들어 게임에서 monster 인스턴스를 만든다고 가정해봅시다. 처음에는 움직이지 않고 불화살을 쏘는 monster를 만든 다음, 게임 디자이너가 이동 기능을 추가해서 플레이어를 따라가게 하기로 결정했다면, 해당 기능을 가진 새로운 객체를 추가하고 monster에 해당 객체를 inject 하면 monster는 이제부터 플레이어를 쫓아다닐 수 있게 됩니다.

제 현장 경험 중 composition의 가장 좋은 사례는 Unity입니다. Unity를 이용해 수많은 다른 게임을 만드는 것이 가능하고 다른 기능, 다른 모습들을 표현할 수 있습니다. 복잡성 측면에서도 다양한 종류가 가능합니다. Unity는 하나의 패턴을 따르는 단일 엔진이며 이 패턴은 훌륭하고 많은 장점이 있습니다.

composition과 함께 사용하는 또 하나의 패턴은 바로 의존성 주입입니다. 작은 객체들로 만들었다면, 그들을 하드 코드(hardcode) 방식으로 만들 필요가 있을까요? inject 방식을 이용해 새로운 인스턴스를 생성할 수 있는데도 불구하고, 하드 코드한 방식으로 의존성을 만들 필요가 있을까요?

Inject가 가능한 객체가 존재한다면 하드 코드한 방식을 유지해야 할 이유가 없습니다. Unity에서 정상적인 객체를 다른 것으로 변경하는 것을 허락하지 않는다면, 변경사항을 적용하기 위해 새로운 클래스를 만들어야 할 것입니다. 플레이어를 위한 클래스, 적군(enemy)을 위한 클래스를 따로 만들어야 한다는 뜻입니다. Unity에서는 composition을 이용해 다른 설정이 적용된 적군 클래스를 만들 수 있습니다.

의존성 주입을 테스트에도 활용할 수 있으며 의존성 주입을 이용해 구현코드를 바꿀 수도 있습니다. 약간 변경된 동일한 객체를 만들 수도 있고, 완전히 다른 작업을 수행할 수도 있는 매우 유용한 방법입니다. 또한, 이것은 더 적은 테스트로도 같은 테스트 커버리지를 달성할 수 있는 장점이 있습니다. Apple도 WWDC 2016 에서 코드 주입을 추천했으며 강력한 의존성 제거, 코드 재사용을 장점으로 언급했습니다. 하지만 그들은 WWDC에서 이들을 완전히 잘못된 방법으로 사용했는데, 이 부분에 대해서는 잠시 후에 살펴보도록 하죠. 우선, 몇 가지 인기 있는 아키텍처 패턴에 대해 알아보겠습니다.

인기있는 아키텍처 패턴들(20:39)

Bogdan Orlov가 작성한 “iOS Architecture Patterns”라는 훌륭한 글이 있습니다. 그의 글에서 다룬 아키텍처 중에 MVC, VIPER, MVVM에 대해서 살펴보겠습니다. MVC가 기본이고 가장 많이 사용하는 패턴이기 때문에 이것을 먼저 알아보도록 하죠. 참고로 Apple은 MVC를 완전히 잘못된 방법으로 사용하고 있습니다.

MVC

Apple MVC가 아닌 고전적인 MVC는 오래전에 만들어졌으며 웹에 더 적합합니다. 컨트톨러는 모델 데이터를 가져와 뷰에 표시합니다. 모델이 변경되면, 컨트롤러는 새로운 뷰를 만듭니다. iOS 환경에서는 특정 프레임워크를 사용해야 하므로, 고전적인 MVC 패턴을 적용하기가 어렵습니다.

Apple은 뷰와 모델을 중재하는 컨트롤러를 작성하면 컨트롤러의 비중이 크지 않을 것으로 생각했지만, 대부분의 사람은 MVC를 구성할 때 컨트롤러의 비중이 매우 큰 massive 뷰 컨트롤러 형태로 사용했습니다. 뷰 컨트롤러는 뷰 라이프 사이클과 매우 강하게 연결되어 있으므로 이것을 확실하게 분리할 수 없습니다. 뷰 컨트롤러를 재사용할 수 없기 때문에 뷰도 재사용할 수 없으며, 모델만 재사용 가능한 형태로 남게 되며, 이는 리소스를 낭비하는 일입니다.

뷰 컨트롤러와 뷰가 모든 것을 책임지고 있습니다. 사람들은 네트워크, 다운로드, 데이터 처리를 위해 이들을 사용합니다. 이 아키텍처로 테스트 코드를 작성한 적이 있다면 사람들이 왜 테스트를 싫어하는지 알 수 있습니다. 테스트 자체가 잘못된 것이 아니라 테스트하려는 아키텍처에 문제가 있는 것입니다. 이 구조에는 composition도 없고, 주입도 없는 것이 일반적입니다.

VIPER

이제 VIPER를 살펴보죠. 앞서 언급한 패턴은 전체 앱을 위한 아키텍처 패턴이라기 보다는 UI 패턴에 가깝습니다. VIPER는 최초로 전체 아키텍처를 다룬 훌륭한 패턴으로 새로운 개념들이 몇 개 등장합니다.

예를 들어, 비즈니스 로직을 담당하는 interactor가 있습니다. Interactor는 composition을 사용하는 다른 서비스들과 연동됩니다. 라우터는 뷰와 뷰 컨트롤러에 다른 프리젠테이션 컨텍스트를 적용할 수 있으며, 이를 이용해 같은 화면을 여러 곳에 제공할 수 있습니다. 프리젠터는 UIKit과는 독립적인 프리젠테이션 로직을 포함하고 있는 클래스입니다. UIKit을 사용하지 않으며, 화면에 나타낼 데이터에 대한 포맷 구성 같은 작업을 담당합니다.

VIPER는 매우 좋은 패턴이지만 수많은 기반 코드를 생성해야 하는 불편함이 있으므로 VIPER에서 필요한 모듈들을 생성해주는 code generator와 같은 도구를 사용하는 것이 일반적입니다. VIPER를 사용하지 않는 주된 이유가 많은 클래스를 생성해야 한다는 점인데, 이러한 문제를 해결하기 위해 VIPER 클래스를 자동으로 생성해주는 code generator 프로젝트가 GitHub에 있으니 참고하시기 바랍니다.

MVVM

iOS에서 가장 인기 있는 패턴은 MVVM일 것입니다. 가장 중요한 것은 UIViewController 와 UIView가 뷰로 분류된다는 점입니다. 뷰 라이프 사이클과 강력하게 연결되어 있으므로 UIViewController는 뷰 계층으로 분류합니다.

뷰 모델을 테스트할 수 있으므로 MVVM은 상당히 쓸만한 패턴입니다. 뷰 모델에는 UIKit과 관련된 코드가 없습니다. 따라서, 뷰 모델은 뷰 컨트롤러 로딩과 의존성이 없으므로 뷰 모델을 이용해 비즈니스 로직을 테스트할 수 있습니다.

MVC의 테스트 용이성이 30% 정도라면 MVVM은 70% 정도 됩니다. 나머지 30%는 UI 테스트를 통해 채우면 됩니다. MVVM의 단점은 바인딩을 도와주는 라이브러리를 함께 사용하지 않으면, 많은 기반 코드를 작성해야 한다는 점입니다. 이들 라이브러리를 사용하더라도, MVVM를 완벽한 FRP 구조로 만들 필요는 없습니다. ReactiveCocoa와 Rx는 멋진 라이브러리지만 사용법이 쉽지 않아서 익숙해지기까지 상당한 시간이 필요합니다. 저는 MVVM에서 바인딩을 처리하는 용도로 이들 라이브러리를 즐겨 사용하고 있습니다.

간단한 observable 클래스만 갖춰도 MVC보다는 훨씬 나은 구조가 될 것입니다. MVVM은 가장 인기 있는 패턴이지만, 많은 사람이 잘못 사용하고 있습니다. MVVM의 장점은 테스트 용이성입니다. 뷰 모델에서는 UIKit 관련된 코드가 없으므로 UI에 독립적인 테스트를 할 수 있습니다. 뷰 컨트롤러의 라이프 사이클을 복제하는 방법, Nib 파일에서 아울렛을 로드할 수 있게 뷰 컨트톨러의 getter를 호출해야 하는지에 대해 더 고민하지 않아도 됩니다. 깔끔하고 멋진 뷰 모델로 인해 이런 문제들이 해결되었으며, 뷰 모델과 상호 작용하는 모든 것들을 테스트할 수 있게 되었습니다.

하지만, MVVM는 뷰 컨트롤러와 어떻게 바인딩할지에 대한 문제가 여전히 남아있습니다. MVVM에는 VIPER의 라우터 역할에 해당하는 것이 없기 때문입니다. WWDC에서 Apple은 라우터가 없다면, 의존성 주입을 사용하라고 권장했습니다. 그들은 이 방법이 코드 재사용을 높이기 때문에 좋다고 말하면서, 특정 뷰 컨트롤러에 다른 뷰 컨트롤러를 넣으면서 의존성을 주입하는 것을 예로 들었습니다.

Apple의 설명은 문제가 있다고 생각합니다. 1개의 의존성을 가진 1개의 뷰 컨트롤러가 있다고 가정해봅시다. 새로운 뷰 컨트롤러를 만들 때마다 그것을 주입하는 것은 문제없이 동작할 것입니다. 하지만, 뷰 컨트롤러를 하나 더 추가한 다음, 뷰 컨트롤러에서 데이터를 모니터하고 싶다면 어떻게 해야 할까요?

예를 들어보죠. 뷰 컨트롤러 A에 뷰 컨트롤러 B의 의존성을 추가한 뒤, 또 다른 뷰 컨트롤러 C를 추가하면 의존성이 뷰 컨트롤러 A와 뷰 컨트롤러 B에 추가됩니다. 결국, 첫 번째 뷰 컨트롤러인 A는 다른 모든 뷰 컨트롤러와 동기화될 것입니다. 즉 뷰 컨트롤러 A는 모든 뷰 컨트롤러에 대한 의존성을 갖게 됩니다.

mvvm_lack_router

일반적으로 앱에서는 수많은 내비게이션을 가질 수 있으므로, 이러한 현상이 많이 발생하겠지요. 많은 의존성이 포함된 뷰 컨트롤러의 코드는 가독성이 매우 떨어집니다. 소스 코드를 봐도 어떤 것이 실제로 필요한 것인지 파악하기 힘들 것입니다. 뷰 컨트롤러 소스에서 데이터베이스도 사용하고, 이미지 프로바이더도 사용하고, logger도 사용하고, 그 밖에 다른 것들도 사용하거나 실제로 사용하는 것은 1개밖에 없을 수도 있겠죠. 또는 아무것도 사용하고 않을 수도 있겠죠.

첫 번째 뷰 컨트롤러가 환영 메시지만 보여주며, 의존성은 없다고 가정해봅시다. 만약 모든 것을 첫 번째 뷰 컨트롤러에 주입한다면 문제가 발생할 것입니다. 라우터가 없다면, 여러분은 이런 구조를 갖게 될 것입니다. 라우터의 부재는 코드 작성 측면에서도 if 문이 증가하는 단점이 있습니다. 예를 들어, iPad과 iPhone에서 같은 뷰 컨트롤러를 제공하지만, iPad에서는 push 방식으로 제공하고, iPhone에서는 modal로 제공할 때 if 문을 사용해서 처리해야 합니다.

func doneButtonTapped() {
    let vc = ChildViewController(prepareNeccessaryState())

    if Device.isIPad() {
        navigationController.pushViewController(vc, animated: true, completion: nil)
    } else {
        var nav = UINavigationController(rootViewController: vc)
        nav.modalPresentationStyle = UIModalPresentationStyle.Popover
        var popover = nav.popoverPresentationController
        popoverContent.preferredContentSize = CGSizeMake(500,600)
        popover.delegate = self
        popover.sourceView = self.view
        popover.sourceRect = CGRectMake(100, 100, 0, 0)

        presentViewController(nav, animated: true, completion: nil)
    }
}

저는 이러한 방식을 선호하지 않습니다. 클라이언트가 사용자 설정에 관한 기능을 변경을 요구하면서 프로필 화면에서 제공하던 사용자 설정 화면을 다른 화면에서 나타나길 원한다면, if 문을 추가하여 변경사항을 처리해야 할 것입니다. 이런 방식은 상태(state)를 추가하는 것이기 때문에 어떤 컨텍스트에서 실행되는지 확인하는 작업을 추가해야 하는 불편함이 발생합니다. 이러한 구조는 수많은 의존성을 일으키며, 나중에 코드를 읽기도 힘들고 코드를 변경하기도 어렵습니다.

라우터의 부재는 MVVM의 최대 장점인 테스트 용이성에도 영향을 줍니다. 위의 예시에서 나타난 것처럼 뷰 컨트롤러 push 로 인해 의존성이 많아지거나, iPad에서 실행되는지를 확인해야 하는 구조에서 테스트를 수행하는 것은 굉장히 어렵습니다.

MVVM - 라우터 의 부재(29:33)

라우터가 없는 MVVM의 단점을 살펴보죠. 우선, 불필요한 의존성이 발생합니다. 자신이 뷰 모델에 포함된 경우를 제외하고는 뷰 모델이 다른 뷰 모델에 대해 알 필요가 없죠. 코드 재사용성도 낮고, if 문이 많이 사용된 스파게티 코드가 나타납니다. 또한, 테스트가 어렵습니다. 뷰 모델을 테스트하고 싶다면, 뷰 컨트롤러를 stub해야 합니다. 제가 원하는 것은 간단한 테스트로, 작성하기도 쉽고, 실제로 어떤 일을 수행하는지 파악하기도 쉬워야 하는데 현재 구조에서는 테스트 코드 가독성이 떨어질 수밖에 없습니다. 테스트 코드의 품질은 실제 코드와 비슷한 수준을 유지해야 하므로 이것은 중요한 문제입니다.

MVVM + Flow Coordinators

제 친구인 Jim Roepcke로부터 MVVM + Flow Coordinators 라는 패턴을 소개받았습니다. 처음에는 이 패턴에 회의적이었지만 현재는 많은 사람이 이 접근법을 개선하고 발전시키고 있습니다.

이것은 기본적으로 좀 더 책임을 가진 라우터라고 할 수 있습니다. 이 주제를 상세히 다룬 글들이 있는데 하나는 제 블로그에 있으며, flow controllers를 이용한 아키텍처 개선방안에 대한 내용을 담고 있습니다.

다른 하나는 coordinators redux 를 다룬 Khanlou의 글입니다. 이것은 라우터와 유사하지만 여러 개의 뷰 컨트롤러가 있을 때 아래와 같은 구조를 갖습니다.

mvvm_flow_coordinator

이 패턴에서는 뷰 컨트롤러 구성을 담당하는 객체가 한 개 있습니다. 만약 MVVM을 사용하면 뷰 모델을 구성하는 역할을 담당하겠지요. 이 패턴은 MVVM뿐만 아니라 MVC에도 적용할 수 있습니다. 위의 그림에서 보면 화살표가 양방향으로 표시되어 있지만, 이 패턴을 사용하고 있는 것을 뷰 컨트롤러가 알아야 한다는 의미는 아닙니다.

저는 뷰 컨트롤러를 블랙박스처럼 사용하는 것을 좋아합니다. 뷰 컨트롤러는 담당 업무에 대한 정보를 제공하는 인터페이스만 노출하는 방식으로 말이죠. 예를 들어, EditUserSettings 뷰 컨트롤러가 있다면, 해당 뷰 컨트롤러는 저장이나 취소를 처리할 텐데 제가 관심 있는 것은 이것뿐입니다.

이 경우, flow coordinator는 특정 컨텍스트에서 뷰 컨트롤러를 생성할 것입니다. EditUserSettings 뷰 컨트롤러가 사용자 프로필에서 나타날 때와 다른 화면에서 나타나는 경우, 각각 다른 설정이 적용될 것입니다.

프로필 화면이 좋은 예가 될 것 같군요. 스냅챗과 같은 앱에서 프로필을 볼 때 왼쪽에 있는 메뉴를 이용해 사용자 프로필을 확인할 수 있습니다. 디자인 변경으로 인해 이제부터 프로필을 확인하려면 포스트로 이동해서 포스팅 작성자의 아이콘을 클릭해야 하는 경우가 발생할 수 있겠죠. 다른 presentation 컨텍스트에 다른 설정 컨텍스트가 필요한 상황입니다.

flow coordinator가 하는 일은 여러 개의 설정 가능한 메소드를 제공하여 특정 시나리오에 따라 설정할 수 있게 하는 것입니다. 뷰 컨트롤러는 어떤 상황에서 이것이 설정되는지 알지 못합니다. 이러한 구조는 같은 뷰 컨트롤러를 여러 곳에서 재사용할 수 있게 해줍니다. 예를 들어, 같은 뷰 컨트롤러를 서로 다른 뷰 컨트롤러의 자식 컨트롤러로 사용할 수 있습니다. 얼마나 많은 트리거가 있는지에 따라 델리게이트나 클로저를 사용할 수 있습니다. 간단한 것이라면 클로저만으로도 충분할 것입니다.

MVVM + Flow Coordinator

flow coordinator는 컨텍스트에 맞춰 설정한 뷰 컨트롤러 또는 뷰 모델을 생성하는 역할을 담당합니다. 그래서 image picker가 프로필 화면과 설정 화면에서 나타날 수 있고 이미지가 포함된 포스팅 작성화면에서도 나타날 수 있습니다. flow coordinator를 사용하면 동일한 코드를 이용해 이러한 작업을 처리할 수 있으며 if 문을 사용해서 어떤 컨텍스트인지 확인하는 작업을 생략할 수 있습니다.

iPad, iPhone에서 다른 형태의 화면을 제공할 수 있으며, 컨테이너 뷰 컨트롤러의 자식 뷰 컨트롤러로 사용할 때도 마찬가지입니다. 많은 곳에서 코드를 재사용할 수 있습니다. 또한, flow coordinator는 coordinating을 통해 뷰 모델 또는 뷰 컨트롤러의 이벤트를 리스닝하고 반응하는 임무를 수행합니다.

예를 들어 어떤 것을 modal 형태로 보여주고 싶다면 flow coordinator에서 설정한 다음, 취소 또는 저장과 관련된 트리거가 발생하면 클로저에서 제거할 수 있습니다. 모든 정보가 뷰 모델 또는 뷰 컨트롤러 외부에 있으면 굉장히 편리합니다.

필요할 때 주입할 수 있다는 점도 flow coordinator의 장점입니다. 각각의 뷰 컨트롤러 또는 뷰 모델은 의존성을 정의할 수 있으며, 의존성 목록을 소스 코드에서 바로 확인할 수 있습니다. 또한, 동기화를 수행하거나 이것을 다른 객체에 전달할 필요가 없으므로, 실제로 어떤 것이 사용되고 있는지 쉽게 파악할 수 있습니다. 이러한 구조는 문서화 측면에서 큰 도움이 되며, 더이상 어떤 것이 사용되는지 고민하지 않아도 되기 때문에 테스팅도 쉬워집니다.

MVVM + ViewModel

뷰 모델은 프리젠테이션에 대한 정보를 알지 못하고, 일반적으로 UIKit과 관련된 코드가 없습니다. identifier로 URL을 사용하는 UIImage도 필요없겠지요. 물론 UIImage를 사용할 수는 있지만 뷰 모델은 기본적으로 뷰와 뷰 컨트롤러가 존재하지 않습니다.

테스팅 측면에서는 뷰 모델을 블랙박스처럼 다룹니다. 매우 단순한 인터페이스를 가지고 테스트한다는 의미입니다. 또한, 뷰 모델은 재사용할 수 있기 때문에 여러 곳에서 같은 패턴을 사용할 수 있습니다.

ReSwift와 개발 지원 도구(35:24)

지금까지 제가 현재 사용하고 있는 MVVM에 대해 살펴보았습니다. 최근 iOS 커뮤니티에서 관심받는 흥미로운 패턴 중 하나를 더 말씀드리자면 바로 ReSwift입니다. ReSwift는 백엔드나 웹 개발에서 사용되는 Redux를 iOS로 이식한 라이브러리이며 단방향 흐름(uni-directional flow) 아키텍처입니다. 상태를 변경하기 위해서는 순수 함수(pure function)를 사용해야 하며, 순수 함수만큼 테스트가 간편한 것도 없을 것입니다. 순수 함수는 반드시 입력과 출력이 있으며, 이것이 여러분이 테스트해야 할 대상입니다.

ReSwift는 모든 상태가 한 곳(single source of truth)에 있습니다. Redux를 기반으로 작성되었기 때문에 앱에 state 구조체는 단 하나만 있습니다. 특히 ReSwift는 훌륭한 개발 지원 기능(dev tooling)을 가지고 있습니다. 단방향 아키텍처이기 때문에 쉽게 flow의 특정 지점에 어떤 것을 추가해서 변경 작업을 하거나 추가기능을 덧붙일 수 있습니다.

모든 mutating 함수에 대한 로그를 기록하고 싶을 때, Redux를 이용하면 쉽게 구현할 수 있습니다. 내부에서 다른 함수를 추가하기만 하면 모든 것이 그것을 통과하기 때문이죠. hot reloading과 같은 것을 만들 수도 있고 버그를 재현할 수도 있습니다.

특정 지역에서만 크래쉬가 발생했다고 가정해봅시다. 자주 발생하는 문제도 아니고 전체 사용자 중 1% 사용자에게만 이러한 현상이 발생하고, 서버에 랙이 발생했거나 하는 특정한 상황에서만 발생하는 오류라면 어떨까요?

이것은 매우 큰 문제죠. 이런 오류를 재현하는 것은 아시다시피 매우 어렵습니다. 오류 재현을 위해서는 모든 상태에 한 군데서 관리되어야 하고 상태를 변경시키는 모든 액션을 저장해야 하기 때문이죠. 이 모든 것들을 기록할 수 있다면, 문제를 재현하는 것도 가능할 것입니다. 크래쉬가 발생하면 크래쉬 발생 전 지점으로 이동해서 모든 것을 재현해보고, 디버그에서 call stack을 확인하여 어떤 것이 문제를 일으켰는지 확인할 수 있습니다. 저는 이것이 크래쉬 보고서를 읽는 것보다 훨씬 더 나은 방법이라고 생각합니다.

리모트 워크 환경에서도 ReSwift를 활용하면 도움이 될 것입니다. ReSwift를 이용하면 특정 기능을 작업할 때, 해당 기능이 제공되어야 하는 상태를 로드할 수 있습니다. 동료에게 그것을 보내면, 그들은 애플리케이션의 해당 상태만 로드해서 확인할 수 있고 이러한 과정을 서로 반복할 수 있을 것입니다.

Playgrounds와 같은 제 작업을 보신 분이라면 제가 코드 주입을 좋아한다는 사실을 알고 계실 것입니다. 저는 실시간 프로그래밍을 좋아하며, 코드 주입을 위해 웹 프레임워크를 사용하지 않고 네이티브에서 사용하길 원합니다. ReSwift 패턴은 코드 주입과 잘 어울립니다. 상태를 로드하는 시점이 프로젝트를 재컴파일하거나 상태를 다시 불러올 수 있는 시점이기 때문이죠.

실시간으로 프로그래밍하면서 화면을 통해 새롭게 추가한 뷰와 기능들이 어떻게 반영되는지 확인할 수 있으므로 만약 코드 주입을 사용해 본 적이 없다면, 꼭 사용해보도록 하세요. 느려서 답답한 Playground 수준을 이야기하는 게 아닙니다. 실제 애플리케이션 개발에서 코드 주입을 사용하면 얼마나 빠른지 느껴보세요.

코드 주입을 위한 두 가지 도구 중 Xcode injection 플러그인을 추천합니다. 소스 코드에서 변경한 사항이 시뮬레이터에 실시간으로 반영되기 때문에 작업시간을 획기적으로 줄일 수 있으며, 특히 UIKit과 관련된 작업이라면 더욱 큰 효과를 발휘합니다. 단순히 디자인을 변경하고 싶을 때 사용해도 좋고요. 저는 이 플러그인을 이용해 많은 개발 시간을 절약할 수 있었으므로 강력하게 추천하고 싶습니다.

ReSwift는 제가 기대하는 프로젝트 중의 하나인데, 현재 4.0 버전이 출시된 상태입니다. (강연시점에서는 1.0 버전이었습니다). 아직은 ReSwift가 상당히 어렵게 느껴지기 때문에 프로젝트에는 활용하고 있지 않습니다. ReSwift는 웹 관점에서 시작된 프로젝트이며, MVC에서 UIKit을 이용해 뷰를 직접 표현하는 것을 허용하고 있습니다.

이 패턴을 사용하는 몇 가지 라이브러리가 있습니다. 이들은 Redux 개념을 기반으로 만들어진 라이브러리들입니다.

iOS Dev Weekly에 소개된 Render 도 그중 하나입니다. Render는 functional view 개념을 사용하며, functional state가 설정된 뷰는 화면에 그릴 수 있습니다. Render 프로젝트가 좀 더 성숙되면 매우 편리할 것 같습니다. 저는 웹 개발자도 아니고 웹을 그렇게 좋아하지도 않지만, 오류를 재현할 방법을 좋아하며, 빠르게 작업하고 쉬는 것을 좋아합니다. 또한, 제가 정말 즐길 수 있는 것을 하는 것을 좋아하며, 이러한 작업을 돕는 도구들은 쉬워야 하고 테스트가 쉬워야 한다고 생각합니다.

결론(40:05)

좋은 아키텍처란 정답이 없는 어려운 주제입니다. 한 개의 아키텍처가 모든 문제를 해결할 수는 없습니다. 개발 과제와 클라이언트의 요구 조건에 맞는 아키텍처가 무엇인지 항상 고민하는 자세가 중요합니다.

Reflection을 기반으로 구축된 신문-출판 플랫폼 환경에서 작업했던 경험을 예로 들어 보겠습니다. Reflection이 사용된 프로젝트를 수행했는데, 위젯 기반의 프리미어 리그 앱 애플리케이션이었습니다. 사용자는 다른 정보가 포함된 위젯을 보고 싶을 것입니다. 댓글, 비디오, 경기 결과 같은 것들이 있겠네요. 이 아키텍처가 멋진 점은 Objective-C의 동적 기능을 활용하여 파일에서 체크만 하면 특정 기능을 활성화하거나 비활성화할 수 있다는 점입니다.

만약 특정 기능을 비활성하고 싶다면 관련이 있는 파일들을 선택한 다음, 타깃은 선택하지 않은 상태로 프로젝트를 컴파일해서 해당 기능을 비활성할 수 있습니다. 특정 기능을 추가하고 싶을 때는 특정 패턴을 따르는 클래스들을 추가한 다음 컴파일하면 새로운 기능이 추가됩니다. 프로젝트에 10명의 개발자들을 추가하더라도 코어 부분은 건드리지 않으면서 독립적으로 일할 수 있습니다.

요구 조건에 맞춰 아키텍처는 완전히 달라질 것입니다. 제 경우, 지금은 MVVM을 기본적으로 사용하고 있지만, 앞으로는 ReSwift나 다른 아키텍처를 사용할 수도 있습니다. 프로그래밍은 항상 무언가를 배울 수 있다는 점이 매력이라고 생각합니다. 제가 지금 옳다고 생각하는 것이 2년 뒤에는 완전히 달라질 수도 있습니다. 만약 그렇지 않다면, 충분히 공부하지 않았기 때문일 겁니다.

깔끔한 코드를 만들 수 있는 디자인을 지향하면서도 고정관념을 갖지 않기 위해 노력합니다. TDD, composition같이 단순한 개념을 따르기만 하더라도, 제가 지난 10년간 작업했던 프로젝트들보다 훨씬 나은 아키텍처를 구성할 수 있다고 믿습니다.

단순하면서도 유연한 아키텍처를 만들고 싶습니다. 과다한 추상화는 지양하며, KISS(keep it simple, stupid) 원칙을 준수하고 싶습니다.

과도한 추상화에 대해서 좀 더 이야기해 보겠습니다. 예전에 게임 엔진과 프레임워크를 만드는 일을 한 적이 있는데, 제 방식과 제 동료의 접근 방식은 완전히 달랐습니다.

제 친구는 가능한 모든 시나리오와 설정들을 지원하기 위해 모든 것을 추상화로 만들었습니다. collision을 작성할 때도 모든 상황들을 처리할 수 있는 collision을 만드려고 했습니다. 친구는 6~7개의 데모게임이 포함된 게임 엔진을 출시했고, 그 중에는 팩맨 3D 버전 같은 게임도 있었습니다. 그 후 그 친구는 취업했습니다.

오픈 소스였던 자신의 엔진을 그는 모든 곳에 사용했습니다. 어린이용 게임을 만드는 회사에 취직한 이후에는 팩맨 3D 버전과 유사한 게임을 개발했습니다. 팩맨 대신 물고기가 있다는 점만 빼구요.

그가 전에 데모로 만들었던 게임과 게임플레이나 그래픽 관점에서는 거의 차이가 없었지만, 그는 게임 개발 소스를 기초부터 다시 작성하고 있었습니다. 친구의 모습을 보며, 단순하며 쉽게 이해할 수 있고, 발전시킬 수 있는 유연함에 대해 고민하게 되었습니다. 이런 유연함은 추상화를 위해 지나친 설계를 할 때 사람들이 자주 놓치는 부분입니다.

추상화로 인해 얻을 수 있는 이점이 분명히 있지만 균형 있게 설계할 때 그 장점이 발휘되며, SOLID 원칙을 따르는 것이 좋은 출발점이라고 생각합니다. SOLID는 아키텍처를 이야기할 때마다 등장하는 개념이며, clean 아키텍처와 관련된 글에서도 항상 언급되는 개념입니다.

질의응답(44:14)

MVVM 을 flow controller와 함께 구현하려고 할 때, 참조할 수 있는 문서나 소스 코드가 있다면 소개해주세요

제 블로그에 있는 글과 Khanlou 블로그에 있는 flow controllers, coordinator redux 에 관한 글을 읽어보시길 권합니다. 구글 검색창에 “Improving Architecture, and it’s Flow Controllers” 또는 “Coordinator Redux”를 입력해보세요. 글에는 약간의 예제 코드와 아키텍처에 대한 설명이 포함되어 있습니다. 궁금한 점을 제 트위터로 문의하신다면 기꺼이 도와드릴게요. 간단한 예제들은 좀 있는데 복잡한 앱 예제는 없는 것 같습니다. 여러분이 오픈 소스로 공개하는 것도 괜찮을 것 같네요. 취업 면접할 때 오픈 소스 개발 경력이 있는지 묻는 경우도 있답니다.

이렇게 어려운 패턴들을 사용하면 UI 전환이나 애니메이션을 처리하기가 힘들어질 수도 있다고 생각하는데, UI 전환이나 애니메이션 작업을 커스터마이징한 아키텍처를 만들기가 어렵나요?

flow coordinator에서는 기본적으로 뷰를 설정할 수 있습니다. 뷰가 특정 transition 델리게이트를 사용하도록 설정할 수 있으며, 기존 설정 방식과 비교해봤을 때 그리 어렵지 않습니다. 오히려, flow coordinator를 이용하면 뷰를 설정한 다음 여러 곳에서 사용할 수 있다는 장점이 있습니다. 예를 들어, 작은 이미지를 클릭하면 큰 이미지로 변하면서 포스트 글로 이동할 때는 전환 애니메이션을 제공하고, 같은 포스트 글을 푸시 알림에서 열었을 때는 전환 애니메이션이 필요 없다고 가정해보죠. 이런 경우, flow coordinator의 장점이 빛나게 됩니다. 델리게이트를 이용해서 하나의 컨트롤러에서만 전환 애니메이션을 구현하면 되니까요.

coordinator를 사용하면 스토리보드 segue의 장점은 이용하지 못한다는 뜻인가요?(스토리보드를 사용할 수 없다는 의미)

segue를 사용할 수 있습니다. 전에 Objective-C 환경에서 스토리보드 propagation 동작 방식을 변경해서 그렇게 한 적이 있습니다. 제가 만든 segue는 flow coordinator에서도 호출되지만, 약간의 노력이 필요하지요. 기본적으로 제공되는 segue 에서는 불가능합니다. 저는 인터페이스 빌더를 선호하며 제가 작성한 segue의 동작방식에 대해 강연도 여러 번 했고, 많은 프로젝트에서 그것을 사용했지만, 아키텍처와 관련해서는 segue를 권장하고 싶지 않습니다. Swift에서 segue를 이용하는 방법은 글로 남겨놓았으니 참고하기 바랍니다. segue를 사용했을 때 얻는 이점이 많진 않지만, segue가 꼭 필요하다면 사용할 수도 있습니다. tableViewCell 클릭을 segue로 설정할 수 있지만 동일한 디자인을 사용하는 곳이 아니라면 해당 tableView 를 다른 곳에서 재활용할 수 없다는 게 큰 단점입니다.

Apple이 SDK 내부에 사용하는 패턴을 발전시키지 않는 이유가 무엇이라고 생각하나요?

MVC 패턴은 iOS가 탄생하기 전인 80년대부터 존재하던 패턴입니다. MVC는 매우 오래된 패턴이지만, composition을 사용하고 TDD 방식을 따른다면 많은 것들을 테스트할 수 있고 컨트롤러를 coordinate 할 수 있으므로, MVC를 사용한다고 해서 나쁜 코드라고 평가할 수는 없습니다. Apple이 기본적인 패턴을 변경하는 일은 매우 어렵고 많은 변화가 동반되는 작업이 될 것입니다. 문서화뿐만 아니라 많은 것들을 변경해야 할 것입니다.

현재 Apple은 커뮤니티의 흐름을 따르기 위해 노력 중입니다. 실제로 Apple 개발자들로부터 인터페이스 빌더 개선방안과 관련된 문의를 여러 차례 받았습니다. Apple은 커뮤니티에서 일어나는 일을 유심히 관찰하고 있으며 다른 패턴을 사용하는 사용자들도 관심 있게 지켜보고 있습니다. Apple이 MVVM을 사용하라고 말할지도 모르겠습니다. 대신 그들은 바인딩 기능을 제공해야 할 것입니다. 바인딩 기능이 없다면 MVVM을 구성하기가 어려워지고 Boilerplate 코드가 많이 필요할 테니까요. FRP는 너무 복잡하므로 Apple이 FRP 흐름에 동참하지 않을 것으로 생각합니다. VIPER 같은 패턴을 사용하면 사람들에게 설명하기 힘들고, 패턴을 이해하는데 많은 시간이 필요합니다.

MVC는 iOS 앱을 처음 개발할 때 좋은 시작점이 될 수 있습니다. MVC를 이용해서 괜찮은 iOS 앱을 배포할 수도 있겠죠. 여기까지는 문제가 없을 수 있지만, 차츰 시간이 흘러 프로젝트가 커진다면 항상 문제가 발생합니다. 더 나은 패턴을 사용할 수도 없고 composition도 사용할 수 없으니까요.

저는 이 문제를 접근성의 관점에서 봐야 한다고 생각합니다. MVC는 매우 쉽게 접근할 수 있습니다. 설명하기 쉬우니까요. 완벽하지는 않지만 설명하기 쉽다는 겁니다. MVVM은 설명은 쉽지만, 바인딩이 꼭 필요합니다. 바인딩이 없으면 작업이 어려워지고 수많은 기반 코드가 필요하니까요.

VIPER는 사람들에게 설명하기 어렵습니다. 특히 처음 이 개념을 접하는 사람에게는 더욱 더 힘들죠.

많은 개발자가 Swift 개발에 참여하고 있으며 iPad에서는 아이들이 코드를 쉽게 배울 수 있는 Playgrounds 앱을 제공하고 있습니다. 매우 기쁜 일이죠.

플랫폼의 접근성은 지속적으로 개선되고 있습니다. 웹 개발을 해봤던 개발자라면, 익숙한 방식을 사용하는 ReSwift를 아키텍처로 선택할 수 있게 된 것이죠. 다른 배경과 다른 경험 수준을 가진 사람들도 쉽게 접근할 수 있게 하는 것이 우선이라고 생각합니다.

수많은 Apple 엔지니어들이 자신들이 제공하는 샘플 코드는 좋은 아키텍처를 보여주는 예제가 아니라 특정 기술을 설명하기 위한 것이라고 말합니다. 그래서 우리는 샘플 코드에 있는 뷰 컨트롤러 구현방식을 따르지 않아야 합니다.

정리하자면, MVC와 관련된 문제는 접근성에 대한 관점의 차이라고 생각하며 Apple은 다른 패턴을 사용하려고 하지 않고, MVC를 좀 더 유연한 구조로 유지하고 있습니다.

감사합니다.

다음: iOS 프로그래밍 아키텍처와 패러다임 #2: Swift에서 프로토콜 중심 프로그래밍(POP)하기

General link arrow white

컨텐츠에 대하여

2016년 6월에 진행한 mDevCamp 행사의 강연입니다. 영상 녹화와 제작, 정리 글은 Realm에서 제공하며, 주최 측의 허가 하에 이곳에서 공유합니다.

Krzysztof Zabłocki

Programmer that loves solving problems with code, always looking for new and interesting challenges. Prior to iOS he did games and graphics programming. He’s worked with a lot of startups and big clients like: The New York Times, Headspace, Mashable, Unilever, Shell, The News International.

His work has received many awards including Apple Essential, Best of Year. He enjoys teaching and sharing his knowledge, which has led him to speak at over 20 conferences around the world and building libraries and tools used by thousands of developers, including immersive technologies like Playgrounds.

4 design patterns for a RESTless mobile integration »

close