I'm FanJae.

Unity 게임 개발 캠프 개인 프로젝트 7일차. 캐릭터 선택 이후 게임 서버 입장 흐름 구현 본문

Projects/MyToyMapleServer

Unity 게임 개발 캠프 개인 프로젝트 7일차. 캐릭터 선택 이후 게임 서버 입장 흐름 구현

FanJae 2026. 9. 20. 22:02

1. 시작에 앞서

- 이전 작업에서는 LoginServer와 GameServer를 분리하고, 로그인에 성공했을 때 생성한 인증 티켓을 GameServer에 전달하는 구조를 구현했다.

- 클라이언트는 LoginServer에서 전달받은 인증 키를 이용해 GameServer에 접속하고, GameServer에서는 미리 등록된 인증 티켓을 확인하여 사용자를 인증하는 방식이었다.

- 다만 당시 구조에서는 LoginServer가 인증 티켓을 GameServer에 전달한 이후 실제로 티켓이 정상적으로 등록되었는지 확인하지 않고 있었다.

- 또한 로그인에 성공하자마자 인증 티켓을 발급하고 있었기 때문에, 실제 게임에서 필요한 캐릭터 선택 과정이 존재하지 않았다.

- 일반적인 게임의 접속 흐름을 생각하면 다음과 같은 단계가 필요하다.

로그인
   ↓
캐릭터 목록 확인
   ↓
캐릭터 선택
   ↓
게임 서버 입장

- 따라서 이번 작업에서는 GameServer의 인증 티켓 등록 결과를 LoginServer에서 확인할 수 있도록 서버 간 패킷을 보완했다.

- 이후 로그인 상태를 세션에 저장하고 캐릭터 목록 조회와 캐릭터 선택 기능을 추가하여, 캐릭터를 선택한 시점에 인증 티켓을 발급하고 GameServer로 이동하도록 전체 흐름을 변경했다.


2. 기존 인증 티켓 전달 구조

- 기존에는 LoginServer에서 로그인에 성공하면 인증 키를 생성한 뒤 바로 GameServer에 인증 티켓을 등록했다.

- 전체적인 흐름은 다음과 같았다.

Client
   ↓
LoginServer
   ↓
로그인 성공
   ↓
AuthKey 생성
   ↓
GameServer에 인증 티켓 등록
   ↓
Client에게 AuthKey 전달

- GameServer에서는 LoginServer로부터 전달받은 accountId와 authKey를 저장해두고 있었다.

- 이후 클라이언트가 GameServer에 접속하여 같은 authKey를 전달하면 저장된 인증 티켓을 찾아 사용자를 인증했다.

- 기본적인 서버 간 인증 흐름은 만들어졌지만, LoginServer가 인증 티켓을 전송한 뒤 GameServer가 실제로 해당 티켓을 정상적으로 처리했는지 확인할 방법이 없었다.


3. 인증 티켓 등록 응답 추가

- 먼저 LoginServer와 GameServer 사이의 인증 티켓 등록 패킷을 요청과 응답 형태로 나눴다.

enum class ServerPacketOpcode : uint16_t
{
    RegisterAuthTicketRequest = 1,
    RegisterAuthTicketResponse = 2
};

- 기존에는 인증 티켓을 전달하는 패킷 하나만 존재했지만, 변경 이후에는 다음과 같은 흐름으로 동작한다.

LoginServer
      ↓
RegisterAuthTicketRequest
      ↓
GameServer
      ↓
인증 티켓 등록
      ↓
RegisterAuthTicketResponse
      ↓
LoginServer

- GameServer에서는 인증 티켓을 등록한 뒤 결과를 응답 패킷으로 반환하도록 변경했다.

- 응답 패킷에는 등록 결과와 함께 요청에 사용했던 authKey를 다시 포함시켰다.

struct RegisterAuthTicketResponse
{
    RegisterAuthTicketResult result = RegisterAuthTicketResult::Failed;
    uint64_t authKey = 0;
};

- LoginServer에서는 응답 패킷의 Opcode와 크기를 확인하고, 반환된 authKey 역시 자신이 요청했던 값과 동일한지 확인한다.

- 이를 통해 패킷을 전송했다는 것과 GameServer에서 실제 처리가 완료되었다는 것을 구분할 수 있도록 했다.


4. GameServer 연결 실패 처리

- 서버가 분리되면서 GameServer의 상태도 별도로 고려할 필요가 있었다.

- 예를 들어 LoginServer는 정상적으로 동작하고 있어도 GameServer가 종료되어 있거나 연결할 수 없는 상황이 발생할 수 있다.

