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

# 스탬프 시트

Game Server Services의 트랜잭션 시스템인 스탬프 시트에 대한 설명



GS2에서는 각 서비스 간을 연계시키는 트랜잭션 구조로서 《스탬프 시트》라는 구조를 사용합니다.

GS2 내의 API 중, 플레이어에게 불이익이 되는 조작을 《소비 액션》이라고 부르고, 반대로 플레이어에게 이익이 되는 조작을 《입수 액션》이라고 부릅니다.

게임 내 상점에서 《1000 젬을 사용》하여 《가챠를 10번 뽑는》처리가 있다고 합시다.
이 경우, 《1000 젬을 사용》이 소비 액션이고, 《가챠를 10번 뽑는》것을 입수 액션으로 볼 수 있습니다.

조금 더 예시를 나열해 보겠습니다.

- 《메시지를 읽음 처리》하는 소비 액션을 실행하고 《메시지에 첨부된 젬 100개를 수령》하는 입수 액션을 실행한다
- 《스태미나를 10 소비》하는 소비 액션을 실행하고 《퀘스트 1을 시작 상태로 만드는》 입수 액션을 실행한다
- 《퀘스트 1의 시작 상태를 삭제》하는 소비 액션을 실행하고 《퀘스트 1의 클리어 보상을 입수》하는 입수 액션을 실행한다
- 《현재 시각으로 최종 방치 보상 수령 시간을 갱신》하는 소비 액션을 실행하고 《방치 시간에 따른 보상을 입수》하는 입수 액션을 실행한다
- 《매일 초기화되는 횟수 제한 카운터를 1 증가》하는 소비 액션을 실행하고 《하루 1회만 받을 수 있는 아이템을 입수》하는 입수 액션을 실행한다

이처럼 GS2에서는 모든 게임 사이클이 무언가를 소비하고, 무언가를 얻는 것으로 표현됩니다.

## 스탬프 시트의 구조

스탬프 시트는 여러 개의 《소비 액션》과 1개의 《입수 액션》으로 구성되며,
스탬프 시트의 발행도 상점 기능이나 퀘스트 기능과 같은 GS2의 마이크로서비스가 수행합니다.
발행되는 스탬프 시트의 내용은 상점 기능에 등록한 상품 마스터 데이터 등을 바탕으로 결정됩니다.

스탬프 시트의 실행은
```
《소비 액션》의 실행 -> 《입수 액션》의 실행
```
순서로 처리됩니다.

이 순서로 처리함으로써, 부정하게 《입수 액션》이 여러 번 실행되는 것을 방지하고 있습니다.

《소비 액션》을 실행하면, GS2의 각 기능을 제공하는 마이크로서비스는 실행 완료를 증명하는 서명을 발행합니다.
《입수 액션》을 실행할 때는, 모든 《소비 액션》에서 받은 서명을 함께 전송합니다.
《입수 액션》을 실행하기 전에 서명 검증을 수행하여, 모든 것이 실행 완료되었음을 확인한 후에 《입수 액션》을 실행합니다.

## 서비스 디스커버리

스탬프 시트의 《소비 액션》이나 《입수 액션》의 내용에 따라, 적절한 마이크로서비스에 처리를 위임해야 합니다.
하지만 GS2의 기능은 나날이 추가되고 있으며, 《소비 액션》이나 《입수 액션》의 종류는 계속 늘어나고 있어, 이러한 분기 처리를 유지보수하는 것은 힘든 일입니다.

그래서 GS2에서는 GS2-Distributor라는, 받은 스탬프 시트를 적절한 마이크로서비스로 전달하는 마이크로서비스를 제공하고 있습니다.
GS2-Distributor에 스탬프 시트를 전송함으로써, 스탬프 시트의 《소비 액션》이나 《입수 액션》의 내용에 따라 적절한 마이크로서비스로 전달하는 역할을 합니다.

## 중복 실행 방지

스탬프 시트는 중복 실행할 수 없도록 설계되어 있습니다.
정확히 말하면, GS2의 모든 《소비 액션》이나 《입수 액션》 API에는 중복 실행 방지 구조가 마련되어 있습니다.

《소비 액션》이나 《입수 액션》의 API는 《Duplication Avoider》 파라미터를 받을 수 있게 되어 있습니다.
여기에 값을 지정하고 API 처리가 정상 종료되면, 응답 내용이 GS2에 의해 일정 기간 저장됩니다.
동일한 요청 페이로드에 《Duplication Avoider》가 지정되어 있고, 과거에 동일한 《Duplication Avoider》 값으로 처리를 실행한 적이 있는 경우에는, 그 결과를 정상 처리 완료로 응답하고 실제로는 처리를 수행하지 않게 되어 있습니다.
그리고 스탬프 시트에 의한 《소비 액션》이나 《입수 액션》의 실행에서는, 《Duplication Avoider》에 스탬프 시트 고유의 ID인 《트랜잭션ID》를 지정하게 되어 있습니다.

