Rust 해시 맵으로 키 - 값 쌍 저장하기

Beginner

This tutorial is from open-source community. Access the source code

소개

해시 맵 (Hash Maps) 에서 연관된 값과 함께 키 저장하기에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.

이 랩에서는 해시 맵의 개념과 연관된 값과 함께 키를 저장하는 방법에 대해 알아보겠습니다.

해시 맵 (Hash Maps) 에서 연관된 값과 함께 키 저장하기

마지막으로 살펴볼 일반적인 컬렉션은 *해시 맵 (hash map)*입니다. HashMap<K, V> 타입은 *해싱 함수 (hashing function)*를 사용하여 K 타입의 키를 V 타입의 값에 매핑하여 저장합니다. 이 해싱 함수는 이러한 키와 값을 메모리에 배치하는 방식을 결정합니다. 많은 프로그래밍 언어에서 이러한 종류의 데이터 구조를 지원하지만, hash, map, object, hash table, dictionary, 또는 associative array 등과 같이 다른 이름을 사용하는 경우가 많습니다.

해시 맵은 벡터 (vector) 처럼 인덱스를 사용하여 데이터를 찾는 대신, 임의의 타입일 수 있는 키를 사용하여 데이터를 찾고 싶을 때 유용합니다. 예를 들어, 게임에서 각 팀의 점수를 해시 맵에 추적할 수 있습니다. 여기서 각 키는 팀 이름이고 값은 각 팀의 점수입니다. 팀 이름이 주어지면 해당 점수를 검색할 수 있습니다.

이 섹션에서는 해시 맵의 기본 API 를 살펴볼 것이지만, 표준 라이브러리의 HashMap<K, V>에 정의된 함수에는 더 많은 유용한 기능이 숨겨져 있습니다. 언제나 그렇듯이, 자세한 내용은 표준 라이브러리 문서를 참조하십시오.

새로운 해시 맵 생성하기

빈 해시 맵을 생성하는 한 가지 방법은 new를 사용하고 insert로 요소를 추가하는 것입니다. Listing 8-20 에서는 BlueYellow라는 이름을 가진 두 팀의 점수를 추적하고 있습니다. Blue 팀은 10 점으로 시작하고 Yellow 팀은 50 점으로 시작합니다.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

Listing 8-20: 새로운 해시 맵을 생성하고 일부 키와 값을 삽입하기

먼저 표준 라이브러리의 collections 부분에서 HashMapuse해야 합니다. 세 가지 일반적인 컬렉션 중에서 이 해시 맵은 가장 덜 사용되므로, prelude 에서 자동으로 범위 내로 가져오는 기능에 포함되지 않습니다. 해시 맵은 또한 표준 라이브러리에서 지원이 적습니다. 예를 들어, 해시 맵을 생성하기 위한 내장 매크로가 없습니다.

벡터와 마찬가지로 해시 맵은 데이터를 힙 (heap) 에 저장합니다. 이 HashMapString 타입의 키와 i32 타입의 값을 갖습니다. 벡터와 마찬가지로 해시 맵은 동질적 (homogeneous) 입니다. 즉, 모든 키는 동일한 타입을 가져야 하고, 모든 값은 동일한 타입을 가져야 합니다.

해시 맵에서 값에 접근하기

Listing 8-21 에 표시된 것처럼, get 메서드에 키를 제공하여 해시 맵에서 값을 가져올 수 있습니다.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

Listing 8-21: 해시 맵에 저장된 Blue 팀의 점수에 접근하기

여기서 score는 Blue 팀과 관련된 값을 가지며, 결과는 10이 됩니다. get 메서드는 Option<&V>를 반환합니다. 해시 맵에 해당 키에 대한 값이 없으면 getNone을 반환합니다. 이 프로그램은 Option을 처리하기 위해 copied를 호출하여 Option<&i32> 대신 Option<i32>를 얻은 다음, unwrap_or를 사용하여 scores에 해당 키에 대한 항목이 없는 경우 score를 0 으로 설정합니다.

