Swift 25. 프로토콜 지향 프로그래밍

 Swift 25. 프로토콜 지향 프로그래밍




Swift 2.0 부터 Swift는 프로토콜 지향 언어


객체 지향 프로그래밍 패러다임에 기반을 둔 언어는 대부분 클래스의 상속을 사용해 타입에 공통된 기능을 구현


Swift 표준 라이브러리에서 타입과 관련된 내용은 대부분 구조체로 구현되어 있음

상속이 되지 않는 구조체로 다양한 공통 기능을 구현할 수 있었던 이유는 프로토콜과 익스텐샨, 제네릭을 잘 활용했기 때문





1. 프로토콜 초기 구현


프로토콜에서는 기능을 정의만 하고 구현하지는 않음 

해당 프로토콜을 채택한 모든 타입에 각각 모든 기능을 구현해줘야 하는데 그러면 코드 중복도 많이 일어나고 유지보수도 힘들어짐

프로토콜과 익스텐션을 결함하면 이러한 문제를 해결할 수 있음


프로토콜 초기구현 : 각각의 타입에 프로토콜의 요구사항을 구현하지 않고 프로토콜 익스텐션을 이요해서 구현

익스텐션에 프로토콜 이름을 작성하고 { } 안에 필요한 기능들을 구현하면 해당 프로토콜을 준수하는 모든 타입에 구현한 기능들이 추가됨

(저장 프로퍼티는 익스텐션으로 추가할 수 없으므로 모든 타입에 각각 구현해 줘야 함)



* 예시 - Receiveable, Sendable 프로토콜을 준수하는 Message, Mail 클래스를 프로토콜 익스텐션을 사용하지 않고 각각 필요한 기능을 구현

// 수신하는 기능

protocol Receiveable {

    // 메소드 요구

    func received(data: Any, from: Sendable)

}

// 발신하는 기능

protocol Sendable {

    // 읽기 전용 프로퍼티

    var from: Sendable { get }

    var to: Receiveable? { get } // 옵셔널

    

    // 메소드 요구

    func send(data:  Any)

    

    // 타입 메소드 요구 (발신 가능 인스턴스 여부 반환)

    static func isSendableInstance(_ instance: Any) -> Bool

}



// 수신, 발신이 모두 가능한 Message 클래스

class Message: Receiveable, Sendable {

    

    // 읽기 전용 프로퍼티로 구현

    var from: Sendable { // 발신이 가능한 Sendable 프로토콜을 준수하는 타입의 인스턴스

        return self

    }

    

    // 저장 프로퍼티로 구현

    var to: Receiveable? // 수신 가능한 Receiveable 프로토콜을 준수하는 타입의 인스턴스

    

    // 메세지 발신 메소드

    func send(data: Any) {

        guard let receiver: Receiveable = self.to else {

            // to 프로퍼티가 nil이면

            print("전송할 대상이 없음")

            return

        }

        // to 프로퍼티가 nil이 아니면

        // 수신 가능한 인스턴스의 received 메소드 호출

        receiver.received(data: data, from: self.from)

    }

    

    // 메시지 수신 메소드

    func received(data: Any, from: Sendable) {

        print("메시지가 도착했습니다.")

        print("\(from) :  \(data)")

    }

    

    // 상속 가능한 타입 메소드

    // 발신 가능한지 여부 반환

    class func isSendableInstance (_ instance: Any) -> Bool {

        if let sendableInstance: Sendable = instance as? Sendable {

            // 해당 인스턴스가 Sendable 프로토콜을 준수하면

            return sendableInstance.to != nil // to 프로퍼티가 nil이 아니면 true

        }

        return false

    }

}


// 수신, 발신이 가능한 Mail 클래스

class Mail: Receiveable, Sendable{

    // 읽기 전용 프로퍼티로 구현

    var from: Sendable  {

        return self

    }

    // 저장 프로퍼티로 구현

    var to: Receiveable? // 옵셔널

    

    // 메일 발신 메소드

    func send(data: Any) {

        guard let receiver: Receiveable = self.to else {

            // to 프로퍼티가 nil이면

            print("전송할 대상이 없음")

            return

        }

        // to 프로퍼티가 nil이 아니면

        // 수신 가능한 인스턴스의 received 메소드 호출

        receiver.received(data: data, from: self.from)

    }

    

    // 메일 수신 메소드

