| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- c#
- C++
- Toy Project
- docker
- multi-thread
- design pattern
- Online Judge
- git
- 독서
- team project
- Photon
- Network Programming
- System Programming
- Data Structure
- PS
- BOJ
- Unity
- Today
- Total
I'm FanJae.
Unity 게임 개발 캠프 개인 프로젝트 8일차. 인증 키 생성과 인증 티켓 유효 시간 관리 본문
Unity 게임 개발 캠프 개인 프로젝트 8일차. 인증 키 생성과 인증 티켓 유효 시간 관리
FanJae 2026. 9. 22. 18:371. 시작에 앞서
- 이전 작업에서는 로그인 이후 캐릭터를 선택하면 LoginServer에서 인증 티켓을 생성하고, 이를 GameServer에 미리 등록한 뒤 클라이언트가 해당 인증 키를 이용해 GameServer에 접속하도록 구성했다.
- 이를 통해 다음과 같은 기본적인 입장 흐름을 만들 수 있었다.
로그인
↓
캐릭터 선택
↓
인증 티켓 발급
↓
GameServer에 티켓 등록
↓
클라이언트 GameServer 접속
↓
인증 티켓 확인
- 기본적인 동작은 가능했지만, 인증 티켓을 실제 입장 인증 정보로 사용하려고 보면 추가로 고려해야 하는 부분이 있었다.
- 우선 인증 키는 단순히 1, 2, 3 ... 형태로 증가하는 값을 사용하고 있었다.
- 또한 한번 발급된 인증 티켓에는 별도의 유효 시간이 없었기 때문에, 사용하지 않은 티켓이 계속 GameServer에 남을 수 있었다.
- 인증 키가 중복되는 경우 역시 별도로 처리하지 않고 있었으며, 만료된 티켓을 주기적으로 제거할 방법도 필요했다.
- 이번 작업에서는 인증 키의 생성 방식을 변경하고, 인증 티켓에 중복 검사와 유효 시간을 추가한 뒤 IOCP의 Timeout을 이용해 만료된 티켓을 주기적으로 정리하도록 구조를 보완했다.
2. 임시 캐릭터 데이터 구조 정리
- 인증 구조를 수정하기 전에 이전 작업에서 임시로 사용하고 있던 캐릭터 데이터도 먼저 정리했다.
- 기존에는 캐릭터 목록을 반환하는 부분과 캐릭터 선택이 유효한지 검사하는 부분에서 각각 별도의 값을 사용하고 있었다.
캐릭터 목록 반환
→ 1001, 1002 캐릭터 데이터 직접 작성
캐릭터 선택 검증
→ characterId == 1001 || characterId == 1002
- 기능 자체는 동작하지만 동일한 캐릭터 정보를 여러 곳에서 따로 관리하고 있었다.
- 예를 들어 테스트 캐릭터가 하나 추가된다면 다음 두 부분을 모두 수정해야 한다.
캐릭터 목록 데이터 수정
+
캐릭터 선택 검증 조건 수정
- 이를 하나의 임시 캐릭터 목록으로 통합했다.
std::array<CharacterInfo, 2> CreateCharacters()
{
std::array<CharacterInfo, 2> characters{};
characters[0].characterId = 1001;
memcpy(characters[0].name, "Warrior", strlen("Warrior"));
characters[0].level = 10;
characters[1].characterId = 1002;
memcpy(characters[1].name, "Magician", strlen("Magician"));
characters[1].level = 15;
return characters;
}
- 캐릭터 목록을 반환할 때도 해당 데이터를 사용하고, 캐릭터 선택을 검증시 같은 데이터를 조회하도록 변경했다.
for (size_t i = 0; i < characters.size(); ++i)
{
response.characters[i] = characters[i];
}
for (const CharacterInfo& character : characters)
{
if (character.characterId == characterId)
return true;
}
- 아직 실제 DB를 사용하는 단계는 아니지만, 같은 데이터를 기준으로 목록 조회와 선택 검증이 이루어지도록 정리한 것이다.
3. 기존 인증 키 생성 방식
- 이전에는 인증 키를 다음과 같이 단순 증가시키는 방식으로 생성했다.
uint64_t nextAuthKey = 1;
uint64_t authKey = nextAuthKey++;
- 테스트 단계에서는 가장 단순하게 사용할 수 있는 방식이다.
- 생성된 값 역시 다음과 같이 예측 가능하다.
1
2
3
4
5
...
- 하지만 인증 키는 클라이언트가 GameServer 입장 시 전달하는 임시 인증 정보다.
- 따라서 실제 인증 정보에 가까운 용도로 사용하려면 단순 증가 값보다는 쉽게 예측할 수 없는 값을 사용하는 것이 적절하다.
- 또한 인증 키 생성 코드가 LoginPacketHandler 내부에 존재하고 있었기 때문에 패킷 처리와 키 생성이라는 서로 다른 역할이 한 곳에 들어가 있었다.
4. AuthKeyGenerator 분리
- 인증 키 생성 역할을 별도로 관리하기 위해 AuthKeyGenerator를 추가했다.
class AuthKeyGenerator
{
public:
bool Generate(uint64_t& authKey);
};
- 이후 캐릭터 선택 과정에서는 직접 값을 증가시키는 대신 AuthKeyGenerator에 인증 키 생성을 요청한다.
uint64_t authKey = 0;
if (!session.GetAuthKeyGenerator().Generate(authKey))
{
return false;
}
- 전체적인 책임은 다음과 같이 나눌 수 있다.
LoginPacketHandler
│
│ 인증 키 필요
↓
AuthKeyGenerator
│
│ AuthKey 생성
↓
LoginPacketHandler
│
↓
GameServer 티켓 등록
- LoginPacketHandler에서는 인증 키를 어떤 방식으로 생성하는지 알 필요 없이, 생성된 값을 받아서 인증 티켓 등록에 사용한다.
- 인증 키 생성 방식과 로그인 패킷 처리 로직을 분리한 것이다.
5. 난수 기반 인증 키 생성
- 실제 인증 키는 Windows에서 제공하는 BCryptGenRandom을 이용해 생성하도록 했다.
NTSTATUS status = BCryptGenRandom(
nullptr,
reinterpret_cast<PUCHAR>(&authKey),
sizeof(authKey),
BCRYPT_USE_SYSTEM_PREFERRED_RNG
);
- 생성하는 값은 기존 인증 티켓에서 사용하고 있던 uint64_t 형식을 그대로 유지했다.
AuthKey
→ 64bit Random Value
- 또한 기본값으로 사용하고 있는 0은 정상적인 인증 키로 발급하지 않도록 했다.
do
{
...
}
while (authKey == 0);
- 결과적으로 기존의 순차 증가 방식은 64비트 난수 형태로 변경되었다.
- 인증 키 자체를 암호처럼 사용하는 것은 아니지만, GameServer 입장을 허용하는 임시 인증 값인 만큼 쉽게 추측할 수 있는 순차 값보다 난수 형태가 적절하다고 판단했다.
6. 인증 티켓 중복 등록 문제
- 난수 기반으로 변경하더라도 같은 값이 절대 생성되지 않는다고 가정할 수는 없다.
- 64비트 공간에서 충돌 가능성은 낮지만, 인증 티켓을 저장하는 쪽에서도 동일한 키가 이미 존재하는지 확인할 필요가 있다.
- 기존 AuthTicketManager에서는 다음과 같이 값을 저장했다.
_tickets[authKey] = AuthTicket{ ... };
- 이 방식에서는 같은 authKey가 다시 들어오면 기존 티켓의 값이 새로운 값으로 덮어써질 수 있다.
기존 Ticket
authKey = A
accountId = 1
↓
새로운 Ticket
authKey = A
accountId = 2
↓
기존 데이터 덮어쓰기
- 인증 키를 기준으로 사용자를 구분하고 있는 상황에서는 이런 동작을 허용하지 않는 편이 적절하다.
7. emplace를 이용한 중복 등록 방지
- 이를 해결하기 위해 인증 티켓 등록 시 operator[] 대신 emplace()를 사용하도록 변경했다.
auto result = _tickets.emplace(
authKey,
AuthTicket{ accountId, characterId, authKey, expiresAt }
);
return result.second;
- emplace()는 동일한 Key가 이미 존재하면 새로운 값을 삽입하지 않는다.
bool Add(
uint32_t accountId,
uint32_t characterId,
uint64_t authKey
);
- 성공 여부에 따라 다음과 같이 처리한다.
새로운 AuthKey
↓
등록 성공
↓
Success
기존에 존재하는 AuthKey
↓
등록 실패
↓
Failed
- GameServer에서는 이 결과를 RegisterAuthTicketResponse에 담아 LoginServer로 반환한다.
- 결과적으로 인증 키 충돌이 발생하더라도 기존 티켓을 덮어쓰지 않고 등록 실패로 처리하도록 변경했다.
8. 인증 티켓에 유효 시간이 필요한 이유
- 다음으로 고려한 부분은 '발급된 인증 티켓을 언제까지 사용할 수 있도록 할 것인가'에 있다.
- 이전 구조에서는 인증 티켓이 다음 두 경우 중 하나가 될 때까지 유지된다.
티켓 발급
↓
클라이언트가 사용
↓
Consume 후 제거
티켓 발급
↓
클라이언트가 사용하지 않음
↓
계속 유지
- 두 번째 경우가 문제였다.
- 예를 들어 캐릭터 선택까지 완료한 뒤 클라이언트가 GameServer에 접속하지 않거나 연결이 끊어진다면, 해당 티켓은 사용할 일이 없더라도 계속 _tickets에 남아 있게 된다.
- 또한 오래전에 발급된 인증 키를 계속 사용할 수 있도록 둘 필요도 없다.
- 인증 티켓은 GameServer에 입장하기 위한 짧은 시간 동안만 필요한 임시 인증 정보이기 때문이다.
9. 인증 티켓에 만료 시간 추가
- 이를 위해 AuthTicket에 만료 시간을 추가했다.
struct AuthTicket
{
uint32_t accountId = 0;
uint32_t characterId = 0;
uint64_t authKey = 0;
std::chrono::steady_clock::time_point expiresAt;
};
- 현재는 티켓 등록 시점을 기준으로 30초 동안 유효하도록 설정했다.
auto expiresAt =
std::chrono::steady_clock::now()
+ std::chrono::seconds(30);
- 따라서 인증 티켓의 상태는 다음과 같이 볼 수 있다.
티켓 생성
↓
30초 동안 유효
↓
사용
또는
만료
- 시간 측정에는 std::chrono::steady_clock을 사용했다.
- 인증 티켓에서 필요한 것은 실제 날짜와 시간이 아니라 발급 이후 일정 시간이 경과했는지 여부이기 때문에 경과 시간을 측정하기 위한 Clock을 사용했다.
10. Consume 시 만료 여부 확인
- 클라이언트가 GameServer에 입장하면 인증 키를 이용해 Consume()을 호출한다.
- 기존에는 인증 키가 존재하면 바로 티켓을 반환하고 제거했다.
- 변경 이후에는 티켓을 찾은 뒤 만료 여부를 먼저 확인한다.
if (std::chrono::steady_clock::now() >= it->second.expiresAt)
{
_tickets.erase(it);
return false;
}
- 아직 유효한 경우에만 실제 인증 티켓을 반환한다.
ticket = it->second;
_tickets.erase(it);
return true;
- 따라서 전체 과정은 다음과 같다.
AuthKey 전달
↓
티켓 존재 확인
↓
만료 여부 확인
↓
┌──────────────┐
│ │
유효 만료
│ │
↓ ↓
티켓 반환 티켓 삭제
│ │
↓ ↓
게임 입장 인증 실패
- 또한 기존과 동일하게 정상적으로 사용된 티켓 역시 바로 제거된다.
- 즉, 하나의 인증 티켓은 한 번만 사용할 수 있으며 일정 시간이 지나면 사용할 수 없도록 구성했다.
11. 사용되지 않은 만료 티켓
- Consume()에서 만료 여부를 확인하는 것만으로는 한 가지 문제가 남는다.
- 클라이언트가 티켓을 아예 사용하지 않는 경우다.
인증 티켓 생성
↓
클라이언트 GameServer 미접속
↓
Consume 호출 안 됨
↓
만료 여부 검사 안 됨
- 이 경우 티켓은 이미 만료되었어도 _tickets 안에 그대로 남아 있게 된다.
- 따라서 만료된 티켓을 별도로 찾아 제거할 수 있도록 CleanupExpired()를 추가했다.
void AuthTicketManager::CleanupExpired()
{
auto now = std::chrono::steady_clock::now();
for (auto it = _tickets.begin(); it != _tickets.end();)
{
if (now >= it->second.expiresAt)
{
it = _tickets.erase(it);
}
else
{
++it;
}
}
}
- 이를 통해 인증 요청이 들어왔을 때만 만료 여부를 확인하는 것이 아니라, 사용되지 않은 티켓도 주기적으로 정리할 수 있는 구조를 만들었다.
12. 만료 티켓을 언제 정리할 것인가
- CleanupExpired()를 만들고 나면 다음 문제는 이를 언제 호출할 것인가였다.
- GameServer에서는 IOCP를 통해 네트워크 이벤트를 처리하고 있었다.
- 당시 메인 루프에서는 다음과 같이 IOCP 이벤트를 무한정 기다리고 있었다.
worker.Dispatch(INFINITE);
- INFINITE로 기다리면 네트워크 이벤트가 없는 동안에는 Dispatch()가 반환하지 않는다.
- 따라서 아래와 같은 주기적인 관리 작업을 실행하기 어렵다.
- 종료된 Session 정리
- 만료된 AuthTicket 정리
- 추후 추가될 주기 작업
- 네트워크 요청이 계속 들어온다면 루프가 반복되겠지만, 요청이 없는 상태에서도 정리 작업은 실행될 필요가 있었다.
13. IOCP Timeout 적용
- 이를 위해 GameServer의 IOCP 대기 시간을 무한 대기에서 1초로 변경했다.
worker.Dispatch(1000);
- 이제 완료된 I/O가 없다면 최대 1초 이후에는 Dispatch()가 반환하게 된다.
IOCP Dispatch
↓
I/O Event 존재
↓
이벤트 처리
↓
정리 작업
IOCP Dispatch
↓
1초 동안 이벤트 없음
↓
Timeout
↓
정리 작업
- 이렇게 하면 네트워크 이벤트가 없는 상태에서도 주기적으로 메인 루프로 제어권이 돌아온다.
14. Timeout과 오류 구분
- 다만 GetQueuedCompletionStatus()는 Timeout이 발생한 경우에도 실패 값을 반환한다.
- 따라서 기존 구조에서 단순히 반환값만 확인하면 다음 두 상황을 구분하기 어렵다.
실제 IOCP 오류
Timeout
- Timeout은 서버를 종료해야 하는 오류가 아니다.
- 단순히 설정한 시간 동안 완료 이벤트가 없었다는 의미이기 때문이다.
- 따라서 GetCompletion()에 timedOut 값을 별도로 추가했다.
bool GetCompletion(
DWORD& bytes,
ULONG_PTR& key,
OVERLAPPED*& overlapped,
bool& ioSuccess,
bool& timedOut,
DWORD timeoutMs
);
- GetQueuedCompletionStatus()가 실패하고 WAIT_TIMEOUT이 확인되면 다음과 같이 처리한다.
if (GetLastError() == WAIT_TIMEOUT)
{
timedOut = true;
return true;
}
- 즉, 전체 함수 호출 자체는 정상적으로 처리된 것으로 보고 Timeout 여부만 별도로 전달한다.
- IocpWorker에서도 Timeout은 실패로 처리하지 않는다.
if (timedOut)
{
return true;
}
- 이를 통해 다음 세 가지 상황을 구분할 수 있게 되었다.
I/O 완료
→ 정상 이벤트 처리
Timeout
→ 오류가 아님
→ 메인 루프로 복귀
실제 오류
→ Dispatch 실패 처리
15. Timeout을 이용한 주기적인 정리
- IOCP에서 1초마다 제어권을 반환할 수 있게 되면서 GameServer의 메인 루프에서 정리 작업을 수행하도록 했다.
while (true)
{
if (!worker.Dispatch(1000))
{
...
}
sessionManager.Cleanup();
authTicketManager.CleanupExpired();
}
- 전체적인 흐름은 다음과 같다.
IOCP 이벤트 대기
↓
I/O 완료 또는 1초 Timeout
↓
Session 정리
↓
만료 AuthTicket 정리
↓
다시 IOCP 이벤트 대기
- AuthTicketManager에서 만료 기능만 만들어두는 것으로 끝내지 않고, 실제로 해당 정리 함수가 일정한 주기로 실행될 수 있도록 서버 루프와 연결한 것이다.
16. 최종 인증 티켓 흐름
- 이번 작업 이후 인증 티켓의 전체 흐름은 다음과 같이 정리할 수 있다.
캐릭터 선택
↓
AuthKeyGenerator
↓
64bit 난수 AuthKey 생성
↓
LoginServer
↓
GameServer에 Ticket 등록 요청
↓
중복 AuthKey 검사
↓
30초 만료 시간 설정
↓
AuthTicket 저장
- 이후 클라이언트가 GameServer에 입장하는 경우에는 다음과 같이 동작한다.
Client
↓
AuthKey 전달
↓
GameServer
↓
AuthTicket 조회
↓
만료 여부 확인
↓
┌──────────────┐
│ │
유효 만료
│ │
↓ ↓
Consume 제거
│ │
↓ ↓
GameSession 인증 실패
- 사용되지 않은 티켓 역시 IOCP Timeout을 통해 주기적으로 정리한다.
1초 Timeout
↓
CleanupExpired()
↓
만료 Ticket 제거
17. 변경하면서 얻은 점
- 전날까지는 인증 티켓을 이용해 LoginServer와 GameServer를 연결하는 흐름 자체를 만드는 것에 집중했다.
- 하지만 실제로 인증 티켓을 계속 관리하려고 보면 단순히 값을 생성하고 저장하는 것만으로는 부족했다.
- 기존 구조에서는 다음과 같은 문제가 있었다.
1. 예측 가능한 인증 키
2. 동일한 인증 키 등록 가능
3. 사용되지 않은 티켓이 계속 남음
4. 오래된 인증 티켓도 사용 가능
5. IOCP가 무한 대기하면 주기적인 정리가 어려움
- 이를 각각 다음과 같이 변경했다.
순차 증가 AuthKey
↓
난수 기반 AuthKey
------------------
덮어쓰기 방식 등록
↓
중복 등록 거부
------------------
무기한 Ticket
↓
30초 유효 시간
------------------
Consume 시에만 제거
↓
주기적인 만료 Ticket 정리
------------------
IOCP 무한 대기
↓
Timeout 기반 주기 처리
- 특히 마지막 IOCP Timeout 처리는 인증 티켓만을 위한 기능이라기보다, 네트워크 이벤트가 없는 상태에서도 서버가 일정 주기로 관리 작업을 수행할 수 있게 만들었다는 점에서 의미가 있었다.
18. 고려해 볼 만한 사항
- 현재 인증 티켓의 유효 시간은 30초로 고정되어 있다.
- 실제 서비스 환경에서는 클라이언트의 네트워크 상태나 서버 이동 과정 등을 고려하여 적절한 시간을 결정할 필요가 있다.
- 너무 짧게 설정하면 정상적인 사용자도 GameServer에 접속하기 전에 티켓이 만료될 수 있다.
유효 시간이 너무 짧음
↓
네트워크 지연
↓
GameServer 접속 전에 만료
- 반대로 너무 길게 설정하면 이미 필요하지 않은 티켓이 오랫동안 유지된다.
- 현재 CleanupExpired() 역시 1초마다 전체 티켓을 순회한다.
- 지금처럼 인증 티켓 수가 적은 단계에서는 문제가 되지 않지만, 티켓 수가 많아진다면 매번 전체 컨테이너를 순회하는 방식의 비용도 고려할 필요가 있다.
- 또한 현재 구조는 단일 GameServer 프로세스 안에 AuthTicketManager가 존재하기 때문에, 서버를 여러 대로 확장한다면 어느 GameServer가 어떤 인증 티켓을 가지고 있을 것인지에 대한 구조도 추가로 필요하다.
- 이번 단계에서는 우선 인증 티켓이 중복되지 않고 일정 시간이 지나면 사용할 수 없으며, 사용되지 않은 티켓도 서버에서 정리되는 흐름을 만드는 데 중점을 두었다.
19. 정리
- 기존에는 인증 키를 단순 증가 값으로 생성하고 있었지만, 이번 작업에서 AuthKeyGenerator를 추가하고 BCryptGenRandom을 이용한 64비트 난수 기반 인증 키 생성 방식으로 변경했다.
- 인증 키 생성 책임 역시 LoginPacketHandler에서 분리하여 별도의 객체에서 관리하도록 했다.
- GameServer의 AuthTicketManager에서는 동일한 인증 키가 이미 존재할 경우 기존 데이터를 덮어쓰지 않고 등록을 거부하도록 변경했다.
- 또한 인증 티켓에 30초의 유효 시간을 추가하고, 만료된 인증 티켓으로는 GameServer에 입장할 수 없도록 처리했다.
- 클라이언트가 사용하지 않은 인증 티켓도 제거할 수 있도록 CleanupExpired()를 추가했다.
- 이를 실제 서버에서 주기적으로 실행하기 위해 IOCP의 무한 대기를 1초 Timeout 방식으로 변경하고, Timeout을 실제 오류와 별도로 구분하도록 IOCP 처리 구조도 보완했다.
- 이후 GameServer 메인 루프에서 종료된 Session과 만료된 인증 티켓을 주기적으로 정리하도록 연결했다.