솔리디티 (Solidity)
지난 포스트에선 Remix IDE에서 솔리디티 예제 파일을 컴파일 하고 배포하는 것을 해 보았습니다.
오늘 할 것은 앞서 한 것들을 바탕으로 실제로 솔리디티를 배우고 소스를 코딩하는 것을 목표로 할 것입니다.
솔리디티의 구조
지난 사용했었던 예제 .sol 파일을 라인별로 분석해 솔리디티의 기본 구조를 알아봅시다.
1 라인: 소스코드의 라이선스를 GPL-3.0으로 명시
3 라인: Version Pragma: 소스코드가 이용하는 컴파일러 버전 명시
- Sematic versioning을 따르고 있습니다. major.minor.patch
- ^(캐럿 연산자): ‘이상’ ← 이하 이상 할 때 그 이상입니다.
9 라인: 컨트랙트의 범위를 나타내는 부분으로 중괄호로(28라인과 함께) 감싸져 있는 것을 보실 수 있습니다.
11라인: 상태 변수(State Variable)이 선언되어 있습니다.
State Variable
- 블록체인(contract storage)에 값이 저장되는 변수
- 상태 변수의 접근 제어자(Visibility)를 external, public, priavte와 같이 지정 가능
- 기본형, 고조체, 배열 등 다양한 자료형이 존재
17라인: 함수(Function)이 선언되어 있습니다.
Function
- 컨트랙트 단위의 기능(해당 스마트컨트랙이 할 수 있는 기능)
- 매개 변수, 제어자, 반환값 지정 가능
- 함수 내부서 상태 변수의 값을 변경하거나 읽을 수 있음 (Read&Write)
솔리디티 문법
기본형 Primitives 타입
- 논리형 bool: true or false
- 정수형 uint: unsigned integer int: signed integer
- 8 ~ 256 bit를 표현할 수 있으며, uint는 uint256과 같습니다
- 주소형 address: 이더리움의 주소를 표현
- 바이트형 bytes# or byte[]: 데이터를 바이트로 표현할 수 있습니다
접근 제어자 Visibility
private | internal | public | external | |
---|---|---|---|---|
설명 | - 컨트랙트 내에서만 접근 가능 | - 현재 컨트랙트와 자식 컨트랙트에서 접근 가능 | - 현재 컨트랙트, 자식 컨트랙트, 외부 컨트랙트 및 주소에서 접근 가능 | - 외부 컨트랙트와 주소에서 접근 가능 (내부 접근 불가) |
State Variables | O | X | O | O |
Functions | O | O | O | O |
- public으로 되어 있으면 외부 컨트랙트에서 해당 상태 변수를 바로 조회할 수도 있습니다.
Solidity 예제
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ParentTest {
// State variables 테스트
string private privateVar = "private variable test";
string internal internalVar = "internal variable test";
string public publicVar = "public variable test";
// Private function
function privateFunc() private pure returns (string memory) {
return "private test function called";
}
// Private fucntion을 테스트할 fucntion
function testPrivateFunc() public pure returns (string memory) {
return privateFunc();
}
// Internal function
function internalFunc() internal pure returns (string memory) {
return "internal test function called in Parent Contract";
}
// Internal fucntion을 테스트할 fucntion
function testInternalFunc() public pure virtual returns (string memory) {
return internalFunc();
}
// Public functions 테스트
function publicFunc() public pure returns (string memory) {
return "public test function called";
}
// External functions
function externalFunc() external pure returns (string memory) {
return "external test function called";
}
}
contract ChildTest is ParentTest {
// Internal function call be called inside child contracts.
function testInternalFunc() public pure override returns (string memory) {
return internalFunc();
}
function testInternalFuncInChild() public pure returns (string memory){
// return privateFunc(); //에러 발생
return internalFunc();
}
}
- 타 언어와 다른 특징으로는 함수의 return타입을 뒤에 선언한다는 점
- internal 함수는 자식에서 호출이 가능
- public 함수는 자식 뿐만 아닌 외부에서도 모두 호출이 가능
- 이 예시를 컴파일 해서 배포(배포 방법은 이전 포스트 참고)한 후 각 함수를 호출해보면 private 와 public과 같은 Visibility를 잘 확인하실 수 있습니다.
자주 쓰는 자료형(타입)
배열 Array
- 고정 길이, 가변 길이 배열이 존재합니다
- 배열형 자료구조 제어 방법 index에 접근 push, pop, delete 사용
- 함수 내에서 로컬 변수로 배열을 사용하기 위해서는 고정 길이로 선언해야 합니다
인덱스는 0부터 시작합니다
Solidity 예제
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Array { // 고정 길이 배열, 모든 원소는 0으로 초기화 됨 uint[10] public fixedSizeArr; // 가변길이 배열 uint[] public arr; // 초기화 안 한 상태 uint[] public arr2 = [1, 2, 3]; // 같은 라인에서 초기화도 같이 한 상태 // 배열의 접근(조회) 방법 function get(uint i) public view returns (uint) { return arr2[i]; } // 새 원소를 배열에 추가 // 이 함수가 호출되고 나면 배열의 크기를 확인해 보자 function push(uint i) public { arr.push(i); } // 배열에서 마지막 원소 삭제 // 이 함수가 호출되고 나면 배열의 크기를 확인해 보자 function pop() public { arr.pop(); } // delte 지시자를 사용하지만 실상은 특정 인덱스의 원소를 0으로 초기화시키는 동작을 함 // 이 함수가 호출되고 나면 배열의 크기를 확인해 보자 function remove(uint index) public { delete arr[index]; } // 배열의 크기를 반환하는 함수 function getLength() public view returns (uint) { return arr.length; } // 전체 배열을 반환하는 함수 function getArr() public view returns (uint[] memory) { return arr; } // 고정길이의 5칸짜리 배열을 생성하고 그것을 반환해주는 함수 function createArray() external pure returns (uint[] memory){ // create array in memory, only fixed size can be created uint[] memory a = new uint[](5); return a; } }
매핑 Mapping (자주 쓰임)
예시를 통해 Mapping에 대해 다음을 배워봅시다.
- 매핑형 선언
- 접근, 추가, 삭제
- 매핑에 저장된 key의 목록을 얻을 수 있는 방법을 제공하지 않습니다
Solidity 예제
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
contract Mapping {
// 매핑의 선언
// address(주소)를 uint로 매핑
mapping(address => uint) public addrToUint;
// 매핑 내의 키 값으로 값을 연결함
// 만약 해당하는 키가 없으면, 해당 타입의 기본 값을 반환함, 0.
function get(address _addr) public view returns (uint) {
return addrToUint[_addr];
}
// 매핑 내 address 키에 해당하는 unit 값을 업데이트
function set(address _addr, uint _i) public {
addrToUint[_addr] = _i;
}
// 매핑 내 address 키에 해당하는 unit 값을 기본 값으로 초기화, 0.
function reset(address _addr) public {
delete addrToUint[_addr];
}
// 이런식으로 매핑의 값을 uint 배열 때처럼 불러오는 것은 불가능하다(에러)
// 왜냐하면 매핑은 내부적으로 키를 저장해 놓는 것이 아니라 키 자체의 sha3해시에 의해 계산된
// 상태 메모리(state memory)에 저장된 값만 저장한다. 따라서 매핑의 조회는 원래 키가 제공되어야만 이를 계산하여
// 조회할 수 있게 구현되어 있기에 반드시 key가 있는 상태로 조회되어야 한다.
// function getMapping() public view returns (mapping memory){
// return addrToUint;
// }
}
사용자 선언 자료형 - Struct
다음을 예시를 활용해 배워봅시다. Struct는 한글로 구조체라고도 합니다.
- 여러 자료형을 하나의 관점으로 묶어서 관리하고자 할 때 선언합니다. (마치 C언어 같음)
struct Todo{
string todoText;
bool isComplete;
}
- 구조체의 Array, Mapping 의 값으로 지정이 가능합니다.
- 구조체 생성, 접근, 변경
- 함수 안에서 struct 상태 변수 참조 방법
Solidity 예제
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
contract Struct {
//구조체의 선언
struct Todo {
string text;
bool boolean;
}
// 구조체의 배열 선언
Todo[] public structTodoArray;
// 주소를 Todo 타입으로 매핑한다.
mapping(address => Todo) public addrToStruct;
// 새로운 구조체를 생성해서 구조체 배열에 푸시
// 접근방식 1
function create1(string memory _text) public {
structTodoArray.push(Todo(_text, false));
}
// 접근방식 2
function create2(string memory _text) public {
structTodoArray.push(Todo({text: _text, boolean: false}));
}
// 접근방식 3
function create3(string memory _text) public {
Todo memory s;
s.text = _text;
structTodoArray.push(s);
}
// 구조체 배열 내 특정 인덱스의 Todo 인스턴스의 text를 변경
function updateText(uint _index, string memory _text) public {
Todo storage s = structTodoArray[_index];
s.text = _text;
}
// 불리언(Boolean) 값을 토글
function updateBoolean(uint _index) public {
Todo storage s = structTodoArray[_index];
bool current = s.boolean;
s.boolean = !current;
}
}
함수 function
예제를 통해 다음을 배워봅시다.
- 함수 선언 방법
- 매개변수 유무, 반환 값 유무
- view, pure 함수의 특징
- 2개 이상의 값을 반환하도록 선언
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
contract Function {
//여러 상태변수들 선언
uint public num = 1;
uint public a = 1;
string public s = "hello solidity";
bool public b = true;
// 파라미터와 리턴값이 없는 함수
function addOne() public {
num++;
}
// 하나의 파라미터와 리턴값이 있는 함수
function addNumber(uint x) public returns (uint) {
num += x;
return num;
}
// view - 상태변수들을 변경하지 않는다, 단순히 읽음. 가스가 안듬.
function addAndReturn(uint x) public view returns (uint) {
return num + x;
}
// pure - 상태 변수를 읽거나 수정하지 않음. 가스가 안듬.
function add(uint x, uint y) public pure returns (uint) {
return x + y;
}
// 여러 값들을 리턴하는 방법.
function returnMany() public view returns (uint, string memory, bool) {
return (a, s, b);
}
}
Data location
- 배열이나 구조체처럼 복합타입(Complex type)의 경우 데이터의 위치(Data location)에 대한 어노테이션(Annotation)이 추가됩니다.
메모리(memory)와 저장소(storage), 콜데이터(calldata)로 구분되는 데이터의 위치는 각 타입별로 기본값이 있으나 필요에 따라서 재지정이 가능합니다.
- 메모리(memory) : 함수 내에서 임시로 데이터(storage 등)를 저장 할 때 사용하는 변수이다. 임시 데이터(temporary data)
- 저장소(storage) : 블록체인 상에 영구이 저장된다. 영구데이터(permanent data) 영역에 데이터가 저장되므로 다른 키워드에 비해 큰 비용을 초래한다.
- 콜데이터(calldata) : 리턴 파라미터를 제외한 외부 함수의 파라미터들, 데이터를 호출하고 메모리와 비슷하게 동작 → 함수에 전달되는 매개 변수처럼 변경 불가능하고 임시적인 데이터 저장 영역임.
Solidity 예제
// SPDX-License-Identifier: UNLICENSED pragma solidity >=0.7.0 <0.9.0; contract HamburgerFactory { struct Hamburger { string name; string status; } Hamburger[] Hamburgers; //햄버거를 초기화하고 배열에 햄버거를 넣음 function createHamburger(string memory _text) public { Hamburgers.push(Hamburger({name: _text, status: "not eat"})); } function eatStorageHamburger(uint _index) public { // storage로 선언된 myHamburger로 햄버거의 인덱스를 참조전달함 Hamburger storage myHamburger = Hamburgers[_index]; myHamburger.status = "eaten"; //따라서 수행되고 나면 값이 바뀜 } function eatMemoryHamburger(uint _index) public view{ // 메모리 변수의 선언 Hamburger memory myHamburger = Hamburgers[_index]; myHamburger.status = "eaten"; //상태가 변해도 블록체인에는 영향X. } function getStatusHamburger(uint _index) public view returns(string memory){ return Hamburgers[_index].status; //해당 값이 memory 형태에 담겨서 return됨 } }
제어문
제어문은 타 언어와 매우 유사하므로 예시를 위주로 설명하겠습니다.
조건문 If-Else
//함수 내에서만 사용 가능함.
if(x < 10){
return 0;
} else if (x < 20){
return 1;
} else {
return 2;
}
// 삼항 연산자
return _x < 10 ? 1 : 2;
반복문 for / while
//함수 내에서만 사용 가능함.
for (uint i = 0; i < 10; i++) {
if (i == 3) {
continue;
}
if (i == 5) {
break;
}
}
uint i;
//함수 내에서만 사용 가능함.
while (i < 10){
i++;
}
- 반복문 사용시 주의사항: 이더리움은 튜링 완전머신으로 반복문을 제대로 지원하지만 함수를 수행할 때 수행 시간에 따라 가스가 발생하기 때문에 로직을 비효율적으로 하면 많은 돈이 소모될 수 있습니다.
화폐 단위
- 화폐 단위에는 ether와 wei, gwei가 있습니다.
- 이더리움 내에선 소수점을 허용하지 않기 때문에 이와 같은 방식을 사용합니다.
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
contract EtherUnits {
uint public oneWei = 1 wei;
uint public oneGwei = 1 gwei;
uint public oneEther = 1 ether;
// 1 wei is equal to 1
bool public isOneWei = 1 wei == 1;
// 1 ether is equal to 10^18 wei
bool public isOneEther1 = oneEther == 1e18;
// 1 ether is equals to 10^9 gwei.
bool public isOneEther2 = oneEther == 10**9 * oneGwei;
// 1 gwei is equals to 10^9 wei.
bool public isOneGwei = oneGwei == 10**9 * oneWei;
}