[Cocoa] Cocoa의 MVC

모델-뷰-컨트롤러 디자인 패턴

모델-뷰-컨트롤러(MVC) 디자인 패턴은 꽤 오래된 패턴이다. 다양한 형태의 MVC는 스몰토크의 초창기부터 존재해왔다. MVC는 앱의 전체적인 아키텍처는 물론 앱 내부에서의 객체의 일반적인 역할에 따라 이들을 분류한다. MVC를 구현하는데는 다른 디자인패턴들이 조합되어 사용될 수도 있다.

객체지향 프로그램은 디자인에 MVC를 준수하면 여러 이점을 얻을 수 있다. 객체지향으로 설계된 프로그램 내의 객체들은 재사용되려는 의도로 만들어지며, 이들의 인터페이스는 보다 간결하고 우아하게 디자인되어야 한다. 결국 MVC 기반의 프로그램은 그렇지 않은 프로그램에 비해 변경의 필요에 보다 잘 대응할 수 있다-쉽게 확장 가능하다.- 게다가 많은 코코아의 기술이나 아키텍처-이를테면 바인딩이나 도큐먼트 아키텍처, 스트립트지원-은 모둔 MVC에 기반을 두고 있으며, 커스텀 객체를 만들 때 이 패턴에서 역할을 수행하도록 할 것을 요구한다.

MVC 객체들의 역할과 관계

MVC 디자인 패턴은 객체에는 세 종류가 있다고 간주한다. 모델 / 뷰 / 컨트롤러 객체가 그 세 종류이다. MVC는 앱내에서 이러한 객체들이 수행하는 역할을 규정하고, 이들의 통신을 정의한다. 앱을 디자인할 때의 주요한 단계중 하나는 객체를 이 세 그룹(MVC)중 하나로 고르거나 필요한 객체를 만드는 일이다. 각각의 타입의 객체는 추상적인 경계를 기준으로 구분되며, 이 기준을 오가며 통신하게 된다.

모델 – 데이터와 그 기본처리동작을 캡슐화함

모델 객체는 특별한 지식을 나타낸다. 모델 객체에는 앱의 데이터가 저장되며, 모델 객체는 이 데이터를가공하는 방법을 정의한다. 잘 디자인된 MVC앱은 모델 객체에 중요한 정보를 담아두게 된다. 앱의 상태에 대한 영구적인 데이터는 그것이 파일이나 DB에 저장된다 하더라도 한번 로딩되면 모델 객체에 상주하게 된다. 모델 객체가 특정한 부문의 문제와 관련된 지식을 나타내기 때문에, 이는 쉽게 재사용될 수 있다.

이상적으로 모델 객체는 모델 객체는 UI와 명시적인 연결을 갖지 않으며, 따라서 UI가 직접적으로 모델 객체의 데이터를 수정할 수는 없다. 예를 들어 주소록 앱을 만든다고 하면 한 사람을 나타내는 모델 객체를 만들 수 있다. 그리고 이 모델에는 그 사람의 생일을 저장할 수 있을 것이다. 여기까지는 좋은 아이디어이다. 하지만, 날짜 포맷 스트링이나 생일을 표시하는 방법과 관련된 정보는 다른 어딘가에 저장하는 것이 좋다.

실제로 이런 구분이 항상 최선의 것은 아닐 수 있다. 그리고 MVC에도 유융통성을 적용할 정도의 여유는 있을 것이다. 하지만 모델 객체는 표현이나 유저 인터페이스와는 일반적으로 관련이 없어야 한다. 모델이 '표시방법'과 관련된 정보를 담을 수 있는 예외적인 케이스는 그래픽 앱을 만들 때가 될 수 있다. 하나하나의 도형은 모델이며, 이 모델은 자신이 어떻게 표시되어야 하는지에 대한 정보를 담고 있어야 한다. 하지만 이 경우에도 모델 객체는 그렇나 정보만을 독립적으로 보관해야 하며, 특정한 뷰 클래스에 의존해서는 안된다. 즉 표시에 필요한 정보는 담되, 실제로 표시하는 방법에 대해서는 모델은 몰라야 한다. 모델이 스스로를 그리도록하는 것은 뷰의 요청에 의해서만 이루어져야 한다.

