[Swift] 프로토콜 [2]

임승섭·2023년 6월 26일
0

Swift

목록 보기
13/35

타입으로써의 프로토콜

swift는 protocol을 "일급객체(First Class Citizen)"로 취급한다

  • 타입으로 사용이 가능하다

"protocol은 타입이다"

  • 프로토콜을 변수에 할당할 수 있다
  • 함수를 호출할 때, 프로토콜을 파라미터로 전달할 수 있다
  • 함수에서 프로토콜을 반환할 수 있다
protocol Remote {
	func turnOn()
    func turnOff()
}

class TV: Remote {
	func turnOn() {
    	print("TV 켜기")
    }
    func turnOff() {
    	print("TV 끄기")
    }
}

struct SetTopBox: Remote {
	func turnOn() {
    	print("셋톱박스 켜기")
    }
    func turnOff() {
    	print("셋톱박스 끄기")
    }
    func doNetflix() {
    	print("넷플릭스 하기")
    }
}

let tv2 = TV()	// 얘는 TV 타입

let tv: Remote = TV()		// 프로토콜 타입으로 저장이 가능하다
tv.turnOn()					// 대신, 프로토콜에서 정의한 메서드만 실행할 수 있다.
tv.turnOff()


let sbox: Remote = SetTopBox()
sbox.turnOn()
sbox.turnOff()		// 얘네들만 사용할 수 있다
//sbox.doNetflix()		// 에러 (호출할 수가 없어) <- 프로토콜 타입으로 인식하고 있기 때문에

// 다운캐스팅을 통해 실행
(sbox as! SetTopBox).doNetflix()
  • 장점

    // 프로토콜의 타입 취급의 장점 - 1 ⭐️
    // 
    let electronic: [Remote] = [tv, sbox]      // 프로토콜의 형식으로 담겨있음
    	
    
    for item in electronic {   // 켜기, 끄기 기능만 사용하니 타입캐스팅을 쓸 필요도 없음 (다만, 프로토콜에 있는 멤버만 사용가능)
        item.turnOn()
    }
    	
    	
    	
    	
    // 프로토콜의 타입 취급의 장점 - 2 ⭐️
    func turnOnSomeElectronics(item: Remote) {
        item.turnOn()
    }
    	
    turnOnSomeElectronics(item: tv)
    turnOnSomeElectronics(item: sbox)
  • 프로토콜 준수성 검사

    • is 연산자 : 특정 타입이 프로토콜을 채택하고 있는지 확인
    • as 연산자 : 타입 캐스팅 (특정 인스턴스를 프로토콜로 변환하거나, 그 반대)
      • 프로토콜이 더 범용적인 타입 -> 구체적인 타입 : 다운캐스팅 (as? as!)
      • 구체적인 타입 -> 프로토콜 : 업캐스팅 (as)
    // 1) is연산자 ==============================
    // 양방향 가능
    // 특정타입이 프로토콜을 채택하고 있는지 확인
    tv is Remote
    sbox is Remote
    	
    	
    // 프로토콜 타입으로 저장된 인스턴스가 더 구체적인 타입인지 확인 가능
    electronic[0] is TV
    electronic[1] is SetTopBox
    	
    	
    	
    	
    // 2) as연산자 ==============================
    	
    // 업캐스팅(as)
    let newBox = sbox as Remote
    newBox.turnOn()
    newBox.turnOff()
    	
    	
    	
    // 다운캐스팅(as?/as!)
    let sbox2: SetTopBox? = electronic[1] as? SetTopBox
    sbox2?.doNetflix()
    	
    //(electronic[1] as? SetTopBox)?.doNetflix()

프로토콜의 상속

  • 다중 상속 가능 : 여러 요구사항의 나열일 뿐이다.
protocol Remote {
    func turnOn()
    func turnOff()
}


protocol AirConRemote {
    func Up()
    func Down()
}

