Move 프로그래밍 언어

Move는 Diem 측에서 블록체인 분야를 위해 개발한 언어로, 안전성과 안정성을 갖추고 있습니다. Diem 개발자 웹사이트 에서 언어백서를 열람하실 수 있습니다. 또한 이 언어가 왜 블록체인 용도에 최적화되어 있는지에 대해서도 파악하실 수 있습니다.

본 백서는 Move 언어 관련 정보를 집대성함으로써 모든 정보를 한 권으로 압축하는 과정을 통해 탄생하였습니다.

서문

제가 이 책을 발간하였을 시점에는 Move에 대한 어떠한 문서(또는 참고문헌)도 존재하지 않았고, 따라서 크립토 세계의 숨겨진 보석인 Move 언어를 모두에게 알리고 싶은 뜻에서 작업을 진행하게 되었습니다. 현재는 언어개발자들이 편찬한 Move 관련 문서 가 있습니다만 여전히 초심자들에게는 이 책에 담긴 흐름이 더욱 직관적이며 친근할 것으로 생각됩니다. 저 또한 한때는 초심자였는데, Move는 꽤나 까다롭고 흥미로운 언어이며 일부 개념들은 충격적으로 느껴질 수 있습니다.

제가 이 언어를 이토록 사랑하는 이유를 여러분께서 알게 되실 것을 기대합니다!

- Damir Shamanaev

시작하기

경고: 이 페이지에 있는 컨텐츠는 더 이상 유효하지 않으며 재작업이 필요합니다. Move IDE의 최신 버전이 곧 배포될 것입니다. 현재로서는 move-cli 를 사용하실 것을 권고드립니다.


컴파일된 모든 언어들이 그렇듯이, Move 어플리케이션을 컴파일, 구동 및 디버그 하려면 적절한 도구 모음이 필요합니다. 이 언어는 블록체인 용도로 개발되어 해당 분야에서만 사용되기에, 체인 외부 환경에서 스크립트를 구동하는 것은 간단한 일이 아닙니다. 그러한 경우 각 모듈마다 별도의 환경과 계정 처리 및 컴파일 배포 시스템을 배정해야 하기 때문입니다.

Move 모듈 개발의 간소화가 이루어질 수 있도록 Visual Studio Code에 대응하는 Move IDE 확장프로그램을 개발했습니다. 이 확장 프로그램은 환경 요구사항에 대응하는 것을 도울 것이며, 빌드/구동 환경을 처리해 줌으로써 CLI와 씨름하는 일 없이 Move 언어 학습에만 집중할 수 있도록 돕기 때문에, 사용하시는 것을 강력하게 추천드립니다.

Move IDE 설치 방법

설치를 진행하려면 다음의 항목이 필요합니다.

  1. VSCode (버전 1.43.0 이상) – 이 곳 에서 받으실 수 있습니다. 이미 있으시다면 다음 단계로 진행해 주십시오.
  2. Move IDE – VSCode를 설치한 뒤 이 링크 로 들어가 최신 IDE 버전을 설치하십시오

설치 환경

Move IDE는 디렉토리 구조를 정리할 수 있는 방법을 제공합니다. 프로젝트에 새 디렉토리를 생성하여 VSCode에서 실행하십시오. 이후 아래의 디렉토리 구조를 설치하십시오.

modules/   - directory for our modules
scripts/   - directory for transaction scripts
out/       - this directory will hold compiled sources

또한 .mvconfig.json 이라는 이름의 파일을 생성하여 작업 환경에 libra 를 구성합니다. 아래 내용은 하나의 예시입니다.

{
    "network": "libra",
    "sender": "0x1"
}

또는 difnance를 네트워크로 사용할 수도 있습니다.

{
    "network": "dfinance",
    "sender": "0x1"
}

dfinance는 bech32 'wallet1...' 주소를 사용하며, libra는 16-byte '0x...' 주소를 사용합니다. 로컬 구동 및 실험의 경우 간단하고 짧은 0x1 주소만으로 충분할 것입니다. 그러나 testnet이나 제품환경에서 작업하는 경우 선택한 네트워크에 대응하는 정확한 주소를 사용해야 합니다.

Move로 만든 최초의 어플리케이션

Move IDE를 사용하면 시험 환경에서 스크립트를 구동할 수 있습니다. gimme_five() 함수를 구현한 뒤 VSCode 내부에서 구동하여 작동법을 함께 알아봅시다.

모듈 생성

프로젝트의 modules/ 디렉토리 내부에 hello_world.move 라는 이름의 새로운 파일을 생성합시다.

// modules/hello_world.move
address 0x1 {
module HelloWorld {
    public fun gimme_five(): u8 {
        5
    }
}
}

0x1 이 아닌 본인만의 주소를 사용하기로 결정했다면 반드시 이 파일과 다음 파일에 등장하는 0x1 값을 해당 주소 값으로 수정해 주십시오.

스크립트 작성

다음으로 scripts/ 디렉토리 안에 run_hello.move라고 하는 스크립트를 생성합니다.

// scripts/run_hello.move
script {
    use 0x1::HelloWorld;
    use 0x1::Debug;

    fun main() {
        let five = HelloWorld::gimme_five();

        Debug::print<u8>(&five);
    }
}

이후 스크립트를 열어놓은 상태로 다음의 단계들을 진행하십시오.

  1. (맥의 경우) ⌘+Shift+P 또는 (리눅스/윈도우의 경우) Ctrl+Shift+P 를 눌러 VSCode의 명령 팔레트를 전환합니다.
  2. >Move: Run Script 를 입력한 뒤 적절한 옵션이 나오면 엔터 키를 누르거나 클릭합니다.

짜잔! 실행 결과를 보면 디버그에 5가 출력된 로그 메시지를 확인할 수 있습니다. 이 창이 등장하지 않는다면 이 부분을 다시 진행해 주십시오.

디렉토리 구조는 아래의 형태와 같아야 합니다.

modules/
  hello_world.move
scripts/
  run_hello.move
out/
.mvconfig.json

모듈 디렉토리에 둘 수 있는 모듈의 개수에는 제한이 없습니다. 해당 디렉토리에 있는 모든 모듈은 .mvconfig.json에서 명시해 둔 주소를 사용해서 스크립트에서 접근할 수 있습니다.

구문 기본사항

이 장에서는 Move 언어에 대해 다루겠습니다. 아주 간단하고 기초적인 문법 규칙으로 시작하여 진행하면서 점차 난이도를 높일 예정입니다. 실력 있는 개발자들에게는 너무 쉽게 느껴질 수 있는 내용들이지만, 그래도 꼼꼼하게 살펴보실 것을 권장합니다. 초심자라면 이 장을 통해 Move에 대해 알아야 할 기본 내용을 모두 숙지하길 바랍니다.

개념

Solidity 등의 다른 블록체인 언어와는 달리 Move는 스크립트 (또는 스크립트 형태의 트랜잭션(transaction-as-script)) 와 모듈을 분리합니다. 전자의 경우 트랜잭션에 더 많은 로직을 배치함으로써 시간과 자원을 절약함과 동시에 더욱 높은 유연성을 확보할 수 있으며, 후자는 블록체인 기능성을 확장하거나 다양한 선택지를 제공할 수 있는 맞춤형 스마트 컨트랙트를 구현할 수 있도록 합니다.

기본사항에서는 초심자들에게 상당히 친근하게 다가올 스크립트를 다루면서 시작한 뒤 모듈을 진행하겠습니다.

기본형

Move에는 숫자, 주소 및 불리언(Boolean) 값을 나타낼 수 있도록 정수(u8, u64, u128), booleanaddress 에 대응하는 기본형이 몇 가지 미리 탑재되어 있습니다. Move에 스트링 또는 부동소수점 숫자는 없습니다.

정수형

정수는 u8, u64u128 로 나타나며, 몇 가지 정수 표기법이 아래와 같이 기재되어 있습니다.

script {
    fun main() {
        // define empty variable, set value later
        let a: u8;
        a = 10;

        // define variable, set type
        let a: u64 = 10;

        // finally simple assignment
        let a = 10;

        // simple assignment with defined value type
        let a = 10u128;

        // in function calls or expressions you can use ints as constant values
        if (a < 10) {};

        // or like this, with type
        if (a < 10u8) {}; // usually you don't need to specify type
    }
}

as 연산자

값을 비교하거나 함수 인수에서 다양한 크기의 정수를 필요로 한다면 as 연산자를 사용해서정수 변수를 다른 크기로 캐스팅할 수 있습니다.

script {
    fun main() {
        let a: u8 = 10;
        let b: u64 = 100;

        // we can only compare same size integers
        if (a == (b as u8)) abort 11;
        if ((a as u64) == b) abort 11;
    }
}

불리언

모두에게 친숙한 불리언 유형은 거짓에 해당하는 두 가지 상수가 존재하며 이 둘은 각각 논리형에 해당하는 한 가지 값만 의미할 수 있습니다.

script {
    fun main() {
        // these are all the ways to do it
        let b : bool; b = true;
        let b : bool = true;
        let b = true
        let b = false; // here's an example with false
    }
}

주소

주소는 블록체인에서 발신자 (또는 지갑)의 식별자에 해당합니다. 주소 유형을 필요로 하는 기초적인 작동으로는 코인 전송과 모듈 불러오기가 있습니다.

script {
    fun main() {
        let addr: address; // type identifier

        // in this book I'll use {{sender}} notation;
        // always replace `{{sender}}` in examples with VM specific address!!!
        addr = {{sender}};

        // in Diem's Move VM and Starcoin - 16-byte address in HEX
        addr = 0x...;

        // in dfinance's DVM - bech32 encoded address with `wallet1` prefix
        addr = wallet1....;
    }
}

주석

코드 중간에 추가 설명이 필요할 것 같다면 주석을 사용합니다. 주석 기능은 코드의 일부를 설명하는 것을 목적으로 하며, 해당 텍스트 구간 또는 문장은 실행되지 않습니다.

라인 주석

script {
    fun main() {
        // this is a comment line
    }
}

라인 주석을 작성하려면 이중 사선 “//”을 사용합니다. 사용법은 매우 간단한데, “//뒤에 오는 모든 내용은 해당 라인 끝부분에 추가된 주석으로 간주됩니다. 라인 주석을 사용하면 다른 개발자들에게 짧은 참고사항을 남겨두거나 실행 체인에서 일부 코드를 주석 처리하여 제외시킬 수 있습니다.

script {
    // let's add a note to everything!
    fun main() {
        let a = 10;
        // let b = 10 this line is commented and won't be executed
        let b = 5; // here comment is placed after code
        a + b // result is 15, not 10!
    }
}

블록 주석

모든 라인 내용에 주석을 달 의향이 없거나 하나 이상의 라인을 주석 처리하여 제외하려는 경우 블록 주석을 사용합니다.

블록 주석은 사선 별표 /* 로 시작하며 첫 번째 별표 사선 */ 표시 전까지 들어오는 모든 텍스트를 포함합니다. 블록 주석은 라인 1개에 한정되지 않으며 코드 중 모든 장소에 참고사항을 작성할 수 있는 장점이 있습니다.

script {
    fun /* you can comment everywhere */ main() {
        /* here
           there
           everywhere */ let a = 10;
        let b = /* even here */ 10; /* and again */
        a + b
    }
    /* you can use it to remove certain expressions or definitions
    fun empty_commented_out() {

    }
    */
}

물론 이 예시는 말도 안 되는 내용이지만, 블록 주석의 위력을 잘 보여주고 있습니다. 어느 곳에나 주석 처리를 할 수 있다는 점 명심하세요!

표현식과 스코프

프로그래밍 언어에서 표현식은 값을 반환하는 코드 뭉치입니다. 반환 값을 가지는 함수 호출은 표현식입니다. 이 함수가 반환하는 정수(또는 부울값이나 주소) 또한 표현식입니다. 반환 값은 정수 유형의 값을 가집니다.

표현식은 세미콜론을 사용하여 반드시 분할해 줘야 합니다*

* 세미콜론을 입력하는 경우 ‘내부적으로는‘ ; (empty_expression)으로 취급됩니다. 세미콜론 이후에 표현식을 배치하는 경우 공백란을 대체할 것입니다.

공백 표현식

이 표현식을 직접 사용할 일은 아마 없을 것이지만 Move에서 공백 표현식은 (Rust와 유사한 방식에 해당하는) 빈 괄호로 표기합니다.

script {
    fun empty() {
        () // this is an empty expression
    }
}

공백 표현식은 VM이 자동으로 입력하므로 제외해도 무방합니다.

리터럴 표현식

아래의 코드를 살펴보시면 각 라인에 세미콜론으로 끝나는 표현식이 있는 것을 확인하실 수 있습니다. 마지막 라인에는 세미콜론으로 분리된 표현식이 3개 있습니다.

script {
    fun main() {
        10;
        10 + 5;
        true;
        true != false;
        0x1;
        1; 2; 3
    }
}

좋습니다. 이제 가장 단순한 형태의 표현식들을 배워 보셨는데, 이것들이 필요한 이유는 무엇일까요? 그리고 어떻게 사용할 수 있을까요? let 키워드에 대해 알아볼 시간입니다.

변수와 'let' 키워드

(다른 곳으로 값을 전달하기 위해) 변수에 표현식의 값을 저장하려면 let이라는 키워드가 필요합니다(이미 기본형을 설명하는 장에서 보셨을 것입니다). 이 키워드는 공백(undefined)이거나 표현식의 값이 반영된 새로운 변수를 생성합니다.

script {
    fun main() {
        let a;
        let b = true;
        let c = 10;
        let d = 0x1;
        a = c;
    }
}

let 키워드는 현재 스코프 내부에 새로운 변수를 생성하며 부가적으로 값을 반영하여 해당 변수를 초기 설정합니다. 이 표현식에 대응하는 구문은 let <VARIABLE> : <TYPE>; 또는 let <VARIABLE> = <EXPRESSION> 입니다.

