궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 기초문법 [프로토콜#3 - 타입으로서의 프로토콜, Delegate]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님과Zedd0202님의 게시글을 참고하여 작성되었습니다.

 

타입으로서의 프로토콜

  • 프로토콜은 다른 타입이 허용되는 여러 곳에서 다음과 같은 프로토콜을 사용할 수 있다.
  • 함수, 메서드 또는 이니셜라이저에서의 매개변수 타입 또는 리턴 타입
  • 상수, 변수 또는 프로퍼티로서의 타입
  • 배열 또는 딕셔너리 또는 다른 컨테이너의 항목으로서의 타입

TIP
 
 

타입으로서의 프로토콜
프로토콜은 요구만 하고 스스로 기능을 구현하지 않는다. 하지만 프로토콜은 코드에서 완전한 하나의 타입으로 사용되기 때문에 여러 위치에서 프로토콜을 타입으로 사용할 수 있다.

프로토콜은 이름을 정할 때 대문자 카멜 케이스를 사용하기 때문에 SomeProtocol, AnotherProtocol처럼 첫 글자를 반드시 대문자로 표현해야 한다.

protocol RandemNumberGenrator {
    func random() -> Double
}

RandemNumberGenrator 프로토콜에서 메서드를 요구했고, 이 프로토콜을 채택하는 타입은 반드시 random() 메서드를 구현해야 한다.

 

이제 타입으로서의 프로토콜을 사용해보자!

protocol RandomNumberGenerator {
    func random() -> Double
}

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0

    func random() -> Double {
        lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}

class Dice {
    let sides: Int
    //클래스에서 RandomNumberGenrator을 채택하지 않고
    //genrator 상수 저장 프로퍼티에 타입으로 채택 해줬다.
    let generator: RandomNumberGenerator
    
    //Dice클래스의 저장 프로퍼티에 기본값이 없기때문에 init으로 초기화
    //generator은 RandomNumberGenrator타입 이기 때문에 random()메서드 구현의무 없음(접근은 가능)
    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }

    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())

for _ in 1...5 {
    print("Random dice roll is \(d6.roll())")
}

위 코드를 보면 Dice 자식클래스에 RandemNumberGenrator프로토콜을 채택하지 않고 상수 저장 프로퍼티에 RandemNumberGenrator 타입으로 준 것을 확인할 수 있다. 그래서 generatorRandemNumberGenrator타입이 된다. 

 

또, 클래스의 저장프로퍼티에 기본값이 없기 때문에 init으로 초기화를 해주고, generatorRandemNumberGenrator타입이기 때문에 random()이라는 메서드 요구를 구현할 필요가 없다. random() 메서드에 접근은 가능하다.

Delegate 패턴

"Delegate는 클래스나 구조체가 책임을 일부 다른 타입의 인스턴스로 전달 또는 위임 할 수 있게 하는 Design Pattern이다."

 

Delegate 패턴은 iOS 개발에서 자주 사용되는 디자인 패턴이다.

이것을 사용하지 않고 iOS 앱을 개발하는 것은 거의 불가능에 가깝다고 한다.

 

Delegate란 단어는 '위임하다'라는 사전적 의미 가지고 있듯이, '위임자'라고 생각하면 된다. Delegate 패턴은 delegate 즉, 위임자를 갖고 있는 객체가 다른 객체에게 자신의 일을 위임하는 형태의 디자인 패턴이다.

 

위에 설명이 이해가 안 가는데.. 아래에서 쉽게 풀어보자면!

 

일단 이 Delegate 패턴을 실제로 작동하게 하는 기술이 바로 Protocol 이다.

 

대표적인 예시로 UITableView ClassUITableViewDelegate 프로토콜과 UITableViewDataSource라는 프로토콜을 통해서 Table을 관리, 상호작용하고 셀을 보여준다. 

 

또, UITextViewUITextViewDelegate 프로토콜을 사용하여 TextView 내부의 상태변화(작성 시작, 중지 등..)를 보고받는다. TextView는 사용자의 입력에 따라서 응답하고 이것에 따라 Delegate 기능을 호출하게 된다.

 

예를 들어 아래 예시는 TextView가 포함되어 있고 해당 TextViewdelegation을 사용한다.

class SomeViewController: UIViewController, UITextViewDelegate {
   var textView: UITextView
   
   func viewDidLoad() {
       textView.delegate = self  
       // TextViewDelegate는 SomeViewController의 인스턴스
   }
}

UIViewController를 상속받는 SomeViewController를 생성했고, 동시에 UITextViewDelegate 라는 프로토콜도 채택했다. class내부에는 UITextView의 인스턴스인 textView를 선언했고 viewDeleLoad 매소드에 textView.delegateself로 지정했다.

 

이렇게 UITextViewDelegate를 보면, textView에서 발생하는 모든 이벤트에 응답하기 위해 다양한 함수를 구현할 수 있는 것이다.

  • textViewDidBeginEditing(_:)
  • textViewDidEndEditing(_:)
  • textView(_:shouldChangeTextIn: replacementText:)
  • textViewDidChange(_:)

 

textViewDidChange 기능을 통해 textView에 있는 문자 수를 업데이트하는 등의 기능을 사용할 수 있는 것이다. 

 

또 다른 예를 보자면,

만약 UITextField를 사용하여 iOS 앱을 개발할 때 UITextField의 모든 메서드나 프로퍼티를 처음부터 만들어 나간다.

 

텍스트 필드의 모양, 모서리, cornerRadius 등..

 

