Solidity 0.5.0 에서의 변경사항을 소개합니다.
5월 0.4.24 버전이 나온 후 오랜 기간 동안 거의 업데이트되지 않던 솔리디티가 거의 반년만에 드디어 0.5.0 으로 업데이트되었습니다! 👏👏👏
(참고로 0.4.25 으로 업데이트가 한번 있었으나 문법 등이 바뀐 것은 없고 bug fix 만 있었음)
리믹스 Remix 에서도 11월 13일자로 0.5.0 버전을 선택할 수 있게 되었는데요, 이상한 점은 막상 선택해보면 실제로는 선택이 되지 않고 먹통이 됩니다… 저 뿐만 아니라 다른 분들도 그렇다고 하던데요, 아무래도 뭔가 버그가 있나 싶습니다. 기다려보면 해결되겠죠 뭐. 일단 0.5.0 버전의 환경을 테스트해보기 위해 다음과 같이 0.5.0 버전 중에 가장 최신의 nightly 버전을 선택하시면 됩니다. (참고 : 11월 말 이후로 현재는 해결되었습니다.)
자, 이제 그러면 과연 무엇이 바뀌었는지 체크해보도록 하겠습니다.
Part I. 이건 너무 좋아졌어요!
low-level 함수 호출의 변화
미리보기
callcode
함수가 삭제됨staticcall
함수가 추가됨- 입력 인자가 bytes 하나로 통일됨 (abi.encodeWithSignature 등을 이용)
- 로우레벨 함수 콜도 리턴 값을 돌려줌!!
설명
솔리디티는 함수 호출 중에 revert 가 발생하면 트랜잭션의 실행 전체를 revert 시켜버립니다. 하지만 어떤 경우는 예외처리를 해준 후에 실행을 이어나가고 싶을 때가 있을 것입니다. 이를 위한 기능이 바로 로우레벨 함수 호출 입니다. 로우 레벨로 함수를 호출한 경우에는 함수 실행 중에 revert 가 발생하더라도 실행을 멈추지 않고 개발자가 직접 예외처리를 할 수가 있습니다.
이전 버전에서는 low-level 함수 호출을 위한 기능으로 call
, callcode
, delegatecall
함수들이 있었습니다. 이 중 callcode
함수는 향후 deprecated 될 거라고 이미 예고된 바 있었기 때문에 삭제된 것이 자연스럽게 느껴집니다.
생각지 못했던 것은 새로운 함수인 staticcall
입니다. 솔리디티 공식 문서에 따르면 EVM 레벨에서 상태값이 바뀌지 않는 것을 보장하기 위해 STATICCALL opcode 를 새롭게 추가하였다고 하는데요, 이에 따라 staticcall
함수로 호출된 함수는 그 내부에서 상태값을 바꿀 수 없어야 합니다. 이게 무슨 말이냐? 쉽게 얘기하자면 함수 내에서 상태값을 바꾸지 않는 pure
함수와 view
함수는 staticcall
로 호출한다고 생각하시면 됩니다.
이러한 로우레벨 함수들을 사용할 때 이전버전에서는 첫번째 인자만 function hash 라고 불리던 bytes4 타입의 값만 입력해주면 이후에는 인자의 개수와 타입에 제한을 두지 않았습니다. 예를 들면 다음과 같이 사용했었습니다.
bool success = address(contractAddr).call(bytes4(keccak256("transfer(address,uint256)")), to, value);
그런데 새 버전에서는 bytes 타입의 인자 하나만 받게 되어 있습니다. 다음 예시를 보시면 바뀐 부분을 이해할 수 있으실 겁니다. 기존에 함수의 인자로 들어가던 부분들을 abi.encodeWithSignature
안으로 넣기만 하면 됩니다.
(bool success, bytes memory data) = address(contractAddr).call(abi.encodeWithSignature("transfer(address,uint256)", to, value));
앗? 그런데 returnData?
혹시 위 코드에서 눈치 채셨나요? 네, 맞습니다. 예전에는 call
, delegatecall
을 이용하여 함수를 호출하면 리턴값을 받지 못하였었습니다. 로우레벨로 함수를 호출하면서 리턴값을 받기 위해서는 inline assembly 를 이용하는 방법 뿐이었죠. 아.. 이 얼마나 불편하던 시절이었나.. 😭
그런데 이제 (성공여부, 리턴값)
의 형태를 가진 tuple 로 값을 돌려주도록 업데이트 되었기 때문에 리턴값을 받을 수 있게 되었답니다! 야호!!! 진정 만세!!!!
다만, 여러 개수와 다양한 타입을 하나의 변수에 담아오기 때문에 다음처럼 abi.decode
함수로 data 를 디코드 해주어 사용해야 합니다.
(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
로우레벨 콜이 일반 함수 호출에 비해 고급 기능이라 어렵게 느껴지실 수 있습니다. 하지만 개발하다 보면 런타임 에러에 대한 예외처리를 직접 해줘야 할 경우가 꼭 생기기 때문에 이번 기회에 알아두시면 반드시 도움이 될 겁니다.
지역 변수 scope 의 변화
미리보기
- 이전까지는 Javascript 문법에 가까웠으나 새 버전부터는 C 문법과 같아짐.
- 지역 변수를 선언하고 사용하여야 함.
- 지역 변수의 생명 범위는 중괄호 { } 내부임.
- 중괄호 { } 안팎에 같은 이름의 지역 변수가 있다면 그 둘은 구분됨.
설명
지역 변수의 scope 가 C언어 문법처럼 바뀔 것이라는 것은 예전부터 예고되었던 바인데 드디어 적용되었네요. 저는 C 언어에 친숙한 사람이어서 그런지 Javascript 의 지역 변수 scope 에 적응이 안되는 편이고, 그래서 이번 변화가 매우 반갑습니다. 😙
사실 다음과 같은 코드가 동작하는 것은 너무 이상하지 않나요? 저만 그런가요?
function f() pure public returns (uint) {
x = 2;
uint x;
return x;
}
하지만 새 문법에서는 다음처럼 무조건 x
가 먼저 선언된 후 사용하여야 합니다. 저는 이게 자연스럽다고 생각합니다.
function f() pure public returns (uint) {
uint x;
x = 2;
return x;
}
그리고 예전 버전에서는 중괄호와 무관하게 지역 변수가 선언되었었는데요, 이게 여러 헷갈리는 점을 낳습니다. 다음 코드를 보시죠.
contract A {
int x;
function f() pure public returns (uint) {
x = 2; // x 는 지역변수 or 상태변수?
{
int x = 3;
}
return x;
}
}
위 주석 라인의 x
는 과연 지역변수일까요? 상태변수일까요? 정답은 0.5.0 에서는 상태변수이고, 예전 버전(0.4.25 이하)에서는 중괄호 안에서 선언한 x
가 괄호 밖의, 특히 선언되기 전의 더 윗 라인의 영역까지 영향을 미치기 때문에 지역변수입니다.
참고로 f
함수에 pure
키워드가 붙어 있어서 상태변수는 수정할 수 없어야 합니다. 따라서 위 코드는 0.5.0 에서는 컴파일 에러가 나고, 예전 버전에서는 에러가 나지 않습니다.
배열에 pop 함수가 생기다!
설명
아니? 이렇게 중요한 기능을 왜 Solidity v0.5.0 Breaking Changes 에서 알리지 않았을까요? 저도 모르고 놓칠 뻔 했다가 v0.5.0 문서의 Arrays 관련된 부분에서 우연히 발견했네요!
v0.4.25 버전에서는 다음과 같이 pop
함수가 없습니다.
그런데 v0.5.0 에서는 다음과 같이 pop
함수가 생겼군요!
오 마이갓, 비탈릭느님 감사합니다. 앞으로 잘 사용하겠습니다.
참고로 배열 설명 부분에 붙어 있는 다음 주석도 재밌는 부분입니다. 배열의 크기를 늘릴 때는 해당 공간을 0으로 초기화 한다고 가정하고 일정한 가스 비용을 내지만, 배열의 크기를 줄일 때는 각 엘리먼트의 delete
를 호출하는 것과 같은 비용이 더 들어갈 수 있다고 합니다. 이 주석은 예전버전에서도 마찬가지였을 것 같기는 한데, 기존에는 없던 주석이고 0.5.0 에서 새롭게 추가되었습니다. (혹시 향후 delete 멤버 함수도 추가 되려나…? 기대해봅니다!)
Part II. 중요한 변화
새로운 타입, address payable
미리보기
- 돈을 보낼 수 있는 타입
address payable
신설 address
타입을address payable
타입으로 변환하는 방법은?
설명
이더리움 계정 주소 값을 처리하기 위한 address
타입에 중요한 변화가 생겼습니다. 바로 돈(ether)을 이체할 수 있는 send
와 transfer
함수가 없어진 것이죠!
당연히 기능 자체가 없어진 것은 아닙니다. 돈을 보낼 수 있는 주소 타입 address payable
을 새로 만든 후, 기본 주소 타입 address
에서는 돈을 전송하는 함수들을 빼버린 것입니다. 예를 들어 다음 중 윗쪽 코드는 동작을 하는데, 아랫쪽 코드는 동작을 하지 않습니다.
address payable to = 0x...; // 돈을 받을 주소
to.send(1 ether); // 나(트랜잭션을 실행한 사람)의 돈 1이더를 to 에게 보냄
to.transfer(1 ether); // 위와 같은 기능address to2 = 0x...;
to2.send(1 ether); // error
to2.transfer(1 ether); // error
이렇게 일부러 address payable
타입을 하나 더 추가했다는 것은 돈 관리에 있어서 실수하지 말고 더 까다롭게 관리 하라는 이유겠죠?
참고로 send
와 transfer
는 같은 기능을 하는데, send
는 로우레벨 콜이라 에러가 났을 경우에 대한 예외처리가 가능합니다. 이에 대해서는 위에서 설명한 call
, delegatecall
, staticcall
의 로우레벨 콜 경우와 똑같다고 보시면 됩니다.
만약 address
타입을 address payable
타입으로 변환하고 싶은 경우에는 어떻게 해야 할까요? 일단 address payable
타입의 경우에는 address
타입으로 바로 변환이 가능합니다.
하지만 address
타입을 address payable
타입으로는 바로 변경하지 못하고 uint160
타입으로 변환했다가 다시address payable
타입으로 변환하는 식으로 2번의 형변환을 거쳐야 합니다. 그런데 막상 코딩을 해보려니 address payable
타입으로 명시적으로 변환시키는 방법을 모르겠더라고요? 다시 열심히 찾아보니, 아놔… 얘네 뭐임…
“We can define a library for explicitly converting address
to address payable
as a workaround.”
라고 합니다. 명시적으로 변환할 때는 address
타입으로 선언하면 알아서address payable
로 동작시켜준다고 하는데, 아마 컴파일러에서 타입 이름에 스페이스를 포함하여 처리하기가 애매하여 꼼수를 쓴 것 같습니다. 🤬
관련하여 다음 코드를 참고하세요.
(0x...).transfer(1 ether); // 주소 상수 뒤에 바로 사용 OKaddress to = 0x...;
address(to).transfer(1 ether); // 에러!
address(uint160(to)).transfer(1 ether); // 두번 변환은 OKaddress payable x = address(uint160(to)); // 저장할 때도 두번 변환해야 OK
부가 설명
msg.sender
는 자동으로address payable
타입을 리턴한다고 합니다.- 만약
c
가 돈을 수신할 수 있는(=payable fallback function 을 가진) Contract 타입이라면address(c)
명령은 자동으로address payable
타입을 리턴한다고 합니다. - 중요한 점 하나 더 -
msg.value
는payable
함수 혹은internal
함수에서만 호출 가능하다고 합니다! 이 내용을 모르시면 이 부분에서 왜 에러가 나는지 많이 헷갈리실 듯 하네요. 다음 코드처럼 msg.value 값을 리턴해주는 internal 함수를 만들어 사용하는 것은 괜찮습니다.
function msgvalue() internal returns (uint256) {
return msg.value;
}
명시적으로 선언하라!
설명
솔리디티 0.5.0 버전에서는 여러 부분을 명확하게 의무적으로 선언하도록 문법을 변경한 부분이 많습니다. 이렇게 되면 암시적으로 표현한 부분에서 나오는 실수는 줄여줄 수 있겠지만, 자잘한 형변환도 일일이 명시적으로 선언해줘야 하므로 어떤 부분에서는 굉장히 귀찮습니다. 기존 소스를 새 버전으로 포팅한다면 가장 귀찮은 부분도 이 부분이 될 가능성이 높습니다.
제 생각에는 소스를 한번 배포하고 나면 업그레이드가 되지 않는다는 이더리움의 특성상, 귀찮더라도 실수를 줄여주는 쪽으로 집중한 것 같습니다. 자, 그럼 이제 변경된 부분들을 확인해보겠습니다.
- 함수의 visibility 를 의무적으로 명시해줘야 함.
예전에는 함수의 visibility 를 생략 가능했고, 생략하면 public
이었습니다. 그런데 0.5.0 버전부터는 함수의 visibility (external
or public
or internal
) 를 의무적으로 선언하여야 합니다.
- 인터페이스 내의 함수 선언과 fallback 함수는 항상
external
임.
그 중에서도 특히 무조건 외부에서 호출되는 것이 분명한 인터페이스 내의 함수 선언(인터페이스를 상속받은 콘트랙트에서 사용)과 fallback 함수(외부에서 이 콘트랙트에 입금을 할 때 자동 호출)는 무조건 external
키워드를 붙여줘야 합니다.
- 레퍼런스 타입 변수의 data location 을 의무적으로 선언해줘야 함.
예전에는 레퍼런스 타입(struct
or array
or mapping
)의 데이타 로케이션(storage
or memory
or calldata
)을 생략했을 때, 함수의 파라메터는 memory
또는 calldata
로, 지역 변수는 storage
로 자동으로 선언이 되었으나, 이제는 명시적으로 선언해줘야 합니다. 참고로 각 데이타 로케이션의 의미는 다음과 같습니다.
memory
: 함수 내에서만 사용하는 임시 메모리 공간
storage
: 상태변수를 참조 (하기 때문에 영구적으로 저장되는 공간)
calldata
: external
함수의 파라메터를 위한 공간. memory
와 역할은 비슷하지만 external
함수에 이용하기 위해 좀 더 긴 lifetime 을 가지고 있음.
- Contract 타입을 address 로 형변환하여서 사용하여야 함.
예전에는 Contract 타입이 기본적으로 address 타입의 기능을 포함하고 있었으나, 이제는 별개의 타입으로 구분됩니다. 따라서 address 의 기능 (transfer
, balance
등)을 사용하고 싶다면 명시적으로 address 타입으로 변환해서 사용해야 합니다.
- Contract 타입 간의 형변환은 상속 받은 조상(ancestor)으로만 가능함.
예전에는 Contract 타입 간의 형변환이 아주 자유로웠는데, 이제는 상속 관계에서 자손 콘트랙트가 조상 콘트랙트로만 형변환을 할 수 있습니다. 저는 더 명확해진 것 같아서 바뀐 새 방식이 좋다고 생각합니다.
만약 콘트랙트 타입의 변수 b
를 아무 상속 관계가 없는 A
라는 이름의 콘트랙트 타입으로 변환하려면 A(address(b))
와 같이 address
를 포함하여 두 단계로 형 변환하면 됩니다.
- bytesX 와 uintY 의 경우, 사이즈가 같은 경우에만 형변환이 가능함.
bytesX 는 패딩을 오른쪽으로 합니다. 길이가 긴 데이타를 짧은 데이타로 변환할 때 오른쪽을 잘라내고, 길이가 짧은 데이타를 긴 데이타로 변환할 때는 오른쪽에 0 을 붙인다는 뜻입니다. 예를 들면 다음과 같습니다.
bytes1 c = 1;
bytes2 d = bytes2(c); // d 는 1 이 아닌 0x0100
bytes1 e = bytes1(d); // 0x0100 에서 오른쪽을 padding 하므로 다시 1
bytesX 는 숫자가 아니라 그저 컴퓨터에 순서대로 저장된 0 과 1 로 구성된 데이타로 보기 때문입니다.
하지만 숫자의 경우는 다르죠. uintY 는 패딩을 왼쪽으로 합니다.
uint8 f = 1;
uint16 g= uint16(f); // 당연히 그대로 1 (저장 공간으로 보면 0x0001)
uint8 h = uint8(g); // 역시나 그대로 1uint16 i = 0x100;
uint8 j = uint8(i); // 왼쪽을 패딩(왼쪽 바이트를 잘라냄)하기 때문에 0 이 됨.
자 그러면, 다음 코드는 과연 어떻게 동작할까요?
bytes1 a = bytes1(0x100);
bytes 는 오른쪽을 패딩하기 때문에 1이 나올 것 같은데 맞나요? 정답은, 아닙니다! 사이즈 다른 암묵적 형변환에서는 자동으로 숫자 변환을 먼저 수행하도록 되어 있기 때문에 결과는 0 이 됩니다.
bytes1 a = bytes1(uint8(0x100)); // 위 코드는 이렇게 구동됨. 따라서 0.
어때요? 헷갈리죠? 이런 이유 때문에 bytesX 와 uintY 사이의 형변환에서 사이즈가 다른 경우에는 무조건 명시적으로 선언하도록 바뀌었습니다. 따라서 개발할 때 다음 두 코드 중 하나를 선택하여 사용하여야 합니다.
bytes1 a = bytes1(0x100); // 0.5.0 부터는 error
bytes1 b = bytes1(uint8(0x100)); // 결과는 0
bytes1 c = bytes1(bytes2(0x100)); // 결과는 1
이 외에도 다양한 암묵적 형변환이 가능할텐데요, 이에 대해서 솔리디티 공식문서에서는 “데이타 손실이 없는 경우에 대해서는 암묵적 변환을 허용한다.” 라고 씌여 있습니다. (할많하않…)
Part III. 삭제(혹은 deprecated) 되었어요.
주요 내용 요약
- 이제
constant
키워드는 아예 삭제되었습니다.view
로 사용하세요. - The “loose assembly” syntax 를 불허한다고 합니다. 저는 사용하지 않았던 기능입니다만.. 😅
var
타입이 삭제되었습니다. 저는 타입을 명시적으로 선언하는 것을 좋아해서 안 썼던 타입이지만, 자주 사용하시던 분들께는 큰 변화일 수도 있을 것 같습니다.- Uninitialized storage variables 는 허용되지 않는다고 합니다. 따라서 storage 변수를 선언할 때 항상 어떤 상태 변수를 참조할 것인지가 지정되어야 할 것입니다. 이 부분은 헷갈릴 수 있었던 부분으로 잘 바뀐 것 같습니다.
years
단위가 삭제 되었다고 합니다. 이 내용은 오래 전부터 삭제될 거라고 예고되었었는데요, 1years
는 윤년 계산이 포함되지 않은 단순 365days
이기 때문에 헷갈릴 수 있다는 이유입니다.suicide
함수가 아예 삭제되었습니다.selfdestruct
로 사용하세요.sha3
함수가 아예 삭제되었습니다.keccak256
로 사용하세요.throw
함수가 아예 삭제되었습니다.revert
,require
,assert
를 사용하세요.- 콘트랙트와 이름이 같은 함수를 생성자로 사용할 수 없습니다.
constructor
키워드를 사용한 생성자로 사용하세요. (예전에는 warning 만 띄웠었는데 강제사항이 됨)
Part IV. 그 외 기타 사항
누군가에게는 중요한 변화일 수도 있는 마이너 업데이트들
이제 주요 업데이트들은 모두 다 다룬 것 같습니다. 하지만 우리는 각기 다른 업무를 서로 다른 스타일로 개발을 하고 있기 때문에, 저한테는 별로 중요하지 않은 마이너 업데이트가 다른 누군가에게는 중요하게 느껴질 수도 있습니다. 예를 들면 다음과 같은 업데이트들이 있습니다.
- 16진수를 표현할 때 대문자 0X… 는 허용하지 않음. 0x… 로 사용해야 함.
- 튜플의 개수가 다른 경우 assign 되지 않음.
- unary operator + 는 허용되지 않음. 예를 들어
a = +1;
은 실행되지 않음. - 구현 부분이 없는 함수(가상 함수)는 modifier 를 사용할 수 없음.
- 그 외 다수 …
최대한 많은 내용을 소개하려고 노력했으나, 모든 마이너 업데이트들까지 소개하기는 버겁네요. 이보다 더 상세한 내용은 공식 문서의 https://solidity.readthedocs.io/en/latest/050-breaking-changes.html# 를 읽어봐주세요.
ONE MORE THING
이번 업데이트로 새롭게 예약된 예약어들이 꽤 많습니다. 아직 자세한 내용 없이 키워드만 오픈 된 정도지만, 키워드들만 봐도 앞으로 솔리디티의 발전 방향을 어느 정도 읽어볼 수 있는 부분이 아닌가 라는 생각이 듭니다.
- 새로운 예약어들 :
alias
,apply
,auto
,copyof
,define
,immutable
,implements
,macro
,mutable
,override
,partial
,promise
,reference
,sealed
,sizeof
,supports
,typedef
andunchecked
.
마치며…
사실 버전 0.5.0 에서 파격적인 변화는 생각보다 별로 없다는 생각이 듭니다. 하지만 여러 제한사항이 더 까다로워지고, 특히 명시적으로 선언해야 하는 부분들이 많아지면서 기존과 다르게 개발해야 할 부분들은 꽤 많아졌습니다. 기존 소스를 포팅하거나 활용할 때 차이점을 분명히 알아야 하는데, 아직 이런 부분들을 자세히 소개한 콘텐츠가 없다고 생각하여 이 문서를 작성해보았습니다. 이 땅의 모든 솔리디티 개발자 분들께 도움되길 바랍니다.
————————————————
Who is Tae Kim ?
- 케이스타라이브의 CTO
- 케이스타라이브 (kstarlive.com) : 900만 팔로어 전세계 1위 한류 미디어
- 케이스타코인 (kstarcoin.com) : 한류 코인 프로토콜. 스팀잇처럼 커뮤니티 활동을 하면서 코인을 얻을 수 있으며, 한류 콘텐츠 구매, 공연 예매, 한국 관광 관련, 기부 및 팬클럽 활동 등에 사용될 계획입니다.