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

Swift : 기초문법 [프로토콜#5 - 준수, 옵셔널 프로토콜 요구사항]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

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

 

프로토콜 조합

하나의 매개변수가 여러 프로토콜을 모두 준수하는 타입이어야만 한다면, 하나의 매개변수에 여러 프로토콜을 한 번에 조합하여 요구 가능하다. 

  • 엠 퍼센트(&)를 여러 프로토콜 이름 사이에 사용한다. SomeProtocol & AnotherProtocol
  • 하나의 매개변수가 프로토콜 둘 이상을 요구할 수도 있다.
  • 특정 클래스의 인스턴스 역할을 할 수 있는지 함께 확인 가능하다.
  • 구조체나 열거형 타입은 조합 불가
  • 조합 중 클래스 타입은 한 타입만 조합 가능
  • 프로토콜의 목록 외에도, 프로토콜 조합에는 필요한 슈퍼 클래스를 지정하는데 사용할 수 있는 클래스 타입이 포함될 수 있다.
protocol Named {
   var name: String { get }
}

protocol Aged {
   var age: Int { get }
}

struct Person: Named, Aged {
    var name: String
    var age: Int
}

class Car: Named {
   var name: String
   
   init(name: String) {
      self.name = name
   }
}

class Truck: Car, Aged {
   var age: Int
   
   init(name: String, age: Int) {
      self.age = age
      super.init(name: name)
   }
}

NamedAged 프로토콜이 읽기 전용 프로퍼티로 정의되어있고, Car 클래스는 Named만 채택 하고 TruckCarAged를 채택한 후에 이니셜 라이저로 초기화해줬다. 

 

Truck 클래스를 보면 CarnameAgedageget만 요구 했기 때문에 어떠한 종류의 프로퍼티로도 요구사항을 만족할 수 있다.

class Truck: Car, Aged { }

//클래스 밖에 있는 함수.
//Named와 Aged 프로토콜이 파라미터 안에 & 으로 조합 되어 있다.
//즉, Named 프로토콜과 Aged 프로토콜 둘 다 준수하는 타입 이여야 한다.
func celebrateBirthday(to celebrator: Named & Aged) {
   print("올해로 \(celebrator.age)세가 되신 \(celebrator.name)님 생신 축하드립니다!")
}

NamedAged 프로토콜이 파라미터 안에 & 으로 조합되어 있다. 즉, Named 프로토콜과 Aged 프로토콜 둘 다 준수하는 타입이어야 한다는 의미!

 

호출은 아래와 같이 가능한데, 오류가 나는 부분을 주의 깊게 보는 것을 추천한다!

let Seogun: Person = Person(name: "서근", age: 99)
celebrateBirthday(to: Seogun) //올해로 99세가 되신 서근님 생신 축하드립니다!

let myCar: Car = Car(name: "붕붕이")
//error! Aged 프로토콜(age)을 충족하지 않음
celebrateBirthday(to: myCar)

//error! 클래스 & 프로토콜 조합에서 클래스 타입은 한 타입만 조합 가능하다
var someVariable: Car & Aged & Truck

//Car클래스의 인스턴스 역할도 수행 가능하며,
//Aged 프로토콜을 준수하는 인스턴스만 할당 가능하다
var someVariable: Car & Aged
someVariable = Truck(name: "트럭", age: 5)

//error! myCar은 Aged프로토콜을 준수하지 않았으므로 오류
someVariable = myCar

전체 코드

<hide/>
protocol Named {
   var name: String { get }
}

protocol Aged {
   var age: Int { get }
}

struct Person: Named, Aged {
    var name: String
    var age: Int
}

class Car: Named {
   var name: String
   
   init(name: String) {
      self.name = name
   }
}

class Truck: Car, Aged {
   var age: Int
   
   init(name: String, age: Int) {
      self.age = age
      super.init(name: name)
   }
}


//클래스 밖에 있는 함수.
//Named와 Aged 프로토콜이 파라미터 안에 & 으로 조합 되어 있다.
//즉, Named 프로토콜과 Aged 프로토콜 둘 다 준수하는 타입 이여야 한다.
func celebrateBirthday(to celebrator: Named & Aged) {
   print("올해로 \(celebrator.age)세가 되신 \(celebrator.name)님 생신 축하드립니다!")
}

let Seogun: Person = Person(name: "서근", age: 99)
celebrateBirthday(to: Seogun) //올해로 99세가 되신 서근님 생신 축하드립니다!

let myCar: Car = Car(name: "붕붕이")
//celebrateBirthday(to: myCar) //error! Aged 프로토콜(age)을 충족하지 않음

//error! 클래스 & 프로토콜 조합에서 클래스 타입은 한 타입만 조합 가능하다
//var someVariable: Car & Aged & Truck

//Car클래스의 인스턴스 역할도 수행 가능하며,
//Aged 프로토콜을 준수하는 인스턴스만 할당 가능하다
var someVariable: Car & Aged
someVariable = Truck(name: "트럭", age: 5)

//error! myCar은 Aged프로토콜을 준수하지 않았으므로 오류
//someVariable = myCar

위 예제와 또 다른 예제를 한번 더 보자!!

protocol Named {
    var name: String { get }
}

class Address {
    var city: String
    var zipCode: Int
    init(city: String, zipCode: Int) {
        self.city = city
        self.zipCode = zipCode
    }
}

Named 프로콜과 Address 클래스를 생성했다. 이제 이 프로토콜과 클래스를 채택/상속한 클래스를 하나 더 만들어보면

class Delivery: Address, Named {
    var name: String
    init(name: String, city: String, zipCode: Int) {
        self.name = name
        super.init(city: city, zipCode: zipCode)
    }
}

Delivery 클래스는 Address 클래스를 상속받고, Named 프로토콜을 채택한 것을 확인할 수 있는데, Named 프로토콜이 요구한 name을 저장 프로퍼티로 구현해줬다.

 

그리고 class에 기본값이 없기 때문에 init으로 초기화를 해줬다!

//파리미터로 클래스인 Address와 Named 가 있다.
//이 의미는, Address의 하위 클래스 이머, Named 프로토콜을 준수하는 모든 타입!
func whereTo(to customer: Address & Named) {
    print("\(customer.name)님께 \(customer.city) 지역으로 배달을 시작합니다.")
}

이 코드를 보면, 위에서는 파라미터에 프로토콜 & 프로토콜을 넣어줬지만, 지금은 클래스 & 프로토콜 조합인 것을 확인할 수 있는데, 이 의미는 Address의 하위 클래스이며, Named 프로토콜을 준수하는 모든 타입을 의미한다.

 

이것이 위에서 말한 '프로토콜의 목록 외에도, 프로토콜 조합에는 필요한 슈퍼 클래스를 지정하는 데 사용할 수 있는 클래스 타입이 포함될 수 있다'를 의미한 코드이다.

 

주의해야 할 점은 Address를 상속받고 있지 않거나, Named 프로토콜을 준수하고 있지 않다면, 이 파라미터로 들어오지 못한다.

let seogun: Delivery = Delivery(name: "서근", city: "대전", zipCode: 33223)
whereTo(to: seogun) //서근님께 대전 지역으로 배달을 시작합니다.

let mijin: Delivery = Delivery(name: "미진", city: "서울")
whereTo(to: mijin) //error! Address 클래사의 zipCode 요구 조건을 충족하지 않아 오류

프로토콜 준수 확인 

  • 타입캐스팅에 사용했던 isas 연산자를 통해 대상이 프로토콜을 준수하는지 확인 가능
  • 확인 후 특정 프로토콜로 캐스팅 가능
  • is 연산자가 프로토콜을 준수하면 true 반환. 그렇지 않으면 false
  • 다운캐스팅 연산자 as? 는 프로토콜 타입의 옵셔널 값을 반환. 인스턴스가 해당 프로토콜을 준수하지 않으면 값은 nil
  • as! 는 강제로 프로토콜 타입을 설정, 다운캐스팅이 실패하면 런타임 오류!

위에서 사용한 코드를 가져와서 프로토콜 준수 여부를 아래와 같이 확인 가능하다.

protocol Named {
   var name: String { get }
}

protocol Aged {
   var age: Int { get }
}

struct Person: Named, Aged {
    var name: String
    var age: Int
}

class Car: Named {
   var name: String
   
   init(name: String) {
      self.name = name
   }
}

let Seogun: Person = Person(name: "서근", age: 99)
let myCar: Car = Car(name: "붕붕이")
print(Seogun is Named) //true
print(Seogun is Aged) //true

print(myCar is Named) //true
print(myCar is Aged) //false

if let castedInstance: Named = Seogun as? Named {
    print("\(castedInstance)는 Named 이다.")
} //Person(name: "서근", age: 99)는 Named 이다.

if let castedInstance: Aged = Seogun as? Aged {
    print("\(castedInstance)는 Aged 이다.")
} //Person(name: "서근", age: 99)는 Aged 이다.

if let castedInstance: Person = myCar as? Person {
    print("\(castedInstance)는 Person 이다.")
} else {
    print("Error!")
} //Error!

if let castedInstance: Aged = myCar as? Aged {
    print("\(castedInstance)는 Aged 이다.")
} //출력 없음. 캐스팅 실패

이렇게 보면 데이터 타입의 타입캐스팅과 똑같다는 것을 확인할 수 있다. 프로토콜도 하나의 타입이기 때문에 당연한 것!

 

또 다른 예제를 보자면

protocol Named {
    var name: String { get }
}

class Aclass: Named {
    var name: String
    init(name: String) {
        self.name = name
    }
}

class Bclass {
    var name: String
    init(name: String) {
        self.name = name
    }
}

Named라는 프로토콜이 있고, AclassNamed프로토콜을 채택. Bclass는 아무것도 채택하지 않았다.

 

이제 인스턴스를 생성하고 AnyObject 타입의 빈 배열을 생성해 그 안에 AClass의 인스턴스와 BClass 인스턴스를 append 시켜준다.

let firstInstance: Aclass = Aclass(name: "서근")
let secondInstance: Bclass = Bclass(name: "미진")

var array = [AnyObject]()
array.append(firstInstance)
array.append(secondInstance)

그런 다음 이 배열을 for 구문을 하고 as? 다운캐스팅해줄 수 있다. AclassNaemd 프로토콜을 준수했고, Bclass는 채택하지 않았기 때문에 결과는 아래와 같다.

var array = [AnyObject]()
array.append(firstInstance)
array.append(secondInstance)

for index in array {
    if let index = index as? Named {
        print(index.name) // "서근"
    } else {
        print("Naemd를 채택하지 않음") 
     //"Naemd를 채택하지 않음" Bclass의 "미진"은 Named를 채택하지 않았기 때문!
    }
}

옵셔널(선택적) 프로토콜 요구 사항

  • 프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정 가능하다.
  • 옵셔널 요구사항 앞에 optional 수정자가 접두어로 붙는다.
  • 옵셔널 요구사항을 정의하고 싶은 프로토콜은 반드시 @objc 속성으로 부여된 프로토콜이어야 한다.
  • @objc 프로토콜은 Objective-C 클래스 나 다른 @objc 클래스를 상속받은 클래스에서만 사용할 수 있다.
  • 구조체나 열거형에 의해 채택될 수 없다.
  • 옵셔널 요구사항에서 메서드나 프로퍼티를 사용하면 해당 타입이 자동으로 옵셔널이 된다.
  • 예를 들어, (Int) -> String 타입의 메서드가 ((Int) -> String)? 타입이 된다.
  • 요구사항이 프로토콜을 준수하는 타입으로 구현되지 않았을 가능성을 나타내기 위해 옵셔널체이닝을 사용하고, 호출 가능
  • 옵셔널 요구를 하면 프로토콜을 준수하는 타입에 해당 요구사항을 구현할 필요 없다.

TIP
 
 

Foundation 프레임워크
@objc 속성을 사용하려면 Foundation 프레임워크 모듈을 import 해야 한다.

import Foundation

@objc protocol Moveable {
    func walk()
    @objc optional func fly()
}

class Tiger: NSObject, Moveable {
    func walk() {
        print("호랑이가 걷는다.")
    }
}

class Bird: NSObject, Moveable {
    func walk() {
        print("새가 걷는다")
    }
    func fly() {
        print("새가 난다.")
    }
}

let tiger: Tiger = Tiger()
let brid: Bird = Bird()

tiger.walk()
brid.fly()

var movableInstance: Moveable = tiger
//옵셔널체이닐 fly?()를 통해 실제로 메서드가 구현되었는지 호출 시도
movableInstance.fly?() // 출력 없음. tiger에는 fly 메서드가 없기 때문

movableInstance = brid
movableInstance.fly?() // "새가 난다"

Moveable 프로토콜은 옵셔널 요구사항인 fly() 메서드가 있기 때문에 @objc 속성을 부여했고, @objc 속성을 사용하기 위해 TigerBird 클래스에 각각 Object-C의 클래스인 NSObject를 상속받았다. 

 

Tiger는 날 수 없기 때문에 fly() 메서드를 구현하지 않았다. (옵셔널 요구사항이기 때문에 정의하지 않아도 됨)

Bird는 날 수 있기 때문에 fly() 메서드를 구현했다.

 

그리고 각 클래스의 인스턴스를 구현하여 호출 가능하다.

 

또, var movableInstanceMoveable 프로토콜이 할당되어있는데, 인스턴스 타입에 실제로 fly() 메서드가 구현되어 있는지 확인하려면 옵셔널 체이닝을 통해 확인 가능하다.

 

옵셔널 체이닝을 사용하기 위해서는 메서드 이름 뒤에 물음표(?)를 붙여 표현한다. fly?()

 

 

읽어주셔서 감사합니다🤟

 

 

 


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


서근


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