🔥 프로토콜을 타입으로 사용하기

614자
7분

프로토콜은 그 자체로는 어떤 기능도 구현하지 않습니다. 그럼에도 불구하고, 프로토콜을 코드에서 타입으로 사용할 수 있어요.

프로토콜을 타입으로 사용하는 가장 일반적인 방법은 제네릭 제약 조건으로 프로토콜을 사용하는 것입니다. 제네릭 제약 조건이 있는 코드는 프로토콜을 준수하는 모든 타입과 함께 작동할 수 있으며, 특정 타입은 API를 사용하는 코드에 의해 선택됩니다. 예를 들어, 인자를 받는 함수를 호출할 때 해당 인자의 타입이 제네릭이라면, 호출하는 쪽에서 타입을 선택하게 됩니다.

swift
protocol Greetable {
    func greet() -> String
}
 
class Person: Greetable {
    var name: String
 
    init(name: String) {
        self.name = name
    }
 
    func greet() -> String {
        return "Hello, my name is \(name)."
    }
}
 
class Robot: Greetable {
    var model: String
 
    init(model: String) {
        self.model = model
    }
 
    func greet() -> String {
        return "Beep boop, I am \(model)."
    }
}
 
func introduce<T: Greetable>(greeter: T) {
    print(greeter.greet())
}
 
let person = Person(name: "John")
let robot = Robot(model: "RX-78")
 
introduce(greeter: person) // "Hello, my name is John." 출력
introduce(greeter: robot)  // "Beep boop, I am RX-78." 출력
swift
protocol Greetable {
    func greet() -> String
}
 
class Person: Greetable {
    var name: String
 
    init(name: String) {
        self.name = name
    }
 
    func greet() -> String {
        return "Hello, my name is \(name)."
    }
}
 
class Robot: Greetable {
    var model: String
 
    init(model: String) {
        self.model = model
    }
 
    func greet() -> String {
        return "Beep boop, I am \(model)."
    }
}
 
func introduce<T: Greetable>(greeter: T) {
    print(greeter.greet())
}
 
let person = Person(name: "John")
let robot = Robot(model: "RX-78")
 
introduce(greeter: person) // "Hello, my name is John." 출력
introduce(greeter: robot)  // "Beep boop, I am RX-78." 출력

위 코드에서 introduce 함수는 Greetable 프로토콜을 준수하는 어떤 타입이든 받을 수 있습니다. 호출하는 쪽에서 Person이나 Robot을 선택하여 전달할 수 있죠.

불투명 타입(Opaque type)을 사용하는 코드는 프로토콜을 준수하는 어떤 타입과도 작동합니다. 기본 타입은 컴파일 시점에 알려져 있고, API 구현부에서 해당 타입을 선택하지만, 해당 타입의 정체는 API의 클라이언트로부터 숨겨집니다. 불투명 타입을 사용하면 함수의 특정 반환 타입을 숨기고 값이 주어진 프로토콜을 준수한다는 것만 보장함으로써 API의 구현 세부 정보가 추상화 계층을 통해 누출되는 것을 방지할 수 있습니다.

swift
protocol Shape {
    func draw() -> String
}
 
struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result = [String]()
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")
    }
}
 
struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let result = Array(repeating: line, count: size)
        return result.joined(separator: "\n")
    }
}
 
struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
 
struct JoinedShape<T: Shape, U: Shape>: Shape {
    var top: T
    var bottom: U
    func draw() -> String {
        return top.draw() + "\n" + bottom.draw()
    }
}
 
func makeTrapezoid() -> some Shape {
    let top = Triangle(size: 2)
    let middle = Square(size: 2)
    let bottom = FlippedShape(shape: top)
    let trapezoid = JoinedShape(top: top, bottom: JoinedShape(top: middle, bottom: bottom))
    return trapezoid
}
 
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
/*
  *
  **
  **
  **
  **
  *
*/
swift
protocol Shape {
    func draw() -> String
}
 
struct Triangle: Shape {
    var size: Int
    func draw() -> String {
        var result = [String]()
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")
    }
}
 
struct Square: Shape {
    var size: Int
    func draw() -> String {
        let line = String(repeating: "*", count: size)
        let result = Array(repeating: line, count: size)
        return result.joined(separator: "\n")
    }
}
 
struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")
    }
}
 
struct JoinedShape<T: Shape, U: Shape>: Shape {
    var top: T
    var bottom: U
    func draw() -> String {
        return top.draw() + "\n" + bottom.draw()
    }
}
 
func makeTrapezoid() -> some Shape {
    let top = Triangle(size: 2)
    let middle = Square(size: 2)
    let bottom = FlippedShape(shape: top)
    let trapezoid = JoinedShape(top: top, bottom: JoinedShape(top: middle, bottom: bottom))
    return trapezoid
}
 
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
/*
  *
  **
  **
  **
  **
  *
*/

위 코드에서 makeTrapezoid 함수는 some Shape을 반환합니다. 이는 반환되는 값이 Shape 프로토콜을 준수한다는 것만 보장하고, 실제 타입은 숨깁니다.

박스 프로토콜 타입(Boxed protocol type)을 사용하는 코드는 런타임에 선택된 프로토콜을 준수하는 모든 타입과 작동합니다. 이런 런타임 유연성을 지원하기 위해 Swift는 필요할 때 box라고 알려진 간접층을 추가하는데, 이는 성능 비용이 발생합니다. 이런 유연성 때문에 Swift는 컴파일 시점에 기본 타입을 알 수 없으므로, 프로토콜에서 요구하는 멤버에만 접근할 수 있습니다. 기본 타입의 다른 API에 접근하려면 런타임에 캐스팅해야 합니다.

swift
protocol Drawable {
    func draw() -> String
}
 
func drawACat(_ d: Drawable) {
    print(d.draw())
}
 
struct CatFace: Drawable {
    func draw() -> String {
        return "/\\\\_/\\\\\n( o o )\n \\\\~/"
    }
 
    func meow() {
        print("Meow!")
    }
}
 
let face = CatFace()
drawACat(face)
// face.meow() // 에러: Drawable 프로토콜에는 meow() 메서드가 없음
(face as CatFace).meow() // "Meow!" 출력 - 런타임 캐스팅 필요
swift
protocol Drawable {
    func draw() -> String
}
 
func drawACat(_ d: Drawable) {
    print(d.draw())
}
 
struct CatFace: Drawable {
    func draw() -> String {
        return "/\\\\_/\\\\\n( o o )\n \\\\~/"
    }
 
    func meow() {
        print("Meow!")
    }
}
 
let face = CatFace()
drawACat(face)
// face.meow() // 에러: Drawable 프로토콜에는 meow() 메서드가 없음
(face as CatFace).meow() // "Meow!" 출력 - 런타임 캐스팅 필요

위 코드에서 drawACat 함수는 Drawable 프로토콜을 준수하는 어떤 타입이든 받을 수 있습니다. 하지만 Drawable 프로토콜에 정의되지 않은 메서드에 접근하려면 런타임에 실제 타입으로 캐스팅해야 합니다.

제네릭 제약 조건으로 프로토콜을 사용하는 방법에 대한 자세한 내용은 제네릭을 참조하세요. 불투명 타입과 박스 프로토콜 타입에 대한 자세한 내용은 불투명 타입과 박스 타입을 참조하시면 됩니다.

프로토콜을 타입으로 사용하는 기능은 Swift에서 추상화와 다형성을 강력하게 지원하는 핵심 요소입니다. 이를 통해 코드의 유연성과 재사용성을 크게 높일 수 있으니, 꼭 익혀두시는 것이 좋겠죠?

YouTube 영상

채널 보기
Const 펑터 - 아무것도 안 하는 펑터가 필요한 이유 | 프로그래머를 위한 카테고리 이론
변환 파이프로 컨트롤러 코드 깔끔하게 만들기 | NestJS 가이드
미들웨어 vs 가드, 왜 NestJS에서는 가드가 더 똑똑할까? | NestJS 가이드
C++ 속의 펑터 | 프로그래머를 위한 카테고리 이론
매번 ValidationPipe 복붙하세요? NestJS 전역 파이프로 한 번에 해결하기 | NestJS 가이드
NestJS 전역 에러 처리 | NestJS 가이드
앨런 튜링이 들려주는 튜링 테스트와 보편 기계 이야기
NestJS 필터 바인딩 - Method, Controller, Global Scope 비교 | NestJS 가이드