    func received(data: Any, from: Sendable) {

        print("메일이 도착했습니다.")

        print("\(from) :  \(data)")

    }

    

    // 상속 불가능한 타입 메소드

    // 발신 가능한지 여부 반환

    static func isSendableInstance (_ instance: Any) -> Bool {

        if let sendableInstance: Sendable = instance as? Sendable {

            // 해당 인스턴스가 Sendable 프로토콜을 준수하면

            return sendableInstance.to != nil // to 프로퍼티가 nil이 아니면 true

        }

        return false

    }

}



* 예시 - Receiveable, Sendable 프로토콜을 준수하는 Message, Mail 클래스를 프로토콜 익스텐션을 사용해서 구현

// 수신하는 기능

protocol Receiveable {

    // 메소드 요구

    func received(data: Any, from: Sendable)

}


// 프로토콜 익스텐션

extension Receiveable {

    // 메시지 수신 메소드

    func received(data: Any, from: Sendable) {

        print("메시지가 도착했습니다.")

        print("\(from) :  \(data)")

    }

}


// 발신하는 기능

protocol Sendable {

    // 읽기 전용 프로퍼티

    var from: Sendable { get }

    var to: Receiveable? { get } // 옵셔널

    

    // 메소드 요구

    func send(data: Any)

    

    // 타입 메소드 요구 (발신 가능 인스턴스 여부 반환)

    static func isSendableInstance(_ instance: Any) -> Bool

}


// 프로토콜 익스텐션

extension Sendable {

    // 읽기 전용 프로퍼티로 구현

    var from: Sendable { // 발신이 가능한 Sendable 프로토콜을 준수하는 타입의 인스턴스

        return self

    }

    

    // 메세지 발신 메소드

    func send(data: Any) {

        guard let receiver: Receiveable = self.to else {

            // to 프로퍼티가 nil이면

            print("전송할 대상이 없음")

            return

        }

        // to 프로퍼티가 nil이 아니면

        // 수신 가능한 인스턴스의 received 메소드 호출

        receiver.received(data: data, from: self.from)

    }

    

    // 발신 가능한지 여부 반환

    static func isSendableInstance (_ instance: Any) -> Bool {

        if let sendableInstance: Sendable = instance as? Sendable {

            // 해당 인스턴스가 Sendable 프로토콜을 준수하면

            return sendableInstance.to != nil // to 프로퍼티가 nil이 아니면 true

        }

        return false

    }

    

}




// 수신, 발신이 모두 가능한 Message 클래스

class Message: Receiveable, Sendable {

    

    // 저장 프로퍼티로 구현

    var to: Receiveable? // 수신 가능한 Receiveable 프로토콜을 준수하는 타입의 인스턴스

}


// 수신, 발신이 가능한 Mail 클래스

class Mail: Receiveable, Sendable{

    // 저장 프로퍼티로 구현

    var to: Receiveable? // 옵셔널

}

-> 해당 프로토콜을 준수하는 모든 타입에 각각 필요한 기능들을 구현해 주지 않아도 프로토콜 익스텐션에 필요한 기능들을 모두 구현해 주면(저장 프로퍼티 제외)  각각의 타입에는 저장 프로퍼티만 구현해서 프로토콜의 요구사항을 준수할 수 있음

=> 코드 중복을 줄일 수 있음





프로토콜 익스텐션에서 구현한 기능을 사용하지 않고 타입의 특성에 맞게 변경해서 구현하려면 제정의 하면 됨

컴파일러는 프로토콜을 준수하는 타입에 프로토콜의 요구사항을 찾아보고 있으면 해당 기능을 실행하고 없으면 프로토콜 익스텐션에서 찾아서 실행



* 예시 - Mail 클래스의 send 메소드 재정의

class Mail: Receiveable, Sendable{

    // 저장 프로퍼티로 구현

    var to: Receiveable? // 옵셔널

    

    // 메소드 재정의

    func send(data: Any) {

        print("재정의된 메소드 호출됨")

        print(data)

    }

}



var A: Mail = Mail()

A.send(data: "Hello")

<실행결과>

재정의된 메소드 호출됨

Hello

-> 프로토콜 익스텐션에서 구현한 메소드가 아닌 해당 타입에서 구현한 메소드가 호출됨





- 프로토콜 익스텐션과 제네릭의 결합


프로토콜 익스텐션에 제네릭을 더하면 코드의 재사용성은 더 좋아짐