- 기존 구조에서는 GameServer에 인증 티켓을 등록하지 못하면 로그인 처리 자체가 실패하는 형태에 가까웠다.

- 이를 구분하기 위해 로그인 결과에 ServerUnavailable을 추가했다.

enum class LoginResult : uint8_t
{
    Success = 0,
    InvalidAccount = 1,
    ServerUnavailable = 2
};

- 이에 따라 로그인 실패 원인을 다음과 같이 구분할 수 있게 되었다.

InvalidAccount
→ 계정 자체가 유효하지 않은 경우

ServerUnavailable
→ 계정 인증은 가능하지만 GameServer를 사용할 수 없는 경우

- 서버를 분리한 만큼 각 서버의 상태에 따라 실패 원인을 구분할 필요가 있었기 때문에 추가한 처리였다.


5. 로그인과 게임 서버 입장 과정 분리

- 인증 티켓 등록 구조를 보완한 이후에는 로그인과 GameServer 입장 과정을 다시 정리했다.

- 기존에는 로그인 성공 시점에 바로 인증 키를 발급했다.

로그인 성공
   ↓
AuthKey 생성
   ↓
GameServer 티켓 등록

- 하지만 캐릭터 선택 기능을 추가하려고 보면 이 구조에는 문제가 있다.

- 사용자가 어떤 캐릭터로 게임에 입장할지 결정하지 않았는데 이미 GameServer 입장을 위한 인증 티켓이 만들어지고 있기 때문이다.

- 따라서 로그인 성공의 의미를 계정 인증에 성공한 상태로 한정했다.

- 로그인 성공 시에는 인증 키를 만들지 않고, LoginSession에 현재 로그인된 계정 정보를 저장하도록 변경했다.

session.SetAuthenticated(true);
session.SetAccountId(request.accountId);

- 실제 GameServer 입장을 위한 인증 키는 이후 캐릭터를 선택한 시점에 생성하도록 변경했다.


6. LoginSession에 로그인 상태 저장

- 로그인 이후에도 캐릭터 목록 조회와 캐릭터 선택 요청을 처리해야 하기 때문에 LoginServer에서는 현재 연결된 세션이 어떤 계정인지 알고 있어야 한다.

- 이를 위해 LoginSession에 다음 정보를 추가했다.

uint32_t _accountId = 0;
bool _authenticated = false;

- 로그인에 성공하면 해당 값을 세션에 저장한다.

LoginSession
 ├─ authenticated
 └─ accountId

- 이후 캐릭터 관련 요청에서는 클라이언트가 전달한 계정 정보를 다시 신뢰하는 것이 아니라 이미 인증된 LoginSession의 계정 정보를 기준으로 처리한다.

- 또한 캐릭터 목록 조회와 캐릭터 선택 모두 로그인된 세션에서만 가능하도록 처리했다.

if (!session.IsAuthenticated())
    return false;

- 결과적으로 하나의 LoginSession이 현재 어떤 계정으로 인증된 연결인지 나타내도록 변경했다.


7. 캐릭터 목록 조회 추가

- 로그인 이후에는 사용자가 보유한 캐릭터를 확인할 수 있도록 캐릭터 목록 패킷을 추가했다.

CharacterListRequest
CharacterListResponse

- 캐릭터 정보에는 현재 다음 데이터를 포함하도록 구성했다.

struct CharacterInfo
{
    uint32_t characterId = 0;
    char name[MAX_CHARACTER_NAME_LENGTH] = {};
    uint16_t level = 1;
};

- 아직 DB를 연결한 상태는 아니기 때문에 테스트용 캐릭터 데이터를 직접 반환하도록 구현했다.

Character 1
 ├─ ID : 1001
 ├─ Name : Warrior
 └─ Level : 10

Character 2
 ├─ ID : 1002
 ├─ Name : Magician
 └─ Level : 15

- 캐릭터 데이터 자체보다는 로그인 이후 캐릭터 목록을 조회하는 단계가 접속 흐름에 추가되었다는 점이 중요했다.

- 전체 흐름은 다음과 같이 확장되었다.

LoginRequest
      ↓
LoginResponse
      ↓
CharacterListRequest
      ↓
CharacterListResponse

 


8. 캐릭터 선택 처리

- 캐릭터 목록을 확인한 이후에는 선택한 캐릭터의 ID를 LoginServer로 전달하도록 했다.

struct CharacterSelectRequest
{
    uint32_t characterId = 0;
};

- LoginServer에서는 현재 로그인된 계정과 전달받은 characterId를 이용해 정상적인 캐릭터인지 확인한다.

