[10편] 프로토콜과 제네릭 — Swift의 다형성 (1부 완결)

🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: 9편

1부의 마지막 편입니다. 프로토콜과 제네릭은 Swift에서 “재사용 가능한 코드”를 만드는 두 축입니다. 클래스 상속 없이도 다형성을 구현할 수 있고, 타입을 몰라도 동작하는 함수를 만들 수 있습니다. Swift 표준 라이브러리와 SwiftUI 전체가 이 두 개념 위에서 돌아갑니다.


프로토콜이란

프로토콜은 “이 타입은 이런 기능을 갖고 있다”는 계약입니다. Java나 Kotlin의 인터페이스와 비슷하지만, 기본 구현을 제공할 수 있다는 점에서 더 강력합니다.

protocol Greetable {
    var name: String { get }
    func greet() -> String
}

struct User: Greetable {
    var name: String

    func greet() -> String {
        return "안녕하세요, \(name)입니다"
    }
}

struct Bot: Greetable {
    var name: String

    func greet() -> String {
        return "저는 봇 \(name)입니다"
    }
}

let things: [any Greetable] = [User(name: "철수"), Bot(name: "Jarvis")]
for thing in things {
    print(thing.greet())
}
// 안녕하세요, 철수입니다
// 저는 봇 Jarvis입니다

클래스뿐 아니라 구조체와 열거형도 프로토콜을 채택할 수 있습니다. 이것이 Swift가 클래스 상속에 덜 의존하는 이유 중 하나입니다.


프로토콜 익스텐션 — 기본 구현 제공

프로토콜에 기본 구현을 붙일 수 있습니다. 채택한 타입이 구현하지 않으면 기본 구현이 쓰입니다.

protocol Greetable {
    var name: String { get }
    func greet() -> String
}

extension Greetable {
    func greet() -> String {       // 기본 구현
        return "안녕하세요, \(name)"
    }
}

struct Guest: Greetable {
    var name: String
    // greet()를 구현하지 않아도 됨 — 기본 구현 사용
}

print(Guest(name: "방문자").greet())  // 안녕하세요, 방문자

이 패턴을 프로토콜 지향 프로그래밍(Protocol-Oriented Programming)이라고 부릅니다. 공통 동작을 클래스 계층 없이 프로토콜 익스텐션으로 배포할 수 있습니다.


자주 쓰는 내장 프로토콜

Swift 표준 라이브러리는 여러 프로토콜을 제공합니다. 직접 구현하지 않아도 Codable처럼 컴파일러가 자동으로 합성해주는 것들도 있습니다.

프로토콜 의미 자동 합성
Equatable ==로 동등 비교 가능 구조체·enum에서 가능
Comparable <, > 등 순서 비교 가능 부분 가능
Hashable Dictionary 키, Set 원소로 사용 가능 구조체·enum에서 가능
Codable JSON 직렬화·역직렬화 가능 프로퍼티가 모두 Codable이면 가능
Identifiable 고유 id 프로퍼티 보유 (SwiftUI List에서 사용) 없음
CustomStringConvertible description 프로퍼티로 출력 형식 지정 없음
struct Point: Equatable, Hashable, CustomStringConvertible {
    var x: Int
    var y: Int

    var description: String { "(\(x), \(y))" }
}

let a = Point(x: 1, y: 2)
let b = Point(x: 1, y: 2)

print(a == b)         // true (Equatable 자동 합성)
print(a)              // (1, 2) (CustomStringConvertible)

var visited: Set<Point> = [a, b]
print(visited.count)  // 1 (Hashable — 중복 제거)

제네릭 — 타입을 몰라도 동작하는 코드

제네릭(generics)을 쓰면 특정 타입에 묶이지 않고 어떤 타입에도 동작하는 함수나 타입을 만들 수 있습니다.

// Int 전용 swap
func swapInts(_ a: inout Int, _ b: inout Int) {
    let temp = a; a = b; b = temp
}

// 제네릭 swap — 어떤 타입에도 동작
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a; a = b; b = temp
}

var x = 1, y = 2
swap(&x, &y)
print(x, y)  // 2 1

