DreamStory Contract 코딩

이전에 스마트 컨트랙트에 사용할 변수, 함수 등 모든 것을 기술했기에 이것을 단순히 코딩하는 것은 매우 간단합니다. 지금부터는 간단히 스마트 컨트랙트를 코딩하고 솔리디티의 중요한 개념 몇 가지를 별도로 상세히 소개하겠습니다.


컨트랙트

// setup solidity compiler version
pragma solidity ^0.4.17;

// dream story contract
contract DreamStory {
 ...
}

pragma로 솔리디티 컴파일러의 버전을 제한합니다. 0.4.17이상의 컴파일러 버전과 호환되도록 설정합니다. 주의할 것은 0.4.17 이상과 호환이라고 하지만, 0.5.0 이상의 버전과 호환되는 것을 의미하지는 않습니다.

컨트랙트의 이름은 DreamStory입니다.


컨트랙트 Constructor

    /*
     * Constructor
     @param min_down_price minimum download price in wei
     */
    function DreamStory( uint _min_down_price ) public {
        // set author to message sender who is the creator of this dream story
        author= msg.sender;
        // set minimum download price
        min_down_price_wei= _min_down_price;
    }

주로 C++, Python 코딩을 하는 버릇이 있어서 솔리디티 코딩할 때도 dogygen 형태의 주석 다는 버릇이 있습니다. @param은 함수 매개변수입니다. 1개의 인자를 받습니다. Author가 꿈 일기를 올릴 때, Contributor가 사용허가를 다운로드 받는 데 최소로 필요한 금액을 입력합니다.

또 한가지 코딩 스타일인데, 함수 인자와 상태변수를 구별하기 위해 함수 인자의 경우 _min_dwn_price처럼 변수 이름 앞에 언더바를 붙입니다. 이건 구글 코딩 스타일이라고 하는데, 나쁘지 않네요.

Remix에서 Constructor 함수 이름을 Constructor로 사용하라는 경고 메시지가 나옵니다. 일단 무시하겠습니다. Constructor 함수에서는 단순히 최소 다운로드하기 위한 금액과 꿈 일기의 저자 author 변수를 설정합니다.


Download sturct

    // download struct
    struct Download {
        // address of the downloder
        address downloader;
        // download price in wei
        uint price_wei;
        // download date
        uint date;
    }

Download의 이력을 관리하기 위한 구조체입니다. 구성요소는 다운로더의 주소, 다운로드 금액, 다운로드한 날짜를 저장합니다. Download struct는 아직 인스턴스화 또는 객체화되지 않았습니다. 변수를 정의해야 객체화됩니다.


상태변수

    // download history
    Download[] public downloads;
    // author of a dream story
    address public author;
    // list of contributors as mapping
    mapping( address => bool ) public contributors;
    // number of votes which is the number of contributors
    uint public votes_count;
    // list of approval contributors
    mapping( address => bool ) public approvals;
    // number of approvals
    uint public approvals_count;
    // minimum download price in wei
    uint public min_down_price_wei;
    // list of downloaders as mapping
    mapping( address => bool ) public downloaders;

Download sturct가 비로소 배열 형태의 상태변수로 정의됩니다. 다른 변수는 주석을 보면 금방 의미를 파악할 수 있습니다. 변수명이 좀 긴 것도 있는데, 이더리움에서 ether 이외에도 wei가 기본 단위로 쓰여 확실히 구별했습니다.

중요하게 볼 부분은 mapping 변수입니다.


mapping 변수

위 상태변수에 나타난 변수를 하나 예를 들어 설명해 보겠습니다.

mapping( address => bool ) contributors; 

이렇게 변수 앞에 데이터 타입이 좀 이상하게 적혀있습니다. 다른 언어에서는 보기 어려운 방식입니다. 그러나 mapping이란 개념은 다른 언어에도 있는 것입니다. 간단히는 키-값 쌍의 데이터 타입입니다. 즉, 어떤 키와 그것의 쌍을 이루는 값을 저장하는 것입니다. 솔리디티에서는 그것을 좀 더 구체적으로 표시한 것 뿐이니, 겁먹지 마세요.

