Python 클래스가 __eq__를 정의하여 동등성 비교를 사용자 정의하면, 인터프리터는 명시적으로 재정의하지 않는 한 __hash__를 None으로 자동 설정합니다. 이로 인해 인스턴스가 해시 가능하지 않게 되어 dict의 키나 set의 멤버로 사용할 수 없게 됩니다. 기본 불변 조건에 따르면, __eq__를 통해 동등하게 비교된 객체는 동일한 해시 값을 반환해야 합니다. 이를 위반하면 해시 기반 컬렉션에서 정의되지 않은 동작이 발생합니다. 따라서 매핑 키로 그러한 객체를 사용하려고 하면 TypeError: unhashable type가 발생합니다.
개발 팀은 dict의 키로 User 객체를 사용하여 활성 세션을 저장하는 세션 관리 서비스를 구축하고 있었습니다. User 클래스는 user_id를 기준으로 인스턴스를 비교하도록 __eq__를 구현하여, 동일한 데이터베이스 사용자를 나타내는 두 개의 다른 객체가 동등하게 처리되도록 했습니다. 초기 구현은 다음과 같았습니다:
class User: def __init__(self, user_id, name): self.user_id = user_id self.name = name def __eq__(self, other): if not isinstance(other, User): return NotImplemented return self.user_id == other.user_id
초기에는 팀이 __hash__를 구현하지 않아 기본 동작만으로 충분할 것이라고 가정했습니다. 그러나 서비스가 cache[user] = session_data를 사용하여 세션을 캐시하려고 하자 Python은 **TypeError: unhashable type: 'User'**를 발생시키며 서비스를 중단시켰습니다.
팀은 세 가지 해결책을 고려했습니다. 첫 번째 접근법은 id(self)를 해시 값으로 사용하는 것이었습니다. 그러나 이는 중요 불변 조건을 위반하여 서로 다른 User 인스턴스가 동일한 user_id를 갖더라도 서로 다른 해시를 가질 수 있었기 때문에 거부되었습니다. 이로 인해 전혀 다른 키로 표시되어 캐시 검색이 완전히 중단되고 동일한 논리적 사용자에 대한 중복 항목이 허용되었습니다.
두 번째 접근법은 hash(self.user_id)를 해시 값으로 사용하는 것이었습니다. 이는 동등한 사용자들이 동일한 user_id를 공유하므로 불변 조건을 충족했습니다. 하지만 user_id가 변경된 후 객체가 사라지지 않도록 하기 위해서는 user_id가 변경 불가능해야 했습니다.
세 번째 옵션은 User 객체를 키로 사용하는 것을 포기하고 대신 문자열 user_id를 직접 사용하는 것이었습니다. 이는 안전하고 간단하지만 타입 안전성은 희생되었고 User 객체로의 ID 매핑을 유지해야 했기에 추가적인 조회 로직으로 코드베이스가 복잡해졌습니다.
팀은 두 번째 해결책을 선택하여 클래스에 다음과 같은 구현을 추가했습니다:
def __hash__(self): return hash(self.user_id)
그들은 또한 user_id를 읽기 전용 속성으로 만들어 불변성을 보장했습니다. 이로써 User 인스턴스를 키로 사용할 수 있는 능력을 유지하면서도 올바른 동등성 의미를 유지할 수 있었습니다. 그 결과로 사용자를 객체 인스턴스의 정체성에 관계없이 올바르게 식별하는 강력한 캐시가 구축되었습니다.
왜 __eq__가 정의되었으나 __hash__가 구현되지 않았을 때 Python이 자동으로 __hash__를 None으로 설정하나요?
클래스가 __eq__를 정의하면, 객체에서 상속받은 기본 아이덴티티 기반 해시가 논리적으로 무효화됩니다. 기본 __hash__는 id(self)에 의존하기 때문에 두 개의 서로 다른 객체는 서로 다른 해시를 가집니다. 만약 __eq__가 값을 비교하도록 재정의되면 서로 다른 인스턴스가 동일할 수 있지만 서로 다른 해시를 가지게 되어, a == b이면 hash(a) == hash(b)라는 기본 규칙을 위반하게 됩니다. Python은 이러한 불일치를 방지하기 위해 __hash__를 None으로 설정하여 클래스가 해시 불가능하다고 명시적으로 표시하며, 불안정한 기본 동작으로 인해 발생할 수 있는 위험한 상황을 방지합니다.
변경 가능한 객체가 변경 가능한 필드를 기반으로 해시를 구현한 후 사전 키로 사용될 경우 어떤 일이 발생하나요?
__hash__가 변경 가능한 상태에 의존하게 되면, 객체가 dict에 삽입된 후 해시 값이 변경될 수 있습니다. 사전은 삽입 시 해시 값에 따라 해시 버킷에 키를 저장합니다. 나중에 해시가 변하면 후속 조회는 다른 해시를 계산하고 다른 버킷을 검색하게 되어 원래의 키를 찾을 수 없게 됩니다. 객체는 메모리에 남아 있지만 정상적인 키 접근을 통해 찾거나 삭제할 수 없습니다. 이는 메모리 누수와 논리적 불일치를 초래하므로, Python은 해시 가능한 객체가 불변이거나 불변 식별자에 기반해야 한다고 요구합니다.
@dataclass 데코레이터가 __eq__와 __hash__ 생성을 어떻게 처리하며, unsafe_hash=True를 사용하는 위험은 무엇인가요?
기본적으로 @dataclass는 필드 값을 기반으로 __eq__를 생성하지만 __hash__를 None으로 설정하여 인스턴스가 해시 불가능하게 만듭니다. 이 보수적인 기본값은 변경 가능한 데이터 클래스와 관련된 버그를 방지합니다. 해시를 활성화하려면 frozen=True를 설정하거나 명시적으로 unsafe_hash=True를 설정해야 합니다. unsafe_hash=True 매개변수는 필드 값에 따라 해시를 생성하도록 Python에 강제할 수 있지만, 이는 객체가 사전 키로 사용된 후 필드가 변경되면 해시가 변경되고 키가 접근 불가능해지는 위험을 초래합니다. 후보자들은 종종 unsafe_hash가 단순한 경고가 아니라 사전 불변성을 깨는 기능적 위험이라는 점을 간과합니다.