벡터에서와 유사한 방식으로 for 루프를 사용하여 해시 맵의 각 키 - 값 쌍을 반복할 수 있습니다.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

for (key, value) in &scores {
    println!("{key}: {value}");
}

이 코드는 각 쌍을 임의의 순서로 출력합니다.

Yellow: 50
Blue: 10

해시 맵과 소유권

i32와 같이 Copy 트레이트를 구현하는 타입의 경우, 값은 해시 맵으로 복사됩니다. String과 같은 소유된 값의 경우, 값은 이동되고 해시 맵이 해당 값의 소유자가 됩니다. 이는 Listing 8-22 에서 보여줍니다.

use std::collections::HashMap;

let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try
// using them and see what compiler error you get!

Listing 8-22: 삽입된 후 키와 값이 해시 맵에 의해 소유됨을 보여줍니다.

insert 호출을 통해 변수 field_namefield_value가 해시 맵으로 이동된 후에는 사용할 수 없습니다.

값에 대한 참조를 해시 맵에 삽입하는 경우, 값은 해시 맵으로 이동되지 않습니다. 참조가 가리키는 값은 해시 맵이 유효한 기간 이상 동안 유효해야 합니다. 이러한 문제에 대해서는 "생명주기를 사용하여 참조 유효성 검사"에서 자세히 설명합니다.

해시 맵 업데이트하기

키 - 값 쌍의 수는 증가할 수 있지만, 각 고유한 키는 한 번에 하나의 값만 연결될 수 있습니다 (그러나 그 반대는 아닙니다. 예를 들어, Blue 팀과 Yellow 팀 모두 scores 해시 맵에 10 값을 저장할 수 있습니다).

해시 맵의 데이터를 변경하려는 경우, 키에 이미 값이 할당된 경우를 어떻게 처리할지 결정해야 합니다. 이전 값을 완전히 무시하고 새 값으로 바꿀 수 있습니다. 이전 값을 유지하고 새 값을 무시하여 키에 이미 값이 없는 경우에만 새 값을 추가할 수 있습니다. 또는 이전 값과 새 값을 결합할 수 있습니다. 각 방법을 살펴보겠습니다!

값 덮어쓰기

해시 맵에 키와 값을 삽입한 다음, 동일한 키를 다른 값으로 삽입하면 해당 키와 관련된 값이 대체됩니다. Listing 8-23 의 코드가 insert를 두 번 호출하더라도, 해시 맵은 하나의 키 - 값 쌍만 포함합니다. 이는 Blue 팀의 키에 대한 값을 두 번 모두 삽입하기 때문입니다.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);

println!("{:?}", scores);

Listing 8-23: 특정 키에 저장된 값을 대체하기

이 코드는 {"Blue": 25}를 출력합니다. 원래 값인 10이 덮어쓰여졌습니다.

키가 존재하지 않는 경우에만 키와 값 추가하기

특정 키가 해시 맵에 이미 값과 함께 존재하는지 확인한 다음 다음 작업을 수행하는 것이 일반적입니다. 키가 해시 맵에 존재하면 기존 값은 그대로 유지되어야 합니다. 키가 존재하지 않으면 키와 해당 값을 삽입합니다.

해시 맵에는 이를 위한 특별한 API 인 entry가 있으며, 확인하려는 키를 매개변수로 사용합니다. entry 메서드의 반환 값은 Entry라는 열거형 (enum) 으로, 존재할 수도 있고 존재하지 않을 수도 있는 값을 나타냅니다. Yellow 팀의 키에 연결된 값이 있는지 확인하려는 경우를 가정해 보겠습니다. 값이 없으면 값 50을 삽입하고, Blue 팀에 대해서도 동일하게 수행하려고 합니다. entry API 를 사용하면 코드는 Listing 8-24 와 같습니다.

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);

