Swift/개념 & 응용

[Swift] Hashable

유정주 2022. 8. 14. 12:24
반응형

Hash

Hashable에 대해 알아보기 전에

Hash에 대해 알아야 합니다.

 

Hash란 해시 함수에 의해 얻어지는 값으로 해시값, 해시코드로도 불리는데요.

해시 함수는 특정 input을 넣었을 때 항상 일정한 output이 나오는 함수입니다.

위 사진처럼 "안녕하세요"가 input으로 들어가면 해시 로직을 거쳐 항상 동일한 output이 나와야 합니다.

 

input이 달라지면 output도 달라져야 하는데요.

로직에 따라 다른 input이라도 output이 같을 수 있습니다.

이때! 다른 output이 나오는 input의 개수가 많으면 많을수록 좋은 해시함수랍니다.

 

아무튼,

오늘 포스팅에서 중요한 해시의 성질은

input을 넣었을 때 "일정한" output이 나와야 한다는 점입니다.

이것만 기억해 주세요 ㅎ

 

Hashable

public protocol Hashable : Equatable {
    var hashValue: Int { get }
    func hash(into hasher: inout Hasher)
}

Hashable은 뒤에 able이 들어간 것에서 유추할 수 있듯이 프로토콜 중 하나로,

Hasher에 해시되어 정수 해시 값을 생성할 수 있는 타입입니다.

 

Hashable 프로토콜을 준수하는 모든 타입은 Set이나 Dictionary의 키로 사용할 수 있습니다.

 

여기서 위에서 강조했던 해시의 특성이 중요하게 적용되는데요.

var set: [String: String] = ["사과": "apple", "바나나": "banana"]
print(set["사과"]!)
print(set["바나나"]!)

"사과"를 input 하면 항상 "apple"이 나와야 하고,

"바나나"를 input 하면 항상 "banana"가 나와야 합니다.

 

이러한 해시 특성만 만족하면 Set과 Dictionary의 키로 쓸 수 있고,

이를 준수하도록 도와주는 것이 Hashable 프로토콜입니다.

 

Swift의 기본 타입은 기본적으로 Hashable 합니다.

각 타입에 dot(.)을 이용해 hashValue를 확인할 수 있고

"사과"의 hashValue를 출력해보면

print("사과".hashValue) //5239748680733565909
print("사과".hashValue) //5239748680733565909
print("사과".hashValue) //5239748680733565909
print("사과".hashValue) //5239748680733565909

모두 동일한 값을 가지는 것을 볼 수 있습니다.

 

Equatable

Equatable에 대한 내용은 여기에서 확인해 주세요.

 

Hashable은 Equatable을 채택하고 있습니다.

Swift의 기본 타입은 Hashable을 준수하므로 Equatable도 준수한다는 의미이므로

자연스럽게 == 를 쓸 수 있는 것입니다.

 

Hashable에서 Equatable은 hashValue가 고유값인지 확인하는 역할을 합니다.

또 해시 값이 같은데 객체가 다를 수도 있습니다. 해시 충돌 가능성이 있기 때문입니다.

그렇기 때문에 Equatable을 이용해 객체가 같은지 확인합니다.

 

Hashable 채택 & 준수하기

Hashable은 이름 옆에 Hashable을 적어서 채택할 수 있습니다.

구조체와 클래스는 약간의 차이가 있기 때문에 따로따로 알아볼게요.

 

구조체

struct Human {
    let name: String
    let age: Int
}

이 Human 구조체를 이용해 Dictionary를 만들어보겠습니다.

 

일단 Hashable을 채택하지 않고 Dictionary의 Key로 사용해봅시다.

Human이 Hashable을 준수하지 않는다는 컴파일 에러가 발생하네요.

 

구조체는 구조체의 프로퍼티가 모두 Hashable 하다면

Hashable을 채택하는 것만으로 추가 구현 없이 사용 가능합니다.

 

Human 구조체는 String, Int가 모두 기본 자료형이기 때문에 Hashable을 채택하는 것만으로

Hashable하게 사용할 수 있어요.

