---
title: "Solana Account Archive: 모든 슬롯에서 계정 상태 조회하기"
description: "1년치 Solana를 리플레이하여 구축한, 삭제(pruning)되지 않는 아카이브로 모든 Solana 계정의 과거 슬롯 상태를 getAccountInfo로 조회합니다."
---

# Solana Account Archive: 모든 슬롯에서 계정 상태 조회하기

<ImageBlock
  src="https://media.alchemy.com/solana-account-archive.png"
  alt="Solana Account Archive: 모든 슬롯에서 계정 상태 조회"
  width={1920}
  height={900}
  priority
/>

Account Archive는 모든 Solana 계정에 대해 임의의 과거 slot 시점의 `getAccountInfo`를 답합니다(vote 계정과 slot별 sysvar는 예외). 과거 데이터의 범위는 깊고 계속 넓어지며, 절대 pruning되지 않습니다. 특정 시점의 상태를 읽으려면 `slot` 파라미터를 사용하고, 계정의 업데이트 이력을 따라가려면 `lastUpdateBeforeSlot` / `firstUpdateAfterSlot`를 사용하십시오. 읽기 요청은 마이크로초 단위의 중앙값 지연 시간으로 처리됩니다.

## Solana는 현재만 기억한다

경험 많은 개발자도 놀라는 Solana의 특징이 하나 있습니다: validator는 각 계정에 대해 정확히 한 개의 복사본, 즉 최신 상태만 저장합니다. 모든 쓰기는 이전 상태를 그 자리에서 덮어쓰므로, 계정이 변경되는 순간 이전 모습은 사라집니다. 노드가 해당 slot의 전체 블록을 여전히 가지고 있더라도, 그 slot 시점의 계정 상태는 어디에도 더 이상 존재하지 않습니다.

이것은 버그가 아니라 설계된 특징입니다. Solana가 빠른 이유 중 하나이기도 합니다. 하지만 이로 인해 다음과 같은 지극히 합당한 질문들에 답할 수 없게 됩니다:

- **디버깅**: "우리의 청산이 slot 285,401,337에서 실행되었다. 그 정확한 slot에서 오라클 가격 계정은 어떤 값이었나?"
- **백테스팅**: "지난 6개월 동안 매 시점의 이 풀의 reserve를 재구성하라."
- **감사**: "3월 3일에 이 토큰 계정이 무엇을 보유하고 있었는지 증명하라."
- **복구**: "우리 인덱서가 4시간 동안 다운되었다. 우리가 놓친 상태 전이를 정확히 재구성하라."

`getBlock`가 이를 커버한다고 생각할 수 있지만, 그렇지 않습니다. 이는 과거 slot의 트랜잭션과 그 전/후 잔액을 반환할 뿐, 계정 데이터는 반환하지 않습니다. 대부분의 애플리케이션에서 중요한 상태는 데이터 바이트, 즉 오더북, 가격 피드, 포지션, 또는 설정값에 있습니다.

Solana Account Archive는 전체 이력을 다시 가져옵니다. 2025년 7월 이후 임의의 slot에서 임의의 계정에 대해 `getAccountInfo`를 답하며, 여러분이 이미 사용하는 것과 동일한 요청/응답 형태를 사용합니다. 두 가지가 이를 차별화합니다. 첫째, archive는 절대 pruning되지 않으므로 조회 가능한 범위는 시간이 지날수록 계속 늘어납니다. 둘째, 체인 자체의 slot별 부기 항목(vote 계정과 매 slot마다 재작성되는 일부 sysvar)은 건너뜁니다. 이는 애플리케이션 개발자에게는 유용한 이력이 아니라 노이즈이기 때문입니다.

## API: getAccountInfo의 확장

우리는 의도적으로 새로운 메서드를 만들지 않았습니다. archive는 표준 JSON-RPC를 사용하며 `getAccountInfo`를 구현합니다. 여러분이 이미 알고 있는 모든 설정 옵션들—`encoding` (`base64`, `base58`, `base64+zstd`, `jsonParsed`), `commitment`, `dataSlice`, `minContextSlot`—을 그대로 지원하며, 여기에 상호 배타적인 세 가지 새로운 파라미터가 추가됩니다:

<EmbeddedTable
  table={{
    columns: [
      {
        key: "parameter",
        width: 200,
        title: "Parameter",
        dataType: "object",
      },
      {
        key: "semantics",
        width: 320,
        title: "Semantics",
        dataType: "object",
      },
      {
        key: "useItFor",
        width: 220,
        title: "Use it for",
        dataType: "object",
      },
    ],
    data: [
      {
        parameter: { title: "<code>slot</code>", tooltip: "", icon: "" },
        semantics: {
          title:
            "State as of slot <code>S</code> (inclusive: the latest write with <code>slot &lt;= S</code>)",
          tooltip: "",
          icon: "",
        },
        useItFor: { title: "Point-in-time snapshots", tooltip: "", icon: "" },
        id: 0,
      },
      {
        parameter: {
          title: "<code>lastUpdateBeforeSlot</code>",
          tooltip: "",
          icon: "",
        },
        semantics: {
          title: "The most recent write strictly before <code>S</code>",
          tooltip: "",
          icon: "",
        },
        useItFor: {
          title: "Walking an account's history backward",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        parameter: {
          title: "<code>firstUpdateAfterSlot</code>",
          tooltip: "",
          icon: "",
        },
        semantics: {
          title: "The first write strictly after <code>S</code>",
          tooltip: "",
          icon: "",
        },
        useItFor: {
          title: "Walking an account's history forward",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
    ],
  }}
/>

이 세 파라미터는 `minContextSlot`와도 상호 배타적이며, 함께 사용하면 요청이 거부됩니다. 셋 다 생략하면 일반적인 최신 상태 `getAccountInfo`를 얻습니다. archive는 drop-in superset입니다. 다음은 완전한 point-in-time 읽기 예시로, 토큰 계정의 slot 400,000,000 시점 상태입니다:

<CodeSnippet
  language="bash"
  code={`curl $ARCHIVE_URL -X POST -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getAccountInfo",
  "params": [
    "3emsAVdmGKERbHjmGfQ6oZ1e35dkf5iYcS6U4CPKFVaa",
    { "encoding": "jsonParsed", "slot": 400000000 }
  ]
}'`}
/>

응답은 표준 `getAccountInfo` 형태입니다:

<CodeSnippet
  language="json"
  code={`{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "context": { "apiVersion": "4.1.0", "slot": 400000000 },
    "value": {
      "lamports": 2040821,
      "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      "data": {
        "program": "spl-token",
        "parsed": {
          "info": {
            "isNative": false,
            "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
            "owner": "7VHUFJHWu2CuExkJcJrzhQPJ2oygupTWkL2A2For4BmE",
            "state": "initialized",
            "tokenAmount": {
              "amount": "1056153754430274",
              "decimals": 6,
              "uiAmount": 1056153754.430274,
              "uiAmountString": "1056153754.430274"
            }
          },
          "type": "account"
        },
        "space": 165
      },
      "executable": false,
      "rentEpoch": 18446744073709551615,
      "space": 165
    }
  }
}`}
/>

여기서 의미를 주의하십시오: `slot: S`는 `S` 시점의 상태를 의미합니다. 계정이 slot `S - 40,000`에 마지막으로 쓰여진 후 변경되지 않았다면, 여러분은 그 쓰기 결과를 받게 됩니다. 이는 slot `S`에서 실행되는 어떤 프로그램이라도 보았을 값과 정확히 동일합니다. 그리고 `jsonParsed`는 과거 상태에도 동작합니다: 표준 RPC 노드가 사용하는 것과 동일한 디코더가 과거 바이트에 적용되므로, 커버리지 초기의 토큰 계정도 디코딩된 형태로 반환되며 opaque하게 나오지 않습니다.

### 계정 이력에 대한 iterator

`lastUpdateBeforeSlot`와 `firstUpdateAfterSlot`는 archive를 계정의 모든 상태 전이에 대한 iterator로 만듭니다.

`lastUpdateBeforeSlot`는 `context.slot`에서 찾은 쓰기의 실제 slot을 반환하므로, 각 응답이 다음 요청의 cursor가 됩니다:

<CodeSnippet
  language="python"
  code={`# Walk every state transition of an account, newest to oldest.
cursor = current_slot + 1
while True:
    try:
        result = rpc("getAccountInfo", [pubkey, {"lastUpdateBeforeSlot": cursor}])
    except RpcError as e:
        if e.code == -32020:           # walked past the edge of coverage; done
            break
        raise
    slot = result["context"]["slot"]   # the slot this write landed at
    handle(slot, result["value"])      # value is null if this write closed the account
    cursor = slot                      # strictly-before: no overlap, no gaps`}
/>

이 walk에는 정확히 하나의 정지점이 있습니다: 에러 `-32020`이며, 이는 cursor가 archive 커버리지의 경계를 넘어섰다는 의미입니다. 도중에 `null` 값이 나오는 것은 `getAccountInfo`에서 늘 그렇듯이 계정이 그 slot 시점에 존재하지 않았다는 의미입니다. 여기서는 `context.slot`의 쓰기가 계정을 삭제했기 때문입니다. 그 slot을 다시 cursor로 넣으면 walk는 계정의 더 이전 이력을 계속 진행합니다.

`firstUpdateAfterSlot`은 반대로 동작합니다: 시간을 거슬러 올라가는 대신 앞으로 이동하며, 주어진 slot 바로 다음의 쓰기를 반환합니다. 이는 알려진 slot부터 따라잡으면서 최신 상태뿐 아니라 모든 중간 상태가 필요한 인덱서에 이상적입니다.

이 둘을 함께 사용하면 계정의 전체 생애주기를 따라갈 수 있습니다: 모든 잔액 변화, 모든 데이터 변경, 모든 소유권 변경을, 각각이 정확히 어느 slot에 발생했는지와 함께 말입니다.

## 발생하는 대로 새로운 쓰기 기록하기

앞으로의 상태를 기록하는 것은 상대적으로 쉬운 부분입니다. archive는 Geyser 스트림을 통해 네트워크를 청취하며 계정 쓰기가 발생하는 대로 기록합니다. 이때 기록의 신뢰성을 유지하는 두 가지 규칙이 있습니다:

finalize된 slot만 archive에 들어갑니다. Solana는 tip에서 지속적으로 fork가 발생하며, 몇 초 동안 실제처럼 보이던 블록이 그냥 사라질 수 있습니다. archive는 네트워크가 finalize할 때까지 각 slot의 업데이트를 보류하므로, 영구 기록에는 실제로 일어난 것만 담깁니다.

아무것도 조용히 유실되지 않습니다. 모든 Solana 블록은 자신의 부모 블록을 명시하므로, 스트림이 놓친 블록(일시적 오류, 재연결, 서비스 배포 등)은 즉시 감지되어 backfill됩니다.

그 아래의 저장 계층은 그 자체로 별도의 이야기입니다. 여기서 중요한 것은 결과입니다: point-in-time 읽기는 hot path에 놓일 만큼 충분히 빠르며, 마이크로초 단위의 중앙값 지연 시간을 갖습니다(내부 서비스 계층에서 측정).

## 지난 1년을 재구성하기

오늘부터 이후로 ingest하는 것만으로도 내년에는 유용한 archive를 얻을 수 있습니다. 우리는 지난 1년치도 원했는데, 이는 이력을 전혀 남기지 않는 체인에서 이미 발생한 수천만 개 slot의 계정 상태를 재구성하는 것을 의미했습니다. 그리고 이는 방대한 양의 이력입니다: Solana는 약 400ms마다 하나의 블록을 생성하며, 하루에 약 216,000개의 slot이 생깁니다. 1년이면 약 7,800만 slot, 수천억 건의 계정 업데이트, 그리고 1페타바이트가 넘는 원시 계정 데이터가 됩니다. 각 slot마다 전체 데이터 페이로드가 포함된 수천 건의 계정 쓰기가 발생합니다.

### "그냥 트랜잭션을 파싱하면 된다"가 작동하지 않는 이유

과거 블록을 가져와서 각 트랜잭션이 각 계정에 무엇을 했는지 디코딩하는 매력적인 지름길이 있습니다. 이는 근본적인 이유로 실패하며, 이는 새겨둘 가치가 있습니다: Solana 트랜잭션은 상태 변화에 대한 기술(description)이 아니라 프로그램 호출(invocation)입니다.

계정에 실제로 무슨 일이 일어나는지는 프로그램의 실행에 달려 있습니다: 다른 프로그램으로 확산되는 CPI, 중간에 읽히는 sysvar, compute metering, 그 slot 시점의 런타임의 정확한 semantics 등입니다. 블록은 트랜잭션이 어떤 계정을 건드렸는지와 잔액이 어떻게 이동했는지를 기록하지만, 실제로 여러분이 원하는 데이터 바이트는 오직 코드를 실행해야만 결정됩니다.

트랜잭션이 계정에 무엇을 했는지 정확히 아는 방법은 단 하나뿐입니다: 실제 런타임으로, 실제로 실행되었던 상태에 대해 실행하는 것입니다.

### Replay: 체인을 다시 실행하기

그래서 우리는 그렇게 합니다. backfill 파이프라인은 본질적으로 역사를 다시 사는 validator입니다:

1. 신뢰할 수 있는 스냅샷에서 부팅합니다. Solana Foundation은 과거 스냅샷의 공개 archive—특정 slot 시점의 모든 계정에 대한 완전한 캡처—를 유지합니다. slot `A`의 스냅샷을 로드하면 그 순간의 수억 개 계정에 대한 완전하고 정본(canonical)인 상태를 얻습니다.
2. 모든 블록을 앞으로 replay합니다. `A + 1`부터, 각 과거 블록은 실제 validator 런타임을 거쳐 실행됩니다: 시뮬레이션이 아니라 실제 실행이며, 로그 파싱이 아닙니다. 모든 트랜잭션이 실행되고, 런타임이 생성하는 계정 쓰기가 slot별로 캡처되고 인덱싱됩니다.
3. 다음 스냅샷에서 멈추고 이를 증명합니다. 이 단계가 전체를 신뢰할 수 있게 만드는 핵심입니다. replay가 다음 정본 스냅샷이 존재하는 slot `B`에 도달하면, 우리는 replay된 최종 상태를 그것과 비교합니다. 여기에는 accounts lattice hash도 포함됩니다—이는 네트워크 자체가 계산하고 합의하는, 전체 계정 집합에 대한 암호학적 커밋입니다. 그 범위 내에서 단 하나의 계정의 단 한 바이트라도 어긋났다면 해시가 일치하지 않아 해당 범위는 거부됩니다. 우리는 replay가 정확하다고 가정하지 않습니다. backfill된 모든 범위는 신뢰되기 전에 합의 진리(consensus ground truth)에 대해 검증됩니다.

### 오래된 블록은 그 시대의 런타임이 필요하다

replay를 진정으로 어렵게 만드는 지점이 여기 있습니다: Solana의 실행 semantics는 시간에 따라 버전이 관리됩니다. feature gate는 특정 epoch에서 활성화되고, compute budget 규칙이 바뀌며, syscall이 추가되고, edge case 동작이 수정됩니다. 12개월 전의 트랜잭션은 그 slot에서 클러스터가 실제로 동작했던 방식과 정확히 동일하게 동작하는 런타임으로 replay되어야 합니다. 오늘날의 validator로 이를 replay하면 3단계에서 명확하게, 한 번도 일어난 적 없는 역사를 만들어냈다고 알려줄 것입니다.

실제로 backfill fleet은 여러 개의 고정된(pinned) validator 계보를 운영하며, 각각은 자신이 충실하게 재현할 수 있는 시대를 담당합니다. 작업은 epoch 경계를 따라 나뉘어, 각 chunk가 검증 가능한 스냅샷에서 시작하고 끝나도록 합니다. archive를 더 과거로 확장하는 것은 대체로 올바른 런타임 시대를 구축하고 replay compute 비용을 지불하는 문제입니다. 그리고 각 epoch에 정렬된 chunk가 독립적이기 때문에, 이력은 완벽하게 병렬화됩니다. 1년치를 backfill하는 것은 대체로 얼마나 많은 replay worker를 동시에 운영하느냐의 문제입니다.

## 지속적으로 검증되는 신뢰

해시로 검증된 backfill은 과거를 다루지만, 별개의 문제는 serving path가 운영 환경에서 계속 정직하게 유지되는가입니다. 그래서 독립적인 watchdog이 archive의 답변을 라이브 RPC 노드와 지속적으로 교차 검증합니다. 여러분이 사용할 것과 동일한 공개 API를 통해서 말입니다. 정확성은 우리에게 가장 중요하며, 그래서 서비스가 실행되는 매 분마다 재검증됩니다. 다층 접근 방식: backfill된 이력에 대한 합의 기반 해시, tip에서의 finalize 게이팅된 쓰기, 그리고 그 위에 지속적인 라이브 교차 검증까지—이 모든 것이 시간이 흘러도 신뢰할 수 있는 진실의 기록을 보장합니다.

## 이것으로 무엇을 만들 수 있는가

이 글 서두의 모든 질문은 이제 API 호출 하나로 해결됩니다:

- "그 정확한 slot에서 오라클 계정은 무엇이었나?" `slot`를 설정한 `getAccountInfo` 호출 한 번이면 됩니다. 실패한 트랜잭션이 읽었던 모든 계정의 정확한 상태를, 그것이 실행된 slot 시점 그대로 가져올 수 있습니다. 더 이상 스크린샷과 추측으로 오라클 입력값을 재구성할 필요가 없습니다.
- "이 풀의 reserve를 6개월치 재구성하라." 고정된 slot 간격으로 풀 계정을 샘플링하고, 거래 이벤트로부터 짜맞춘 근사치가 아니라 실제로 체인에 있었던 것에 대해 백테스트하십시오.
- "3월 3일에 이 토큰 계정이 무엇을 보유했는지 증명하라." "slot S 시점에 계정 X는 정확히 이 바이트를 담고 있었다." 이는 답할 수 있으며, 네트워크 자체가 합의한 해시에 anchor되어 있습니다.
- "우리 인덱서가 놓친 상태 전이를 재구성하라." 마지막으로 알려진 slot부터 `firstUpdateAfterSlot`로 앞으로 페이징하면 모든 중간 상태를 순서대로 받을 수 있습니다.

그리고 서두에서 묻지 않았던 한 가지: 인덱서를 전혀 운영하지 않고도 얻는 잔액 및 포지션 이력입니다. `lastUpdateBeforeSlot`로 토큰 계정을 뒤로 따라가면 완전한 타임라인을 얻을 수 있습니다. 과거 slot 시점의 지갑 전체 토큰 보유 현황에 대해서는 [historical Solana token balances](https://www.alchemy.com/blog/historical-solana-token-balances)도 참고하십시오.

## 시작하기

Solana는 validator를 가볍게 유지하고 현재 상태에 집중시킴으로써 빠른 속도를 유지합니다. 과거 계정 상태는 단순히 다른 곳에 존재해왔으며, 지금까지 생태계는 이를 우회해서 대응해왔습니다. Solana Account Archive는 이를 바꿉니다. finalize 게이팅된 라이브 ingestion과, 시대에 충실한 런타임을 통해 이력을 다시 실행하고 결과를 합의 해시에 대해 증명하는 backfill 파이프라인을 통해, 과거 `getAccountInfo`는 그저 또 하나의 RPC 호출이 됩니다: 동일한 메서드, 동일한 응답 형태, 파라미터 하나만 추가될 뿐입니다.

후속 글에서는 저장 계층을 다룰 예정입니다: 1년치 Solana 계정 쓰기가 디스크에 어떻게 구성되어야 새로운 업데이트를 ingest하는 것과 point-in-time 쿼리에 답하는 것 모두 빠르게 유지되는지 살펴보겠습니다. archive가 인덱싱하는 모든 것은 압축되고, 중복성을 위해 최소 3개의 복제본으로 저장되며, 절대 pruning되지 않으므로 조회 가능한 범위는 계속 늘어날 뿐입니다.

[Get started on Solana](https://www.alchemy.com/docs/reference/solana-api-quickstart), 그리고 더 깊은 이력이 필요하거나 대규모에서의 특화된 솔루션과 가격을 알아보고 싶다면 [저희에게 연락하세요](https://www.alchemy.com/contact-sales).

## FAQ

### 특정 slot에서 Solana 계정의 상태를 어떻게 얻나요?

Account Archive에서 계정의 pubkey와 config 객체 내의 `slot` 파라미터를 사용하여 `getAccountInfo`를 호출하세요: `getAccountInfo(pubkey, { "slot": S })`. 응답은 표준 `getAccountInfo` 형태이며, slot `S` 시점의 계정 상태, 즉 `S` 시점 또는 그 이전의 가장 최근 쓰기를 반환합니다.

### Solana 계정의 전체 업데이트 이력을 어떻게 얻나요?

cursor 파라미터로 페이징하세요. `getAccountInfo(pubkey, { "lastUpdateBeforeSlot": S })`는 `S` 바로 이전의 가장 최근 쓰기를 반환하며, 그 쓰기의 실제 slot은 `context.slot`에 담깁니다. 그 slot을 다음 cursor로 다시 넣으면 모든 상태 전이를 거슬러 따라갈 수 있습니다. `firstUpdateAfterSlot`는 같은 방식으로 앞으로 따라갑니다.

### slot, lastUpdateBeforeSlot, firstUpdateAfterSlot의 차이는 무엇인가요?

`slot`는 point-in-time 읽기입니다: slot `S` 시점(포함)의 상태입니다. `lastUpdateBeforeSlot`와 `firstUpdateAfterSlot`은 exclusive한 이력 cursor입니다: `S` 바로 이전 또는 바로 이후의 가장 가까운 쓰기입니다. 이 셋은 상호 배타적이며, 모두 생략하면 일반적인 최신 상태 읽기가 됩니다.

### jsonParsed 인코딩이 과거 계정 상태에도 작동하나요?

예. 표준 Solana RPC 노드가 사용하는 것과 동일한 계정 디코더가 과거 바이트에 적용되므로, 토큰 계정, mint, 그리고 기타 알려진 프로그램 계정은 커버리지 내 어떤 slot에서도 파싱된 형태로 반환됩니다. `base64`, `base58`, `base64+zstd`, `jsonParsed`을 포함한 모든 표준 인코딩이 작동하며, `dataSlice`도 지원됩니다.

### 얼마나 과거까지 조회할 수 있나요?

현재 커버리지는 2025년 7월까지 확장되어 있으며, backfill을 통해 시간이 지남에 따라 더 과거로 확장됩니다. 이력은 절대 pruning되지 않으므로, 한 번 인덱싱된 모든 것은 계속 조회 가능하며 범위는 늘어나기만 합니다.

### 과거 데이터가 정확하다는 것은 어떻게 알 수 있나요?

backfill된 범위는 해당 시대의 validator 런타임을 통해 모든 트랜잭션을 재실행하여 생성되며, 결과 상태는 네트워크 자체의 합의 산출물—정본 스냅샷의 accounts lattice hash를 포함하여—에 대해 검증됩니다. 라이브 ingestion은 finalize된 slot만 커밋하며, 독립적인 watchdog이 서빙되는 결과를 라이브 RPC 노드와 지속적으로 교차 검증합니다.

### 모든 계정이 커버되나요?

순수하게 slot별 체인 부기 항목인 것을 제외한 모든 계정이 커버됩니다: vote 계정과 매 slot마다 재작성되는 세 가지 sysvar(`SlotHashes`, `SlotHistory`, `RecentBlockhashes`)는 인덱스에서 제외됩니다. 프로그램 계정, 토큰 계정, mint, PDA, 지갑, 그리고 나머지 sysvar(Clock, Rent 등)는 모두 커버됩니다.

### 새로운 SDK나 클라이언트가 필요한가요?

아니요. 어떤 Solana JSON-RPC 클라이언트도 작동합니다. archive는 표준 `getAccountInfo`를 구현하며, 과거 데이터 관련 파라미터는 기존 config 객체 안의 추가 필드일 뿐입니다.
