Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

루비 해시(Hash)의 숨겨진 고급 기법 완벽 가이드

루비 개발자들은 해시(Hash)를 워낙 자주 사용하다 보니, 이제 모든 기능을 다 알고 있다고 착각하기 쉽습니다.

하지만 겉보기에 단순해 보이는 루비 해시에는 아직 우리가 모르는 강력한 기능이 숨어 있습니다. 해시는 단순한 키-값 저장소가 아니라, 매우 흥미롭고 정교한 작업을 수행할 수 있게 해주는 강력한 도구입니다.

모든 객체가 해시 키가 될 수 있습니다

본격적으로 살펴보기 전에, 놓치기 쉬운 한 가지 사실을 짚고 넘어가겠습니다. 우리는 주로 문자열과 심볼을 해시 키로 사용하지만, 그렇다고 다른 종류의 객체를 키로 쓸 수 없다는 뜻은 아닙니다. 실제로 거의 모든 객체를 해시 키로 활용할 수 있습니다.

# 숫자도 해시 키가 될 수 있습니다
{1 => "one"}[1] # "one"

# 루비 커널 모듈도 가능합니다
{Kernel => 1}[Kernel] # 1

# 특정 클래스별로 값을 저장할 수도 있습니다
{Kernel => 1, String => 2}["hello world".class] # 2

# 불리언 값도 키로 사용할 수 있습니다
{true => "verdad"}[1==1] # "verdad"

# 복잡한 배열이나 다른 해시조차 키가 될 수 있습니다
{[[1,0],[0,1]] => "identity matrix"}[[[1,0], [0,1]]] # "identity matrix"

일부는 실무에서 자주 쓰이지 않을 수 있지만, 모두 얼마든지 활용 가능한 옵션입니다.

기본값을 직접 제어할 수 있습니다

해시 h = { a: 1 }가 있다고 가정해 봅시다. 존재하지 않는 키에 접근하면 — 예를 들어 h[:x] — nil이 반환됩니다. 별도로 지정하지 않는 한 nil이 모든 해시의 기본값이기 때문입니다.

생성자에 인수를 전달하면 새 해시의 기본값을 직접 설정할 수 있습니다.

h = Hash.new("This attribute intentionally left blank")
h[:a] = 1
h[:a] # 1
h[:x] # "This attribute intentionally left blank"

동적 기본값 생성하기

여기서부터 집중하세요. 이 기술이 바로 이후에 소개할 모든 내용의 기초가 되는 핵심입니다.

생성자에 블록을 전달하면 기본값을 코드로 동적으로 생성할 수 있습니다. 아래 예제에서는 기본값에 타임스탬프를 추가해, 값이 매번 새로 생성된다는 것을 확인할 수 있도록 했습니다.

h = Hash.new { |hash, key| "#{key}: #{ Time.now.to_i }" }
h[:a] # "a: 1435682937"
h[:a] # "a: 1435682941"
h[:b] # "b: 1435682943"

중요한 점은 이 "기본값" 블록이 단순히 기본값을 반환하는 것 외에도 다양한 작업을 수행할 수 있다는 것입니다.

존재하지 않는 키에 접근할 때 예외 발생시키기

해시의 가장 큰 문제 중 하나는 조용히 실패한다는 점입니다. 실수로 user[:phnoe]라고 입력했는데 user[:phone]을 입력해야 했다면, 예외를 발생시키는 대신 해시는 그저 nil을 반환할 뿐입니다. 하지만 이 동작은 변경할 수 있습니다.

h = Hash.new { |hash, key| raise ArgumentError.new("No hash key: #{ key }") }
h[:a]=1
h[:a] # 1
h[:x] # ArgumentError: No hash key: x 예외 발생

이 기술은 특정 해시에만 적용되기 때문에 디버깅과 리팩토링에 특히 유용합니다. Hash 클래스를 몽키패칭하는 방식보다 훨씬 덜 침습적으로 원하는 동작을 추가할 수 있습니다.

참고: 새로 작성하는 코드에서 이 방법이 Hash.fetch를 대체해야 한다는 의미는 아닙니다. 디버깅과 리팩토링 시 활용할 수 있는 흥미로운 트릭 정도로 기억해 두면 좋습니다.

지연 생성(Lazy) 조회 테이블 만들기

이 기술은 반복적인 계산 결과를 캐싱할 때 매우 유용합니다. 수많은 제곱근을 계산해야 한다고 상상해 보세요. 아래와 같이 필요할 때마다 값을 채워 나가는 지연 생성 조회 테이블을 만들 수 있습니다.

sqrt_lookup = Hash.new { |hash, key| hash[key] = Math.sqrt(key) }
sqrt_lookup[9] # 3.0
sqrt_lookup[7] # 2.6457513110645907
sqrt_lookup    # {9=>3.0, 7=>2.6457513110645907}

재귀적 지연 조회 테이블

재귀 함수의 각 호출 결과를 캐싱하고 싶다면 어떻게 해야 할까요? 팩토리얼 계산을 예로 들어 보겠습니다. "4 팩토리얼", 즉 "4!"는 "4x3x2x1"을 다르게 표현한 것입니다. 해시를 활용하면 이를 재귀적으로 우아하게 구현할 수 있습니다.

factorial = Hash.new do |h,k| 
  if k > 1
    h[k] = h[k-1] * k
  else
    h[k] = 1
  end
end

factorial[4] # 24
factorial    # {1=>1, 2=>2, 3=>6, 4=>24}

초기화 이후에도 기본값 변경하기

해시가 이미 생성된 후에도 기본값을 제어할 수 있습니다. 이때는 defaultdefault_proc 세터 메서드를 사용합니다.

h={}
h[:a] # nil
h.default = "new default"
h[:a] # "new default"

h.default_proc = Proc.new { Time.now.to_i }
h[:a] # 1435684014

루비 찾기: 무한히 중첩되는 해시 게임

재미 삼아, 지금까지 배운 모든 기술을 하나의 극도로 쓸모없는 예제에 담아 보겠습니다. 오래된 텍스트 기반 게임 '어드벤처(Adventure)'를 기억하시나요? 그 역대급으로 단순한 버전을 만들어 보겠습니다.

당신은 동굴 안에 있다고 상상해 보세요. 북(n), 남(s), 동(e), 서(w) 네 방향으로 이동할 수 있습니다. 네 선택지 중 세 개는 계속 탐험을 이어갈 수 있는 새로운 "방"으로 연결됩니다. 하지만 단 하나의 선택지만이 "루비"로 이어집니다. 그래서 이 게임의 이름은 "루비 찾기(Find the Ruby)"입니다.

동굴의 각 방은 하나의 해시에 해당합니다. 각 해시에는 엔트리가 하나만 있으며, ["n", "s", "e", "w"] 중 무작위로 선택된 키가 "You found the ruby!"라는 값을 가집니다. 플레이어가 틀린 방향을 선택하면 새로운 해시가 생성되어 트리에 추가됩니다.

generator = Proc.new do |hash, key| 
  hash[key] = Hash.new(&generator).merge(["n", "s", "e", "w"][rand(4)] => "You found the ruby!")
end
dungeon = Hash.new(&generator)
dungeon["n"] # <Hash ...
dungeon["n"]["s"] # <Hash ...
dungeon["n"]["s"]["w"] # "You found the ruby!"