애플사이다의 iOS 개발 일지

[Swift] Hashable 해야 한다? 해쉬값이란? (간단 요약) 본문

Swift/Swift 문법

[Swift] Hashable 해야 한다? 해쉬값이란? (간단 요약)

Applecider 2021. 9. 25. 17:50

안녕하세요. 애플사이다 입니다.

Swift Language Guide의 네 번째 챕터 Collection Types에 "Hashable"과 "해쉬값 (Hash Value)"이 등장합니다.

 

해쉬 개념을 제대로 이해하려면 해쉬 테이블 (Hash Table)이라는 자료구조에 대해 알아야 합니다.

그에 앞서, 이번 포스트에서는 "해쉬값"이 무엇인지 기초 개념을 정리해보겠습니다. 

 


해쉬값 (Hash Value) 이란?

- 데이터를 간단한 숫자로 변환한 것

원본 데이터를 특정 규칙에 따라 처리하여 간단한 숫자로 만든 것을 해쉬값이라고 한다. 

정확히는 원본 데이터 (객체)를 해쉬 함수 (hash function)을 사용하여 64bit의 Int값으로 변환한 것이다. 

 

2개의 데이터를 비교할 때, 데이터가 동일하면 각 데이터의 해쉬값도 동일하다.

하지만 데이터가 조금이라도 다르면, 완전히 다른 해쉬값을 가진다. (따라서 해쉬값을 통해 원본 데이터를 유추할 수 없다.)

let hi: String = "안녕하세요"
let hello: String = "안녕하세요"
let apple: String = "애플사이다와 함께 하는 Swift 공부"

print(hi.hashValue)    // 2897817589225975386 - 해쉬값
print(hello.hashValue) // 2897817589225975386
print(apple.hashValue) // 1147360803777020165
  • "안녕하세요"라는 데이터를 담고 있는 상수 hi 및 상수 hello의 해쉬값은 2897817589225975386로 동일하다.
    즉, 동일한 데이터는 동일한 해쉬값을 가진다.
  • 반면 "애플사이다와 함께 하는 Swift 공부"라는 데이터를 담고 있는 상수 apple의 해쉬값은 다르다.
  • 단, 코드를 컴파일 및 실행할 때마다 모든 해쉬값이 변경되므로 주의해야 한다.

✅ 단, 2개의 서로 다른 데이터가 동일한 해쉬값을 가질 수도 있다. 즉, "해쉬값이 동일하면, 2개의 데이터가 동일하다"는 참이 아닐 수 있다.

해쉬값은 일정 크기의 Int값이므로 유한하고, 데이터 양은 무한하기 때문에 해쉬값이 충분하지 않기 때문이다.

이 때문에 해쉬 충돌 (collision)이 발생한다. 해쉬 충돌은 다른 데이터 구조 (연결 리스트, 선형조사법 등)를 사용하여 해결할 수 있다.

 

해쉬값은 해쉬 테이블 (Hash Table)이라는 자료구조에서 사용하는 개념이다. 

Hashable 해야 한다는 것은 무슨 뜻일까?

- Hashable 한 타입의 데이터는 해쉬값 (hash value)을 구할 수 있다.

Swift Language Guide의 Collection Types 챕터에 이런 내용이 있다.

A type must be hashable in order to be stored in a set.

 

Set에 어떤 값을 저장하고 싶다면, "해당 값의 타입은 Hashable 해야 한다" 라는 뜻이다.

이때 Hashable 해야 한다는 것은 Hashable Protocol을 준수 (Conform)한다는 의미이다.

또한 Hashable 한 타입의 데이터는 해쉬값을 구할 수 있다.

 

Hashable Protocol을 준수하는 타입의 예시를 보자.

struct Drink: Hashable {
    let name: String
    let mainIngredient: String
    let alchol: Double
}
struct RandomData: Hashable {
    let string1: String
    let string2: String
    let double1: Double
}

let lemonade = Drink(name: "lemonade", mainIngredient: "lemon", alchol: 0.0)
let appleCider = Drink(name: "appleCider", mainIngredient: "apple", alchol: 5.0)

