
Solana 프로그래밍 모델: Solana 개발 입문
이 글에서 다루는 내용
Solana의 탈중앙화 컴퓨팅 접근 방식은 단순한 원칙에 기반합니다. 모든 것은 계정이라는 자체 메모리 영역에 저장됩니다. Solana는 공개 키가 해당 계정의 고유 식별자 역할을 하는 글로벌 키/값 저장소로 작동합니다. 계정은 상태를 저장하므로 Solana의 핵심 기반입니다. 프로그램부터 토큰 잔액까지 모든 것을 보관합니다. 트랜잭션은 계정을 업데이트하고 상태 변경을 반영하는 데 사용됩니다.
이 글에서는 Solana 아키텍처의 복잡성을 살펴봅니다. 먼저 클러스터와 상태의 개념을 개괄한 뒤, Solana의 기반 요소인 계정과 프로그램의 역할을 설명합니다. 이어서 트랜잭션이 계정과 프로그램 간의 동적 상호작용을 지원하는 방식을 알아봅니다.
이 글을 마치면 Solana의 프로그래밍 모델을 깊이 이해하게 됩니다. 클러스터 아키텍처, 데이터 저장에서 계정이 맡는 핵심 역할, 트랜잭션이 계정 데이터를 업데이트하는 과정을 익힐 수 있습니다. 또한 임대료 시스템과 버전이 지정된 트랜잭션 등 Solana만의 기능도 살펴봅니다.
Solana 클러스터란 무엇인가요?
Solana 아키텍처의 중심에는 클러스터가 있습니다. 클러스터는 트랜잭션을 처리하고 하나의 원장을 유지하기 위해 협력하는 검증인 집합입니다. Solana에는 각각 특정 목적을 담당하는 여러 독립 클러스터가 있습니다.
- Localhost: 기본 포트 8899에서 실행되는 로컬 개발 클러스터입니다. Solana Command-line Interface (CLI)에는 에어드롭이나 속도 제한 없이 개발자 요구에 맞게 구성할 수 있는 테스트 검증인이 내장되어 있습니다
- Devnet: Solana에서 부담 없이 테스트하고 실험할 수 있는 샌드박스 환경입니다
- Testnet: Solana 핵심 기여자가 새로운 업데이트와 기능을 메인넷에 적용하기 전에 시험하는 환경입니다. 성능 테스트를 실행하려는 개발자도 사용합니다
- Mainnet Beta: 실제 트랜잭션이 발생하는 라이브 무허가형 클러스터입니다. 사용자, 개발자, 토큰 보유자, 검증인이 매일 상호작용하는 “진짜” Solana입니다
각 클러스터는 서로를 전혀 인식하지 못한 채 독립적으로 작동합니다. 각 운영 환경의 무결성을 보장하기 위해 잘못된 클러스터로 전송된 트랜잭션은 거부됩니다.
클러스터를 하나의 거대한 데이터 힙으로 생각해 보세요. 컴퓨터 과학에서 힙은 데이터를 동적으로 저장하고 수정할 수 있는 메모리 영역을 뜻합니다. 다만 클러스터가 실제로 힙 자료 구조를 사용하는 것은 아닙니다. 이 비유는 클러스터가 필요할 때 할당하고 해제할 수 있는 여러 메모리 영역으로 구성된다는 점을 이해하기 위한 개념적 도구입니다. 클러스터를 동적 힙으로 이해하면 네트워크 안에서 데이터가 관리되고 접근되며 보호되는 방식을 파악할 수 있습니다.
이 거대한 데이터 힙을 일종의 디지털 창고로 볼 수도 있습니다. 여기서 데이터는 선반 위 상자와 같습니다. 각 상자에는 고유한 라벨과 이동 및 내용 변경에 관한 구체적인 규칙이 있습니다. 따라서 허가된 이동이나 변경만 허용되는 안전하고 체계적인 시스템이 유지됩니다.
Solana에서 프로그램이라 부르는 스마트 컨트랙트에는 직접 관리할 수 있는 창고 또는 힙의 일부가 할당됩니다. 프로그램은 창고의 어느 공간에서든 데이터를 읽을 수 있지만, 소유하지 않은 공간의 내용을 변경하려면 특정 권한이 필요합니다. 보편적으로 허용되는 유일한 작업은 Solana의 네이티브 암호화폐인 lamport를 창고 내 어느 공간으로든 전송하는 것입니다.
프로그램을 포함한 모든 상태는 이 힙에 존재합니다. 각 영역에는 이를 소유하고 관리하는 프로그램이 있습니다. 예를 들어 프로그램은 온체인 프로그램의 로드, 배포, 업그레이드를 담당하는 프로그램인 BPFLoader가 소유합니다. 이러한 메모리 영역, 즉 디지털 창고의 상자를 계정이라고 합니다.
계정이란 무엇인가요?
Solana의 모든 것은 계정입니다. 계정은 컴퓨터의 파일처럼 데이터를 지속해서 보관하는 컨테이너입니다. 계정은 상태, 즉 계정 잔액, 소유권 정보, 프로그램 보유 여부, 임대료 정보를 저장하는 Solana 프로그램 모델의 구성 요소입니다.
Solana에는 세 가지 유형의 계정이 있습니다.
- 데이터를 저장하는 계정
- 실행 가능한 프로그램을 저장하는 계정
- 네이티브 프로그램을 저장하는 계정
기능에 따라 다음과 같이 더 세분화할 수 있습니다.
- 실행 가능 계정 - 코드를 실행할 수 있는 계정
- 실행 불가능 계정 - 코드 실행 기능 없이 데이터를 저장하는 계정입니다. 코드를 보유하지 않기 때문입니다
위 그림에는 실행 가능 계정과 실행 불가능 계정의 몇 가지 예가 나와 있습니다. 실행 가능 계정 중 Bubblegum은 프로그램 계정의 예입니다. 압축 NFT를 생성하고 관리하는 데 사용되는 Metaplex의 프로그램입니다. Vote Program은 네이티브 프로그램 계정의 예입니다. 검증인의 투표 상태와 보상을 추적하는 계정을 생성하고 관리하는 데 사용됩니다. 프로그램 계정과 네이티브 프로그램 계정의 차이는 프로그램이란 무엇인가요? 섹션에서 설명합니다. 지금은 Solana에 여러 유형의 실행 가능 계정이 있다는 점만 알아두면 됩니다.
또한 모든 실행 불가능 계정은 데이터 계정으로 분류할 수 있습니다. 데이터 계정의 예는 다음과 같습니다.
- Associated Token Account - 특정 토큰, 잔액, 소유자에 관한 정보를 보유하는 계정입니다. 예: Alice가 10 USDC를 보유
- System Account - System Program이 생성하고 소유하는 계정입니다
- Stake Account - 보상을 받을 가능성을 위해 검증인에게 토큰을 위임하는 데 사용되는 계정입니다
계정 구조
계정은 AccountInfo 구조체에 따라 구성됩니다.
pub struct AccountInfo<'a> {
pub key: &'a Pubkey,
pub lamports: Rc>,
pub data: Rc>,
pub owner: &'a Pubkey,
pub rent_epoch: Epoch,
pub is_signer: bool,
pub is_writable: bool,
pub executable: bool,
}계정은 고유한 32바이트 공개 키인 주소(key)로 식별됩니다.
lamports 필드에는 이 계정이 소유한 lamport의 수가 저장됩니다. 1 lamport는 Solana의 네이티브 토큰인 SOL의 10억분의 1입니다.
data는 이 계정에 저장된 원시 데이터 바이트 배열을 뜻합니다. 디지털 자산의 메타데이터부터 토큰 잔액까지 무엇이든 저장할 수 있으며 프로그램이 수정할 수 있습니다.
owner 필드에는 프로그램 계정의 주소로 표시되는 이 계정의 소유자가 저장됩니다. 계정 소유권에는 몇 가지 규칙이 있습니다.
- 계정 소유자만 계정 데이터를 변경하고 lamport를 인출할 수 있습니다
- 누구나 계정에 lamport를 입금할 수 있습니다
- 계정 데이터가 0으로 초기화된 경우 계정 소유자는 새 소유자에게 소유권을 이전할 수 있습니다
is_signer 필드는 해당 계정의 소유자가 트랜잭션에 서명했는지를 나타내는 불리언 값입니다. 즉, 트랜잭션에 관여하는 프로그램에 해당 계정이 서명자인지를 알려줍니다. 서명자라는 것은 계정이 공개 키에 대응하는 비공개 키를 보유하고 있으며 제안된 트랜잭션을 승인할 권한이 있다는 뜻입니다.
is_writable 필드는 계정 데이터를 수정할 수 있는지를 나타내는 불리언 값입니다. Solana에서는 병렬 처리를 위해 트랜잭션이 계정을 읽기 전용으로 지정할 수 있습니다. 런타임은 여러 프로그램이 읽기 전용 계정에 동시에 접근하도록 허용하며, 쓰기 가능 계정에서 발생할 수 있는 쓰기 충돌은 트랜잭션 처리 순서로 관리합니다. 따라서 충돌하지 않는 트랜잭션만 병렬로 처리됩니다.
executable 필드는 계정이 명령을 처리할 수 있는지를 나타내는 불리언 값입니다. 그렇습니다. 프로그램도 계정에 저장됩니다. 다음 섹션에서 자세히 알아보겠습니다. 먼저 임대료 개념을 살펴봐야 합니다.
rent_epoch 필드는 이 계정에 다음 임대료가 부과될 에포크를 나타냅니다. 에포크는 리더 일정이 유효한 슬롯 수입니다. 운영체제의 일반적인 파일과 달리 Solana 계정의 수명은 lamport 수량으로 표현됩니다. 계정의 지속 여부가 lamport 잔액에 달려 있다는 개념에서 임대료가 등장합니다.
임대료
임대료는 Solana에서 계정을 유지하고 검증인 메모리에 보관하기 위해 발생하는 저장 비용입니다. 임대료 징수는 리더 일정이 유효한 슬롯으로 정의되는 시간 단위인 에포크를 기준으로 산정됩니다. 임대료는 다음과 같이 작동합니다.
- 임대료 징수 - 임대료는 에포크마다 한 번 징수됩니다. 트랜잭션에서 계정을 참조할 때도 징수될 수 있습니다
- 임대료 분배 - 징수된 임대료 일부는 소각되어 유통량에서 영구적으로 제거됩니다. 나머지는 각 슬롯이 끝난 후 투표 계정에 분배됩니다
- 임대료 납부 - 계정에 임대료를 낼 만큼 충분한 lamport가 없으면 가비지 컬렉션이라는 과정을 통해 데이터가 삭제되고 계정 할당이 해제됩니다
- 임대료 면제 - 2년 치 임대료에 해당하는 최소 잔액을 유지하면 계정의 임대료가 면제됩니다. 모든 새 계정은 계정 크기에 따라 정해지는 임대료 면제 기준을 충족해야 합니다
- 임대료 회수 - 사용자는 계정을 닫고 남은 lamport를 회수할 수 있습니다. 이를 통해 계정에 저장된 임대료를 돌려받을 수 있습니다
특정 계정 크기의 임대료는 getMinimumBalanceForRentExemption RPC 엔드포인트로 추정할 수 있습니다. Test Drive는 계정의 데이터 길이를 usize로 입력받아 이 과정을 간소화합니다. Solana rent CLI 하위 명령어를 사용해 계정의 임대료 면제에 필요한 최소 SOL도 추정할 수 있습니다. 예를 들어 이 글을 작성하는 시점에 solana rent 20000 명령을 실행하면 Rent-exempt minimum: 0.14009088 SOL이 반환됩니다.
Solana의 주소
Solana에는 실제로 두 가지 “유형”의 주소가 있습니다. Solana는 주소 생성에 SHA-512 (SHA-2)와 Curve22519 타원 곡선을 사용하는 EdDSA 서명 체계인 ed25519를 사용합니다. 그 결과 기본 주소 형식으로 쓰이는 32바이트 공개 키가 생성됩니다. 해시되지 않으므로 그대로 사용할 수 있습니다.
유효한 주소가 되려면 ed25519 곡선 위의 점이어야 합니다. 하지만 모든 주소가 이 곡선에서 파생될 필요는 없습니다. Program Derived Address(PDA)는 곡선 밖에서 생성되므로 대응하는 비공개 키가 없고 서명에 사용할 수 없습니다. PDA는 System Program을 통해 생성되며 프로그램이 계정을 관리해야 할 때 사용됩니다. 이는 독자에게 Solana의 여러 주소 유형을 알리기 위한 참고 설명입니다. PDA는 향후 글에서 다루겠습니다.
Solana 계정은 Ethereum 계정과 어떻게 다른가요?
Ethereum에는 외부 소유 계정(EOA)과 컨트랙트 계정이라는 두 가지 주요 계정 유형이 있습니다. EOA는 비공개 키로 제어되지만 컨트랙트 계정은 컨트랙트 코드로 제어되며 자체적으로 트랜잭션을 시작할 수 없습니다.
EOA와 컨트랙트 계정은 모두 같은 계정 구조를 따릅니다.
- 잔액 - 모든 계정에는 Ether 단위로 측정되는 잔액이 있습니다
- Nonce - EOA에서는 계정에서 전송한 트랜잭션 수입니다. 컨트랙트에서는 계정이 생성한 컨트랙트 수입니다
- Storage Root - 계정 저장소 내용을 인코딩한 Merkle Patricia Trie 루트 노드의 256비트 해시입니다
- CodeHash - 컨트랙트의 Ethereum Virtual Machine(EVM) 코드 해시입니다. 불변이므로 생성 후 상태는 바뀔 수 있어도 코드는 바뀌지 않습니다. 프록시 패턴 사용 등 Ethereum에서 컨트랙트를 업그레이드하는 예외가 있지만 이 글의 범위를 벗어납니다. EOA에는 코드가 없으므로 빈 문자열의 해시가 저장됩니다
Solana는 모든 계정이 프로그램이 될 수 있는 더 통일된 계정 모델을 채택합니다. 코드와 데이터를 분리해 더 효율적이고 유연한 환경을 제공합니다. Solana 프로그램은 무상태이며, 중복 배포 없이 여러 데이터 계정과 상호작용합니다. 사용자가 여러 프로그램 간에 자산을 옮기지 않고 여러 프로토콜과 상호작용하려는 탈중앙화 금융(DeFi) 애플리케이션에 특히 유리합니다. 반면 Ethereum의 프로그래밍 모델은 코드와 상태를 하나의 엔터티로 결합합니다. 이 때문에 상호작용이 더 복잡해지고 상태 변경에 필요한 가스로 비용이 높아질 수 있습니다.
과거 Solana 계정은 임대료를 납부했으며, 활성 상태를 유지하려면 최소 잔액을 보유해야 했습니다. 이를 통해 사용하지 않거나 자금이 부족한 계정을 네트워크가 최종적으로 회수해 상태 팽창을 줄였습니다. 최근 업데이트 이후 메인넷에는 임대료를 납부하는 계정이 더 이상 없으며 모든 계정이 임대료 면제 조건을 충족해야 합니다. 반면 Ethereum은 가스로 리소스 할당을 관리합니다. 이 모델에서는 컨트랙트 저장소를 명시적으로 삭제하지 않는 한 영구적으로 유지됩니다. Solana는 상태 저장 비용을 더 예측하기 쉽게 만들지만, Ethereum의 비용은 달라질 수 있으며 네트워크 혼잡 시 감당하기 어려운 수준으로 높아질 수 있습니다.
다음 섹션에서는 Solana가 프로그램 로직과 상태를 분리하는 방식을 살펴봅니다. Ethereum의 프로그래밍 모델과 비교하면서 이 모듈식 접근 방식이 개발자에게 투명하고 예측 가능한 비용 구조를 제공하는 동시에 더 효율적인 온체인 작업을 지원하는 방법을 알아보겠습니다.
Solana 프로그램이란 무엇인가요?
프로그램은 BPF Loader가 소유한 실행 가능 계정입니다. 트랜잭션과 프로그램 로직을 처리하도록 설계된 Solana Runtime이 프로그램을 실행합니다.
Solana 프로그래밍 모델의 특징 중 하나는 코드와 데이터의 분리입니다. 프로그램은 무상태이므로 내부에 상태를 저장하지 않습니다. 대신 작업에 필요한 모든 데이터는 별도의 계정에 저장되며 트랜잭션을 통해 참조 형식으로 프로그램에 전달됩니다. 이 설계에서는 하나의 범용 프로그램 배포가 여러 계정과 상호작용할 수 있습니다.
Solana 프로그램은 다음 작업을 수행할 수 있습니다.
- 추가 계정 소유
- 다른 계정에서 읽거나 다른 계정에 자금 추가
- 소유한 계정의 데이터 수정 또는 자금 차감
프로그램에는 두 가지 유형이 있습니다.
- 온체인 프로그램 - 사용자가 작성해 Solana에 배포한 프로그램입니다. 일반적으로 프로그램을 배포한 계정인 업그레이드 권한자가 업그레이드할 수 있습니다
- 네이티브 프로그램 - Solana 코어에 통합된 프로그램입니다. 검증인 운영에 필요한 기본 기능을 제공합니다. 네이티브 프로그램은 네트워크 전체 소프트웨어 업데이트를 통해서만 업그레이드할 수 있습니다. 대표적인 예로 System Program, BPF Loader Program, Vote Program이 있습니다.
온체인 프로그램과 네이티브 프로그램은 모두 사용자와 다른 프로그램이 호출할 수 있습니다. 주된 차이는 업그레이드 방식입니다. 온체인 프로그램은 업그레이드 권한자가 업그레이드할 수 있지만 네이티브 프로그램은 클러스터 업데이트의 일부로만 업그레이드할 수 있습니다.
Solana Labs는 Solana Program Library라는 엄선된 온체인 프로그램 모음을 관리합니다. 이 라이브러리는 토큰 대출과 스테이크 풀 생성 등 다양한 온체인 작업을 지원합니다. 예를 들어 Associated Token Account Program은 사용자의 지갑을 해당 토큰 계정에 연결하는 표준과 메커니즘을 정의합니다. SPL은 계속 발전합니다. Token-2022 같은 프로그램은 Token Program이 제공하는 기능을 기반으로 이를 확장합니다.
Solana 프로그램은 일반적으로 상용구를 줄이고 직렬화와 역직렬화를 간소화하는 독자적 프레임워크인 Anchor의 도움을 받아 Rust로 개발합니다. Rust가 선호되지만 반드시 사용해야 하는 것은 아닙니다. C, C++ 및 LLVM의 BPF 백엔드를 대상으로 하는 모든 언어를 사용할 수 있습니다. BPF 백엔드는 프로그램을 BPF 바이트코드로 컴파일할 수 있게 하는 LLVM 구성 요소입니다. 최근 Solang과 Neon Labs의 발전으로 프로그램 개발에 Solidity도 사용할 수 있게 되었습니다.
프로그램은 일반적으로 Localhost와 Devnet에서 개발하고 테스트한 뒤 Testnet 또는 Mainnet Beta에 배포합니다. 개발자는 Solana CLI에서 solana program deploy <path to program> 명령으로 프로그램을 배포할 수 있습니다. BPF 바이트코드를 포함한 ELF 공유 객체로 컴파일된 프로그램은 지정된 Solana 클러스터에 업로드됩니다. 배포된 프로그램은 executable로 표시된 계정에 저장되며 계정 주소가 program_id 역할을 합니다.
초기에는 Solana 프로그램 크기의 두 배인 계정을 사용해 프로그램을 배포했습니다. Solana 1.16 업데이트는 크기 조정이 가능한 계정을 지원합니다. 개발자에게 더 유연한 리소스 할당을 제공합니다. 이제 개발자는 더 작은 계정으로 프로그램을 배포하고 나중에 크기를 확장할 수 있습니다.
앞서 설명했듯 프로그램이 상호작용하는 모든 데이터는 참조로 전달되는 별도 계정에 저장되므로 프로그램은 무상태로 간주됩니다. 모든 프로그램에는 명령 처리가 이루어지는 단일 진입점이 있습니다. 이 진입점은 program_id, 계정 배열, 바이트 배열 형태의 명령 데이터를 받습니다. 트랜잭션이 프로그램을 호출하면 Solana Runtime이 이를 실행합니다.
트랜잭션이란 무엇인가요?
트랜잭션은 온체인 활동의 핵심 기반입니다. 프로그램을 호출하고 상태 변경을 실행하는 메커니즘입니다. Solana의 트랜잭션은 검증인에게 수행할 작업, 대상 계정, 필요한 권한 보유 여부를 알려주는 명령 묶음입니다.
트랜잭션은 세 가지 주요 부분으로 구성됩니다.
- 읽거나 쓸 계정 배열
- 하나 이상의 명령
- 하나 이상의 서명
Solana의 트랜잭션은 Transaction 구조체를 따릅니다. 이 구조체는 네트워크가 작업을 처리하고 검증하는 데 필요한 정보를 제공합니다. 정의는 다음과 같습니다.
pub struct Transaction {
pub signatures: Vec,
pub message: Message,
}signatures 필드에는 직렬화된 Message에 대응하는 서명 집합이 들어 있습니다. 각 서명은 수수료 납부자부터 시작해 Message의 account_keys 목록에 있는 계정 키와 연결됩니다. 수수료 납부자는 트랜잭션 처리 시 발생하는 수수료를 부담하는 계정입니다. 일반적으로 트랜잭션을 시작하는 계정입니다. 필요한 서명 수는 메시지의 MessageHeader에 정의된 num_required_signatures와 같습니다.
message 자체는 Message 유형의 구조체입니다. 다음과 같이 정의됩니다.
pub struct Message {
pub header: MessageHeader,
pub account_keys: Vec,
pub recent_blockhash: Hash,
pub instructions: Vec,
}메시지의 header에는 부호 없는 8비트 정수 세 개가 들어 있습니다. 필요한 서명 수(num_required_signatures), 읽기 전용 서명자 수, 읽기 전용 비서명자 수입니다.
account_keys 필드는 트랜잭션에 관련된 모든 계정 주소를 나열합니다. 읽기-쓰기 접근을 요청하는 계정이 먼저 나오고 읽기 전용 계정이 뒤따릅니다.
recent_blockhash는 32바이트 SHA-256 해시를 포함하는 최근 블록 해시입니다. 클라이언트가 원장을 마지막으로 확인한 시점을 나타내며 최근 트랜잭션의 유효 기간 역할을 합니다. 검증인은 오래된 블록 해시가 포함된 트랜잭션을 거부합니다. 또한 최근 블록 해시를 포함하면 이전 트랜잭션과 완전히 동일한 트랜잭션이 거부되므로 중복 트랜잭션을 방지할 수 있습니다. 어떤 이유로든 네트워크에 제출하기 훨씬 전에 트랜잭션에 서명해야 한다면 최근 블록 해시 대신 영구 트랜잭션 nonce를 사용해 고유한 트랜잭션임을 보장할 수 있습니다.
instructions 필드에는 하나 이상의 CompiledInstruction 구조체가 들어 있으며, 각 구조체는 네트워크 검증인이 수행할 특정 작업을 지시합니다.
명령
명령은 Solana 프로그램을 한 번 호출하기 위한 지시입니다. 프로그램 실행 로직의 최소 단위이며 Solana에서 가장 기본적인 작업 단위입니다. 프로그램은 명령에서 전달된 데이터를 해석하고 지정된 계정에서 작업합니다. Instruction 구조체는 다음과 같이 정의됩니다.
pub struct Instruction {
pub program_id: Pubkey,
pub accounts: Vec,
pub data: Vec,
}program_id 필드는 실행할 프로그램의 공개 키를 지정합니다. 명령을 처리할 프로그램의 주소입니다. 이 공개 키가 가리키는 프로그램 계정의 소유자는 프로그램 초기화와 실행을 담당하는 로더를 지정합니다. 로더는 배포된 온체인 Solana Bytecode Format(SBF) 프로그램을 실행 가능한 것으로 표시합니다. Solana 런타임은 실행 가능으로 표시되지 않은 계정을 호출하려는 모든 트랜잭션을 거부합니다.
accounts 필드는 명령이 읽거나 쓸 수 있는 계정을 나열합니다. 이러한 계정은 AccountMeta 값으로 제공해야 합니다. 명령이 데이터를 변경할 수 있는 계정은 쓰기 가능으로 지정해야 하며, 그렇지 않으면 트랜잭션이 실패합니다. 프로그램은 소유하지 않았거나 필요한 권한이 없는 계정에 쓸 수 없기 때문입니다. 계정의 lamport를 변경할 때도 마찬가지입니다. 프로그램이 소유하지 않은 계정에서 lamport를 차감하면 트랜잭션이 실패하지만 어느 계정에든 lamport를 추가하는 것은 허용됩니다. accounts 필드는 프로그램이 읽거나 쓰지 않는 계정도 지정할 수 있습니다. 런타임의 프로그램 실행 일정에 영향을 주기 위한 것이며 그 외에는 이 계정이 무시됩니다.
data는 프로그램에 전달되는 입력 역할을 하는 범용 부호 없는 8비트 정수 벡터입니다. 프로그램이 실행할 인코딩된 명령을 포함하므로 매우 중요한 필드입니다.
Solana는 명령 데이터 형식에 구애받지 않습니다. 하지만 bincode와 borsh(Binary Object Representation Serializer for Hashing)를 통한 직렬화를 기본 지원합니다. 직렬화는 복잡한 데이터 구조를 전송하거나 저장할 수 있는 연속된 바이트로 변환하는 과정입니다. 모든 작업이 온체인에서 이루어지므로 데이터 인코딩 방식을 선택할 때 디코딩 오버헤드를 고려해야 합니다. Borsh 직렬화는 안정적인 명세와 JavaScript 구현을 제공하고 일반적으로 더 효율적이므로 bincode보다 선호되는 경우가 많습니다.
프로그램은 지원되는 명령을 더 쉽게 구성하기 위해 헬퍼 함수를 사용합니다. 예를 들어 System Program은 SystemInstruction::Assign 명령을 구성하는 헬퍼 함수를 제공합니다.
pub fn assign(pubkey: &Pubkey, owner: &Pubkey) -> Instruction {
let account_metas = vec![AccountMeta::new(*pubkey, true)];
Instruction::new(
system_program::id(),
&SystemInstruction::Assign { owner: *owner },
account_metas,
)
}이 함수는 처리 시 지정된 계정의 소유자를 제공된 새 소유자로 변경하는 명령을 구성합니다.
하나의 트랜잭션에는 여러 명령이 포함될 수 있으며 나열된 순서대로 순차적이고 원자적으로 실행됩니다. 즉, 모든 명령이 성공하거나 모두 실패합니다. 따라서 명령 순서가 중요할 수 있습니다. 프로그램은 잠재적인 취약점 악용을 막기 위해 가능한 모든 명령 순서를 안전하게 처리하도록 강화해야 합니다.
예를 들어 초기화 해제 중에 프로그램은 계정의 lamport 잔액을 0으로 설정해 계정 초기화를 해제하려 할 수 있습니다. 이는 Solana 런타임이 해당 계정을 삭제한다고 가정합니다. 이 가정은 트랜잭션 사이에서는 유효하지만 명령이나 Cross-Program Invocation 사이에서는 유효하지 않습니다. Cross-Program Invocation은 향후 글에서 다루겠습니다. 프로그램은 초기화 해제 과정의 잠재적 결함에 대비해 계정 데이터를 명시적으로 0으로 만들어야 합니다. 그렇지 않으면 공격자가 후속 명령을 실행해 트랜잭션이 완료되기 전에 계정을 재사용하는 등 삭제된 것으로 간주된 계정을 악용할 수 있습니다.
버전이 지정된 트랜잭션이란 무엇인가요?
Solana의 트랜잭션은 클러스터 전반에서 데이터를 빠르고 안정적으로 전송하기 위해 IPv6 Maximum Transmission Unit (MTU) 표준을 사용합니다. Solana의 네트워킹 스택은 보수적인 MTU 크기인 1280바이트를 사용합니다. 헤더 공간을 제외하면 패킷 데이터에 1232바이트를 사용할 수 있습니다. 따라서 Solana 트랜잭션은 이 크기로 제한됩니다.
이 크기 제약은 다양한 네트워크 개선을 지원하지만 하나의 트랜잭션에서 수행할 수 있는 작업의 복잡성도 제한합니다. 계정 주소 하나가 32바이트의 저장 공간을 차지하므로 명령이 없는 트랜잭션에는 최대 35개의 계정을 저장할 수 있습니다. 이 제약은 단일 트랜잭션에서 서명이 필요 없는 계정을 35개 넘게 사용해야 하는 사례에 어려움을 줍니다.
이 문제를 해결하기 위해 여러 버전의 트랜잭션 형식을 지원하는 새로운 트랜잭션 형식이 도입되었습니다. 현재 Solana 런타임은 두 가지 트랜잭션 버전을 지원합니다.
legacy- 기존 트랜잭션 형식0(Version 0) - Address Lookup Table을 지원하는 최신 트랜잭션 형식
Version 0은 Address Lookup Table(ALT)을 지원하기 위해 출시되었습니다. ALT는 본질적으로 계정 주소를 테이블 형태의 온체인 데이터 구조에 저장합니다. 이 테이블은 계정 주소를 저장하는 별도 계정이며, 트랜잭션에서 1바이트 u8 인덱스로 주소를 참조할 수 있게 합니다. 포함된 각 계정이 32바이트 대신 1바이트만 사용하면 되므로 트랜잭션 크기가 크게 줄어듭니다. ALT는 DeFi 애플리케이션에서 흔히 볼 수 있는 다수의 계정이 관련된 복잡한 작업에 특히 유용합니다.
이 다이어그램은 Solana Cookbook의 버전이 지정된 트랜잭션 섹션을 바탕으로 수정했습니다
“버전이 지정된 트랜잭션”이라는 용어는 Solana가 레거시와 Version 0 트랜잭션 형식을 모두 지원하는 방식을 뜻합니다. 이 접근 방식은 런타임 개선을 수용하면서도 결합 가능성을 보장합니다.
버전이 지정된 트랜잭션의 구조
VersionedTransaction는 다음과 같이 정의됩니다.
pub struct VersionedTransaction {
pub signatures: Vec,
pub message: VersionedMessage,
}signatures 필드는 트랜잭션 서명자의 서명 목록입니다. 트랜잭션을 인증하고 무결성을 유지합니다. message은 트랜잭션의 실제 내용입니다. 레거시 메시지와 Version 0 메시지를 모두 처리하는 얇은 열거형 래퍼인 VersionedMessage 유형으로 캡슐화됩니다.
pub enum VersionedMessage {
Legacy(Message),
V0(Message),
}메시지 버전은 직렬화 과정의 첫 번째 비트로 결정됩니다. 첫 번째 비트가 설정되어 있으면 나머지 7비트를 사용해 Version 0부터 직렬화된 Message 버전을 결정합니다. 첫 번째 비트가 설정되어 있지 않으면 모든 바이트를 사용해 레거시 Message 형식을 인코딩합니다. 이는 이름이 같은 Message 구조체 두 개가 서로 다른 모듈인 legacy와 v0로 분리되어 있기 때문입니다.
Message는 트랜잭션의 압축된 내부 형식을 나타냅니다. 네트워크 전송과 런타임 조작에 사용됩니다. 트랜잭션 명령이 사용하는 모든 계정의 선형 목록, 계정 배열 구조를 설명하는 MessageHeader, 최근 블록 해시, 메시지 명령의 압축 인코딩을 포함합니다. 다음은 v0 Message 구조체의 구조입니다.
pub struct Message {
pub header: MessageHeader,
pub account_keys: Vec,
pub recent_blockhash: Hash,
pub instructions: Vec,
pub address_table_lookups: Vec,
}레거시 메시지와 v0 메시지의 차이는 address_table_lookups 필드의 포함 여부입니다.
프로그래밍 모델과 Solana 트랜잭션 흐름의 통합
Solana의 프로그래밍 모델은 계정 및 트랜잭션 시스템과 긴밀하게 통합되어 있습니다. 각 개념이 연결되는 방식은 다음과 같습니다.
- 상태로서의 계정 - Solana의 계정은 프로그램의 상태 컨테이너 역할을 합니다. 프로그래밍 모델은 명령에 응답해 이 컨테이너에 저장된 데이터를 수정하는 방식을 중심으로 구성됩니다
- 명령 - 프로그램은 트랜잭션에 포함된 명령을 처리하는 로직을 정의합니다. 명령은 계정 데이터와 상호작용하는 실행 가능한 구성 요소입니다
- 직렬화와 처리 - 트랜잭션이 직렬화되면 프로그램의 명령이 계정 상태 변경을 지시합니다. 직렬화 과정은 레거시 또는 Version 0 트랜잭션 형식 중 어느 것을 사용하든 프로그램의 설계를 따릅니다
- 원자성 - Solana의 프로그래밍 모델은 명령의 원자적 처리를 보장합니다. 프로그램은 동시 트랜잭션을 안전하고 효율적으로 처리하도록 설계해야 합니다
- 확장성 - Solana의 프로그래밍 모델은 Address Lookup Table(ALT) 같은 기능으로 확장성을 지원합니다. 이 테이블은 트랜잭션 크기를 줄이고 트랜잭션이 참조할 수 있는 계정 수를 늘립니다
Solana의 프로그래밍 모델은 단순히 코드를 작성하는 것에 그치지 않습니다. 더 넓은 생태계에서 코드가 상호작용하는 방식을 이해해야 합니다. 계정은 네트워크에서 데이터를 저장하고 수정하는 주요 수단으로서 이 모델의 핵심입니다. 트랜잭션은 검증인에게 생성, 업데이트, 삭제할 데이터를 알려 온체인 활동을 지원합니다. 개발자가 Solana 생태계에서 성능과 시너지를 최적화한 애플리케이션을 구축하려면 이러한 요소를 깊이 이해해야 합니다.
결론
축하합니다! 이 글에서는 Solana 시스템 아키텍처의 복잡성을 살펴보고 클러스터를 하나의 거대한 데이터 힙으로 이해했습니다. 이 힙이 계정이라는 독립된 메모리 영역으로 구성되며 Solana 프로그래밍 모델의 기반이 되는 방식도 알아봤습니다. 계정은 사용자 토큰부터 네트워크 동작을 정의하는 프로그램까지 모든 것을 저장하며, 이 모든 것은 트랜잭션을 통해 수정됩니다.
개발자에게 Solana의 탈중앙화 컴퓨팅 접근 방식을 이해하는 것은 매우 중요합니다. Solana의 기능을 온전히 활용하는 애플리케이션을 구축하려면 계정, 프로그램, 트랜잭션의 세부 구조를 파악해야 합니다. 코드와 상태가 분리된 시스템을 이해하는 과정입니다. 그 결과 무상태 프로그램은 계정을 통해 데이터와 상호작용하며 전례 없는 수준의 결합 가능성과 업그레이드 가능성을 제공합니다.
투자자와 일반 사용자 모두에게 Solana의 설계가 견고하고 유연하며 효율적인 생태계를 만드는 방식을 이해하는 것은 중요합니다. 그래야 플랫폼의 실현 가능성과 Solana에서만 가능한 혁신적인 애플리케이션을 지원할 역량을 제대로 평가할 수 있습니다.
익명의 독자 여러분, 여기까지 읽어주셔서 감사합니다! 더 깊이 알아볼 준비가 되셨나요? 지금 Discord에 참여해 Solana 프로그래밍을 시작하세요.
추가 자료 / 더 읽어보기
관련 아티클
Helius 구독하기
최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요


