티스토리 뷰

업그레이드 가능한 컨트랙트 작성해보기 실습을 통해 블록체인 스마트 컨트랙트의 배포 후 코드 수정 불가능(Immutability) 한계를 극복하는 실무 기법을 정리합니다. 이더리움 가상머신(EVM) 생태계에서 서비스 중단 없이 로직을 변경하기 위해 널리 사용되는 방식은 상태 데이터와 비즈니스 로직을 분리하는 프록시 패턴(Proxy Pattern)입니다.
본 가이드에서는 OpenZeppelin의 Hardhat Upgrades 라이브러리를 활용하여 기본 상태를 가진 Box 컨트랙트를 배포한 뒤, 신규 기능을 추가한 BoxV2로 실제 업그레이드를 수행하고 상태 변수(State)가 안전하게 유지되는 전체 파이프라인을 단계별로 검증합니다.
스마트 컨트랙트 업그레이드 원리와 프록시 아키텍처 구조
블록체인에 한 번 배포된 스마트 컨트랙트는 원칙적으로 바이트코드 수정이 불가능합니다. 하지만 실무에서는 보안 버그 수정, 신규 기능 추가, 규제 대응 등의 사유로 컨트랙트 로직을 수정해야 하는 상황이 빈번하게 발생합니다. 이러한 문제를 해결하는 핵심 아키텍처가 바로 프록시 패턴(Proxy Pattern)입니다.

| 구성 요소 | 주요 역할 | 스토리지 사용 여부 |
|---|---|---|
| Proxy 컨트랙트 | 사용자 호출을 받아 로직 컨트랙트로 delegatecall 전달 | 실제 모든 상태 변수(State) 저장 |
| Implementation (Box / BoxV2) | 실제 비즈니스 로직 및 함수 실행 코드 포함 | 로직만 제공하며 자체 스토리지는 비움 |
| ProxyAdmin 컨트랙트 | 프록시의 관리 권한을 행사하고 구현체 주소 교체 관리 | 관리자 전용 제어 스토리지 보유 |
사용자는 프록시 컨트랙트의 주소와 직접 상호작용하며, 프록시는 delegatecall을 통해 Implementation 컨트랙트의 로직을 실행합니다. 이 과정에서 모든 상태 데이터는 프록시의 스토리지 슬롯에 기록되므로 로직 컨트랙트를 교체하더라도 기존 누적 데이터는 그대로 유지됩니다.
개발 환경 설정 및 초기 로직 컨트랙트(Box) 구현
OpenZeppelin이 제공하는 Hardhat 플러그인을 설치하면 프록시 배포, 검증, 업그레이드 전 안전성 검사(Storage Collision 검사 등)를 손쉽게 자동화할 수 있습니다. 프로젝트 터미널에서 필수 의존성 패키지를 설치합니다.


| 구분 | 패키지명 | 설치 명령 |
|---|---|---|
| 스마트 컨트랙트 표준 라이브러리 | @openzeppelin/contracts | npm install @openzeppelin/contracts |
| 하드햇 업그레이드 전용 플러그인 | @openzeppelin/hardhat-upgrades | npm install --save-dev @openzeppelin/hardhat-upgrades |
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;
contract Box {
uint public val;
function initialize(uint _val) external {
val = _val;
}
}
프록시 패턴에서는 표준 배포 시 실행되는 constructor 대신 initialize 함수를 사용합니다. 생성자는 컨트랙트 배포 시점에 배포 주소의 스토리지에만 기록되므로, 프록시의 스토리지 컨텍스트에서 상태를 설정하기 위해서는 배포 직후 호출되는 별도의 초기화 함수가 필수적입니다.
테스트넷 배포 및 배포 아티팩트(3종 컨트랙트) 검증
Hardhat 환경 설정 파일(hardhat.config.js)에 업그레이드 플러그인과 네트워크 구성을 추가한 뒤 배포 스크립트를 작성합니다.



