프로토콜 지향 프로그래밍이란?
Swift는 클래스 상속 중심의 OOP 대신 프로토콜 지향 프로그래밍(Protocol-Oriented Programming, POP)을 권장합니다. 프로토콜은 타입이 따라야 할 인터페이스를 정의하고, 프로토콜 확장으로 기본 구현을 제공합니다. 구조체와 열거형에도 적용할 수 있어 참조 타입의 문제(의도치 않은 공유 상태)를 피할 수 있습니다.
이 글에서는 프로토콜의 기본부터 연관 타입, 프로토콜 합성까지 실전 패턴을 정리합니다.
프로토콜 기본
프로토콜은 프로퍼티와 메서드의 청사진을 정의합니다. 타입이 프로토콜을 채택(conform)하면 해당 요구사항을 반드시 구현해야 합니다.
import Foundation
// 프로토콜 정의
protocol Describable {
var description: String { get } // 읽기 전용 프로퍼티
func summarize() -> String // 메서드
}
protocol Identifiable {
var id: String { get }
}
// 구조체에서 프로토콜 채택
struct User: Describable, Identifiable {
let id: String
let name: String
let email: String
var description: String {
return "\(name) (\(email))"
}
func summarize() -> String {
return "사용자 \(id): \(name)"
}
}
struct Product: Describable, Identifiable {
let id: String
let name: String
let price: Int
var description: String {
return "\(name) — \(price)원"
}
func summarize() -> String {
return "상품 \(id): \(name) (\(price)원)"
}
}
// 프로토콜 타입으로 다형성 사용
func printSummaries(_ items: [Describable]) {
for item in items {
print(item.summarize())
}
}
let user = User(id: "U001", name: "홍길동", email: "hong@example.com")
let product = Product(id: "P001", name: "키보드", price: 89000)
printSummaries([user, product])
// 실행 결과:
// 사용자 U001: 홍길동
// 상품 P001: 키보드 (89000원)
프로토콜 확장과 기본 구현
프로토콜 확장(Protocol Extension)으로 기본 구현을 제공하면, 채택하는 타입에서 별도 구현 없이 사용할 수 있습니다.
import Foundation
// 프로토콜 정의
protocol Loggable {
var logPrefix: String { get }
}
// 프로토콜 확장 — 기본 구현 제공
extension Loggable {
var logPrefix: String {
return String(describing: type(of: self))
}
func log(_ message: String) {
let timestamp = ISO8601DateFormatter().string(from: Date())
print("[\(timestamp)] [\(logPrefix)] \(message)")
}
func logError(_ message: String) {
log("ERROR: \(message)")
}
}
// Comparable을 확장하여 범위 제한 기능 추가
extension Comparable {
func clamped(to range: ClosedRange<Self>) -> Self {
return min(max(self, range.lowerBound), range.upperBound)
}
}
// Collection 확장 — 안전한 인덱스 접근
extension Collection {
subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
// 프로토콜 채택 — 기본 구현 자동 사용
struct OrderService: Loggable {
func processOrder(id: String) {
log("주문 \(id) 처리 시작")
// 비즈니스 로직...
log("주문 \(id) 처리 완료")
}
}
struct PaymentService: Loggable {
let logPrefix = "Payment" // 기본 구현 재정의
func charge(amount: Int) {
log("결제 \(amount)원 요청")
}
}
let orderService = OrderService()
orderService.processOrder(id: "ORD-001")
// 실행 결과:
// [2026-02-22T15:00:00Z] [OrderService] 주문 ORD-001 처리 시작
// [2026-02-22T15:00:00Z] [OrderService] 주문 ORD-001 처리 완료
let paymentService = PaymentService()
paymentService.charge(amount: 50000)
// 실행 결과:
// [2026-02-22T15:00:00Z] [Payment] 결제 50000원 요청
// Comparable 확장 사용
let temperature = 35
let safe = temperature.clamped(to: 0...30)
print("원래: \(temperature), 제한: \(safe)")
// 실행 결과: 원래: 35, 제한: 30
// 안전한 인덱스 접근
let items = ["A", "B", "C"]
print("items[1]: \(items[safe: 1] ?? "없음")") // B
print("items[5]: \(items[safe: 5] ?? "없음")") // 없음
// 실행 결과:
// items[1]: B
// items[5]: 없음
연관 타입 (Associated Types)
연관 타입은 프로토콜 내부에서 사용할 타입을 추상화합니다. 제네릭과 결합하여 타입 안전한 인터페이스를 정의할 수 있습니다.
import Foundation
// 연관 타입을 가진 프로토콜
protocol Repository {
associatedtype Entity // 구현 시 구체 타입 결정
associatedtype ID: Hashable
func findById(_ id: ID) -> Entity?
func findAll() -> [Entity]
func save(_ entity: Entity) -> Entity
func deleteById(_ id: ID) -> Bool
}
// 구체 타입으로 구현
struct Article {
let id: Int
var title: String
var content: String
}
class ArticleRepository: Repository {
// Entity = Article, ID = Int 으로 결정됨
private var storage: [Int: Article] = [:]
func findById(_ id: Int) -> Article? {
return storage[id]
}
func findAll() -> [Article] {
return Array(storage.values)
}
func save(_ entity: Article) -> Article {
storage[entity.id] = entity
return entity
}
func deleteById(_ id: Int) -> Bool {
return storage.removeValue(forKey: id) != nil
}
}
// 제네릭 함수에서 프로토콜 사용
func printAllEntities<R: Repository>(from repo: R) where R.Entity: CustomStringConvertible {
let all = repo.findAll()
for entity in all {
print(" - \(entity)")
}
}
// Article을 CustomStringConvertible로 확장
extension Article: CustomStringConvertible {
var description: String {
return "[\(id)] \(title)"
}
}
let repo = ArticleRepository()
let _ = repo.save(Article(id: 1, title: "Swift POP 가이드", content: "..."))
let _ = repo.save(Article(id: 2, title: "SwiftUI 시작하기", content: "..."))
if let article = repo.findById(1) {
print("찾은 게시글: \(article)")
}
// 실행 결과: 찾은 게시글: [1] Swift POP 가이드
print("전체 게시글:")
printAllEntities(from: repo)
// 실행 결과:
// 전체 게시글:
// - [1] Swift POP 가이드
// - [2] SwiftUI 시작하기
프로토콜 합성과 some/any
여러 프로토콜을 조합하여 타입 제약을 표현할 수 있습니다. Swift 5.7 이후 some과 any 키워드로 존재 타입과 불투명 타입을 구분합니다.
import Foundation
protocol Cacheable {
var cacheKey: String { get }
var ttl: Int { get } // 초 단위
}
protocol Serializable {
func toJSON() -> String
}
// 프로토콜 합성 — 여러 프로토콜 동시 채택
struct CachedUser: Cacheable, Serializable, CustomStringConvertible {
let id: String
let name: String
var cacheKey: String { "user:\(id)" }
var ttl: Int { 3600 }
var description: String { "\(name) (캐시: \(cacheKey))" }
func toJSON() -> String {
return "{\"id\": \"\(id)\", \"name\": \"\(name)\"}"
}
}
// 프로토콜 합성 타입으로 매개변수 정의
func cacheAndSerialize(_ item: any Cacheable & Serializable) {
print("캐시 키: \(item.cacheKey), TTL: \(item.ttl)초")
print("JSON: \(item.toJSON())")
}
// some — 불투명 반환 타입 (구체 타입 숨김, 타입 정보 유지)
func createDefaultUser() -> some Cacheable & Serializable {
return CachedUser(id: "default", name: "기본 사용자")
}
let user = CachedUser(id: "U001", name: "홍길동")
cacheAndSerialize(user)
// 실행 결과:
// 캐시 키: user:U001, TTL: 3600초
// JSON: {"id": "U001", "name": "홍길동"}
let defaultUser = createDefaultUser()
print("기본 사용자 캐시 키: \(defaultUser.cacheKey)")
// 실행 결과: 기본 사용자 캐시 키: user:default
실전 팁
- 구조체 + 프로토콜 우선: 클래스 상속보다 구조체 + 프로토콜 채택을 기본으로 사용합니다. 값 타입은 의도치 않은 상태 공유를 방지합니다.
- 프로토콜 확장으로 기본 구현: 모든 채택 타입에 공통 로직이 있으면 프로토콜 확장에 기본 구현을 제공합니다.
- 작은 프로토콜: 하나의 큰 프로토콜보다 여러 작은 프로토콜을 정의하고 합성합니다 (인터페이스 분리 원칙).
- 연관 타입은 제네릭 대안: 프로토콜 내부에서 타입을 추상화할 때 연관 타입을 사용합니다.
- some vs any:
some은 구체 타입 정보가 유지되어 성능이 좋고,any는 다양한 구체 타입을 담을 수 있어 유연합니다. 기본적으로some을 사용하고, 이종 컬렉션이 필요할 때any를 사용합니다.