protocol SuperRemoteProtocol: Remote, AirConRemote {   // 다중 상속 가능
    // func turnOn()
    // func turnOff()
    // func Up()
    // func Down()
    
    func doSomething()	// 추가적인 요구사항
}

// 그냥 다 구현해주면 된다.
class HomePot: SuperRemoteProtocol {
    func turnOn() { }
    
    func turnOff() { }
    
    func Up() { }
    
    func Down() { }
    
    func doSomething() { }
}

클래스 전용 프로토콜

// AnyObject라는 프로토콜을 상속
protocol SomeProtocol: AnyObject {      // AnyObject는 "클래스 전용 프로토콜"
    func doSomething()
}

// 구조체에서는 채택할 수 없게 됨 ⭐️
//struct AStruct: SomeProtocol {
//
//}


// 클래스에서만 채택 가능
class AClass: SomeProtocol {
    func doSomething() {
        print("Do something")
    }
}

프로토콜 합성

protocol Named {
	var name: String { get}
}

protocol Aged {
	var age: Int { get }
}

// 하나의 구조체에서 여러 프로토콜 채택 가능
struct Person: Named, Aged {
	// 저장 속성으로 구현해줬다
	var name: String
    var age: Int
}


// 프로토콜의 합성
// 프로토콜 두 개를 병합해서 타입으로 사용
// "두 가지 프로토콜을 모두 채택한 타입"
func wishHappyBirthday(to celebrator: Named & Aged) {	// 임시적인 타입(두 가지 프로토콜을 채택한 타입)으로 인식
	print("생일축하해, \(celebrator.name), 넌 이제 \(celebrator.age)살이 되었구나!")
}
// 별건 없고, 그냥 이렇게도 타입으로 쓸 수 있구나~ 하고 이해하면 된다

let birthdayPerson = Person(name: "홍길동", age: 20)
wishHappyBirthday(to: birthdayPerson)

// 하나의 타입으로 사용이 가능하다
let whoIsThis: Named & Aged = birthdayPerson