- 현재는 실제 DB가 없기 때문에 테스트용 검증 로직을 사용했다.

- 올바르지 않은 캐릭터가 전달된 경우에는 별도의 결과를 반환하도록 구성했다.

enum class CharacterSelectResult : uint8_t
{
    Success = 0,
    InvalidCharacter = 1,
    ServerUnavailable = 2
};

- 이를 통해 캐릭터 선택 실패 역시 다음과 같이 구분할 수 있다.

InvalidCharacter
→ 선택한 캐릭터가 유효하지 않은 경우

ServerUnavailable
→ 캐릭터는 정상적이지만 GameServer에 접근할 수 없는 경우

 


9. 인증 티켓 발급 시점 변경

- 가장 큰 변경은 인증 티켓을 발급하는 시점이었다.

- 기존에는 로그인에 성공하면 바로 인증 티켓을 만들었다.

로그인 성공
   ↓
인증 티켓 발급

- 변경 이후에는 다음과 같이 동작한다.

로그인 성공
   ↓
캐릭터 목록 조회
   ↓
캐릭터 선택
   ↓
인증 티켓 발급

- 선택한 캐릭터가 정상적으로 확인되면 이때 authKey를 생성하고 GameServer에 인증 티켓을 등록한다.

session.GetGameServerClient().RegisterAuthTicket(
    session.GetAccountId(),
    request.characterId,
    authKey
);

- 즉, 어떤 캐릭터로 GameServer에 접속할지가 결정된 이후에만 입장용 인증 티켓을 만들도록 변경했다.


10. 인증 티켓에 CharacterId 추가

- 인증 티켓을 캐릭터 선택 시점에 발급하게 되면서 티켓에 characterId도 함께 저장하도록 변경했다.

- 기존 인증 티켓은 다음과 같은 정보만 가지고 있었다.

AuthTicket
 ├─ accountId
 └─ authKey

- 변경 이후에는 다음과 같이 구성된다.

AuthTicket
 ├─ accountId
 ├─ characterId
 └─ authKey
struct AuthTicket
{
    uint32_t accountId = 0;
    uint32_t characterId = 0;
    uint64_t authKey = 0;
};

- LoginServer가 GameServer에 전달하는 패킷 역시 동일하게 변경했다.

struct RegisterAuthTicketRequest
{
    uint32_t accountId = 0;
    uint32_t characterId = 0;
    uint64_t authKey = 0;
};

- 실제로 AuthTicketManager::Add() 역시 accountId와 authKey만 받던 구조에서 characterId까지 함께 저장하도록 변경되었다.


11. GameSession에 캐릭터 정보 귀속

- 클라이언트는 캐릭터 선택에 성공하면 LoginServer로부터 authKey와 GameServer 접속 정보를 전달받는다.

- 이후 GameServer에 연결하여 authKey를 전달하면 GameServer에서는 등록되어 있던 인증 티켓을 확인한다.

- 인증 티켓이 정상적으로 확인되면 티켓에 포함되어 있던 계정과 캐릭터 정보를 GameSession에 저장한다.

session.SetAccountId(ticket.accountId);
session.SetCharacterId(ticket.characterId);
session.SetAuthenticated(true);

- 기존에는 GameSession에 계정 정보만 저장했지만, 이번 작업에서 characterId도 추가했다.

- 결과적으로 GameServer에서는 인증 이후 해당 세션이 어떤 계정의 어떤 캐릭터인지 알 수 있게 된다.

GameSession
 ├─ accountId
 ├─ characterId
 └─ authenticated

- 이후 게임 로직에서는 클라이언트가 매 요청마다 accountId나 characterId를 전달하도록 만들기보다, 인증된 GameSession에 저장된 정보를 기준으로 플레이어를 식별할 수 있는 구조가 된다.


12. 전체 입장 흐름

- 이번 작업까지 반영한 전체 흐름은 다음과 같다.

Client
   │
   │ LoginRequest
   ▼
LoginServer
   │
   │ 계정 확인
   │
   │ LoginSession에
   │ accountId 저장
   ▼
Client
   │
   │ CharacterListRequest
   ▼
LoginServer
   │
   │ 캐릭터 목록 반환
   ▼
Client
   │
   │ CharacterSelectRequest
   │ characterId
   ▼
LoginServer
   │
   │ 로그인 계정과
   │ 캐릭터 정보 확인
   │
   │ AuthKey 생성
   │
   │ RegisterAuthTicketRequest
   │
   ▼
GameServer
   │
   │ AuthTicket 등록
   │ accountId
   │ characterId
   │ authKey
   │
   │ RegisterAuthTicketResponse
   ▼