제네릭은 타입에 한정되지 않도록 다양한 기능을 구현




* 예시 - 프로토콜 익스텐션과 제네릭을 이용한 스택과 큐 구현

// 자기 자신의 요소들을 프린트

protocol SelfPrintable {

    func printSelf()

}

// 프로토콜 익스텐션을 이용한 기능 구현

extension SelfPrintable where Self: Container {

    func printSelf() {

        print(items)

    }

}


// SelfPrintable 프로토콜을 상속받은 Container 프로토콜

protocol Container: SelfPrintable {

    // 연관 타입

    associatedtype ItemType

    

    // 아이템 배열

    var items: [ItemType] { get set }

    // 아이템 개수

    var count: Int { get }

    

    // 아이템 추가 메소드

    mutating func append(item: ItemType)

    

    // 정수형 서브스크립트

    subscript(i: Int) -> ItemType { get }

}

// 프로토콜 익스텐션을 이용한 기능 구현

extension Container {

    mutating func append(item: ItemType) {

        items.append(item)

    }

    

    var count: Int {

        return items.count

    }

    

    subscript(i: Int) -> ItemType {

        return items[i]

    }

}


// Constainer 프로토콜을 상속받는 스택에 필요한 기능을 정의한 Popable 프로토콜

protocol Popable: Container {

    // 마지막 요소 삭제하고 반환하는 메소드

    mutating func pop() -> ItemType?

    // 배열 마지막에 새 요소를 추가하는 메소드

    mutating func push(_ item: ItemType)

}

// 프로토콜 익스텐션을 이용한 기능 구현

extension Popable {

    mutating func pop() -> ItemType? {

        return items.removeLast()

    }

    

    mutating func push(— item: ItemType) {

        self.append(item: item)

    }

}


// Container 프로토콜을 상속받는 큐에 필요한 기능을 정한 Insertable 프로토콜

protocol Insertable: Container {

    // 첫번째 요소를 삭제하고 반환하는 메소드

    mutating func delete() -> ItemType?

    // 배열 마지막에 새 요소를 추가하는 메소드

    mutating func insert(_ item: ItemType)

}

// 프로토콜 익스텐션을 기용한 기능 구현

extension Insertable {

    mutating func delete() -> ItemType? {

        return items.removeFirst()

    }

    

    mutating func insert(_ item: ItemType) {

        self.append(item: item)

    }

}


// Popable 프로토콜을 준수하는 제네릭 타입 Stack

struct Stack<Element>: Popable {

    var items: [Element] = [Element]()

}


// Insertable 프로토콜을 준수하는 제네릭 타입 Queue

struct Queue<Element>: Insertable {

    var items: [Element] = [Element]()

}


익스텐션에 의한 프로토콜 초기 구현으로 해당 프로토콜을 채택한 것만으로 해당 프로토콜의 모든 요구사항 (저장 프로퍼티 제외)이 자동으로 구현됨

프로토콜 익스텐션을 통해 필요한 공통 기능을 미리 구현해서 나중에 따로 추가 구현할 필요가 없음

제네릭을 이용해서 각각의 요소의 타입을 사용할 때 결정해서 유연하게 동작

Swift의 클래스는 다중 상속을 지원하지 않기때문에 부모 클래스에 기능이 부족하면 자식 클래스에 기능을 다시 구현해야 했지만 프로토콜 초기 구현을 통해 해당 필요한 기능들을 요구하는 프로토콜을 채택하기만 하면 됨

프로토콜 초기구현을 통해 프로토콜을 채택하는 것 만으로 해당 타입에 기능을 추가해서 사용할 수 있음


Swift의 많은 기능들도 프로토콜, 익스텐션, 제네릭의 조합으로 구현되어 있음







2. 맵, 필터, 리듀스 구현


위에서 구현한 Stack에 맴, 필터, 리듀스 메소드 구현



(1) 맵 구현

맵 : 컨테이너가 담고 있던 각각의 값을 매개변수를 통해 받은 함수에 적용하고 그 결과를 다시 컨테이너에 포장해서 반환


* 예시

extension Stack {

    func map<T>(transform: (Element) -> T) -> Stack<T> {

        // 반환할 새로운 컨테이너 생성

        var transformedStack: Stack<T> = Stack<T>()

        

        // 각각의 요소를 전달받은 함수에 대입

        for item in items {

            transformedStack.items.append(transform(item))

        }

        // 결과 반환

        return transformedStack

    }

}


