Iteration 가능한 mapping 을 가지는 스마트 컨트랙트 작성

Youngbae An
RayonProtocol
Published in
6 min readAug 29, 2018
iterable-mapping

Solidity로 작성하는 스마트 컨트랙트에는 key-value값을 저장하기 위해 mapping 타입을 제공한다. 이 mapping 타입은 특정 key에 대한 value값을 저장하거나 읽어오는데 유용하게 사용된다. 하지만 mapping에 저장된 모든 목록을 얻어오거나, 순차적으로 목록을 반복하여 value값을 읽어오기 위한 iteration 기능은 제공하지 않는다.

이번 글에서는 mapping에 저장된 목록들을 iteration하는 패턴을 살펴보자. 그리고 mapping에 새로운 목록을 추가하거나 기존 목록을 삭제할 때 유의해야 할점도 알아본다.

mapping 만 포함된 non-iterable한 스마트 컨트랙트

address 타입을 key로 가지고 bytes32 타입을 value로 가지는 mapping 을 포함한 스마트 컨트랙트 예제를 살펴보자

mapping(address => bytes32) 타입의 map과 add(),remove(),contains(), getByKey() 함수를 포함하고 있다.

mapping 타입은 특정 key에대한 값을 저장하고 조회하는 기능을 제공한다. 하지만 iterable한 key 목록을 반환해 주는 기능을 제공하지 않는다. iterable가능한 mapping 을 만들고 싶으면 추가로 기능을 포함시켜야 한다.

Iterable한 mapping을 만들때 고려해야 할 내용들

1. mapping의 key들을 array로 저장

map에 저장되는 key들을 기록하기 위해 array로 keyList를 추가한다. keyList는 map의 key에 해당하는 타입인 address의 array로 구성한다.

add() 함수에서는 map에 value를 기록하는 동작외에 keyList에 key를 추가하는 동작이 추가된다.

keyList가 포함되면서 전체 map의 길이를 반환하는 size()함수와 전체 key목록을 반환하는 getKeys()함수가 추가 되었다.

위 예제에서 몇가지 포함되지 않은 기능이 있다.

  • add()에 기존 존재하는 key에 대한 처리가 안되어 있다.
  • remove()가 구현되어 있지 않다.
  • contains()에서 value값이 0인지 아닌지로 map에 저장된 항목인지를 판단한다. 이동작으로 value값에 0을 넣을수 없는 제약이 생기게 된다.

위 항목들의 공통점은 add(), remove() 실행시 입력된 key에 대해 keyList의 index값을 알 수 없기 때문에 나오는 문제점 들이다. keyList의 모든 항목을 loop돌면서 index값을 찾을수도 있겠지만, 비효율적이기 때문에 keyList의 index위치를 기록해 두는게 필요하다.

2. array의 index값을 같이 기록

map에 저장하는 value값에 keyList의 index값을 포함해서 저장해보자.

새로운 struct를 만들어 value와 index를 포함시킨 타입인 Entry를 만들어 mapping의 value의 타입으로 바꾸어 본다.

Entry struct에 value값과 keyList의 index가 포함된다. 여기서 주의할 점은 index값은 keyList의 index값에 1을 더한값이 저장된다. keyList의 index는 0~(length-1) 값까지 가지게 되는데, 0은 value가 없는 항목을 나타내기 위한 값으로 사용되기 때문에 Entry에는 1~length 의 값이 기록된다.

add() 함수내용을 살펴보면 입력된 key값으로 부터 map에서 Entry를 찾은 다음에 저장된 index값을 비교한다. index = 0이면 새로운 항목이 추가되는 동작으로 keyList에 push()로 항목을 추가한다. 그리고 Entry.index값을 keyList에 추가된 index 값에 1을 더한값으로 저장한다.

contains() 함수는 앞에서 설명하였듯이 Entry의 index값이 0인지를 판단하면 된다. 이 경우 value값으로 0을 저장할 수 있게 된다.

remove() 함수는 포함되어 있지 않은데, 다음 부분에서 설명한다.

3. remove()구현시 array의 항목 삭제

remove() 함수는 add()보다 좀더 복잡하다. 입력된 key값으로 map에서 Entry를 찾아서 delete시키고, keyList의 삭제시킬 index값을 얻을 수 있다. Array에서 항목을 삭제할때는 고려해야 할 점이 있다. keyList에 새로운 항목을 추가할때는 Array타입의 push() 함수로 쉽게 항목을 추가할 수 있다. 그러나 항목을 삭제할때는 직접 Array의 index를 변경하는 동작을 구현해 주어야 한다.

line 2~4 을 살펴보면 remove() 함수에 입력된 key값을 map에서 Entry를 찾는다. Entry.index값을 체크하여 entry가 없는 항목이거나 잘못된 index값인 경우는 에러를 발생시킨다.

line 7~11 을 살펴보면, keyList에서 index항목을 삭제하는 과정이다. Array에서 삭제를 위한 함수를 제공하지 않는데. delete keyList[index]를 실행하면 해당 항목값이 0으로 바뀌게 되지 keyList의 항목이 삭제되지 않는다. 그래서 여기서는 keyList의 마지막 항목의 값을 삭제시키는 index로 옮기고 마지막 항목을 삭제하는 동작을 한다. keyList의 마지막 항목의 삭제는 keyList.length값을 1 줄이는 동작만 하면된다. delete keyList[keyList.length-1]은 해당값을 0으로만 바꾸는 동작이기 때문에 굳이 실행시키면서 gas를 소비할 필요는 없다.

line 12은 map의 항목을 삭제시키는 과정이다.

최종 코드

앞에서 설명한 내용을 정리하여 iterable한 mapping을 가지는 스마트 컨트렉트 예제를 완성해 보면 다음과 같다.

위 예제 코드 및 테스트 코드는 github(https://github.com/rayonprotocol-research/solidity-iterable-mapping) 을 통해서 확인 해 볼수 있다.

--

--