[Sui Object] #2. Address-Owned Objects

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

1.png

개요

이전 글에서 Sui의 Object Model과 소유권 유형을 살펴보았습니다. 이번 글에서는 그중 가장 기본적인 소유권 형태인 Address-Owned Object를 다룹니다.

Address-Owned Object는 32바이트 Address가 소유하는 Object입니다. 여기서 32바이트 Address란 Account Address 또는 다른 Object의 ID를 의미합니다. 소유자만이 해당 Object에 접근할 수 있으며, 다른 Address로 전송할 수 있습니다.


Address-Owned Object 생성

Address-Owned Object를 생성하려면 Object를 만든 뒤 특정 Address로 전송해야 합니다. Sui의 transfer Module은 이를 위해 두 가지 함수를 제공합니다.


transfer::transfer

transfer::transfer는 key Ability만 가진 Object를 전송할 때 사용합니다.

public fun transfer<T: key>(obj: T, recipient: address)

이 함수는 Object를 정의한 Module 내부에서만 호출할 수 있습니다. Object의 전송 로직을 Module이 직접 제어하고 싶을 때 사용합니다.

module example::color_object {
    use sui::transfer;

    public struct ColorObject has key {
        id: UID,
        red: u8,
        green: u8,
        blue: u8,
    }

    public fun create(red: u8, green: u8, blue: u8, recipient: address, ctx: &mut TxContext) {
        let obj = ColorObject {
            id: object::new(ctx),
            red,
            green,
            blue,
        };
        transfer::transfer(obj, recipient);
    }
}

ColorObject는 key Ability만 가지고 있으므로 transfer::transfer를 사용합니다. Module 외부에서는 이 Object를 직접 전송할 수 없고, create 같은 함수를 통해서만 전송이 이루어집니다.


transfer::public_transfer

transfer::public_transfer는 key + store Ability를 가진 Object를 전송할 때 사용합니다.

public fun public_transfer<T: key + store>(obj: T, recipient: address)

store Ability가 있으면 어떤 Module에서든 호출할 수 있습니다. Object를 자유롭게 전송할 수 있도록 허용하고 싶을 때 사용합니다.

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

public fun create(value: u64, recipient: address, ctx: &mut TxContext) {
    transfer::public_transfer(
        FreeObject { id: object::new(ctx), value },
        recipient,
    );
}

FreeObject는 key + store Ability를 가지므로 transfer::public_transfer를 사용합니다. 이 Object는 Module 외부에서도 자유롭게 전송할 수 있습니다.


공유 Object로의 변환 불가

Address-Owned Object는 한 번 소유권이 설정되면 Shared Object로 변환할 수 없습니다. Object를 공유하려면 생성 시점에 transfer::share_object를 호출해야 합니다. 이미 특정 Address에 전송된 Object는 공유할 수 없으므로, Object의 소유권 형태는 설계 단계에서 미리 결정해야 합니다.


Address-Owned Object를 사용하는 경우

Address-Owned Object는 다음과 같은 상황에서 적합합니다.

  • 단일 소유권이 필요한 경우: NFT, 지갑, 개인 설정 데이터처럼 특정 Address가 독점적으로 소유해야 하는 Asset에 적합합니다.
  • 성능이 중요한 경우: Address-Owned Object는 Shared Object와 달리 합의(Consensus) 과정을 거치지 않습니다. 소유자가 명확하므로 트랜잭션을 병렬로 처리할 수 있어 더 빠른 실행이 가능합니다.
  • 접근 제어가 필요한 경우: 소유자만 Object에 접근할 수 있으므로, 별도의 권한 검사 로직 없이도 자연스럽게 접근이 제한됩니다.

Address-Owned Object와 상호작용

Address-Owned Object의 소유자는 Account Address 또는 Object ID입니다. 소유자 유형에 따라 Object에 접근하는 방식이 달라집니다.


Account Address가 소유자인 경우