뷰 – 사용자에게 정보를 표시한다.

뷰 객체는 앱의 모델의 정보를 표시하는 법을 알고 있으며, 사용자에게 데이터를 수정하도록 할 수 있다. 뷰는 데이터를 저장하는 것과는 관련이 없어야 한다. (물론 절대적으로 모든 정보를 저장해서는 안된다는 것은 아니다, 뷰는 일종의 캐시로 정보를 저장하여 성능의 향상을 꾀할 수도 있다.) 하나의 뷰는 하나의 모델의 일부분 혹은 모델 전체 내지는 여러 다른 모델 객체들의 내용을 표시할 임무를 맡게 된다. 뷰는 상당히 다양한 형태로 만들어질 수 있다.

뷰 객체 역시 재사용 가능하도록 만들어지고, 조정 가능해야 한다. 또한 여러 앱 간에서도 통일성이 있어야 한다. 코코아에서는 아주 많은 양의 뷰 객체를 정의하고 있고, 이들을 인터페이스 빌더의 라이브러리를 통해 제공하고 있다. NSButton과 같은 앱킷의 뷰 객체를 재사용하여 앱의 버튼이 다른 코코아 앱의 버튼과 통일성 있는 외형을 가질 수 있음을 보장할 수 있다.

뷰는 모델의 데이터를 정확하게 표시하고 있음을 보증해야 한다. 결과적으로 뷰는 모델에서 일어난 변화에 대해서 알 필요가 있다. 모델은 특정한 뷰에 묶여있지 않으며, 변경이 발생했을 때 이를 알려줄 수 있는 다른 방법이 필요하다.

컨트롤러 – 뷰와 모델을 연결함

컨트롤러 객체는 앱의 뷰 객체와 모델 객체 사이에서 중개자 역할을 한다. 컨트롤러는 종종 뷰가 표시하려는 모델의 정보에 접근하도록 해주며, 모델의 변화가 어떤 뷰로 전달되어야 하는지를 조정하는 역ㅇ할을 수행한다. 또한 컨트롤러는 셋업이나 조율 작업을 수행할 수 있다. 이를 통해 앱내의 객체들의 사이클을 관리할 수 있다.

전형적인 코코아 MVC 디자인에서는 사용자가 뷰를 통해 특정한 값을 입력하거나 어떤 액션을 취할 때, 그 값이나 선택지가 컨트롤러 객체로 전달된다. 컨트롤러 객체는 들어온 입력을 해석해서-이는 앱별로 정의된 규칙대로 수행한다-모델 객체에게 할 일을 알려준다. 예를 들어 "새로운 값을 추가하라"거나 "현재 레코드를 삭제하라"는 지시를 모델로 전달하게 된다. 또한 모델 객체의 속성이 변경된 것을 반영하도록 한다. 또한 같은 입력에 대해 어떤 컨트롤러들은 뷰의 외형이나 동작을 변경하도록 할 수 있다. 특정한 조건에서 버튼이 사용불가 상태로 되거나 한느 등의 조정을 수행한다. 다시말해 모델에서 어떤 값의 변화나 새로운 값의 추가가 발생하면, 모델은 컨트롤러에게 이를 알려주게 되고, 다시 컨트롤러는 그 값에 따라서 뷰에 표시되는 내용을 변경하거나, 뷰의 동작을 제한하게 된다.

컨트롤러 객체는 재사용 가능할수도 있고, 그렇지 않을 수도 있다. 이는 컨트롤러 타입에 의해 결정되는데, 이에 대한 자세한 내용은 "코코아 컨트롤러 객체의 타입"을 참고하도록 하자.

역할들을 묶기

