이더리움 블록체인은 은행 계좌와 비슷한 '계정'들로 이루어져 있다. 각 계정은 블록체인 상의 통화인 '이더'를 잔고로 보유하고 있다. 계좌 간에 돈을 이체할 수 있듯이 계정을 통해 다른 계정과 이더를 주고 받을 수 있다.
은행 계좌가 계좌번호를 가지고 있듯이 각 계정들도 고유한 주소를 가지고 있고, 주소를 알면 이더를 보내줄 수 있다. 즉, 특정 주소는 특정 유저가 소유하는 것이다. 주소는 아래와 같은 방식으로 나타낸다.
0x0cE446255506E92DF41614C46F1d6df9Cc969183
솔리디티에서는 주소를 address라는 자료형으로 표현할 수 있다.
매핑은 구조체와 비슷하게 구조화된 데이터를 저장하는 또다른 자료형이다. 구조체에서 각 데이터가 평등한 지위를 가지고 저장되는 것과 달리, 매핑에서는 두 개의 데이터가 각각 키, 혹은 값 이라는 지위를 부여받고 저장되는 점이 특징이다. 따라서 매핑을 키-값 (key-value) 저장소라고 할 수 있다. 매핑에 키와 값이 쌍으로 저장됐을 때, 해당 키를 넣으면 값에 해당하는 자료가 검색되는 식이다. 매핑은 다음과 같이 선언한다.
mapping (키의 자료형 => 값의 자료형) 매핑의 이름;
매핑을 만들었으면 매핑에 값을 저장해야한다. 값을 저장할 때는 아래와 같은 방법으로 한다.
매핑의 이름[키] = 값;
저장된 매핑에 키를 넣어서 값을 반환받고 싶을 때는 아래와 같이 하면 된다.
매핑의 이름[키];
mapping (int => string) findNameFromNumber;
findNameFromNumber[940424100000] = 'Alice';
findNameFromNumber[940424100000]; // 'Alice'가 반환된다.
위의 예시에서 int형을 키로, string 형을 값으로 가지는 매핑을 선언했다. 이 매핑에 주민등록번호와 이름을 쌍으로 저장해서 주민등록번호를 입력하면 이름을 검색해주는 프로그램을 만들 수 있을 것이다.
따로 저장하지 않아도 솔리디티의 모든 함수에서 사용 가능한 변수 중의 하나이다. msg.sender는 해당 함수를 호출한 사람의 주소를 가리킨다.
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 "불러올 파일의 이름";
일반적으로 명시적으로 나타내면서 변수를 선언할 일은 없지만 함수 내부의 구조체와 배열을 처리할 때 사용해야 할 수 있다.
우리는 Lesson 1에서 함수 접근 제어자로 public과 private을 배웠다.
Lesson 2에서 두 개의 함수 접근 제어자를 더 배운다.
블록체인 상에 있으면서 우리가 소유하지 않은 컨트랙트와 우리 컨트랙트가 상호작용을 하려면 인터페이스를 정의해야 한다. 블록체인 상의 아래와 같은 컨트랙트가 존재한다고 해보자.
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.상호작용하려는함수();
}
이 내용은 정보를 공유하기 위함이 아닌 저의 개인적인 공부 기록을 남기기 위함입니다. 따라서 틀린 정보가 있을 수 있으니 유의해서 봐주시면 감사하겠습니다.