[공지] 게임 데이터 유실(초기화/롤백) 방지, 대응 가이드

안녕하세요, 게임 서버 뒤끝입니다.

게임 데이터 초기화/롤백 현상이 발생할 수 있는 케이스들과 방지 + 대응 방법을 안내드립니다.
특히 장시간 게임을 이용할 수 있는 '방치형 RPG’게임에서는 본 가이드의 내용을 꼭 적용해주시기 바랍니다.

  • 데이터 유실은 크게 '초기화’와 ‘롤백’, 두가지 유형으로 구분할 수 있습니다.
  1. 초기화 : 정상 로그인 후 과거의 데이터를 불러오지 못하고 로컬의 초기값을 덮어쓰는 경우
  2. 롤백 : 정상 로그인 후 플레이 중, 토큰이 만료되어 더 이상 저장할 수 없는 경우

1) 기존 플레이 데이터가 존재하지만, 게임 데이터 불러오기 요청 실패 후 새로운 row 착오 insert (초기화)

게임 데이터 불러오기시 네트워크 연결상태가 좋지않거나 서버에 부하가 발생하였을 때에는 공통에러가 리턴될 수 있습니다.

이 때 유저의 데이터 row가 없는 것으로 잘못 판단, 새로운 row 생성시 게임 데이터 초기화 현상이 발생할 수 있습니다.

유저의 데이터 row가 없는 것을 확신할 수 있는 조건은 데이터 불러오기 요청이 성공하고 rows가 0으로 반환된 경우입니다.

게임 데이터 불러오기 함수 호출 시 (성공 + rows의 갯수가 0일때만) 새로운 row를 insert하는 로직을 구성해주시기 바랍니다.

추천 로직

  • 새로운 데이터 row 생성의 조건 : (게임 데이터 불러오기 요청 성공) && (리턴되는 rows의 갯수가 0개) and 조건으로 확인
  • 게임 데이터 불러오기 함수의 성공여부를 확인 : 실패시 최대 3번까지 복구 시도

Sample Code : 데이터 불러오기 요청 실패시 새로운 row를 잘못 생성하는 이슈

int userDataCallCount = 0;
string userDataIndate = string.Empty;

public void GetUserData()
{
    userDataCallCount++;
    if (userDataCallCount > 3)
    {
        //3번 시도 후 안될 경우 이상 현상으로 분류 게임 재시작 권장
        //ShowUI("데이터 불러오기중 오류가 발생했습니다. 타이틀화면으로 돌아갑니다.");
        //SceneManager.ChangeScene("TitleScene");
        return;
    }
    SendQueue.Enqueue(Backend.GameData.GetMyData, "UserData", new Where(), getCallback =>
    {
        if (getCallback.IsSuccess())
        {
            JsonData json = getCallback.FlattenRows();
            if (json.Count <= 0)
            {
                Debug.Log("데이터가 존재하지 않습니다");
                SendQueue.Enqueue(Backend.GameData.Insert, "UserData", insertCallback =>
               {
                   //데이터 생성
                   if (insertCallback.IsSuccess())
                   {
                       userDataIndate = insertCallback.GetInDate(); // 삽입한 row의 indate가져오기
                       InitUserData(); // UserData 초기화
                   }
               });
            }
            else
            {
                userDataIndate = json[0]["inDate"].ToString();
                //SetUserData(json[0]); // [임의의 함수] 서버에서 불러온 json 데이터를 변수에 저장해주는 함수
            }
        }
        else
        {
            string errorMessage = getCallback.GetMessage();

            Param param = new Param();
            param.Add("errorMessage", errorMessage);

            Backend.GameLog.InsertLog("GetUserData", param);

            if (errorMessage.Contains("maintenance"))
            {
                //서버 점검중
            }
            else if (errorMessage.Contains("accessToken"))
            {
                //액세스 토큰 재시도
            }
            else
            {
                GetUserData(); // 같은 함수 재호출(3번까지 되도록 limit 필요)
            }
        }
    });
}

2) 로그인 후 24시간이 경과하여 bad accessToken 오류 발생 (롤백)