struct Human: Hashable {
    let name: String
    let age: Int
}

let humanDict: [Human: String] = [Human(name: "시리", age: 10): "애플"]
let siri = Human(name: "시리", age: 10)
print(humanDict[siri]!) //애플

 

기본 자료형이 아니더라도 Hashable 하기만 하면

Hashable 구조체의 프로퍼티로 사용할 수 있습니다.

struct Man: Hashable {
    let name: String
    let age: Int
    let girlFriend: Human
}

Human은 Hashable 하기 때문에

Hashable한 Man 구조체의 프로퍼티로 사용할 수 있습니다.

 

만약 Human이 Hashable 하지 않다면

컴파일 에러가 발생하고,

직접 Hashable을 준수해야 합니다.

 

클래스

클래스는 직접 Hashable을 준수해줘야 합니다.

 

위의 Human을 class로만 바꿔주면

이니셜라이저부터 구현하라고 나오네요 ㅎ;

이니셜라이저를 구현하고 나면

Hashable을 준수하지 않는다고 컴파일 에러가 나옵니다.

 

클래스에서 Hashable을 구현하기 위해서는 

Equable과 해시 함수를 직접 구현해줘야 합니다.

class Human {
    let name: String
    let age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

extension Human: Hashable {
    static func == (lhs: Human, rhs: Human) -> Bool {
        return (lhs.name == rhs.name) && (lhs.age == rhs.age)
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
        hasher.combine(age)
    }
}

== 연산자는 이름과 나이가 같으면 같은 객체로 판단하게 했습니다.

 

해시 함수 구현 방법은 hashValue의 주석에서 알 수 있었는데요.

hashValue를 직접 지정하지 말고 hash(into:)를 사용하라고 하네요!

그리고 hash(into:)를 보면 hasher.combine(_:)을 이용하라고 나와있습니다.

 

그래서 나온 코드가

func hash(into hasher: inout Hasher) {
    hasher.combine(name)
    hasher.combine(age)
}

이거인거죠.

 

Hashable을 모두 준수하면

let humanDict: [Human: String] = [Human(name: "시리", age: 10): "애플"]
let siri = Human(name: "시리", age: 10)
print(humanDict[siri]!) //애플

제대로 Dictionary의 키로 사용할 수 있게 됩니다.

 

name으로만 판단하면 어떻게 될까요?

지금은 name과 age 둘 다 이용해서 ==과 해시 함수를 구현했습니다.

name 하나로만 구현하면 어떻게 될까요?

extension Human: Hashable {
    static func == (lhs: Human, rhs: Human) -> Bool {
        return lhs.name == rhs.name
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
}

let humanDict: [Human: String] = [Human(name: "시리", age: 10): "애플"]
let siri = Human(name: "시리", age: 10)
print(humanDict[siri]!) //애플

일단 사용했던 코드는 잘 동작합니다.

 

근데 다른 Human도 이용해서 테스트하면 어떻게 될까요?

let siri = Human(name: "시리", age: 10)
let newSiri = Human(name: "시리", age: 1)

let humanDict: [Human: String] = [siri: "애플"]
print(humanDict[newSiri]!) //애플

newSiri는 시리와 이름이 똑같고 age가 1입니다.

딕셔너리에는 siri만 들어있죠.

그리고 newSiri를 출력하면 애플이 출력됩니다!

 

name이 같기 때문에 ==와 해시 함수가 siri와 newSiri가 같다고 판단해서

애플이 출력되는 것이죠.

 

즉 2개의 input에 대해 1개의 output이 나오게 되면서

서로 다른 Key에 간섭을 하게 되는 것입니다.

 

이런 코드의 문제는 

humanDict[newSiri] = "갤럭시"
print(humanDict[siri]!) //갤럭시

다른 키를 이용해 값을 업데이트가 될 수 있기 때문에

원치 않은 결과가 발생할 수 있습니다.

 

 

참고

https://developer.apple.com/documentation/swift/hashable

https://babbab2.tistory.com/149

https://zeddios.tistory.com/498


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형