Documentation index for AI agents

GS2-Distributor

트랜잭션 처리 기능

GS2-Distributor 는 GS2 의 각 마이크로서비스를 넘나드는 트랜잭션 처리를 실현하기 위한 핵심 서비스입니다. 플레이어의 “아이템을 소비하여 보상을 얻는다”, “스태미나를 소비하여 퀘스트를 시작한다” 와 같은, 여러 마이크로서비스를 넘나드는 일련의 처리를 안전하게 실행하기 위한 기반을 제공합니다.

GS2 에서는 플레이어에게 불이익이 되는 조작을 《소비 액션》, 이익이 되는 조작을 《입수 액션》이라고 부릅니다.
GS2-Distributor 는 이러한 액션들을 모은 《트랜잭션》을 받아, 적절한 마이크로서비스로 전달·실행함으로써 게임 사이클을 구성하는 모든 처리를 일관된 형태로 다룰 수 있게 합니다.

트랜잭션의 구조에 대한 자세한 내용은 트랜잭션 을 참조하세요.

트랜잭션을 구성하는 액션

GS2 의 트랜잭션은 다음의 3가지 종류의 액션으로 구성됩니다.

graph LR
  Issue["트랜잭션 발행<br/>(스토어·퀘스트·미션 등)"] --> Verify["검증 액션"]
  Verify --> Consume["소비 액션"]
  Consume --> Acquire["입수 액션"]
  Acquire --> Done["완료"]

검증 액션 (VerifyAction)

트랜잭션 실행을 시작하기 전에, 소비 액션이나 입수 액션을 실행할 수 있는 상태인지를 사전에 체크하는 액션입니다.
예를 들어 “특정 아이템을 소지하고 있는지”, “랭크가 일정 값 이상인지”, “특정 퀘스트를 클리어했는지” 와 같은 조건을, 소비가 발생하기 전에 확인할 수 있습니다.

검증에 실패한 경우, 소비 액션·입수 액션은 실행되지 않습니다.

소비 액션 (ConsumeAction)

플레이어에게 불이익이 되는 처리를 나타내는 액션입니다.
아이템 소비, 통화 소비, 스태미나 소비, 횟수 제한 카운터 증가 등이 여기에 해당합니다.

소비 액션이 실행되면, 각 마이크로서비스는 “실행 완료” 를 증명하는 서명을 발행합니다. 이 서명은 다음 입수 액션 실행 시에 검증되며, 소비 액션을 모두 통과하지 않으면 입수 액션은 실행할 수 없는 구조입니다.

입수 액션 (AcquireAction)

플레이어에게 이익이 되는 처리를 나타내는 액션입니다.
아이템 입수, 통화 입수, 경험치 입수, 퀘스트 시작 처리 등이 여기에 해당합니다.

입수 액션은 관련된 모든 소비 액션이 정상적으로 종료했음을 나타내는 서명이 갖춰졌을 때에만 실행됩니다.

트랜잭션의 구조

GS2 의 트랜잭션은 “여러 개의 소비 액션 + 1개의 입수 액션” 이라는 구조로 발행됩니다.
트랜잭션은 GS2-Showcase / GS2-Quest / GS2-Mission 등 각 마이크로서비스의 트랜잭션 발행 API 응답으로 받으며, 게임 클라이언트는 취득한 트랜잭션을 GS2-Distributor 를 통해 실행합니다.

또한 GS2 SDK 에는 트랜잭션 실행을 자동화하는 구조가 내장되어 있어, 대부분의 경우 게임 측에서 트랜잭션을 의식적으로 실행할 필요는 없습니다.
발행 API 결과에 포함된 TransactionDomainWaitAsync 를 호출하는 것만으로, 소비 액션·입수 액션이 올바른 순서로 실행되어 결과를 취득할 수 있습니다.

DistributorModel

DistributorModel 은 네임스페이스 내에 등록하는 리소스 배포 설정 단위입니다.