그럼, 중복 실행 방지 구조가 어떤 과정으로 실행되는지 순서대로 살펴보겠습니다.
먼저, 다음과 같은 스탬프 시트가 존재한다고 가정합니다.

- **소비 액션**
  - 아이템을 1개 소비
  - 횟수 제한 카운터를 1 증가
- **입수 액션**
  - 젬을 10개 입수

이 스탬프 시트를 실행하는 경우, 먼저 《아이템을 1개 소비》를 실행합니다.

1. **《아이템을 1개 소비》 실행**
   - 아이템이 1개 소비됩니다.
   - 서버 측에 "트랜잭션ID: XXX로 아이템 소비 성공"이라는 기록이 남습니다.

2. **《횟수 제한 카운터를 1 증가》 실행**
   - 여기서 네트워크 오류나 서버 오류가 발생하여 처리가 중단되었다고 합시다.
   - 이 시점에서 아이템은 이미 소비된 상태로 멈춰 있어, 플레이어에게는 불이익한 상태입니다.

3. **스탬프 시트 재시도**
   - 클라이언트 또는 서버 사이드의 자동 실행에 의해, 처음부터 다시 실행됩니다.
   - 다시 **《아이템을 1개 소비》**를 실행하려 하지만, 여기서 **Duplication Avoider**가 작동합니다.
   - 이미 성공 기록이 있으므로, 실제로는 소비 처리를 수행하지 않고, 이전 성공 응답을 그대로 반환합니다.

4. **《횟수 제한 카운터를 1 증가》 재시도**
   - 이전에 실패했던 부분이지만, 이번에는 정상적으로 성공했다고 합시다.
   - 서버 측에 성공 기록이 남습니다.

5. **《젬을 10개 입수》 실행**
   - 마지막으로 입수 액션을 실행하여 젬을 지급하고 완료됩니다.

이처럼 스탬프 시트를 실행하는 쪽에서는 "어디까지 진행되었는지"를 세세하게 관리할 필요가 없으며, 실패하면 단순히 "처음부터 다시 하기"만 하면, Duplication Avoider에 의해 불일치 없이 최종 상태까지 도달할 수 있게 되어 있습니다.

## 스탬프 시트 재시도의 자동화

스탬프 시트 실행에 실패한 경우에는 재시도하면 된다고 했지만, 말은 쉬워도 실제로 이를 철저히 지키는 것은 어렵습니다.
그래서 GS2에서 스탬프 시트를 발행하는 마이크로서비스는 《스탬프 시트 자동 실행》이라는 옵션을 가지고 있습니다.
이 옵션을 활성화하면, 어떤 이유로든 재시도가 필요한 상황이 발생했을 때 서버 사이드에서 자동으로 재시도가 수행됩니다.

《소비 액션에서 소비할 아이템의 잔량이 부족함》과 같이 재시도로 해결되지 않는 경우에는, 재시도가 수행되지 않을 수 있습니다.

## 여러 개의 입수 액션

중복 실행 방지 구조가 있다면, 입수 액션이 여러 개 있어도 되지 않을까 생각할 수 있습니다. 그것은 기본적으로 맞습니다.
하지만 주의해야 할 점은, 중복 실행 방지를 위한 응답 내용의 보존 기간이 무기한이 아니라는 것입니다.
응답 내용의 보존 기간이 지나면, 과거에 발행된 스탬프 시트를 다시 실행할 수 있게 되어 버립니다.
이 경우에도 소비 액션의 실행은 필요하지만, 소비 액션을 실행했다고 해도 다시 실행되는 것 자체가 바람직하지 않은 경우도 있습니다.
이러한 문제에 대처하기 위해, 스탬프 시트 실행이 끝났을 때 스탬프 시트 자체를 무효화하는 프로세스를 포함시켜, 응답 내용의 보존 기간이 지나더라도 다시 실행할 수 없도록 하고 있습니다.

여기서 《입수 액션》이 여러 개 있으면, 언제 스탬프 시트를 무효화해야 할지가 어려워집니다.
그래서 스탬프 시트에서는 《입수 액션》을 1개만 설정할 수 있도록 설계되어 있습니다.

하지만 현실적으로 게임을 만들다 보면, 입수 액션이 항상 1개로 끝나는 것은 아닙니다.
가장 일반적인 게임 사이클을 구성하는 퀘스트 기능조차, 《경험치 지급》과 《드롭 아이템 입수》라는 2개의 입수 액션이 동시에 발생합니다.

이러한 경우에 대응하기 위해, GS2에서는 잡 큐를 이용하고 있습니다.
잡 큐는 플레이어별로 마련되는 지연 실행용 큐입니다.

