본문 바로가기
프로그래밍 놀이터/iOS

[Effective Objective-C] #23 객체 간 통신에 델리게이트와 데이터 소스 프로토콜을 사용하라

by 돼지왕 왕돼지 2017. 9. 3.
반응형

 [Effective Objective-C] #23 객체 간 통신에 델리게이트와 데이터 소스 프로토콜을 사용하라


출처 : Effective Objective-C

@optional, @protocol, @selector, autoniling, bitfield, conform, data source pattern, delegate pattern, delegate suffix, flow of information, id<delegate>, Instrument, java interface, non-owning, optional, optional keyword, protocol, protocol memory semantic, protocol 메모리 시맨틱, respondsToSelector, respondsToSelector cache, retain cycle, SEL, strong, struct, unsafe_unretained, weak, [Effective Objective-C] #23 객체 간 통신에 델리게이트와 데이터 소스 프로토콜을 사용하라, 객체 간 통신, 구조체, 낭비, 내성, 다중 상속, 데이터 소스, 데이터 소스 패턴, 데이터 소스 프로토콜, 델리게이트, 델리게이트 패턴, 런타임, 리스트 뷰, 리테인 순환, 메서드 이름, 메서드 추가, 비소유, 비지니스 로직 분리, 비트필드, 상속, 선택적, 응답 내성, 인터페이스, 자바, 정보 흐름, 책임, 최적화, 측정, 카멜 표기법, 카테고리, 캐싱, 테이블 뷰, 프레임워크, 프로토콜, 핫스팟, 확장 카테고리



-

프로토콜은 자바의 인터페이스와 비슷한 기능이다.

오브젝티브-C 는 다중 상속을 지원하지 않는다.

그래서 프로토콜이 클래스가 구현해야 할 메서드 집합을 정의할 방법을 제공한다.

프로토콜은 대부분 델리게이트 패턴을 구현하기 위해 사용한다.



-

프로토콜을 배워서 사용하면 코드에 사용 방법을 훌륭하게 문서화 할 수 있어 유지 보수하기 훨씬 쉬운 코드를 만들 수 있다.



-

카테고리 역시 오브젝티브-C 의 핵심 기능 중 하나다.

카테고리는 상속을 사용해야 하는 다른 언어와는 달리 클래스를 상속받지 않고 메서드를 추가할 수 있게 하는 기능이다.

이 기능은 오브젝티브-C 런타임이 매우 동적이기 때문에 가능하다.

카테고리를 충분히 이해하지 않고 사용하면 곤란한 상황에 빠질 것이다.



-

객체가 서로 얘기해야 하는 상황이 자주 있다.

그리고 얘기하는 방법은 많다.

오브젝티브-C 개발자들이 널리 사용하는 프로그래밍 디자인 패턴 중 하나는 델리게이트 패턴이다.

이 패턴의 핵심은 특정 인터페이스를 정의하는 것인데 어떤 객체든 이 인터페이스를 따르면(conform) 다른 객체의 델리게이트가 될 수 있다.

이 다른 객체는 특정 정보를 얻기 위해 델리게이트에 요청하거나 델리게이트가 관심 있어 하는 일이 발생했을 때 델리게이트에 알린다.



-

이 패턴을 사용하면 데이터를 비지니스 로직으로부터 분리할 수 있다.

예를 들어 데이터 목록을 보여주는 사용자 인터페이스 뷰는 데이터를 어떻게 보여주는지에 대해서만 책임이 있다.

어떤 데이터를 보여줘야 할지 또는 데이터 상호 작용이 일어났을 때 어떻게 해야 하는지에 대해 결정할 필요가 없다.

뷰 객체는 데이터와 이벤트를 처리하는 객체를 담은 프로퍼티를 가질 수 있다.

이를 각각 데이터 소스와 델리게이트라고 한다.



-

오브젝티브-C 에서는 일반적으로 프로토콜을 사용하여 이 패턴을 구현한다.

코코아를 구성하는 많은 프레임워크에서 이 방법을 사용한다.



-