하나의 객체에 MVC롤을 합치는 경우도 있을 수 있다. 예를 들면 뷰와 컨트롤러를 묶는 것이다. 이 경우에 이 객체는 '뷰 컨트롤러'가 된다. 같은 방법으로 '모델-컨트롤러'를 만들 수 있다. 이따금씩 앱의 디자인에서 이러한 디자인은 수용할만한 변형이라 하겠다.

모델 컨트롤러는 컨트롤러 그 자체가 모델 레이어와 밀접하게 연결되어 있다. 모델 컨트롤러는 모델 객체를 "소유"한다. 이 객체의 일차적인 의무는 모델을 관리하고 뷰와 통신하는 것이다. 모델에 적용되는 액션 메소드 전체는 통상 모델 컨트롤러에서 구현된다. 문서 아키택쳐는 이러한 메소들을 많이 제공한다. 예를 들어 NSDocument 객체는 (이는 문서 아키텍쳐의 중심이 되는 객체이다.)파일을 저장하는 액션 메소드를 자동으로 다루게 된다.

뷰 컨트롤런느 뷰 계층과 밀접하게 관련된 컨트롤러이다. 이 컨트롤러는 뷰를 소유하게 된다. 뷰 컨트롤러는 뷰를 관리하고 멜 객체와 교신하는 임무를 가진다. 모델의 값을 뷰에 ㅍ시하는 액션 메소드는 통상 뷰 컨트롤러에서 구현된다. NSWindowController 객체는 뷰 컨트롤러의 한 예이다. "Design Guildlines for MVC Application"에서 MVC롤을 통합하는 것과 관련한 디자인 조언을 얻을 수 있다.

코코아 컨트롤러 객체의 타입

바로 앞 글에서 컨트롤러 객체의 추상적인 개요에 대해서 간단히 언급했지만, 실제 상황은 보다 복잡하다. 코코아에서는 "중계 컨트롤러"와 "코디네이팅 컨트롤러"라는 두 가지 종류의 컨트롤러가 존재한다. 각각의 컨트롤러 객체는 다른 클래스들과 연관되며, 다른 범위의 동작을 제공한다.

중계 컨트롤러는 주로 NSController로부터 상속되며, 코코아 바인딩에 사용된다. 이들은 뷰와 모델 사이의 데이터 흐름을 중계한다. (코코아 바인딩은 AppKit에서만 제공되므로 iOS에서는 지원되지 않는다.) 중계 컨트롤러는 주로 기성 객체인 경우가 많으며 인터페이스 빌더 라이브러리에서 끌어다 놓아서 사용하게 된다. 그리고 인터페이스 빌더에서 뷰의 프로퍼티와 컨트롤러의 프로퍼티를 연결하고, 다시 컨트롤러 프로퍼티와 모델 프로퍼티간의 연결을 만들어주게 된다. 결과적으로 사용자가 뷰에서 어떤 데이터의 값을 변경하면 이것이 자동적으로 모델 객체에 반영-컨트롤러 객체를 통해서-된다. 그리고 반대로 모델에서 값이 변경되면 이 것이 자동적으로 뷰에 반영된다. 추상 클래스인 NSControllerNSObjectController, NSArrayController, NSUserDefaultsController등의 구체화된 서브 클래스들을 가지고 있으며, NSTreeController는 변경사항을 확정하거나 취소하는 기능과 섹션의 관리, placeholder 값 등의 기능을 ㅈ원해준다ㅏ.

코디네이팅 컨트롤러NSWindowControllerNSDocumentController와 같은 것들인데, 이 이들의 역할은 전체 앱이나 앱의 어떤 부분의 기능을 총괄하는 역할을 담당한다. nib 파일로부터 객체를 로딩하는 등의 처리를 수행하는 것은 코디네이팅 컨트롤러이며, 그외에 다음과 같은 작업을 담당한다.

  • 옵저빙 알림이나 델리게이션 메시지에 대해 응답한다.
  • 액션 메소드에 대해 응답한다.
  • 소유하고 있는 객체들의 라이프 사이클을 관리한다.
  • 객체간의 연결을 만들며, 셋업 동작을 수행한다.