DistributorModel 에는 다음을 설정할 수 있습니다.

  • inboxNamespaceId: 배포물이 오버플로우된 경우의 자동 전송처가 되는 GS2-Inbox 의 네임스페이스 GRN
  • whiteListTargetIds: 트랜잭션을 통해 실행을 허용하는 액션의 화이트리스트

여러 개의 DistributorModel 을 준비함으로써, 스토어용·퀘스트용·미션용 등 용도별로 다른 전송 규칙을 나누어 사용하는 것이 가능합니다.

배포물의 오버플로우 처리

입수 액션을 실행할 때, 플레이어의 보유 수 상한을 초과하는 등의 이유로 “그 자리에서 받을 수 없는” 상태가 되는 경우가 있습니다.
이러한 경우에 대비하여 DistributorModel 의 inboxNamespaceId 를 설정해 두면, 받지 못한 배포물을 GS2-Inbox 에 메시지로 자동 전송하여, 플레이어가 나중에 받을 수 있도록 대피시킬 수 있습니다.

이를 통해 보상을 받지 못하고 소실되는 상황을 방지할 수 있습니다.

graph TD
  Acquire["입수 액션 실행"] --> Check{"플레이어에게 부여 가능?"}
  Check -- Yes --> Granted["플레이어에게 부여"]
  Check -- No (상한 초과 등) --> Inbox["GS2-Inbox 로 전송"]
  Inbox --> Receive["플레이어는 나중에 수신"]

배치 리퀘스트

GS2-Distributor 는 여러 개의 GS2 API 호출을 하나의 리퀘스트로 모아 실행하는 《배치 리퀘스트》 기능을 제공합니다.

여러 서비스에 대한 요청을 한 번의 API 호출로 완료할 수 있으므로, 네트워크 왕복 횟수를 줄여 응답 시간을 개선할 수 있습니다. 게임 시작 시 여러 마이크로서비스로부터 초기 데이터를 한꺼번에 취득하는 경우 등에 특히 유용합니다.

배치 리퀘스트에 포함되는 각 리퀘스트는 독립적으로 처리되며, 각각에 대한 응답이 반환됩니다.

마스터 데이터 관리

마스터 데이터를 등록함으로써 마이크로서비스에서 이용 가능한 데이터나 동작을 설정할 수 있습니다.

마스터 데이터의 종류에는 다음이 있습니다.

  • DistributorModel: 트랜잭션의 전송 규칙과 오버플로우 시 전송처

다음은 마스터 데이터의 JSON 예입니다.

{
  "version": "2019-09-09",
  "distributorModels": [
    {
      "name": "default",
      "metadata": "default distributor",
      "inboxNamespaceId": "grn:gs2:{region}:{ownerId}:inbox:namespace-0001"
    }
  ]
}

마스터 데이터의 등록은 매니지먼트 콘솔에서 등록하는 것 외에도, GitHub 에서 데이터를 반영하거나, GS2-Deploy 를 사용해 CI 에서 등록하는 등의 워크플로우를 구성하는 것이 가능합니다.

트랜잭션 액션

GS2-Distributor 는 다른 서비스에서 발행된 트랜잭션을 실행하는 쪽의 서비스이며, 자신이 소비 액션·입수 액션을 발행하지는 않습니다.

구현 예제

트랜잭션 실행

