스마트 컨트랙트 : Maintenance 패턴

Seungwon Go
ReturnValues
Published in
10 min readSep 26, 2018

블록체인에 배포되는 스마트컨트랙트는 다른 프로그램과 달리, 한번 배포되면 더이상 수정이 불가능하게 됩니다. 그래서 컨트랙트 내에서 버그가 발생하더라도 이에 대한 조치가 불가능해서 큰 피해를 입게 되는 상황이 발생할 수 있어서 개발시 더욱더 큰 주의를 기울여야 합니다.

이번 포스팅에서는 스마트컨트랙트를 유지보수 할 수 있는 방법을 정리해 보고자 합니다.

Data Segregation 패턴

이 패턴은 비즈니스 로직 부분과 데이터가 저장되는 부분을 분리하여 별개의 컨트랙트를 구현하는 개발 패턴입니다.

아래 2개의 컨트랙트를 통해 데이터를 저장하는 컨트랙트와 비즈니스 로직을 수행하는 컨트랙트를 어떻게 분리하여 사용하는지 알아보도록 하겠습니다.

contract DataStorage { 
mapping(bytes32 => uint) uintStorage;
function getUintValue(bytes32 key) public view returns (uint) {
return uintStorage[key];
}
function setUintValue(bytes32 key, uint value) public {
uintStorage[key] = value;
}
}

위의 컨트랙트는 key-value를 통해 데이터를 저장하고, 가져올 수 있는 2개의 함수로 구성되어 있습니다.

import "./DataStorage.sol"; contract BusinessLogic { 
DataStorage dataStorage;
function setDataStorage(address _address) public {
dataStorage = DataStorage(_address);
}
function setData(string _key, uint _value) public {
bytes32 key = keccak256(_key);
dataStorage.setUintValue(key, _value);
}
function getData(string _key) public returns (uint) {
bytes32 key = keccak256(_key);
return dataStorage.getUintValue(key);
}
}

위의 컨트랙트에서는 데이터를 저장하는 DataStorage 컨트랙트가 이미 블록체인에 배포되어 있는 상태에서, 해당 컨트랙트의 address를 setDataStorage함수로 할당한 후 컨트랙트 내에서 데이터를 저장하고, 가져오는데 사용하고 있습니다.

이렇게 데이터를 저장하는 컨트랙트를 분리해서 구현을 하면, 향후 비즈니스 로직을 담고 있는 컨트랙트를 업그레이드한 새로운 컨트랙트를 개발하더라도, 기존에 저장한 데이터를 그대로 사용할 수 있게 됩니다.

데이터 타입에 따라 데이터를 저장할 수 있는 컨트랙트를 아래와 같이 만들어 보았습니다.

contract DataStorage {     
mapping(bytes32 => uint) UIntStorage;
mapping(bytes32 => string) StringStorage;
mapping(bytes32 => address) AddressStorage;
mapping(bytes32 => bytes) BytesStorage;
mapping(bytes32 => bool) BooleanStorage;
mapping(bytes32 => int) IntStorage;
function setUIntValue(bytes32 record, uint value) {
UIntStorage[record] = value;
}
function getUIntValue(bytes32 record) constant returns (uint){
return UIntStorage[record];
}
function setStringValue(bytes32 record, string value) {
StringStorage[record] = value;
}
function getStringValue(bytes32 record) constant returns (string){
return StringStorage[record];
}
function setAddressValue(bytes32 record, address value) {
AddressStorage[record] = value;
}
function getAddressValue(bytes32 record) constant returns (address){
return AddressStorage[record];
}
function setBytesValue(bytes32 record, bytes value) {
BytesStorage[record] = value;
}
function getBytesValue(bytes32 record) constant returns (bytes){
return BytesStorage[record];
}
function setBooleanValue(bytes32 record, bool value) {
BooleanStorage[record] = value;
}
function getBooleanValue(bytes32 record) constant returns (bool){
return BooleanStorage[record];
}
function setIntValue(bytes32 record, int value) {
IntStorage[record] = value;
}
function getIntValue(bytes32 record) constant returns (int){
return IntStorage[record];
}
}

Satellite 패턴

이 패턴은 특정 함수, 즉 복잡성이 높고 향후 개선되거나 변경될 여지가 있는 중요한 함수를 분리하여 별도의 컨트랙트로 개발하여 관리하는 패턴입니다.

