[Sui Object] #6. Party Objects

이 글은 Sui Object 시리즈의 6번째 글입니다.

1.png

개요

이전 글에서는 하나의 Object를 다른 Object 안에 중첩시키는 Wrapped Object를 살펴보았습니다. 지금까지 우리는 Sui의 세 가지 소유권 형태인 Address-Owned, Shared, Immutable Object를 다루었고, Wrapping이라는 Object 구성 패턴까지 알아보았습니다.

이번 글에서는 또 다른 소유권 형태인 Party Object를 다룹니다. Party Object는 한마디로 단일 주소가 소유하지만, consensus가 순서를 정하고 버전을 관리하는(consensus-address-owned) Object입니다. 언뜻 모순처럼 들립니다. 지금까지 살펴본 Address-Owned Object는 단일 주소가 소유하는 대신 consensus를 거치지 않았고, Shared Object는 consensus를 거치는 대신 특정 소유자가 없었기 때문입니다. Party Object는 이 두 형태의 특성을 한데 모아, 단일 소유권을 유지하면서도 consensus의 이점을 함께 누리는 새로운 선택지를 제공합니다.

이 글에서는 Party Object가 정확히 무엇인지, 기존 소유권 형태와 어떻게 다른지 살펴봅니다. 이어서 transfer module이 제공하는 함수로 Party Object를 생성하는 방법을 예제와 함께 알아보고, 마지막으로 Party Object를 언제 사용하면 좋은지와 현재의 제약 사항까지 차례대로 정리하겠습니다.


Party Object란?

Party Object는 단일 주소가 소유하지만 consensus를 통해 순서가 정해지고 버전이 관리되는 Object입니다. 공식 문서에서는 이를 consensus-address-owned object라고 표현합니다. 이름 그대로 consensus(합의)와 address-owned(주소 소유)라는 두 성질을 모두 가진 Object입니다.

이 차이는 Object의 소유권 정보에서 드러납니다. 일반적인 Address-Owned Object의 소유자가 AddressOwner로 표시되는 것과 달리, Party Object는 소유권이 ConsensusAddressOwner 형태로 표현됩니다. 여전히 특정 주소 하나가 소유하고 있지만, 그 Object를 사용하는 트랜잭션은 consensus를 거쳐 순서가 결정된다는 의미입니다.

그렇다면 단일 주소가 소유하는데 굳이 consensus를 거치게 만드는 이유는 무엇일까요? 핵심은 동시성에 있습니다. 일반 Address-Owned Object는 한 번에 하나의 트랜잭션만 처리할 수 있는 반면, Party Object는 consensus가 순서를 정리해주기 때문에 여러 트랜잭션을 동시에 진행(inflight)시킬 수 있습니다. 즉, 소유권의 명확함은 그대로 유지하면서 처리량 측면의 유연함을 얻는 것입니다.

참고로 현재 공개적으로 사용할 수 있는 Party Object는 단일 소유자(single owner) 형태 한 가지뿐입니다. 흥미로운 점은 sui::party::Party 타입 자체가 이미 여러 구성원(multiple members)의 권한을 표현할 수 있도록 설계되어 있다는 것입니다. 다만 현재 공개된 소유권 모드는 party::single_owner로 제한되어 있어, 실제로 사용할 수 있는 것은 단일 소유자 방식입니다.


다른 소유권 형태와의 비교

Party Object의 위치를 가장 잘 이해하는 방법은 앞서 살펴본 세 가지 소유권 형태와 나란히 놓고 비교해보는 것입니다.

Address-Owned Object와 비교하면, 둘 다 단일 주소가 소유한다는 점은 같습니다. 차이는 트랜잭션 처리 방식입니다. Address-Owned Object는 한 번에 하나의 트랜잭션만 처리(inflight)할 수 있지만, Party Object는 consensus가 순서를 정리해주기 때문에 여러 트랜잭션을 동시에 진행할 수 있습니다. 그 대가로 Party Object는 consensus를 거치므로, fast path를 타는 Address-Owned Object에 비해 약간의 지연을 감수합니다.