LoginServer
   │
   │ 등록 성공 확인
   │
   │ CharacterSelectResponse
   │ authKey
   │ gameServerPort
   ▼
Client
   │
   │ GameServer 연결
   │
   │ EnterGameRequest
   │ authKey
   ▼
GameServer
   │
   │ AuthTicket 확인
   │
   │ accountId
   │ characterId
   │ GameSession에 저장
   ▼
게임 입장

- 이전에는 단순히 로그인 성공 후 GameServer로 넘어가는 구조였다면, 이번 작업을 통해 계정 인증과 캐릭터 선택, 게임 서버 입장을 각각 별도의 단계로 나눌 수 있게 되었다.


13. 변경하면서 얻은 점

- 처음에는 로그인 성공과 동시에 GameServer 입장에 필요한 인증 정보를 만들었다.

로그인 성공
      ↓
GameServer 입장 준비

- 하지만 캐릭터 선택 과정이 추가되면서 로그인과 게임 서버 입장은 같은 단계가 아니라는 점이 명확해졌다.

- 변경 이후에는 다음과 같이 역할을 나눴다.

LoginServer

계정 인증
   ↓
캐릭터 조회
   ↓
캐릭터 선택
   ↓
입장 정보 생성
GameServer

인증 티켓 확인
   ↓
계정 정보 확인
   ↓
캐릭터 정보 확인
   ↓
GameSession 귀속

- 또한 LoginServer가 GameServer에 인증 티켓을 전달한 뒤 응답을 확인하도록 만들면서, 서버 간 요청 역시 전송 여부만 확인하는 것이 아니라 실제 처리 결과까지 확인하도록 변경했다.

- 서버를 분리하면서 단순히 프로그램을 두 개로 나누는 것만으로 끝나는 것이 아니라, 두 서버 사이에서 어떤 정보를 어느 시점에 전달할 것인지도 함께 설계해야 한다는 점을 확인할 수 있었다.


14. 고려해 볼 만한 사항

- 현재 캐릭터 목록과 캐릭터 소유 여부는 테스트를 위한 임시 데이터로 처리하고 있다.

- 실제로 사용하려면 계정에 속한 캐릭터 정보를 DB에서 조회하고, 선택한 캐릭터가 해당 계정의 캐릭터인지 검증하는 과정이 필요하다.

- 또한 현재 인증 티켓은 LoginServer에서 생성한 값을 GameServer에 전달하고, 클라이언트가 이후 동일한 값을 사용하는 구조다.

- 따라서 다음과 같은 부분도 추가로 고려할 필요가 있다.

인증 키의 생성 방식

같은 인증 키가 중복될 가능성

발급한 티켓이 사용되지 않는 경우

인증 티켓을 언제까지 유효하게 볼 것인지

- 특히 인증 티켓은 GameServer 입장을 허용하기 위한 일종의 임시 인증 정보이기 때문에 무기한 유지하는 것보다는 일정한 유효 시간을 두는 방식이 필요할 수 있다.

- 이번 단계에서는 우선 로그인부터 캐릭터 선택, GameServer 입장까지의 흐름을 연결하는 데 중점을 두었다.


15. 정리

- 기존에는 로그인에 성공한 직후 인증 키를 생성하고 GameServer에 인증 티켓을 등록하는 구조를 사용했다.

- 먼저 LoginServer가 인증 티켓을 전달한 이후 GameServer의 실제 처리 결과를 확인할 수 있도록 요청과 응답 패킷을 분리했다.

- GameServer 연결에 실패한 경우도 별도로 구분할 수 있도록 오류 결과를 추가했다.

- 이후 로그인 성공 상태를 LoginSession에 저장하고 캐릭터 목록 조회와 캐릭터 선택 패킷을 추가했다.

- 인증 티켓의 발급 시점도 로그인 성공 시점에서 캐릭터 선택 이후로 이동했다.

- 이에 따라 인증 티켓에는 기존 accountId와 authKey뿐만 아니라 선택한 캐릭터의 characterId도 함께 저장하도록 변경했다.

- GameServer에서는 해당 인증 티켓을 확인한 뒤 accountId와 characterId를 GameSession에 저장하여 실제 게임 세션과 플레이어 정보를 연결했다.

- 처음에는 로그인 서버와 게임 서버 사이에 인증 티켓을 전달하는 것만 고려했지만, 실제 접속 과정을 추가하면서 로그인, 캐릭터 선택, 게임 서버 입장을 각각 분리할 필요가 있었고 그에 맞춰 인증 정보가 전달되는 시점과 구조를 수정했다.

Comments