Swift의 기본 프로토콜 세 가지: Equatable, Comparable, Printable

애플의 공식 스위프트 가이드(?)에 언급된 세 가지 프로토콜로 Equatable, Comparable, Printable이 있다. '-able' 이라는 이름이 붙은 걸로 보면 특정 동작이 가능하도록 유도하는 프로토콜로 추측이 가능하다. 이 프로토콜들에 대해 간략히 정리해본다.

Equatable

Equatable 프로토콜은 Equal 이라는 것을 구현하기 위해 사용한다. 스위프트에서는 '==' 연산자가 Equal 연산자다. 다만 이 프로토콜은 '==' 오퍼레이터 오버로드가 필요하다.
struct MyData1: Equatable {
    var value1: Int = 0
    var value2: Int = 0
}

func == (lhs: MyData1, rhs: MyData1) -> Bool {
    return (lhs.value1 == rhs.value1 && lhs.value2 == rhs.value2)
}

let v11 = MyData1(value1: 10, value2: 20)
let v12 = MyData1(value1: 20, value2: 40)
let v13 = MyData1(value1: 10, value2: 20)

v11 == v12    // false
v11 == v13    // true
그런데 프로토콜임에도 별도로 구현해야 하는 메소드나 프로퍼티가 없다는 건 참으로 의아하다. 거기다, 실제로 Equatable 을 빼버려도 '==' 오퍼레이터 오버로드를 통해 동일한 동작이 된다.

Comparable과 함께 쓸 용도가 아니라면 굳이 별도로 선언하지 않아도 되는 프로토콜이다. 그냥 '==' 오퍼레이터 오버로드를 강제하기 위한 용도라고 생각하자.

Comparable

이름처럼 Compare, 즉 수치 비교가 가능하게 하는 것을 구현하는 용도이다. 그런데 이 녀석도 Equatable 처럼 별도로 구현하는 메소드나 프로퍼티가 없다. 그리고, 이 녀석은 반드시 Equatable을 함께 사용해야 한다는 제약이 붙어있다.
struct MyData2: Equatable, Comparable {
    var value: Int = 0
}

func < (lhs: MyData2, rhs: MyData2) -> Bool {
    return lhs.value < rhs.value
}

func == (lhs: MyData2, rhs: MyData2) -> Bool {
    return lhs.value == rhs.value
}

let v21 = MyData2(value: 10)
let v22 = MyData2(value: 20)
let v23 = MyData2(value: 10)

v21 < v22  // true
v21 > v22  // false
v21 < v23  // false
v21 > v23  // false
이번에도 별도의 메소드나 프로퍼티 구현 없이 오퍼레이터 오버로드를 이용해 동작한다.

그런데 오퍼레이터 오버로드 하는 부분을 보자. '<' 만 구현이 되어있다. 하지만 구현하지 않은 '>' 오퍼레이터도 동작하는 것을 확인 할 수 있다.

이는 '<' 와 '==' 오퍼레이터를 오버로드 함으로써 자연스럽게 '>' 는 별도의 오버로드가 필요없다는 점 때문이다. 간단히 생각해보자. 수치가 '작지도 않고 같지도 않으면 당연히 크다는 의미' 가 되니까 당연하다.

정확히 말하자면, Comparable 프로토콜은 '<' 연산자와의 비교를 위해 존재하는 프로토콜이다. 그래서 반드시 '< 오퍼레이터를 오버로드 하도록 강제한다. 물론 Equatable도 함께 필수이기 때문에 함께 '==' 도 오버로딩 해 놓았다. 이 상태 만으로 '>' 오퍼레이터의 오버로드는 필요가 없다. 이것이 Comparable의 특징이다.

물론 Comparable과 Equatable 프로토콜을 사용하지 않는다면 '>' 오퍼레이터도 오버로드 해야 정상적으로 동작할 것이다.

Printable

이름대로 인쇄 가능하게, 단순하게 말해서 오브젝트를 문자열로 바꾸는 기능을 제공하는 프로토콜이다. 다행히도(?) 이 녀석은 특정 프로퍼티를 구현하는 식으로 동작한다.
struct MyType: Printable {
    var value: Int = 0
   
    var description: String {
        return "MyType.value = \(value)"
    }
}

let v3 = MyType(value: 50)
v3                      // MyType.value = 50
println(v3)             // MyType.value = 50
여기서 구현한 description 이라는 String 프로퍼티의 getter가 Printable의 핵심이자 전부라고 봐도 된다. 이 녀석은 println이나 NSLog 등을 통해 로그로 남기거나 혹은 문자열 시리얼라이제이션(Serialization)이 필요할 때 요긴하게 사용 할 수 있다.

Objective-C를 사용해 봤다면 NSObject의 description 메소드를 구현하는 것을 알테니 이와 동일하다는 것도 눈치챌 수 있을 것이다. 단지 스위프트에서는 Printable 이라는 프로토콜에 별도로 정의가 되어있다는 점만 차이가 있다.

물론 위 사유로 인해 만약 특정 클래스가 NSObject를 상속받고 있다면 Printable의 description 프로퍼티는 오버라이드 표기가 있어야만 한다.
struct MyType: NSObject, Printable {
    // ...
    override var description: String {
        return “..."
    }
}
어쩌면 위 예제의 경우에는 Printable 프로토콜을 선언에서 빼도 문제가 없을지도 모르겠다.

[관련글] Swift - 오퍼레이터 오버로드(Operator Overloads)
[관련글] Swift - 프로토콜(Protocols)
[관련글] 스위프트(Swift) 가이드

댓글

이 블로그의 인기 게시물

소수점 제거 함수 삼총사 ceil(), floor(), round()

버전(Version)을 제대로 이해하기