Shared Object와 비교하면, 둘 다 consensus를 거쳐 순서가 정해진다는 공통점이 있습니다. 하지만 Shared Object는 특정 소유자가 없어 누구나 접근할 수 있는 반면, Party Object는 단일 주소가 명확히 소유합니다. 또한 Party Object는 다른 주소로 전송(transfer)하거나 다른 Object 안에 wrapping할 수 있지만, Shared Object는 한 번 공유되면 그렇게 할 수 없습니다.

Immutable Object와 비교하면, Immutable Object는 한 번 생성되면 누구도 변경할 수 없고 소유자도 없습니다. 반면 Party Object는 여전히 변경 가능(mutable)하며 소유자가 존재합니다. 필요하다면 필드를 추가하거나 전송하는 등 상태를 바꿀 수 있고, 경우에 따라 Immutable Object로 전환하는 것도 가능합니다.

아래 표는 네 가지 소유권 형태를 한눈에 정리한 것입니다.

소유권 형태소유자consensus동시 트랜잭션전송/wrapping
Address-Owned단일 주소거치지 않음불가 (1개)가능
Shared없음 (공유)거침가능불가
Immutable없음 (불변)해당 없음읽기 전용불가
Party단일 주소거침가능가능

Party Object 생성하기

그렇다면 Party Object는 어떻게 만들까요? Object를 생성하는 과정 자체는 다른 Object와 다르지 않습니다. object::new로 UID를 만들어 struct를 구성하면 됩니다. 핵심은 그 Object를 어떻게 전송하느냐에 있습니다.

Sui는 일반 Object를 단일 주소에게 전송할 때 transfer::transfer와 transfer::public_transfer를 사용합니다. Party Object에는 이에 대응하는 별도의 전송 함수가 마련되어 있습니다. 바로 transfer module이 제공하는 party_transfer와 public_party_transfer입니다. 어떤 Object를 이 함수들로 전송하는 순간, 해당 Object는 ConsensusAddressOwner가 소유하는 Party Object가 됩니다.

이번 섹션에서는 두 전송 함수의 차이를 먼저 살펴본 뒤, 실제 예제 코드로 Party Object를 생성하고 전송하는 과정을 확인하겠습니다.


party_transfer와 public_party_transfer

두 함수의 시그니처는 다음과 같습니다.

public fun party_transfer<T: key>(obj: T, party: sui::party::Party)
public fun public_party_transfer<T: key + store>(obj: T, party: sui::party::Party)

두 함수의 관계는 일반 전송 함수인 transfer와 public_transfer의 관계와 똑같습니다.

  • party_transfer: Object에 custom transfer policy를 직접 정의할 때 사용합니다. 타입 제약이 T: key이며, 해당 Object를 정의한 Module 안에서만 호출할 수 있습니다. store ability가 없는 Object의 전송 규칙을 모듈이 직접 통제하고 싶을 때 적합합니다.
  • public_party_transfer: Object가 store ability를 가진 경우 사용합니다. 타입 제약이 T: key + store이며, 이 조건을 만족하는 Object는 어느 Module에서나 자유롭게 전송할 수 있습니다.

두 함수 모두 두 번째 인자로 sui::party::Party 값을 받습니다. 이 Party 값이 곧 "누가 이 Object를 소유할 것인가"를 나타냅니다. 앞서 설명했듯 현재 공개적으로 만들 수 있는 Party는 단일 소유자뿐이며, 소유자 주소를 받아 party::single_owner(owner) 형태로 생성합니다. 이렇게 만든 Party를 전송 함수에 넘기면, 해당 Object는 그 주소가 소유하는 Party Object가 됩니다.


예제 코드

Sui 공식 예제(object_basics module)를 통해 Party Object를 생성하는 실제 코드를 살펴보겠습니다. 먼저 예제에서 사용하는 Object 타입은 다음과 같이 단순합니다.

public struct Object has key, store {
    id: UID,
    value: u64,
}

Object는 key와 store ability를 모두 가지고 있습니다. store가 있으므로 앞서 살펴본 public_party_transfer를 사용할 수 있습니다.

다음은 Object를 새로 만들면서 곧바로 Party Object로 전송하는 함수입니다.

use sui::party;

public fun create_party(value: u64, recipient: address, ctx: &mut TxContext) {
    let party = party::single_owner(recipient);
    transfer::public_party_transfer(
        Object { id: object::new(ctx), value },
        party,
    )
}

