Cocoa Internals - 코코아 디자인 패턴

코코아 디자인 패턴

디자인 패턴은 프로그래밍 과정에서 문제 해결을 위해 반복해서 경험하는 객체와 클래스 관계를 정리한 것이다.

Cocoa 프레임워크는 디자인 패턴 책이 나오기 이전 NeXT 시절부터 객체 중심 소프트웨어를 만들기 위해 다양한 패턴을 반영하여 만들어졌다.

코코아 프레임워크 핵심 패턴

두 단계 초기화 패턴

초기화 과정

NSObject를 상속받은 모든 클래스는 인스턴스가 만들어지기까지 두 단계two-phase에 걸쳐서 초기화가 이루어진다.

Pen *pen = [[Pen alloc] init];
  1. +alloc 메세지를 보내어 힙 공간에 객체 인스턴스 메모리 공간을 할당한다.
  2. -init 메세지를 보내어 객체 인스턴스 속성이나 내부에 필요한 객체나 값을 초기화한다.

1단계의 메모리 할당이 되어 있지 않다면 2단계의 초기화 과정은 진행이 불가능하다.

1단계의 메모리 할당이 되었으나 2단계 초기화 과정을 진행하지 않아도 메모리 할당 과정에서 객체 내부의 모든 값이 0으로 초기화되나, 명시적인 초기화 과정을 진행하는 것을 권장한다.

Pen *pen = [Pen new];

위와 같이 간편한 메소드를 사용하여 메모리 할당 및 초기화 과정을 한꺼번에 진행할 수 있다.

+new에 메세지를 보내는 것은 +alloc에 메세지를 보낸 후 -init에 메세지를 보내는 것과 동일하다.

지정 초기화 메소드

Designated Initializer

메소드 명칭이 init으로만 시작하면 인자 값에 따라 여러 개의 초기화 메소드를 만들 수 있다.

여러 초기화 메소드 중에서도 기준이 되는 지정 초기화 메소드를 둘 것을 권장한다.

예를 들어 NSString의 지정 초기화 메소드는 -init이고, UIView의 지정 초기화 메소드는 -initWithFrame:이다.

지정 초기화 메소드를 만들 때 다음을 주의해야 한다.

  • 서브클래스에서 지정 초기화 메소드를 구현할 때는 반드시 부모의 지정 초기화 메소드를 호출해야 한다.
  • 서브클래스에서 부모에 없는 새로운 보조 초기화 메소드를 만들 때는 반드시 자신의 지정 초기화 메소드를 호출해야 한다.
    • Swift의 편의 이니셜라이저convenience initializer의 개념과 동일함
  • 수퍼클래스의 지정 초기화 메소드에서 반환되는 객체를 self에 할당한다.
  • 수퍼클래스의 지정 초기화 메소드가 nil을 반환하면 인스턴스 내부 변수를 사용하지 않고 nil을 그대로 반환한다.

위의 내용은 Swift의 지정 이니셜라이저와 편의 이니셜라이저의 관계, Swift의 2단계 초기화와 그 맥락을 같이 한다.

MVC(Model-View-Controller) 패턴

Objective-C에 가장 많은 영향을 준 언어는 Smalltalk이며, MVC 패턴 또한 이것에서 이어진 흐름이라고 할 수 있다.

  • 컨트롤러 객체는 모델 객체를 업데이트하며, 모델 객체는 컨트롤러 객체에 변화를 준다.
  • 컨트롤러 객체는 뷰 객체를 업데이트하며, 뷰 객체는 컨트롤러 객체에 입력 및 변화를 준다.

모델 객체

화면을 구성하거나 내부 처리를 위한 데이터를 추상화하여, 타입을 지정한 자료 구조로 표현하고, 데이터를 처리하는 로직을 정의한다.

모델 객체를 정의하거나 파일, 데이터베이스의 형태로 저장될 수 있다.

모델 객체는 뷰 객체와 직접적으로 연결되지 않으며, 데이터에 접근하는 유일한 추상화 객체가 된다.

뷰 객체

iOS에서는 일반적으로 UIView를 상속받는 클래스에 해당한다. 앱 화면 자체를 그려서 표시하며, 사용자 선택에 따라 입력을 받거나 피드백을 준다.

뷰 객체가 화면에 표시하는 정보는 모델 객체가 가지고 있는 데이터에 기반한다.

컨트롤러 객체

뷰 객체와 모델 객체 사이에서 중재자 역할을 한다.

사용자 입력에 따른 새로운 데이터 변화를 확인하고, 관련 모델에 새로운 데이터를 업데이트해준다.

