View

300x250

이번에 야곰 리펙토링 강의를 들으면서 Swift의 “프로토콜” 이 다른 언어에 비해 굉장히 특이한 컨셉을 가지고 있다는 걸 알았다. 어떻게 보면 abstract class나 interface를 통해 구현하는 OOP의 개념들이 프로토콜에 담겨있는데, 이게 구조체나 enum 타입에 모두 적용되게 한다는게 꽤나 놀라운 일이라고 생각된다. 구조체는 값 타입이라며!!! enum도 값 타입이라며!!!

 

내용이 나왔던 김에 강의에서 소개해준 관련된 WWDC 영상들을 보면서 어떤 컨셉으로 설계되었고, 어떻게 동작하는 건지 공부하면서 한 번 정리해보려고 한다.


What I Learned

이번에 본 [ WWDC 15 의 Protocol-Oriented Programming in Swift ] 는 이미 많은 사람들이 블로그에 정리해둔 스위프트 WWDC의 기본같은 영상인 것 같다.

 

프로토콜에 대한 내용을 쭉 들어봤을 때, class의 상속관계를 사용할 때 보다 기존 코드의 확장이 필요한 시점에 프로토콜을 활용하면 원본 코드를 건드리지 않으면서 훨씬 더 자유롭게 확장을 할 수 있고, 다른 인스턴스와 상호작용을 하는 경우 해당 인스턴스의 타입이나 채택한 프로토콜에 따라 동작을 분기할 수도 있도록 설계되어있다. 요것 뿐만 아니라 클래스의 첫 설계 시점에 상속할 부모 클래스를 선택하는 것과 다르게 extension과 프로토콜 채택을 통해 “기능 확장이 필요한 시점에 채택이 이루어질 수 있다”는 것도 확장성에서 큰 장점으로 작용되는 특징이라고 느꼈다.

 

내부적인 동작 방식에 대해서는 또 다른 WWDC 영상에서 소개가 되고있는데, [ Understanding Swift Performance ] 라는 영상도 곧 들어볼 예정이다! 기대기대


우리가 클래스를 사용하는 이유

Encapsulation, Access Control, Abstraction, Namespace 분리, Expressive Syntax, Extensibility 등의 목적으로 클래스를 사용하도록 배웠다.

그러나 여기에 있는 목적들은 사실 Struct를 통해서도 만들어낼 수 있다.

 

클래스의 단점?

  • Implicit Sharing
    • reference를 통한 값 접근 방식에 의해 race condition, lock 등의 문제가 발생할 수 있음
    • 그치만, 사실 swift의 collection 들은 모두 값 타입으로 구성되어 있기 때문에 요런게 문제가 되지는 않음 ㅎㅎ
  • 상속의 불편한 점
    • 단일 상속으로 인해 하나의 부모 클래스만 가질 수 있기 때문에, 클래스의 기능 확장 시점이 아닌 처음 만들 때 부모 클래스를 잘 선택해야함
    • 자식 클래스 init 시점에 부모 클래스의 init이 모두 호출되어야함
    • 요것도 사실은 swift에서는 delegate pattern으로 컨트롤 해주고 있는 문제
  • 타입 관계
    • 자식 클래스에서 부모 클래스의 함수를 끌어다 사용하더라도, 자식의 프로퍼티를 활용하기 어려움 (냄새나는 코드를 작성하기 쉬움)
class Ordered {
    func precedes (other: Ordered) -> Bool { fatalError("implement me!") }
}

class Label: Ordered { var text: String = "" ⋯ }

class Number : Ordered {
    var value: Double = 0

    override func precedes (other: Ordered) -> Bool {
        // other가 Number인지 확인해야함
        // -> type safety hole
        return value ‹ (other as! Number).value
    }
}

 

더 나은 추상화 방법이 필요하다

  • 값 타입을 지원해야함
  • 정적 타입 관계 (static type relationship)과 동적 디스패치(dynamic dispatch)를 지원해야함
    • 요건 바로 위의 타입 관계에서의 문제 해결을 말함
  • 단일 상속이 아닌 복수개의 기능을 가져올 수 있어야 함
  • retroactive modeling이 가능해야함
    • 원본을 수정하지 않은 채로 확장판을 만들어낼 수 있어야 함
  • 모델에게 데이터 초기화와 인스턴스 데이터에 대한 책임을 넘기지 않아야 함
  • 무엇을 구현해야 하는지 정확하게 명시해야함

⇒ 이를 위해 Swift 에서는 Protocol 을 사용한다.

 

프로토콜 활용해서 개선하기

위에서 봤던 코드를 프로토콜을 사용하는 방식으로 개선하자면 아래처럼 된다.

protocol Ordered {
    // 프로토콜은 함수를 구현하지 않음
    // Self를 통해 프로토콜이 아닌 프로토콜을 채택한 구현체의 타입을 반영할 수 있음
    func precedes (other: Self) -> Bool 
}

class Label: Ordered { var text: String = "" ⋯ }

