Swift에서 프로토콜(Protocol)이란 무엇이며, 어떻게 사용하나요?

소혜 (Sohye)
xohxe (김소혜)· April 03, 2024

Swift에서 프로토콜은 특정 작업이나 기능에 적합한 메서드, 프로퍼티 및 기타 요구 사항의 청사진(Blueprint)이다. 이러한 요구 사항의 실제 구현을 제공하기 위해 클래스, 구조체 또는 열거형에 의해 채택될 수 있다. 프로토콜을 사용하면 채택된 유형이 준수해야 하는 일련의 규칙이나 동작을 정의할 수 있다.

특히 마지막 문장은 대체 무슨말인지 싶겠지만, 쉽게 설명하자면 어떤 프로퍼티, 메서드에 대해 구현하는 것이 아니라, 해당 기능에 의해 필요한 요구사항을 선언해두는 것이 프로토콜(Protocol)이다.

Swift에서 프로토콜을 사용하는 방법

그렇다면 프로토콜을 사용하는 방법을 간단한 예를 통해 알아보자.

COPY
// 프로토콜 선언
protocol Identifiable {    
   var id: String { get }
}  

// 프로토콜을 채택하는 경우
struct User: Identifiable { 
   var id: String
}

이 예에서 Identifiable은 단일 요구 사항이 있는 프로토콜이다. 모든 채택 유형은 Stringid 속성을 가져야 하며, User 구조체는 이 프로토콜을 채택하고 id 속성을 제공하여 요구 사항을 충족한다.

COPY
protocol FirstProtocol {	
}
// 여러개의 프로토콜을 채택하는 경우
struct SomeStructure: Identifiable, FirstProtocol {
}

// 클래스 상속과 프로토콜 채택이 동시에 되는 경우 -> 슈퍼 클래스를 제일 앞쪽에
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

1. 프로토콜 요구 사항

프로토콜을 정의할 때는 몇 가지 요구사항이 있다.

1-1. Property 속성

  • 프로퍼티 선언 시에는, 항상 var 키워드를 붙여야한다. - 이는 속성의 가변성과는 관련이 없다. - let 은 사용 불가능하다. 프로토콜의 요구사항과 다르게 쓰기가 불가능해지기 때문이다.
  • 연산 프로퍼티인지, 저장 프로퍼티인지는 강제할 수 없다.
  • 프로퍼티의 가변성은 변수 타입 뒤에 붙는 {get set} 의 조합으로 결정된다. - get, set 이 모두 있는 프로퍼티라면, 읽기와 쓰기 모두 가능 하도록 해야하며 - get 만 있는 프로퍼티라면 읽기만 가능해도 된다. get이 둘다 가능한 이유는 프로토콜은 최소한의 요구사항이기 때문이다.
  • type property는 항상 static 키워드를 사용해야한다. - 타입 프로퍼티는 서브클래스에게 상속되지만, 오버라이딩은 불가능하다. - 오버라이딩이 가능하게 하려면 구현 시 class 를 붙이면 된다.
COPY
protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get } // get-only
}

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set } // type property
}

1-2. Method 메서드

  • type method: 메서드도 프로퍼티와 마찬가지로 static 키워드로 type method를 구현할 수있다. - 일반적인 instance method 와 type method 처럼 정의하면 되지만, - 프로토콜은 구현은 하지 않는 요구사항이기 때문에 {} 에 해당하는 바디 부분은 없다.
  • instance method: 메서드명과 리턴값을 지정한다.
COPY
protocol SomeProtocol {
	// Type method
	// with no-input, no-return
	static func someTypeMethod() 
}

protocol RandomNumberGenerator {
	// Instance Method 
	// with no-input, Double type return
    func random() -> Double
}

위의 해당하는 프로토콜을 채택한 타입은 해당하는 메서드들을 구현해주면 된다. 프로퍼티 요구사항과 마찬가지로, static 으로 선언된 메서드를 오버라이딩하고 싶을 경우에는 class 키워드를 붙여서 구현해도 문제없다.

1-3. Initializer 생성자

프로토콜에서 Initializer는 메서드 선언 방식과 마찬가지로 { } 없이 init 으로 선언해준다.

COPY
protocol SomeProtocol {
  init(someParameter: Int)
}

프로토콜을 채택한 타입은 required 키워드를 initializer 앞에 붙여주어야하는데, 아래 예시에서 보다시피 SomeClass 를 서브클래싱한 클래스들도 해당 프로토콜을 conform 해야 하기 때문이다.

COPY
class SomeClass: SomeProtocol {
  required init(someParameter: Int) {
    // implementation code
  }
}