스탬프 시트에 《입수 액션》을 여러 개 설정하고 싶은 상황에서는, 《잡 큐에 입수 액션을 실행하는 잡을 여러 개 등록》하는 하나의 《입수 액션》을 설정합니다.
이 프로세스는 자동화되어 있으며, 퀘스트의 마스터 데이터에서 보상을 설정할 때는 여러 개의 《입수 액션》을 설정할 수 있게 되어 있습니다.
그리고 스탬프 시트를 발행할 때 이를 《입수 액션을 실행하는 잡을 여러 개 등록》하는 형태로 변형하여 스탬프 시트를 발행하고 있습니다.

## 스탬프 시트의 실행은 비동기 처리

지금까지의 설명으로 알 수 있듯이, 스탬프 시트의 완료를 기다리는 것은 매우 어렵습니다.
GS2는 다수의 마이크로서비스를 제공하고 있지만, 그중 하나가 정지하더라도 서비스 전체가 정지하지 않도록 설계되어 있습니다.
하지만 장애가 발생한 시점에는 스탬프 시트나 잡 큐의 실행이 지연되어, 결과 반영이 즉시 완료된다는 보장은 없습니다.

한편, 장애가 복구되면 여러분이 아무것도 하지 않아도 스탬프 시트나 잡 큐의 재시도에 의해 처리가 흐르기 시작하여, 결국 정상화됩니다.
지금까지의 개발 스타일과 다르기 때문에 당황할 수도 있습니다.
하지만 부분적인 마이크로서비스 장애로 인해 일시적으로 불일치 상황이 발생하더라도, 아무것도 신경 쓰지 않아도 장애 복구 후 언젠가는 정상화된다는 전체적인 이점을 취하여 GS2에서는 이러한 설계를 채택하고 있습니다.

## 스탬프 시트 발행 시 파라미터 설정

각 서비스로의 요청에는 어느 사용자의 리소스를 조작할지에 대한 정보가 필요하지만,
게임 내 상점이나 퀘스트의 마스터 데이터에는 미리 사용자ID를 정적으로 지정할 수 없습니다.
그래서 스탬프 시트의 요청에 변수를 삽입할 수 있습니다.

마스터 데이터의 액션 요청에 #{userId}라는 플레이스홀더 문자열을 설정하면,
그 부분은 스탬프 시트를 발행할 때 스탬프 시트 발행을 수행한 사용자의 사용자ID로 치환됩니다.

### Config

스탬프 시트의 발행 요청에는 Config(EzConfig)라는 파라미터를 전달할 수 있게 되어 있습니다.
Config(EzConfig)는 키-값 형식으로, 전달한 파라미터로 `#{Config에서 지정한 키 값}`이라는 플레이스홀더 문자열을 치환할 수 있습니다.

**지갑에 잔액을 추가하는 스탬프 시트 예시**
```json
{
  "name": "currency-120-jpy",
  "metadata": "price: 120 currencyCount: 50",
  "consumeActions": [
    {
      "action": "Gs2Money:RecordReceipt",
      "request": "{\"namespaceName\": \"money-0001\", \"contentsId\": \"io.gs2.sample.currency120\", \"userId\": \"#{userId}\", \"receipt\": \"#{receipt}\"}"
    }
  ],
  "acquireActions": [
    {
      "action": "Gs2Money:DepositByUserId",
      "request": "{\"namespaceName\": \"money-0001\", \"userId\": \"#{userId}\", \"slot\": \"#{slot}\", \"price\": 120, \"count\": 50}"
    }
  ]
}
```

**Unity에서 Config 값을 설정하는 예시**
```csharp

    var result = await gs2.Showcase.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Showcase(
        showcaseName: "showcase-0001"
    ).BuyAsync(
        displayItemId: "display-item-0001",
        quantity: 1,
        config: new [] {
           new EzConfig
           {
               Key = "slot",
               Value = Slot.ToString(),
           },
           new EzConfig
           {
               Key = "receipt",
               Value = receipt,
           },
       }
    );
```

### 스탬프 시트 《소비 액션》의 실행 결과

액션 요청 기술 내용에, 예를 들어 `%{Gs2Money:WithdrawByUserId.price}`라는 플레이스홀더 문자열을 설정하면,
그 부분은 《소비 액션》의 실행 결과로 치환되어 변수로 이용할 수 있습니다.
예시로 든 케이스에서는, 실행한 《소비 액션》 중 Gs2Money:WithdrawByUserId의 실행 결과를 참조하여, 반환값의 price를 값으로 사용합니다.
자식 요소를 참조하는 경우에는 `%{Gs2Money:WithdrawByUserId.item.paid}`처럼 점(.)으로 연결하여 참조할 수 있습니다.

동일한 액션이 《소비 액션》으로 여러 번 등록되어 있는 경우 채택되는 값은 정해져 있지 않습니다.




