2025/07/19

11. Swift 프로토콜(Protocol): 규약을 정의하고 기능을 확장하는 설계도

11. Swift 프로토콜(Protocol) 썸네일 이미지
안녕하세요! 지난 블로그에서는 인스턴스를 안전하게 생성하고 해제하는 초기화(Initialization)에 대해 자세히 알아보았죠. 오늘은 Swift 프로그래밍에서 매우 중요하며, Swift를 더욱 Swift답게 만드는 핵심 개념인 프로토콜(Protocol)에 대해 깊이 파고들 것입니다.

프로토콜은 마치 건축물의 '설계도'와 같습니다. 실제 건물을 짓기 전에, 어떤 구조를 가지고 어떤 기능을 해야 할지 미리 약속해두는 것처럼, 프로토콜은 특정 기능을 수행하기 위해 타입이 갖춰야 할 요구사항(규약)을 정의합니다. 클래스, 구조체, 열거형은 이 프로토콜을 '채택(Adopt)'하여 설계도에 명시된 요구사항을 실제로 구현합니다.

프로토콜을 통해 우리는 코드의 유연성과 확장성을 크게 높일 수 있으며, 다형성(Polymorphism)을 구현하는 데 상속보다 더 강력하고 안전한 대안을 제공합니다.

1. 프로토콜(Protocol)이란 무엇인가요?

프로토콜은 특정 역할을 수행하기 위해 타입(클래스, 구조체, 열거형)이 구현해야 하는 메서드, 프로퍼티, 서브스크립트 등 일련의 요구사항을 정의하는 청사진입니다. 프로토콜 자체는 이러한 요구사항을 구현하지 않고, 단지 '이런 것들을 가지고 있어야 한다'고 명시만 합니다.

💡 프로토콜을 사용하는 이유:

  • 공통 기능 정의: 여러 타입이 공유해야 하는 공통적인 동작이나 속성을 규약으로 정의할 수 있습니다.
  • 다형성: 프로토콜 타입을 통해 여러 다른 타입의 인스턴스를 동일한 방식으로 다룰 수 있게 합니다. 이는 유연하고 확장 가능한 아키텍처를 만드는 데 필수적입니다.
  • 코드 유연성: 구체적인 타입 대신 프로토콜에 의존하여 코드를 작성함으로써, 나중에 새로운 타입이 추가되어도 기존 코드를 수정할 필요가 없거나 최소화할 수 있습니다.
  • 모듈화: 각 타입이 어떤 책임을 가지는지 명확하게 정의하여 코드의 역할을 분리하고 모듈화를 촉진합니다.

1.1. 프로토콜의 기본 정의

protocol 키워드를 사용하여 프로토콜을 정의합니다. 프로퍼티는 var 또는 let 키워드 뒤에 get (읽기 가능) 또는 get set (읽기/쓰기 가능)을 명시해야 합니다. 메서드는 구현 없이 선언만 합니다.

//Swift 예제
// 이름을 가질 수 있는 모든 타입이 준수해야 할 프로토콜
protocol Named {
    var name: String { get set } // 읽고 쓸 수 있는 String 타입의 name 프로퍼티 요구
    var fullName: String { get } // 읽기 전용 String 타입의 fullName 프로퍼티 요구
    func sayHello() // sayHello 메서드 요구
}

// 달릴 수 있는 모든 타입이 준수해야 할 프로토콜
protocol Runnable {
    var speed: Double { get }
    func run()
    func stop()
}

2. 프로토콜 채택(Adoption) 및 준수(Conformance)

클래스, 구조체, 열거형은 하나 이상의 프로토콜을 채택(Adopt)하고, 해당 프로토콜이 요구하는 모든 사항을 준수(Conform), 즉 구현해야 합니다. 채택은 타입 이름 뒤에 콜론(:)을 붙이고 프로토콜 이름을 나열하여 표현합니다. 상속(: Superclass)과 함께 사용할 경우, 수퍼클래스 이름 뒤에 프로토콜을 나열합니다.

//Swift 예제
// Person 구조체가 Named 프로토콜을 채택하고 준수
struct Person: Named {
    var name: String
    var age: Int // Named 프로토콜에 없는 추가 프로퍼티

    // fullName은 계산 프로퍼티로 구현
    var fullName: String {
        return "\(name) (나이: \(age))"
    }

    func sayHello() {
        print("안녕하세요, 제 이름은 \(name)입니다.")
    }

    // 구조체 고유의 메서드
    func celebrateBirthday() {
        print("생일 축하해요, \(name)! 이제 \(age + 1)살이 되겠네요.")
    }
}

let jake = Person(name: "Jake", age: 30)
jake.sayHello()       // 출력: 안녕하세요, 제 이름은 Jake입니다.
print(jake.fullName) // 출력: Jake (나이: 30)
jake.celebrateBirthday() // 출력: 생일 축하해요, Jake! 이제 31살이 되겠네요.

// Car 클래스가 Runnable 프로토콜을 채택하고 준수
class Car: Runnable {
    var currentSpeed: Double = 0.0

    var speed: Double { // Runnable 프로토콜 요구사항: speed
        return currentSpeed
    }

    func run() { // Runnable 프로토콜 요구사항: run()
        currentSpeed = 60.0
        print("자동차가 시속 \(currentSpeed)km로 달립니다.")
    }

