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

Swift : 기초문법 [프로토콜#2 - 메서드 요구사항]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님의 Swift 프로그래밍 3판을 참고하여 작성되었습니다.

메서드 요구사항

  • 프로토콜은 특정 인스턴스 메서드나 타입 메서드를 요구할 수 있다.
  • 프로토콜이 요구할 메서드는 프로토콜 정의에서 작성한다.
  • 메서드는 일반 인스턴스 및 타입 메서드와 완전히 동일하지만, 실제 구현부인 중괄호 { } 와 메서드 본문 없이 프로토콜 정의의 일부로 작성된다.
  • 매개변수 기본값을 지정할 수 없다.
  • 가변 파라미터는 일반 메서드와 동일하게 작성할 수 있다.
  • 타입 메서드를 요구할 경우 static 키워드를 사용하고, static 키워드를 사용해 요구한 타입 메서드를 클래스에서 구현할 때는 static 키워드나 class 키워드 어느 쪽을 사용하든 상관없다.

프로토콜에서 메서드를 정의할 때 중괄호와 본문을 작성하지 않고 '정의'만 한다고 한 부분을 알아보자면

protocol SomeProtocol {
    func someMethod()
}

이런 식으로 중괄호 { } , 메서드 본문 없이 오직 정의만 프로토콜에서 하면 된다.

 

또는 일반 메서드와 동일하게 작성할 수 있다.

protocol SomeProtocol {
    func someMethod()
    func anotherMethod(name: String, age: Int) -> Int
    func protocolMethod() -> String
}

타입 메서드 요구사항

타입 메서드를 요구할 경우 static 키워드를 사용하고, static 키워드를 사용해 요구한 타입 메서드를 클래스에서 구현할 때는 static 키워드나 class 키워드 어느 쪽을 사용하든 상관없다.

protocol SomeProtocol {
    static func someTypeMethod()
    static func anotherTypeMethod()
}

위 프로토콜은 아래와 같이 클래스에서 채택할 수 있다.

class SomeClass: SomeProtocol {
    static func someTypeMethod() {
    
    }
    class func anotherTypeMethod() {
    
    }
}

SomeClass에서 SomeProtocol을 채택했고, SomeProtocol이 요구한 메서드를 구현한다. 그리고 someTypeMethodstatic 키워드로, anotherTypeMethod는 class 키워드로 구현했다.

 

타입 프로퍼티때와 마찬가지로 클래스 안에서 class 키워드로 구현하면, 자식 클래스에서 재정의 가능하다. 

class SomeClass: SomeProtocol {
    static func someTypeMethod() {
    
    }
    class func anotherTypeMethod() {
    
    }
}

class SubClass: SomeClass {

   override static func anotherTypeMethod() {
   
   }
}

공식 문서 예제를 한번 살펴보자!

 

필수 메서드 지정 시 함수명과 반환 값을 지정할 수 있고, 구현에 사용하는 괄호는 적지 않아도 된다.

protocol RandomNumberGenerator {
    func random() -> Double
}

random이라는 메서드를 정의했고, RandomNumberGenerator이라는 프로토콜은 이 RandomNumberGenerator를 채택하고 준수하는 클래스, 구조체, 열거형에 random이라는 메서드를 요구하며, 호출될 때마다 Double값을 리턴하게 된다.

 

아래 코드는 따르는 프로토콜의 필수 메서드 random()을 구현한 클래스이다.

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"

LinearCongruentialGenerator이라는 클래스가 RandomNumberGenerator를 채택했고, 여러 개의 저장 인스턴스 프로퍼티들을 선언했다. 저장 프로퍼티에 기본값이 있기 때문에 init()으로 초기화하지 않아도 된다.

 

그리고 RandomNumberGenerator 프로토콜이 요구한 random()이라는 메서드도 구현해주면 된다.

TIP
 
 

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

- 함수, 메서드, 이니셜라이저에서 매개변수 타입 또는 반환 타입으로 사용 가능.
- 프로퍼티, 변수, 상수, 등의 타입으로 사용 가능.
- 배열, 딕셔너리 등 컨테이너 요소의 타입으로 사용 가능. 

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

가변(변경 가능한) 메서드 요구사항 (Mutating)

가끔은 메서드가 인스턴스 내부의 값을 변경할 때가 있다.

 

값 타입(구조체, 열거형)의 인스턴스 메서드에서 자신 내부의 값을 변경할 때는 메서드 func 키워드 앞에 mutating 키워드를 적어 메서드에서 인스턴스 내부의 값을 변경한다는 걸 알려줘야 한다!!라고 저번 mutating 게시글에서 작성한 적이 있다. 

  • 프로토콜이 어떤 타입이든 인스턴스 내부의 값을 변경해야 하는 메서드를 요구하려면 프로토콜의 메서드 정의 앞에 mutating 키워드를 명시해야 한다.
  • 이로 인해 구조체와 열거형에서 프로토콜을 채택하고, 해당 메서드 요구사항을 충족시킬 수 있다.
  • 값 타입인 클래스에서는 mutating 키워드 명시 불필요.
  • 프로토콜에 mutating 키워드를 사용한 메서드 요구가 존재해도, 클래스 구현에서는 mutating 키워드를 사용하지 않아도 된다.
protocol Togglable {
    mutating func toggle()
}

위 프로토콜을 따르는 값 타입(구조체, 열거형)에서 toggle() 메서드를 변경해 사용할 수 있다.

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

또 다른 예제를 보자면

protocol SomeProtocol{
    mutating func SomeMethod(_ num : Int)
}

struct SomeStruct : SomeProtocol{
    var x = 0
    mutating func SomeMethod(_ num :Int) {
        x += num
    }
}

구조체, 열거형은 값 타입 이기 때문에 메서드 안에서 프로퍼티 값을 변경하지 못하지만, mutating 키워드를 사용하여 그것을 가능하게 해 준다. 

 

참조형인 클래스에서는 mutating 키워드가 필요하지 않다. 

protocol SomeProtocol{
    mutating func SomeMethod(_ num : Int)
}

class SomeStruct : SomeProtocol{
    var x = 0
    func SomeMethod(_ num :Int) {
        x += num
    }
}

만약 프로토콜에서 가변 메서드(mutating)를 요구하지 않는다면, 값 타입의 인스턴스 내부 값을 변경하는 mutating 메서드는 구현이 불가능하다.

이니셜라이저 요구사항

  • 프로토콜은 프로토콜을 준수하는 타입에게 특정한 이니셜라이저를 구현하도록 요구 가능하다.
  • 이니셜라이저를 요구하려면 메서드 요구와 마찬가지로 이니셜라이저를 정의하지만 구현은 하지 않는다.
  • 즉, 이니셜라이저의 매개변수를 지정하기만 할 뿐, 중괄호 { } 를 포함한 이니셜라이저 구현 안 함!
protocol SomeProtocol {
   init(someParameter: String)
}
protocol Named {
    var name: String { get }
    
    init(name: String)

}

struct Student: Named {
    var name: String
    
    init(name: String) {
       self.name = name
    }
}

Student 구조체는 Named 프로토콜을 채택하였고, 요구 프로퍼티와 이니셜라이저를 모두 구현했다. 구조체는 상속이 불가능 하기 때문에 이니셜라이저 요구에 대해 크게 신경 쓸 필요가 없지만, 클래스의 경우는 조금 다르다.

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

클래스 타입에서 프로토콜의 이니셔라이저 요구에 부합하는 이니셜라이저를 구현할 때는 이니셜라이저가 지정 이니셜라이저인지, 편의 이니셜라이저인지 중요 하지 않다.

 

하지만, 이니셜라이저 요구에 부합하는 이니셜라이저를 구현할 때는 required 식별자를 붙인 요구 이니셜라이저로 반드시 구현해야 한다.

 

즉!!Student를 상속받는 모든 클래스는 Named 프로토콜을 준수해야 하며, 이것은 상속받는 클래스에 해당 이니셜라이저를 모두 구현해야한다는 뜻이다!

 

그렇기 때문에 Named에서 요구하는 init(name: ) 이니셜라이저를 required 식별자를 붙힌 요구 이니셜라이저로 구현해야 한다.

 

만약, 클래스가 자체 상속을 받을 수 없는 final(재정의 방지) 클래스라면 required 식별자를 붙여줄 필요가 없다. 상속할 수 없는 클래스의 요청이기 때문에 이니셜라이저 구현은 의미가 없기 때문!

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

또, 만약 특정 클래스에 프로토콜이 요구하는 이니셜라이저가 이미 구현되어 있고, 그 클래스를 상속받은 클래스가 존재한다면, requiredoverride 식별자를 모두 사용하여 프로토콜에서 요구하는 이니셜라이저를 구현해야 한다.

protocol Named {
   var name: String { get }
  
   init(name: String)
}

//School 클래스에서는 Named 프로토콜을 채택하지 않았지만 
//Named프로토콜이 요구하는 이니셜라이저가 존재. required 식별자 사용 불필요
class School {
   var name: String
   
   init(name: String) {
      self.name = name
   }
}

//School 슈퍼클래스와 Named 프로토콜을 모두 채택
//슈퍼클래스의 init(name:)를 재정의 해야하고, 
//Named 프로토콜의 이니셜라이저 요구도 충족 해야줘야함. 
//required와 override 모두 표기
class MiddleSchool: School, Named {
    required override init(name: String) {
       super.init(name: name)
    }
}

위 코드를 보면 School  클래스는 Named프로토콜을 채택하지 않았지만 프로토콜이 요구하는 이니셜라이저가 존재는 한다.

 

그리고 MiddleSchool 자식클래스는 School 클래스를 상속받았고 동시에 Named 프로토콜을 채택했기 때문에 School 클래스의 init(name:) 이니셜라이저를 재정의 해야하고, Named 프로토콜의 이니셜라이저 요구도 충족해줘야 한다. 

 

그래서! requiredoverride 식별자를 모두 표기해야 한다!

 

두 식별자의 순서는 상관없다! override required 이든, required override 이든 상관 無!

 

프로토콜은 일반 이니셜라이저 외에도 실패 가능한 이니셜라이저를 요구할 수 도 있다. 실패 가능한 이니셜라이저를 요구하는 프로토콜을 준수하는 타입은 해당 이니셜라이저를 구현할 때 실패 가능한 이니셜라이저로 구현해도, 일반적인 이니셜라이저로 구현해도 무방하다.

protocol Named {
    var name: String { get }  //읽기 전용 프로퍼티
    
    //실패 가능한 이니셜라이저
    init?(name: String)
}

struct Animal: Named {
    var name: String
    
    init!(name: String) {
       self.name = name
    }
}

struct Pet: Named {
    var name: String
    
    init(name: String) {
       self.name = name
    }
}

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

class School: Named {
    var name: String
    
    required init?(name: String) {
       self.name = name
    }
}

다시 한번 말하지만, 값 타입인 구조체와 열거형에선 required 식별자는 사용하지 않아도 된다!

 

 

읽어주셔서 감사합니다 🤟

 

 

 

 


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


서근


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