모델에서 데이터가 변화하면 뷰에 전달하여 새로운 데이터를 화면에 표시한다.

iOS에서 MVC 패턴을 구현할 때 컨트롤러 객체가 비대해지는 경향이 있다. 이를 해결하기 위한 MVVM, VIPER 등의 패턴이 있다.

메세지 셀렉터 패턴

어떤 객체가 Cocoa 객체에게 메세지를 보내면, Cocoa 런타임은 해당 객체 메소드 중에서 메세지를 처리할 메소드를 찾아 메소드의 함수 포인터를 호출한다.

위의 과정을 ‘다이나믹 디스패치’라고 부른다.

런타임에서 메세지를 처리하기 이전에 메소드를 선택하거나 메소드 바인드를 지연시키기 위해 메세지 셀렉터가 사용된다.

셀렉터(SEL)와 구현 포인터(IMP)

Cocoa 객체의 메소드를 찾기 위해 셀렉터와 구현 포인터가 사용된다.

셀렉터는 메세지를 받을 객체의 메소드 중 적합한 메소드를 고르는 역할을 한다.

SEL theSelector = @selector(drawSomething);

셀렉터 선언시 SEL 타입과 @selector() 예약어를 사용한다. 위 코드의 결과로 theSelectordrawSomething이라는 이름의 메소드를 골라서 사용할 수 있는 상태가 된다.

런타임에서 객체 메소드를 저장하는 구조체(objc_method == Method)는 메소드 시그니처(셀렉터), 메소드 매개 변수 타입(문자열), 메소드 구현 포인터(IMP)를 포함한다.

Method 구조체는 Objective-C 런타임의 objc_getClassMethod()objc_getInstanceMethod()와 같은 함수를 사용하여 얻을 수 잇다.

셀렉터 실행 및 지연 실행

[myPen drawSomething];
[myPen performSelector:@selector(drawSomething)];
[myPen performSelector:theSelector];

위의 세 문장은 같은 동작을 한다.

  • 첫 번째 문장은 객체에 메세지를 보내는 방식이다.
  • 두 번째 문장은 메소드에 대한 셀렉터를 찾아 셀렉터를 실행하는 방식이다.
  • 세 번째는 미리 찾았던 셀렉터를 실행하는 방식이다.

NSSelectorFromString() 함수를 사용하여 문자열로 된 메소드 시그니처로부터 셀렉터를 얻을 수 있다.

셀렉터 실행을 통해 메세지를 보내면서 일부러 전달하는 시점을 지연시키는 것이 가능하다. -performSelector:withObject:afterDelay:와 같은 NSObject의 인스턴스 메소드를 사용한다.

최근 이러한 지연 실행은 GCD를 활용하여 구현된다.

타깃과 액션

타겟-액션 패턴에서 셀렉터 패턴이 자주 활용된다.

  • 사용자 인터페이스 객체의 특정 이벤트를 받을 객체를 타겟으로 설정한다.
  • 이벤트를 받아 처리할 메소드를 액션으로 지정한다. 이 때 셀렉터를 사용한다.

셀렉터는 실행 중에 문자열로 지정하는 방식이 가능하기 때문에 특정 객체의 이벤트 처리를 실행 중에 타겟과 액션으로 지정하여 연결할 수 있다.

요약

Cocoa 프레임워크에서 전체적으로 적용되는 두 단계 초기화 패턴 / MVC 패턴 / 셀렉터 패턴에 대해 알아두자.

셀렉터 패턴은 메소드를 동적으로 지정할 수 있게 한다.

객체 사이 결합성을 줄여주는 패턴

객체 관계가 복잡해지고 결합성이 높아지면 요구사항이 변경되었을 때 특정 객체의 인터페이스나 동작을 변경하기 어려워진다.

관련된 객체 사이의 결합성을 줄이고 변화에 대비해서 보다 유연한 구조를 갖도록 하기 위해 몇몇 디자인 패턴을 활용할 수 있다.

싱글턴 패턴

인스턴스가 오직 하나만 있는 객체. 공유 인스턴스shared instance.

UIApplication 공유 인스턴스

UIApplication 클래스는 iOS 앱 프로젝트에서 자주 사용하는 싱글턴 객체 중 하나다.

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

위와 같은 코드를 작성하여 UIApplication의 싱글턴 객체를 생성할 수 있으며, 이는 앱의 이벤트 전달과 액션 처리를 위한 무한 루프를 가지고 있다.

UIApplicationDelegate에서 앱 수준의 중요한 이벤트와 생명 주기를 관리한다.