mapping( address => bool ) contributors;

이 코드의 의미는 contributors라는 변수의 데이터 타입이 mapping 타입인데, 이 mapping은 키가 address 타입이고, 값은 bool 타입이라는 것입니다.

그런데 왜 mapping 변수를 쓸까요? 그냥 address를 배열로 하면 될텐데 말이죠. address[] contributors; 이렇게 말이죠.

이유는 배열로 했을 때는 특정 주소를 찾기 위해 여러 번 반복 작업을 해야 합니다. 아시겠지만, 스마트 컨트랙트의 코드를 실행하는 주체가 작업에 대한 비용을 지불하게 되어 있습니다. 배열을 사용해서 for 문을 돌리면 그만큼의 비용을 지불해야 합니다. 만약 for문을 돌리는 회수가 증가하면 그 비용도 작지만은 않을 것입니다.

이를 방지하기 위해 조금 불편하더라도 mapping 타입의 변수를 사용합니다. mapping의 단점은 배열처럼 변수에 담고 있는 값의 개수를 알 수 없다는 것입니다. 대신에 키를 입력하면 값을 즉시 (다른 별도의 연산 없이) 얻을 수 있습니다. 그러니 maaping 변수를 사용하게 되면 이더리움에서 gas 사용이 최소로 됩니다.

mapping 변수가 담고 있는 요소의 개수를 알 수 없기 때문에, 상태변수의 경우 uint votes_count라는 별도의 변수를 만들어서 개수를 관리합니다.

mapping( address => bool ) contributors;

이 코드에서 contributor의 주소를 키로 저장하고, 값을 true로 입력하면, 나중에 해당 주소를 입력하면 contributor인지 아닌지 바로 알 수 있습니다. 배열을 사용했다면, for문을 돌려서 일일이 검사해야겠죠.


modifier

    // only for author
    modifier onlyAuthor() {
        require( msg.sender == author );
        _;
    }
    // only for contributor
    modifier onlyContributor() {
        // the sender should be in the contributors list
        require( contributors[msg.sender] );
        _;
    }

modifier는 함수를 실행할 수 있는 권한을 제한할 때 주로 사용합니다. 보다 직접적인 사용처는 반복되는 코드, 예를 들어 사용 권한을 체크하는 코드를 반복적으로 사용하지 않기 위함입니다.

반복되는 코드가 없다면 modifier는 굳이 사용하지 않아도 되나, 문법을 익히는 차원에서 사용해 봤습니다. 여기서는 두개의 modifier가 사용됩니다. 특정 함수는 author만 실행할 수 있고, 어떤 함수는 contributor만 실행할 수 있습니다.

onlyAuthor modifier는 함수를 호출하는 사람이 author인지를 체크합니다.

onlyContributor modifier는 함수를 호출하는 사람이 contributor인지를 체크합니다.

두 modifiers에서 require(...) 함수 다음에 나오는 _;도 생뚱맞게 보입니다. 저도 솔리디티에서 처음 봤는데요.

이것은 modifier가 붙은 함수의 본체를 실행하라는 의미입니다. modifier는 주로 함수의 본체 앞에 선언되어 사용되는데요, 함수의 본체가 실행되기 전에 modifier가 실행됩니다. 그래서 modifier가 실행된 후 함수의 본체를 실행하라는 명령이 바로 _;입니다.

조금 헷갈릴수 있는 점은 modifier는 함수에 여러 개 사용될 수 있습니다. 그러면 중첩되는 구조가 되는데(nesting) 이건 그냥 넘어가겠습니다. 너무 어려우면 그렇잖아요.


contribute 함수