동작을 단계별로 살펴보면 다음과 같습니다.

  1. party::single_owner(recipient)로 수신자 주소를 단일 소유자로 하는 Party 값을 만듭니다.
  2. object::new(ctx)로 새 Object를 생성합니다.
  3. transfer::public_party_transfer에 Object와 Party를 함께 넘겨 전송합니다.

이 함수가 실행되고 나면 Object는 recipient가 소유하는 Party Object가 됩니다. 소유자는 분명히 recipient 한 명이지만, 이 Object를 사용하는 트랜잭션은 consensus를 거쳐 순서가 정해집니다.

이미 존재하는 Object를 Party Object로 만들 수도 있습니다. 이때는 새로 생성하지 않고 기존 Object를 그대로 전송하면 됩니다.

public fun party_transfer_single_owner(o: Object, recipient: address) {
    let party = party::single_owner(recipient);
    transfer::public_party_transfer(o, party)
}

함수는 값으로 받은 Object o를 party::single_owner로 만든 Party에게 전송할 뿐입니다. 이처럼 일반 Object를 Party Object로 전환하는 일은 단순히 적절한 전송 함수를 호출하는 것만으로 끝납니다.


Party Object는 언제 사용할까?

Party Object는 "단일 주소가 소유하되, consensus의 순서 보장이 필요한" 상황을 위한 선택지입니다. 구체적으로 다음과 같은 경우에 유용합니다.

여러 트랜잭션을 동시에 처리해야 할 때. 활발하게 사용되는 Object일수록 한 번에 하나의 트랜잭션만 처리하는 Address-Owned Object의 제약은 병목이 됩니다. 예를 들어 자주 입출금이 일어나는 Object라면, Address-Owned 상태에서는 트랜잭션이 줄을 서야 합니다. Party Object로 만들면 consensus가 순서를 정리해주므로 여러 트랜잭션을 동시에 진행(inflight)시킬 수 있어, 소유권은 유지하면서 처리량을 높일 수 있습니다.

Shared Object와 함께 사용할 때. 하나의 트랜잭션이 Shared Object를 다룬다면 그 트랜잭션은 어차피 consensus를 거치게 됩니다. 이때 함께 사용하는 단일 소유 Object를 Party Object로 만들면, Address-Owned Object를 섞어 쓸 때보다 전체 흐름이 더 매끄럽습니다. 트랜잭션이 이미 consensus 경로를 타고 있으므로, Party Object를 함께 두는 편이 자연스럽고 성능 면에서도 이점이 있습니다.

정리하면, Party Object는 소유권의 명확함(단일 주소)과 consensus의 유연함(동시 처리·순서 보장)을 동시에 원할 때 적합한 형태입니다.


제약 사항

Party Object는 강력하지만, 이 글을 쓰는 시점에 몇 가지 제약이 있습니다.

가스 지불에 직접 사용할 수 없습니다. Coin<SUI>를 Party Object로 만들면, 그 Coin으로는 가스를 바로 지불할 수 없습니다. 가스로 사용하려면 먼저 Address-Owned 상태로 되돌려야 합니다. 따라서 가스 비용을 낼 Coin은 Party Object로 두지 않는 편이 좋습니다.

Shared Object로 전환할 수 없습니다. 한 번 Party Object로 만든 Object는 이후에 Shared Object로 바꿀 수 없습니다. 둘 다 consensus를 거친다는 공통점이 있지만, 소유권 모델이 다르기 때문에 이 방향의 전환은 허용되지 않습니다.

현재는 단일 소유자만 지원합니다. 앞서 언급했듯, sui::party::Party 타입은 여러 구성원을 모델링할 수 있도록 설계되어 있지만 공개적으로 사용 가능한 것은 party::single_owner 한 가지뿐입니다.

사용하기 전에 현재 버전에서 지원되는 범위를 공식 문서로 한 번 더 확인하는 것을 권장합니다.


References

전체 목차:

  1. [Sui Object] #1. Object Model
  2. [Sui Object] #2. Address-Owned Objects
  3. [Sui Object] #3. Shared Objects
  4. [Sui Object] #4. Immutable Objects
  5. [Sui Object] #5. Wrapped Objects
  6. [Sui Object] #6. Party Objects