컬렉션
애플리케이션의 데이터에 사용할 데이터 구조를 결정할 때, 스토리지에 읽고 쓰는 데이터의 양을 최소화하는 것이 중요하지만 트랜잭션 비용을 최소화하기 위해 직렬화 및 역직렬화되는 데이터의 양도 최소화해야 합니다. 애플리케이션이 확장되고, 상태를 새 데이터 구조로 마이그레이션하면 비용이 발생하고, 병목 현상이 발생할 수 있으므로, 스마트 컨트랙트에서 데이터 구조의 장단점을 이해하는 것이 중요합니다.
near-sdk
내 컬렉션은 데이터를 청크로 분할하고 필요할 때까지 스토리지에 대한 읽기 및 쓰기를 연기하도록 설계되었습니다. 이러한 데이터 구조는 저수준 스토리지 상호 작용을 처리하고, std::collections
와 유사한 API를 갖는 것을 목표로 합니다..
near_sdk::collections
는 near_sdk::store
로 이동하여 업데이트된 API를 가질 예정입니다. 이는 구현 중이고, 이러한 업데이트된 구조에 액세스하려면 near-sdk
에서 unstable
기능을 활성화하세요.
std::collections
를 사용할 때, 상태가 로드될 때마다 자료 구조의 모든 항목이 스토리지에서 지속적으로 읽고 역직렬화된다는 점을 염두에 두는 것이 중요합니다. 이것은 적지 않은 양의 데이터에 대해서도 큰 비용이 드는 작업이기 때문에, 사용되는 가스의 양을 최소화하기 위해 대부분의 경우 SDK 컬렉션을 사용해야 합니다.
최신 컬렉션과 관련된 문서는 rust 문서에서 찾을 수 있습니다.
SDK에 존재하는 다음 데이터 구조는 다음과 같습니다.
SDK 컬렉션 | 해당하는 std | 설명 |
---|---|---|
LazyOption<T> | Option<T> | 스토리지의 선택적 값입니다. 이 값은 상호 작용할 때만 스토리지에서 읽어 옵니다. 이 값은 스토리지에 값이 저장되어 있으면 Some<T> , 접두사에 값이 존재하지 않는 경우 None 입니다. |
Vector<T> | Vec<T> | 확장 가능한 배열 유형입니다. 값은 메모리에서 샤딩되며, 동적으로 크기가 조정되고, 반복 및 인덱싱 가능한 값에 사용할 수 있습니다. |
LookupMap | HashMap | 이 구조는 컨트랙트에 사용할 수 있는 키-값 스토리지를 둘러싼 얇은 래퍼 역할을 합니다. 이 구조에는 맵의 요소에 대한 메타데이터가 포함되어 있지 않으므로 반복할 수 없습니다. |
UnorderedMap | HashMap | 데이터 구조의 요소를 반복할 수 있도록 추가 데이터를 저장한다는 점을 제외하면, LookupMap 과 유사합니다. |
TreeMap | BTreeMap | 정렬된 UnorderedMap 입니다. 기본 구현은 AVL 트리를 기반으로 합니다. 이 구조는 일관된 순서가 필요하거나, 최소/최대 키에 액세스해야 할 때 사용해야 합니다. |
LookupSet<T> | HashSet<T> | LookupMap 과 유사하지만 값을 저장하지 않는 집합입니다. 이는 값의 고유한 존재 여부를 확인하는 데 사용할 수 있습니다. 이 구조는 반복할 수 없으며, 값 조회에만 사용할 수 있습니다. |
UnorderedSet<T> | HashSet<T> | 세트에 포함된 요소에 대한 추가 메타데이터를 저장하는 반복 가능한 자료형으로, LookupSet 과 같습니다. |
인메모리 HashMap
vs 영구 UnorderedMap
HashMap
은 모든 데이터를 메모리에 보관합니다. 액세스하려면 컨트랙트에서 전체 맵을 역직렬화해야 합니다.UnorderedMap
은 영구 스토리지에 데이터를 보관합니다. 요소에 액세스하려면 해당 요소를 역직렬화하기만 하면 됩니다.
UnorderedMap
사용 사례:
- 한 번의 함수 호출로 컬렉션의 모든 요소를 반복해야 합니다.
- 요소의 수는 작거나 고정되어 있습니다(예: 10개 미만).
HashMap
사용 사례:
- 컬렉션의 제한된 하위 집합(예: 호출당 하나 또는 두 개의 요소)에 액세스해야 합니다.
- 컬렉션을 메모리에 맞출 수 없습니다.
그 이유는, HashMap
이 하나의 스토리지 작업으로 전체 컬렉션을 역직렬화(및 직렬화)하기 때문입니다. 전체 컬렉션에 액세스하는 것이 N
개의 스토리지 작업을 통해 모든 요소에 액세스하는 것보다 가스 비용이 저렴합니다.
UnorderedMap
예시:
/// Using Default initialization.
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, Default)]
pub struct Contract {
pub status_updates: HashMap<AccountId, String>,
}
#[near_bindgen]
impl Contract {
pub fn set_status(&mut self, status: String) {
self.status_updates.insert(env::predecessor_account_id(), status);
assert!(self.status_updates.len() <= 10, "Too many messages");
}
pub fn clear(&mut self) {
// Effectively iterating through all removing them.
self.status_updates.clear();
}
pub fn get_all_updates(self) -> HashMap<AccountId, String> {
self.status_updates
}
}
HashMap
예시:
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
pub status_updates: UnorderedMap<AccountId, String>,
}
#[near_bindgen]
impl Contract {
#[init]
pub fn new() -> Self {
// Initializing `status_updates` with unique key prefix.
Self {
status_updates: UnorderedMap::new(b"s".to_vec()),
}
}
pub fn set_status(&mut self, status: String) {
self.status_updates.insert(&env::predecessor_account_id(), &status);
// Note, don't need to check size, since `UnorderedMap` doesn't store all data in memory.
}
pub fn delete_status(&mut self) {
self.status_updates.remove(&env::predecessor_account_id());
}
pub fn get_status(&self, account_id: AccountId) -> Option<String> {
self.status_updates.get(&account_id)
}
}