NSWindowControllerNSDocumentController는 문서 기반 앱을 위한 코코아 아키텍쳐의 일부분이다. 이 클래스들이 제공하는 기본 동작을 사용하는 대신에 앱에 특화된 동작을 위해서 이들 컨트롤러 클래스를 서브클래싱할 수 있다. 물론 문서 기반 앱이 아니더라도 NSWindwosController를 사용할 수 있다.

코디네이팅 컨트롤러는 종종 nib 파일에 아카이빙되어 있는 객체들을 소유한다. File's Owner로서 코디네이팅 컨트롤러는 객체를 nib 파일로부터 끄집어 내고 이들을 관리한다. 이런 객체들에는 뷰나 윈도우가 포함된다. 파일 소유자로서의 코디네이팅 컨트롤러에 대한 자세한 내용은 "MVC as a Compound Design Patter"을 참고하도록

NSObject를 상속받는 커스텀 객체를 코디네팅 컨트롤러로 쓸 수 있으며 중계 컨트롤러와 코디네이팅 컨트롤러의 기능을 결합할 수도 있다. 중계컨트롤러의 기능을 지원하기 위해 타깃-액션, 아울렛, 델리게이션이나 알림등의 패턴을 사용할 수 있다. 이러한 컨트롤러는 앱에 특화되어 있으며 재사용이 거의 불가능하고 내부에 접착 코드를 많이 포함하게 된다.

복합 디자인 패턴으로서의 MVC

MVC는 다양한 기본적인 디자인 패턴이 복합된 디자인 패턴이다. 이 기본 패턴들은 기능적인 분리를 정의하고, MVC 앱의 특징인 통신의 경로를 정의하는데 사용된다. 하지만 전통적인 MVC의 코코아와는 다르게 기본 패턴들을 사용한다. 그리고 이러한 차이는 컨트롤러와 뷰의 역할의 차이에 기인한다.

스몰토크의 원래 MVC 컨셉은 Composite, Strategy, Observer 패턴을 근간으로 하고 있다.

  • Compose – 앱의 뷰 객체는 실질적으로 계층구조를 이루며, 뷰속에 뷰가 배치되는 복합체를 구성한다. 이러한 디스플레이 요소는 창에서부터 테이블뷰와 같은 복합뷰, 버튼과 같은 단일 뷰에 이르며, 사용자와의 상호작용은 이 계층의 어떤 레벨에서도 일어날 수 있다.
  • Strategy – 컨트롤러 객체는 하나 혹은 그 이상의 뷰에 대한 전략을 구현한다. 뷰는 스스로의 역할을 시각적인 측면을 유지하는 데에만 제한하고, 인터페이스 동작에 대한 처리는 컨트롤러에게 위임한다.
  • Observer – 모델 객체는 앱내의 관심이 있는 객체를 계속해서 저장, 추적하며(보통은 뷰가 된다) 자신의 상태가 변할 때 이를 알려준다.

이러한 전통적인 조합은 다소 복잡해 보인다. 뷰에서 사용자가 어떤 동작을 수행하면 그 결과 이벤트가 생성된다. 컨트롤러는 이벤트를 해석하여 전략에 적용한다. 이 전략이라는 것은 모델에 대한 요청일수도 있고, 뷰에 대해 상태를 바꾸라는 메시지가 될 수도 있다. 그리고 모델은 옵저버로 등록된 모든 객체에게 자신의 상태 변화를 통지한다. 만약 옵저버가 뷰 객체라면 모델의 변화에 따라 뷰가 변경될 수 있다.

