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

# GS2-Datastore

바이너리 데이터 스토리지 기능




GS2-Datastore를 이용하면 임의의 바이너리 데이터를 서버에 저장할 수 있습니다.

업로드한 데이터에는 접근 권한을 설정할 수 있으며, `public`(전체 공개), `protected`(지정한 사용자ID에게만 공개: 최대 100건), `private`(본인에게만 공개) 중에서 선택합니다.

GS2-Datastore는 UGC나 레이싱 게임의 고스트 데이터와 같은 데이터를 업로드하는 것을 주된 목적으로 하며, 플레이어의 사용자 데이터를 저장하는 데에는 반드시 적합하다고 할 수 없습니다.

왜냐하면 소지품의 소지 수량을 바이너리 데이터로 저장할 경우, 세이브 데이터나 앱 본체의 변조로 아이템을 복제하거나 입수량을 부정하게 늘린 개조 앱으로 플레이하는 것을 허용하게 되기 때문입니다.
소지품의 소지 수량이라면 GS2-Inventory와 같은 전용 마이크로서비스를 이용함으로써 이러한 부정 행위를 막을 수 있습니다.

사용자 데이터 중에서도 설정값 등 변조되더라도 게임 밸런스에 영향을 주지 않는 데이터를 저장하는 것을 부정하는 것은 아닙니다.

## 유스케이스

GS2-Datastore가 상정하는 대표적인 유스케이스는 다음과 같습니다.

- 사용자가 게시한 스크린샷이나 리플레이 등 UGC 콘텐츠의 저장
- 레이싱 게임에서의 고스트 데이터 공유
- 포토 모드로 촬영한 이미지의 클라우드 공유
- 게임 설정 등 변조되더라도 게임 밸런스에 영향을 주지 않는 사용자 데이터의 저장
- 스테이지의 커스텀 맵 데이터 공개·공유

## 접근 스코프

데이터 오브젝트에는 3가지 접근 스코프가 있으며, 용도에 따라 구분하여 사용합니다.

| 스코프 | 설명 | 주요 용도 |
| --- | --- | --- |
| `public` | 모든 플레이어가 다운로드 가능 | UGC 콘텐츠 공개, 고스트 공유 |
| `protected` | `allowUserIds`에 지정한 사용자만 다운로드 가능(최대 100건) | 친구에게만 공유 |
| `private` | 업로드한 사용자 본인만 다운로드 가능 | 개인 세이브 데이터, 설정값 |

스코프와 허용 사용자는 `UpdateDataObject`로 나중에 변경하는 것도 가능합니다.

## 아키텍처

GS2-Datastore는 바이너리 데이터의 저장 위치 등을 기록한 메타데이터를 관리하며, 실제 바이너리 데이터의 저장에는 외부 클라우드 스토리지를 이용하고 있습니다.
그렇기 때문에 업로드·다운로드 처리는 여러 단계를 필요로 합니다.

이 처리 흐름은 게임 엔진용 SDK에서는 래핑된 고수준 API가 제공되므로 신경 쓸 필요가 없지만,
각종 프로그래밍 언어용 SDK에는 고수준 API가 제공되지 않으므로 이용자가 직접 여러 단계를 처리해야 합니다.

### 업로드 프로세스

```mermaid
sequenceDiagram
  actor Player as 플레이어
  participant Namespace as GS2-Datastore#Namespace
  participant Storage as Cloud Storage
  Player->>Namespace: PrepareUpload
  Namespace-->>Player: Cloud Storage URL
  Player->>Storage: Upload Payload
  Storage-->>Player: OK
  Player->>Namespace: DoneUpload
  Namespace->>Storage: Check exists
  Namespace-->>Player: OK
```

### 다운로드 프로세스

```mermaid
sequenceDiagram
  actor Player as 플레이어
  participant Namespace as GS2-Datastore#Namespace
  participant Storage as Cloud Storage
  Player->>Namespace: PrepareDownload
  Namespace-->>Player: Cloud Storage URL
  Player->>Storage: Download
  Storage-->>Player: Payload
```

### 업로드 처리 중의 다운로드

GS2-Datastore는 이미 업로드한 데이터를 갱신할 수 있습니다.
Prepare ReUpload를 호출한 후부터 Done Upload를 호출할 때까지는 갱신 전의 기존 파일을 다운로드할 수 있는 상태가 유지되며, 어중간한 데이터가 다운로드되는 일은 없습니다.

### 업로드 데이터의 과거 버전 취득

GS2-Datastore에서는 과거 30일분의 과거 버전에 접근할 수 있도록 되어 있습니다.
데이터 오브젝트의 갱신 이력(DataObjectHistory)을 취득함으로써 과거 각 세대의 ID를 취득할 수 있으며, 그 세대 ID를 지정하여 데이터를 다운로드하는 것이 가능합니다.