초기 iOS는 앱에서 여러 화면을 윈도우 단위가 아닌 뷰 단위로 처리하게 하였으나, 현재는 애플 TV 등과의 화면 연동이 가능해지면서 UIScreen 객체를 활용하여 멀티 스크린의 멀티 윈도우 앱 개발이 가능해졌다.

UIWindow 목록을 관리하고 현재 표시되는 key window를 찾게 해준다.

싱글턴 구현 방법

  1. 앱 시작 시점에 미리 만들어놓고 곧바로 공유 인스턴스를 참조하여 사용
  2. 공유 인스턴스에 처음 접근하는 시점에 객체 인스턴스 생성 (지연 생성)
  3. 앱 생명주기에 맞추어 한 번만 실행되는 부분에서 객체 인스턴스를 미리 만들어둠

NSFileManager 공유 인스턴스

멀티 스레드 환경에서 해당 클래스의 공유 인스턴스에 접근하는 경우 안정적이지 않으므로 스레드별로 인스턴스를 생성하여 사용하는 것이 권장된다.

우리가 만드는 공유 객체가 멀티 스레드 환경에서 안전하지 못하다면 이 또한 스레드별로 인스턴스를 생성하여 사용하는 것이 좋다.

생각거리

싱글턴 객체는 다른 객체와의 과도한 결합성을 줄이는 데 도움을 준다.

하지만 멀티 스레드 환경이나 참조하는 객체가 너무 많은 경우 좋은 선택이 아닐 수 있다.

옵저버 패턴

객체 상태 또는 내부 값이 바뀌면 이와 관련 있는 다수 객체에 한꺼번에 알려주기 위해 사용된다.

상태 변화가 발생했을 때 이 알림을 받을 객체observer를 등록subscribe하고 변화 발생을 알리는publish 방식으로 구현된다.

옵저버의 메소드를 지정하기 위해 셀렉터 패턴도 함께 사용된다.

통보

Notification

Cocoa의 모든 객체는 옵저버가 될 수 있고, 자기 자신이나 다른 객체를 통보 센터notification center에 옵저버로 등록할 수 있다.

모든 객체는 등록되어 있는 옵저버들에게 통보 센터를 통하여 통보를 전달할 수 있다.

통보 센터는 통보를 확인하고 통보룰 전송할 조건을 확인하여 조건에 부합하는 옵저버 객체에게 메세지를 보낸다.

NSNotificationCenter가 통보 센터의 역할을 한다. 해당 클래스의 공유 인스턴스를 통하여 옵저버 등록 및 통보 전달을 한다.

notification name이나 sender가 nil인 경우 해당 조건에 대하여 조건 없이 통보받을 수 있다.

생각거리

통보를 보내는 메소드는 기본적으로 동기적으로 동작한다.

옵저버 개수가 많거나 옵저버에서 처리하는 동작이 느린 경우 NSNotificationQueue와 같은 것을 사용하여 비동기 방식으로 동작하게 하는 것을 생각해볼 수 있다.

응답 체인 패턴

Chain of Responsibility. 책임 연쇄 패턴.

응답할 가능성이 있는 객체들을 체인 형태로 차례대로 확인하고, 더 이상 응답할 객체가 없을 때까지 반복해서 확인하는 방식이다.

체인에 등록된 객체들은 해당 메세지를 처리하거나, 처리하지 않고 다음 객체로 전달할 수 있다.

응답 객체

Responder Object

Cocoa Touch 프레임워크에서 UIResponder 클래스는 이벤트를 받아서 응답하는 모든 객체의 최상위 클래스가 된다.

이는 뷰나 윈도우를 포함하므로 화면을 구성하는 모든 클래스는 응답이 가능한 객체다.

처음 응답 객체

처음 응답 객체는 응답 체인의 처음 항목을 지칭하는 프록시 객체다.

일반적으로 특정 메세지나 이벤트를 처리할 객체를 고정적으로 바인딩하게 되는데, 이러한 것을 원하지 않는다면 이벤트 처리 대상을 처음 응답 객체로 지정할 수 있다. 이러면 응답 체인을 따라 응답 객체에게 해당 이벤트를 순서대로 보낼 수 있다.

처음 응답 객체와 연결한 이벤트에 대한 액션 처리는, 응답 체인에 있는 객체들을 차례대로 확인하면서 처리된다. 처리한 객체가 없다면 해당 이벤트는 무시된다.

