Delegate 패턴

- 위임 패턴

- iOS를 많이 겪어보지 않았다면 생소한 패턴이다.

 

패턴

반복해서 나타나는 형태를 의미한다. -> 유명한 싱글턴, 팩토리, 어댑터, 옵저버 등의 디자인 패턴도 이러한 '패턴' 인 것이다.

 

델리게이트 패턴

객체지향 프로그래밍(OOP)에서 하나의 객체가 모든 일을 처리하는 것이 아니라 일들 중 일부를 다른 객체에 넘기는 것

효율 성 관점에서 아주 중요하다 -> 기능을 위임할 수 있는 객체가 있다는 것은 그만큼 직접 구현해야 하는 부분이 적다는 것이기 때문.

 

GUI 프로그램에서의 예를 들어보자

  1. 마우스 버튼을 클릭한다
  2. 마우스는 시리얼 케이블이나 블루투스 통신을 통해 신호를 전달한다
  3. 신호는 RS232 통신 프로토콜을 이용해 메인보드를 거쳐 OS의 메시지 센터로 전달된다
  4. 신호를 받은 OS는 마우스 포인터의 화면상 좌표를 확인한다
  5. OS는 해당 좌표에서 활성화된 App을 확인한다
  6. Delegate를 이용하여 클릭 신호가 App의 이벤트 처리 함수 onClick()에 대응하였음을 App에게 알린다
  7. APp이 onClick()을 실행한다.

-> 이러한 순차적인 동작을 우리는 직접 처리하지 않는다. 하지만, 클릭 이벤트에 대응되는 메소드를 작성하면 동작한다.

-> 이것은 클릭을 인식하고 App에게 전달해주는 위임(Delegate) 객체가 있기 때문

 

in iOS

iOS에서도 같은 개념이다. 기능을 처리할 객체를 Delegate로 설정하고, 특정 이벤트가 발생할 때 이를 Delegate에 의해 위임된 본래의 객체로 전달해주는 역할이다.

 

예)

메세지를 전달하기 위해 사용되는 Delegate 프로토콜

protocol MessageDelegate: class{
	func printMessage(msg: String)
}

- class를 선언한 이유 : class에서만 사용이 가능하다

 

대리자(프로토콜 채택)

class MainViewController: UIViewController, MessageDelegate{
	...
    //채택한 프로토콜 준수하기 위한 메소드 구현
    func printMessage(msg: String){
    	print(msg)
    }
    
}

일을 넘긴 자(위임자)의 어떠한 부분을 대신 처리해주는 객체인 대리자이다

선언했던 Delegate 프로토콜을 채택하였으며, 채택한 프로토콜을 준수하기 위해 메소드를 구현했다

위임자의 어떠한 이벤트 발생 시, 해당 메소드가 호출된다고 생각하면 된다.

 

만약 SecondViewController가 있고 해당 객체에서 대리자(MainViewController)에게 일을 위임하고 싶다면, 서로의 '존재' 정도는 알아야하지 않겠는가

-> 즉, 위임자가 자신의 대리자가 누구인지는 알아야 일을 부탁하던 요청하던 짬때리던 할 것 아닌가?

-> 따라서 위임자의 대리자가 MainViewController라는 것을 설정해야 한다.

 

위임자의 대리자 설정

//SecondVC에 대리자를 할당하기 위한 변수 선언
//참조 사이클을 회피하기 위해 약한 참조로 선언
class SecondViewController: UIViewController{
	...
    weak var delegate: MessageDelegate?
    ...
    
    @IBAction func tappedPrintButton(_ sender: Any){
    	delegate?.printMessage("여기다 적고 싶은 말")
    }
    
}

//MainViewController에 추가

class MainViewController: ....{
	...
    
    @IBAction func tappedNextButton(_ sender: Any){
		let vc = SecondViewController()
        //SecondVC의 대리자가 self(자신)이라고 설정
        vc.delegate = self
        self.present(vc, animated: true, completion: nil)
    }
    