GS2 SDK 에서는 각 서비스의 트랜잭션 발행 API 응답(TransactionDomain)에 대해 WaitAsync 를 호출함으로써 자동으로 트랜잭션이 GS2-Distributor 를 통해 실행됩니다.
아래는 GS2-Mission 의 보상 수취 처리를 예로 든 트랜잭션 실행 예입니다.

    var transaction = await gs2.Mission.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Complete(
        missionGroupName: "group-0001"
    ).ReceiveRewardsAsync(
        missionTaskName: "task-0001"
    );

    await transaction.WaitAsync();
    const auto Future = Gs2->Mission->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Complete(
        "group-0001" // missionGroupName
    )->ReceiveRewards(
        "task-0001" // missionTaskName
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;

    const auto Transaction = Future->GetTask().Result();
    const auto Future2 = Transaction->Wait();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
var transaction = await ez.mission.namespace_(
    "namespace-0001"
).me(game_session).complete("missionGroup-0001").receive_rewards(
    "missionTask-0001"
)
if transaction.error != null:
    push_error(str(transaction.error))
    return
var async_result = await transaction.result.wait()
if async_result.error != null:
    push_error(str(async_result.error))
    return

배치 리퀘스트 실행

여러 개의 GS2 API 호출을 한 번의 통신으로 모아서 실행합니다.

    var result = await gs2.Distributor.Namespace(
        namespaceName: "namespace-0001"
    ).BatchExecuteApiAsync(
        requestPayloads: new [] {
            new Gs2.Unity.Gs2Distributor.Model.EzBatchRequestPayload
            {
                RequestId = "1",
                Service = "inventory",
                MethodName = "describe_inventories",
                Parameter = "{\"namespaceName\":\"namespace-0001\"}",
            },
            new Gs2.Unity.Gs2Distributor.Model.EzBatchRequestPayload
            {
                RequestId = "2",
                Service = "experience",
                MethodName = "describe_statuses",
                Parameter = "{\"namespaceName\":\"namespace-0001\"}",
            },
        }
    );
    const auto Future = Gs2->Distributor->Namespace(
        "namespace-0001" // namespaceName
    )->BatchExecuteApi(
        []
        {
            const auto v = MakeShared<TArray<Gs2::UE5::Distributor::Model::FEzBatchRequestPayloadPtr>>();
            v->Add(MakeShared<Gs2::UE5::Distributor::Model::FEzBatchRequestPayload>()
                ->WithRequestId(TOptional<FString>("1"))
                ->WithService(TOptional<FString>("inventory"))
                ->WithMethodName(TOptional<FString>("describe_inventories"))
                ->WithParameter(TOptional<FString>("{\"namespaceName\":\"namespace-0001\"}")));
            v->Add(MakeShared<Gs2::UE5::Distributor::Model::FEzBatchRequestPayload>()
                ->WithRequestId(TOptional<FString>("2"))
                ->WithService(TOptional<FString>("experience"))
                ->WithMethodName(TOptional<FString>("describe_statuses"))
                ->WithParameter(TOptional<FString>("{\"namespaceName\":\"namespace-0001\"}")));
            return v;
        }()
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
var domain = ez.distributor.namespace_(
        null
    )

var async_result = await domain.batch_execute_api(
    [
        Gs2DistributorEzBatchRequestPayload.new()
            .with_service("inventory")
            .with_method_name("describeSimpleItems")
            .with_parameter("{\"namespaceName\": \"namespace-0001\", \"inventoryName\": \"inventory-0001\", \"accessToken\": \"accessToken-0001\"}"),
        Gs2DistributorEzBatchRequestPayload.new()
            .with_service("exchange")
            .with_method_name("describeRateModels")
            .with_parameter("{\"namespaceName\": \"namespace-0001\"}"),
    ] # request_payloads
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

마스터 데이터 고정 (Freeze)

트랜잭션이 발행된 후에 마스터 데이터가 업데이트되면, 발행된 트랜잭션과 현재 마스터 데이터 사이에 모순이 생길 가능성이 있습니다.
GS2-Distributor 의 FreezeMasterData 를 호출함으로써, 로그인 중인 플레이어에 대해 사용할 마스터 데이터의 버전을 고정하여, 게임 플레이 중 마스터 데이터 전환으로 인한 불일치를 회피할 수 있습니다.

    await gs2.Distributor.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).FreezeMasterDataAsync(
    );
    // Unreal Engine 용 SDK 에는 FreezeMasterData 가 제공되지 않습니다.
    // 매니지먼트 콘솔 / GS2 CLI / 각종 언어용 일반 SDK (C#/Go/Python/TypeScript/PHP/Java) 를 이용해 주세요.
var async_result = await Gs2DistributorClient.new(connection).freeze_master_data(
    game_session, "namespace-0001"
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

상세 레퍼런스