"해시맵은 해시 함수만큼만 좋아. 나쁜 건 여전히 '정답' 이야 — 그냥 네 O(1) 꿈을 조용히 O(n) 연결 리스트로 되돌릴 뿐이야."
좋은 해시의 세 속성
해시 함수는 이래야 해:
- 결정론적 — 같은 키가 늘 같은 해시를 만들어. 이거 없으면 저장한 걸 못 찾아. 타협 불가.
- 균일 — 다른 키를 테이블에 고르게 퍼뜨려, 슬롯이 비슷한 속도로 차고 충돌이 드물게. 이게 O(1) 을 지켜.
- 빠름 — 계산이 싸야 (O(1) 에 가깝게), 안 그러면 직접 주소법으로 얻은 속도를 해시 자체가 먹어.
좋은 해시는 눈사태 (avalanche) 성질이 있어: 키를 살짝만 바꿔도 ("cat" vs "car") 해시가 완전히 다른 값으로 흩어져, 그게 정확히 비슷한 키가 같은 슬롯에 뭉치는 걸 막아.
나쁜 해시가 왜 조용한 살인자인가
최악의 합법적 해시 함수를 생각해: 모든 키에 return 0. 결정론적이고 빨라 — 그리고 재앙이야. 모든 키가 슬롯 0 에 떨어져서, "해시맵" 이 선형으로 훑는 거대한 체인 하나로 무너져. 조회가 O(n) 이고, 아무것도 망가져 보이지 않아; 코드는 '정답', 그냥 느릴 뿐. 그래서 뭉치는 해시 (많은 진짜 키를 적은 슬롯에 매핑) 가 에러 없이 성능을 조용히 파괴해. 해시 함수의 균일성이 O(1) 과 O(n) 의 차이야.
네가 존중해야 할 계약
해시 함수를 밑바닥부터 쓸 일은 드물어 — 근데 네 객체를 dict 키로 쓰는 순간, 계약을 물려받아: 두 객체가 같으면, 같은 해시를 가져야 한다. Python 에선 __eq__ 랑 __hash__ 가 일치해야 한다는 뜻. 어기면 — '같은' 두 객체가 다른 해시 — 맵이 그걸 다른 슬롯에 저장해서, 하나를 다른 걸로 절대 못 찾아. 같으면-해시도-같음이 해싱을 믿을 수 있게 만드는 규칙이야.
(여담: Python 은 문자열 해시를 프로세스별 무작위 시드로 소금 쳐서, hash("x") 가 실행마다 달라. 공격자가 다 충돌하는 키를 만드는 걸 막는 의도적 보안 조치야 — 이 트랙 마지막 lesson 에서 다시 봐.)
피파의 고백
__eq__ 를 정의하고, 거기 맞게 __hash__ 정의하는 걸 잊었어. 맵이 '같은' 두 객체를 다른 슬롯에 신나게 저장하고 조회하니 둘 다 없다고 맹세했어. 에러 없음 — 그냥 거짓말하는 딕셔너리. 아빠의 규칙, 그 뒤로 박혔어: "__eq__ 를 건드리면, __hash__ 를 건드려. 둘은 한 쌍이거나 버그거나."