    ...
    
}

이렇게 작성하면 정상적으로 동작하는 것을 볼 수 있다.

 

정리

  1. 위임자가 대리자에게 어떤 일을 맡길 지 선언한다(위의 '메세지를 전달하기 위해 사용되는 Delegate 프로토콜')
  2. 위임자는 대리자에게 자신의 일들 중 일부를 맡긴다
    1. 따라서 위임자는 대리자가 누구인지 알아야한다 (위의 '위임자의 대리자 설정')
  3. 대리자는 위임자가 나에게 일을 시켰을 때, 작업할 동작을 구현한다. (위의 '대리자(프로토콜 채택) ')

같은 동작을 하는 클로저 콜백 방식

class MainViewController: UIViewController{
	...
    let secondVC = SecondViewController()
    
	func setClosureCallback(){
        secondVC.printMessage = { str in
        	print(str)
        }
    }
    
    @IBAction func tappedNextButton(_ sender: Any){
    	present(secondVC, animated: true, completion: nil)
    }
}


class SecondViewController: UIViewController{
	var printMessage: (String) -> Void)?
    
    @IBAction func tappedPrintButton(_ sender: Any){
    	printMessage?("여기다 적고 싶은 말")
    }
    
}

Delegate 방식과 클로저 콜백 방식 두가지 예제는 모두 같은 동작을 한다.

그러면 뭘 써?

사실 델리게이트 패턴이라는 말을 많이 들어서 만능인 줄 아는 사람들이 많다. 물론 나도 그렇다

하지만 위처럼 델리게이트 패턴이 아닌 클로저를 전달하므로써 똑같은 동작을 구현할 수 있다.

물론 이전 포스팅에서 알아본 NotificationCenter를 사용해도 가능하다.

각 방법을 사용하기 좋을 때가 있다고는 하는데 너무 다양한 의견이 많아서 정리하기가 힘들다.

그래서 나도 나만의 생각을 얘기해보겠다.

 

Delegate

장점 : 재사용할 수 있는 코드를 작성 가능(프로토콜의 장점)

단점 : delegate 사용을 위해 구현해야 하는 코드가 많다, 다수의 객체들에게 이벤트를 호출하는 방식이 비효율적

 

Notification

장점 : 옵저버에 추가한 다수의 객체들에게 동시에 이벤트를 발송 가능, 코드가 짧음

단점 : 코드의 흐름을 이해하기 어려울 수 있음

 

클로저 콜백

장점 : 쉽게 코드의 흐름을 파악할 수 있음

단점 : 많은 클로저를 사용해야 하는 경우 코드가 방대해짐

 

클로저

  • 제네릭, 프로토콕, 모나드 등과 결합이 가능하다
  • 람다와 비슷하다
  • 일정 기능을 하는 코드를 하나의 블록으로 모아놓은 것
  • 함수도 클로저의 일종

형태

  • 전역함수 : 이름 o, 값 획득 x
  • 중첩된 함수 : 이름 o, 다른 함수의 내부의 값 획득 o
  • 축약 문법으로 작성 : 이름 x, 주변 문맥에 따라 값을 획득 o

 

특징

  • 문맥을 통해 매개변수와 반환 값을 추론 가능하기 때문에, 생략이 가능하다
  • 단 한줄만 있다면 암시적으로 그것을 반환 값으로 취급
  • 축약된 전달인자 이름을 사용할 수 있다
  • 후행 클로저 문법을 사용할 수 있다

 

기본 클로저

정렬을 위한 함수 전달

func backWords(first: String, second: String) -> Bool{
	return first > second
}

var names: [String] = [.....]
let reversed: [String] = names.sorted(by: backWords) // 알파벳의 역순으로 정렬된 배열이 저장

정렬을 위한 클로저 전달

let reversed: [String] = names.sorted(by: { (first:String, second: String) -> Bool in
	return first > second
}) // 위의 코드보다 훨씬 간결해졌다.

