

🚨 Move에서의 Error Handling과 Assertions: 블록체인을 지키는 마지막 수문장
블록체인 스마트 컨트랙트는 단순한 코드가 아닙니다.
실제 자산을 다루는 코드이며, 한 줄의 오류가 수백만 달러의 손실로 이어질 수 있습니다.
Move 언어는 이런 문제를 근본적으로 차단하는 철저한 에러 처리 시스템을 가지고 있습니다.
이번 글에서는 Move의 Error Handling(에러 처리) 과 Assertions(단언문) 이
어떻게 작동하고, 왜 블록체인 보안에 핵심적인 역할을 하는지 살펴봅니다.
🧩 에러 처리란 무엇인가?
에러 처리란 프로그램이 예상치 못한 상황에 어떻게 반응하는가를 정의하는 방식입니다.
일반적인 프로그래밍에서는 “문제가 생기면 예외를 던지고 복구하는” 방식이지만,
Move는 전혀 다른 접근을 취합니다.
🚗 전통적인 에러 처리는 ‘주차 공간이 좁다는 걸 깨닫고 조심히 후진하는 것’이라면,
Move는 ‘처음부터 그 공간에 들어가지 않는 것’과 같습니다.
즉, 문제가 발생하기 전에 미리 차단하는 구조입니다.
이 접근은 단순하고, 안전하며, 무엇보다 블록체인에 가장 적합합니다.
⚙️ Move 트랜잭션에서 에러가 발생하면?
Move에서 트랜잭션이 에러를 만나면 다음이 즉시 일어납니다:
- 실행이 즉시 중단됩니다.
- 트랜잭션 중에 발생한 모든 상태 변화가 폐기됩니다.
- 블록체인의 상태는 완전히 그대로 유지됩니다.
- 가스비는 여전히 부과됩니다.
(이는 공격자가 실패하는 트랜잭션으로 네트워크를 마비시키는 것을 방지하기 위함입니다.) - 에러 코드가 반환되어 원인을 명확히 식별할 수 있습니다.
겉보기엔 “실패한 거래에도 가스비를 내야 한다니 낭비 아닌가?” 싶지만,
이는 보안과 안정성을 위한 설계 철학입니다.
모든 연산에는 비용이 존재해야만 네트워크가 악용되지 않습니다.
🧠 assert!: Move의 수문장
Move에서 에러 처리를 담당하는 핵심 도구는 바로 assert! 매크로입니다.
module 0x42::bank {
const E_INSUFFICIENT_FUNDS: u64 = 1;
public fun withdraw(account: &mut Account, amount: u64) {
assert!(account.balance >= amount, E_INSUFFICIENT_FUNDS);
account.balance = account.balance - amount;
}
}
이 구문은 “조건이 참인지 확인하고, 거짓이면 지정된 에러 코드와 함께 즉시 중단”합니다.
단순해 보이지만, 이것이 Move의 모든 안전성의 근간입니다.
assert!가 실패하면 긴급 정지 버튼을 누른 것과 같습니다.
부분 실행도, 손상된 상태도, 복구 작업도 없습니다.
블록체인 입장에서는 “그 트랜잭션은 존재하지 않았던 것처럼” 처리됩니다.
💡 에러 코드가 문자열보다 중요한 이유
Move는 문자열 에러 메시지 대신 숫자형 에러 코드를 사용합니다.
왜냐하면 블록체인에 문자열을 저장하는 건 비용이 크기 때문입니다.
예를 들어 1번 에러는 E_INSUFFICIENT_FUNDS(잔액 부족)을 의미할 수 있습니다.
개발자는 문서에서 각 코드의 의미를 정의하고,
사용자는 반환된 숫자만으로 문제를 빠르게 파악할 수 있습니다.
🧱 좋은 에러 설계의 구조
대규모 프로젝트일수록 체계적인 에러 구조화는 필수입니다.
module 0x42::marketplace {
// 인증 관련 (1–99)
const E_NOT_ITEM_OWNER: u64 = 1;
const E_MARKETPLACE_PAUSED: u64 = 2;
// 검증 관련 (100–199)
const E_INVALID_PRICE: u64 = 100;
const E_PRICE_TOO_HIGH: u64 = 101;
// 상태 관련 (200–299)
const E_ITEM_ALREADY_LISTED: u64 = 200;
const E_ITEM_NOT_FOUND: u64 = 201;
}
이렇게 그룹을 나누면,
에러 코드만 봐도 “무슨 종류의 문제인지” 즉시 파악할 수 있습니다.
101이라면 “가격 검증 오류”, 201이라면 “상태 불일치 문제”임을 알 수 있죠.
🛒 예제: 마켓플레이스 리스트 함수
public fun list_item(
seller: &signer,
item_id: u64,
price: u64
) acquires ItemRegistry, ListingRegistry, MarketplaceConfig {
// 1️⃣ 운영 여부 확인
let config = borrow_global<MarketplaceConfig>(@marketplace);
assert!(!config.is_paused, E_MARKETPLACE_PAUSED);
// 2️⃣ 아이템 소유자 확인
let seller_addr = signer::address_of(seller);
let registry = borrow_global<ItemRegistry>(@marketplace);
assert!(table::contains(®istry.items, item_id), E_ITEM_NOT_FOUND);
let item = table::borrow(®istry.items, item_id);
assert!(item.owner == seller_addr, E_NOT_ITEM_OWNER);
// 3️⃣ 가격 검증
assert!(price > 0, E_INVALID_PRICE);
assert!(price <= MAX_LISTING_PRICE, E_PRICE_TOO_HIGH);
// 4️⃣ 중복 등록 방지
let listings = borrow_global<ListingRegistry>(@marketplace);
assert!(!table::contains(&listings.active, item_id), E_ITEM_ALREADY_LISTED);
// ✅ 모든 검증 통과
create_listing_internal(seller_addr, item_id, price);
}
Move의 기본 철학은 명확합니다.
“상태를 변경하기 전에 모든 조건을 검증하라.”
이 순서를 지키면 오류 발생 시 블록체인은 완벽히 일관된 상태를 유지합니다.
🔄 다양한 에러 처리 패턴
Move에는 단순한 assert! 외에도 상황에 따라 다양한 패턴이 존재합니다.
1️⃣ Option 패턴: 존재하지 않아도 오류는 아니다
public fun find_listing(item_id: u64): Option<Listing> acquires ListingRegistry {
let listings = borrow_global<ListingRegistry>(@marketplace);
if (table::contains(&listings.active, item_id)) {
option::some(*table::borrow(&listings.active, item_id))
} else {
option::none()
}
}
리스트가 없다고 해서 항상 실패는 아닙니다.
Option<T>를 사용하면, “값이 있을 수도 있고 없을 수도 있는” 상황을 표현할 수 있습니다.
2️⃣ Validation 패턴: 검증과 실행의 분리
복잡한 로직일수록 검증 로직을 별도로 분리해 테스트하기 쉽고 유지보수가 편합니다.
public fun validate_purchase(buyer_addr: address, listing: &Listing, payment_amount: u64) {
assert!(get_balance(buyer_addr) >= payment_amount, E_INSUFFICIENT_FUNDS);
assert!(payment_amount == listing.price, E_INCORRECT_PAYMENT);
assert!(buyer_addr != listing.seller, E_CANNOT_BUY_OWN_ITEM);
}
3️⃣ Result 패턴: 실패도 값으로 다루기
Result<T> 구조체를 만들어, abort하지 않고 다양한 실패 케이스를 반환할 수도 있습니다.
struct Result<T> has drop {
success: bool,
value: Option<T>,
error_code: u64,
}
이 방식은 자동 구매나 여러 시도(fallback)가 필요한 로직에 유용합니다.
🧭 에러 코드 관리 전략
대규모 프로젝트에서는 중앙 집중형 에러 모듈을 두는 것이 좋습니다.
module marketplace::errors {
const E_UNAUTHORIZED: u64 = 1;
const E_INVALID_ARGUMENT: u64 = 2;
const E_INSUFFICIENT_BALANCE: u64 = 1000;
const E_INVALID_PRICE: u64 = 2000;
public fun unauthorized(): u64 { E_UNAUTHORIZED }
public fun insufficient_balance(): u64 { E_INSUFFICIENT_BALANCE }
}
다른 모듈에서는 이를 불러와 재사용합니다.
이렇게 하면 에러 코드가 일관성 있고 관리하기 쉬워집니다.
⚠️ 자주 발생하는 실수들
- 침묵하는 실패 (Silent Failure)
→ 기본값을 반환해 문제를 숨기는 것은 절대 금물입니다.
항상 assert! 또는 Option으로 명확히 처리해야 합니다. - 모호한 에러 코드 (Generic Error)
→ E_INVALID_TX보다는 E_AMOUNT_TOO_LARGE, E_EXPIRED_DEADLINE처럼 구체적으로 정의하세요. - 늦은 검증 (Late Validation)
→ 비싼 연산 전에 모든 검증을 끝내야 가스 낭비를 줄일 수 있습니다.
🧪 테스트: 실패도 테스트하라
Move는 실패를 테스트하는 기능을 기본 제공하며,
#[expected_failure(abort_code = ...)]를 통해 정확히 어떤 이유로 실패해야 하는지까지 검증할 수 있습니다.
#[test]
#[expected_failure(abort_code = market::E_NOT_ITEM_OWNER)]
fun test_list_item_not_owner() {
let owner = @0x123;
let other = @0x456;
create_test_item(owner, 1);
market::list_item(create_signer_for_test(other), 1, 500);
}
✅ 베스트 프랙티스 요약
- 에러 코드는 API의 일부다. 체계적으로 설계하고 문서화하라.
- Fail Fast. 빠르고 구체적으로 실패하라.
- 검증과 실행을 분리하라.
- Abort 함수와 Safe 함수 버전을 함께 제공하라.
- 모든 assertion은 테스트되어야 한다.
- 문서에 에러 조건을 명시하라.
- 절대 ‘조용히 실패’하지 마라.
🔑 핵심 정리
Move의 에러 처리 시스템은 단순히 코드의 예외를 다루는 기능이 아닙니다.
블록체인 전체의 안정성과 일관성을 유지하는 핵심 메커니즘입니다.
- 트랜잭션은 “완전히 성공하거나 완전히 실패한다.”
- 모든 상태 변경 전엔 반드시 검증이 이루어진다.
- 에러 코드는 언어이자 문서다.
- 검증 패턴(Option, Result, Validation)을 통해 복잡한 상황도 유연하게 다룬다.
- 실패 또한 테스트되어야 한다.
이 단순한 구조 덕분에 Move는 복잡한 예외 처리 없이도
항상 안전하고, 예측 가능한 결과만을 남깁니다.
🚀 다음 단계
이제 Move의 핵심 구성요소 — 리소스, 모듈, 함수, 제어 흐름, 그리고 에러 처리 — 를 모두 이해했습니다.
이제 실제로 응용해볼 차례입니다.
📘 추천 가이드
- [Fungible Asset Guide] 토큰 발행 실습
- [NFT Contract Walkthrough] 고유 자산 구현
- [Escrow Contract Guide] 안전한 결제 처리
- [Fee Splitter Module] 수익 분배 시스템 구현
- Become an Early Builder
- Become an Ambassador
- Cedra on X / Discord / Telegram
- Cedra Korea Linktree: https://linktr.ee/cedraKorea
댓글 0개
2025.10.07 20:07:36