코코아의 MVC는 이를 기반으로 하고 있기 때문에, 이 디자인을 그대로 적용한 앱을 만들 수 있다. 바인딩을 사용하면 뷰와 모델이 바로 연결되어 서로간의 교신을 할 수 있다. 하지만 여기에는 이론적인 문제점이 있다. 뷰 객체와 모델 객체는 앱에서 재사용이 가장 빈번한 객체들이다. 뷰는 앱이 지원하는 OS의 룩앤필을 통일감있게 따라야 하며, 이러한 통일성은 본질적인 것이다. 또한 그러한 가운데서도 재사용성을 높여야 한다. 모델 객체 역시 특정 분야의 문제와 관련된 데이터를 저장하고 조작하도록 정의된다. 재사용성을 높이기 위해서는 결국 이 둘의 역할을 완전히 분리하는 것이 옳다.

이러한 디자인에서 컨트롤러는 전략 패턴을 구사함과 동시에 중계 패턴을 사용하게 된다. 컨트롤러는 뷰와 모델 사이의 통신에 대해 양방향으로 중계한다. 모델에서의 변경 사항은 컨트롤러를 통해서 뷰에 전달된다. 또한 뷰 객체는 타깃-액션 매커니즘의 구현을 통해 명령 패턴을 수행하게 된다.

이렇게 MVC 패턴이 축소된데에는 이론적인 이유뿐만아니라 실실적인 이유도 있다. 중계 컨트롤러는 NSController의 세부 서브클래스로부터 만들어지며 이들은 중계 패턴을 구현하는 것외에도 선택영역의 관리나 플레이스홀더와 같은 여러 편의 기능을 제공하는데 이러한 잇점을 그대로 사용할 수 있다. 만약 바인딩 기술을 채택하지 않더라도 뷰 객체는 Cocoa Notification과 같은 매커니즘을 사용하여 뷰 객체로부터 알림을 받을 수 있다. 그러나 이러한 방식은 개별 뷰 클래스를 이런 통지를 받아 동작하도록 다시 서브클래싱해야 하는 단점이 있다.

잘 디자인된 코코아 MVC 패턴은 아예 코디네이팅 컨트롤러가 중계 컨트롤러를 소유하는 방식으로 만들어진다. (중계 컨트롤러는 인터페이스 빌더에서 생성되어 nib 파일에 아카이빙되어 들어가게 된다.)

MVC 앱을 위한 디자인 가이드

  • NSObject의 서브클래스로 새로운 커스텀 중계 컨트롤러를 만드는 것은 가능하지만, 여기에 필요한 모든 코드를 굳이 새로 작성할 이유가 없다. 대신에 NSController 클래스의 구체화된 서브 클래스를 사용하여 코코아 바인딩을 활용하라. NSObjectController, NSArrayController, NSUserDefaultsController, NSTreeController 등을 쓰면 되고 필요시에는 이들을 다시 서브클래싱 할 수 있다.