// Int 형 Stack 생성해서 요소 추가

var myStack: Stack<Int> = Stack<Int>()

myStack.push(1)

myStack.push(2)

myStack.push(3)

myStack.printSelf()


// map 메소드 실행

var newStack: Stack<String> = myStack.map{"\($0)"} // 각 요소를 String으로 변환

newStack.printSelf()

<실행결과>

[1, 2, 3

["1", "2", "3"]



(2) 필터 구현

필터 : 컨테이너 내부의 값을 걸러서 추출해 새로운 컨테이너에 담아서 반환

매개변수로 반환타입이 Bool인 함수 전달


* 예시

extension Stack {

    func filter(includeElement: (Element) -> Bool) -> Stack<Element> {

        // 반환할 새로운 컨테이너 생성

        var filteredStack: Stack<Element> = Stack<Element>()

        

        // 각각의 요소를 전달받은 함수에 대입

        for item in items {

            if includeElement(item) {

                filteredStack.items.append(item)

            }

        }

        // 결과 반환

        return filteredStack

    }

}


// Int 형 Stack 생성해서 요소 추가

var myStack: Stack<Int> = Stack<Int>()

myStack.push(1)

myStack.push(2)

myStack.push(3)

myStack.printSelf()


// filter 메소드 실행

var newStack: Stack<Int> = myStack.filter{ (item: Int) -> Bool in

    // 홀수 요소만 추출

    return item % 2 == 1

}

newStack.printSelf()

<실행결과>

[1, 2, 3]

[1, 3]



(3) 리듀스 구현

리듀스 : 컨테이너 내부의 콘텐츠를 하나로 합쳐줌

반환타입은 컨테이너의 요소의 타입과 같음


* 예시

extension Stack {

    func reduce<T>(_ first: T, nextPartialResult: (T, Element) -> T) -> T {

        // 초기값 지정

        var result = first

        

        // 각각의 요소를 전달받은 함수에 대입

        for item in items {

            result = nextPartialResult(result, item)

        }

        // 결과 반환

        return result

    }

}


// Int 형 Stack 생성해서 요소 추가

var myStack: Stack<Int> = Stack<Int>()

myStack.push(1)

myStack.push(2)

myStack.push(3)

myStack.printSelf()


// reduce 메소드 실행

let combinedInt = myStack.reduce(100){ (result: Int, next: Int) -> Int in

    return result + next

}

print(combinedInt)


let combinedDouble = myStack.reduce(100.0){ (result: Double, next: Int) -> Double in

    return result + Double(next)

}

print(combinedDouble)


let combinedString = myStack.reduce("") { (result: String, next: Int) -> String in

    return result + " \(next)"

}

print(combinedString)

<실행결과>

[1, 2, 3]

106

106.0

 1 2 3






3. 기본 타입 확장


프로토콜 초기구현을 통해 Swift의 기본타입을 확장해서 원하는 기능을 공통적으로 추가할 수 있음

Swift 표준 라이브러리에 정의되어있는 실제 코드를 보고 수정할 수 없기 때문에 익스텐션, 프로토콜, 프로토콜의 초기구현을 사용해 기본 타입에 기능을 추가



* 예시 - Int, Double, String에 자기 자신을 출력하는 printSelf() 메소드 추가

// 자기 자신을 출력하는 메소드를 요구하는 프로토콜 정의

protocol SelfPrint {

    func printSelf()

}


// 프로토콜의 익스텐션을 이용해서 해당 기능 구현

extension SelfPrint {

    func printSelf() {

        print(self)

    }

}


// Int, Double, String 타입에 익스텐션을 이용해서 SelfPrint 프로토콜을 채택]

extension Int: SelfPrint { }

extension Double: SelfPrint { }

extension String: SelfPrint { }


// 프로토콜 초기구현으로 기능을 구현해서 해당 프로토콜을 준수하는 것만으로 해당 기능을 사용할 수 있음

123.printSelf()

3.14.printSelf()

"Hello World".printSelf()

<실행결과>

123

3.14

Hello World


-> Swift의 기본타입에 프로토콜 초기구현을 통해 공통 기능을 추가









댓글

이 블로그의 인기 게시물

Spring Boot 6. 계시판 프로젝트

Spring Boot 2. Maria 데이터베이스 & Spring Data JPA

Django 10. DB 연동