이더리움 스마트 컨트랙트에서 가스(Gas)는 곧 비용
같은 기능이라도 코드 작성 방식에 따라 가스 소비량이 수 배 이상 차이
배포 이후에는 수정 불가이므로 사전 최적화가 유일한 기회
Gas와 EVM 연산 비용
EVM(이더리움 가상머신)은 각 연산(Opcode)마다 고정된 가스 비용 부과
실제 수수료는 사용된 가스량 × Gas Price(Gwei)로 계산
EVM 데이터 저장 위치별 비용 구조 — storage(영구), memory(임시), stack(연산용)
출처: ethereum.org — Ethereum Virtual Machine
위치별 접근 비용 차이가 최적화의 핵심 근거
| 데이터 위치 | Opcode | 가스 비용 | 특징 |
|---|---|---|---|
| storage 읽기 (cold) | SLOAD | 2,100 | 블록체인 영구 저장, EIP-2929 이후 |
| storage 쓰기 (신규) | SSTORE | 20,000+ | 가장 비싼 연산 |
| memory 읽기/쓰기 | MLOAD/MSTORE | 3 | 트랜잭션 종료 시 소멸 |
| 바이트코드 인라인 값 | PUSH | 3 | immutable / constant 적용 시 |
| 산술 연산 | ADD, MUL | 3~5 | 매우 저렴 |
2021년 Berlin 하드포크(EIP-2929)로 SLOAD 비용이 800 → 2,100으로 인상
스토리지 접근 횟수를 줄이는 패턴의 절감 효과가 이때부터 더욱 크게 작용
패턴 1. Custom Error — revert 비용을 4바이트로
require의 문자열은 배포 시 바이트코드에 그대로 포함
문자열 길이에 비례해 배포 비용 증가, revert 시마다 전체 문자열 ABI 인코딩 반환
Custom Error는 함수 시그니처 selector 4바이트만 반환 — 길이 고정
| 방식 | revert 반환 크기 | 바이트코드 영향 |
|---|---|---|
require("Only owner can call this.") | 문자열 길이(~30바이트) 비례 | 문자열 전체 포함 |
revert NotOwner() | selector 4바이트 고정 | 시그니처 해시만 포함 |
// Custom Error 선언 및 사용
error NotOwner();
error InsufficientBalance(uint256 available, uint256 required);
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
function withdraw(uint256 amount) public {
if (balance < amount)
revert InsufficientBalance(balance, amount);
}
Solidity 0.8.4부터 도입, 파라미터를 추가해도 string 방식보다 항상 유리
패턴 2. immutable / constant — SLOAD를 PUSH로 교체
배포 후 변경되지 않는 값을 일반 상태 변수로 선언하면 읽을 때마다 SLOAD 발생
immutable과 constant는 값을 바이트코드에 직접 인라인 삽입
읽기 연산이 SLOAD(2,100 gas) → PUSH(3 gas)로 교체됨
| 키워드 | 값 결정 시점 | 읽기 비용 | 대표 사용 예 |
|---|---|---|---|
constant | 컴파일 시점 | ~3 gas (PUSH) | 고정 상수 (uint256 MAX = 1000) |
immutable | constructor 실행 시 | ~3 gas (PUSH) | 배포자 주소 (msg.sender) |
| 일반 상태 변수 | 런타임 언제든 | 2,100 gas (SLOAD) | 런타임 변경이 필요한 값 |
contract Counter {
uint256 public constant MAX_VALUE = 10000; // 컴파일 타임 고정
address public immutable owner; // 배포 시 결정, 이후 불변
constructor() {
owner = msg.sender;
}
}
패턴 3. storage 캐싱 — SLOAD 횟수 최소화
같은 스토리지 변수를 함수 내에서 반복 참조하면 그 횟수만큼 2,100 gas가 누적
로컬 변수에 한 번 캐싱하면 이후 접근은 memory 비용(~3 gas)으로 처리
루프와 결합 시 절감폭이 배열 크기에 비례해 선형으로 증가
// Before — SLOAD 3회 (6,300 gas)
function report() public {
emit Log(counter);
uint256 x = counter * 2;
uint256 y = counter + 1;
}
// After — SLOAD 1회 (2,100 gas) + MLOAD 2회 (6 gas)
function report() public {
uint256 c = counter; // 단 한 번의 SLOAD
emit Log(c);
uint256 x = c * 2;
uint256 y = c + 1;
}
배열을 순회하는 경우 arr.length도 스토리지 읽기 대상
루프 전에 uint256 len = arr.length로 캐싱하는 것만으로 반복 SLOAD 제거 가능
패턴 4. mapping으로 O(n) → O(1)
배열 순회는 데이터 크기에 비례해 가스가 선형 증가
데이터가 충분히 커지면 블록 가스 한도 초과로 트랜잭션 자체가 실패하는 상황 발생
mapping은 해시 테이블 구조로, 크기와 무관하게 항상 일정한 가스 소비
| 조회 방식 | 시간 복잡도 | 가스 특성 |
|---|---|---|
배열 순회 (for) | O(n) | 데이터 크기에 비례해 선형 증가 |
| mapping 조회 | O(1) | 크기 무관, 항상 고정 |
// 역매핑 패턴 — O(1) 조회 + 전체 목록 순회를 모두 지원
address[] public userList;
mapping(address => uint256) public userIndex;
function register(address user) internal {
userIndex[user] = userList.length; // mapping으로 인덱스 저장
userList.push(user); // 배열로 전체 목록 유지
}
function getIndex(address user) public view returns (uint256) {
return userIndex[user]; // O(1) 조회
}
정리
| 패턴 | Before | After | 절감 핵심 |
|---|---|---|---|
| Custom Error | require("string") |
revert Error() |
revert 비용 고정 4바이트 |
| immutable | 일반 상태 변수 | immutable 선언 |
SLOAD(2,100) → PUSH(3) |
| constant | 일반 상태 변수 | constant 선언 |
스토리지 슬롯 미사용 |
| storage 캐싱 | 반복 SLOAD | 로컬 변수 1회 캐싱 | SLOAD 횟수 최소화 |
| mapping 활용 | 배열 순회 O(n) | mapping 조회 O(1) | 가스 선형 증가 차단 |
이더리움 컨트랙트는 배포 후 수정 불가
가스 최적화는 비용 절감인 동시에 복잡한 스토리지 로직을 단순화해
reentrancy 등 보안 취약점 노출 면적 축소의 의미도 존재
참고 자료
[1] Ethereum Foundation — Ethereum Virtual Machine (EVM)
https://ethereum.org/en/developers/docs/evm/
[2] Vitalik Buterin, Martin Swende — EIP-2929: Gas cost increases for state access opcodes (2020)
https://eips.ethereum.org/EIPS/eip-2929
[3] Solidity Documentation — Custom Errors
https://docs.soliditylang.org/en/latest/control-structures.html#custom-errors
[4] Solidity Documentation — The Optimizer
https://docs.soliditylang.org/en/latest/internals/optimizer.html
[5] ethereum.org — Gas and Fees
https://ethereum.org/en/developers/docs/gas/