Account Address가 소유한 Object는 해당 Address의 서명이 포함된 트랜잭션에서 직접 접근할 수 있습니다. 트랜잭션의 입력(Input)으로 Object ID를 지정하면, Sui 런타임이 소유권을 확인한 뒤 Object를 함수에 전달합니다.

public fun update_value(obj: &mut MyObject, new_value: u64) {
    obj.value = new_value;
}

이 함수를 호출할 때, 트랜잭션 서명자가 MyObject의 소유자여야 합니다. 소유자가 아닌 Address가 호출하면 트랜잭션이 실패합니다.


Object ID가 소유자인 경우

Object가 다른 Object에 의해 소유될 수도 있습니다. 이를 Child Object라고 합니다. Child Object는 트랜잭션 입력으로 직접 지정할 수 없고, 부모 Object를 통해 동적으로 접근해야 합니다. transfer::receive를 사용하여 부모 Object의 컨텍스트에서 Child Object를 수신하고 인증합니다.

public fun receive_child(parent: &mut UID, child: Receiving<MyObject>): MyObject {
    transfer::receive(parent, child)
}

CLI로 Object 조회

Sui CLI를 사용하면 특정 Address가 소유한 Object 목록을 조회할 수 있습니다.

# 현재 활성 Address 확인
$ export ADDR=$(sui client active-address)

# 해당 Address가 소유한 Object 목록 조회
$ sui client objects $ADDR

# 특정 Object의 상세 정보 조회
$ sui client object <OBJECT_ID>

Address-Owned Object 테스트

Sui는 sui::test_scenario Module을 통해 트랜잭션 시뮬레이션 기반의 테스트를 지원합니다. Address-Owned Object의 생성, 전송, 소유권 변경을 테스트하는 방법을 살펴보겠습니다.

앞서 작성한 ColorObject를 기반으로 테스트를 작성합니다.


Object 생성과 전송

Object를 생성한 뒤 특정 Address로 전송하고, 해당 Address가 Object를 소유하는지 확인합니다.

#[test]
fun test_create() {
    let sender = @0xA;

    let mut ts = ts::begin(sender);
    {
        ts.next_tx(sender);
        let obj = new(255, 0, 255, ts.ctx());
        transfer::transfer(obj, sender);
    };

    // sender가 ColorObject를 소유하는지 확인
    {
        ts.next_tx(sender);
        let obj: ColorObject = ts.take_from_sender();
        assert!(obj.red == 255);
        assert!(obj.green == 0);
        assert!(obj.blue == 255);
        ts::return_to_sender(&ts, obj);
    };

    ts.end();
}

take_from_sender를 사용하면 현재 트랜잭션 서명자가 소유한 Object를 가져올 수 있습니다. 테스트가 끝나면 return_to_sender로 Object를 반환해야 합니다.


소유권 변경 검증

Object를 다른 Address로 전송한 뒤, 소유권이 정상적으로 이전되었는지 검증합니다.

#[test]
fun test_transfer() {
    let sender = @0xA;
    let recipient = @0xB;

    let mut ts = ts::begin(sender);

    // ColorObject 생성 및 sender에게 전송
    {
        ts.next_tx(sender);
        let obj = new(255, 0, 255, ts.ctx());
        transfer::transfer(obj, sender);
    };

    // sender가 recipient에게 전송
    {
        ts.next_tx(sender);
        let obj: ColorObject = ts.take_from_sender();
        transfer::transfer(obj, recipient);
    };

    // sender는 더 이상 소유하지 않음
    {
        ts.next_tx(sender);
        assert!(!ts.has_most_recent_for_sender<ColorObject>());
    };

    // recipient이 소유함
    {
        ts.next_tx(recipient);
        assert!(ts.has_most_recent_for_sender<ColorObject>());
    };

    ts.end();
}

has_most_recent_for_sender로 해당 Address가 Object를 소유하는지 확인할 수 있습니다. 전송 후 sender는 소유권을 잃고, recipient이 새로운 소유자가 됩니다.


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