2015년 1월 21일 수요일

[Swift] 딜리게이션 패턴(Delegation Pattern)

스위프트(Swift)는 Objective-C의 많은 기능들을 새롭게 포장해서 제공해 주고 있는데 프로토콜도 그 중 하나이다. 이 프로토콜(Protocols)과 딜리게이션 패턴(Deleagation Pattern)에 대해 간단히 정리해 본다.

프로토콜에 관해서는 별도로 정리한 내용이 있으니 잘 모른다면 ​아래 링크를 읽어보자.

[관련글] Swift - 프로토콜(Protocols)

딜리게이션 패턴(Delegation Pattern)

iOS나 OSX용 앱을 만들 때 delegate 라 불리우는 것을 종종 사용해 봤을 것이다. 이 delegate 라는 용어가 사용되면 그게 바로 딜리게이션 패턴을 구현한 것이라고 볼 수 있다.

딜리게이션 패턴은 특정 클래스가 하는 일 중 일부를 자신의 커스텀 클래스에서 대신 처리하기 위해 사용할 수 있는 패턴이다. 다른 언어와 비교하자면 클래스 추상화(Class Abstraction)와 비슷하게 생각 할 수도 있다. 물론 정확한 용어로는 delegate의 의미인 '위임한다' 라는 표현이 맞겠지만...
참고로 딜리게이션 패턴(Delegation Pattern)은 데코레이터 패턴(Decorator Pattern)의 하위 분류로서 '확장 기능 제공' 을 위한 디자인 패턴 중 하나이다.
딜리게이션 패턴을 구현한 예를 보자. 우선은 딜리게이트의 규칙을 정의한 프로토콜과 이 프로토콜을 사용하는 클래스이다.
protocol BlahProtocol {
    func blah(blahObject: BlahClass)
}

class BlahClass {
    var delegate: BlahProtocol?

    func something() {
        self.delegate?.blah(self)
    }
}
이 내용이 사실 딜리게이션 패턴의 전부이다. 위의 BlahClass에서 하는 일 중 특정한 경우 BlahProcotol에 명시된 blah 라는 메소드 호출 할 수 있도록 기술되어 있다.

이제 이 클래스를 이용해 보자.
class CustomClass: BlahProtocol {
    let blah = BlahClass()

    init() {
        self.blah.delegate = self       
        self.blah.something()
    }
    
    func blah(blahObject: BlahClass) {
        println("Calling with blah protocol")
    }
}

let cc = CustomClass()
// 콘솔에 "Calling with blah protocol" 이 찍힌다.
여기서 구현한 CustomClass는 BlahProcotol의 규칙에 따라 특정한 메소드를 구현했다. 이렇게 함으로써 내부에서 쓰는 BlahClass 인스턴스의 delegate에 자신을 위임(delegate)했다.

딜리게이션 패턴은 이처럼 특정 클래스에서 하는 일 중 일부를 자신의 클래스에서 구현 할 수 있도록 하는 디자인 패턴이다.

Optional Protocol Method

프로토콜에는 optional을 이용해 필수적으로 구현하지 않아도 되는 옵셔널 메서드 원형을 제시 할 수 있다. Objective-C 의 프로토콜에서 '@optional' 키워드를 붙여서 쓰던 것과 비슷하다.

하지만 스위프트에는 제약사항이 있다. 해당 프로토콜이 반드시 Objective-C 쪽에 알려질 수 있도록 만들어야 한다는 점이다. 좀 귀찮지만 위 코드를 아래처럼 수정하자.
@objc
protocol BlahProtocol {
    func blah(blahObject: BlahClass)

    // 아래는 optional이다.
    optional func blahBlah(blahObject: BlahClass, whyBlahBlah: String)
}

@objc
class BlahClass {
    var delegate: BlahProtocol?
    
    func something() {
        self.delegate?.blah(self)
    }

    func somethingAnother() {
        // optional이기 때문에 없을 수도 있다.
        // 그래서 옵셔널 타입 형식으로 호출한다.
        self.delegate?.blahBlah?(self, whyBlahBlah: "because blah blah")
    }
}
프로토콜에 @objc를 명시했다. 그리고 옵셔널 메서드인 blahBlah도 만들었다. optional 이라고 써 있으니 바로 이해 가능할 것이다.

클래스에도 @objc를 명시한 이유는 프로토콜에서 사용하는 인자로 전달되는 클래스이기 때문이다. 빼 먹은 경우 Xcode에서 친절하게 해결방법을 제시해 주니 별로 어렵게 생각 할 필요는 없다.