최근에 저는 특정 고객사 ERP 시스템을 구축하는 프로젝트를 담당한적이 있습니다. 프로젝트 시작 시점에는 회계시스템의 인보이스 발행 금액이 선적일 기준이였는데, 프로젝트 오픈후에 국제 회계 표준이 인도일 기준으로 변경됨으로써 많은 변경사항이 발생하였고, 이로 인해 굉장히 애를 먹은적이 있습니다. 그런데 만약에 이 프로그램이 블록체인에 올라가는 스마트컨트랙트 였고, 이와 같은 변경사항을 고려하지 못한 프로그램이였다면 이를 해결할 방법이 없었을 것입니다.

이처럼 변경여지가 있는 비즈니스 로직이나, 지금 당장은 변경여지가 없더라도 매우 중요한 비즈니스 로직을 처리하는 함수의 경우에는 별도의 컨트랙트를 구현함으로써, 혹시 모를 버그 혹은 개선사항을 반영할 수 있도록 고려해야 합니다.

아래 컨트랙트는 특정 계산을 처리하는 함수를 별도의 컨트랙트로 구현하였습니다.

contract Satellite { 
function calculateVariable(uint _a, uint _b) public pure returns (uint){
// calculate var
return _a * _b;
}
}

아래의 컨트랙트에서는 미리 개발되어져서 블록체인에 배포된 위 컨트랙트의 address를 setSatelliteAddress 함수를 통해 할당함으로써 컨트랙트 내에서 사용할 수 있도록 구현되어져 있습니다.

import "../../authorization/Ownership.sol"; 
import "./Satellite.sol";
contract SatelliteExample is Owned {
uint public variable;
address satelliteAddress;
function setVariable(uint _a, uint _b) public onlyOwner {
Satellite s = Satellite(satelliteAddress);
variable = s.calculateVariable(_a, _b);
}
function setSatelliteAddress(address _address) public onlyOwner {
satelliteAddress = _address;
}
}

이렇게 중요 함수를 별도의 컨트랙트로 분리하여 블록체인에 배포한 후, 사용함으로써, 필요에 의해 해당 함수를 변경해야 할 상황이 발생하면, 해당 함수를 가지는 컨트랙트를 다시 개발하여 배포한 후, setSatelliteAddress 함수를 통해 다시 할당하여 사용할 수 있게 됩니다.

Contract Register 패턴

이 패턴은 필요에 의해 컨트랙트를 재개발하여 사용하게 될때, 컨트랙트에 대한 버전을 관리하기 위한 패턴입니다.

예를들어, 아래 컨트랙트에서 contractAddress가 데이터를 저장 컨트랙트라고 가정해 봅시다. 어떤 이유에서 데이터 저장 구조가 바뀌게 되어, 새로운 데이터 저장 구조를 가지는 컨트랙트를 개발하여 새로 배포해서 사용해야 하는 상황이 발생했다고 생각해 봅시다. 이때, 새롭게 배포된 contractAddress를 changeContract 함수를 통해 변경하고, contractVersions 변수에 저장하면, 각 버전에 따라 저장되었던 데이터를 해당 버전의 구조에 따라 조회할 수 있게 됩니다.

import "../authorization/Ownership.sol"; contract Register is Owned { 
address contractAddress;
address[] contractVersions;
function changeContract(address _newContract) public onlyOwner() returns (bool) {
if(_newContract != dataContract) {
contractVersions.push(_newContract);
contractAddress = _newContract;
return true;
}
return false;
}
}

Contract Relay 패턴

이 패턴 역시 컨트랙트의 새로운 버전을 관리할 수 있는 개발 패턴이지만, 이 패턴의 경우에는 새로운 버전의 컨트랙트의 구조가 기존 버전과 똑같이 않다면 사용할 수가 없는 한계를 가지고 있습니다.

import "../authorization/Ownership.sol"; contract Relay is Owned { 
address public currentVersion;
constructor(address _address) public {
currentVersion = _address;
owner = msg.sender;
}
function changeContract(address _newVersion) public onlyOwner() {
currentVersion = _newVersion;
}
// fallback function
function() public {
require(currentVersion.delegatecall(msg.data));
}
}

이 외에도 다양한 maintenance 방법이 존재할 것입니다. 우리 속담에 ‘꺼진불도 다시 보자'라는 속담이 있습니다. 스마트컨트랙트 개발은 보고 또 보고, 확인하고 또 확인해도 모자랄만큼 많은 주의를 기울여야 하며, 개발 시 maintenance를 위한 다각도의 접근이 반드시 필요합니다.

--

--