변수를 생성하여 초기 설정을 완료한 뒤에는 변수 이름을 사용하여 값을 변경하거나 접근할 수 있습니다. 상기 예시에서 변수 a는 함수 끝부분에서 초기 설정이 이루어졌으며 변수 c의 값을 할당 받았습니다.

After you've created and initialized variable you're able to modify or access its value by using a variable name. In example above variable a was initialized in the end of function and was assigned a value of variable c.

등호 =는 할당 연산자입니다. 우측 표현식을 좌측 변수에 할당하게 됩니다. 예를 들면 a = 10 이라는 식에서 변수 a10이라는 정수가 할당되었습니다

정수형 대응 연산자

Move에는 정수 값을 변형할 수 있는 다양한 종류의 연산자가 아래와 같이 있습니다. | Operator | Op | Types | | |----------|--------|-------|---------------------------------| | + | sum | uint | Sum LHS and RHS | | - | sub | uint | Subtract RHS from LHS | | / | div | uint | Divide LHS by RHS | | * | mul | uint | Multiply LHS times RHS | | % | mod | uint | Division remainder (LHS by RHS) | | << | lshift | uint | Left bit shift LHS by RHS | | >> | rshift | uint | Right bit shift LHS by RHS | | & | and | uint | Bitwise AND | | ^ | xor | uint | Bitwise XOR | | | | or | uint | Bitwise OR |

LHS - 좌측 표현식, RHS - 우측표현식; uint: u8, u64, u128.

밑줄 "_" 로 사용되지 않음을 표기

Move에서는 입력된 모든 변수가 사용되어야 합니다(그렇지 않으면 코드가 컴파일되지 않습니다). 따라서 초기 설정(initialize)을 한 변수를 방치해서는 안 되는데, 밑줄 _을 사용하면 의도적으로 사용되지 않은 상태로 표시할 수 있습니다.

이 스크립트를 컴파일하려고 시도하는 경우 오류가 발생할 것입니다.

script {
    fun main() {
        let a = 1;
    }
}

에러 내용:


    ┌── /scripts/script.move:3:13 ───
    │
 33 │         let a = 1;
    │             ^ Unused assignment or binding for local 'a'. Consider removing or replacing it with '_'
    │

컴파일러 메시지가 명확함으로 이런 경우에는 대신 밑줄만 추가하면 되겠습니다.

script {
    fun main() {
        let _ = 1;
    }
}

섀도잉

Move에서는 동일한 변수를 두 번 정의하는 것이 가능한데, 한 가지 제한사항이 있습니다. 바로 계속 사용되어야 하는 상태여야 한다는 것입니다. 상기 예시에서는 두 번째 a변수만 사용되고 있으며, 첫 번째 변수 let a = 1은 사용되지 않은 상태입니다. 바로 다음 라인에서는 첫 번째 변수가 사용되지 않은 그 상태에서 a재정의하고 있습니다.

script {
    fun main() {
        let a = 1;
        let a = 2;
        let _ = a;
    }
}

그러나 첫 번째 변수를 사용해 줌으로써 정상적으로 진행되도록 할 수 있습니다.

script {
    fun main() {
        let a = 1;
        let a = a + 2; // though let here is unnecessary
        let _ = a;
    }
}

블록 표현식

블록은 표현식이며, 중괄호 {}로 표시되어 있습니다. 블록에는 다른 표현식(및 다른 블록) 이 들어올 수도 있습니다. 약간의 제한은 있으나, 친숙한 중괄호가 등장하는 것에서 짐작할 수 있듯 함수 바디 또한 블록으로 분류할 수 있습니다.

script {
    fun block() {
        { };
        { { }; };
        true;
        {
            true;

            { 10; };
        };
        { { { 10; }; }; };
    }
}

스코프 이해하기

