저장소(storage)는 계약 계정에 영구히 남는 256비트 키-값 영역으로, 상태를 바꾸는 거래가 끝난 뒤에도 유지됩니다. 메모리(memory)는 메시지 호출마다 새로 생기는 바이트 단위 작업 공간이라 호출이 끝나면 사라지고, 커질수록 확장 가스가 붙습니다. 호출 데이터(calldata)는 외부 호출로 들어온 ABI 바이트를 담는 읽기 전용 영역이어서 원본을 고칠 수 없고 필요한 부분만 읽을 수 있습니다. Solidity 참조형에서 데이터 위치는 단순한 성능 표시가 아니라 별칭(alias)이냐 복사냐를 가르는 규칙입니다. 저장소 포인터를 수정하면 영구 상태가 바뀌고, 저장소 값을 메모리 변수에 대입하면 별도 복사본이 생깁니다.
세 영역은 수명부터 다릅니다
계약 계정마다 있는 저장소는 256비트 슬롯 번호에서 256비트 값으로 이어지는 영구 키-값 공간입니다. 함수가 끝나도 남아 있고, 다음 거래가 같은 상태를 읽습니다. Solidity 상태 변수는 압축 배치, 매핑 해시, 동적 배열 규칙에 따라 슬롯에 놓입니다. 다른 계약의 저장소를 SLOAD로 직접 읽을 수는 없으며, CALL과 DELEGATECALL 중 어떤 문맥이냐에 따라 어느 계정의 저장소인지가 정해집니다.
메모리는 메시지 호출마다 깨끗이 비워진 상태로 주어지고 바이트 단위로 주소를 매기지만, 일반적인 워드 연산은 32바이트씩 다룹니다. 높은 오프셋을 처음 건드리면 확장 비용이 붙고, 크기가 커질수록 선형 항과 제곱 항이 함께 늘어납니다. 호출 데이터는 거래나 외부 호출이 넘겨준 바이트이며 읽기 전용이라 CALLDATALOAD·CALLDATACOPY로 읽습니다.
데이터 위치는 같은 배열을 어디에 놓을지 정하는 표식이 아니라, 언제 사라지고 누가 수정 결과를 보게 되는지 정하는 실행 규칙입니다.
JOBCOIN 해설