// protocol은 구조체에서도 채택할 수 있음
// -> ref로 인해 발생하는 문제 제거 가능
struct Number : Ordered {
    var value: Double = 0

    override func precedes (other: Number) -> Bool {
        return value ‹ other.value
    }
}

Self 처럼 타입을 고정하거나 Generic을 통해 하나의 타입을 가진 Array로 강제할 수 있다. 이를 Homogeneous Collection 이라고 부름!

// 기존 방식에서는 sortedKeys에 Ordered를 상속받는 여러 타입이 
// 하나의 배열에 섞여서 한 번에 들어오는 문제가 발생할 수 있음
func binarySearch(sortedKeys: [Ordered], forKey k: T) -> Int { ... }

// swift에서 generic을 사용해주면 protocol의 함수이더라도 
// 프로토콜의 구현체 하나의 타입만 들어오도록 만들 수 있음
func binarySearch<T : Ordered>(sortedKeys: [T], forKey k: T) -> Int { ... }

프로토콜을 사용하면 장점

  • 기존 코드를 건드리지 않고 확장 수정이 가능하다 (retroactive modeling)
    • 기존 코드에 “새로운 기능을 추가한다”는 코드를 넣지 않더라도 쉽게 확장이 가능함
       
     

protocol newFeature {
	func 새로운기능() -> 반환값
}

extension originFeature: newFeature {
	func 새로운기능() -> 반환값 { 구현 }
}
  • 확장에 대한 컨트롤이 쉽다
protocol MyProtocol {
	func 새로운기능() -> 반환값
}

extension MyProtocol {
	func 새로운기능() -> 반환값 { 구현1 }
	func 또다른기능() -> 반환값 { 구현2 }
} 

extension OriginFeature: myProtocol {
	func 새로운기능() -> 반환값 { 구현3 }
	func 또다른기능() -> 반환값 { 구현4 }
}

// 함수 실행 시 OriginFeature의 Extension에서 함수를 찾아 실행한다.
let origin = OriginFeature()
origin.새로운기능() => 구현3 실행
origin.또다른기능() => 구현4 실행

// MyProtocol 타입으로 정한다면, MyProtocol에서 함수 명세를 찾게됨
// 이때 또다른 기능은 MyProtocol에 정의되어있지 않으므로, 
// MyProtocol의 Extension에서 함수를 찾아 실행하게 된다.
let new: MyProtocol = OriginFeature()
new.새로운기능() => 구현3 실행
new.또다른기능() => 구현2 실행

 

    • 사실 요건 이번에 WWDC를 보면서 처음 알게된 방식임… 오 신기하다 ㅋㅋㅋㅋ
    •   extension MyType where T : BType {
            func indexOf(element: T) -> Index? { ... }
        }
      
        -> T가 BType을 따를 때에만 MyType에서 indexOf 함수를 사용할 수 있음
    • retroactive와 조건부 확장을 함께 쓰면 이런 것도 가능함
        protocol Drawable { 
            func isEqualTo(other: Drawable) -> Bool
            func draw()
        }
      
        extension Drawable where Self : Equatable {
            // Equatable인 타입만 받아왔기 때문에, == 연산자는 사용이 가능한 상황
            // 이때, Drawable 끼리의 비교가 필요한 상황!
            // 파라미터를 Self 타입으로 받아버리면 같은 타입끼리만 비교 가능 ... X
      
            func isEqualTo(other: Drawable) -> Bool {
                // 만약 현재 인스턴스와 other 인스턴스가 같은 타입이라면
                // == 을 통해 값을 비교하기
                if let o = other as? Self { return self == o }
                // 만약 다른 경우에는 비교 의미가 없으므로 false 반환
                return false
            }
        }
    • 프로토콜을 채택한 타입에 대한 정보를 가져와 함수 실행에 영향을 줄 수도 있음

protocol Drawable { 
	func isEqualTo(other: Drawable) -> Bool
	func draw()
}

extension Drawable where Self : Equatable {
	// Equatable인 타입만 받아왔기 때문에, == 연산자는 사용이 가능한 상황
	// 이때, Drawable 끼리의 비교가 필요한 상황!
	// 파라미터를 Self 타입으로 받아버리면 같은 타입끼리만 비교 가능 ... X
	
	func isEqualTo(other: Drawable) -> Bool {
		// 만약 현재 인스턴스와 other 인스턴스가 같은 타입이라면
		// == 을 통해 값을 비교하기
		if let o = other as? Self { return self == o }
		// 만약 다른 경우에는 비교 의미가 없으므로 false 반환
		return false
	}
}

 

클래스는 언제 써야하냐

  • Implicit Sharing이 필요할 때 (copy가 아닌 reference로 접근이 필요할 때)
  • 외부 환경에 인스턴스의 lifetime이 엮여있는 경우 (외부에서 수정이 가능 … reference)
  • 인스턴스가 단순히 write-only conduit 목적(데이터 전달용)으로 사용되는 경우
320x100
Share Link
reply
반응형
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30