이더리움 컨트랙트 스토리지 슬롯: 패킹·매핑·eth_getStorageAt 읽는 법
Solidity가 이더리움 컨트랙트 스토리지 슬롯을 배치하는 원리와 매핑·배열 위치 계산, eth_getStorageAt 검증 방법을 설명합니다.

블록 익스플로러에는 컨트랙트의 공개 변수가 반듯한 표처럼 보입니다. 하지만 체인이 실제로 저장하는 것은 변수 이름이나 데이터베이스 표가 아니에요. 번호가 붙은 위치에 들어 있는 32바이트 값입니다. **이더리움 컨트랙트 스토리지 슬롯(Ethereum Contract Storage Slot)**을 이해하면 정체를 알 수 없던 16진수 값을 근거 있는 상태 정보로 읽을 수 있고, 일상적인 블록체인 기초를 EVM이 실제로 다루는 상태와 연결할 수 있어요.
이 글에서는 Solidity 기본 레이아웃, 슬롯 패킹(Packing), 매핑과 동적 배열, eth_getStorageAt JSON-RPC 메서드를 설명합니다. 프록시, 잘못된 소스 코드, 과거 블록 기준과 private 데이터가 비밀이 아니라는 실무 함정도 함께 짚어볼게요.
이더리움 컨트랙트 스토리지 슬롯이란 무엇인가요?
컨트랙트 스토리지를 번호가 붙은 금고 상자가 끝없이 늘어선 벽이라고 생각해 보세요. 각 상자의 열쇠는 256비트이고, 안에는 256비트 값 하나가 들어갑니다. Solidity는 상자에 사람이 읽기 쉬운 변수 이름과 자료형을 붙이지만 EVM은 슬롯 키와 워드(Word)를 다뤄요.
Solidity 스마트 컨트랙트 문서는 영구 스토리지를 256비트 워드에서 256비트 워드로 이어지는 키-값 저장소라고 설명합니다. 스토리지는 계정 하나에 속하고 호출과 트랜잭션이 끝나도 유지돼요. 컨트랙트가 스스로 전체 슬롯을 열거할 수는 없습니다. 호출마다 새로 만들어지는 EVM 메모리, 트랜잭션 끝에 초기화되는 트랜지언트 스토리지와 수명이 다릅니다.
논리적인 변수가 어느 물리 슬롯을 쓰는지는 Solidity 컴파일러가 정해요. 스토리지 참조가 라이브러리 경계를 오갈 수 있기 때문에 Solidity는 레이아웃 규칙을 언어의 외부 인터페이스 일부로 취급합니다.
Solidity는 상태 변수를 어떻게 배치하고 패킹하나요?
일반 상태 변수는 상속 순서를 확정한 뒤 선언 순서에 따라 슬롯 0부터 배치됩니다. 32바이트가 필요한 값은 슬롯 하나를 모두 써요. 32바이트보다 작은 값은 서로 이웃하고 한 슬롯에 들어갈 수 있을 때 같은 슬롯을 나눠 쓸 수 있습니다.
단순한 예시를 볼게요.
contract Ledger {
uint128 public deposited;
uint128 public withdrawn;
mapping(address => uint256) public balances;
uint256[] public checkpoints;
}기본 레이아웃은 다음처럼 시작합니다.
| 선언 | 기준 슬롯 | 슬롯에 담기는 내용 |
|---|---|---|
deposited, withdrawn | 0 | 16바이트 값 두 개를 패킹 |
balances | 1 | 매핑 기준점, 항목은 파생 슬롯에 저장 |
checkpoints | 2 | 동적 배열 길이, 원소는 다른 위치에 저장 |
패킹은 임의로 압축하는 과정이 아닙니다. Solidity 스토리지 레이아웃 명세가 정확한 규칙을 정의해요. 먼저 선언한 패킹 값은 슬롯의 하위 바이트 쪽에 배치되고, 남은 공간에 들어가지 않는 값은 새 슬롯에서 시작합니다. 구조체(Struct)와 배열 데이터는 새 슬롯 경계에서 시작하지만 그 내부 구성원은 같은 규칙으로 패킹될 수 있어요.
패킹은 영구 상태가 차지하는 슬롯 수를 줄일 수 있지만 모든 쓰기 비용이 항상 낮아진다는 뜻은 아닙니다. 패킹된 필드 하나만 바꿀 때도 같은 워드의 나머지 바이트를 보존해야 해요. 자료형은 정확성을 먼저 기준으로 고르고, 실제 배포할 컴파일러 버전과 호출 방식으로 가스를 측정하세요.
Note
상수(Constant)와 불변 변수(Immutable)는 일반 가변 상태와 다르게 처리됩니다. 소스에 선언된 모든 항목의 순서만 보고 슬롯을 단정하면 안 돼요.
매핑과 동적 배열의 위치는 어떻게 계산하나요?
매핑(Mapping)은 키가 몇 개 생길지 미리 알 수 없어서 Solidity가 연속된 상자를 예약할 수 없습니다. 대신 선언 위치 p를 기준점으로 잡고 각 항목의 위치를 Keccak-256으로 계산해요.
키가 값 자료형인 매핑 항목의 위치는 개념적으로 다음과 같습니다.
keccak256(h(key) . p)h(key)는 규칙에 맞게 키를 패딩하거나 인코딩한 값이고, p는 32바이트 기준 슬롯이며, .은 이어 붙이기를 뜻합니다. 위 예시의 balances[user]는 사용자 주소를 인코딩한 값과 슬롯 1로부터 계산돼요. 기준 슬롯 자체에는 매핑 크기나 키 목록이 저장되지 않습니다. 연속 슬롯을 읽는 것만으로 모든 매핑 항목을 발견할 수 없는 이유예요.
동적 배열은 기준 슬롯을 다르게 사용합니다. 슬롯 p에는 배열 길이가 들어가고, 실제 원소 데이터는 keccak256(p)에서 시작해요. 고정 크기 원소의 패킹 여부에 따라 특정 인덱스의 오프셋도 달라질 수 있습니다. 중첩된 매핑과 배열은 이 파생 규칙을 반복하므로 자료형, 키 인코딩 또는 부모 슬롯 하나만 틀려도 전혀 관계없는 워드를 그럴듯하게 읽을 수 있어요.
짧은 bytes 값과 문자열에는 별도 인코딩이 있습니다. 짧은 내용은 길이 표시와 함께 기준 슬롯에 들어갈 수 있지만 긴 내용은 분리된 데이터 영역을 사용해요. 모든 동적 값이 기본 배열 예시와 같다고 가정하지 말고 컴파일러 레이아웃 메타데이터와 정확한 Solidity 문서를 확인하세요.
eth_getStorageAt으로 슬롯을 어떻게 읽나요?
이더리움 표준 JSON-RPC 메서드인 eth_getStorageAt은 세 가지 매개변수를 받습니다.
- 컨트랙트 주소
- 16진수 수량으로 표현한 스토리지 위치
- 블록 번호 또는
latest,safe,finalized같은 태그
슬롯 0을 읽는 요청은 다음과 같아요.
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getStorageAt",
"params": ["0xContractAddress", "0x0", "finalized"]
}응답은 32바이트 데이터입니다. 디코딩은 별도 단계예요. 변수의 자료형, 바이트 오프셋, 패킹 규칙을 알아야 합니다. 전체 워드에 주소 하나가 저장됐다면 관련 20바이트는 오른쪽에 정렬됩니다. uint128 두 개가 패킹됐다면 변환하기 전에 각 절반을 분리해야 해요.
매핑을 읽을 때는 파생 슬롯을 로컬에서 먼저 계산한 뒤 그 32바이트 위치를 eth_getStorageAt에 전달합니다. 자체 유틸리티를 믿기 전에 이더리움 문서의 인코딩 예시를 테스트 벡터로 사용하세요. 여기에는 Keccak-256 함수가 필요합니다. 표준화된 SHA3-256은 관련된 함수지만 패딩 방식이 달라 이더리움 Keccak과 같은 결과를 만들지 않아요.
블록 매개변수도 주장에 포함됩니다. latest는 “이 노드가 현재 최신이라고 보는 상태”를 묻습니다. 특정 블록을 지정하면 제공자가 그 과거 상태를 보존하고 서비스하는 경우 역사적 질문에 답할 수 있어요. 감사나 정산 판단에 결과를 사용한다면 체인 ID, 컨트랙트 주소, 블록 번호와 블록 해시를 함께 보관하세요.
직접 추측하기보다 컴파일러 메타데이터를 활용하세요
슬롯을 손으로 계산하는 일은 학습과 독립 검증에 유용합니다. 하지만 컴파일러도 스토리지 레이아웃 메타데이터를 출력할 수 있어요. 표준 JSON 컴파일러 출력에서 storageLayout을 요청하면 변수 라벨, 슬롯, 오프셋, 자료형 식별자와 인코딩·바이트 크기를 설명하는 자료형 표를 얻습니다.
신뢰할 수 있는 확인 순서는 다음과 같아요.
- 검증된 소스를 확보하고 실제 배포에 사용된 컴파일러 설정을 식별합니다.
- 해당 소스를 컴파일하면서 스토리지 레이아웃 출력을 요청해요.
- 런타임 주소가 직접 컨트랙트인지, 어떤 프록시의 구현인지 확인합니다.
- 블록 기준을 정하고 필요한 매핑 또는 배열 슬롯을 계산해요.
- JSON-RPC로 워드를 읽고 보고된 자료형과 오프셋에 따라 디코딩합니다.
- 공개 게터(Getter)가 있다면 알려진 값과 교차 확인하세요.
스토리지 레이아웃 JSON 형식은 실험적이라고 문서화되어 있으므로 도구는 메타데이터 형식 변화에 대응해야 합니다. 이미 배포된 컨트랙트의 실제 레이아웃은 소스를 새 컴파일러로 다시 컴파일한다고 바뀌지 않아요.
프록시와 네임스페이스는 시작점을 바꿉니다
프록시(Proxy)에서는 구현 바이트코드가 프록시의 스토리지를 대상으로 실행됩니다. 구현 주소의 슬롯 0을 읽으면 실제 앱의 잔액이나 설정과 무관한 값이 나올 수 있어요. 먼저 프록시 실행 맥락을 확인한 다음 의도한 블록에서 프록시 주소를 읽어야 합니다.
ERC-1967은 프록시의 구현, 비콘(Beacon), 선택적 관리자(Admin)를 위한 특별 슬롯을 정합니다. 이 위치는 컴파일러의 일반 스토리지 트리와 충돌하지 않도록 파생돼요. 업그레이드 구조를 식별하는 데 유용하지만 모든 애플리케이션 변수의 위치를 설명하지는 않습니다.
ERC-7201은 네임스페이스 스토리지(Namespaced Storage)를 위한 NatSpec 주석과 위치 계산식을 표준화합니다. 네임스페이스는 슬롯 0에서 시작하는 기본 트리 대신 파생 위치에 구조체의 뿌리를 둬요. 주석은 의도를 문서화할 뿐이며 ERC도 컴파일러가 해당 계산식을 강제하지 않는다고 명시합니다. 실제 접근자 코드와 계산된 상수를 검토하세요.
리스크와 한계
온체인 데이터가 스스로 의미를 설명하지는 않아요
반환된 바이트는 노드가 서비스한 주소와 블록의 상태이지만, 변수 이름과 자료형은 워드 안에 들어 있지 않습니다. 잘못된 소스나 다른 구현의 ABI를 적용하면 권위 있어 보이지만 틀린 해석이 나올 수 있어요.
private은 비밀이라는 뜻이 아니에요
Solidity 가시성은 다른 컨트랙트가 언어 문법으로 변수에 접근하는 방식을 제한합니다. 상태를 암호화하지는 않아요. 슬롯 위치와 체인 데이터가 있으면 누구나 바이트를 읽을 수 있습니다. 비밀번호, 시드 구문(Seed Phrase), 암호화하지 않은 개인정보, 공개 전 비밀값을 컨트랙트 스토리지에 저장하면 안 됩니다.
업그레이드는 레이아웃을 망가뜨릴 수 있어요
프록시 업그레이드는 기존 스토리지를 다시 사용합니다. 변수를 재정렬하거나 호환되지 않는 자료형으로 바꾸면 새 코드가 오래된 바이트를 다른 의미로 해석할 수 있어요. 네임스페이스와 표준 프록시 슬롯은 일부 충돌 위험을 줄이지만 업그레이드 검증, 접근 권한 검토, 테스트와 모니터링을 대신하지 않습니다.
원시 슬롯 읽기는 승인 근거가 아니에요
슬롯 값은 의도한 블록보다 오래됐거나 잘못 디코딩됐거나 다른 체인·주소에서 읽었을 수 있습니다. 수동으로 해석한 워드 하나만으로 출금을 승인하거나 입금을 확정하거나 투자 결정을 내리지 마세요. 맥락과 애플리케이션 불변 조건을 함께 확인해야 합니다.
실무 체크리스트
- 체인 ID, 컨트랙트 주소, 블록 번호 또는 태그를 확인했나요?
- 주소가 프록시, 구현, 비콘, 직접 컨트랙트 중 무엇인지 식별했나요?
- 검증된 소스, 컴파일러 버전, 스토리지 레이아웃 메타데이터가 일치하나요?
- 패킹 값을 해석하기 전에 슬롯, 바이트 오프셋, 자료형을 확인했나요?
- 매핑 키를 정확히 인코딩하고 Keccak-256을 사용했나요?
- 동적 배열, 문자열, 바이트, 구조체와 네임스페이스에 맞는 규칙을 적용했나요?
- 가능하면 게터, 이벤트 이력 또는 애플리케이션 불변 조건과 교차 확인했나요?
-
private으로 표시된 값도 공개적으로 읽을 수 있다고 가정했나요?
자주 묻는 질문
0이 아닌 컨트랙트 슬롯을 모두 열거할 수 있나요?
기본 컨트랙트 인터페이스나 eth_getStorageAt을 순서대로 호출하는 방식으로는 어렵습니다. 키 공간이 매우 크고 매핑 기준 슬롯에는 키 목록이 저장되지 않아요. 디버그 또는 상태 검사 확장이 제공자별 기능을 제공할 수 있지만 여기서 설명한 이식 가능한 표준 JSON-RPC 메서드는 아닙니다.
슬롯 0에는 항상 소스 파일의 첫 변수가 들어가나요?
아니요. 상속, 패킹, 상수, 불변 변수, 사용자 지정 레이아웃과 프록시 실행 맥락이 결론을 바꿀 수 있어요. 정확한 컴파일러 메타데이터와 배포 맥락을 확인하세요.
컨트랙트 스토리지는 이더리움 상태 트라이와 같은 건가요?
아니요. 슬롯은 컨트랙트 수준의 키-값 관점입니다. 이더리움은 계정의 스토리지를 스토리지 트라이(Storage Trie)로 커밋하고, 계정 상태를 다시 전역 상태 루트에 연결해요. 머클 패트리샤 트라이 가이드에서 이 인증 구조를 설명합니다.
eth_getStorageAt이 컨트랙트 상태를 바꿀 수 있나요?
아니요. 읽기 전용 RPC 질의입니다. 상태 변경 트랜잭션을 실행하지 않으며 반환 바이트에 대한 해석이 옳다고 증명하지도 않아요.
1차 출처
- Solidity: 상태 변수 스토리지 및 트랜지언트 스토리지 레이아웃
- Solidity: 스마트 컨트랙트 소개 — 스토리지·메모리·스택
- Ethereum.org: JSON-RPC API — eth_getStorageAt
- ERC-1967: 프록시 스토리지 슬롯
- ERC-7201: 네임스페이스 스토리지 레이아웃
바이트를 전체 맥락 속에서 읽으세요
이더리움 컨트랙트 스토리지 슬롯은 EVM 수준에서 단순합니다. 하나의 256비트 키가 하나의 256비트 값으로 이어져요. 어려운 부분은 그 워드 주변의 Solidity 자료형, 패킹, 파생 위치, 실행 주소와 블록 맥락을 복원하는 일입니다. 컴파일러 메타데이터를 우선하고 위치 계산을 독립적으로 재현하며 중요한 결론은 다른 인터페이스와 교차 확인하세요.
이 글은 교육 목적이며 투자 조언이 아닙니다. 스마트 컨트랙트와 크립토 자산은 실패하거나 가치를 잃을 수 있어요. 최신 1차 문서를 확인하고, 잃어도 되는 금액으로 테스트하며, 스스로 조사하세요(DYOR).
함께 읽으면 좋은 글