델리게이트 프로토콜은 보통 클래스 이름 바로 뒤에 delegate 단어를 붙여 이름을 짓는다.

이 때 전체 이름은 카멜 표기법을 사용한다.



-

프로토콜을 델리게이트가 있는 클래스에 프로퍼티로 제공된다.


@interface EOCNetworkFetcher : NSObject

@property (nonatomic, weak) id <EOCNetworkFetcherDelegate> delegate;

@end


프로퍼티를 strong  이 아닌 weak 으로 정의하는 것이 중요하다.

비소유(nonowning) 관계여야 하기 때문이다.

보통 델리게이트가 될 객체 또한 델리게이트를 소유하는 객체를 소유한다.


델리게이트를 잡고 있는 프로퍼티의 소유관계가 strong 속성이었다면 리테인 순환(retain cycle)이 발생했을 것이다.

이 때문에 델리게이트 프로퍼티는 항상 autoniling 의 장점이 있는 weak 프로퍼티 또는 autoniling 이 필요 없다면 unsafe_unretained 를 이용해 정의해야 한다.




-

델리게이트를 구현한다는 것은 클래스가 델리게이트 프로토콜을 구현한다고 선언하고,

그 프로토콜의 메서드 중 원하는 메서드를 구현하는 것이다.

클래스가 프로토콜을 구현한다고 선언하는 방법은 인터페이스나 클래스 확장 카테고리를 이용하는 것이다.

프로토콜은 인터페이스에 선언하면 다른 사람들에게 이 클래스가 프로토콜을 구현한다고 알려야 할 때 도움이 된다.

그러나 델리게이트의 경우 보통 내부적으로만 사용하기 때문에 다음과 같이 클래스 확장 카테고리에 선언하는 것이 더 일반적이다.

@implementation EOCDataModel () <EOCNetworkFetcherDelegate>

@end


@implementation EOCDataModel

- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data{

     ...

}


- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFailWithError:(NSError*)error {

     ...

}

@end



-

보통 델리게이트 프롵토콜의 메서드들은 선택적(optional)이다.

델리게이트가 될 객체가 모든 메서드를 처리하지 않기 때문이다.


메서드가 선택적이라는 것을 표시하기 위해 델리게이트 프로토콜은 거의 대다수 메서드 또는 모든 메서드에

@optional 키워드를 이용해 메서드를 선택적으로 만든다.

@protocol EOCNetworkFetcherDelegate

@optional

-(void) networkFetcher:(EOCNetworkFetcher*)fetcher didReceiveData:(NSData*)data;

-(void) networkFetcher:(EOCNetworkFetcher*)didFailWithError:(NSError*)error;

@end


-

델리게이트의 선택적 메서드(optional method)를 호출하기 전에는 델리게이트가 호출하는 선택자에 응답할 수 있는지 확인하기 위해 내성을 사용해야 한다.

NSData *data = …;

if ( [_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)] ){

     [_delegate networkFetcher:self didReceiveData:data];

}

respondsToSelector: 메서드는 델리게이트가 특정 메서드를 구현했는지 확인하는 데 사용된다.






-

델리게이트 메서드의 이름을 올바르게 쓰는 것도 중요하다.

메서드 이름에서 무슨 일이 일어났고 왜 델리게이트 메서드가 호출되었는지 알 수 있어야 한다.



-

델리게이트로부터 정보를 얻기 위해 delegate 메서드를 사용할 수 있다.



-

프로토콜은 클래스가 필요한 데이터를 얻기 위한 인터페이스를 제공하는 데 사용될 수 있다.

이 같은 델리게이트 패턴의 또 다른 사용 방법을 데이터 소스 패턴(Data Source Pattern)이라고 한다.

이 패턴은 클래스에 데이터를 제공하는 것에 초점을 잡고 있기 때문이다.

보통 델리게이트의 정보 흐름(flow of information)은 클래스 쪽으로 향한다.

정보의 흐름은 클래스로부터 나온다.


예를 들어 사용자 인터페이스 프레임워크의 리스트 뷰(list view) 객체는 리스트에 보여줄 데이터를 제공받기 위해 데이터 소스 프로토콜(data source protocol)을 사용할 것이다.