후행 클로저

- 함수나 메서드의 마지막 전달인자가 클로저일 때, 소괄호를 닫고 작성할 수 있다.

let reversed: [String] = names.sorted(){ (first: String, second: String) -> Bool in 
	return first > second
}

//OR

let reversed: [String] = names.sorted{ (first: String, second: String) -> Bool in 
	return first > second
} // 소괄호 내에 어떠한 매개변수도 없기 때문에, 생략 가능

 

클로저의 간소화

- 클로저를 간소화 함으로써 가독성이 좋아지고, 코드가 간결해진다.

- 하지만 너무 많은 간소화를 통하면 소통이 되지 않을 수도 있으니 조심

 

문맥을 이용한 타입 유추

let reversed = names.sorted{ (first, second) in
	return first > second
} // 매개변수와 반환 타입을 생략 가능

단축인자 이름 사용

let reversed: [String] = names.sorted{
	return $0 > $1
} // $0 = first , $1 = second 라고 생각하면 된다

암시적 반환 표현

let reversed: [String] = names.sorted{ $0 > $1 }

아래로 내려오면서 코드가 훨씬 간결해지는 것을 볼 수 있다.

지금 예로 든 클로저는 간단해서 "어? 무조건 간소화하는 게 좋은 거 아냐?" 라고 생각할 수 있지만, 클로저 내부에서 많은 동작을 요구하고 매개변수 타입또한 복잡해지면 무턱대고 간소화하는 것은 오히려 역효과를 부를 수 있다

 

연산자 함수

- 연산자도 일종의 함수이다

 

// > 연산자 정의
public func ><T : Comparable>(lhs: T, rhs: T) -> Bool

T는 제네릭으로 이후에 자세히 포스팅할 것이다

여기서만큼은 T는 String이라고 생각하면 된다

 

우리는 위에서 reversed를 구하기 위해 (String, String) -> Bool 타입인 클로저를 사용했다

......

....?

> 연산자도 (String, String) -> Bool 타입이네?

 

그렇다, 클로저에도 타입이 존재하기 때문에, 매개변수의 클로저와 같은 타입의 클로저/함수는 매개변수로 전달할 수 있다

-> 쉽게 생각하면 된다.

var a: Int = 0 , var b: Int = 10  // 여기서 a, b 모두 Int지 않은가?

func printInt(num: Int){

    print(num)

} // 이 함수에서 필요한 매개변수인 num과 타입이 같은 a, b는 매개변수의 인자가 될 수 있는 것과 같다

 

다시 연산자 함수로 돌아가서

let reversed: [String] = names.sorted(by: >)

이렇게 간단하게 연산자 함수를 통해 작성도 가능하다

 

값 획득

- 클로저에서 개인적으로 가장 어려웠던 개념이다

- 이 자체가 어렵다기 보다는 다른 개념들까지 생각하며 염두하게 되면 헷갈리는 개념이기도 하다

- 값 획득을 통해 정의한 상수/변수가 더이상 존재하지 않아도 내부에서 그 값을 참조/수정이 가능

  1. 값 획득을 통해 정의한 상수/변수

  2. 존재하지 않아도

  3. 내부에서 그 값을 참조/수정이 가능

3가지로 나누어서 보자

 

//makeIncrementer 함수는 하나의 Int 매개변수를 받아서
// ()->Int 타입의 클로저를 반환한다

//incrementer 함수는 매개변수로 아무것도 받지 않고
// Int 타입을 반환한다