그리고 WeatherViewControllerTextFieldDidBeginEditing() 메서드를 생성한다고 가정해보자. WeatherViewController가 편집을 감지할 때 이를 알리기 위해선 텍스트 필드가 필요한데, 이것을 작동하려면 UITextField에 대한 클래스 정의를 UITextField에 만들고 거기에서 textFieldDidBeginEditing 메서드를 호출하면 된다.

 

이렇게!

하지만 또 다른 AnotherClass가 있고 그곳에 textFieldDidBeginEditing메서드를 사용하려고 할 때 또다시 메서드를 클래스 안에 생성해야 할까? 이럴 때 필요한 것이 바로 Delegate Design Pattern!

 

솔루션은 이러하다.

  • UITextFieldDelegate 프로토콜을 생성하고 textFieldDidBeginEditing 메서드 생성.
  • UITextFielddelegate 변수를 생성하고 반드시 UITextFieldDelegate를 데이터 타입으로 채택.
  • WeatherViewControllerUITextField를 새롭게 초기화하여 생성하고. TextField.delegateself로 지정해준다.
  • 마지막으로 textFieldDidBeginEditing 메서드를 호출.

이렇게 하면 또 다른 Class가 있어도 UITextField를 수정하지 않고 그대로 사용 가능하다는 장점이 있다.

이처럼 Delegate를 통해서 업무를 위임하는 것이 아니라 기능을 확장하기 위해서 사용하는 것이 바로 Delegate Design Pattern 이다.

 

클래스 만들어보기

UITextField를 사용할 때, 프로토콜과 Delegate에 대해 더 깊게 알아보도록 하자! (엄청 복잡하기 때문에.. 정신줄 단단히!!!)

 

Udemy에서 나온 Delegate 패턴에 대해 인용해서 가져와보자면

1. EmergenctCallHandler 라는 콜센터가 있고, 호출기(Delegate)가 있다.

 

2. 이 Delegate를 소지할 수 있는 사람은 CPR을 할 줄 알아야 하고, 이것은 AdvancedLifeSupport라는 인증서(Protocol)이 있어야 한다. 즉, 이 프로토콜을 소지하고 있는 사람은 Delegate를 가질 수 있는 조건이 있으며 그게 어떠한 사람이던(Class) 상관없다.

 

코드로 알아보자면 아래와 같다.

//MARK: 1
protocol AdvancedLifeSupport {
    func performCPR() //COR을 수행할줄 알아야함.
}

//MARK: 2
class EmergencyCallHandler {
    //delegate로 AdvancedLifeSupport가 채택되었기 때문에 반드시 CPR을 할 줄 알아야 함.
    //또, 반듯이 이 delegate는 AdvancedLifeSupport 데이터 타입을 채택해야 함.
    var delegate: AdvancedLifeSupport?
    
    func assessSituation(Director: String) {
        print("무슨일이 일어나고있는지 \(Director)에게 전달해주세요.")
    }
    
    //이 메서드는 대리인이 누구인지 상관없다.
    //delegate를 가지고있다면 CPR을 진행하라 에만 관심이 있음.
    func medicalEmergency() {
        delegate?.performCPR()
    }
}

//MARK: 3
//대리인 지정
struct Paramedic: AdvancedLifeSupport {
    
    //EmergencyCallHandler가 초기화 될때
    init(handler: EmergencyCallHandler) {
        handler.delegate = self
    }
    
    //어떻게 CPR을 할지 메서드 작성
    func performCPR() {
        print("위생병이 가슴압박 30회, 인공호흡 2회 시행, 가슴압박 및 인공호흡 반복한다.")
    }
}

//대리인
class Doctor: AdvancedLifeSupport {
    
    init(handler: EmergencyCallHandler) {
        handler.delegate = self
    }
    
    func performCPR() {
        print("의사가 가슴압박 30회, 인공호흡 2회 시행, 가슴압박 및 인공호흡 반복한다.")
    }
    
    func useStethoscope() {
        print("청진기를 사용하여 심장소리를 듣는다")
    }
}

//대리인
//Doctor 클래스를 참조한다.
class Surgeon: Doctor {
    
    //슈퍼클래스인 Doctor가 하는 CPR을 동일하게 수행가능
    override func performCPR() {
        super.performCPR()
        print("외과의사가 의사가하는 일을 옆에서 서포트 한다.")
    }
    
    func useElectricDrill() {
        print("외과의사가 피를 석션 한다.")
    }
}

//MARK: 4
let Seogun = EmergencyCallHandler()
let Mijin = Surgeon(handler: Seogun)

Seogun.assessSituation(Director: "서근")
Seogun.medicalEmergency()

//무슨일이 일어나고있는지 서근에게 전달해주세요.
//의사가 가슴압박 30회, 인공호흡 2회 시행, 가슴압박 및 인공호흡 반복한다.
//외과의사가 의사가하는 일을 옆에서 서포트 한다.

Delegate를 채택해서 어떤 일을 할지 결정하고, 위임자를 설정해서 누가 누구의 일을 대신 맡을지 정해주고, 특정 상황에서 무슨 일을 대신해주면 되는지 작성해주면 Delegate Design Pattern 을 사용할 수 있다.

Delegation을 사용하는 이유

1. Delegateprotocol을 준수하는 것만으로도 구현이 가능하므로 매우 유연하다.

2. 완전한 Class 또는 Struct를 상속할 필요가 없기 때문에 Delegate를 더 가볍게 사용할 수 있다.

3. Class간 요구 사항을 전달해주는 protocol만 있으면 연결이 쉽다.

4. Delegate, Delegate Design Pattern은 한 Class와 또 다른 Class의 상호작용을 간단히 할 수 있도록 돕는다.

 

 

읽어주셔서 감사합니다 🤟

 

 

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.