    func stop() { // Runnable 프로토콜 요구사항: stop()
        currentSpeed = 0.0
        print("자동차가 멈췄습니다.")
    }
}

let myCar = Car()
myCar.run()  // 출력: 자동차가 시속 60.0km로 달립니다.
myCar.stop() // 출력: 자동차가 멈췄습니다.

3. 프로토콜을 타입으로 사용하기

프로토콜은 타입처럼 사용될 수 있습니다. 즉, 변수, 상수, 함수의 매개변수나 반환 타입으로 프로토콜 타입을 지정할 수 있습니다. 이는 어떤 특정 타입의 인스턴스가 아니라, '어떤 프로토콜을 준수하는 어떤 타입의 인스턴스'라도 받을 수 있음을 의미합니다. 이것이 바로 프로토콜의 진정한 힘, 즉 다형성(Polymorphism)을 구현하는 방법입니다.

//Swift 예제
// Named 프로토콜을 준수하는 어떤 타입이라도 받을 수 있는 함수
func introduce(person: Named) {
    print("소개합니다: \(person.fullName)")
    person.sayHello()
}

let alice = Person(name: "Alice", age: 25)
introduce(person: alice) // 출력: 소개합니다: Alice (나이: 25)\n안녕하세요, 제 이름은 Alice입니다.

// 새로운 타입 User가 Named 프로토콜을 준수한다면 introduce 함수 사용 가능
struct User: Named {
    var name: String
    var email: String
    var fullName: String { return "\(name) (\(email))" }
    func sayHello() { print("반갑습니다, 저는 \(name)입니다.") }
}

let bob = User(name: "Bob", email: "bob@example.com")
introduce(person: bob) // 출력: 소개합니다: Bob (bob@example.com)\n반갑습니다, 저는 Bob입니다.

introduce 함수는 Person 인스턴스든, User 인스턴스든, Named 프로토콜만 준수한다면 어떤 타입의 인스턴스라도 받아들일 수 있습니다. 이는 코드의 유연성을 극대화합니다.

4. 프로토콜 확장 (Protocol Extensions): 기본 구현 제공하기

Swift의 프로토콜 확장(Protocol Extensions)은 프로토콜에 새로운 메서드나 연산 프로퍼티의 기본 구현(Default Implementation)을 추가할 수 있게 해줍니다. 이는 프로토콜을 채택하는 모든 타입이 자동으로 해당 기능을 가지게 되어 코드 중복을 줄이고 재사용성을 높이는 데 기여합니다.
//Swift 예제
// Named 프로토콜에 인사말 메서드의 기본 구현 추가
extension Named {
    func customGreeting() {
        print("👋 (프로토콜 확장): 안녕하세요, \(name)님!")
    }
}

let charlie = Person(name: "Charlie", age: 40)
charlie.customGreeting() // 출력: 👋 (프로토콜 확장): 안녕하세요, Charlie님!

let david = User(name: "David", email: "david@example.com")
david.customGreeting() // 출력: 👋 (프로토콜 확장): 안녕하세요, David님!

5. 프로토콜 지향 프로그래밍 (Protocol-Oriented Programming, POP)

Swift는 객체 지향 프로그래밍(OOP) 기능(클래스, 상속 등)도 지원하지만, Apple은 프로토콜 지향 프로그래밍(POP)을 적극적으로 권장합니다. POP는 상속 대신 프로토콜과 프로토콜 확장을 사용하여 코드 재사용성과 유연성을 높이는 패러다임입니다.
  • 상속의 한계: 단일 상속만 가능하며, 깊은 상속 계층은 복잡성을 증가시키고 유연성을 저해할 수 있습니다.
  • 프로토콜의 장점: 다중 프로토콜 채택이 가능하며, 값 타입(구조체, 열거형)에서도 기능을 확장할 수 있어 더 많은 타입에 적용 가능합니다.
POP는 코드를 더 모듈화하고 테스트하기 쉽게 만들며, 예상치 못한 부작용을 줄이는 데 도움이 됩니다.

정리하며

오늘은 Swift 프로그래밍의 핵심이자 강력한 설계 도구인 프로토콜(Protocol)에 대해 자세히 알아보았습니다.
  • 프로토콜: 타입이 갖춰야 할 요구사항(메서드, 프로퍼티 등)의 청사진을 정의합니다.
  • 채택 및 준수: 클래스, 구조체, 열거형이 프로토콜을 채택하고 모든 요구사항을 구현합니다.
  • 다형성: 프로토콜을 타입으로 사용하여 다양한 타입을 유연하게 다룰 수 있습니다.
  • 프로토콜 확장: 프로토콜에 기본 구현을 제공하여 코드 재사용성을 높입니다.
  • 프로토콜 지향 프로그래밍(POP): Swift에서 상속보다 선호되는 패러다임으로, 유연하고 확장 가능한 코드 작성을 돕습니다.
프로토콜은 처음에는 다소 추상적으로 느껴질 수 있지만, Swift 개발의 깊은 단계로 나아갈수록 그 중요성을 더욱 절감하게 될 것입니다. 오늘 배운 내용을 바탕으로 다양한 프로토콜을 직접 정의하고 채택해보면서 Swift의 강력한 기능을 경험해보세요!

다음 시간에는 Swift의 오류 처리(Error Handling) 방식에 대해 알아보겠습니다.

궁금한 점이 있다면 언제든지 댓글로 남겨주세요!

0 comments:

댓글 쓰기