// hardhat.config.js 필수 플러그인 로드
require("@nomiclabs/hardhat-ethers");
require("@nomiclabs/hardhat-etherscan");
require("@openzeppelin/hardhat-upgrades");
const { ethers, upgrades } = require("hardhat");
async function main() {
const Box = await ethers.getContractFactory("Box");
const box = await upgrades.deployProxy(Box, [42], { initializer: "initialize" });
await box.deployed();
console.log("Box deployed to:", box.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
작성된 스크립트를 폴리곤 뭄바이(Mumbai) 테스트넷에 배포합니다:
npx hardhat run scripts/deploy.js --network mumbai
배포가 완료되면 블록 탐색기(Polyscan)에서 트랜잭션 발신자(from) 기준으로 총 3개의 컨트랙트가 순차 생성된 것을 확인할 수 있습니다.
1. Box (Logic/Implementation)
2. Proxy (Transparent Upgradeable Proxy)
3. ProxyAdmin (업그레이드 권한 제어 컨트랙트)
배포된 Box 로직 컨트랙트의 ReadContract를 조회하면 val 값이 0으로 나타납니다. 이는 로직 컨트랙트 자체에 데이터가 저장되지 않고, 프록시 스토리지에 42로 정상 기록되었기 때문입니다. Polyscan의 프록시 주소에서 'Is this a proxy?' 기능을 활성화하면 'Read as Proxy' 탭에서 저장된 42를 확인할 수 있습니다.
BoxV2 로직 구현 및 프록시 업그레이드 실행
기존 Box 컨트랙트에 새로운 상태 변경 함수인 inc()를 추가한 BoxV2 컨트랙트를 작성합니다. 업그레이드 컨트랙트에서는 initialize를 재선언하지 않는데, 이는 초기화 함수가 시스템 전체 라이프사이클에서 단 1회만 호출되어야 하기 때문입니다.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.4;
contract BoxV2 {
uint public val;
function inc() external {
val += 1;
}
}
const { ethers, upgrades } = require("hardhat");
const PROXY_ADR = "0x3f5F0ebA1bc4A2F3A0bf674F80181602e1b48743";
async function main() {
const BoxV2 = await ethers.getContractFactory("BoxV2");
await upgrades.upgradeProxy(PROXY_ADR, BoxV2);
console.log("Box upgraded successfully");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
명령어 npx hardhat run scripts/upgrade_box.js --network mumbai를 실행하면 두 개의 트랜잭션이 발생합니다.
첫 번째는 새로운 BoxV2 로직 컨트랙트를 배포하는 트랜잭션이며, 두 번째는 ProxyAdmin을 통해 프록시가 가리키는 구현체 주소를 BoxV2로 교체하는 업그레이드 트랜잭션입니다.
상태 보존 검증 및 업그레이드 시 실무 주의사항
업그레이드 완료 후 탐색기에서 프록시 컨트랙트 주소의 'Write as a proxy' 탭으로 이동하면 새롭게 추가된 inc 함수가 정상적으로 노출됩니다. Web3 지갑(MetaMask)을 연결하고 inc 함수를 실행하는 트랜잭션을 전송합니다.

트랜잭션 확정 후 'Read as Proxy' 탭에서 val 값을 재조회하면 기존 저장값 42에서 1이 증가한 43으로 변경된 것을 확인할 수 있습니다. 프록시 주소와 누적 상태 변수를 그대로 보존하면서 신규 로직을 성공적으로 적용한 것입니다.
- 기존 변수 순서 및 타입 유지: 상태 변수는 스토리지 슬롯 순서대로 저장되므로, 기존 선언된 변수 사이에 새 변수를 끼워 넣으면 스토리지 충돌(Storage Collision)이 발생합니다.
- 생성자(Constructor) 사용 금지: 구현체 컨트랙트에서는 생성자 대신 OpenZeppelin의 Initializable 패턴을 적용하여 초기화 로직을 분리해야 합니다.
- 관리자 계정 분리: 프록시 관리 권한(ProxyAdmin)은 일반 사용자 호출과의 함수 선택자 충돌을 방지하기 위해 엄격히 통제된 다중서명(Multisig) 지갑 등으로 관리해야 합니다.
스마트 컨트랙트의 불변성은 블록체인의 강력한 신뢰 기반이지만, 예기치 못한 비즈니스 로직 수정이나 보안 취약점 패치에는 치명적인 한계가 될 수 있습니다. OpenZeppelin Hardhat Upgrades 플러그인을 활용한 프록시 패턴은 스토리지 상태를 영구히 보존하면서 로직만 안전하게 교체할 수 있는 실무 표준 솔루션입니다.
초기화 함수(initialize)의 1회 호출 원칙과 스토리지 슬롯 충돌 방지 등 프록시 특유의 개발 제약 사항을 사전에 철저히 숙지하고 개발에 임해야 합니다. 테스트넷 환경에서의 반복적인 업그레이드 시뮬레이션을 통해 안정적인 DApp 및 스마트 컨트랙트 아키텍처를 구축하시길 바랍니다.
이더리움(ETH) 거래 및 시장 대응을 준비 중이신가요?
이더리움(ETH) 거래를 준비 중이라면 관련 거래소의 거래 수수료와 진행 중인 혜택 조건을 함께 확인해 보세요. 공식 제휴 링크를 통해 신규 가입 및 KYC 인증 완료 시 거래 수수료 할인과 웰컴 보너스 혜택이 적용됩니다. 실제 거래를 시작하기 전 공식 가이드에서 세부 지급 조건과 적용 절차를 먼저 확인하시기 바랍니다.
[안내: 본 안내는 제휴 링크를 포함하고 있으며, 가입 및 이용 조건 충족 시 소정의 파트너 혜택이 발생할 수 있습니다. 모바일 앱 이용자는 APK 설치 가이드를 참고하세요.]
[투자 유의 안내] 본 글은 단순 정보 제공을 목적으로 하며 특정 가상자산에 대한 투자 권유나 자문이 아닙니다. 암호화폐 거래는 높은 가격 변동성과 원금 손실 위험을 수반하므로 모든 투자 판단과 책임은 투자자 본인에게 있습니다.