로그인 이후 24시간이 지나면 액세스토큰이 만료되어 뒤끝 서버 이용 요청이 실패하게 됩니다.
뒤끝 accessToken 가이드

이 상황에서 게임 플레이가 지속되면, 게임 클라이언트에서는 게임이 정상적으로 실행되지만 데이터 저장요청등은 실패하기 때문에 게임 플레이어는 데이터 롤백이 발생한 것으로 인식하게 됩니다.

따라서 게임을 정상 이용중인 경우 24시간이 경과하기 전에 accessToken을 재발급받아야 합니다.

  • accessToken 만료를 방지하기 위해 일정 주기(8시간 또는 12시간)마다 토큰 재발급이 진행되도록 + bad accessToken 오류 발생 시 토큰 재발급이 진행되도록 구성해주세요.

추천 로직

  • 모든 뒤끝 함수 응답에서 bad accessToken이 발생했는지 확인
  • bad accessToken 발생 시 Backend.BMember.RefreshTheBackendToken 호출
  • 코루틴등의 반복 로직을 이용하여 8시간 12시간마다 Backend.BMember.RefreshTheBackendToken 호출
  • Backend.BMember.RefreshTheBackendToken 함수 호출 → bad refreshToken이외의 이유로 실패시 최대 3번까지 복구 시도

주의점

  • Backend.BMember.RefreshTheBackendToken 함수 호출 후 bad refreshToken 에러가 리턴되는 경우 : 다른 기기에서 같은 계정으로 로그인 성공, 과거의 accessToken과 refreshToken이 만료된 상태입니다.

bad refreshToken 오류가 리턴되면 리프레시 토큰 재발급 로직을 중지하고, (중복 로그인 + 게임 종료) 안내 UI를 노출하는 것을 권장합니다.

statusCode : 401
errorCode : BadUnauthorizedException
message : bad refreshToken, 잘못된 refreshToken 입니다

Sample Code : 8시간마다 리프레시 하는 로직

// 로그인 이후 함수를 호출해줍니다.
IEnumerator RefreshToken()
{
    int count = 0;
    WaitForSeconds waitForRefreshTokenCycle = new WaitForSeconds(60 * 60 * 8); // 60초 x 60분 x 8시간
    WaitForSeconds waitForRetryCycle = new WaitForSeconds(15f);
    // 첫 호출시에는 리프레시 토큰하지 않도록 8시간을 기다리게 해준다.
    bool isStart = false;
    if (!isStart)
    {
        isStart = true;
        yield return waitForRefreshTokenCycle; // 8시간 기다린 후 반복문 시작
    }
    BackendReturnObject bro = null;
    bool isRefreshSuccess = false;
    // 이후부터는 반복문을 돌면서 8시간마다 최대 3번의 리프레시 토큰을 수행하게 된다.
    while (true)
    {
        for (int i = 0; i < 3; i++)
        {
            bro = Backend.BMember.RefreshTheBackendToken();
            Debug.Log("리프레시 토큰 성공 여부 : " + bro);
            if (bro.IsSuccess())
            {
                Debug.Log("토큰 재발급 완료");
                isRefreshSuccess = true;
                break;
            }
            else
            {
                if (bro.GetMessage().Contains("bad refreshToken"))
                {
                    Debug.LogError("중복 로그인 발생");
                    isRefreshSuccess = false;
                    break;
                }
                else
                {
                    Debug.LogWarning("15초 뒤에 토큰 재발급 다시 시도");
                }
            }
            yield return waitForRetryCycle; // 15초 뒤에 다시시도
        }
        // 3번 이상 재시도시 에러가 발생할 경우, 리프레시 토큰 에러 외에도 네트워크 불안정등의 이유로 이후에도 로그인에 실패할 가능성이 높습니다. 
        // 유저들이 스스로 토큰 리프레시를 할수 있도록 구현해주시거나 수동 로그인을 하도록 구현해주시기 바랍니다.
        if(bro == null)
        {
          Debug.LogError("토큰 재발급에 문제가 발생했습니다. 수동 로그인을 시도해주세요");
          //ShowUI("토큰이 재발급에 실패했습니다. 다시 로그인해주시기 바랍니다.");
        }
        if (!bro.IsSuccess())
        {
            Debug.LogError("토큰 재발급에 문제가 발생하였습니다 : " + bro);
            //ShowUI("토큰 재발급에 문제가 발생하였습니다 : 수동 로그인을 시도해주시기 바랍니다." + bro);
        }
        // 성공할 경우 값 초기화 후 8시간을 wait합니다.
        if (isRefreshSuccess)
        {
            Debug.Log("8시간 토큰 재 호출");
            isRefreshSuccess = false;
            count = 0;
            yield return waitForRefreshTokenCycle;
        }
    }
}