다만, 앱이 무척 간단하고, 이런 저런 접착 코드를 쓰는 일에 큰 어려움이 없다면 그렇게 해도 된다.

  • 하나의 객체에 MVC의 역할을 복합적으로 쓸 수는 있지만, 가급적 객체의 역할을 분리하라. 이러한 분리는 객체의 재사용성을 높이는데 도움이 된다. 만약 하나의 클래스에 이러한 롤을 합치려거든, 주요한 롤을 먼저 선택하고, 나머지 롤에 대해서는 카테고리 등으로 구현할 수 있다.

  • 잘 디자인된 MVC앱의 목적은 가능한 많은 객체를 재사용하는 것이다. 뷰나 모델은 매우 재사용성이 높도록 디자인되어야 한다. 앱마다의 별도 동작은 컨트롤러 객체에 집중되도록 한다.

  • 뷰가 모델의 상태에 직접적으로 변화를 감지할 수 있는 방법들이 다양하게 존재하지만, 그렇게 하지 않는 것이 최선이다. 뷰 객체는 모델의 상태에 대해 배우기 위해서 컨트롤러를 거쳐가는 것이 최선이다. 여기에는 두 가지 이유가 있다.

    • 바인딩을 직접 구현하는 것은 NSController 클래스가 주는 장점을 취하지 못하게 한다.
    • 바인딩을 사용하지 않는다면 이러한 뷰에 대해 모두 서브클래싱한 새로운 뷰를 작성해야 한다.
  • 앱의 클래스에 대해 제한된 코드 의존성을 유지하라. 한 클래스가 다른 클래스에 대해 의존도가 높아지면 그만큼 재사용성도 떨어진다.

    • 뷰 클래스는 모델 클래스에 의존해서는 안된다. (커스텀 뷰인 경우는 예외)
    • 뷰 클래스는 중계 컨트롤러의 클래스에 의존해서는 안된다.
    • 모델 클래스는 다른 모델 클래스를 제외한 어떤 클래스에도 의존해서는 안된다.
    • 중계 컨트롤러는 모델 클래스에 의존해서는 안된다. (커스텀 클래스인 경우는 예외)
    • 중게 컨트롤러는 뷰나 코디네이팅 컨트롤러에 의존해서는 안된다.
    • 코디네이팅 컨트롤러는 모든 MVC 타입에 대해 의존한다.
      "의존한다"는 것은 해당 클래스의 헤더를 임포트해서 그 클래스의 동작을 다 알고 있다는 뜻. 의존하지 않는 다는 의미는 다른 클래스의 존재 자체를 모르거나, 혹은 클래스의 종류에 무관하게 동작할 수 있다는 것이다.
  • 만약 코코아에서 어떤 프로그래밍 문제를 해결할 수 있는 아키텍쳐를 제공하고 있고, 이 아키텍쳐가 특정한 타입의 객체를 MVC 롤에 사용한다면 그것을 그대로 사용하라.

코코아의 MVC

MVC는 많은 코코아 기술과 매커니즘의 근간이다. 결과적으로 객체지향에서 MVC의 중요성은 앱의 확장성과 재사용성이 더 요구될 수록 비중이 커진다. 만약 만들고자하는 앱이 MVC 기반의 코코아 기술과 연계된다면, 앱이 MVC를 따랐을 때 가장 잘 동작하게 될 것이고 이러한 기술들을 무리 없이 쓸 수 있게 된다. 만약 이렇게 하는 것에 어려움을 많이 겪고 있다면, 이러한 '분리'를 제대로 하지 못한 것이다.

다음의 기술들은 MVC를 기반으로한 코코아의 일부들이다.

  • 문서 아키텍쳐 – 이 아키텍쳐에서 문서 기반의 앱은 전체 앱(NSDocumentController), 문서 창(NSWindowController), 컨트롤러와 모델이 합쳐진 개별 문서(NSDocument)객체로 이루어진다.

  • 바인딩 – MVC는 바인딩의 핵심이다. NSController의 서브클래스들은 뷰와 모델의 연결을 만들고 세팅할 수 있는 기성 클래스를 제공한다.

  • 앱 스크립트 지원 – 스크립팅을 지원하는 앱을 만들기 위해서는 MVC를 잘 따라야 할 뿐만 아니라, 앱의 모델 객체도 잘 디자인해야 한다. 앱의 상태에 액세스하고 특정한 동작을 요청하는 스크립팅 명령은 모델이나 컨트롤러 객체로 전달된다.

  • 코어데이터 – 코어데이터 프레임워크는 모델 객체의 그래프를 관리하고, 이들이 영구저장소에 적절히 저장되고 로드될 수 있도록 해준다. 코어 데이터는 바인딩과 밀접하게 결합되어 있는 기술이다. MVC와 객체 모델링 디자인패턴은 코어데이터 아키텍쳐의 핵심이다.

  • 되돌리기 되돌리기 아키텍처에서는 모델 객체가 다시 한 번 주연을 맡게 된다. 모델 객체의 원시 메소드(주로 액세서들)에서 이러한 되돌리기가 구현된다. 뷰와 컨트롤러의 액션도 이러한 동작과 관련된다.