Jupyo's Daily Story
프로토콜 (Protocols) 본문
프로토콜 (Protocols) | Swift
준수하는 타입이 구현해야 하는 요구사항을 정의합니다. 프로토콜 (protocol) 은 메서드, 프로퍼티, 그리고 특정 작업이나 기능의 부분이 적합한 다른 요구사항의 청사진을 정의합니다. 프로토콜
bbiguduk.gitbook.io
프로토콜(protocol)은 특정 기능 수행에 필요한 메서드와 프로퍼티의 청사진입니다. 클래스, 구조체, 열거형은 이 프로토콜을 채택하여 요구사항을 구현할 수 있고, 이를 프로토콜 준수라고 합니다. 또한 프로토콜 확장을 통해 기본 구현을 제공하거나 추가 기능을 정의할 수 있습니다.
프로토콜 구문 (Protocol Syntax)
클래스, 구조체, 그리고 열거형과 유사한 방법으로 프로토콜을 정의합니다.
protocol SomeProtocol {
// protocol definition goes here
}
프로토콜 채택
콜론(:)으로 구분된 타입의 이름 뒤에 특정 프로토콜을 채택합니다. 여러 프로토콜은 콤마(,)로 구문되고 리스트화 할 수 있습니다.
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
클래스가 상위 클래스를 가진 경우에 콤마로 구분하여 모든 프로토콜보다 가장 상위에 위치 시킵니다.
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
프로토콜은 타입이기 때문에 Swift 에서 다른 타입 (Int, String, 그리고 Double 등) 의 이름처럼 대문자로 시작합니다.
프로퍼티 요구사항 (Property Requirements)
- 프로토콜은 프로퍼티의 이름과 타입을 지정할 수 있으며, 모든 프로퍼티는 var로 선언해야 합니다.
- 프로퍼티 접근 수준 지정
- { get } : 읽기만 가능한 프로퍼티 (저장 프로퍼티, 계산 프로퍼티 모두 가능)
- { get set } : 읽기/쓰기 모두 가능한 프로퍼티 (상수 프로퍼티는 불가)
- 프로토콜은 프로퍼티가 저장 프로포티인지 계산 프로퍼티인지는 지정하지 않고, 접근 방식만 정의합니다.
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
프로토콜에서 타입 프로퍼티를 선언할 때는 항상 static 키워드를 사용합니다. 클래스가 이를 구현할 때는 class나 static 키워드를 선택하여 사용할 수 있습니다.
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
다음은 단일 인스턴스 프로퍼티 요구사항을 가지는 프로토콜의 예입니다.
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed 프로토콜은 준수하는 타입이 String 타입의 읽기 전용 프로퍼티인 fullName을 반드시 구현하도록 요구합니다.
다음은 FullyNamed 프로토콜을 채택하고 준수하는 구조체에 대한 예입니다.
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"
다음은 FullyNamed 프로토콜을 채택하고 준수하는 더 복잡한 클래스입니다.
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
return (prefix != nil ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"
Starship 클래스는 FullyNamed 프로토콜을 준수하며, fullName을 계산 프로퍼티로 구현합니다. 이 프로퍼티는 필수값 name과 옵셔널 prefix를 조합하여 전체 이름을 반환합니다.
메서드 요구사항 (Method Requirements)
- 프로토콜에서 메서드 선언
- 메서드의 이름과 파라미터만 정의(구현부 없음)
- 가변 파라미터 사용 가능
- 파라미터의 기본값 지정 불가
- 타입 메서드
- 프로토콜에서는 항상 static 키워드 사용
- 클래스 구현 시 class 또는 static 사용 가능
protocol SomeProtocol {
static func someTypeMethod()
}
아래의 예제는 단일 인스턴스 메서드 요구사항을 가지는 프로토콜을 정의합니다.
protocol RandomNumberGenerator {
func random() -> Double
}
RandomNumberGenerator 프로토콜은 0.0부터 1.0 미만의 Double 값을 반환하는 random() 메서드를 요구합니다. 구체적인 난수 생성 방식은 프로토콜에서 정의하지 않고, 준수하는 타입에서 자유롭게 구현할 수 있습니다.
다음은 RandomNumberGenerator 프로토콜을 채택하고 준수하는 클래스의 구현입니다.
이 클래스는 선형 합동 생성기(Linear congruential generator)로 알려진 의사 난수(Pseudorandom number) 생성기 알고리즘을 구현합니다.
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
}
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"
변경 메서드 요구하상 (Metating Method Requirements)
값 타입(구조체, 열거형)에서 인스턴스를 수정하는 메서드를 구현할 때 mutating 키워드가 필요합니다. 프로토콜에서 인스턴스를 수정하는 메서드를 정의할 때도 mutating 키워드를 사용해야 하며, 이를 통해 값 타입이 해당 프로토콜을 채택할 수 있습니다.
프로토콜에서 인스턴스 메서드 요구사항에 mutating을 표시하더라도, 클래스에서 이 메서드를 구현할 때는 mutating 키워드를 작성할 필요가 없습니다. mutating 키워드는 구조체와 열거형에서만 사용됩니다.
예제의 Togglable 프로토콜은 toggle() 이라는 하나의 인스턴스 메서드를 요구하며, 이 메서드는 프로토콜을 준수하는 타입의 상태를 변경(반전)하는 역할을 합니다. 프로토콜 정의에서 toggle() 메서드에 mutating 키워드를 사용하여, 이 메서드가 호출될 때 인스턴스의 상태를 변경할 것임을 나타냅니다.
protocol Togglable {
mutating func toggle()
}
아래 예제는 OnoffSwitch 라는 열거형을 정의합니다. 이 열거형은 on 과 off 두 가지 상태를 토글하며, Togglable 프로토콜의 요구사항에 맞춰 toggle 구현에 mutating 키워드를 표시합니다.
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off:
self = .on
case .on:
self = .off
}
}
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on
초기화 구문 요구사항 (Initializer Requirements)
프로토콜은 초기화 구문을 요구사항으로 지정할 수 있으며, 구현부 없이 선언만 합니다.
protocol SomeProtocol {
init(someParameter: Int)
}
프로토콜 초기화 구문 요구사항의 클래스 구현 (Class Implementations of Protocol Initializer Requirements)
프로토콜을 채택한 클래스에 초기화 구문 요구사항을 구현할 수 있습니다. 이 모든 케이스에 대해 required 수식어와 구현부를 작성해야 합니다.
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
required 수식어를 사용하면 준수하는 클래스의 모든 하위 클래스에 초기화 구문 요구사항의 명시적 또는 상속된 구현을 제공하여 프로토콜을 준수할 수 있습니다.
final 클래스는 하위 클래스가 될 수 없으므로 final 수석어로 표시된 클래스에 required 수식어를 프로토콜 초기화 구문 구현에 표시할 수 없습니다.
하위 클래스가 상위 클래스의 초기화 구문을 재정의하려면 required와 override 수식어 모두 표시합니다.
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// initializer implementation goes here
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// "required" from SomeProtocol conformance; "override" from SomeSuperClass
required override init() {
// initializer implementation goes here
}
}
타입으로 프로토콜 (Protocols as Types)
제네릭 제약조건으로 사용
- 프로토콜을 준수하는 모든 타입과 작동 가능
- 코드 호출 시점에 구체적인 타입 선택
func process<T: Protocol>(item: T)
불투명 타입(OpaqueType)으로 사용
- 컴파일 시점에 구체적인 타입이 정해짐
- API 구현부에서 타입을 선택하고 외부에는 숨김
- 추상화 계층 유지에 도움
func getData() -> some Protocol
박스형 프로토콜 타입(Boxed Protocol Type)으로 사용
- 런타임에 타입이 결정됨
- 간접 계층(box)이 추가되어 성능 비용 발생
- 프로토콜에 정의된 멤버만 접근 가능
- 다른 기능 사용 시 런타임 캐스팅 필요
var item: Protocol
위임 (Delegation)
위임은 특정 타입의 일부 책임을 다른 타입의 인스턴스에게 위임하는 디자인 패턴입니다.
이 패턴은 프로토콜을 통해 구현되며, 다음과 같은 특징이 있습니다.
- 구현 방식
- 위임할 책임을 프로토콜로 정의
- 위임받는 타입(delegate)이 해당 프로토콜을 준수
- 사용 목적
- 특정 동작에 대한 응답 처리
- 외부 소스의 실제 타입을 알 필요 없이 데이터 검색
아래 예제는 주사위 게임과 게임의 진행사항을 관찰하는 위임에 대한 중첩된 프로토콜을 정의합니다.
// DiceGame 클래스: 주사위 게임 구현
class DiceGame {
let sides: Int // 주사위 면의 수
let generator = LinearCongruentialGenerator() // 난수 생성기
weak var delegate: Delegate? // 순환 참조 방지를 위한 약한 참조 delegate
init(sides: Int) {
self.sides = sides
}
// 주사위 굴리기 구현: 1부터 sides까지의 난수 반환
func roll() -> Int {
return Int(generator.random() * Double(sides)) + 1
}
// 게임 실행 메서드
func play(rounds: Int) {
delegate?.gameDidStart(self) // 게임 시작 delegate 호출
// 라운드별 게임 진행
for round in 1...rounds {
let player1 = roll() // 플레이어1 주사위 굴리기
let player2 = roll() // 플레이어2 주사위 굴리기
// 승자 판정 및 delegate 메서드 호출
if player1 == player2 {
delegate?.game(self, didEndRound: round, winner: nil) // 무승부
} else if player1 > player2 {
delegate?.game(self, didEndRound: round, winner: 1) // 플레이어1 승리
} else {
delegate?.game(self, didEndRound: round, winner: 2) // 플레이어2 승리
}
}
delegate?.gameDidEnd(self) // 게임 종료 delegate 호출
}
// DiceGame 클래스 내부에 중첩된 Delegate 프로토콜
// AnyObject 상속으로 클래스 전용 프로토콜 지정
protocol Delegate: AnyObject {
func gameDidStart(_ game: DiceGame) // 게임 시작 시 호출
func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) // 라운드 종료 시 호출
func gameDidEnd(_ game: DiceGame) // 게임 종료 시 호출
}
}
다음 예제는 DiceGame.Delegate 프로토콜을 채택하는 DiceGameTracker 라는 클래스입니다.
// DiceGameTracker 클래스: DiceGame의 Delegate 프로토콜을 준수하여 게임 진행 상황을 추적
class DiceGameTracker: DiceGame.Delegate {
var playerScore1 = 0 // 플레이어1의 승리 횟수
var playerScore2 = 0 // 플레이어2의 승리 횟수
// 게임 시작 시 호출되는 delegate 메서드
func gameDidStart(_ game: DiceGame) {
print("Started a new game")
playerScore1 = 0 // 플레이어1 점수 초기화
playerScore2 = 0 // 플레이어2 점수 초기화
}
// 각 라운드가 끝날 때 호출되는 delegate 메서드
func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) {
switch winner {
case 1: // 플레이어1 승리
playerScore1 += 1
print("Player 1 won round \(round)")
case 2: // 플레이어2 승리
playerScore2 += 1
print("Player 2 won round \(round)")
default: // 무승부
print("The round was a draw")
}
}
// 게임이 종료될 때 호출되는 delegate 메서드
func gameDidEnd(_ game: DiceGame) {
if playerScore1 == playerScore2 { // 최종 점수가 같을 경우
print("The game ended in a draw.")
} else if playerScore1 > playerScore2 { // 플레이어1의 승리 횟수가 더 많은 경우
print("Player 1 won!")
} else { // 플레이어2의 승리 횟수가 더 많은 경우
print("Player 2 won!")
}
}
}
DiceGame 과 DiceGameTracker 은 다음과 같이 동작합니다.
let tracker = DiceGameTracker()
let game = DiceGame(sides: 6)
game.delegate = tracker
game.play(rounds: 3)
// Started a new game
// Player 2 won round 1
// Player 2 won round 2
// Player 1 won round 3
// Player 2 won!
확장으로 프로토콜 준수성 추가 (Adding Protocol Conformance with an Extension)
확장은 기존 타입에 새로운 프로퍼티, 메서드, 그리고 서브 스크립트를 추가할 수 있으므로 프로토콜이 요구할 수 있는 모든 요구사항을 추가할 수 있습니다.
타입의 기존 인스턴스는 확장에 인스턴스의 타입이 추가될 때 자동으로 프로토콜을 채택하고 준수합니다.
protocol TextRepresentable {
var textualDescription: String { get }
}
Dice 클래스가 위 TextRepresentable 을 채택하고 준수하기 위해 확장될 수 있습니다.
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
이제 모든 Dice 인스턴스를 TextRepresentable 로 처리할 수 있습니다.
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"
조건적으로 프로토콜 준수 (Conditaionally Conforming to a Protocol)
타입을 확장할 때 제약 조건을 나열하여 일반 타입이 프로토콜을 조건적으로 준수할 수 있도록 만들 수 있습니다.
extension Array: TextRepresentable where Element: TextRepresentable {
var textualDescription: String {
let itemsAsText = self.map { $0.textualDescription }
return "[" + itemsAsText.joined(separator: ", ") + "]"
}
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// Prints "[A 6-sided dice, A 12-sided dice]"
확장으로 프로토콜 채택 선언 (Declaring Protocol Adoption with an Extension)
타입이 프로토콜의 모든 요구사항을 미이 구현하고 있다면, 빈 확장을 통해 해당 프로토콜을 채택할 수 있습니다.
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
Hamster 의 인스턴스는 TextRepresentable 이 요구된 타입 어디서든 사용될 수 있습니다.
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"
타입은 요구사항이 충족된다고 해서 프로토콜을 자동으로 채택하지 않습니다. 항상 프로토콜 채택을 명시적으로 선언해야 합니다.
'Swift' 카테고리의 다른 글
static, class, final class 프로퍼티/메서드 (2) | 2024.10.26 |
---|---|
생명주기 (Lifecycle) (2) | 2024.10.14 |
콜렉션 타입(Collection Type) - 딕셔너리(Dictionary) (4) | 2024.10.08 |
콜렉션 타입(Collection Type) - 집합(Set) (4) | 2024.10.07 |
콜렉션 타입(Collection Type) - 배열(Array) (2) | 2024.10.07 |