Scope (as it's said in Wikipedia) is a region of code where binding is valid. In other words - it's a part of code in which variable exists. In Move scope is a block of code surrounded by curly braces - basically a block.

블록의 정의는 실제로 스코프를 정의하는 것입니다.

script {
    fun scope_sample() {
        // this is a function scope
        {
            // this is a block scope inside function scope
            {
                // and this is a scope inside scope
                // inside functions scope... etc
            };
        };

        {
            // this is another block inside function scope
        };
    }
}

이 샘플에 등장하는 주석에서 볼 수 있듯이 스코프는 블록(또는 함수)을 통해 정의되며, 중첩시키는 것도 가능하며 정의할 수 있는 스코프의 개수에는 제한이 없습니다.

변수의 지속시간과 가시성

Let 키워드는 변수를 생성한다는 점은 이미 알고 있지만, 정의된 변수는 해당 정의가 이루어진스코프 내부(즉 중첩된 스코프 내부)에서만 존속한다는 사실은 모르고 계셨을 것입니다. 요약하자면 소속된 스코프 외부에서는 접근할 수 없으며, 해당 스코프가 끝나는 지점에서 곧바로 변수도 수명을 다 합니다.

script {
    fun let_scope_sample() {
        let a = 1; // we've defined variable A inside function scope

        {
            let b = 2; // variable B is inside block scope

            {
                // variables A and B are accessible inside
                // nested scopes
                let c = a + b;

            }; // in here C dies

            // we can't write this line
            // let d = c + b;
            // as variable C died with its scope

            // but we can define another C
            let c = b - 1;

        }; // variable C dies, so does C

        // this is impossible
        // let d = b + c;

        // we can define any variables we want
        // no name reservation happened
        let b = a + 1;
        let c = b + 1;

    } // function scope ended - a, b and c are dropped and no longer accessible
}

변수는 정의된 스코프(또는 블록) 내부에만 존재합니다. 소속된 스코프가 끝나는 시점에 변수도 수명이 끝납니다.

블록 반환값

앞서 블록은 표현식이라는 점을 배워보았는데, 왜 그런지, 그리고 블록의 반환값은 무엇인지에 대해서는 아직 다루지 않았습니다.

블록은 값을 반환할 수 있는데, 이 때 반환되는 값은 세미콜론이 뒤에 붙지 않는다면 해당 블록 내부에 있는 마지막 표현식의 값에 해당합니다.

조금 어려울 수 있으니 예시를 몇 개 들도록 하겠습니다.

script {
    fun block_ret_sample() {

        // since block is an expression, we can
        // assign it's value to variable with let
        let a = {

            let c = 10;

            c * 1000  // no semicolon!
        }; // scope ended, variable a got value 10000

        let b = {
            a * 1000  // no semi!
        };

        // variable b got value 10000000

        {
            10; // see semi!
        }; // this block does not return a value

        let _ = a + b; // both a and b get their values from blocks
    }
}

(세미콜론 없는) 스코프 내부의 마지막 표현식이 바로 해당 스코프의 반환값입니다.

요약

이 장의 핵심을 요약해 보겠습니다.

  1. 각 표현식은 블록의 반환값인 경우를 제외하고는 반드시 세미콜론으로 끝나야 합니다;
  2. let 키워드는 값을 지니는 변수를 새로 생성하거나, 자신이 포함된 스코프와 동일한 지속시간을 지니는 우측 표현식을 생성합니다.
  3. 블록은 반환값을 가지거나 가지지 않는 표현식입니다.

다음으로 실행 흐름을 제어하는 방법과 논리 스위치에 블록을 사용하는 방법을 살펴보겠습니다.

추가 참고 자료

제어 흐름

Move는 명령성을 지니는 언어이며, 그러한 언어 특성상 제어 흐름이라는 요소를 구비함으로써 코드 블록을 구동할지 또는 이를 건너뛰거나 다른 블록을 대신 실행할지를 선택할 수 있습니다.

Move에서는 루프 (whileloop)와 if표현식을 사용할 수 있습니다.

if 표현식

if표현식을 사용하면 일부 조건이 참인 경우 특정 코드 블록을 실행하고, 조건이 거짓인 경우 다른 블록을 실행할 수 있습니다.

script {
    use 0x1::Debug;

    fun main() {

        let a = true;

        if (a) {
            Debug::print<u8>(&0);
        } else {
            Debug::print<u8>(&99);
        };
    }
}

이 예시에서는 if + block 을 사용하여 a == true 인 경우 0을 출력하고 afalse 인 경우 99 를 출력하도록 했습니다.

if (<bool_expression>) <expression> else <expression>;

if 구문은 이처럼 간단하며, 표현식임과 동시에 세미콜론으로 끝나야 합니다. 이는 또한 let 명령문과 함께 사용해야 하는 이유이기도 합니다!

script {
    use 0x1::Debug;

    fun main() {

        // try switching to false
        let a = true;
        let b = if (a) { // 1st branch
            10
        } else { // 2nd branch
            20
        };

        Debug::print<u8>(&b);
    }
}

이제 변수 b는 표현식에 따라 다른 값을 할당 받게 됩니다. 그러나 if의 두 분기점은 모두 동일한 유형을 반환해야 합니다! 그렇지 않은 경우 변수 b는 다른 유형(또는 미정의)이 될 가능성이 있으며 이는 통계적으로 입력된 언어에서는 불가능합니다. 컴파일러 용어로는 분기 호환성 이라고 하며, 양 분기가 모두 호환 가능한(동일한) 유형을 반환해야 함을 뜻합니다.

ifelse 없이 단독으로 사용될 수도 있습니다.

script {
    use 0x1::Debug;

    fun main() {

        let a = true;

        // only one optional branch
        // if a = false, debug won't be called
        if (a) {
            Debug::print<u8>(&10);
        };
    }
}

그러나 else 분기가 없는 if 표현식은 조건이 충족되지 않은 경우 할당에 사용될 수 없으며 변수가 정의되지 않을 가능성이 존재하게 되는데, 다시 말하지만 이는 불가능합니다.

루프를 사용한 반복

Move에서는 루프를 정의하는 방식이 두 가지 있습니다.

  1. while 을 사용한 조건부 루프
  2. 무한 loop

while을 사용한 조건부 루프

while은 루프를 정의할 수 있는 방법으로, 루프는 일부 조건이 참일 때 실행되는 표현식입니다. 즉 조건이 일 때 코드가 계속해서 반복된다는 것입니다. 어느 조건을 구현하려는 경우 주로 외부 변수(또는 집계기)를 사용합니다.

script {
    fun main() {

        let i = 0; // define counter

        // iterate while i < 5
        // on every iteration increase i
        // when i is 5, condition fails and loop exits
        while (i < 5) {
            i = i + 1;
        };
    }
}

또한 알아 두면 좋은 점으로, whileif와 마찬가지로 표현식이므로 끝에 세미콜론이 들어와야 합니다. While 루프의 일반 구문은 다음과 같습니다.

while (<bool_expression>) <expression>;

if와는 달리 while은 값을 반환할 수 없으므로 (if 표현식에서 진행했던) 변수 할당은 실시할 수 없습니다.

접근 불가능한 코드

Move의 신뢰도는 보안성 확보와 직결되어 있습니다. 그렇기 때문에 모든 변수를 사용하는 것이 의무화되어 있으며 동일한 이유에서 접근 불가능한 코드를 사용하는 것을 금지합니다. 디지털 자산들은 프로그래밍이 가능하므로 코드 형태로 사용될 수 있고(이후 자원 장 에서 배울 예정입니다), 접근 불가능한 영역에 이들을 두는 경우 불편과 궁극적으로는 이들의 손실까지도 이어질 수 있습니다.

바로 그러한 이유에서 접근 불가능한 코드는 중대한 문제인 것입니다. 이제 이 부분을 분명히 했으니 다음으로 넘어가겠습니다.

무한 loop

무한 루프를 정의하는 방법이 하나 있습니다. 비조건적이며 실제로 강제로 멈추지 않는 한 무한대로 진행됩니다. 아쉽게도 컴파일러는 대부분의 상황에서 루프의 유/무한 여부를 정의할 수 없으며 코드 발행을 막지 못합니다. 자칫 이를 실행하게 되는 경우 모든 확보 자원(블록체인 용어로는 가스)을 소모하게 되므로, 무한 루프를 사용할 때는 코드를 적절하게 테스트해 볼 필요가 있으며 조건부(while) 루프로 바꾸는 편이 훨씬 더 안전합니다.

무한 루프는 loop 키워드로 정의됩니다.

script {
    fun main() {
        let i = 0;

        loop {
            i = i + 1;
        };

        // UNREACHABLE CODE
        let _ = i;
    }
}

그러나 컴파일러는 허용하더라도 이 키워드는 불가능합니다.

script {
    fun main() {
        let i = 0;

        loop {
            if (i == 1) { // i never changed
                break // this statement breaks loop
            }
        };

        // actually unreachable
        0x1::Debug::print<u8>(&i);
    }
}

어느 루프가 실제로 무한인지를 이해하는 것은 컴파일러에게 있어서는 매우 복잡한 작업이기 때문에, 루프 오류를 피하는 것은 전적으로 코드 작성자인 여러분 본인에게 달려 있습니다. 앞서 말씀드렸듯이, 자산 손실로 이어질 수 있는 부분입니다.

continuebreak 로 루프 제어하기

continuebreak 키워드를 사용하면 각각 반복을 한 라운드 건너뛰거나 정지할 수 있습니다. 두 키워드 모두 각 유형의 루프에서 자유롭게 사용할 수 있습니다.

예를 들어 loop에 두 가지 조건을 추가해 보겠습니다. 만약 i가 짝수라면 continue를 사용함으로써 해당 키워드를 호출한 뒤 코드를 진행하는 일 없이 다음 번 반복으로 넘어 가겠습니다.

break 키워드를 사용하면 반복을 멈추고 루프를 끝내게 됩니다.

script {
    fun main() {
        let i = 0;

        loop {
            i = i + 1;

            if (i / 2 == 0) continue;
            if (i == 5) break;

            // assume we do something here
         };

        0x1::Debug::print<u8>(&i);
    }
}

세미콜론 이야기를 하자면 breakcontinue가 블록 내부의 마지막 키워드라면, 이 뒤에 오는 다른 코드가 실행될 일이 없으므로 세미콜론을 붙일 수 없습니다. Semi 조차도 사용할 수 없는데, 아래 내용을 참조해 주십시오.

script {
    fun main() {
        let i = 0;

        loop {
            i = i + 1;

            if (i == 5) {
                break; // will result in compiler error. correct is `break` without semi
                       // Error: Unreachable code
            };

            // same with continue here: no semi, never;
            if (true) {
                continue
            };

            // however you can put semi like this, because continue and break here
            // are single expressions, hence they "end their own scope"
            if (true) continue;
            if (i == 5) break;
        }
    }
}

조건부 중단(abort)

어떤 조건이 실패한 경우 트랜잭션의 실행을 중단해야 할 필요가 간혹 발생합니다. 그러한 경우 abort 키워드를 사용할 수 있습니다.

script {
    fun main(a: u8) {

        if (a != 10) {
            abort 0;
        }

        // code here won't be executed if a != 10
        // transaction aborted
    }
}

abort 키워드를 사용하면 바로 뒤에 오는 에러 코드를 출력하며 작업을 중단시키게 됩니다.

내장된 assert 사용하기

내장되어 있는 assert(<condition>, <code>) 메서드는 abort + 조건을 포괄하며 코드 상에서 위치를 불문하고 접근할 수 있습니다.

script {

    fun main(a: u8) {
        assert(a == 10, 0);

        // code here will be executed if (a == 10)
    }
}

assert()는 조건이 충족되지 않은 경우 실행을 중단하게 되며, 반대의 경우에는 아무 기능도 발휘하지 않습니다.

모듈과 불러오기

모듈은 개발자가 자신의 주소로 발행하게 되는 함수와 유형을 하나로 묶어 놓은 집합입니다. 앞서 우리는 스크립트만을 사용해 왔는데, 스크립트는 발행된 모듈이나 0x1 주소로 발행된 모듈의 집합인 표준 라이브러리만을 사용하여 작동할 수 있습니다.

모듈은 발신자의 주소로 발행됩니다. 표준 라이브러리는 0x1 주소로 발행됩니다.

모듈을 발행하는 경우 어떠한 함수도 실행되지 않습니다. 모듈을 사용하려면 스크립트를 사용하십시오.

모듈은 module 키워드로 시작해서, 모듈의 이름과 중괄호가 뒤를 잇는 형태로 구성되어 있습니다. 이 중괄호 안에는 모듈의 내용이 위치하게 됩니다.

module Math {

    // module contents

    public fun sum(a: u64, b: u64): u64 {
        a + b
    }
}

모듈은 다른 사람들이 접근할 수 있는 코드를 발행할 수 있는 유일한 방법입니다. 새로운 유형과 자원 또한 모듈 환경에서만 정의될 수 있습니다.

초기값을 기준으로 했을 때 여러분의 모듈은 여러분의 주소를 사용하여 컴파일 및 발행됩니다. 그러나 어떤 모듈을 테스트 내지는 개발 용도 등을 목적으로 로컬 상에서 사용할 필요가 있거나 모듈 파일 내부에 주소를 명시하고 싶다면 address <ADDR> {} 구문을 사용하십시오.

address 0x1 {
module Math {
    // module contents

    public fun sum(a: u64, b: u64): u64 {
        a + b
    }
}
}

이 예시에서 보신 바와 같이, 모듈 라인에는 들여쓰기를 사용하지 않는 것이 가장 좋습니다.

불러오기

Move의 초기값 환경은 비어 있습니다. 사용할 수 있는 유형은 기본형(정수형, 논리형 및 주소)이 유일하며, 공백 환경에서 진행할 수 있는 유일한 작업은 이러한 유형들과 변수를 작동시키는 것이지만 이것만으로는 의미있거나 유용한 작업을 진행할 수는 없습니다.

이러한 상황을 변경하려면 발행된 모듈(또는 표준 라이브러리)을 불러와야 합니다.

직접 불러오기

모듈에 해당하는 주소를 코드로 직접 불러오는 형식으로 사용할 수 있습니다.

script {
    fun main(a: u8) {
        0x1::Offer::create(a == 10, 1);
    }
}

이 예시에서는 표준 라이브러리인 Offer 모듈을 0x1 주소에서 불러온 뒤 메서드 assert(expr: bool, code: u8)를 사용했습니다.

Use 키워드

코드를 더욱 짧게 하고 (0x1 주소는 짧지만 다른 주소들은 꽤 길다는 점을 기억해 주세요!) 불러오기 내용을 정리하려면 use 키워드를 사용하십시오.

use <Address>::<ModuleName>;

여기에서 <Address>항목은 발행자의 주소이며 <ModuleName>은 모듈의 이름입니다. 단순한 내용인데, 여기에서도 Vector 모듈을 0x1에서 불러올 것입니다.

use 0x1::Vector;

모듈 내용에 접근하기

불러온 모듈의 메서드(또는 유형)에 접근하려면 :: 기호를 사용하십시오. 모듈은 한 가지 수준의 정의만을 가질 수 있기 때문에 모듈에서 공개적으로 정의하는 모든 내용은 더블 콜론을 사용하여 접근할 수 있습니다.

script {
    use 0x1::Vector;

    fun main() {
        // here we use method empty() of module Vector
        // the same way we'd access any other method of any other module
        let _ = Vector::empty<u64>();
    }
}

스크립트로 불러오기

스크립트에서 불러온 내용은 반드시 script {} 블록 내부에 위치해야 합니다.

script {
    use 0x1::Vector;

    // in just the same way you can import any
    // other module(s). as many as you want!

    fun main() {
        let _ = Vector::empty<u64>();
    }
}

모듈로 불러오기

모듈로 불러온 내용은 반드시 module {} 블록 내부에 명시되어야 합니다.

module Math {
    use 0x1::Vector;

    // the same way as in scripts
    // you are free to import any number of modules

    public fun empty_vec(): vector<u64> {
        Vector::empty<u64>();
    }
}

멤버 불러오기

불러오기 명령문은 확장할 수 있습니다. 불러오고자 하는 모듈의 멤버를 명시하는 것도 가능합니다.

script {
    // single member import
    use 0x1::Signer::address_of;

    // multi member import (mind braces)
    use 0x1::Vector::{
        empty,
        push_back
    };

    fun main(acc: &signer) {
        // use functions without module access
        let vec = empty<u8>();
        push_back(&mut vec, 10);

        // same here
        let _ = address_of(acc);
    }
}

Self를 사용하여 모듈과 멤버를 함께 불러오기

멤버 불러오기 구문에 작은 익스텐션을 적용해 주면 전체 모듈과 멤버를 불러올 수 있습니다. 모듈의 경우 Self를 사용하십시오.

script {
    use 0x1::Vector::{
        Self, // Self == Imported module
        empty
    };

    fun main() {
        // `empty` imported as `empty`
        let vec = empty<u8>();

        // Self means Vector
        Vector::push_back(&mut vec, 10);
    }
}

Useas 의 만남

(2개 이상의 모듈이 동일한 이름을 가졌을 때 발생할 수 있는) 이름 지정 관련 충돌을 해결하고 코드의 길이를 축약하려는 경우 as키워드를 사용하여 불러온 모듈의 이름을 변경하는 것도 좋습니다.

구문:

use <Address>::<ModuleName> as <Alias>;

스크립트 내:

script {
    use 0x1::Vector as V; // V now means Vector

    fun main() {
        V::empty<u64>();
    }
}

동일 모듈 내:

module Math {
    use 0x1::Vector as Vec;

    fun length(&v: vector<u8>): u64 {
        Vec::length(&v)
    }
}

자기 자신과 멤버 불러오기의 경우(모듈 및 스크립트에 적용 가능):

script {
    use 0x1::Vector::{
        Self as V,
        empty as empty_vec
    };

    fun main() {
        // `empty` imported as `empty_vec`
        let vec = empty_vec<u8>();

        // Self as V = Vector
        V::push_back(&mut vec, 10);
    }
}

상수

모듈 또는 스크립트 수준의 상수를 정의할 수 있습니다. 일단 정의된 뒤에는 상수를 변경할 수 없으며, 특정 모듈 (예를 들면 역할 식별자 또는 체결 가격) 내지는 스크립트에 대해 상수 값을 정의하기 위해 사용합니다.

상수는 기본형(정수, 논리형 및 주소)이나 벡터로 정의될 수 있습니다. 상수는 부여된 이름을 사용하여 접근할 수 있으며 정의된 스크립트/모듈 상 로컬 위치에 존재하게 됩니다.

상수가 속한 모듈 외부로부터 상수에 접근하는 것은 불가능합니다.

script {

    use 0x1::Debug;

    const RECEIVER : address = 0x999;

    fun main(account: &signer) {
        Debug::print<address>(&RECEIVER);

        // they can also be assigned to a variable

        let _ = RECEIVER;

        // but this code leads to compile error
        // RECEIVER = 0x800;
    }
}

모듈에서도 동일한 용도입니다.

module M {

    const MAX : u64 = 100;

    // however you can pass constant outside using a function
    public fun get_max(): u64 {
        MAX
    }

    // or using
    public fun is_max(num: u64): bool {
        num == MAX
    }
}

요약

상수에 관하여 알아야 할 중요한 내용은 다음과 같습니다.

  1. 정의된 뒤에는 변경이 불가능합니다
  2. 모듈 또는 스크립트에 로컬적으로 존재하며 외부에서는 사용할 수 없습니다
  3. 일반적으로 실용적 목적을 지닌 모듈 수준 상수 값을 정의할 때 사용합니다
  4. 또한 중괄호를 사용하여 상수를 표현식으로 정의하는 것도 가능하나 해당 표현식의 구문은 매우 제한적입니다.

추가 참고 자료

함수

함수는 Move 상에서 실행이 이루어지는 유일한 요소입니다. 함수는 fun키워드로 시작하여 함수 이름, 인수가 들어가는 소괄호 및 바디가 들어가는 중괄호가 뒤에 따라오는 구조입니다.

fun function_name(arg1: u64, arg2: bool): u64 {
    // function body
}

앞서 몇 가지 함수를 보셨을 텐데, 이제 사용법을 알아보도록 하겠습니다.

비고: Move에서 함수는 snake_case 형태, 즉 소문자를 사용하고 공백 대신 밑줄로 띄어쓰기를 대체하는 형태로 작성합니다.

스크립트의 함수

스크립트 블록은 메인으로 간주되는 하나의 함수만을 포함할 수 있습니다. 인수를 포함하는 경우도 있는 이 함수가 트랜잭션으로 실행됩니다. 아주 제한적인데, 값을 반환하는 것이 불가능하며 이미 발행된 모듈에 존재하는 다른 함수를 작동하기 위해서만 사용할 수 있습니다.

주소가 존재하는지를 확인하는 간단한 스크립트의 예시입니다.

script {
    use 0x1::Account;

    fun main(addr: address) {
        assert(Account::exists(addr), 1);
    }
}

이 함수는 인수를 지닐 수 있는데, 이 경우 addr 인수와 유형 address를 가지며 불러온 모듈을 작동시킬 수도 있습니다.

비고: 함수가 단 하나 존재하기 때문에 이름은 자유롭게 붙이셔도 무방합니다. 그러나 일반적인 프로그래밍 개념을 따라 main이라고 칭하는 것이 좋습니다.

모듈에 있는 함수

스크립트 환경은 상당히 제한되어 있으며, 모듈에서 비로소 함수의 전체 잠재력이 발휘됩니다. 다시 복습해 보자면 모듈은 함수와 유형을 하나로 묶어 발행한 집합이며(다음 장에서 알아보겠습니다) 단일 또는 복수의 작업을 해결하게 됩니다.

여기에서 우리는 간단한 수학 모듈을 생성하여 기본적인 수학적 함수와 몇 가지 도움 메서드를 사용자들에게 제공해 보도록 하겠습니다. 대부분의 요소들은 모듈 없이도 활용이 가능하지만 우리의 목적은 교육이니까요!

module Math {
    fun zero(): u8 {
        0
    }
}

먼저 Math라는 이름의 모듈을 정의하고 함수를 하나 넣어두었습니다. zero()라는 함수이며, u8유형의 값인 0을 반환합니다. 표현식 을 기억하시나요? 0은 이 함수의 반환값이기 때문에 뒤에 세미콜론이 오지 않습니다. 블록에서도 마찬가지이죠. 즉 함수 바디는 블록과 매우 흡사합니다.

First step: we've defined a module named Math with one function in it: zero(), which returns 0 - a value of type u8. Remember expressions? There's no semicolon after 0 as it is the return value of this function. Just like you would do with block. Yeah, function body is very similar to block.

함수의 인수

이미 매우 잘 알고 계시겠지만 다시 반복합시다. 함수는 함수로 전달되는 값인 인수를 가져올 수 있습니다. 개수 제한 없이 필요한 만큼 가져올 수 있는데, 각 인수는 함수 바디 내부에서의 이름과 Move의 다른 변수들과 동일하게 유형을 두 가지 속성으로 가집니다.

함수의 인수는 스코프 내부에서 정의되는 다른 모든 변수들과 마찬가지로 함수 바디 내부에만 존재합니다. 함수 블록이 끝난 시점에는 변수도 소멸됩니다.

module Math {

    public fun sum(a: u64, b: u64): u64 {
        a + b
    }

    fun zero(): u8 {
        0
    }
}

우리가 보고 있는 Math의 경우 함수 sum(a,b)가 새로 등장하는데, 2개의 u64값을 합하여 결과인 u64 합을 반환합니다(유형은 변경할 수 없습니다).

몇 가지 구문 규칙들을 살펴봅시다.

  1. 인수에는 반드시 유형이 존재해야 하며 쉼표로 분리해야 합니다.
  2. 함수의 반환 값은 소괄호 뒤에 위치하며 콜론이 뒤에 와야 합니다.

이 함수를 스크립트에서는 어떻게 사용할 수 있을까요? 바로 불러오기 기능입니다!

script {
    use 0x1::Math;  // used 0x1 here; could be your address
    use 0x1::Debug; // this one will be covered later!

    fun main(first_num: u64, second_num: u64) {

        // variables names don't have to match the function's ones
        let sum = Math::sum(first_num, second_num);

        Debug::print<u64>(&sum);
    }
}

return 키워드

return 키워드를 사용하면 함수의 실행을 종료하고 값을 반환하도록 만들 수 있습니다. 이 때 if 조건과 함께 사용해야 하는데, 제어 흐름에서 조건부 스위치를 만들 수 있는 유일한 방법이기 때문입니다.

module M {

    public fun conditional_return(a: u8): bool {
        if (a == 10) {
            return true // semi is not put!
        };

        if (a < 10) {
            true
        } else {
            false
        }
    }
}

다수의 반환값

기존 예시에서는 반환값이 없거나 하나인 경우를 다루었습니다. 만약 유형을 불문하고 여러 개의 값을 반환할 수 있는 방법이 있다면 사용해 보고 싶지 않으신가요? 함께 진행해 보겠습니다!

다수의 반환값을 명시하려면 소괄호를 사용해야 합니다.

module Math {

    // ...

    public fun max(a: u8, b: u8): (u8, bool) {
        if (a > b) {
            (a, false)
        } else if (a < b) {
            (b, false)
        } else {
            (a, true)
        }
    }
}

이 함수는 ab라는 두 개의 인수를 가져다가 두 개의 값을 반환합니다. 첫 번째 값은 전달된 두 값 중 최댓값이며 두 번째 값은 논리값, 즉 입력된 숫자 두 개가 동일한지를 나타냅니다. 구문을 잘 살펴보시면 단일 반환 인수를 명시하는 대신에 소괄호를 추가하였고 반환 인수 유형들을 기재하였습니다.

이제 이 함수의 결과를 스크립트 상 존재하는 다른 함수에서 어떻게 사용할 수 있는지를 보겠습니다.

script {
    use 0x1::Debug;
    use 0x1::Math;

    fun main(a: u8, b: u8)  {
        let (max, is_equal) = Math::max(99, 100);

        assert(is_equal, 1)

        Debug::print<u8>(&max);
    }
}

이 예시에서는 튜플을 파괴했는데, max 함수의 반환 값이 가지는 값과 유형을 토대로 새로운 변수를 두 개 생성했습니다. 순서는 보존되어 있으며 변수 maxu8 유형을 가지게 되고 최댓값을 저장하는데, 이 때 is_equal논리값입니다.

굳이 2개로 제한되지는 않습니다. 반환되는 인수의 개수는 전적으로 여러분에게 달려있는데, 데이터 구조에 대해 배우게 되면서 복잡한 데이터를 반환할 수 있는 다른 수단을 곧 알게 되실 것입니다.

함수의 가시성

모듈을 정의할 때는 어떤 함수들은 다른 개발자들이 접근할 수 있게 하고, 다른 함수들은 감춰둔 상태로 둘 필요가 있습니다. 이럴 때 함수 가시성 제어자를 사용하게 됩니다.

초기 설정 상태의 경우 모듈에서 정의된 모든 함수는 개인, 즉 다른 모듈이나 스크립트에서 접근할 수 없습니다. 지금까지 집중해 오셨다면 우리가 Math 모듈에서 정의했던 함수들 중 몇몇은 정의 구간에 앞서 public이라는 키워드가 붙어 있었다는 걸 알고 계실 것입니다.

module Math {

    public fun sum(a: u64, b: u64): u64 {
        a + b
    }

    fun zero(): u8 {
        0
    }
}

이 예시에서 sum() 함수는 모듈을 불러왔을 때 외부에서 접근할 수 있으나, zero() 함수는 초기 설정 상 개인 수준으로 정의되어 있어 접근할 수 없습니다.

public 키워드는 함수의 기본 가시성 수준인 비공개 를 변경하여 공개, 즉 외부에서 접근할 수 있도록 변경해 줍니다.

즉 사실상 sum() 함수를 공개해 두지 않았다면 이 작업은 진행할 수 없을 것입니다.

script {
    use 0x1::Math;

    fun main() {
        Math::sum(10, 100); // won't compile!
    }
}

로컬 함수 접근

접근이 불가능하다면 개인 함수를 생성하는 것에 아무런 의미가 없을 것입니다. 공개 함수가 호출되는 기능이 있는 반면 개인 함수는 내부 작업을 진행하는 역할을 맡습니다.

개인 함수는 정의된 모듈 내부에서만 접근할 수 있습니다.

그럼 동일한 모듈에 있는 함수들을 어떻게 접근할 수 있을까요? 불러왔을 때와 동일하게 해당 함수를 호출해 주기만 하면 됩니다!

module Math {

    public fun is_zero(a: u8): bool {
        a == zero()
    }

    fun zero(): u8 {
        0
    }
}

모듈에 정의된 모든 함수는 동일한 모듈에서 접근할 수 있으며, 이 때 가시성 제어자의 유무나 종류는 전혀 관계없습니다. 이러한 방식을 통해 개인 함수는 개인 기능이나 지나치게 위험한 작업을 드러내는 일 없이 공개 함수 내부에서 호출되는 형태로 사용될 수 있습니다.

네이티브 함수

네이티브 함수는 특별한 종류의 함수인데, Move가 제공하는 범위를 뛰어넘는 기능을 구현하여 추가적인 성능을 발휘할 수 있도록 합니다. 네이티브 함수는 VM에서 자체적으로 정의하면 다양한 구현마다 다르게 나타날 수 있습니다. 즉 이들은 Move 구문에서는 구현을 지니지 않으며 함수 바디를 가지는 대신 세미콜론으로 끝납니다. native 키워드를 사용하면 네이티브 함수를 표시할 수 있습니다. 함수 가시성 조정자와 충돌하지 않으며, 동일한 함수가 동시에 nativepublic 함수로 작동할 수도 있습니다.

Diem의 표준 라이브러리에 등장하는 예시입니다.

module Signer {

    native public fun borrow_address(s: &signer): &address;

    // ... some other functions ...
}

고급 주제

이번에는 Move에서 널리 사용되고 있는 몇 가지 프로그래밍 개념들에 대해 살펴볼 예정입니다. 크게 분류해 보자면 고유의 유형 시스템 기능인 - 능력, 소유권한 (and how it differs from Rust's one), 제네릭벡터 가 있습니다. 이 개념들은 모두 Move 언어의 안전성과 유연성을 확보할 수 있는 든든한 토대가 됩니다.

구조

Structure is a custom type which contains complex data (or no data). It can be described as a simple key-value storage where key is a name of property and value is what's stored. Defined using keyword struct. Struct can have up to 4 abilities, they are specified with type definition.

구조는 복잡한 데이터를 포함하는 맞춤설정형 유형입니다(데이터를 포함하지 않을 수도 있음). 이 개념은 키-값 저장소로 표현될 수도 있는데, 이 때 키는 어느 속성의 이름이며 값은 저장된 내용을 뜻합니다. struct 라는 키워드를 사용하여 정의되는데, 구조체는 유형 정의를 통해 명시되는 능력을 최대 4개까지 보유할 수 있습니다.

struct 는 Move에서 맞춤설정형 유형을 생성할 수 있는 유일한 방법입니다.

정의

구조체의 정의는 모듈 내부에서만 허용됩니다. struct 키워드로 시작하여 이름과 중괄호가 뒤를 잇는데, 이 때 구조체의 필드는 다음과 같이 정의됩니다.

struct NAME {
    FIELD1: TYPE1,
    FIELD2: TYPE2,
    ...
}

구조체의 정의에 관한 예시들을 참고하십시오.

module M {

    // struct can be without fields
    // but it is a new type
    struct Empty {}

    struct MyStruct {
        field1: address,
        field2: bool,
        field3: Empty
    }

    struct Example {
        field1: u8,
        field2: address,
        field3: u64,
        field4: bool,
        field5: bool,

        // you can use another struct as type
        field6: MyStruct
    }
}

단일 구조체의 최대 필드 수는 65535입니다.

각각의 정의된 구조체는 새로운 유형이 됩니다. 이 유형은 모듈 함수를 접근할 때와 마찬가지로, 모듈을 통해 접근할 수 있습니다.

M::MyStruct;
// or
M::Example;

반복된 정의

never 처럼 짧을 수 있습니다.

반복적인 구조체 정의는 불가능합니다.

다른 구조체를 유형으로 사용하는 것도 가능하지만 동일한 구조체를 반복적으로 사용할 수는 없습니다. Move 컴파일러는 반복되는 정의들을 점검하여 이러한 방식으로 코드를 컴파일할 수 없도록 합니다.

module M {
    struct MyStruct {

        // WON'T COMPILE
        field: MyStruct
    }
}

새 구조체 생성하기

이 유형을 사용하려면 인스턴스를 생성해야 합니다.

신규 인스턴스는 정의된 모듈 내부에만 생성될 수 있습니다.

신규 인스턴스를 생성하는 경우 정의를 사용하되, 유형을 전달하는 대신 해당 유형의 값을 전달합니다.

module Country {
    struct Country {
        id: u8,
        population: u64
    }

    // Contry is a return type of this function!
    public fun new_country(c_id: u8, c_population: u64): Country {
        // structure creation is an expression
        let country = Country {
            id: c_id,
            population: c_population
        };

        country
    }
}

Move also allows you to create new instances shorter - by passing variable name which matches struct's field (and type!). We can simplify our new_country() method using this rule:

Move에서는 또한 구조체의 영역(및 유형!)에 일치하는 변수 이름을 전달함을 통해 신규 인스턴스를 더욱 짧게 생성할 수 있습니다. 이 규칙을 사용하면 new_country() 메서드를 좀 더 간단하개 표현할 수 있습니다.

// ...
public fun new_country(id: u8, population: u64): Country {
    // id matches id: u8 field
    // population matches population field
    Country {
        id,
        population
    }

    // or even in one line: Country { id, population }
}

(필드가 없고) 비어 있는 구조체를 생성하려면 중괄호를 사용해 주면 간단하게 처리됩니다.

public fun empty(): Empty {
    Empty {}
}

구조체 필드에 접근하기

(필드 없는 구조체를 생성하는 것이 가능하긴 하지만) 필드에 접근할 방법이 없었다면 구조체는 사실상 쓸모 없었을 것입니다.

모듈만 해당 구조체의 필드에 접근할 수 있습니다. 모듈 필드 외부는 개인 영역입니다.

구조체 필드는 모듈 내부에서만 확인할 수 있습니다. 해당 모듈 외부에서는 그저 한 유형으로 간주될 뿐입니다. 구조체의 영역에 접근하려면 . (마침표) 기호를 사용하십시오.

// ...
public fun get_country_population(country: Country): u64 {
    country.population // <struct>.<property>
}

만약 중첩된 구조체 유형이 동일한 모듈에 정의되어 있다면, 다음과 같이 일반적으로 설명될 수 있는 유사한 방식을 통해 접근할 수 있습니다.

<struct>.<field>
// and field can be another struct so
<struct>.<field>.<nested_struct_field>...

구조의 분해

구조를 분해하려면 let <STRUCT DEF> = <STRUCT> 구문을 사용합니다.

module Country {

    // ...

    // we'll return values of this struct outside
    public fun destroy(country: Country): (u8, u64) {

        // variables must match struct fields
        // all struct fields must be specified
        let Country { id, population } = country;

        // after destruction country is dropped
        // but its fields are now variables and
        // can be used
        (id, population)
    }
}

사용되지 않은 변수는 Move 에서 금지하고 있으며 필드를 사용하지 않고 어느 구조를 분해해야 할 필요가 있다는 점에 유념하십시오. 사용되지 않는 구조체 필드의 경우 밑줄 _ 기호를 사용하십시오.

module Country {
    // ...

    public fun destroy(country: Country) {

        // this way you destroy struct and don't create unused variables
        let Country { id: _, population: _ } = country;

        // or take only id and don't init `population` variable
        // let Country { id, population: _ } = country;
    }
}

분해가 현재로서는 그리 중요하게 다가오지 않을 수 있으나, 자원 부분으로 넘어가게 되면 아주 중요하니까 꼭 기억해 두십시오.

구조체 필드에 획득자 함수 구현하기

구조체 필드를 외부에서 읽을 수 있게 하려면 해당 필드를 읽을 메서드들을 구현해서 반환 값으로 전달해야 합니다. 일반적으로 획득자 메서드는 구조체 필드와 동일한 방식으로 호출되나, 모듈이 하나 이상의 구조체를 정의하고 있는 경우 불편함을 유발할 수 있습니다.

module Country {

    struct Country {
        id: u8,
        population: u64
    }

    public fun new_country(id: u8, population: u64): Country {
        Country {
            id, population
        }
    }

    // don't forget to make these methods public!
    public fun id(country: &Country): u8 {
        country.id
    }

    // don't mind ampersand here for now. you'll learn why it's 
    // put here in references chapter 
    public fun population(country: &Country): u64 {
        country.population
    }

    // ... fun destroy ... 
}

획득자를 생성함으로써 모듈 사용자로 하여금 구조체의 필드에 접근할 수 있도록 합니다.

script {
    use {{sender}}::Country as C;
    use 0x1::Debug;

    fun main() {
        // variable here is of type C::Country
        let country = C::new_country(1, 10000000);

        Debug::print<u8>(
            &C::id(&country)
        ); // print id

        Debug::print<u64>(
            &C::population(&country)
        );

        // however this is impossible and will lead to compile error
        // let id = country.id;
        // let population = country.population.

        C::destroy(country);
    }
}

이제 맞춤설정 유형의 구조체를 정의하는 방법을 알게 되셨는데, 초기 설정 상 해당 기능은 제한되어 있습니다. 다음 장에서는 이 유형의 값들을 어떻게 통제 및 사용하게 될지를 정의하는 방식인 능력에 대해 배워보도록 하겠습니다.

능력을 지니는 유형

Move는 아주 유연하고 맞춤 설정이 가능한 독특한 유형 체계를 가지고 있습니다. 각 유형은 최대 4개의 능력을 지닐 수 있으며, 능력을 사용함으로써 해당 유형의 값들이 어떻게 사용, 제외 또는 저장되는지를 정의하게 됩니다.

능력에는 복사, 제외, 비축 및 키 저장에 해당하는 4가지 종류가 있습니다.

간략하게 설명하자면 다음과 같은데,

  • Copy (복사) - 값이 복사(또는 어느 값에 의해 복제)될 수 있습니다.
  • Drop (제외) - 스코프 끝 부분에서 값을 제외할 수 있습니다.
  • Key (키) - 값을 전체 저장 작업에 로 사용할 수 있습니다.
  • Store (저장) - 값을 전체 저장소 내부에 저장할 수 있습니다.

이 페이지에서는 copydrop 능력을 자세하게 다루고, keystore 능력에 대한 맥락은 자원 장으로 넘어가면 더욱 상세하게 제공될 것입니다.

능력 구문

기본형 및 내재된 유형의 능력들은 사전에 정의되어 있으며 변경할 수 없습니다. 정수, 벡터, 주소 및 논리값에는 복사, 제외저장 능력이 있습니다.

그러나 구조체를 정의하는 경우 이 구문을 사용하여 모든 조합의 능력을 자유롭게 명시할 수 있습니다.

struct NAME has ABILITY [, ABILITY] { [FIELDS] }

또는 예를 들자면 다음과 같습니다.

module Library {
    
    // each ability has matching keyword
    // multiple abilities are listed with comma
    struct Book has store, copy, drop {
        year: u64
    }

    // single ability is also possible
    struct Storage has key {
        books: vector<Book>
    }

    // this one has no abilities 
    struct Empty {}
}

능력이 없는 구조체

능력을 사용하는 방법이나 언어 상 불러오게 되는 요소들을 곧바로 살펴보기에 앞서, 능력이 없는 언어란 어떤 일이 일어나는지를 알아봅시다.

module Country {
    struct Country {
        id: u8,
        population: u64
    }
    
    public fun new_country(id: u8, population: u64): Country {
        Country { id, population }
    }
}
script {
    use {{sender}}::Country;

    fun main() {
        Country::new_country(1, 1000000);
    }   
}

If you try to run this code, you'll get the following error:

error: 
   ┌── scripts/main.move:5:9 ───
   │
 5 │     Country::new_country(1, 1000000);
   │     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot ignore values without the 'drop' ability. The value must be used
   │

메서드 Country::new_country()는 값을 하나 생성하는데, 이 값이 어디에도 전달되지 않고 함수가 끝날 때 자동으로 제외됩니다. 그러나 Country 유형에는 제외 능력이 없기 때문에 실패하게 되는 것입니다. 구조체 정의를 변경하여 제외 능력을 추가해 봅시다.

제외

능력 구문을 사용하면 이 구조체에 특정적인 drop 능력을 추가하게 됩니다. 이 구조체에 대응하는 모든 인스턴스가 제외 능력을 가지게 되며 제외가 가능해 질 것입니다.

module Country {
    struct Country has drop { // has <ability>
        id: u8,
        population: u64
    }
    // ...
}

이제 Country 구조체가 제외될 수 있으므로 스크립트를 실행할 수 있게 됩니다.

script {
    use {{sender}}::Country;

    fun main() {
        Country::new_country(1, 1000000); // value is dropped
    }   
}

비고: 제외 능력은 제외 동작만을 정의합니다. 분해 는 제외 기능을 필요로 하지 않습니다.

복사

Country 구조체의 신규 인스턴스를 생성하고 제외하는 방법을 알아보았는데, 사본을 생성하려고 하면 어떻게 해야 할까요? 초기 설정 상 구조체들은 값을 통해 전달되며, 해당 구조체의 사본을 생성하려면 copy 키워드를 사용하면 됩니다. (다음 장에서 상세하게 다룰 예정)

script {
    use {{sender}}::Country;

    fun main() {
        let country = Country::new_country(1, 1000000);
        let _ = copy country;
    }   
}
   ┌── scripts/main.move:6:17 ───
   │
 6 │         let _ = copy country;
   │                 ^^^^^^^^^^^^ Invalid 'copy' of owned value without the 'copy' ability
   │

예상하셨겠지만 복사 능력이 없는 채로 유형의 사본을 만드는 것은 불가능합니다. 컴파일러 메시지는 분명합니다.

module Country {
    struct Country has drop, copy { // see comma here!
        id: u8,
        population: u64
    }
    // ...
}

해당 변경 사항을 바탕으로 상기 코드는 컴파일과 실행이 이루어질 것입니다.

요약

  • 기본형에는 저장, 복사 및 제외 능력이 있습니다.
  • 초기 설정 상 구조체에는 능력이 없습니다.
  • 복사 및 제외 능력은 어떤 값들이 복사 및 제외될지를 각각 결정합니다.
  • 한 구조체는 최대 4개의 능력을 지닐 수 있습니다.

추가 참고 자료

소유권과 참조

Move VM은 Rust와 유사한 소유권 체계를 구현합니다. 이를 가장 잘 설명하고 있는 자료가 바로 Rust Book 입니다.

Rust의 구문은 약간 다른 면도 있고 수록된 예시 중 일부를 이해하는 것도 쉽진 않지만, Rust Book에 수록된 소유권 장은 꼭 읽어 보시는 것을 추천 드립니다. 이 장에서도 핵심을 다룰 예정입니다.

각 변수는 하나의 소유자 스코프만을 가집니다. 소유자 스코프가 끝나는 시점에 소유했던 변수들은 제외됩니다.

이러한 행동 양상은 표현식 장에서 앞서 살펴본 적이 있습니다. 스코프와 변수의 수명은 동일하다는 점 기억하시나요? 지금이야말로 왜 그런 일이 일어나는가를 파고들어 볼 시간입니다.

소유자는 변수를 갖고 있는 스코프입니다. 변수는 해당 스코프 내부에서 정의되거나(예: 스크립트에서 let을 사용) 인수로서 스코프에 전달될 수 있습니다. Move에 존재하는 유일한 스코프는 함수이기 때문에, 변수를 스코프에 넣을 다른 방법은 존재하지 않습니다.

각 변수에는 단 하나의 소유자만 존재하며, 즉 어느 변수가 인수 형태로 함수에 전달되었다면 해당 함수가 새로운 소유자 가 되어, 변수가 더 이상 첫 번째 함수의 소유가 아닌 것입니다. 또는 변수의 소유권을 해당 함수가 가져왔다고 말해도 무방할 것입니다.

script {
    use {{sender}}::M;

    fun main() {
        // Module::T is a struct
        let a : Module::T = Module::create(10);

        // here variable `a` leaves scope of `main` function
        // and is being put into new scope of `M::value` function
        M::value(a);

        // variable a no longer exists in this scope
        // this code won't compile
        M::value(a);
    }
}

우리가 값을 내부로 전달했을 때 value()안에서 어떤 일이 일어나는지 알아봅시다.

module M {
    // create_fun skipped
    struct T { value: u8 }

    public fun create(value: u8): T {
        T { value }
    }

    // variable t of type M::T passed
    // `value()` function takes ownership
    public fun value(t: T): u8 {
        // we can use t as variable
        t.value
    }
    // function scope ends, t dropped, only u8 result returned
    // t no longer exists
}

물론 임시방편으로 원본 변수와 추가적인 결과를 가지는 튜플을 반환하는 것이겠지만(이 때 반환 값은 (T, u8)), Move에는 더 나은 해결책이 있습니다.

Move와 Copy

먼저 Move VM의 작동 방식, 그리고 함수에 값을 전달하면 어떤 일이 일어나는가를 이해해 둘 필요가 있습니다. VM에는 MoveLoc과 CopyLoc이라는 바이트코드 지침이 2개 존재하는데, 둘다 각각 movecopy키워드를 통해 수동으로 사용할 수 있습니다.

어느 변수를 다른 함수로 전달하는 경우, 해당 변수는 이동 중인 상태이며 MoveLoc OpCode가 사용됩니다. move 키워드를 사용하면 코드가 어떤 형태가 될 지 살펴봅시다.

script {
    use {{sender}}::M;

    fun main() {
        let a : Module::T = Module::create(10);

        M::value(move a); // variable a is moved

        // local a is dropped
    }
}

이 경우는 유효한 Move 코드이지만, 해당 값이 여전히 이동될 것이라는 걸 알고 있는 상태에서 굳이 명시적으로 이동시킬 필요는 없습니다. 숙지하셨으면 이제 Copy로 넘어가겠습니다.

This is a valid Move code, however, knowing that value will still be moved you don't need to explicitly move it. Now when it's clear we can get to copy.

copy 키워드

어느 값을 함수에 전달하고 (이동 지점에) 변수의 사본을 저장하려는 경우, copy 키워드를 사용하면 됩니다.

script {
    use {{sender}}::M;

    fun main() {
        let a : Module::T = Module::create(10);

        // we use keyword copy to clone structure
        // can be used as `let a_copy = copy a`
        M::value(copy a);
        M::value(a); // won't fail, a is still here
    }
}

이 예시에서는 변수(즉 값) a사본을 메서드 value의 첫 번째 호출로 전달하고 a를 로컬 스코프에 저장하여 두 번째로 호출이 진행될 경우 다시 사용할 수 있도록 처리했습니다.

값을 복사함으로써 우리는 이를 복제하게 되었고 프로그램의 메모리 크기를 증가시켰는데, 이를 감안하면 해당 키워드는 사용할 수는 있겠으나 크기가 큰 데이터를 복사하게 되는 경우 메모리 측면에서 비싼 대가를 치를 수 있습니다. 블록체인에서는 낭비할 바이트라고는 하나도 없으며 실행 가격에 영향을 끼치기 때문에, copy 키워드를 매번 사용하게 되면 비용이 크게 올라갈 수 있습니다.

이제 불필요한 복사를 피하고 실제로 돈을 절약할 수 있도록 돕는 기능인 참조에 대해 배워볼 준비가 되었습니다.

참조

여러 프로그래밍 언어에서는 참조 기능을 구현해 놓고 있습니다(위키피디아 참조). 참조는 어느 변수(주로 메모리에서의 한 구획)로 이어지는 링크인데, 이동할 값을 대신하여 프로그램의 다른 부분들로 전달할 수 있는 요소입니다.

참조(&로 표기)는 소유권을 확보하지 않고도 해당 값을 인용할 수 있도록 합니다.

예시를 변경해서 참조가 어떻게 사용되었는가를 알아봅시다.

module M {
    struct T { value: u8 }
    // ...
    // ...
    // instead of passing a value, we'll pass a reference
    public fun value(t: &T): u8 {
        t.value
    }
}

& 표시를 인수 유형 T에 추가하였는데, 이를 통해 인수 유형을 기존의 T로부터 T 참조 내지는 &T로 변경하였습니다.

Move에서는 두 가지 유형의 참조를 지원하는데, & 로 정의되는 불변 유형(예: &T)과 &mut에 해당하는 가변 유형(예: &mut T)이 있습니다.

불변 참조는 값을 변경하지 않고 읽을 수 있게 합니다. 반면 가변 유형은 값을 읽고 변경할 수 있습니다.

module M {
    struct T { value: u8 }

    // returned value is of non-reference type
    public fun create(value: u8): T {
        T { value }
    }

    // immutable references allow reading
    public fun value(t: &T): u8 {
        t.value
    }

    // mutable references allow reading and changing the value
    public fun change(t: &mut T, value: u8) {
        t.value = value;
    }
}

이제 업그레이드된 모듈 M을 어떻게 사용할지를 보겠습니다.

script {
    use {{sender}}::M;

    fun main() {
        let t = M::create(10);

        // create a reference directly
        M::change(&mut t, 20);

        // or write reference to a variable
        let mut_ref_t = &mut t;

        M::change(mut_ref_t, 100);

        // same with immutable ref
        let value = M::value(&t);

        // this method also takes only references
        // printed value will be 100
        0x1::Debug::print<u8>(&value);
    }
}

불변(&) 참조를 사용하면 구조체로부터 데이터를 읽을 수 있으며, 가변(&mut)을 사용하면 이를 변경할 수 있습니다. 적절한 참조 유형을 사용함을 통해 보안성을 유지할 수 있으며 모듈 판독을 보조하여 독자들로 하여금 해당 메서드가 값을 변경하는지 아니면 읽기만 진행하는지를 알 수 있게 합니다.

차용 확인

Move는 참조를 사용하는 방식을 제어하며 예기치 못한 참사가 일어나는 일을 막도록 도와줍니다. 예시를 통해 이해해 봅시다. 모듈과 스크립트를 보면서 무슨 일이 일어나는지, 그리고 그 이유는 무엇인지를 함께 생각해 보겠습니다.

module Borrow {

    struct B { value: u64 }
    struct A { b: B }

    // create A with inner B
    public fun create(value: u64): A {
        A { b: B { value } }
    }

    // give a mutable reference to inner B
    public fun ref_from_mut_a(a: &mut A): &mut B {
        &mut a.b
    }

    // change B
    public fun change_b(b: &mut B, value: u64) {
        b.value = value;
    }
}
script {
    use {{sender}}::Borrow;

    fun main() {
        // create a struct A { b: B { value: u64 } }
        let a = Borrow::create(0);

        // get mutable reference to B from mut A
        let mut_a = &mut a;
        let mut_b = Borrow::ref_from_mut_a(mut_a);

        // change B
        Borrow::change_b(mut_b, 100000);

        // get another mutable reference from A
        let _ = Borrow::ref_from_mut_a(mut_a);
    }
}

이 코드는 컴파일이 진행되며 오류 없이 작동합니다. 먼저 여기에서 일어나고 있는 일은 가변 참조를 A에 사용하여 내부 구조체인 B에 가변 참조를 적용할 수 있도록 합니다. 그 뒤에 B를 변경하고, 계속 작업을 반복할 수 있습니다.

하지만 마지막 두 표현식을 바꾸어 B로의 가변 참조가 남아 있는 상태에서 A에 새로 가변 참조를 생성하려고 시도하면 어떻게 될까요?

let mut_a = &mut a;
let mut_b = Borrow::ref_from_mut_a(mut_a);

let _ = Borrow::ref_from_mut_a(mut_a);

Borrow::change_b(mut_b, 100000);

아마 오류가 발생했을 것입니다.

    ┌── /scripts/script.move:10:17 ───
    │
 10 │         let _ = Borrow::ref_from_mut_a(mut_a);
    │                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid usage of reference as function argument. Cannot transfer a mutable reference that is being borrowed
    ·
  8 │         let mut_b = Borrow::ref_from_mut_a(mut_a);
    │                     ----------------------------- It is still being mutably borrowed by this reference
    │

코드가 컴파일되지 않을 것입니다. 그 이유는 &mut A&mut B에 의해 차용되고 있기 때문입니다. 만약 내용에 대한 가변 참조를 확보한 상태에서 A를 변경할 수 있게 된다면, 내용에 대한 참조가 여전히 여기 존재하는 상황에서 A가 변경된다는 이상한 상황이 발생하게 됩니다. B가 실제로 존재하지 않는데 mut_b가 어딜 지정해야 하는 걸까요?

이로써 몇 가지 결론이 도출됩니다.

  1. 컴파일 에러가 일어나게 되는데, 즉 Move 컴파일러는 이런 사례들을 방지한다는 것을 뜻합니다. 이는 차용 확인이라고 하는 기능입니다(원본은 Rust 언어의 개념). 컴파일러는 차용 그래프를 축적하여 차용된 값을 이동하는 것은 허용하지 않습니다. 이는 Move를 블록체인에서 사용하기에 안전한 이유 중 하나입니다.
  2. 참조에서 참조를 생성하는 것도 가능하므로, 원본 참조는 신규 참조에서 차용하게 됩니다. 불변 및 가변 참조는 불변에서, 가변 참조는 가변에서만 생성할 수 있습니다.
  3. 참조가 차용된 경우 다른 값들도 연결되어 있으므로 이동시킬 수 없습니다.

참조 해제

참조는 별표 *를 사용하여 연결된 값에서 참조 해제를 진행할 수 있습니다.

참조를 해제하는 경우에는 사본을 생성하게 됩니다. 해당 값에 복사 능력이 있는지를 확인하세요.

module M {
    struct T has copy {}

    // value t here is of reference type
    public fun deref(t: &T): T {
        *t
    }
}

참조 해제 연산자는 원본 값을 현재의 스코프로 이동해 주지 않습니다. 대신 이 값의 사본을 생성합니다.

Move에서 구조체의 내부 필드를 복사하기 위해 사용할 수 있는 기법이 하나 있는데, 바로 *&입니다. 필드에 대한 참조를 해제하는 것입니다. 여기 짧은 예시가 있습니다.

module M {
    struct H has copy {}
    struct T { inner: H }

    // ...

    // we can do it even from immutable reference!
    public fun copy_inner(t: &T): H {
        *&t.inner
    }
}

By using *& (even compiler will advise you to do so) we've copied the inner value of a struct.

*&를 사용하면 구조체의 내부 값을 복사하게 됩니다(컴파일러에서도 권장하는 기능입니다).

기본형의 참조

기본형은 단순하기 때문에 참조로 전달될 필요가 없으며 복사 작업을 대신 진행하게 됩니다. 해당 유형을 값으로 하여 함수에 전달한다 하더라도 현재 스코프에 남아있을 것입니다. 일부러 move 키워드를 사용할 수는 있으나, 기본형은 크기가 매우 작기 때문에 참조나 이동을 통해 전달하는 것보다 복사하는 것이 더 저렴할 수도 있습니다.

script {
    use {{sender}}::M;

    fun main() {
        let a = 10;
        M::do_smth(a);
        let _ = a;
    }
}

이 스크립트는 a를 참조로 전달하지 않았음에도 컴파일 될 것입니다. VM에서 이미 배치해 두었기 때문에 copy를 추가할 필요는 없습니다.

제네릭 이해하기

제네릭은 블록체인 세계에서 Move 언어가 독특성을 지니게 하며 Move의 유연성의 근원으로 작용하기 때문에 Move에 필수불가결한 요소입니다.

우선 Rust Book 에서 인용하자면 제네릭은 구체적인 유형 또는 기타 속성을 대신하는 추상적인 대역입니다. 실제적인 측면에서 이야기하자면 제네릭은 단일 함수를 작성할 때 사용되는 방법으로, 어떠한 유형에도 사용할 수 있으며 이렇게 작성한 함수는 모든 유형의 견본 취급자로 사용될 수 있기 때문에 견본이라고 칭하기도 합니다.

Move에서 제네릭은 structfunction의 서명에 적용될 수 있습니다.

구조체 내부에서의 정의

우선 u64 값을 저장하는 상자인 Box를 생성합시다. 이미 진행해 본 작업이므로 주석은 생략하겠습니다.

module Storage {
    struct Box {
        value: u64
    }
}

이 상자는 u64 유형의 값만 저장할 수 있다는 건 자명한 사실입니다. 그러나 동일한 상자를 u8 유형이나 bool형에 대응하도록 생성하고 싶다면 어떻게 할까요? Box1Box2를 생성하는 게 좋을까요? 아니면 다른 모듈을 발행해야 할까요? 둘 다 오답입니다. 제네릭을 사용하면 되니까요.

module Storage {
    struct Box<T> {
        value: T
    }
}

구조체 이름 옆에 <T>를 입력했습니다. 부등호 기호 <..>들은 제네릭의 유형을 정의하기 위해 사용하며, T는 이 구조체에서 우리가 견본으로 삼은 유형에 해당합니다. 구조체 바디 정의 내부에서는 T를 일반 유형으로 사용했습니다. T라는 유형은 실존하는 것이 아니라, 모든 유형이 들어올 수 있도록 하는 문자입니다.

함수 내 서명

이제 u64 유형을 우선 값으로 사용할 이 구조체에 생성자를 생성하겠습니다.

module Storage {
    struct Box<T> {
        value: T
    }

    // type u64 is put into angle brackets meaning
    // that we're using Box with type u64
    public fun create_box(value: u64): Box<u64> {
        Box<u64>{ value }
    }
}

제네릭은 명시된 유형 매개 변수를 가져야 하기 때문에 약간 더 복잡한 정의를 지니는데, 따라서 일반 구조체 BoxBox<u64>가 됩니다. 제네릭의 부등호 기호 안에는 모든 유형을 전달할 수 있습니다. create_box메서드를 더욱 일반성을 띠도록 하여 사용자들이 유형을 명시할 수 있게 처리해 줍시다. 다른 제네릭을 함수 서명에 활용함으로써 말입니다

module Storage {
    // ...
    public fun create_box<T>(value: T): Box<T> {
        Box<T> { value }
    }

    // we'll get to this a bit later, trust me
    public fun value<T: copy>(box: &Box<T>): T {
        *&box.value
    }
}

함수 호출에서의 사용

우리는 방금 구조체에서 진행했던 방식과 동일하게, 부등호들을 함수 서명에서 함수의 이름 바로 뒤에 추가했습니다. 이제 이 함수를 어떻게 사용하면 좋을까요? 함수 호출 상에서 유형을 명시해 주면 됩니다.

script {
    use {{sender}}::Storage;
    use 0x1::Debug;

    fun main() {
        // value will be of type Storage::Box<bool>
        let bool_box = Storage::create_box<bool>(true);
        let bool_val = Storage::value(&bool_box);

        assert(bool_val, 0);

        // we can do the same with integer
        let u64_box = Storage::create_box<u64>(1000000);
        let _ = Storage::value(&u64_box);

        // let's do the same with another box!
        let u64_box_in_box = Storage::create_box<Storage::Box<u64>>(u64_box);

        // accessing value of this box in box will be tricky :)
        // Box<u64> is a type and Box<Box<u64>> is also a type
        let value: u64 = Storage::value<u64>(
            &Storage::value<Storage::Box<u64>>( // Box<u64> type
                &u64_box_in_box // Box<Box<u64>> type
            )
        );

        // you've already seen Debug::print<T> method
        // which also uses generics to print any type
        Debug::print<u64>(&value);
    }
}

여기에서는 3가지 유형, 즉 bool형, u64 그리고 Box<u64>를 토대로 Box 구조체를 사용했습니다. 마지막 유형은 굉장히 복잡해 보이겠지만 작동 방식을 이해하고 좀 더 친숙해 진 다음에는 루틴의 일부가 될 것입니다.

더 진행하기에 앞서 잠시 되돌아갑시다. Box 구조체에 제네릭을 추가함으로써 이 상자는 추상적인 성격을 지니게 되었는데, 우리가 활용할 수 있는 용량에 비해 정의는 상당히 간단한 편입니다. 이제는 u64, address, 심지어 다른 box나 구조체를 아우르는 모든 유형을 가지는 Box를 생성할 수 있습니다.

능력 확인의 제약 사항

능력 에 대해 앞서 배웠는데, 제네릭에서는 능력을 “확인” 하거나 제약하게 됩니다. 제약의 경우 대응하는 능력에 따라 이름이 결정됩니다.

fun name<T: copy>() {} // allow only values that can be copied
fun name<T: copy + drop>() {} // values can be copied and dropped
fun name<T: key + store + drop + copy>() {} // all 4 abilities are present

...또는 구조체의 경우

struct name<T: copy + drop> { value: T } // T can be copied and dropped
struct name<T: store> { value: T } // T can be stored in global storage

다음 구문을 숙지 바랍니다. +(plus)부호는 처음부터 직관적이지 않을 수 있으나, Move의 키워드 목록에서 유일하게 +를 사용하는 곳입니다.

제약이 걸린 시스템의 예시입니다.

module Storage {

    // contents of the box can be stored
    struct Box<T: store> has key, store {
        content: T
    }
}

또한 내부 유형(또는 제네릭 유형)은 반드시 컨테이너의 능력(key를 제외한 모든 능력이 해당)을 가져야 한다는 점도 숙지해 두십시오. 조금만 생각해 보면 모든 부분은 상식적이고 직관적입니다. copy(복사) 능력이 있는 구조체라면 내용 또한 복사 능력을 가져야 합니다. 그렇지 않다면 컨테이너 객체가 복사가능한 것으로 간주될 수 없을 것입니다. Move 컴파일러는 이 논리를 따르지 않는 코드도 컴파일하도록 허용하겠지만 해당 능력들은 사용할 수 없게 될 것입니다. 다음의 예시를 참조하십시오.

module Storage {
    // non-copyable or droppable struct
    struct Error {}
    
    // constraints are not specified
    struct Box<T> has copy, drop {
        contents: T
    }

    // this method creates box with non-copyable or droppable contents
    public fun create_box(): Box<Error> {
        Box { contents: Error {} }
    }
}

이 코드는 성공적으로 컴파일 및 발행이 진행되었습니다. 그러나 실행해 보게 되면…

script {
    fun main() {
        {{sender}}::Storage::create_box() // value is created and dropped
    }   
}

Box가 제외할 수 없다는 오류가 출력됩니다.

   ┌── scripts/main.move:5:9 ───
   │
 5 │   Storage::create_box();
   │   ^^^^^^^^^^^^^^^^^^^^^ Cannot ignore values without the 'drop' ability. The value must be used
   │

이런 일이 발생하는 이유는 내부 값에 제외 능력이 없기 때문입니다. 컨테이너의 능력은 내용에 의해 자동으로 제한이 되므로, 예를 들어 복사, 제외 및 저장 능력을 가진 컨테이너 구조체가 있고 내부 구조체에는 제외 능력밖에 없다면 해당 컨테이너를 복사하거나 저장하는 것은 불가능할 것입니다. 또 다른 관점에서 볼 때 이 컨테이너는 내부 유형에 대한 제약 사항을 가질 필요가 없이, 내부에 어떤 유형이 들어있든지 사용될 수 있는 유연성을 확보하는 것도 가능합니다.

그러나 실수를 피하기 위해 항상 점검을 게을리하지 말고, 필요하다면 함수와 구조체에서 제네릭 관련 제약사항을 명시하는 것이 좋습니다.

아래 구조체가 보다 안전한 예시가 되겠습니다.

// we add parent's constraints
// now inner type MUST be copyable and droppable
struct Box<T: copy + drop> has copy, drop {
    contents: T
}

제네릭에서의 여러 유형

유형은 한 개에서 그치지 않고 여러 개를 사용하는 것도 가능합니다. 제네릭 유형들은 부등호 기호 내부에 입력되며 쉼표로 분리합니다. 2가지 다른 유형을 가지는 상자 2개를 포함하는 새로운 유형인 Shelf를 추가해 봅시다.

module Storage {

    struct Box<T> {
        value: T
    }

    struct Shelf<T1, T2> {
        box_1: Box<T1>,
        box_2: Box<T2>
    }

    public fun create_shelf<Type1, Type2>(
        box_1: Box<Type1>,
        box_2: Box<Type2>
    ): Shelf<Type1, Type2> {
        Shelf {
            box_1,
            box_2
        }
    }
}

Shelf에 대응하는 유형 매개 변수를 수록하여 구조체의 필드 정의에 대응시켰습니다. 또한 여기에서 볼 수 있듯 제네릭 내부에 위치한 유형 매개 변수의 이름은 관계없습니다. 적절한 이름만 선택해 주면 되겠고, 각 유형 매개 변수는 정의 내부에서만 유효하기 때문에 T1이나 T2T와 대응시킬 필요는 없습니다.

다수의 제네릭 유형 매개 변수를 사용하는 것은 단일 변수의 사용 때와 크게 다르지 않습니다.

script {
    use {{sender}}::Storage;

    fun main() {
        let b1 = Storage::create_box<u64>(100);
        let b2 = Storage::create_box<u64>(200);

        // you can use any types - so same ones are also valid
        let _ = Storage::create_shelf<u64, u64>(b1, b2);
    }
}

하나의 정의에서는 최대 18,446,744,073,709,551,615 (u64 크기) 개의 제네릭을 사용할 수 있습니다. 물론 이런 제한에 도달할 일은 전혀 없으니, 제한 받을 염려 없이 마음껏 필요한 만큼 사용하시면 됩니다.

사용되지 않은 유형의 매개 변수

제네릭에서 명시된 모든 유형이 사용될 필요는 없습니다. 다음의 예시를 참고해 주십시오.

module Storage {

    // these two types will be used to mark
    // where box will be sent when it's taken from shelf
    struct Abroad {}
    struct Local {}

    // modified Box will have target property
    struct Box<T, Destination> {
        value: T
    }

    public fun create_box<T, Dest>(value: T): Box<T, Dest> {
        Box { value }
    }
}

때로는 작업에 제네릭을 제약 또는 상수로 사용하는 것도 적절한 선택입니다. 스크립트에서의 사용법을 함께 보도록 합시다.


script {
    use {{sender}}::Storage;

    fun main() {
        // value will be of type Storage::Box<bool>
        let _ = Storage::create_box<bool, Storage::Abroad>(true);
        let _ = Storage::create_box<u64, Storage::Abroad>(1000);

        let _ = Storage::create_box<u128, Storage::Local>(1000);
        let _ = Storage::create_box<address, Storage::Local>(0x1);

        // or even u64 destination!
        let _ = Storage::create_box<address, u64>(0x1);
    }
}

여기에서는 제네릭을 사용하여 유형을 표시했지만, 실제로 사용하지는 않습니다. 왜 이러한 정의가 중요한지는 자원 개념을 배울 때 함께 습득하시게 될 것입니다. 우선 지금은 제네릭의 또 다른 사용법이라고만 이해하셔도 무방합니다.

벡터로 집합 관리하기

본인만의 유형을 생성하고 복잡한 데이터를 저장하도록 하는 struct 유형은 이미 익숙하실 것입니다. 하지만 가끔은 좀 더 동적이고, 확장성과 관리성이 좋은 유형이 필요할 때가 있습니다. Move에서는 벡터가 그러한 기능을 담당합니다.

Vector is a built-in type for storing collections of data. It is a generic solution for collection of any type (but only one). As its functionality is given to you by the VM; the only way to work with it is by using the Move standard library and native functions.

벡터는 데이터 집합을 저장하는 역할을 맡는 내장된 유형입니다. 벡터는 모든 종류의 단일 유형에 대응하는 제네릭 솔루션입니다. 이 기능은 실제 Move 언어가 아니라 VM에서 제공하는 것이므로, Move standard librarynative 함수를 사용해야만 작업할 수 있습니다.

script {
    use 0x1::Vector;

    fun main() {
        // use generics to create an emtpy vector
        let a = Vector::empty<&u8>();
        let i = 0;

        // let's fill it with data
        while (i < 10) {
            Vector::push_back(&mut a, i);
            i = i + 1;
        }

        // now print vector length
        let a_len = Vector::length(&a);
        0x1::Debug::print<u64>(&a_len);

        // then remove 2 elements from it
        Vector::pop_back(&mut a);
        Vector::pop_back(&mut a);

        // and print length again
        let a_len = Vector::length(&a);
        0x1::Debug::print<u64>(&a_len);
    }
}

벡터는 단일 비참조 유형에 해당하는 값을 최대 u64 크기까지 저장할 수 있습니다. 대형 저장소를 관리함에 있어 벡터가 어떤 도움이 되는지를 살펴보기 위해 모듈을 작성해 보겠습니다.

module Shelf {

    use 0x1::Vector;

    struct Box<T> {
        value: T
    }

    struct Shelf<T> {
        boxes: vector<Box<T>>
    }

    public fun create_box<T>(value: T): Box<T> {
        Box { value }
    }

    // this method will be inaccessible for non-copyable contents
    public fun value<T: copy>(box: &Box<T>): T {
        *&box.value
    }

    public fun create<T>(): Shelf<T> {
        Shelf {
            boxes: Vector::empty<Box<T>>()
        }
    }

    // box value is moved to the vector
    public fun put<T>(shelf: &mut Shelf<T>, box: Box<T>) {
        Vector::push_back<Box<T>>(&mut shelf.boxes, box);
    }

    public fun remove<T>(shelf: &mut Shelf<T>): Box<T> {
        Vector::pop_back<Box<T>>(&mut shelf.boxes)
    }

    public fun size<T>(shelf: &Shelf<T>): u64 {
        Vector::length<Box<T>>(&shelf.boxes)
    }
}

우선 shelf와 이에 대응하는 box 몇 개를 생성하고 모듈에 벡터를 반영하여 작업을 진행해 보겠습니다.

script {
    use {{sender}}::Shelf;

    fun main() {

        // create shelf and 2 boxes of type u64
        let shelf = Shelf::create<u64>();
        let box_1 = Shelf::create_box<u64>(99);
        let box_2 = Shelf::create_box<u64>(999);

        // put both boxes to shelf
        Shelf::put(&mut shelf, box_1);
        Shelf::put(&mut shelf, box_2);

        // prints size - 2
        0x1::Debug::print<u64>(&Shelf::size<u64>(&shelf));

        // then take one from shelf (last one pushed)
        let take_back = Shelf::remove(&mut shelf);
        let value     = Shelf::value<u64>(&take_back);

        // verify that the box we took back is one with 999
        assert(value == 999, 1);

        // and print size again - 1
        0x1::Debug::print<u64>(&Shelf::size<u64>(&shelf));
    }
}

Vectors are very powerful. They allow you to store huge amounts of data (max length is 18446744073709551615) and to work with it inside indexed storage.

벡터는 아주 강력합니다. 최대 길이 18446744073709551615 에 해당하는 대규모의 데이터를 저장할 수 있도록 하며, 색인 처리된 저장소 내부에서 작업을 진행할 수 있습니다.

인라인 벡터 정의에 대응하는 Hex 및 Bytestring 리터럴

벡터는 또한 스트링을 대표하는 역할을 맡습니다. VM은 스크립트 상 main 함수에 vector<u8>을 인수로 전달하는 방법을 지원합니다.

그러나16진법 리터럴을 사용하여 스크립트나 모듈에서 vector<u8>을 정의할 수도 있습니다.

script {

    use 0x1::Vector;

    // this is the way to accept arguments in main
    fun main(name: vector<u8>) {
        let _ = name;

        // and this is how you use literals
        // this is a "hello world" string!
        let str = x"68656c6c6f20776f726c64";

        // hex literal gives you vector<u8> as well
        Vector::length<u8>(&str);
    }
}

Bytestring 리터럴을 사용하면 좀 더 단순하게 접근할 수 있습니다.

script {

    fun main() {
        let _ = b"hello world";
    }
}

ASCII 스트링으로 취급되며 마찬가지로 vector<u8>로 해석됩니다.

벡터 공략집

표준 라이브러리에서 제공하는 벡터 메서드 관련 공략집입니다.

  • <E> 형 Empty 벡터 생성
Vector::empty<E>(): vector<E>;
  • 벡터 길이 확인
Vector::length<E>(v: &vector<E>): u64;
  • 벡터 끝으로 element 밀기:
Vector::push_back<E>(v: &mut vector<E>, e: E);
  • 벡터의 element에 대한 가변성 확보. 불변성 확인 필요 시 Vector::borrow() 사용
Vector::borrow_mut<E>(v: &mut vector<E>, i: u64): &E;
  • 벡터 끝에서 element pop하기:
Vector::pop_back<E>(v: &mut vector<E>): E;

Move 라이브러리 내 벡터 모듈 관련 사항: link

프로그래밍 가능한 자원

이제 드디어 Move의 핵심 기능인 자원에 대해 배워보도록 하겠습니다. Move를 독특하고, 안전하고 강력하게 만들어주는 요소입니다.

우선 Diem 개발자 웹사이트(Libra에서 Diem으로 명칭이 변경된 뒤 소스 페이지를 제거하였음) 에서 핵심 요점들을 함께 보면서 시작하겠습니다.

  1. Move의 핵심 기능은 맞춤형 자원 유형을 정의할 수 있는 능력입니다. 자원 유형은 풍부한 프로그래밍 제작성을 토대로 안전한 디지털 자산을 인코딩하기 위해 사용됩니다.
  2. 자원은 언어 상 일반적인 값들입니다. 데이터 구조로 저장될 수 있고, 절차들에 인수 형태로 전달되거나 반환되는 등의 작업들을 진행할 수 있습니다.

자원은 특별한 유형의 구조에 해당하는데, Move 코드에서 곧바로 자원을 정의하고 새로 생성하거나 기존의 자원을 사용하는 것 모두가 가능합니다. 따라서 다른 데이터(벡터나 구조체 등)를 사용할 때와 동일한 방법으로 디지털 자산을 관리할 수 있습니다.

  1. Move 유형의 시스템은 자원에 대하여 특별한 안전 보장을 제공합니다. Move의 자원은 절대로 복제, 재활용 또는 폐기될 수 없습니다. 모든 자원 유형은 해당 유형을 정의한 모듈을 통해서만 생성 또는 파괴할 수 있습니다. 이러한 보장은 Move 가상머신에서 바이트코드 인증을 거쳐 정적으로 진행됩니다. Move의 가상머신은 바이트코드 인증자를 통과하지 않은 코드를 실행하지 않습니다.

참조 및 소유권 장에서 Move가 스코프의 보안을 확보하고 변수의 소유자 스코프를 제어하는 방식에 대해 함께 살펴보았습니다. 그리고 제네릭 장에서는 유형 일치에 관한 특별한 방법을 통해 복사 가능/불가능 유형을 분리할 수 있다는 점을 다루었습니다. 이러한 모든 기능은 자원 유형이 더욱 안전하도록 만들어 줍니다.

  1. 모든 Diem 화폐는 제네릭 Diem::T 유형을 사용하여 구현됩니다. 예를 들어 LBR 화폐는 Diem::T<LBR::T> 로 나타나며, 가상 USD 화폐는 Diem::T<USD::T>로 구현될 것입니다. Diem::T 는 언어 특성 상 특별한 계층적 지위가 없으며, 모든 Move 자원은 동일한 수준의 보호 혜택을 누리게 됩니다.

Diem 화폐와 마찬가지로, 다른 화폐나 다른 자산 유형도 Move를 통해 구현할 수 있습니다.

추가 참고 자료

서명자로서의 전송자

리소스 사용에 대해 설명하기에 앞서 서명자(signer) 유형에 및 그 존재 이유에 대해서 이해가 선행되어야 합니다.

signer는 네이티브 복사불가능한 (유사 리소스) 유형으로, 거래에서 전송자의 주소를 담고 있습니다.

signer 유형은 전송 권한을 의미합니다. 다시 말하면, signer를 사용한다는 것은 전송자의 주소 및 리소스에 접근한다는 것을 의미합니다. 실제로 서명이나 서명하는 행위와는 관계가 없으며, Move VM에서는 단순히 전송자를 의미합니다.

signer 유형은 오직 하나의 기능을 갖습니다 – 드롭(Drop)

스크립트에서의 signer

Signer는 네이티브 유형이기에 생성이 필요하다. 하지만 vector 와는 다르게 코드로 직접 생성할 수는 없으며, 스크립트 인자(script argument)로 받아야 합니다:

script {
    // signer is an owned value
    fun main(account: signer) {
        let _ = account;
    }
}

signer 인자는 VM이 자동적으로 스크립트에 추가되기에, 사용자가 직접 전송하거나 스크립트에 추가할 필요가 없으며 직접 추가할 방법도 없습니다. 나아가, signer 인자는 항상 *참조(reference)*입니다. 표준 라이브러리 (Diem의 경우 – DiemAccount)는 실제 signer 값에 접근할 수 있지만, 해당 값을 사용하는 함수는 접근제한(private)이며, signer 값을 사용하거나 전송할 수 없습니다.

현재로써 signer 유형을 담고 있는 변수의 정규 명칭(canonical name)은 *어카운트(account)*입니다.

표준 라이브러리에서의 signer 모듈

네이티브 유형은 네이티브 함수를 필요로 하고, signer 유형의 경우에는 0x1::Signer 입니다. 해당 모듈은 비교적 단순합니다. (Diem에서 원본 모듈에 대한 링크):

module Signer {
    // Borrows the address of the signer
    // Conceptually, you can think of the `signer`
    // as being a resource struct wrapper arround an address
    // ```
    // resource struct Signer { addr: address }
    // ```
    // `borrow_address` borrows this inner field
    native public fun borrow_address(s: &signer): &address;

    // Copies the address of the signer
    public fun address_of(s: &signer): address {
        *borrow_address(s)
    }
}

보다시피 2개의 메소드가 있으며 그 중 하나는 네이티브이고, 다른 하나는 역참조 연산자를 사용하여 주소를 복사하기에 보다 간편합니다.

모듈을 사용하는 것 역시 이처럼 단순합니다.

script {
    fun main(account: signer) {
        let _ : address = 0x1::Signer::address_of(&account);
    }
}

모듈 내 signer

module M {
    use 0x1::Signer;

    // let's proxy Signer::address_of
    public fun get_address(account: signer): address {
        Signer::address_of(&account)
    }
}

&signer 유형을 인자로 사용하는 메소드는 전송자의 주소를 사용한다는 점을 명시적으로 보여줍니다.

해당 유형을 사용한 이유 중 하나는 전송 권한을 요구하는 메소드와 요구하지 않는 메소드를 구분하기 위함입니다. 따라서 메소드가 사용자를 기망하여 리소스에 대해 권한 없는 접근을 허용하지 않습니다.

추가 자료 및 PR

리소스란 무엇인가

리소스는 Move 백서에서 설명하는 개념입니다. 본래는 독자적인 유형으로 적용되었으나 이후 기능이 추가되면서 2개의 기능으로 대체되었습니다: KeyStore. 리소스는 디지털 자산을 저장하는데 있어 완벽한 유형을 목표로 하며, 복사불가하고 드롭이 불가능해야 합니다. 하지만 이와 동시에 저장 가능하고 계좌간 전송이 가능해야 합니다.

정의

리소스는 오직 keystore 기능만 갖는 구조체(struct)입니다.

module M {
    struct T has key, store {
        field: u8
    }
}

키 및 스토어 기능

Key ability allows struct to be used as a storage identifier. In other words, key is an ability to be stored as at top-level and be a storage; while store is the ability to be stored under key. You will see how it works in the next chapter. For now keep in mind that even primitive types have store ability - they can be stored, but yet they don't have key and cannot be used as a top-level containers.

Store ability allows value to be stored. That simple.

키 기능은 구조체를 스토어 식별자로 사용할 수 있게 합니다. 다시 말하면, key 는 톱-레벨로 저장하며 저장소로 작동하는 기능이고; store는 키 아래에 저장될 수 있는 기능입니다. 다음 장에서 해당 구조가 어떻게 작동하는지 알 수 있습니다. 현재로서는 기본 유형도 스토어 기능을 갖고 있다는 점을 기억해야 합니다. – 저장은 가능하지만 key 가 없으며 톱-레벨 컨테이너(container)로 사용할 수는 없습니다.

스토어 기능은 값을 저장할 수 있게 하여 그 정도로 간단합니다.

리소스 개념

본래 리소스는 Move에서 자체적인 유형이 있었으나, 기능이 추가되면서 및/또는 스토어 기능으로 적용하는 보다 추상적인 개념이 되었습니다. 리소스에 대한 설명은 다음과 같습니다:

  1. 리소스는 계좌(account)에 저장된다 – 따라서 계좌에 배정되었을 시에만 존재하며, 해당 계좌를 통해서만 접근할 수 있습니다;
  2. 계좌는 하나의 유형을 갖는 하나의 리소스만 담을 수 있으며, 그 리소스는 key 기능을 가져야 합니다;
  3. 리소스는 복사하거나 드롭 할 수 없지만 저장할 수는 있습니다.
  4. 리소스 값은 반드시 사용되어야 합니다. 리소스를 생성하거나 계좌에서 가져왔을 경우 드롭할 수 없으며 저장하거나 분해해야 합니다.

이론은 이쯤으로 하고 실전으로 가보겠습니다!

리소스별 예시

여기서는 드디어 리소스 사용법을 배울 수 있습니다. 리소스 및 메소드를 정의하고 작업하는 과정을 설명할 것이며, 그 절차를 거치면 템플릿으로 사용할 수 있는 완전한 계약을 만들 수 있습니다.

이제 컬렉션 계약을 생성하면 다음 작업이 가능합니다:

  • 컬렉션 시작
  • 컬렉션에서 아이템을 추가 또는 제거
  • 컬렉션을 파괴

자, 이제 시작합시다!

리소스 생성 및 이동

먼저 모듈을 생성합니다.

// modules/Collection.move
module Collection {


    struct Item has store {
        // we'll think of the properties later
    }

    struct Collection has key {
        items: vector<Item>
    }
}

모듈 다음의 모듈에서 주 리소스를 호출하기 위한 규칙이 있습니다(e.g. Collection::Collection). 규칙을 따른다면 여러분의 모듈을 다른 사람들이 쉽게 읽고 사용할 수 있습니다.

생성 및 이동

기능을 갖는 컬렉션 구조체를 정의했으며, Collection 은 유형 Item 의 벡터를 담습니다. 신규 컬렉션을 시작하고 계좌에 리소스를 저장하는지 살펴봅니다. 여기서 저장되는 리소스는 전송자의 주소에 영원히 남아있습니다. 그 소유자로부터 리소스를 변조하거나 탈취할 수 없습니다.

// modules/Collection.move
module Collection {

    use 0x1::Vector;

    struct Item has store {}

    struct Collection has key {
        items: vector<Item>
    }

    /// note that &signer type is passed here!
    public fun start_collection(account: &signer) {
        move_to<Collection>(account, Collection {
            items: Vector::empty<Collection>()
        })
    }
}

Remember signer? Now you see how it in action! To move resource to account you have built-in function move_to which takes signer as a first argument and Collection as second. Signature of move_to function can be represented like:

signer를 기억 하시나요? 이제 실전에서 사용해볼 때가 되었습니다. 계좌에 리소스를 이동시키려면 내장 함수인 move_to 를 사용하여 signer 를 첫 인자로, Collection 을 두 번째로 사용합니다. move_to 함수의 시그니처는 다음과 같이 표현될 수 있습니다:


native fun move_to<T: key>(account: &signer, value: T);

이는 두 가지 결론으로 귀결됩니다.

  1. 본인의 계좌에만 리소스를 넣을 수 있으며, 타인 계좌의 signer 값에 접근할 수 없기에 리소스를 넣을 수 없습니다다.
  2. 하나의 계좌에는 하나의 유형을 갖는 하나의 리소스만을 저장할 수 있습니다. 동일한 작업을 두 번 하는 경우 기존 리소스를 파기하게 되고, 이는 발생해서는 안 됩니다. (코인이 저장되어 있는데 부주의로 인해 잔고 없음을 입력하여 저장된 모든 코인을 잃는 것을 생각해보면 됩니다!). 존재하는 리소스를 생성하려는 두 번째 시도는 에러와 함께 실패할 것입니다.

주소에서 존재 확인하기

To check if resource exists at given address Move has exists function, which signature looks similar to this.

특정 주소에서 리소스가 존재하는지 확인하기 위해서는 exists 함수를 사용하며, 시그니쳐는 다음과 같습니다.


native fun exists<T: key>(addr: address): bool;
    

제네릭 유형을 사용했기에 해당 함수는 유형에 독립적이며, 주소에 존재하는지 확인하기 위해 그 어떠한 리소스도 사용할 수 있습니다. 실제로 특정 주소에 리소스가 존재하는지 확인하는 것은 누구나 할 수 있습니다. 하지만 존재를 확인하는 것은 저장된 값에 접근하는 것을 의미하지 않습니다!

사용자가 이미 컬렉션을 갖고 있는지 확인하는 함수를 작성해 봅시다.

// modules/Collection.move
module Collection {

    struct Item has store, drop {}

    struct Collection has store, key {
        items: Item
    }

    // ... skipped ...

    /// this function will check if resource exists at address
    public fun exists_at(at: address): bool {
        exists<Collection>(at)
    }
}

리소스를 생성하고, 전송자에게 이동하며, 리소스가 이미 존재하는지 확인할 수 있습니다. 이제는 리소스를 읽고 수정하는 방법을 배울 시간입니다!

리소스 읽기 및 수정

To read and modify resource Move has two more built-in functions. Their names perfectly match their goals: borrow_global and borrow_global_mut.

리소스를 읽고 수정하기 위해 Move는 2개의 내재 함수를 갖고 있으며, 함수들의 명칭은 그 목적을 다음과 같이 정확히 명시합니다: borrow_globalborrow_global_mut.

borrow_global를 사용한 불변형 대여

소유 및 참조에 대한 부분에서 변형 (&mut) 및 불변형 참조에 대해서 이미 학습하였습니다. 그 지식을 실전에 적용할 때입니다!

// modules/Collection.move
module Collection {

    // added a dependency here!
    use 0x1::Signer;
    use 0x1::Vector;

    struct Item has store, drop {}
    struct Collection has key, store {
        items: vector<Item>
    }

    // ... skipped ...

    /// get collection size
    /// mind keyword acquires!
    public fun size(account: &signer): u64 acquires Collection {
        let owner = Signer::address_of(account);
        let collection = borrow_global<Collection>(owner);

        Vector::length(&collection.items)
    }
}

많은 일들이 일어난 것 같습니다. 먼저 메소드 시그니처부터 다룰텐데요, 전역 함수 borrow_global<T> 는 리소스 T에 대해 불변 참조를 갖습니다. 그 시그니처는 다음과 같습니다.


native fun borrow_global<T: key>(addr: address): &T;

해당 함수를 사용하면 특정 주소에 저장된 리소스에 대해 읽기 권한을 얻습니다. 이는 해당 모듈은 그 어떠한 주소에 있는 리소스도 읽을 수 있다는 점을 의미합니다. (해당 기능을 적용할 경우)

추가 결론: 대여 검사로 인해 리소스나 그 내용으로 참조를 되돌릴 수 없습니다. (원본 리소스 참조는 스코프 범위에서 막합니다.)

리소스는 복사불가 유형이기에 역참조 연산자 '*' 를 사용하는 것은 불가능합니다.

키워드 확보

설명이 필요한 세부내용 하나가 더 있는데요, 바로 함수 반환값 이후에 넣는 키워드 acquires 입니다. 해당 키워드는 함수에서 확보한 모든 리소스를 명시적으로 정의합니다. 실제로는 중첩함수가 리소스를 확보하더라도 각 확보한 리소스를 구체화해야 합니다. 즉, 부모 스코프는 확보 목록에 리소스를 특정해야 합니다.

acquires 를 포함하는 함수에 대한 문법은 다음과 같습니다.


fun <name>(<args...>): <ret_type> acquires T, T1 ... {

borrow_global_mut를 사용하는 변형 대여

리소스에 변형 참조를 하려면 borrow_global 에 add _mut 를 추가하면 된다. 컬렉션에 새로운 아이템 (현재는 비어 있는)을 추가하는 함수를 작성해봅시다.

module Collection {

    // ... skipped ...

    public fun add_item(account: &signer) acquires T {
        let collection = borrow_global_mut<T>(Signer::address_of(account));

        Vector::push_back(&mut collection.items, Item {});
    }
}

리소스에 대한 변형 참조는 그 내용에 변형 참조를 생성 가능하게 합니다. 따라서 예시 에서처럼 내부 벡터 item을 수정할 수 있습니다.

borrow_global_mut 의 시그니처는 다음과 같습니다:


native fun borrow_global_mut<T: key>(addr: address): &mut T;

리소스 이전 및 파괴

Final function of this section is move_from which takes resource from account. We'll implement destroy function which will move collection resource from account and will destroy its contents.

이 장에서 다룰 마지막 함수는 move_from 으로, 계좌에서 리소스를 이전한다. destroy 함수는 계좌에서 컬렉션 리소스를 옮기고 그 내용을 파괴합니다.

// modules/Collection.move
module Collection {

    // ... skipped ...

    public fun destroy(account: &signer) acquires Collection {

        // account no longer has resource attached
        let collection = move_from<Collection>(Signer::address_of(account));

        // now we must use resource value - we'll destructure it
        // look carefully - Items must have drop ability
        let Collection { items: _ } = collection;

        // done. resource destroyed
    }
}

리소스 값은 사용되어야 합니다. 따라서 계좌로부터 가져온 리소스는 분해하거나 반환값으로 전달해야 합니다. 염두해야 할 점은 값을 외부로 전달하여 스크립트에 포함하더라도 할 수 있는 행동은 제한되는데, 스크립트는 구조체나 리소스를 다른 곳으로 전달하는 것 외에는 다른 작업을 허용하지 않습니다. 이를 염두에 두고 모듈을 적절하게 설계하여 사용자들이 반환된 리소스로 할 수 있는 선택지를 주어야 합니다.

마지막 시그니처는 다음과 같습니다.


native fun move_from<T: key>(addr: address): T;

추가 단계

이 장에서는 Move 문법에서 리소스 제한이 표현되는 방식을 살펴보았습니다. 나아가 리소스를 생성, 검증, 접근, 수정, 및 파괴하는 방법도 배웠습니다. 이 장은 백서에서 마지막 부분이지만, 해당 모듈을 수정하여 필요에 따라 다음 선택지를 고려할 수 있습니다.

  1. 모듈을 수정하여 제네릭으로 모든 유형을 지원하도록 하기;
  2. Offer module을 검토하여 다른 계좌에 컬렉션을 제시하는 다른 방법을 생각하기.

GitHub 에서 모듈 컬렉션의 코드 원본을 확인할 수 있습니다.

튜토리얼

이 장에서는 Move언어의 기본 사용에 대한 튜토리얼 및 그 능력을 시현해 볼 수 있습니다.

ERC20 토큰 작성

예정...

번역본

타 언어로도 열람 가능합니다.:

수정요청

오타, 오역 등 정정사항을 반영하고자 하실 경우 GitHub를 통해 이슈제기 또는 PR을 제출하시기 바랍니다.