if lemonade == appleCider { // Drink 구조체가 Hashable Protocol을 준수하므로 두 인스턴스를 == 연산자로 비교 가능하다.
    print("Drinks are the same, because their hash values \(lemonade.hashValue) and \(appleCider.hashValue) are the same.")
} else {
    print("Drinks are different, because their hash values \(lemonade.hashValue) and \(appleCider.hashValue) are different.")
}
// "Drinks are different, because their hash values 498791461213300727 and 8360371392129835764 are different." 출력 - 다른 인스턴스이며, 서로 다른 해쉬값을 가진다.
// 주의 - 컴파일 할 때마다 모든 해쉬값이 변경된다.

let randomData = RandomData(string1: "appleCider", string2: "apple", double1: 5.0)
print(appleCider.hashValue) // 8360371392129835764
print(randomData.hashValue) // 8360371392129835764
// 다른 객체이지만, 동일한 해쉬값을 가진다. - 해쉬값 충돌

//if randomData == appleCider { // 컴파일 에러 - 객체의 타입이 다르므로 == 연산자 사용 불가
//    //...
//}
  • 사용자 정의 타입인 구조체 Drink는 원래 인스턴스끼리 == 연산자로 비교할 수 없다.
    하지만 구조체가 Hashable Protocol을 준수하도록 변경하면, 비교가 가능하다.
  • Hashable Protocol을 준수하므로 hashValue 프로퍼티를 통해 해쉬값을 확인할 수 있다. (lemonade.hasValue)
    서로 다른 인스턴스인 lemonade 및 appleCider의 해쉬값이 서로 다르다.
  • "해쉬값이 동일하면, 2개의 데이터가 동일하다"는 참이 아닐 수 있다. 참이 아닌 예를 randomData 인스턴스로 나타냈다.
    appleCider.hashValue는 randomData.hashvalue와 동일하다. 하지만 두 인스턴스는 서로 다르다.

참고 - Hashable Protocol의 정의는 아래와 같다.

Hashable Protocol은 Equatable Protocol을 준수하고 있다.

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

참고 - Equatable Protocol이란?

Swift 컴파일러가 == 연산자 또는 != 연산자를 사용할 때, Equatable Protocol 활용한다.

예를 들어 2개의 구조체 인스턴스를 == 연산자로 비교하면, 컴파일 오류가 발생한다. 이때 구조체가 Equatable Protocol를 준수하도록 설정하면, 비교가 가능하다.

public protocol Equatable { // Equatable Protocol의 정의
    static func == (lhs: Self, rhs: Self) -> Bool
}

Hashable 한 타입 / 해쉬값은 어디에 사용할까?

Swift에서 Set 타입의 값, Dictionary 타입의 Key은 Hashable 해야 한다. 

또한 Swift의 기본 타입인 String, Int 등은 자동으로 Hashable 하다. 

열거형 (Enum)이 연관값 (associated value)을 가지지 않으면, 자동으로 Hashable 하다.

예를 들면 Set의 값에 String 타입을 담을 수 있다. 그리고 사용자 정의 타입을 Hashable 하도록 설정하면, Set의 값에 해당 타입을 담을 수 있다.

 

해쉬값은 왜 사용할까? 값의 검색 속도가 빠르기 때문이다. (해쉬 테이블에 대한 이해가 필요하다.)

쉽게 말하자면, 원래는 처음부터 순서대로 값을 찾아야 한다. (선형 검색의 시간복잡도는 O(n)이다.)

반면, 해쉬 테이블에서는 해쉬값을 index로 사용하여 원하는 값의 위치를 한 번에 알 수 있다. (시간복잡도는 O(1)이다.)

 

참고 - 해쉬값은 블록체인 기술에도 사용된다.

만약 아주 복잡한 형태의 2개의 데이터가 있을 때, 차이가 아주 미세하다면 데이터가 동일한지 판단하기가 번거로울 수 있다.

이때 데이터의 해쉬값은 Int 타입의 비교적 간단한 형태이므로 해쉬값을 비교하여 데이터가 동일한지 빠르게 확인할 수 있다.

 

 

- Reference

 

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

Comments