또 리스트 뷰는 리스트의 사용자 상호 작용을 다루는 델리게이트가 있다.

데이터 소스와 델리게이트 프로토콜을 분리하면 서로 상관없는 로직을 구분함으로써 인터페이스가 깔끔해진다.

게다가 데이터 소스가 될 객체와 델리게이트가 될 객체를 따로 가질 수 있지만 보통은 한 객체가 두 가지 일을 모두 한다.


구현하는 모든 델리게이트와 데이터 소스 패턴의 메서드는 선택적이다.



-

해당 선택자에 델리게이트가 응답할 수 있는지 검사하는 연산은 매우 빠르다.

그러나 이를 반복적으로 한다면 첫 호출 이후의 응답은 잠재적으로 낭비다.

델리게이트가 변하지 않는다면 주어진 선택자가 갑자기 응답한다거나 응답하지 않는 상황은 매우 드물기 때문이다.

이런 이유로 보통 델리게이트가 프로토콜의 메서드에 응답하는지 여부를 캐싱해서 최적화한다.



-

캐싱하기 가장 좋은 방법은 비트필드(bitfield) 데이터 타입을 사용하는 것이다.

이것은 C 의 기능인데 오브젝티브-C 에서는 잘 쓰진 않지만 이 목적으로 쓰기에는 훌륭하다.

비트필드는 다음과 같이 구조체의 특정 필드를 특정 크기의 비트 수로 설정할 수 있다.

struct data{

     unsigned int fieldA : 8;

     unsigned int fieldB : 4;

     unsigned int fieldC : 2;

     unsigned int fieldD : 1;

};


이 구조체에서 fieldA 는 정확히 8비트를 사용한다.

또한 fieldB 는 4비트, fieldC 는 2비트, fieldD 는 1비트를 사용할 것이다.

그래서 fieldA 는 0부터 255의 값을 저장할 수 있다.

그리고 fieldD 는 0 또는 1을 저장할 수 있다.

캐싱해야 하는 것은 델리게이트가 어떤 메서드를 구현하고 있는지 여부다.




-

@interface EOCNetworkFetcher(){

     struct{

          unsgined int didReceiveData : 1;

          unsigned int didFailWithError : 1;

          unsigned int didUpdateProgressTo : 1;

     } _delegateFlags;

}

@end


- (void)setDelegate:(id<EOCNetworkFetcher>)delegate {

     _delegate = delegate;

     _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];

     ...

}

이렇게 하면 delegate 메서드를 호출할 때마다 델리게이트가 주어진 선택자에 응답할 수 있는지 확인하는 대신에 표식(flag)만 확인하면 된다.


if ( delegateFlags.didUpdateProgressTo ){

     [_delegate networkFetcher:self didUpdateProgressTo:currentProgress[;

}



-

델리게이트 메서드가 많이 호출된다면 전체적으로 최적화가 이루어진다.

이 최적화를 필요로 하는 부분은 코드에 따라 다른다.

여러분의 코드를 측정하고(instrument) 핫스팟(내성이 많이 발생하는 부분)이 어디인지 찾고 이 구현을 적용하여 속도를 최적화하라.

이것은 데이터를 반복적으로 요청하는 데이터 소스 프로토콜에서 대부분 효과가 있다.




기억할 점


객체가 다른 객체에 특정 이벤트를 보내고 받을 필요가 있을 때 인터페이스(데이터를 주고 받는 규약)로 델리게이트 패턴을 사용하라.


델리게이트가 지원해야 하는 인터페이스를 정의하는 프로토콜의 메서드는 선택적 메서드로 정의하라.


객체가 다른 객체로부터 데이터를 가져와야 할 때 델리게이트 패턴을 사용하라.

이 델리게이트 패턴을 특히 ‘데이터 소스 프로토콜’ 이라고 한다.


필요하다면 프로토콜에서 어떤 메서드가 응답하는지 여부를 캐싱하기 위해 비트필드 구조체를 구현하라.




반응형

댓글