BlahClass의 somethingAnother 메소드 구현 내용을 잘 보자. 옵셔널 체인을 이용해 딜리게이트의 호출을 유연하게 처리하고 있다. 즉, 위임받은 오브젝트에 blahBlah 라는 메서드가 구현되어 있지 않으면 somethingAnother는 아무 일도 하지 않는다. 죽지 않는것만 해도 어디인가. :-)

이제 이 BlahClass를 사용하는 예제를 보자.

class CustomClass: BlahProtocol {
    let blah = BlahClass()
    
    init() {
        self.blah.delegate = self
        self.blah.something()
        self.blah.somethingAnother()
    }
    
    func blah(blahObject: BlahClass) {
        println("Calling with blah protocol")
    }
    
    func blahBlah(blahObject: BlahClass, whyBlahBlah: String) {
        println("Why = \(whyBlahBlah)")
    }
}

let cc = CustomClass()
// 콘솔에 "Calling with blah protocol" 및
// "Why = because blah blah" 가 찍힌다.
프로토콜 메서드를 구현한 것 중 두 번째인 blahBlah 메소드는 옵셔널이기 때문에 구현하지 않아도 무관하다. BlahClass의 somethingAnother 메서드에서 이 blahBlah를 호출하는데, 옵셔널 체인을 이용해 존재하지 않으면 실행하지 않도록 구현되어 있기 때문이다.

옵셔널 메서드 구현은 확실히 스위프트가 편하다. Objective-C 라면 responseToSelector를 이용해 귀찮게 코딩을 해야 되기 때문에...

weak 및 class-only protocol

상황에 따라 다르겠지만, 딜리게이션 패턴에서 딜리게이트(delegate)라는 프로퍼티는 weak가 되어야 할 때도 있다. 잘못하면 Retain Cycles가 발생 할 수도 있기 때문이다. 강조하지만 상황에 따라서이지 필수는 아니다. 잘 생각해서 구현하자.

그리고 위임을 받는 인스턴스를 반드시 클래스 타입으로 제한하는 것도 생각해 볼 문제이다. 구조체일 경우 레퍼런스 관리에서 클래스와는 다른 현상이 발생 할 수 있으니 미연에 방지하고자 하는 거라고 생각하자. 만약 프로토콜의 메서드에서 해당 프로토콜을 이용하는 클래스 오브젝트 레퍼런스를 받아서 써야 한다면 반드시 클래스 전용 프로토콜이 되어야 할 것이다. (위 예에서 프로토콜의 blahBlah 메서드가 이 경우이다)

그래서 최종적으로 수정된 코드는 아래와 같다.
@objc
// 클래스 전용 프로토콜(class-only procotols)
protocol BlahProtocol: class {
    func blah(blahObject: BlahClass)
    optional func blahBlah(blahObject: BlahClass, whyBlahBlah: String)
}

@objc
class BlahClass {
    // 약한 참조(weak reference delegate)
    weak var delegate: BlahProtocol?
    
    func something() {
        self.delegate?.blah(self)
    }
    
    func somethingAnother() {
        self.delegate?.blahBlah?(self, whyBlahBlah: "because blah blah")
    }
}
이 정도면 이제 Swift 뿐만 아니라 Objective-C 코드에서도 이 클래스를 가져다 쓰는데 여러운 문제는 없을 거란 생각이 든다. -_-;;;

[관련글] Swift - 프로토콜(Protocols)
[관련글] Swift Memory Management #3 구조체(struct)와 클래스(class)

댓글 2개 :

익명 :

optional을 @obj와 optional로 하지 않고

그냥 다 implement하는 대신 아무 기능도 하지 않는 코드를 삽입하는건 어떻게 생각하세요?
Objective-C를 사용하지 않는 앱이라는 가정하에서요!

Renn Seo :

구현하지 않은 것과 아무일도 하지 않게 구현하는 것은 뭐... 결론적으론 같긴 하겠지요. 하지만 optional 은 구현 여부를 파악할 수 있기 때문에 위임을 하는 녀석이 optional 쪽 구현이 어떻냐에 따라 어떻게 처리할 수 있는 여지가 남아있습니다.

...

빈 구현 대신 protocol의 기본(공통?)적인 구현부를 extension을 이용해 미리 구현해 둘 수 있습니다. 아마도 비슷한 이야기지 않을까 생각되네요.