프로토콜의 선택적 요구사항 구현

  • 채택한 곳에서 특정 요구사항을 구현해도 되고, 안해도 되는 요구사항
    (선택적(Optional))
  • 어트리뷰트 키워드
    • @available, @objc, @escaping, ...
      1. 선언에 대한 추가정보 제공하는 어트리뷰트
        @available(iOS 10.0) 
        class MyClass {
      2. 타입에 대한 추가정보 제공하는 어트리뷰트
        func doSomething(completion: @escaping() -> () ) {
        }
  • 선택적인 멤버 (구현해도 되고~ 안해도 되고~) 선언
    • @objc : object C 코드에서도 사용할 수 있게 해주는 어트리뷰트
      • 프로토콜 앞에는 "@objc", 멤버 앞에는 "@objc obtional" 추가
// @objc 추가
@objc protocol Remote {
	// @objc optional 추가 -> 채택하는 곳에서 구현해도 되고 안해도 된다.
    // -> 선택적 멤버로 선언한다는 뜻
    @objc optional var isOn: Bool { get set }
    func turnOn()
    func turnOff()
    @objc optional func doNeflix()
}


class TV: Remote {
    var isOn = false	// 지워도 상관 없다. (optional)
    
    func turnOn() {}
    
    func turnOff() {}
}

let tv1: TV = TV()
print(tv1.isOn)		// 이거 하려면 isOn 구현해줘야 하겠지

/*@objc는 클래스 전용 프로토콜이기 때문에, 구조체나 열거형에서는 아예 채택이 불가능*/

프로토콜의 확장

  • 기본(디폴트) 구현을 제공한다
  • 코드의 중복구현을 피하기 위함이다
  • 프로토콜 테이블 (witness table), 클래스 테이블 (virtual table)
    • 클래스와 관련된 메서드 테이블 : virtual table
    • 프로토콜의 반드시 요구되는 메서드를 저장하는 테이블.
      즉, 프로토콜 본체에서 선언한 메서드 : witness table
      • 채택 시 사용자 구현 가능 (커스텀) 우선순위 1
      • 채택 시 사용자 구현 안하면 기본 구현 사용 우선순위 2
protocol Remote {
    func turnOn()
    func turnOff()
}

// 채택 ===> 실제구현해야함(여러타입에서 채택한다면 반복적으로 구현해야하는 점이 불편)
// 동일한 구현 내용을 코드를 반복해서 쓰고 있다...
class TV1: Remote {
    //func turnOn() { print("리모콘 켜기") }
    //func turnOff() { print("리모콘 끄기") }
    
    // 만약 구현을 한다면, 이걸로 작동
    func turnOn() { print("티비 켠다아") }
    
    // 요구사항 메서드가 아닌걸 여기서 구현한다면, 타입에 따른 선택 (우선순위x)
    func doAnotherAction() {
    	print("TV의 또 다른 동작")
    }
}

struct Aircon1: Remote {
    //func turnOn() { print("리모콘 켜기") }
    //func turnOff() { print("리모콘 끄기") }
}

// 동일한 구현 내용 (리모콘 켜기, 리모콘 끄기) -> 불편하다
// 아래처럼 확장하면, 위에 다 주석때려도 정상적으로 함수 실행 가능하다

// 프로토콜의 확장 -> 디폴트 구현을 제공한다
extension Remote {                         // (요구사항의 메서드 우선순위 적용 - 프로토콜 메서드 테이블 만듦)
    func turnOn() { print("리모콘 켜기") }    // 1. (채택)구현시 해당 메서드 2. 기본 메서드
    func turnOff() { print("리모콘 끄기") }   
    
    func doAnotherAction() {               // (요구사항 메서드 X - 테이블 만들지 않음)
        print("리모콘 또 다른 동작")            // 타입에 따른 선택 (Direct Dispatch)
    }
}

// 프로토콜 테이블 : 한번 만들면 불변



// 우선순위 (처음부터 있던 메서드(반드시 요구)) : 실제로 구현한거 > 확장에서 (기본으로) 구현한거 
// 우선순위x (확장에서 새로 만든 메서드(반드시 요구x))
	// 해당 변수의 타입에 따라 달라짐
    // 프로토콜 타입 -> 확장에서 만든 함수
    // 클래스 타입 -> 클래스에서 만든 함수



// 반드시 요구하는 요구사항 메서드는 "우선순위" 고려해서 작동된다!! - Witness Table

// 반드시 요구하지 않는 요구사항 메서드는 타입에 따라 선택된다 - Direct Dispatch
var tv2: Remote = TV1()	// 프로토콜로 저장하면 프로토콜로 작동
tv2.doAnotherAction()
var tv3: TV1 = TV1()	// 원래 타입으로 저장하면 원래 타입에서 정의한 걸로 작동
var tv4 = TV1()			// 원래 타입으로 저장하면 원래 타입에서 정의한 걸로 작동
tv3.doAnotherAction()
tv4.doAnotherAction()

예제 - 클래스

// 위에서 extension한 프로토콜 Remote 이용한다

class Ipad: Remote {
	func turnOn() { print("아이패드 켜기") }
    
    func doAnotherAction() { print("아이패드 다른 동작") }
}

/*
=> 클래스 테이블 (Virtual table)
func turnOn() { print("아이패드 켜기") }
func turnOff() { print("리모콘 끄기") }
func doAnotherAction() { print("아이패드 다른 동작") }
*/

/*
=> 프로토콜 테이블 (Witness table)
 func turnOn()  { print("아이패드 켜기") }
 func turnOff() { print("리모콘 끄기") }
*/

// 서로 다른 타입으로 변수 생성해보자
let ipad: Ipad = Ipad()
let ipad2: Remote = Ipad()
ipad.turnOn()           // 클래스 - V테이블
ipad.turnOff()          // 클래스 - V테이블
ipad.doAnotherAction()  // 클래스 - V테이블
ipad2.turnOn()           // 프로토콜 - W테이블
ipad2.turnOff()          // 프로토콜 - W테이블
ipad2.doAnotherAction()  // 프로토콜 - Direct (직접 메서드 주소 삽입)

예제 - 구조체

struct SmartPhone: Remote {
	func turnOn() { print("스마트폰 켜기") }
    func doAnotherAction() { print("스마트폰 다른 동작") }
}
// => 구조체는 따로 메서드 테이블이 없다. 원래부터 direct로 주소 삽입
/*
=> 프로토콜 테이블 (Witness table)
 func turnOn()  { print("스마트폰 켜기") }
 func turnOff() { print("리모콘 끄기") }
*/

// 서로 다른 타입으로 변수 생성
var iphone: SmartPhone = SmartPhone()
iphone.turnOn()             // 구조체 - Direct (직접 메서드 주소 삽입)
iphone.turnOff()            // 구조체 - Direct (직접 메서드 주소 삽입)
iphone.doAnotherAction()    // 구조체 - Direct (직접 메서드 주소 삽입)

var iphone2: Remote = SmartPhone()
iphone2.turnOn()            // 프로토콜 - W테이블
iphone2.turnOff()           // 프로토콜 - W테이블
iphone2.doAnotherAction()   // 프로토콜 - Direct (직접 메서드 주소 삽입)

프로토콜 지향 프로그래밍

  • swift : 객체 지향, 프로토콜 지향, 함수형 언어
  • 프로토콜 지향 : 기존의 언어에서는 없던 개념. swift에서 처음으로 제시
  • 객체지향 프로그래밍
    • 장점 : "상속"
    • 단점
      • 하나의 클래스만 상속 가능
      • 상위클래스의 메모리 구조 따라감 (필요하지 않은 속성/메서드도 따라감)
      • 클래스(reference type)에서만 사용 가능
  • 프로토콜 지향 프로그래밍
    • 여러 개의 프로토콜 채택 가능 (다중 상속 느낌)
    • 메모리 구조에 대한 특정 요구사항이 없다. (선택적도 가능 @optional)
    • 모든 타입에서 채택 가능 (value type도 가능)
    • 확장에서 구체적 정의 가능 (재정의 가능)
    • 타입으로 사용 가능 -> 활용성 good
    • 여러가지 채택 -> 조합의 장점 -> 구성/재사용성 good

프로토콜 확장의 적용 제한

  • 확장 - 형식 제한 가능
  • 특정 프로토콜을 채택한 타입에만 프로토콜 확장이 적용 (where)
  • 특정 프로토콜을 채택하지 않으면, 프로토콜 확장이 적용되지 않기 때문에,
    확장이 없는 것과 동일하게 메서드를 직접 구현해야 한다
protocol Remote {
	func turnOn()
    func turnOff()
}

extension Remote {
	func turnOn() { print("리모콘 켜기") }
    func turnOff() { print("리모콘 끄기") }
}

protocol Bluetooth {
	func blueOn()
    func blueOff()
}

// 대문자 Self : 해당 타입을 의미한다 (타입 자신을 의미) (Bluetooth를 채택한 SmartPhone)
// 소문자 self : 타입 내에서 인스턴스 자신을 의미

// Remote 프로토콜을 채택 하였는지 여부. (채택한 경우에만, Bluetooth 프로토콜 확장이 적용된다.)
extension Blutooth where Self: Remote {	
	func blueOn() { print("블루투스 켜기") }
    func blueOff() { print("블루투스 끄기") }
}

// 이렇게 하면, 기본 제공 다 먹을 수 있다
class SmartPhone: Remote, Bluetooth {
}

// 이거만 하려면, 직접적으로 blueOn, blueOff를 구현해주어야 한다!!
// 즉, Bluetooth extension이 적용되지 않는 것
class Ipad: Bluetooth {
	func blueOn() {
    	print("hih")
    }
    func blueOff() {
    	print("ihi")
    }
}

0개의 댓글