일반적인 응답 체인 계층 구조는, hit-test가 가능한 최상위 뷰가 처음 응답 객체가 되어, 수퍼 뷰를 따라 올라가, 뷰 컨트롤러, 윈도우, 애플리케이션 객체까지 올라간다.

응답 체인 탐색

-setNextResponder: 메소드를 사용해서 명시적으로 다음 응답 객체를 지정할 수 있다.

멀티 윈도우 환경에서는 활성화된 키 윈도우 객체에 대한 탐색을 마친 후 메인 윈도우로 넘어가서 응답 체인을 탐색하기도 한다.

생각거리

iOS는 단일 윈도우 기반이라 응답 체인을 고려하는 경우가 적지만, 화면 구조가 복잡할수록 뷰와 뷰의 이벤트를 처리하는 객체를 명시적으로 바인딩하는 것을 지양해야 한다.

UI 변화와 기능 변경에 유연하게 대처하기 위해, 결합성을 줄일 수 있는 응답 체인을 구성하여 이벤트를 처리하는 것이 권장된다.

호출 패턴

Cocoa의 호출invocation 기법은 일반적인 커맨드command 패턴과 비슷하나 애플 스타일로 해당 패턴을 다시 정의한 것이다.

셀렉터와 타겟 객체를 지정하고 인자 값을 넘겨서 타겟의 셀렉터를 호출하는 방식으로 구현되었다.

프록시 패턴과 함께 사용하여 특정 객체가 메세지를 받기 전에 인자 값을 확인하거나, 내용을 변경하거나 동일한 메세지를 복사하여 다른 객체에 전달하는 방식을 구현할 수 있다.

NSInvocation 클래스

메세지 이름, 시그니처, 메세지 수신자, 인자 값을 포함하고, 실행 후 반환 값을 담을 수 있다.

  1. 메소드 시그니처를 문자열 형태로 지정
  2. 문자열 형태의 메소드 시그니처로부터 셀렉터 획득
  3. 셀렉터로부터 메소드 시그니처 객체(NSMethodSignature) 획득
  4. 위의 값들을 사용하여 NSInvocation 객체 생성

생각거리

NSInvocation 객체를 직접 생성하여 사용하는 것은 타겟-액션 패턴을 사용하는 것보다 번거롭다.

객체에 보내는 메세지 자체를 추상화하여 복사하거나, 일부러 지연시키거나 반복하여 여러 객체에 전달하려는 경우 호출 패턴을 사용할 수 있다.

요약

객체들 사이의 결합성을 줄여서 유연한 구조를 갖도록 도와주는 패턴들을 염두해두자.

객체가 많아지고 그 관계가 복잡해지면 결합성이 높아져 코드의 변경이 어렵게 된다.

  • 싱글턴 패턴은 한 개의 리소스에 접근해야 하는 경우 유용하다.
  • 옵저버 패턴은 어떠한 객체의 변화를 다른 여러 객체에 알려야 하는 경우 유용하다.
  • 응답 체인 패턴은 이벤트 메세지를 받을 객체를 명시적으로 지정하지 않고 처리할 수 있게 한다.
  • 호출 패턴은 객체가 보내는 메세지를 객체로 만들 수 있게 한다.

객체 내부의 복잡성을 감춰주는 패턴

객체 설계 범위는 객체마다 갖는 고유한 역할과 책임을 기준으로 한다.

객체의 역할과 책임에 대한 내부 구현 방식은 최대한 감추고 부품화하여 손쉬운 재사용을 가능하게 한다.

팩토리 추상화 패턴

추상 팩토리abstract factory 패턴.

세부 클래스를 지정하지 않고 관련된 클래스 패밀리(클래스 클러스터)를 생성할 수 있는 인터페이스를 제공한다.

예를 들어 NSNumber 클래스는 다양한 수를 저장할 수 있도록 여러 타입(int, char, float 등)을 하나의 클래스로 추상화하였다.

NSString 클래스로 인스턴스를 만들어도 내부적으로는 그 하위 클래스를 사용하여 인스턴스를 생성하며, 그 구현은 개발자가 몰라도 된다. 단지 NSString에 정의된 인터페이스를 사용하기만 하면 된다.

이러한 방식으로 구현된 클래스는 복잡함을 감추어 단순하게 사용할 수 있는 반면 감춘 부분이 많아 확장하기 어렵다. 따라서 카테고리 방식으로 원하는 기능을 추가하는 것이 권장된다.

파사드 패턴

내부 객체들의 복잡한 관계를 감추고 하나의 클래스가 인터페이스를 모두 담당하는 패턴.

