Swift 프로토콜과 확장 | Protocol, Extension

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 스타일 결합도↑
제네릭만 사용정적 디스패치타입 폭발 가능

추가 리소스


정리

핵심 요약

  1. Protocol: 인터페이스, 요구사항 정의
  2. Extension: 기존 타입에 기능 추가
  3. 기본 구현: 프로토콜 확장으로 제공
  4. POP: 상속 대신 프로토콜 조합
  5. 조건부 확장: where 절로 제약

다음 단계

  • Swift 제네릭
  • Swift 에러 처리
  • Swift 비동기

관련 글

  • Swift 시작하기 | iOS 개발 공식 언어 완벽 입문
  • Swift 변수와 타입 | var, let, 옵셔널
  • Swift 함수와 클로저 | 함수 정의, 클로저, 고차 함수
  • Swift 클래스와 구조체 | Class, Struct, Enum
  • Swift 제네릭 | Generic 함수, 타입, 제약