> For the complete documentation index, see [llms.txt](/llms.txt)

# GS2-Distributor

트랜잭션 처리 기능




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

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

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

## 트랜잭션을 구성하는 액션

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

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

### 검증 액션 (VerifyAction)

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

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

### 소비 액션 (ConsumeAction)

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

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

### 입수 액션 (AcquireAction)

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

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

## 트랜잭션의 구조

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

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

## DistributorModel

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

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

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

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

### 배포물의 오버플로우 처리

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

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

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

## 배치 리퀘스트

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

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

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

## 마스터 데이터 관리

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

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

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

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

```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 를 통해 실행됩니다.<br>
아래는 GS2-Mission 의 보상 수취 처리를 예로 든 트랜잭션 실행 예입니다.



**Unity**
```csharp

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

    await transaction.WaitAsync();
```
**Unreal Engine**
```cpp

    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;
```
**Godot**
```gdscript

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 호출을 한 번의 통신으로 모아서 실행합니다.



**Unity**
```csharp

    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\"}",
            },
        }
    );
```
**Unreal Engine**
```cpp

    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;
```
**Godot**
```gdscript

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)

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



**Unity**
```csharp

    await gs2.Distributor.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).FreezeMasterDataAsync(
    );
```
**Unreal Engine**
```cpp

    // Unreal Engine 용 SDK 에는 FreezeMasterData 가 제공되지 않습니다.
    // 매니지먼트 콘솔 / GS2 CLI / 각종 언어용 일반 SDK (C#/Go/Python/TypeScript/PHP/Java) 를 이용해 주세요.
```
**Godot**
```gdscript

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

```


## 상세 레퍼런스

[GS2-Distributor API 레퍼런스](../../api_reference/distributor)