Sample Code : 뒤끝함수 에러 발생 시 토큰 재발급 하는 로직

public void GetData()
{
   var bro = Backend.GameData.Get("table", new Where());

   if(!bro.IsSuccess())
   {
      if(bro.GetStatusCode() == "401")
      {
         if(bro.GetMessage().Contains("accessToken"))
         {
            StartCoroutine(RefreshToken());
         }
      }
   }
}


IEnumerator RefreshToken()
{
    int count = 0;

    WaitForSeconds waitForRetryCycle = new WaitForSeconds(15f);

    BackendReturnObject bro = null;

    while (count < 3)
    {
        bro = Backend.BMember.RefreshTheBackendToken();

        Debug.Log("리프레시 토큰 : " + bro);

        if (bro.IsSuccess())
        {
            Debug.Log("토큰 재발급 완료");
            break;
        }
        else
        {
            if (bro.GetMessage().Contains("bad refreshToken"))
            {
                Debug.LogError("중복 로그인 발생");
		//ShowUI("중복 로그인이 발생했습니다. 다시 로그인해주시기 바랍니다." + bro);
                break;
            }
            else
            {
                Debug.LogWarning("15초 뒤에 토큰 재발급 다시 시도");
            }
        }
        count++;
        yield return waitForRetryCycle;
    }
    if(bro == null)
    {
        Debug.LogError("토큰 재발급에 문제가 발생했습니다. 수동 로그인을 시도해주세요");
        //ShowUI("토큰이 재발급에 실패했습니다. 다시 로그인해주시기 바랍니다.");
    }
    // 3번 이상 재시도시 에러가 발생할 경우, 리프레시 토큰 에러 외에도 네트워크 불안정등의 이유로 이후에도 로그인에 실패할 가능성이 높습니다. 유저들이 스스로 토큰 리프레시를 할수 있도록 구현해주시거나 수동 로그인을 하도록 구현해주시기 바랍니다.
    if (!bro.IsSuccess()) 
    {
        Debug.LogError("토큰 재발급에 문제가 발생했습니다. 수동 로그인을 시도해주세요");
        //ShowUI("토큰이 유효하지 않습니다. 다시 로그인해주시기 바랍니다.\n" + bro);
    }
}
좋아요 1

안녕하세요, 게임 서버 뒤끝입니다.

두 예시 코드에 로직 오류가 확인되어 해당 코드의 수정이 이루어졌습니다.

안내가 된 이전 코드는 로직이 정상적으로 작동되지 않아 함수 호출에 accessToken 에러 발생 시 토큰을 재발급하지 않을 수 있으므로 수정된 코드로 교체해주시기 바랍니다.

로직 오류가 발생되는 두 예시 코드는 아래와 같습니다.

  • Sample Code : 데이터 불러오기 요청 실패시 새로운 row를 잘못 생성하는 이슈
  • Sample Code : 뒤끝함수 에러 발생 시 토큰 재발급 하는 로직

수정 전 코드

errorMessage.Contains("bad accessToken")

수정 후 코드

errorMessage.Contains("accessToken")

잘못된 코드 안내로 불편을 드려 죄송합니다.
안내가 이루어지는 예시 코드의 경우 더욱 면밀히 신경 써 게시될 수 있도록 하겠습니다.

감사합니다.

좋아요 1