곰투 크립토
guide전체 41편 중 37편

이더리움 초기화 코드 vs 런타임 바이트코드: 실제로 배포되는 것은?

이더리움 초기화 코드가 한 번 실행된 뒤 런타임 바이트코드를 반환하는 과정과 생성자 인자, 가스, 크기 제한, 검증, CREATE2의 관계를 설명해요.

GOMTU
GOMTU
크립토 리서치 · 2026년 9월 11일 · 약 7분
공유𝕏in
이더리움 초기화 코드 vs 런타임 바이트코드: 실제로 배포되는 것은?

Solidity 컨트랙트를 컴파일해 긴 16진수 데이터를 배포했는데, 나중에 컨트랙트 주소에서 읽은 바이트가 다를 수 있어요. 오류라고 단정할 일은 아닙니다. 이더리움 컨트랙트 생성에는 한 번만 실행되는 **초기화 코드(Initcode)**와 이후 남는 **런타임 바이트코드(Runtime Bytecode)**가 따로 있기 때문이에요. 이 차이는 블록체인 기초에서 배포 가스, 생성자 동작, 소스 검증, 결정론적 주소를 이해하는 연결 고리입니다.

초기화 코드는 빈 부지에 설계도와 자재를 들고 들어가는 시공팀과 비슷해요. 시공팀은 초기 상태를 만들고 건물을 완성하지만 그곳에 남지는 않습니다. 런타임 바이트코드는 공사가 끝난 뒤 인도되는 건물이에요. 시공이 실패하거나 잘못된 결과를 내면 이더리움은 반쯤 지어진 컨트랙트를 남기지 않습니다.

초기화 코드와 런타임 바이트코드란?

광고

초기화 코드는 생성 코드(Creation Code) 또는 이니셜라이제이션 코드(Initialization Code)라고도 부르는 EVM 프로그램이에요. 최상위 배포 트랜잭션은 받는 주소(to)가 비어 있고 데이터에 초기화 코드를 담습니다. 이미 실행 중인 컨트랙트도 메모리의 초기화 코드를 CREATE 또는 CREATE2 옵코드에 넘길 수 있어요.

EVM은 새 계정의 문맥에서 이 임시 프로그램을 실행합니다. 생성자(Constructor) 로직을 돌리고, 영구 스토리지(Storage)를 초기화하고, 다른 컨트랙트를 만들거나 외부 계정을 호출할 수 있어요. 실행 끝에 바이트 배열을 반환하면 그 결과가 새 계정의 런타임 바이트코드가 됩니다. 이후 누군가 컨트랙트를 호출할 때 실행되는 프로그램이죠.

이더리움 옐로 페이퍼의 컨트랙트 생성 규칙도 같은 순서를 명시합니다. 초기화 코드를 실행해 출력 바이트를 얻고, 코드 저장 비용(Code-deposit Cost)을 부과한 뒤, 생성이 성공해야만 결과 상태를 유지해요. Solidity 생성자 공식 문서는 컴파일러 관점에서 이를 설명합니다. 생성자 코드는 생성 중 실행되지만, 최종 배포 코드는 외부에서 호출할 함수만 담고 생성자에서만 쓰는 코드는 제외해요.

배포 트랜잭션 또는 CREATE/CREATE2
                |
                v
       초기화 코드 + 생성자 인자
                |
          EVM에서 한 번 실행
        /                       \
     실패                        RETURN 바이트
      |                              |
컨트랙트 상태 없음             런타임 바이트코드 저장

그래서 디버깅할 때 단순히 “바이트코드”라고만 부르면 혼란이 생겨요. 도구가 생성 바이트코드, 생성자 인자를 붙인 전체 초기화 코드, 배포된 런타임 바이트코드 중 무엇을 보여주는지 먼저 확인해야 합니다.

Solidity 배포가 온체인 코드로 바뀌는 과정

Solidity 컴파일러 출력은 주요 결과물을 구분해요. 표준 JSON 출력에서 evm.bytecode.object는 생성 바이트코드이고, evm.deployedBytecode.object는 배포 바이트코드입니다. 두 필드는 Solidity 컴파일러 출력 사양에서 확인할 수 있어요.