이는 삭제된 데이터에도 적용되며, 삭제 요청 후 30일 후에 실제로 삭제됩니다.
단, 법적 요건에 따라 데이터 삭제를 수행한 경우에는 이 조건에 해당하지 않을 수 있습니다.

### 데이터 크기와 상태

1개의 데이터 오브젝트의 최대 크기는 10MB입니다. 업로드 중에는 `UPLOADING`, 완료되면 `ACTIVE`, 삭제 요청 후에는 `DELETED`로 전이합니다. `DELETED` 상태의 데이터는 30일 이내라면 `restoreDataObject`로 복원할 수 있습니다.

```mermaid
stateDiagram-v2
  [*] --> UPLOADING: PrepareUpload
  UPLOADING --> ACTIVE: DoneUpload
  ACTIVE --> UPLOADING: PrepareReUpload
  ACTIVE --> DELETED: DeleteDataObject
  DELETED --> ACTIVE: RestoreDataObject (30일 이내)
  DELETED --> [*]: 30일 경과
```

### 데이터 오브젝트의 주요 속성

| 속성 | 설명 |
| --- | --- |
| `dataObjectId` | 데이터 오브젝트의 고유 ID(GRN) |
| `name` | 데이터 오브젝트 이름. 사용자마다 고유 |
| `userId` | 업로드한 사용자의 ID |
| `scope` | 접근 스코프 (`public` / `protected` / `private`) |
| `allowUserIds` | `protected` 스코프에서 참조를 허용하는 사용자ID 목록 |
| `platform` | 업로드에 사용된 플랫폼 정보 |
| `status` | 데이터의 상태 (`UPLOADING` / `ACTIVE` / `DELETED`) |
| `generation` | 현재 세대의 식별자 |
| `previousGeneration` | 1개 전 세대의 식별자 |

### 스크립트 트리거

데이터 오브젝트의 업로드 완료 보고 전후에 GS2-Script를 호출하는 이벤트 트리거를 설정할 수 있습니다. 동기 실행으로 완료 보고를 거부하거나, 비동기 실행으로 Amazon EventBridge를 이용한 외부 연동도 가능합니다.

설정할 수 있는 주요 이벤트 트리거와 스크립트 설정명은 다음과 같습니다.

- `doneUploadScript`(완료 통지: `doneUploadDone`): 업로드 완료 보고 전후

동기 실행 스크립트로 업로드를 거부함으로써, 이미지의 내용을 별도 서비스에서 검사하여 부적절한 콘텐츠라면 등록을 거부하는 운영이 가능합니다.

## 구현 예제

### 데이터 업로드

데이터 업로드는 PrepareUpload → 클라우드 스토리지로의 PUT → DoneUpload의 3단계로 구성되지만, 게임 엔진용 SDK에서는 `UploadAsync` 호출 1회로 완결됩니다.



**Unity**
```csharp

    var result = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).UploadAsync(
        name: "dataObject-0001",
        scope: "public",
        data: data
    );

    var item = await result.ModelAsync();
    var dataObjectId = item.DataObjectId;
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto Future = Domain->Upload(
        "dataObject-0001", // name
        data, // data
        "public" // scope
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var client = Gs2DatastoreClient.new(connection)
var async_result = await client.upload(
    game_session, "namespace-0001", "public", null, data, "dataObject-0001"
)
if async_result.error != null:
    push_error(str(async_result.error))
    return
var item = async_result.result

```


### 기존 데이터의 재업로드

같은 이름의 데이터 오브젝트에 대해 새로운 바이너리를 기록할 때는 재업로드를 사용합니다.
재업로드 중에도 `DoneUpload`가 호출될 때까지는 갱신 전의 기존 데이터를 가져올 수 있습니다.



**Unity**
```csharp

    var domain = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).ReUploadAsync(
        data: newData
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->ReUpload(
        NewData
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var client = Gs2DatastoreClient.new(connection)
var data_object = ez.datastore.namespace_(
    "namespace-0001"
).me(game_session).data_object("dataObject-0001")
var async_result = await client.re_upload(
    game_session, "namespace-0001", data_object._domain, new_data
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

```


### 데이터 다운로드(데이터 오브젝트 ID 지정)



**Unity**
```csharp

    var binary = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DownloadAsync(
        dataObjectId: dataObjectId
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    );
    const auto Future = Domain->Download(
        dataObjectId // dataObjectId
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var client = Gs2DatastoreClient.new(connection)
var async_result = await client.download(
    game_session, "namespace-0001", data_object_id
)
if async_result.error != null:
    push_error(str(async_result.error))
    return
var binary = async_result.result

```


