Swift 프로토콜과 확장 | Protocol, Extension
이 글의 핵심
Swift 프로토콜과 확장에 대해 정리한 개발 블로그 글입니다. protocol Drawable { func draw() var color: String { get set } }
들어가며
프로토콜은 “이 타입이 구현해야 할 행동”을 설계도처럼 적어 두는 역할입니다. 클래스 상속 대신 프로토콜 확장으로 공통 구현을 나누는 패턴이 흔합니다.
1. 프로토콜 정의
기본 프로토콜
protocol Drawable {
func draw()
var color: String { get set }
}
struct Circle: Drawable {
var color: String
func draw() {
print("\(color) 원 그리기")
}
}
let circle = Circle(color: "빨강")
circle.draw()
프로퍼티 요구사항
protocol Named {
var name: String { get } // 읽기 전용
var fullName: String { get set } // 읽기/쓰기
}
struct Person: Named {
var name: String
var fullName: String
}
메서드 요구사항
protocol Resettable {
mutating func reset()
}
struct Counter: Resettable {
var count = 0
mutating func reset() {
count = 0
}
}
다중 프로토콜 채택
protocol Drawable {
func draw()
}
protocol Movable {
func move(to x: Int, y: Int)
}
struct Shape: Drawable, Movable {
func draw() {
print("도형 그리기")
}
func move(to x: Int, y: Int) {
print("(\(x), \(y))로 이동")
}
}
2. Extension (확장)
기본 확장
extension Int {
func squared() -> Int {
return self * self
}
var isEven: Bool {
return self % 2 == 0
}
}
print(5.squared()) // 25
print(4.isEven) // true
프로토콜 구현 분리
protocol Describable {
func describe() -> String
}
struct User {
let name: String
let age: Int
}
extension User: Describable {
func describe() -> String {
return "\(name), \(age)세"
}
}
조건부 확장
extension Array where Element: Numeric {
func sum() -> Element {
return reduce(0, +)
}
}
let numbers = [1, 2, 3, 4, 5]
print(numbers.sum()) // 15
3. 프로토콜 기본 구현
protocol Greetable {
var name: String { get }
func greet()
}
extension Greetable {
func greet() {
print("안녕하세요, \(name)님!")
}
}
struct Person: Greetable {
let name: String
}
let person = Person(name: "홍길동")
person.greet() // 안녕하세요, 홍길동님!
4. 실전 예제
예제: 도형 시스템
protocol Shape {
func area() -> Double
func perimeter() -> Double
}
extension Shape {
func describe() {
print("넓이: \(area()), 둘레: \(perimeter())")
}
}
struct Circle: Shape {
let radius: Double
func area() -> Double {
return Double.pi * radius * radius
}
func perimeter() -> Double {
return 2 * Double.pi * radius
}
}
struct Rectangle: Shape {
let width: Double
let height: Double
func area() -> Double {
return width * height
}
func perimeter() -> Double {
return 2 * (width + height)
}
}
let shapes: [Shape] = [
Circle(radius: 5),
Rectangle(width: 10, height: 20)
]
for shape in shapes {
shape.describe()
}
실전 심화 보강
실전 예제: 네트워크 레이어 추상화 (URLSession 대역 테스트용)
프로토콜로 요청을 감싸면, 프로덕션은 실제 세션, 테스트는 목을 주입할 수 있습니다.
1단계: 요청을 나타내는 타입과 응답 프로토콜을 정의합니다.
import Foundation
protocol HTTPClientProtocol {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}
extension URLSession: HTTPClientProtocol {}
struct UserDTO: Codable {
let id: Int
let name: String
}
protocol UserProviding {
func fetchUser(id: Int) async throws -> UserDTO
}
struct RemoteUserService: UserProviding {
let baseURL: URL
let client: HTTPClientProtocol
func fetchUser(id: Int) async throws -> UserDTO {
let url = baseURL.appendingPathComponent("users/\(id)")
let req = URLRequest(url: url)
let (data, _) = try await client.data(for: req)
return try JSONDecoder().decode(UserDTO.self, from: data)
}
}
struct MockHTTPClient: HTTPClientProtocol {
let data: Data
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
(data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
}
}
2단계: 테스트에서 MockHTTPClient에 JSON 문자열을 넣어 디코딩 경로만 검증합니다.
자주 하는 실수
- 프로토콜에 연관 타입이 필요한데 일반 제네릭처럼 써서 컴파일 에러가 나는 경우.
extension으로 기본 구현을 줬는데 구체 타입에서 시그니처를 살짝 바꿔 의도치 않은 오버로드가 생기는 경우.Shape같은 프로토콜을 배열에 넣을 때any Shape(existential)와 제네릭의 차이를 모르고 성능·동적 디스패치를 혼동하는 경우.
주의사항
- Objective-C와 브리징 시 **
@objc protocol**은struct준수가 불가능합니다. - 프로토콜 확장의 기본 구현은
mutating요구사항과 조합할 때 제약이 까다롭습니다. 필요하면Self제약을 명시하세요.
실무에서는 이렇게
- 작은 프로토콜 여러 개(읽기 전용, 쓰기 전용)로 쪼개면 조합이 쉽고 테스트도 가벼워집니다.
- UI 레이어에는
ObservableObject+ 프로토콜로 ViewModel을 추상화해 프리뷰에 목 객체를 주입합니다. - 문서화를 위해 프로토콜 이름은
-able,-ing,Providing등 팀 컨벤션을 통일합니다.
비교 및 대안
| 방식 | 장점 | 단점 |
|---|---|---|
| 프로토콜 + 구조체 | 값 타입, 조합 용이 | 프로토콜 존재형 저장 시 주의 |
| 상속 기반 베이스 클래스 | 공통 코드 한곳 | UIKit 스타일 결합도↑ |
| 제네릭만 사용 | 정적 디스패치 | 타입 폭발 가능 |
추가 리소스
정리
핵심 요약
- Protocol: 인터페이스, 요구사항 정의
- Extension: 기존 타입에 기능 추가
- 기본 구현: 프로토콜 확장으로 제공
- POP: 상속 대신 프로토콜 조합
- 조건부 확장: where 절로 제약
다음 단계
- Swift 제네릭
- Swift 에러 처리
- Swift 비동기
관련 글
- Swift 시작하기 | iOS 개발 공식 언어 완벽 입문
- Swift 변수와 타입 | var, let, 옵셔널
- Swift 함수와 클로저 | 함수 정의, 클로저, 고차 함수
- Swift 클래스와 구조체 | Class, Struct, Enum
- Swift 제네릭 | Generic 함수, 타입, 제약