일반적인 배포 흐름은 다음과 같습니다.

  1. 컴파일러가 생성 바이트코드와 예상 배포 바이트코드를 만듭니다.
  2. 배포 도구가 생성자 인자를 ABI 인코딩해 생성 바이트코드 뒤에 붙여요.
  3. to 필드가 빈 트랜잭션이 이 전체 초기화 코드를 담거나, 팩토리가 CREATE 또는 CREATE2에 넘깁니다.
  4. EVM은 만들어질 주소의 임시 실행 문맥을 열고 초기화 코드를 실행해요.
  5. 생성자는 스토리지를 쓰고, 불변값(Immutable)을 정하고, 로그를 내거나 외부 호출을 한 뒤 되돌릴 수도 있습니다.
  6. 초기화 코드가 성공하면 런타임 바이트코드를 반환해요.
  7. 이더리움이 반환된 코드를 저장하는 가스를 부과하고 모든 생성 조건이 통과되면 코드와 초기 상태를 확정합니다.

생성자 인자는 이 흐름의 입력에 속합니다. 생성 로직이 읽을 수 있지만 런타임 코드 뒤에 그대로 보관되는 것은 아니에요. 생성자는 값을 스토리지에 복사할 수도 있고, 불변값으로 런타임 코드에 새길 수도 있으며, 검사에만 쓰거나 아예 무시할 수도 있습니다.

Solidity는 생성 코드가 반환 전에 런타임 코드 속 불변값 위치를 실제 값으로 바꾼다고 설명해요. 따라서 컴파일러가 보여주는 예상 배포 바이트코드의 자리표시자와 특정 배포 후 온체인에 저장된 정확한 바이트가 다를 수 있습니다.

초기화 코드가 런타임 코드보다 큰 이유

초기화 코드는 최종 프로그램을 조립하는 데 필요한 재료를 모두 담아야 해요. 생성자에서만 쓰는 분기, 메모리로 코드를 복사하는 루틴, 인코딩된 생성자 인자, 라이브러리 링크 자리표시자, 초기 상태를 만드는 데이터가 포함될 수 있습니다. 그중 상당 부분은 생성이 끝나면 사라져요.

조립식 가구를 떠올려 보세요. 배송 상자에는 설명서, 완충재, 조립 도구가 함께 들어 있지만 완성된 책상은 포장 전체보다 단순합니다. 마찬가지로 설정 로직이 많은 컨트랙트는 초기화 코드는 크고 런타임 코드는 작을 수 있어요.

반대의 추측도 위험합니다. 생성자 코드가 짧다고 배포 비용까지 낮은 것은 아니에요. 런타임 코드 자체를 저장하는 데 가스가 들고, 생성자의 스토리지 쓰기나 외부 호출이 영수증 가스의 큰 부분을 차지할 수 있습니다. 전체 수수료 구조는 이더리움 가스비 작동 원리에서 이어서 볼 수 있어요.

서로 다른 두 가지 프로토콜 크기 제한

이더리움은 임시 프로그램과 그 프로그램이 반환하는 코드에 별도의 제한을 적용합니다.

대상프로토콜 제한초과할 때
초기화 코드49,152바이트너무 큰 생성 트랜잭션은 유효하지 않으며, CREATE·CREATE2 입력이 너무 크면 예외 중단
런타임 바이트코드24,576바이트초기화 코드가 최대 크기보다 큰 코드를 반환하면 컨트랙트 생성 실패

EIP-3860은 최대 초기화 코드 크기(MAX_INITCODE_SIZE)를 런타임 코드 제한의 두 배로 정하고, 시작된 32바이트 워드마다 2가스를 부과합니다. 클라이언트가 점프 목적지(Jump Destination)를 분석하는 작업 등에 비용을 매기고 생성 코드에 명시적인 상한을 둔 규칙이에요.

EIP-170은 런타임 코드의 24,576바이트 상한을 정의합니다. 이는 저장하려고 반환한 바이트의 크기예요. Solidity 소스 전체 길이도 아니고 16진수 문자열의 글자 수도 아닙니다. 한 바이트는 16진수 두 글자로 표현되며 도구에 따라 0x 접두사가 붙을 수도 있어요.

