Crypto Zombie로 Solidity 처음부터 공부하기 [Lesson 2]

Sungmin Oh·2021년 3월 14일
0

주소

이더리움 블록체인은 은행 계좌와 비슷한 '계정'들로 이루어져 있다. 각 계정은 블록체인 상의 통화인 '이더'를 잔고로 보유하고 있다. 계좌 간에 돈을 이체할 수 있듯이 계정을 통해 다른 계정과 이더를 주고 받을 수 있다.
은행 계좌가 계좌번호를 가지고 있듯이 각 계정들도 고유한 주소를 가지고 있고, 주소를 알면 이더를 보내줄 수 있다. 즉, 특정 주소는 특정 유저가 소유하는 것이다. 주소는 아래와 같은 방식으로 나타낸다.

0x0cE446255506E92DF41614C46F1d6df9Cc969183

솔리디티에서는 주소를 address라는 자료형으로 표현할 수 있다.

매핑

매핑은 구조체와 비슷하게 구조화된 데이터를 저장하는 또다른 자료형이다. 구조체에서 각 데이터가 평등한 지위를 가지고 저장되는 것과 달리, 매핑에서는 두 개의 데이터가 각각 키, 혹은 값 이라는 지위를 부여받고 저장되는 점이 특징이다. 따라서 매핑을 키-값 (key-value) 저장소라고 할 수 있다. 매핑에 키와 값이 쌍으로 저장됐을 때, 해당 키를 넣으면 값에 해당하는 자료가 검색되는 식이다. 매핑은 다음과 같이 선언한다.

mapping (키의 자료형 => 값의 자료형) 매핑의 이름;

매핑을 만들었으면 매핑에 값을 저장해야한다. 값을 저장할 때는 아래와 같은 방법으로 한다.

매핑의 이름[키] = 값;

저장된 매핑에 키를 넣어서 값을 반환받고 싶을 때는 아래와 같이 하면 된다.

매핑의 이름[키];

  • 예시
mapping (int => string) findNameFromNumber;
findNameFromNumber[940424100000] = 'Alice';
findNameFromNumber[940424100000];  // 'Alice'가 반환된다.

위의 예시에서 int형을 키로, string 형을 값으로 가지는 매핑을 선언했다. 이 매핑에 주민등록번호와 이름을 쌍으로 저장해서 주민등록번호를 입력하면 이름을 검색해주는 프로그램을 만들 수 있을 것이다.

msg.sender

따로 저장하지 않아도 솔리디티의 모든 함수에서 사용 가능한 변수 중의 하나이다. msg.sender는 해당 함수를 호출한 사람의 주소를 가리킨다.

require

require는 이름에서도 유츄할 수 있듯이 조건문의 일종이다. 프로그램 실행 중에 require문을 만났을 때, require문 내부의 조건이 참일 때는 정상적으로 진행되지만 조건이 참이 아닐 때 함수가 에러 메세지를 발생시키고 프로그램 실행을 멈추게 된다.

  • 예시
function howToUseRequire (int _num) returns (string) {
    require(_num == 5);
    return("That is correct!");
}

위의 예시에서 함수에 5를 입력하면 "That is correct!"라는 문자열이 출력되겠지만, 그 이외의 숫자를 입력하면 에러 메세지가 발생하며 프로그램이 멈출 것이다.

상속

코드가 길어질 때 엄청나게 긴 컨트랙트를 만드는 것 보다 여러 컨트랙트에 코드 로직을 나누는 것이 합리적이다. 이러한 과정을 관리하기 쉽게 하는 솔리디티의 기능이 상속이다. 상속은 아래와 같은 방법으로 사용한다.

contract 컨트랙트 is 상속할 컨트랙트 {

}

예를 들어서 A 컨트랙트가 B 컨트랙트를 상속한다면, A 컨트랙트에서는 B 컨트랙트에서 public으로 선언된 모든 함수에 접근이 가능하다.

import

코드가 길어질 때 여러 컨트랙트로 나누는 것 뿐만 아니라 각 코드를 각각의 파일로 따로 보관하면 관리하기 더 편할 것이다. 이 때, 한 파일에서 다른 파일의 코드를 읽어와야 할 때 import를 사용한다. import는 아래와 같은 방법으로 사용한다.

