🤖 이 글은 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