스마트 컨트랙트란? 작동 원리와 활용 사례 완전 정리 (2026)
스마트 컨트랙트의 개념부터 자판기 비유로 풀어낸 작동 원리, 디파이·NFT·RWA 활용 사례, 버그·해킹·불변성 리스크, FAQ까지 한 번에 정리했습니다.

이더리움 eth_getProof 가이드: 계정·스토리지 상태 검증하기
eth_getProof 응답의 계정·스토리지 증명을 블록 상태 루트와 연결해 검증하는 원리, 절차, 신뢰 경계와 한계를 설명합니다.
이더리움 프록시 컨트랙트 완전 정리: delegatecall·UUPS·업그레이드 리스크
이더리움 프록시 컨트랙트의 delegatecall 구조, 투명·UUPS·비콘 방식의 차이와 실제 업그레이드 권한을 안전하게 확인하는 법을 설명해요.
관련 주제 살펴보기

이더리움 글램스테르담 업그레이드: ePBS·BAL과 확정된 사실
글램스테르담은 2026년 4분기 예정이에요. 확정된 범위, 세폴리아 일정, ePBS, 블록 레벨 접근 리스트와 남은 변수를 설명합니다.
크립토 주소 포이즈닝: 송금 전 지갑 주소를 검증하는 방법
주소 포이즈닝은 거래 내역에 닮은 지갑 주소를 심는 사기예요. 작동 원리와 안전한 전체 주소 검증 체크리스트를 알아보세요.