### 데이터 다운로드(사용자ID와 데이터 오브젝트 이름 지정)



**Unity**
```csharp

    var binary = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).User(
        userId: "user-0001"
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).DownloadByUserIdAndDataObjectNameAsync(
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->User(
        "user-0001" // userId
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->DownloadByUserIdAndDataObjectName(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var domain = ez.datastore.namespace_(
        "namespace-0001"
    ).user(
        "user-0001"
    ).data_object(
        "dataObject-0001"
    )

var async_result = await domain.prepare_download_by_user_id_and_data_object_name(
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 자신이 업로드한 데이터의 다운로드(데이터 오브젝트 이름 지정)



**Unity**
```csharp

    var binary = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).DownloadOwnAsync(
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->DownloadOwn(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var client = Gs2DatastoreClient.new(connection)
var async_result = await client.download_own_data(
    game_session, "namespace-0001", "dataObject-0001"
)
if async_result.error != null:
    push_error(str(async_result.error))
    return
var binary = async_result.result

```


### 자신이 업로드한 데이터의 목록 취득



**Unity**
```csharp

    var items = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObjectsAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto It = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObjects();
    TArray<Gs2::UE5::Datastore::Model::FEzDataObjectPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.datastore.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).data_objects(
    )

var async_result = await iterator.load()
if async_result.error != null:
    # 에러를 처리
    push_error(str(async_result.error))
    return

var items = async_result.result

```


### 데이터 오브젝트의 접근 스코프 변경

`UpdateDataObject`를 이용하면 접근 스코프나 허용 사용자 목록을 나중에 변경할 수 있습니다.
예를 들어, 처음에는 `private`로 생성하고 공유 준비가 완료된 시점에 `public`으로 전환하는 운영이 가능합니다.



**Unity**
```csharp

    var domain = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).UpdateDataObjectAsync(
        scope: "protected",
        allowUserIds: new [] { "user-0002", "user-0003" }
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->UpdateDataObject(
        "protected", // scope
        { "user-0002", "user-0003" } // allowUserIds
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var domain = ez.datastore.namespace_(
        "namespace-0001"
    ).me(game_session).data_object(
        "dataObject-0001"
    )

var async_result = await domain.update_data_object(
    "public", # scope
    null # allow_user_ids
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 데이터 오브젝트 삭제

삭제 요청 후 30일 이내라면 `RestoreDataObject`로 복원할 수 있습니다.



**Unity**
```csharp

    await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).DeleteDataObjectAsync(
    );
```
**Unreal Engine**
```cpp

    const auto Domain = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    );
    const auto Future = Domain->DeleteDataObject(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError()) return false;
```
**Godot**
```gdscript

var domain = ez.datastore.namespace_(
        "namespace-0001"
    ).me(game_session).data_object(
        "dataObject-0001"
    )

var async_result = await domain.delete_data_object(
)
if async_result.error != null:
    if async_result.error.type == "InvalidStatusException":
        # DataObject가 조작 가능한 상태가 아닙니다
        pass
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 데이터 오브젝트의 갱신 이력 취득

과거 세대를 취득함으로써 롤백이나 과거 리플레이 열람 등을 구현할 수 있습니다.



**Unity**
```csharp

    var items = await gs2.Datastore.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).DataObject(
        dataObjectName: "dataObject-0001"
    ).DataObjectHistoriesAsync(
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto It = Gs2->Datastore->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->DataObject(
        "dataObject-0001" // dataObjectName
    )->DataObjectHistories();
    TArray<Gs2::UE5::Datastore::Model::FEzDataObjectHistoryPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.datastore.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).data_object(
        "dataObject-0001"
    ).data_object_histories(
    )

var async_result = await iterator.load()
if async_result.error != null:
    # 에러를 처리
    push_error(str(async_result.error))
    return

var items = async_result.result

```


## 오래된 세대의 데이터에 대한 접근 제한

데이터를 다운로드할 때는 세대ID를 지정하여 구체적인 파일을 특정합니다. 세대ID를 다운로드 요청에 추가함으로써 목록을 조회한 시점의 데이터를 확실히 다운로드하는 것을 보장할 수 있습니다.

단, 언제까지나 오래된 세대의 데이터에 접근할 수 있는 것이 바람직하지 않은 경우도 있습니다. 그렇기 때문에 데이터 소유자 이외에는 갱신 후 60분 이내이면서 1개 전 세대에 한해서만 오래된 세대의 데이터 다운로드를 허용하는 옵션이 존재합니다.

## 상세 레퍼런스

[GS2-Datastore API 레퍼런스](../../api_reference/datastore)