/*
     * A contributor donates some money for a dream story.
     * So the money will be transfered to this contract address, resulted in increasing the balance
     * @note this function can receive some money
     */
    function contribute() public payable {
        // increase the vote counts
        votes_count++;
        // set contributor address to true
        contributors[ msg.sender ]= true;
    }

사용자가 꿈 일기에 일정 금액을 기부하고 싶을 때, contribute할 때 호출되는 함수입니다.

사용자는 기부할 일정 금액을 입력합니다. 함수는 이 금액을 받기 위해서 payable이라는 지시어를 사용합니다. 이게 없으면 코인을 받을 수 없습니다.

함수는 그저 contributor의 주소를 저장합니다. 이걸 mapping으로 구현하는데, mapping 변수에 키로 contributor의 주소를 입력하고 값을 true로 설정합니다. 그러면 나중에 주소를 입력하면 contributor인지 아닌지 바로 알 수 있습니다.

그런데, 사용자가 보낸 코인은 어디서 받아 쌓아두는 것일까요?

사용자는 코인을 스마트 컨트랙트로 보내는 것입니다. 그러면 스마트 컨트랙트의 기본 변수인 balance에 코인이 적립됩니다. Remix에서 나중에 테스트해보겠습니다.


download 함수

    /*
     * Download (license of) the dream story
     * @param down_price the download price in wei that a contributor is willing to pay
     */
    function download( uint _down_price ) public onlyContributor {
        // check if the contributor has downloaded before.
        // if so, no need to download again
        require( !downloaders[msg.sender] );
        // local variable is basically stored in storage,
        // and literal such as struct is created in memory since it is temporary.
        // storage variable references the original variable
        // memory variable copies the original variable
        // memory is temporary, storage is global.
        Download memory new_download= Download({
           downloader: msg.sender,
           price_wei: _down_price,
           date: now
        });

        // add it to the downloads array
        downloads.push( new_download );
    }

contributor가 꿈 일기를 다운로드하고자 다운로드 금액을 입력하였을 때 호출되는 함수입니다.

onlyContributor modifier가 사용되었습니다. 즉 contributor만 실행할 수 있는 함수입니다.

또, 함수 내부에서 이미 다운로드 받았는지 검사하는 부분이 있습니다. 이미 다운로드 받은 사람은 함수 내용을 실행하지 못합니다.

함수 내부에서는 새롭게 Download 구조체를 만들어 객체화하여 downloads 배열에 추가합니다.

여기서 솔리디티에서 중요한 부분이 또 나옵니다. 바로 storage 타입의 변수와 memory타입의 변수입니다. 이 부분도 별도로 좀 더 상세히 알아보겠습니다.


storage VS memory 변수

솔리디티에서는 변수가 저장되는 방식에 따라 변수를 크게 두가지로 나뉩니다. 한 가지가 더 있긴 하지만 중요도가 낮아서 생략하겠습니다.

스마트 컨트랙트는 블록체인 상에서 상주하는 프로그램 같은 것입니다. 따라서 스마트 컨트랙트가 관리하는 상태변수는 쉽게 내용이 지워져서는 안 됩니다. 그래서 상태변수는 파일과 유사한 형태인 storage 타입으로 저장됩니다.

반면에 함수의 인자나 변수에 대입하고자 하는 문자열(예, string str= "my smart contract"; 여기서 문자열 "my smart contract"와 같은 것을 리터럴literal이라고 합니다.)과 같은 리터럴은 파일과 같은 곳에 저장할 필요 없이 임시적으로 memory로 저장하면 되겠죠.

memory 형태의 변수는 금방 쓰고 사라질 것을 저장하는 데 사용됩니다. 컴퓨터의 RAM과 같다고 보면 되겠습니다. 컴퓨터 껐다 키면 내용이 사라지는 것이요.