import "불러올 파일의 이름";

storage vs memory

  • storage
    • 변수가 블록체인 상에 영구적으로 저장됨
    • 상태 변수(함수 외부에서 선언된 변수)의 기본 값
    • gas를 많이 소모한다.
  • memory
    • 임시적으로 저장됨
    • 함수 내부에 선언된 변수의 기본 값

일반적으로 명시적으로 나타내면서 변수를 선언할 일은 없지만 함수 내부의 구조체와 배열을 처리할 때 사용해야 할 수 있다.

함수 접근 제어자

우리는 Lesson 1에서 함수 접근 제어자로 public과 private을 배웠다.

  • public : 누구나 이 함수를 호출 가능함
  • private : 다른 컨트랙트에서 이 함수를 호출할 수 없음

Lesson 2에서 두 개의 함수 접근 제어자를 더 배운다.

  • internal
    함수가 정의된 컨트랙트를 상속하는 컨트랙트에서도 접근이 가능하다는 점을 제외하면 private과 같다.
  • external
    함수가 컨트랙트 바깥에서만 호출될 수 있고 컨트랙트 내의 다른 함수에 의해 호출될 수 없다는 점을 제외하면 public과 같다.

인터페이스

블록체인 상에 있으면서 우리가 소유하지 않은 컨트랙트와 우리 컨트랙트가 상호작용을 하려면 인터페이스를 정의해야 한다. 블록체인 상의 아래와 같은 컨트랙트가 존재한다고 해보자.

contract LuckyNumber {
  mapping(address => uint) numbers;

  function setNum(uint _num) public {
    numbers[msg.sender] = _num;
  }

  function getNum(address _myAddress) public view returns (uint) {
    return numbers[_myAddress];
  }
}

이 컨트랙트의 getNum 함수를 우리의 코드에서 사용하고자 할 때, 우리 코드에 인터페이스를 아래와 같이 정의해야 한다.

contract NumberInterface {
    function getNum(address _myAddress) public view returns (uint) ;
}

인터페이스를 정의하는 것이 마치 새로운 컨트랙트를 생성하고 그 컨트랙트 내부에 함수를 선언하는 것과 비슷하게 보인다. 하지만 여기에는 몇 가지 차이점이 있다.

  • 함수 몸체를 선언하지 않는다. (중괄호를 쓰지 않고 세미콜론으로 끝낸다.)
  • 다른 컨트랙트와 상호작용하고자 하는 함수만을 선언하고 다른 함수나 변수는 선언하지 않는다.

이제는 이 인터페이스를 내 컨트랙트에서 활용하는 방법을 알아보자.

contract MyContract {
    address NumberInterfaceAddress = 0xab38...
    // 우리가 이용하고자 하는 이더리움 상의 컨트랙트 주소이다.
    NumberInterface numberContract = NumberInterface(NumberInterfaceAddress)
    // numberContract라는 NumberInterface를 생성했고, 이제 numberContract는 다른 컨트랙트를 가리킨다.
    
    function someFunction() public {
    uint num = numberContract.getNum(msg.sender);
    // 이렇게 getNum 함수를 호출할 수 있다.
    }
}

이런 방식으로 이더리움 상의 다른 컨트랙트와 상호작용 할 수 있다. 다만 상호작용하는 함수가 public이나 external로 선언되어 있어야 한다.

  • 정리
contract 인터페이스의 이름 {
    function 함수의 이름(자료형 인자);
}
// 인터페이스 정의하기


contract 컨트랙트A {
    address 변수a = 상호작용 하려고 하는 컨트랙트의 주소;
    인터페이스의 이름 컨트랙트B = 인터페이스의 이름(변수a);
    
    컨트랙트B.상호작용하려는함수();
}

이 내용은 정보를 공유하기 위함이 아닌 저의 개인적인 공부 기록을 남기기 위함입니다. 따라서 틀린 정보가 있을 수 있으니 유의해서 봐주시면 감사하겠습니다.

profile
ambitious person

0개의 댓글