두 상한은 합의 규칙이지 권장 목표가 아닙니다. 경계에 가까운 컨트랙트는 감사와 배포가 어려워질 수 있고, 기능 추가나 컴파일러 설정 변경만으로 제한을 넘을 수 있어요. 실제 사용할 컴파일러 설정으로 나온 결과물을 바이트 단위로 측정하세요.

CREATE2에서 이 구분이 중요한 이유

CREATE2는 미래 주소를 계산할 때 런타임 코드만 보는 것이 아니라 keccak256(init_code)를 입력으로 사용합니다. 생성 바이트코드 뒤에 붙은 생성자 인자도 주소에 영향을 줘요. 컴파일러 버전, 최적화 설정, 연결된 라이브러리 주소, 메타데이터가 달라지면 소스가 거의 같아 보여도 생성 코드와 주소가 달라질 수 있습니다.

런타임 코드가 같아도 잘못된 주소 예측을 구해주지는 못해요. 서로 다른 초기화 코드가 똑같은 런타임 코드를 반환할 수 있지만, 초기화 코드 해시가 다르므로 CREATE2 주소는 달라집니다. 반대로 생성자는 외부 상태를 관찰할 수 있으므로 고정된 배포 레시피라도 입력과 최종 상태를 모두 검증해야 해요.

검증과 디버깅에서 자주 생기는 착각

서로 다른 결과물을 비교해요

블록 탐색기는 컨트랙트 주소에 저장된 코드를 읽으므로 기본 비교 대상은 런타임 바이트코드입니다. eth_getCode 결과를 배포 트랜잭션의 전체 입력과 비교하면 맞지 않아요. 입력에는 초기화 코드와 생성자 인자가 함께 들어 있기 때문입니다.

컴파일러 메타데이터를 무시해요

Solidity는 기본적으로 런타임 바이트코드 뒤에 CBOR 인코딩 정보를 붙입니다. 컨트랙트 메타데이터 공식 문서는 소스나 컴파일 설정이 바뀌면 메타데이터 해시와 최종 바이트도 달라질 수 있다고 설명해요. 설명되지 않는 뒤쪽 바이트를 지워 억지로 일치시키지 마세요. 컴파일러 버전, 최적화 횟수, EVM 대상, 라이브러리, 소스, 메타데이터 설정을 맞춰야 합니다.

불변값과 라이브러리를 빠뜨려요

불변값은 생성 중 런타임 코드에 직접 패치될 수 있어요. 연결된 라이브러리 주소도 바이트코드를 바꿉니다. 컴파일러의 불변값 참조와 링크 참조 메타데이터를 이용해 차이를 설명한 뒤, 링크와 배포 값이 반영된 결과를 비교하세요.

프록시의 코드만 보고 끝내요

프록시 주소의 런타임 바이트코드는 애플리케이션 전체가 아니라 작은 위임 프로그램일 수 있어요. 스토리지는 프록시에 남지만 로직은 다른 주소를 통해 실행됩니다. 어떤 소스가 실제 동작을 지배한다고 말하기 전에 프록시 컨트랙트와 업그레이드 권한을 따로 추적하세요.

리스크와 한계

생성 과정도 실행 가능한 동작이에요. 생성자는 신뢰하지 않는 컨트랙트를 호출하고 의존성을 만들거나 외부 상태를 가정할 수 있습니다. 외부 호출 가능한 런타임 함수만큼 생성 경로도 주의 깊게 검토해야 해요.

트랜잭션 성공이 의도한 코드의 증거는 아니에요. 체인, 주소, 런타임 코드 해시, 생성자 인자, 초기 스토리지, 소유권, 이벤트를 확인하세요. 잘못된 설정을 오류 없이 배포해도 결과는 잘못된 것입니다.

실패한 배포에도 가스가 들 수 있어요. 되돌림(Revert)과 가스 부족(Out of Gas)은 컨트랙트 생성을 취소하지만, 네트워크가 수행한 계산 비용은 발신자가 부담합니다. 먼저 시뮬레이션하고 현실적인 가스 여유를 두세요.

