정보처리기사 시험에도 나오는 "GoF의 디자인 패턴"에서 나온 패턴이나 전략들이 소프트웨어 개발 전반에 영향을 미쳤다.
이 장에서 모든 디자인 패턴을 설명하지는 않는다. 대신 코코아 프레임워크에 반영되어 있는 디자인 패턴들 중 반드시 알아야 하는 핵심 패턴과 객체 사이의 결합성을 줄여주는 패턴에 대해 설명한다.
코코아 프레임워크 핵심 패턴 (3가지)
메모리 관리를 위한 두단계 초기화 패턴
객체의 역할에 따라 구분하는 MVC 패턴
객체가 처리할 메세지를 지연시키는 메세지 셀렉터 패턴
두단계 초기화 패턴
초기화 과정
NSObject에서 상속받은 클래스의 인스턴스가 만들어지기까지는 2단계의 초기화 과정이 이뤄진다.
Pen *aPen = [[Pen alloc] init];
alloc -> 메모리에 올리는 과정 : 힙 공간에 객체 인스턴스 메모리 공간을 할당 init -> 초기화 과정 : 객체 인스턴스 속성이나 내부 객체들을 초기화
다시 말해, 1단계에서는 +(instancetype)alloc 클래스 메서드로 객체 인스턴스 메모리를 할당 2단계에서는 메모리에 할당한 객체 인스턴스의 내부를 -(instancetype)init 인스턴스 메서드로 초기화한다.
당연하듯, 메모리에 객체 인스턴스가 존재하지 않으면 내부 프로퍼티, 객체들을 가르키는 주소또한 없기 때문에 1단계가 없이는 2단계를 진행할 수 없다.
반대로, 1단계는 됐는데, 2단계는 안됐다. -> 사용할 수 있는가? A : 사용할 수 있을 수도 있다. (항상 사용할 수 있는 것은 아니기 때문) 기본적으로 1단계가 거치면 객체 내부의 모든 값들은 0으로 초기화된다. -> 하지만 init을 통해 2단계를 거치는 것은 권장한다
두 단계의 초기화 과정을 한 단계로 줄이기 위해 Convenience Methods를 클래스 메서드로 제공하기도 한다.
Pen *aPen = [Pen new];
지정 초기화 메서드
초기화 메서드는 메서드가 init- 으로 시작하는 조건만 부합하면, 인자 값에 따라 몇개의 초기화 메서드를 만들어도 된다.
NSString의 경우 -initWithBytes:length:encoding: , -initWithString: ... 등 다양한 초기화 메서드가 존재한다.
코코아에서는 여러 초기화 메서드들 중에서 기준이 되는 지정 초기화 메서드를 둘 것을 권장한다. ex 1) NSString의 경우 빈 문자열 객체를 만드는 메서드가 지정 초기화 메서드이고, 다른 메서드들은 초기값을 넣을 수 있는 부가적인 보조 초기화 메서드이다. ex 2) UIView는 -initWithFrame: 메서드가 지정 초기화 메서드이며, 그외의 다른 초기화 메서드는 지원하지 않는다.
이렇듯, 모든 클래스가 다르기 때문에 "클래스 레퍼런스 문서"를 참고할 것을 권장한다.
지정 메서드 초기화를 만들 때 지켜야 할 사항들
상속받은 Sub 클래스에서 지정 초기화 메서드를 구현할 때는 반드시 부모의 지정 초기화 메서드를 호출해야 한다
상속받은 Sub 클래스에서 부모에 없는 새로운 보조 초기화 메서드는 자기의 지정 초기화 메서드를 호출해야 한다
Super 클래스의 지정 초기화 메서드에서 반환되는 객체는 self에 할당한다.
Super 클래스의 지정 초기화 메서드에서 nil을 반환하면, 인스턴스 내부 변수를 사용하지 않고 nil을 그대로 반환한다.
작성 방법은 2.3절을 참고
MVC 패턴
Model - View - Controller로 이뤄진 패턴 1986년 OOPSLA에서 "A Diagram for Object-Oriented Programs"라는 논문에서의 MVC 패턴의 관계 그림
-> 현재 흔히 아는 MVC 패턴이라기보다는
1. Controller를 통해 사용자의 입력을 확인
2. 모델 데이터를 변경
3. 변경된 모델 데이터를 화면에 표시
하는 순차적인 흐름을 나타내었다
위에서 발전하여 우리가 아는 MVC 패턴 구조가 잡혔다.
또한, 코코아 프레임워크에서는 Model - Controller 관계에서 옵저버 패턴, 컴포지트 패턴을 사용하기도 한다. 옵저버 : Model - Controller 사이에서 NSNotificationCenter 컴포지트 : Controller가 해당 Model을 포함하는 경우 사용
모델 객체
화면을 구성
내부 처리를 위한 데이터를 추상화해서 지정한 자료구조로 표현
데이터 처리 로직을 갖고 있음
앱에서 지속적으로 사용하는 데이터는 모델 객체 내부에 캡슐화되고, FileManager나 DB를 통해 영구적으로 저장하여 사용하기도 한다.
MVC 패턴에서는 Model 객체와 View 객체가 직접적으로 연결되면 안된다.
뷰 객체
주로 UIView 클래스를 상속받아 화면 자체를 그리는 역할
사용자의 입력을 받는다
일반적으로 뷰 객체가 표시하는 정보는 모델 객체가 갖고 있는 데이터 기반 -> 이러한 이유로 서로 의존 관계가 생기면 안됨
하나의 View가 0개 이상의 Model Data와 매칭될 수도 있다.
View 객체는 화면 구성을 위해 Model 객체와 밀접한 관계를 갖고 있지만, 해당 관계를 느슨하게 하는 것이 MVC 패턴의 핵심이다. 그 상호 관계를 느슨하게 연결해주는 역할을 바로 Controller 가 담당한다. 따라서 Controller가 연결해주는 Model 객체에 따라서 View 객체는 얼마든지 재사용이 가능하다.
컨트롤러 객체
Controller는 View와 Model사이에서 사용자 입력과 데이터 변화에 대한 연결을 해주는 중재자 역할을 한다. V -> C -> M : 사용자의 입력에 따른 새로운 데이터 변화를 확인, 수정, 삽입, 삭제 M -> C -> V : 변경된 데이터를 받아 View에 전달, 화면에 표시, 인터랙션
View 객체의 화면 구조가 복잡하거나 사용자 입력 방식이 다양할수록 Data Model도 상대적으로 커진다. 이어서 Data Model이 커지면 해당 Model 객체를 조작해주는 Controller의 동작이 복잡해진다.
Massive
MVC 패턴에서 가장 고민거리는 Controller를 구현하는 코드가 복잡하고 길어지는 것이다.
가볍고 재사용성이 높은 Controller 객체를 만들기 위해 다양한 MVC 변형 패턴들을 함께 사용한다 (MVC-C 등) 또한 MVVM 패턴이나 VIPER 등의 다양한 패턴들이 존재한다.
메세지 셀렉터 패턴
다른 객체가 코코아 객체에게 메세지를 보내면 코코아 런타임은 해당 객체 메서드 중, 메세지를 처리한 메서드를 찾아 해당 함수 포인터를 호출한다.
다이내믹 디스패치
런타임이 메세지에 해당하는 객체 메서드에서 찾는 과정 : 런타임 중 찾기
1.4.2 절에서 나옴
런타임에서 메세지를 처리하기 이전에 메서드를 선택, 메서드 바인드를 지연하기 위한 패턴에 대해 알아보자
SEL(셀렉터)와 IMP(구현 포인터)
코코아 객체의 메서드를 찾기 위해서는 SEL과 IMP를 사용해야 한다. SEL : 메세지를 받을 객체의 메서드 중, 적합한 메서드를 고르는(Select) 역할 셀렉터가 없다면 C++처럼 Compile과정에서 객체 메서드에 대한 함수 포인터를 찾아 미리 고정된 메모리 주소에 바인드해놓아야 한다. -> Static 디스패치
SEL theSelector = @selector(drawSomething);
이처럼 셀렉터를 선언할 때는 SEL 타입을 사용하고, 변수는 @selector() 예약어를 사용한다. "theSelector는 drawSomething 이라는 이름을 가진 메서드를 골라서 쓸 수 있는 상태" if) 해당 객체에 메서드가 없다면, 동적으로 바인드되지 않아 에러가 날 수 있다.
참고로 런타임 API 중 method_getName() 함수를 사용하면 @selector()와 동일하게 셀렉터를 찾을 수 있다.
method_name : aㅔ서드 이름과 파라미터 키워드를 포함하는 메서드 시그니처를 SEL 타입으로 저장 method_types : 파라미터들에 대한 타입을 문자열로 저장 method_imp : IMP 타입으로 메서드 구현 포인터를 저장
런타임 API 중 class_getClassMethod() or class_getInstanceMethod() 함수에 클래스 타입과 SEL 타입을 넘기면 위의 Method 구조체를 얻을 수 있다.
상속 관계에 따라서 다이내믹 디스패치에 의해 셀렉터가 항상 동일하지 않을 수 있다.
셀렉터 실행과 지연 실행
아래 세 줄은 동일한 동작을 하는 코드이다.
[myPen drawSomething]; // 직접
[myPen performSelector:@selector(drawSomething)]; // NSObject의 -persormSelector:를 활용하여 drawSomething 메서드 셀렉터를 찾아 실행
[myPen performSelector:theSelector]; // 미리 찾아놓은 @selector
But, 1번과 2번은 특이하게도 변형이 가능하다. 코코아에 있는 NSSelectorFromString() 함수를 사용해서 문자열로 메서드 시그니처를 입력해서 셀렉터를 실행할 수 있다. 더 나아가, 스크립트 언어와 연결해서 코코아 객체에 메세지를 보내는 방식도 가능
메세지를 보내면서 일부러 전달 시점을 지연시키는 방법도 가능하다. NSObject에 -performSelector:(SEL)aSelectorwithObject:(id)anArgument afterDelay:(NSTimeInterval)delay 를 사용하면 된다. delay만큼 지연되며, 내부적으로는 해당 스레드에 이벤트를 감시하는 RunLoop에 전달되고, RunLoopdㅔ서 지연시간동안 기다린 후에 해당 객체로 메세지를 전달한다. 해당 RunLoop에 이벤트가 쌓여있거나 할 때는 항상 delay에 맞춘다는 보장은 없다.
최근에는 셀렉터 실행 방식보다는 저수준 병렬 처리 라이브러리 (GCD) 방식을 권장한다.
타깃과 액션
셀렉터 패턴을 자주 활용하는 경우는 타깃과 액션을 사용하는 경우이다. 타깃 : 특정 이벤트를 받을 객체 액션 : 이벤트를 받아(발생했을 때) 처리할 메서드
타깃-액션 패턴은 동적으로 변경이 가능하다. -> 재사용이 가능하다.
말했듯, 타깃-액션 패턴도 셀렉터를 사용한다. 셀렉터는 문자열로 지정하는 방식도 가능하기 때문에, 특정 객체의 이벤트 처리를 실행 중 타깃-액션으로 지정해서 연결할 수 있다.
요약
코코아 프레임워크는 다양한 디자인 패턴을 기반으로 개발되어 있다. 뼈대를 이루는 핵심적인 디자인 패턴은 반드시 숙지해야 한다. 모르더라도 개발은 할 수 있지만, 이해한다면 모든 코코아 객체를 다루기 훨씬 쉽다.