중요한 것은 함수 내부에선 storage 형태의 변수와 memory 형태의 변수간에 대입을 할 때 오류가 발생할 수 있습니다. 예를 들어 memory 변수 값을 storage 변수에 대입하는 경우입니다.

memory 변수는 특정 저장위치를 갖지 않습니다. memory 변수 값을 storage 변수에 대입하려고 하면, storage 변수는 해당 내용의 저장위치를 접근해서 참조(referencing)하려고 합니다. referencing한다는 것은 같은 저장위치를 가리켜서 해당 내용이 바뀌면 그것을 가리키고 있는 모든 변수의 값이 바뀐다는 얘기입니다.

따라서 download 함수의 경우에 memory 타입인 struct를 그냥 로컬 변수인 new_download에 대입하면 에러가 발생합니다. 로컬 변수는 기본적으로 storage 형태입니다.

따라서 download 함수 코드에서도 new_download 변수 앞에 memory라는 지시어가 사용되었습니다. 이렇게 되면 memory 변수를 복사해 새로운 memory에 저장합니다.

앞으로 솔리디티 코딩할 때 매우 주의해야 하는 부분입니다. 그러나 걱정마세요. Remix에서 에러 메시지를 제대로 띄워 주니 에러가 나타나면 수정하면 됩니다!


approveWithdrawal 함수

/*
     * Approve the payment to the author by a contributor
     * @note a contributor who already approved the withdrawl cannot call it again
     */
    function approveWithdrawal() public onlyContributor {
        // check whether this contributor approved the withdrawl already
        require( !approvals[msg.sender] );
        // set the approval
        approvals[ msg.sender ]= true;
        // increase the counts
        approvals_count++;
    }

approveWithdrawal은 author가 컨트랙트에 누적된 balance를 자신의 계좌로 전송하는 것을 contributor가 허용할 때 호출됩니다. 여기서도 contributor만 실행될 수 있도록 onlyContributor modifier가 사용되었습니다. 함수 내부에서는 이미 contributor가 승인을 했다면 또 다시 승인하지 못하게 막습니다. 이후 승인한 contributor를 별도로 approvals라는 상태변수로 관리합니다. 이때도 mapping 타입 변수가 사용되었습니다. 마지막으로 승인한 contributors의 숫자를 늘립니다.


finalizeWithdrawal 함수

    /*
     * Finalize the withdrawal by the author
     */
    function finalizeWithdrawal() public onlyAuthor {
        // half of contributors should approve the withdrawal
        require( approvals_count > ( votes_count/2 ) );
        // transfer the balance to the author
        author.transfer( this.balance );
    }

author가 스마트 컨트랙트의 balance를 인출하기 위해 호출됩니다. 이 함수에도 author만 실행할 수 있게 onlyAuthor modifier가 사용되었습니다. 이 함수에서는 contributors 절반이 승인을 해야만 금액이 전송됩니다.


getSummary 함수

   /*
     * Get summary of the dream story
     * @return
     */
    function getSummary() public view returns ( uint, uint, uint, uint, uint, address )
    {
        return (
          this.balance,
          votes_count,
          downloads.length,
          min_down_price_wei,
          approvals_count,
          author
        );
    }

드디어 마지막 함수입니다. 내용은 참 길지만 스마트 컨트랙트 코딩만 봤을 때 내용이 많지 않습니다. 그렇습니다. 스마트 컨트랙트는 최소한의 코드로 작성해야 gas 비용을 줄일 수 있습니다. 코드가 길면 여러 사람이 해석하기 어려워 사용하지 않게 되고, 사용자가 gas 비용 지불을 꺼려 더욱 사용하지 않게 될 것입니다.

이 함수에서는 각 화면에 표시될 값을 단순히 반환하고 있습니다. 그러면 story details와 같은 웹페이지에 값을 표시할 수 있습니다.



오늘의 실습: mapping 변수와 storage, memory 형태의 변수 설명 부분을 다시 읽어보고 확실히 이해해 보세요.