func makeIncrementer(amount: Int) -> (()->Int){
    var runningTotal = 0
    func incrementer() -> Int{//중첩된 함수
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let incrementByTwo = makeIncrementer(amount: 2)

let first = incrementByTwo() // 2
let second = incrementByTwo() // 4
let third = incrementByTwo() // 6

 

무언가 이상하다

incrementByTwo 라는 상수에 ()->Int 타입의 클로저를  반환 한 것은 알겠다.

근데 함수안에 있는 변수들은 함수가 종료될 때, 메모리에서 해제된다고 알고 있는데, 왜 값이 계속 증가해?

 

천천히 살펴보자

우선 incrementByTwo = makeIncrementer(amount: 2) 는 ()->Int 타입의 클로저라는 것을 잊지말라

1. first에서 incrementByTwo()를 호출했다

2. incrementByTwo() 내에 있는 runningTotal 변수가 생성된다.

3. incrementer 라는 함수가 생성된다.

4. incrementer 라는 함수가 반환된다.

5. incrementer() 함수가 실행된다.

6. runningTotal 값이 0에서 2로 증가한다

7. 2가 된 runningTotal이 반환된다.

 

이 순서인데.... 아니 4번에 incrementer라는 함수가 반환된 시점에서 runningTotal 변수는 죽은거아냐?

makeIncrementer하는 함수가 끝났잖아?

 

-> 맞는 말이다, 하지만 값 획득을 통해 외부의 값을 사용하는 것이다.

-> 위에서 3가지 단계로 나눠놓은 것을 빗대어 설명하자면

-> 1. 값 획득을 통해 정의한 상수/변수 : amount, runningTotal

-> 2. 존재하지 않아도 : makeIncrementer 함수가 종료되어서 메모리에서 사라져도

-> 3. 내부에서 그 값을 참조/수정이 가능 : incrementer하는 함수 내부에서 해당 값을 참조/수정이 가능

 

이러한 과정을 통해 runningTotal, amount 값을 사용할 수 있다.

 

 

클로저는 참조타입

- 이전 포스팅들에서 말했듯, 값 타입과 참조 타입이 존재한다

- 클로저는 참조타입이다.

- 만약 클로저가 값타입이라고 생각해보면 위의 incrementByTwo는 동작하면 안된다

  -> incrementByTwo 는 let 즉, 상수로 선언되었다.

  -> 상수로 선언된 값 타입 ?? struct!! 상수로 선언된 구조체 인스턴스의 내부 값을 변경할 수 없지 않았던가

  -> 근데 incrementByTwo는 상수인데 내부 값이 증가되는 것을 보면 클로저는 참조 타입이라는 것을 역으로 보일 수 있는 것이다

 

 

탈출 클로저

- 함수의 전달인자로 클로저가 전달되고, 해당 함수가 종료되고 클로저가 호출될 때, 클로저가 함수를 탈출한다고 표현한다

- 예를 들어 비동기 작업을 실행하는 함수들은 클로저를 ComplementHandler 전달인자로 받아온다.

  -> 비동기 작업으로 함수가 종료되고 난 후, 호출할 필요가 있는 클로저를 상요해야할 때 탈출 클로저가 필요하다

 

하지만 지금까지 알아본 함수들은 모두 @escaping 키워드를 찾을 수 없었다

 -> 모두 비탈출 클로저 였기 때문이다

 

typealias VoidVoidClosure: () -> Void

func withNoescapeClosure(closure: VoidVoidClosure){
	closure()
}

func withEscapingClosure(completionHandler: @escaping 
						VoidVoidClosure) -> VoidVoidClosure{
	return completionHandler
}

class SomeClass{
	var x = 10
    
    func runNoescapeClosure(){
    // 비탈출 클로저에서 self 키워드 사용은 선택
    	withNoescapeClosure{ x = 200 }
    }
    
    func runEscapingClosure -> VoidVoidClosure{
    // 탈출 클로저에서 self 키워드 사용은 필수!!
    	return withEscapingClosure{ self.x = 100 }
    }
}

let instance: SomeCLass = SomeClass()
instance.runNoescapeClosure()
print(instance.x) // 200

let returnedClosure: VoidVoidClosure = instance.runEscapingClosure()
returnedClosure()
print(instance.x) // 100


 

 

 

+ Recent posts