var s1 = "hello", s2 = "world"
swap(&s1, &s2)
print(s1, s2)  // world hello

T는 타입 플레이스홀더입니다. 실제 호출 시 Swift가 인자 타입을 보고 T가 무엇인지 추론합니다.

제네릭 타입도 만들 수 있습니다. Swift의 Array<Element>Dictionary<Key, Value>가 모두 제네릭 타입입니다.

struct Stack<Element> {
    private var items: [Element] = []

    mutating func push(_ item: Element) { items.append(item) }
    mutating func pop() -> Element? { items.popLast() }
    var top: Element? { items.last }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop())  // Optional(2)

var stringStack = Stack<String>()
stringStack.push("hello")

타입 제약 — T에 조건 붙이기

제네릭 타입에 프로토콜 조건을 붙이면 해당 프로토콜의 메서드를 함수 내에서 쓸 수 있습니다.

// T가 Comparable을 채택한 경우에만 사용 가능
func largest<T: Comparable>(in array: [T]) -> T? {
    guard !array.isEmpty else { return nil }
    return array.max()
}

largest(in: [3, 1, 4, 1, 5])   // Optional(5)
largest(in: ["banana", "apple", "cherry"])  // Optional("cherry")

where로 더 복잡한 조건도 붙일 수 있습니다.

func merge<T>(_ a: [T], _ b: [T]) -> [T] where T: Equatable {
    return a + b.filter { !a.contains($0) }
}

some과 any — Swift 5.7의 타입 표현

프로토콜을 타입으로 쓸 때 두 키워드의 차이를 알아야 합니다.

some (불투명 타입) — 컴파일 타임에 하나의 구체적인 타입으로 고정됩니다. 호출하는 쪽은 정확한 타입을 몰라도 되지만, 매번 같은 타입이 반환됨을 컴파일러가 알고 최적화합니다. SwiftUI의 body: some View가 대표적입니다.

func makeShape() -> some Shape {
    return Circle()  // 항상 Circle을 반환
}

any (존재 타입) — 런타임에 어떤 타입이든 담을 수 있는 박스입니다. 유연하지만 성능 비용이 있습니다.

func describe(_ item: any Greetable) {
    print(item.greet())  // 런타임에 타입 결정
}

짧게 정리하면: 반환 타입이 항상 같다면 some, 여러 타입을 담아야 한다면 any.


다른 언어와 비교

Java / Kotlin TypeScript Swift
프로토콜/인터페이스 interface interface protocol
기본 구현 Java 8+ default 없음 프로토콜 익스텐션
구조체 채택 불가 (클래스만) 불가 가능
제네릭 <T> <T> <T>
타입 제약 <T extends Comparable> <T extends Comparable> <T: Comparable>

핵심 요약

  • 프로토콜은 타입이 갖춰야 할 기능의 계약이다. 클래스·구조체·열거형 모두 채택할 수 있다.
  • 프로토콜 익스텐션으로 기본 구현을 제공할 수 있다. 채택한 타입이 구현을 생략하면 기본 구현이 쓰인다.
  • Equatable, Hashable, Codable 같은 내장 프로토콜은 구조체와 enum에서 컴파일러가 자동으로 구현을 만들어준다.
  • 제네릭은 타입에 무관하게 동작하는 함수와 타입을 만든다. 타입 제약(T: Protocol)으로 범위를 제한한다.
  • some은 컴파일 타임에 고정된 하나의 타입, any는 런타임에 다양한 타입을 담는 박스다.

1부를 마치며

10편에 걸쳐 Swift 언어의 기초를 다뤘습니다. 변수·타입·제어 흐름부터 함수·클로저·옵셔널, 그리고 구조체·열거형·프로토콜·제네릭까지. 이 개념들이 서로 어떻게 연결되는지는 실제 코드를 작성하면서 점점 명확해집니다.

다음은 2부 — Swift 고급 주제입니다. 에러 처리, 메모리 관리(ARC), Swift Concurrency(async/await, actor)를 다룹니다. 1부보다 복잡하지만, 실제 앱을 만들 때 반드시 필요한 내용들입니다.

🤖 Generated with Claude Code