2018년 1월 26일 금요일

Swift struct 값의 프로토콜을 제대로 판단하지 못 하는 문제

아직 세상은 넓고도 험하다. 코딩을 하던 도중 아래 오류의 문제가 제법 골머리를 썩였다.
Could not cast value of type ‘_SwiftValue’ to ‘SomeProtocolName’
이 글은 위의 오류와 관련된(?) 문제를 해결하던 과정에서 나온 오랜만의 삽질을 정리한 내용이다.

이 문제가 발생하게 된 코드를 자세히 서술하지 못 하는 것은 유감이다. 대신 간략하게 아래와 같이 예제를 써 보았다.
protocol SomeType { 
    ... 
}

struct Anything: SomeType { 
    ... 
}
SomeType 이라는 프로토콜을 하나 만들고, 이 프로토콜을 따르는 Anything 이라는 타입을 하나 만들었다. 물론 여기까지는 별 문제가 없는 내용이다.

프로토콜이 쓰인 경우 아래와 같은 식으로 특정 값이나 오브젝트가 프로토콜을 만족하는지 파악하는게 가능하다.
let a = Anything()

a is SomeType    // true

let b = a as! SomeType
'is' 오퍼레이터로 Anything 타입의 값인 a 가 SomeType 을 따르는지 파악이 가능하다. 여기서는 당연히 무조건 true 가 된다. 그리고 그 아래줄의 강제 캐스팅(as!)의 경우도 문제 없이 동작한다.

그런데 만약 이 'is' 나 'as!' 로 프로토콜을 파악하거나 다루는게 고장난다면 어떻게 해야할까?

문제 (Problems)

개인적으로 NSOutlineView 를 사용하는 프로젝트에서 문제를 발견했다. 우선 가정으로 NSOutlineViewDataSource 프로토콜로 정의된 메소드 중 아래와 같은 식으로 아이템을 넘겨주는 코드를 구현했다고 치자.
func outlineView(_ outlineView: NSOutlineView, 
                 child index: Int, 
                 ofItem item: Any?) -> Any? {
    return Anything()
}
너무 생략하긴 했지만 위에서 선언한 Anything 타입의 인스턴스를 차일드라고 넘겨주려는 의도의 코드이다. 이렇게 되면 역시 NSOutlineViewDataSource 에 정의된 메소드 중 하나인 아래 코드 구현체를 통해 위에서 넘긴 Anything 타입의 인스턴스 item 이 넘어온다.
func outlineView(_ outlineView: NSOutlineView, 
                 isItemExpandable item: Any) -> Bool {
    return item is SomeType     // always false
}
이 isItemExpandable 의 경우 특정한 타입일 때 이 아래에는 다양한 아이템들이 더 들어있다 라는 것을 알려주기 위한 메소드이다. 그런데 여기서 'is' 로 프로토콜을 따르는지 체크해보면 항상 false 가 돌아왔다.

디버거를 이용해 item 이 무슨 타입인지 확인해보면 'ProjectName.Anything' 이라는 확실한 타입이 표시되었다. 앞서 올린 예제대로 SomeType 이라는 프로토콜을 만족시키는 타입이다. 그런데 왜 실패하는 것일까?

이해가 안되어서 위의 메소드에 아래 라인을 하나 추가해 봤다.
let _ = item as! SomeType    // SIGABRT
강제캐스팅을 이용해 확인해 보려는 것인데, 우려와 같이 이 라인은 SIGABRT 즉 프로그램을 죽여버렸다. 오류 메시지는 제일 위에서 언급했던 바로 이 내용이다.
Could not cast value of type ‘_SwiftValue’ to ‘SomeProtocolName’
_SwiftValue 라는 표현은 아마도 Swift 의 Value 형식, 즉 구조체(struct) 인스턴스를 의미하는 것 같다. 즉 해당 프로토콜 형식으로 다이나믹 캐스팅을 할 수 없다는 오류다. 결과적으로 item 이 SomeType 프로토콜을 따르고 있지 않다는 말이다. 도데체 왜?!

해결 (Solutions)

해답부터 적어보자면, Anything 선언부분을 약간 고쳐야 된다.
class Anything: SomeType {
    ...
}
뭐가 바꼈는지 알 수 있을 것이다. struct 가 아닌 class 로 바꾸었다.

이 후로 위의 프로토콜을 파악하는 과정의 문제가 사라졌다.

해설 (Comments)

구글링을 해 본 결과 이 문제에 대한 명확한 애플 공식 문서의 내용을 찾지 못 해서 추측성 이야기만 해야 할 것 같다.

위 문제에서 특징이 있다면 NSOutlineView 가 쓰였다는 점이다. 내 코드에서 만들어서 던져준 값(Anything)이 NSOutlineView 를 통해서 다시 내 코드 쪽으로 돌아오는 과정에서 문제가 생겼다.

그리고 NSOutlineView 는 Objective-C 로 쓰여진 macOS용 프레임워크(Cocoa) 모듈이다.

불행히도 Objective-C 는 Swift struct 값을 Any 즉 id 형식으로 캐스팅 하는 순간 잃어버린다 라고 표현이 가능할 것 같다. 그래서 Anything 이 딜리게이트를 통해 NSOutlineView 에 전달되고 돌아오는 과정에서 Anything 타입의 값이 가지고 있던 프로토콜 정보를 읽어버리게 된다.

이유는 Objective-C 에는 Swift 의 struct 타입에 대응하는 기능이 없기 때문이다. 이게 핵심인 것 같다. (너무 단순하게 이야기 했는데, Objective-C 의 Dynamic Dispatch 와 관련이 있다고 생각된다)

그래서 Anything 의 타입을 class 로 바꾸는 순간 Objective-C 와의 호환성이 생기게 되고 프로토콜 정보 또한 유연하게 넘어가게 된다. 그래서 프로토콜 파악 또한 가능해지게 된다. (@objc 를 붙여도 해결 된다고도 하는데, struct 에는 @objc 를 붙일 수가 없잖아...)

이상이 개인적으로 이해하고 있는 내용이다.

잡설

본론은 끝났고 잡설.

애플 이 XXX야 이런거나 빨리 좀 해결해라. 기껏 struct 니 value type 이니 Protocol Orient Programming 이니 뭐니 좋은 개념 만들어 놓고 정작 쓸 수 없게 만들어 놓으면 어떡하자는 거냐. 내 아까운 4시간을 돌려줘! 부들부들...

[관련글] Swift - 프로토콜(Protocols)
[관련글] 스위프트(Swift) 가이드

댓글 없음 :