소스 검증이 보장하는 범위는 제한적이에요. 소스와 컴파일 설정이 런타임 바이트에 일치하면 재현 가능성에 도움이 됩니다. 하지만 소스가 안전하거나 생성자가 올바른 값을 넣었거나 프록시 구현이 바뀌지 않는다는 뜻은 아니에요.

팩토리는 신뢰 범위를 넓혀요. 팩토리는 어떤 초기화 코드를 공급할지 정하고 권한이나 초기화 로직을 추가할 수 있습니다. 자식 컨트랙트만 보지 말고 팩토리, 솔트 처리, 배포 이벤트도 감사하세요.

배포 전 체크리스트

  • 컴파일러 버전, 최적화 설정, EVM 대상, 메타데이터 설정, 연결 라이브러리를 고정했나요?
  • 생성 바이트코드, 생성자 인자, 전체 초기화 코드, 런타임 바이트코드를 구분했나요?
  • 초기화 코드와 예상 런타임 코드를 16진수 글자 수가 아닌 바이트로 측정했나요?
  • 의도한 체인 또는 충실한 포크에서 생성자 호출과 실패 경로를 시뮬레이션했나요?
  • CREATE2라면 정확한 초기화 코드를 해시하고 목적지 주소를 독립적으로 다시 계산했나요?
  • 배포 후 eth_getCode와 해당 배포에 맞춘 런타임 결과물을 비교했나요?
  • 불변값, 초기 스토리지, 소유자·관리자 권한, 외부 의존성을 확인했나요?
  • 프록시라면 구현 주소와 업그레이드 권한을 별도로 확인했나요?

자주 묻는 질문 (FAQ)

초기화 코드는 이더리움에 영원히 저장되나요?

새 계정의 실행 코드로 저장되지는 않아요. 배포 트랜잭션은 과거 체인 데이터에 남을 수 있지만, 초기화 코드가 성공적으로 반환한 런타임 바이트만 컨트랙트 계정의 코드가 됩니다.

생성자 인자는 런타임 바이트코드의 일부인가요?

자동으로 포함되지는 않습니다. 생성자 인자는 생성 페이로드 뒤에 붙어요. 그 효과가 스토리지, 불변값, 로그에 남을지 또는 아무것도 남지 않을지는 생성자 로직이 결정합니다.

eth_getCode가 컴파일러 바이트코드와 왜 다른가요?

런타임 코드와 생성 코드를 비교했을 수 있어요. 컴파일러 설정, 라이브러리 주소, 메타데이터, 불변값이 달라도 결과가 달라집니다. 프록시라면 구현 컨트랙트라는 간접 계층도 살펴야 해요.

런타임 코드 크기만 알면 전체 배포 가스를 계산할 수 있나요?

아니요. 코드 저장 비용은 한 부분일 뿐입니다. 초기화 코드 실행, 생성자의 스토리지 쓰기, 트랜잭션 데이터, 메모리 확장, 외부 호출, 초기화 코드 워드 비용이 모두 영향을 줄 수 있어요.

서로 다른 주소의 컨트랙트가 같은 런타임 코드를 가질 수 있나요?

네. 주소와 코드는 별개의 속성이에요. 발신자, 논스, 솔트, 초기화 코드가 달라 주소가 달라도 최종 런타임 프로그램은 같을 수 있습니다.

배포를 파일 복사가 아닌 변환으로 보세요

핵심은 파일 복사가 아니라 변환입니다. 이더리움은 초기화 코드를 한 번 실행하고, 출력으로 런타임 바이트코드를 받은 뒤, 저장 비용을 부과하고, 성공할 때만 계정 상태를 확정해요. 각 단계를 구분하면 가스 추정, CREATE2 주소 계산, 블록 탐색기 소스 검증, 생성자 감사가 훨씬 명확해집니다.

이 글은 교육 목적이며 투자 조언이 아닙니다. 스마트 컨트랙트 거래는 되돌릴 수 없고 크립토 자산의 변동성은 커요. 잃어도 되는 범위에서 테스트하고, 최신 공식 문서를 직접 확인하며, 스스로 조사하세요(DYOR).

주요 출처

광고

함께 읽으면 좋은 글

관련 주제 살펴보기

GOMTU의 다른 글