println!("{:?}", scores);

Listing 8-24: 키에 이미 값이 없는 경우에만 삽입하기 위해 entry 메서드 사용

Entryor_insert 메서드는 해당 키가 존재하면 해당 Entry 키에 대한 값에 대한 가변 참조를 반환하도록 정의되어 있으며, 그렇지 않으면 매개변수를 이 키에 대한 새 값으로 삽입하고 새 값에 대한 가변 참조를 반환합니다. 이 기술은 자체적으로 로직을 작성하는 것보다 훨씬 깔끔하며, 또한 borrow checker 와 더 잘 작동합니다.

Listing 8-24 의 코드를 실행하면 {"Yellow": 50, "Blue": 10}이 출력됩니다. entry에 대한 첫 번째 호출은 Yellow 팀에 대한 키를 값 50과 함께 삽입합니다. Yellow 팀에는 이미 값이 없기 때문입니다. entry에 대한 두 번째 호출은 Blue 팀에 이미 값 10이 있기 때문에 해시 맵을 변경하지 않습니다.

이전 값을 기반으로 값 업데이트하기

해시 맵의 또 다른 일반적인 사용 사례는 키의 값을 조회한 다음 이전 값을 기반으로 업데이트하는 것입니다. 예를 들어, Listing 8-25 는 텍스트에서 각 단어가 몇 번 나타나는지 계산하는 코드를 보여줍니다. 단어를 키로 사용하고 값을 증가시켜 해당 단어를 몇 번 보았는지 추적하는 해시 맵을 사용합니다. 단어를 처음 보는 경우 먼저 값 0을 삽입합니다.

use std::collections::HashMap;

let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

println!("{:?}", map);

Listing 8-25: 단어와 개수를 저장하는 해시 맵을 사용하여 단어의 발생 횟수 계산

이 코드는 {"world": 2, "hello": 1, "wonderful": 1}을 출력합니다. 동일한 키 - 값 쌍이 다른 순서로 출력될 수 있습니다. "해시 맵에서 값에 접근하기"에서 해시 맵을 반복하는 것은 임의의 순서로 발생한다는 것을 기억하십시오.

split_whitespace 메서드는 text의 값에서 공백으로 구분된 하위 슬라이스에 대한 반복자를 반환합니다. or_insert 메서드는 지정된 키에 대한 값에 대한 가변 참조 (&mut V) 를 반환합니다. 여기서 해당 가변 참조를 count 변수에 저장하므로 해당 값에 할당하려면 먼저 별표 (*) 를 사용하여 count를 역참조해야 합니다. 가변 참조는 for 루프의 끝에서 범위를 벗어나므로 이러한 모든 변경 사항은 안전하며 borrow checker 에 의해 허용됩니다.

해싱 함수 (Hashing Functions)

기본적으로 HashMap은 해시 테이블과 관련된 서비스 거부 (DoS, denial-of-service) 공격에 대한 저항성을 제공할 수 있는 SipHash라는 해싱 함수를 사용합니다. 이는 사용 가능한 가장 빠른 해싱 알고리즘은 아니지만, 성능 저하와 함께 제공되는 더 나은 보안을 위한 트레이드 오프는 그만한 가치가 있습니다. 코드를 프로파일링하고 기본 해시 함수가 목적에 비해 너무 느리다고 판단되면 다른 해셔 (hasher) 를 지정하여 다른 함수로 전환할 수 있습니다. 해셔BuildHasher 트레이트를 구현하는 타입입니다. 트레이트와 이를 구현하는 방법에 대해서는 10 장에서 다룰 것입니다. 반드시 처음부터 자체 해셔를 구현할 필요는 없습니다. https://crates.io에는 많은 일반적인 해싱 알고리즘을 구현하는 해셔를 제공하는 다른 Rust 사용자가 공유한 라이브러리가 있습니다.

요약

축하합니다! 해시 맵에서 연관된 값으로 키 저장하기 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.