그림은 이 주제의 공통 개념을 단순화한 설명입니다. 아래 항목에서 이 글의 구체적인 조건과 예외를 함께 읽어보세요.
- 세 영역은 수명부터 다릅니다
계약 계정마다 있는 저장소는 256비트 슬롯 번호에서 256비트 값으로 이어지는 영구 키-값 공간입니다.
- 가변성과 복사 규칙을 표로 확인합니다
외부 함수의 동적 매개변수를 calldata로 선언하면 컴파일러가 처음부터 전체를 메모리로 풀지 않고, 접근할 때 필요한 부분만 읽을 수 있습니다.
- 배열 하나도 위치에 따라 하는 일이 달라집니다
외부 함수가 원소 1,000개짜리 uint 배열을 받아 합계만 낸다고 해 보겠습니다.
설명을 위한 개념도 · 실제 가격·거래 내역·통계가 아닙니다.
가변성과 복사 규칙을 표로 확인합니다
외부 함수의 동적 매개변수를 calldata로 선언하면 컴파일러가 처음부터 전체를 메모리로 풀지 않고, 접근할 때 필요한 부분만 읽을 수 있습니다. 함수 안에서 값을 바꿔야 한다면 메모리 복사가 필요합니다. 공개 함수나 내부 호출에서는 컴파일러 버전과 ABI 경로에 따라 동작이 다를 수 있으니 생성된 중간 표현(IR)과 가스 보고서로 확인합니다.
저장소 참조끼리 대입하면 같은 상태 객체를 가리킬 수 있지만, 메모리로 옮기면 복사본이 생깁니다. 메모리 참조끼리 대입하면 같은 메모리 객체를 공유할 수 있어 한 변수로 원소를 바꾸면 다른 참조에서도 그 변화가 보입니다. 값형은 스택 값 복사가 기본이므로 배열·구조체 같은 참조형 규칙을 그대로 적용하지 않습니다.
| 영역 | 수명 | 쓰기 | 대표 비용 요인 |
|---|---|---|---|
| 저장소 | 거래가 끝나도 영구 | 가능 | SLOAD·SSTORE, 첫 접근 여부, 값 변화 |
| 메모리 | 현재 호출 단위 | 가능 | 최대 사용 오프셋과 확장 |
| 호출 데이터 | 현재 호출 입력 | 불가 | 입력 바이트 비용과 복사·해독 |
| 반환 데이터 | 직전 외부 호출 결과 | 직접 수정 불가 | RETURNDATACOPY 크기 |
| 스택 | 현재 실행 단위 | 상단 워드 연산 | 옵코드와 1024 깊이 제한 |
배열 하나도 위치에 따라 하는 일이 달라집니다
외부 함수가 원소 1,000개짜리 uint 배열을 받아 합계만 낸다고 해 보겠습니다. calldata로 받으면 원본을 그대로 순회하며 읽습니다. 원소마다 값을 바꿔야 해서 uint[] memory로 받으면, ABI 디코더가 배열 길이와 내용을 메모리에 펼쳐 놓으면서 확장 비용을 치릅니다. 이 배열을 상태 변수에 저장하면 슬롯마다 SSTORE가 일어나 가장 큰 지속 비용이 듭니다.
저장소 배열을 메모리 지역 변수로 복사한 뒤 고치면 영구 배열은 바뀌지 않습니다. 반대로 저장소 포인터를 잡고 원소를 고치면 거래가 성공했을 때 상태 루트에 반영됩니다. 코드 리뷰에서는 함수 시그니처만 보지 말고, 대입 양쪽의 데이터 위치와 컴파일러가 만든 복사 루프까지 확인해야 합니다.
- 이 데이터가 다음 거래에서도 필요한지부터 따집니다.
- 입력을 고치지 않는다면 calldata를 쓸 수 있는지 확인합니다.
- 메모리 최대 오프셋과 예상 배열 길이에 상한을 둡니다.
- 코드 리뷰에서 저장소 포인터와 메모리 복사를 표시해 둡니다.
- 컴파일러 버전을 고정하고 가스 측정 결과를 비교합니다.
비용은 같은 거래에서 처음 접근하는 콜드 슬롯에서 갈립니다
SSTORE 비용은 슬롯의 원래 값, 현재 값, 새 값과 환급 카운터에 따라 달라집니다. 같은 거래 안에서 처음 접근하는 콜드 슬롯과 이미 접근한 웜 슬롯도 비용이 다릅니다. ‘저장소 쓰기는 늘 20,000 가스’라는 말은 0에서 영이 아닌 값으로 처음 설정하는 일부 경우만 설명합니다.
메모리도 이미 확장한 범위 안에서 다시 접근할 때와 최고 사용 지점을 새로 늘릴 때의 비용이 다릅니다. 호출 데이터 역시 체인의 거래 데이터 요금과 실행 중 복사 비용을 나눠 봐야 합니다. 최적화기가 루프나 경계 검사를 바꿀 수 있으므로 어림 수치 대신 목표 컴파일러·EVM 버전으로 만든 바이트코드를 직접 측정합니다.
안전성은 길이와 별칭 검증에서 시작합니다
공격자가 아주 긴 calldata 배열을 보내면 전체 복사나 루프가 블록 가스 한도 안에서 실패하거나, 서비스의 가스 추정을 바닥내게 만들 수 있습니다. 길이 상한, 페이지 나누기, 머클 증명처럼 입력 크기를 묶어 두는 설계를 씁니다. 어셈블리에서 calldata 오프셋과 길이를 직접 다룰 때는 오버플로와 ABI 범위 검사를 빼먹지 않습니다.
업그레이드 계약은 저장소 배치를 바꾸면 기존 슬롯의 의미를 잃습니다. 메모리 최적화가 옳더라도 영구 변수 순서와 매핑 기준 슬롯은 따로 호환성을 검사해야 합니다. 성능 검토와 업그레이드 안전 검토를 가스 절감 숫자 하나로 합쳐 버리지 않습니다.
자주 묻는 질문
calldata 배열의 원소를 함수 안에서 바꿀 수 있나요?
읽기 전용이라 직접 바꿀 수 없습니다. 고쳐야 한다면 메모리로 복사해 별도 배열로 다룹니다.
메모리는 거래가 진행되는 동안 계속 공유되나요?
아닙니다. 메시지 호출 단위마다 새 메모리가 주어지며, 외부 호출 사이에서는 호출 데이터와 반환 데이터로 값을 주고받습니다.
저장소 변수를 읽기만 해도 가스가 드나요?
체인 위에서 실행하면 SLOAD 비용이 들고, 콜드·웜 접근 여부에 따라 금액이 달라집니다. 체인 밖에서 eth_call로 실행하면 사용자가 수수료를 내지 않지만 실행 규칙은 똑같이 적용됩니다.
더 깊이 읽기
본문에서 다룬 개념과 확인 절차를 다음 글에서 이어서 살펴보세요.