단 protocol를 구현하는 클래스가 final class 라면  required 를 붙일 필요가 없다. final class는 서브클래싱이 불가능하기 때문입니다. required 키워드가 없어도 Compile Error 가 발생하지 않는다.

COPY
final class myClass: SomeProtocol {
    init(number: Int) {
        ...
    }
}

1-4. 서브스크립트 요구사항

COPY
protocol SomeSubscript {
  subscript(_: Int) -> Int { get }
}

struct Some: SomeSubscript {
  subscript(index: Int) -> Int {
    <#code#>
  }
}

get set 조합으로 선언 가능하며 서브스크립트의 구현부는 계산 속성의 규칙을 따르므로 조합에 따라 구현해주면 된다.

예를 들어 프로토콜에서 get 으로 선언된 경우 get 또는 get set 블록 모두를 구현해줄 수 있다.

💡parameter name 은 일치시키지 않아도 요구사항에 충족됩니다.

2. Protocol Extension을 사용하는 이유는 무엇입니까?

프로토콜 확장을 사용하면 기존의 프로토콜에서 새로운 기능을 추가하거나, 이미 채택된 기능에 대해 기본 구현을 제공하여 코드 재사용성을 높일 수 있다. 그리고 여러 타입에 대해 공통적으로 사용되는 요소들을 확장을 통해 한 번에 정의하고, 코드의 중복을 최소화할 수 있다.

프로토콜에는 정의만 한다고 했는데, Extension을 통해 구현을 추가한다는게 어색할 수 있다. 문법적으로는 프로토콜의 구현을 추가하지만, 실제로는 프로토콜을 채용한 타입의 구현이 추가된다고 보면 된다.

정리하자면 프로토콜을 준수하는 모든 타입에서 채택할 수 있는 프로퍼티, 메서드 및 서브스크립트에 대해 기본 구현을 제공할 수 있다. 여기서 기본 구현이란 프로토콜을 채택한 타입에서 구현을 추가하지 않을 시 기본적으로 제공되는 구현을 가리킨다.

프로토콜의 확장은 프로토콜을 채택한 모든 멤버를 추가하는데, 멤버를 추가할 타입을 제한할 수도 있다.

COPY
extension SomeProtocol where Self: Equatable { 
	// where 키워드로 타입 제한
    func hello() {
        print(text)
    }
}

struct A: SomeProtocol { 
	// Equatable을 채택하지 않아 hello 메서드를 직접 구현하지 않으면 에러 발생
    var text = "프로토콜 확장"
}

위와 같이 타입을 제한하였는데 프로토콜만 채택하고 확장에서 제한한 타입을 같이 채택하지 않으면 확장에서 구현한 멤버도 추가되지 않는다. 따라서 아래와 같이 제한한 타입을 추가해주면 에러가 해결된다

COPY
protocol SomeProtocol {
    var text: String { get set }
    func hello()
}

extension SomeProtocol where Self: Equatable {
	// 확장하면서 타입 추가
    func hello() {
        print(text)
    }
}

struct A: SomeProtocol, Equatable { 
	// Equatable을 추가하여 확장에서 구현한 멤버 hello()가 추r
    var text = "프로토콜 확장!"
}

let a = A()
a.hello()

3. 프로토콜 지향 프로그래밍의 장점

프로토콜 지향 프로그래밍(POP)은 Swift에서 사용하는 강력한 프로그래밍 패러다임 중 하나이다. 여러 프로토콜을 채택할 수 있다는 점에서 다중 상속과 유사하다. 프로토콜은 클래스뿐만 아니라, 상속이 불가한 구조체열거형에도 적용될 수 있다. 이는 값 타임을 강화하고 불변성(Immutability)을 유지할 수 있는데 도움이 된다.

프로토콜을 중심으로 작성하면, 더 모듈화되고 유연한 코드를 작성할 수 있게 도와준다. 그리고 기능을 정의하는데 중점을 두므로 타입에 제약을 덜 가하고 더 일반적인 동작을 정의할 수 있다. 이러한 특징들은 테스트, 기능 확장, 유지보수, 재사용성을 높이는데 도움이 된다.

반대로 클래스는 단일 상속만을 지원하고, 상속은 클래스끼리만 할 수 있다. 클래스는 주로 상속을 통한 코드 재사용을 강조하는 반면, 프로토콜은 특정 기능을 가져오는 방식을 강조한다. 이러한 프로토콜의 특징은 더 모듈화된 코드와 유연한 디자인을 가능하게 한다.

다음 게시물
Swift 제네릭(Generic)에 대해 설명해주세요.
이전 게시물
온보딩화면을 만들어보자! - Part 01. 기본스타일

© 2023 - 2024 xohxe. All Rights Reserved.