UIImage와 같은 클래스가 파사드 패턴을 적용한 대표적인 클래스다. 해당 클래스는 여러 이미지 포맷 처리, 압축 데이터 처리, 그리는 방식, 헤더 데이터 구조 등 이미지와 관련된 내부 구조를 자세히 모르더라도 해당 클래스의 인터페이스를 사용하여 거의 모든 동작을 실행할 수 있게 하였다.

생각거리

파사드 패턴은 복잡한 협력 관계를 가진 객체들에 접근하는 단순한 인터페이스를 제공해주는 경우 유용하다.

그러므로 복잡한 협력 관계를 갖는 내부 객체를 먼저 개선하는 것이 중요하다.

Cocoa에서는 클래스 클러스터 객체가 파사드 객체 역할까지 하는 경우가 많다.

번들 패턴

번들 구조란, 실행 파일과 프로그램에서 사용하는 인터페이스, 문자열, 이미지, 음악 같은 리소스를 디렉토리에 구조적으로 묶어놓은 것을 말한다.

모든 macOS 또는 iOS 앱은 ‘앱 번들 구조’로 되어 있고, 개발 과정에 사용하는 프레임워크는 ‘패키지 번들 구조’를 갖추고 있다.

Info.plist 파일

번들 구조에서 앱에 대한 기본 정보를 담고 있는 파일

키 값은 Core Foundation의 CFBundle에 선언되어 있어 CF 접두어가 붙는다.

리소스 지역화

번들 내부에서 리소스 파일을 찾을 때 다음의 규칙을 따른다.

  • 지역화localized하지 않는 글로벌 리소스는 Resources 디렉토리에 포함한다.
  • 사용자 지역 설정에 맞추어 지역별 지역화 리소스를 찾는다.
  • 사용자 언어 설정에 맞추어 언어별 지역화 리소스를 찾는다.
  • Info.plist에 기본 언어로 지정한 개발 언어 리소스를 찾는다.

글로벌 리소스 파일들은 리소스 디렉토리 바로 아래에 있어야 한다.

  • KR.lproj 디렉토리 : 한국 지역
  • en_KR.lproj 디렉토리 : 한국 지역의 영어 사용자
  • ko.lproj 디렉토리 : 한국어 사용자

iOS 앱 번들

실행 파일과 리소스 전체가 하나의 디렉토리로 구성된다.

앱 바이너리 실행 파일 / Info.plist / 앱 아이콘 및 런치 이미지 등을 포함한다.

이처럼 번들 구조는 앱 뿐만 아니라 프레임워크, 플러그인 등 다양한 곳에서 활용된다.

프록시 패턴

프록시proxy 객체는 다른 객체를 감싸거나 대신해서 접근하는 기능을 제공한다.

NSProxy 클래스는 Cocoa 프레임워크에서 유일하게 NSObject 클래스를 상속받지 않고 NSObjectProtocol 프로토콜만 채택하는, 숨겨진 최상위 객체다.

이를 상속받아 만들어진 프록시 객체는 원하는 메세지를 다른 객체로 전달하는 역할을 한다.

송신 객체와 수신 객체 사이에 프록시 객체가 위치하여 중재하는 역할을 한다.

메세지 포워딩

특정 객체에 구현되지 않은 메세지를 보내면 셀렉터와 일치하는 메소드가 없어 런타임 에러가 발생하게 된다.

이 때 메세지를 포워딩하여 에러를 발생시키지 않고 해결할 수 있다.

뒤늦게 동적 메소드 추가하기

+resolveInstanceMethod: 메소드의 구현 여부를 살핀다. 구현되어 있다면 구현된 메소드를 호출한다.

곧바로 포워딩하기

다른 객체가 처리할 수 있는지 확인하여 다른 객체로 메세지를 포워딩한다.

완전 포워딩하기

NSInvocation 클래스를 사용하여 메세지를 담고 원하는 대상 객체로 보낸다.

생각거리

특정 객체 내부에 복잡한 관계를 가진 객체들에게 메세지를 단계적으로 전달해야 하는 경우가 있다.

이 때 내부 객체를 연속해서 델리게이트 객체로 지정하거나 노티피케이션을 보내기보다는, 프록시 패턴을 사용하여 해결하는 것이 좋다.

요약

객체가 많아질수록 객체 간 상속 관계, 참조 관계, 협력 관계는 복잡해질 수밖에 없다.

객체가 복잡한 객체 관계를 숨기고, 적합한 인터페이스만을 제공하고, 객체끼리 효과적이고 효율적인 협력 관계를 갖도록 설계하는 것은 중요하다.