Day 11 — 오늘 배운 게임 서버 개발 지식 5가지
학습·복습 기간: 2026년 11월 11일~13일
학습 주제: Struct와 Class, Greedy 알고리즘, Adapter 패턴, TCP 통신, PostgreSQL SQL 기초
게임 서버 개발 압축 학습 11일차에는 자료구조의 메모리 구조, Greedy 알고리즘, Adapter 패턴, 네트워크 통신, SQL 기초를 복습했다.
이번에 특히 중요했던 것은 코드가 어떤 결과를 내는지만 아는 것이 아니라, 그 결과가 발생하는 내부 원리를 이해하는 것이었다.
1. 자료구조 — Struct 배열과 Class 배열은 GC 부담이 어떻게 다를까?
C#에서 struct는 값 형식(Value Type), class는 참조 형식(Reference Type)이다.
다음 두 타입을 생각해 보자.
public struct MonsterStruct
{
public int Hp;
public int Attack;
}
public class MonsterClass
{
public int Hp;
public int Attack;
}
각 타입으로 10,000개의 요소를 가진 배열을 생성한다.
MonsterStruct[] structs = new MonsterStruct[10000];
MonsterClass[] classes = new MonsterClass[10000];
Struct 배열은 무엇을 저장할까?
MonsterStruct[]는 배열 객체 내부에 10,000개의 구조체 값을 직접 저장한다.
구조체의 Hp, Attack 필드는 배열 요소의 일부로 저장되므로, 개별 구조체를 위해 별도의 관리 객체를 생성하지 않는다.
중요한 점은 Struct라고 해서 무조건 Stack에 저장되는 것은 아니라는 사실이다.
배열 객체는 Managed Heap에 생성되므로, 배열 내부에 존재하는 구조체 값들도 힙에 위치한다.
따라서 Struct 배열 역시 GC의 관리 대상이다.
Class 배열은 무엇을 저장할까?
MonsterClass[]는 구조체 배열과 다르게 객체 참조를 저장할 공간을 가진다.
하지만 다음 코드만으로는 실제 MonsterClass 객체 10,000개가 생성되지 않는다.
MonsterClass[] classes = new MonsterClass[10000];
이 시점에는 배열 객체 한 개만 만들어지고 각 요소의 참조값은 null로 초기화된다.
실제 클래스 객체를 생성하려면 다음과 같은 작업이 필요하다.
for (int i = 0; i < classes.Length; i++)
{
classes[i] = new MonsterClass();
}
이렇게 하면 배열 객체 1개와 MonsterClass 객체 10,000개를 합쳐 총 10,001개의 관리 객체가 생성된다.
둘 다 Managed Heap에 있는데 왜 GC 부담이 다를까?
이 부분을 처음에는 잘못 이해했다.
Class 배열에 참조값이 많기 때문에 GC 부담이 크다고 생각했지만, 중요한 것은 참조값 자체의 개수만이 아니다.
Struct 배열은 이 예시에서 관리 객체 한 개에 10,000개의 구조체 값을 포함한다.
반면 Class 객체를 각각 생성하면 관리 객체의 개수가 증가하고, 각 객체의 할당·추적·수명 관리에 추가 비용이 발생할 수 있다.
비교Struct 배열Class 배열 + 개별 인스턴스
| 배열 객체 | 1개 | 1개 |
| 별도 요소 객체 | 없음 | 10,000개 |
| 배열 요소의 내용 | 구조체 값 | 객체 참조 |
| GC 관리 | 필요 | 필요 |
| 순차 접근 시 캐시 효율 | 유리할 가능성 높음 | 객체 배치에 따라 불리할 수 있음 |
Struct는 항상 빠를까?
그렇지는 않다.
Struct는 값 형식이므로 대입할 때 값이 복사된다.
구조체가 작으면 복사 비용이 낮을 수 있지만, 크기가 크고 복사가 자주 발생한다면 오히려 부담이 될 수 있다.
또한 실제 성능은 구조체 크기, 메모리 접근 방식, JIT 최적화 등에 따라 달라진다.
게임 서버에서 수많은 몬스터의 HP와 공격력을 매 틱 갱신한다면 작은 Struct 배열을 고려할 수 있다.
하지만 성능이 우수하다고 단정하지 않고 실제 벤치마크로 검증해야 한다.
2. 알고리즘 — Greedy가 항상 최적해를 찾지 못하는 이유
Greedy(탐욕 알고리즘)는 각 단계에서 정한 기준에 따라 현재 가장 유리한 선택을 하는 방식이다.
하지만 각 단계의 최선이 전체 문제의 최선과 반드시 일치하지는 않는다.
보상 선택 반례
보상 포인트가 10이고 다음 보상을 선택할 수 있다고 가정하자.
보상필요 포인트보상 가치
| A | 10 | 12 |
| B | 5 | 9 |
| C | 5 | 9 |
각 보상은 최대 한 번 선택할 수 있다.
보상 가치가 가장 큰 것부터 선택하는 Greedy 규칙을 사용하면 A를 선택한다.
A를 선택하면 포인트 10을 모두 사용하고 가치 12를 얻는다.
하지만 B와 C를 함께 선택하면 같은 포인트 10으로 가치 18을 얻는다.
따라서 이 문제에서는 개별 보상 가치가 가장 큰 항목을 먼저 선택하는 Greedy 규칙이 최적해를 보장하지 못한다.
탐욕적 선택 속성과 최적 부분 구조
Greedy의 최적성을 판단할 때는 두 가지 개념을 구별해야 한다.
탐욕적 선택 속성(Greedy-choice Property)
현재 단계의 탐욕적 선택을 포함하는 전체 최적해가 존재하는 성질이다.
최적 부분 구조(Optimal Substructure)
전체 문제의 최적해가 부분 문제의 최적해를 포함하는 성질이다.
처음에는 최적 부분 구조를 매 단계에서 가장 좋은 것을 선택하는 방식이라고 이해했다.
하지만 그것은 Greedy의 선택 방식에 가까운 설명이었다.
최적 부분 구조는 전체 최적해와 부분 최적해 사이의 관계를 말한다.
최단 경로로 이해하는 최적 부분 구조
다음 두 경로가 있다고 가정하자.
경로 1
A --2--> B --6--> G
총비용: 8
경로 2
A --2--> B --1--> C --3--> G
총비용: 6
B에서 G까지 직접 이동하는 비용은 6이지만, C를 경유하면 비용은 4다.
따라서 A → B → G는 최단 경로가 아니다.
A → B → C → G가 더 짧은 경로이고, 그 부분 경로인 B → C → G도 B에서 G까지의 최단 경로가 된다.
이렇게 전체 최적해가 부분 문제의 최적해를 포함하는 성질이 최적 부분 구조다.
단, 최적 부분 구조만 존재한다고 Greedy가 항상 최적해를 구하는 것은 아니다.
작업 선택 문제와 Greedy
서로 겹치지 않는 작업을 최대한 많이 선택하는 문제에서는 종료 시간이 빠른 작업을 우선 선택하는 Greedy 알고리즘을 사용할 수 있다.
종료 시간이 빠른 작업을 선택하면 이후 작업을 선택할 수 있는 여지를 최대한 남길 수 있기 때문이다.
하지만 어떤 Greedy 규칙이 최적해를 보장하는지는 문제에 맞는 근거가 필요하다.
이번 복습에서는 반례 계산은 정확했지만, 최적 부분 구조의 의미를 처음에 잘못 설명했다.
이후 추가 문제를 통해 개념을 수정했으므로 다음에는 힌트 없이 직접 설명할 수 있는지 확인할 예정이다.
3. 디자인 패턴 — Adapter로 외부 결제 SDK 연결하기
게임 서버에서는 Google Play, App Store, 외부 웹상점 등 다양한 결제 시스템과 연동할 수 있다.
문제는 각 외부 SDK가 제공하는 인터페이스와 응답 데이터 형식이 서로 다를 수 있다는 점이다.
Adapter 패턴의 역할
Adapter 패턴은 서로 호환되지 않는 인터페이스를 연결하는 패턴이다.
게임 서버에서 다음과 같은 공통 인터페이스를 사용한다고 가정하자.
public interface IPaymentVerifier
{
Task<VerificationResult> VerifyAsync(
string receipt);
}
그런데 외부 SDK는 다른 메서드를 제공한다.
CheckPurchaseAsync(receipt)
이 경우 Adapter가 서버와 외부 SDK 사이에서 변환 역할을 담당할 수 있다.
PurchaseService
↓
IPaymentVerifier.VerifyAsync()
↓
PaymentAdapter
↓
외부 SDK.CheckPurchaseAsync()
↓
외부 SDK 응답 수신
↓
서버용 VerificationResult로 변환
↓
PurchaseService에 반환
위 호출 구조는 개념 설명용이며, 실제 SDK의 정확한 API 계약을 나타내는 것은 아니다.
Adapter를 사용하는 이유
Adapter를 사용하면 PurchaseService가 외부 SDK의 구체적인 메서드나 반환 형식에 직접 의존하는 부분을 줄일 수 있다.
나중에 결제 SDK가 변경되더라도 Adapter 내부의 구현을 변경하는 방식으로 대응할 수 있다.
여기서 중요한 것은 Adapter가 단순히 여러 SDK를 하나의 함수로 합치는 패턴은 아니라는 점이다.
서로 다른 인터페이스와 데이터 형식을 서버가 사용하는 방식에 맞게 연결하는 것이 핵심이다.
Strategy·Decorator·Adapter 비교
패턴주요 목적
| Strategy | 실행할 동작이나 알고리즘 교체 |
| Decorator | 기존 객체를 감싸 부가 기능 확장 |
| Adapter | 서로 다른 인터페이스 연결 |
다만 단순한 SDK 호출 하나를 위해 항상 Adapter 클래스를 만들어야 하는 것은 아니다.
또한 Adapter 자체가 결제 검증의 신뢰성, 거래 중복 방지, 트랜잭션 정합성을 자동으로 보장하지는 않는다.
실제 결제 처리에서는 거래 ID 검증, 멱등성, 오류 처리 등을 별도로 구현해야 한다.
4. C# 게임 서버 — TCP, HTTP, WebSocket의 차이
게임 서버에서 통신 방식을 선택하려면 각 프로토콜의 특성을 이해해야 한다.
TCP는 메시지가 아니라 바이트 스트림이다
TCP는 신뢰성 있는 순서형 바이트 스트림을 제공하지만 애플리케이션 메시지의 경계를 보장하지 않는다.
예를 들어 클라이언트가 100바이트 메시지 하나를 보냈다고 가정하자.
서버의 ReadAsync()가 다음과 같이 데이터를 받을 수도 있다.
클라이언트 전송: 100 bytes
서버 첫 번째 ReadAsync: 40 bytes
서버 두 번째 ReadAsync: 60 bytes
따라서 ReadAsync() 한 번이 메시지 하나에 대응한다고 생각하면 안 된다.
반대로 여러 메시지의 바이트가 하나의 읽기 작업에서 함께 전달될 수도 있다.
Length-Prefix로 메시지 경계 구분하기
TCP에서 메시지를 구분하는 방법 중 하나가 Length-Prefix다.
메시지 앞에 본문 길이를 기록한 헤더를 붙여 전송한다.
예를 들어 프로토콜에서 헤더 크기를 4바이트로 정했다면 다음과 같다.
[4바이트 메시지 길이][메시지 본문]
서버는 다음 순서로 처리한다.
- 헤더 4바이트가 모두 수신될 때까지 읽는다.
- 헤더에서 본문 길이를 해석한다.
- 해당 길이만큼 본문을 모두 수신할 때까지 읽는다.
- 완전한 메시지가 준비되면 역직렬화하거나 명령을 처리한다.
여기서 중요한 사실은 헤더 4바이트조차 한 번의 ReadAsync로 모두 받을 수 있다는 보장이 없다는 점이다.
따라서 부분 수신을 처리하는 반복 로직이 필요하다.
또한 최대 패킷 크기를 검증하고 헤더의 바이트 순서를 송수신 측에서 일치시켜야 한다.
HTTP와 WebSocket은 언제 사용할까?
HTTP는 요청·응답 중심의 통신에 적합하다.
로그인, 계정 정보 조회, 결제 요청 등에서 활용할 수 있다.
WebSocket은 연결을 유지하면서 클라이언트와 서버가 양방향 메시지를 주고받는 데 적합하다.
실시간 채팅이나 상태 변경 알림 등에 사용할 수 있다.
실시간 전투처럼 지연에 민감한 상황에서는 UDP 기반의 프로토콜도 고려할 수 있다.
다만 UDP가 항상 TCP보다 빠르다는 뜻은 아니다. 유실·순서·재전송 등을 애플리케이션에서 어떻게 처리하는지도 함께 고려해야 한다.
5. 게임 서버 DB — WHERE, ORDER BY, LIMIT 복습
이번 DB 학습에서는 복잡한 트랜잭션보다 기본 SQL의 조건과 정렬을 다시 확인했다.
특히 NULL 검사와 AND·OR 조건식에서 오류가 있었기 때문에 복습이 필요했다.
NULL은 어떻게 검사할까?
다음과 같은 테이블이 있다고 가정하자.
user_idnicknamedeleted_at
| 1 | Alice | NULL |
| 2 | Bob | 2026-11-10 |
| 3 | Carol | NULL |
| 4 | David | 2026-11-12 |
deleted_at에 값이 존재하는 플레이어를 조회하려면 다음과 같이 작성한다.
SELECT user_id, nickname
FROM users
WHERE deleted_at IS NOT NULL;
결과는 Bob과 David다.
- IS NULL: 값이 NULL인 행
- IS NOT NULL: 값이 NULL이 아닌 행
일반적인 SQL에서는 NULL을 = NULL로 검사해서는 안 된다.
NULL은 알 수 없는 값이라는 의미이므로 비교 연산의 결과에 UNKNOWN이 발생할 수 있다.
AND와 OR의 우선순위
다음 SQL을 살펴보자.
SELECT nickname
FROM users
WHERE level >= 10
AND gold >= 500
OR gold < 100;
SQL에서 AND는 OR보다 우선순위가 높다.
따라서 이 조건은 다음과 같은 의미다.
WHERE (level >= 10 AND gold >= 500)
OR gold < 100
예를 들어 Carol이 레벨 12, 골드 800이라면 첫 번째 조건을 만족한다.
Eve가 레벨 20, 골드 50이라면 두 번째 조건을 만족한다.
따라서 두 플레이어가 조회 대상이다.
실무에서는 복잡한 조건식을 괄호로 명확하게 표현하여 의도하지 않은 결과를 방지하는 것이 좋다.
ORDER BY와 LIMIT
골드가 가장 많은 플레이어 3명을 조회하려면 다음 SQL을 사용할 수 있다.
SELECT nickname, gold
FROM users
ORDER BY gold DESC
LIMIT 3;
- ORDER BY gold: 골드 기준 정렬
- DESC: 높은 값부터 낮은 값 순서
- LIMIT 3: 최대 세 행 반환
복습에서 주어진 데이터의 결과는 다음과 같았다.
순위nicknamegold
| 1 | Carol | 800 |
| 2 | Bob | 700 |
| 3 | Alice | 200 |
이번에는 SQL 문법과 예상 결과를 모두 정확하게 작성할 수 있었다.
다만 실제 PostgreSQL에서 실행해 검증한 것은 아니다.
오늘 새롭게 알게 된 점
- Struct 배열도 Managed Heap에 생성되며 GC 대상이다.
- Class 배열에 참조 공간을 생성하는 것과 실제 Class 객체를 개별 생성하는 것은 다르다.
- GC 부담은 단순한 참조 개수가 아니라 관리 객체의 할당·수명·참조 관계 등을 함께 고려해야 한다.
- Greedy의 현재 최선 선택과 최적 부분 구조는 서로 다른 개념이다.
- Adapter는 외부 SDK 호출 및 응답 형식을 서버 인터페이스에 맞게 연결한다.
- TCP에서는 Length-Prefix 헤더도 부분 수신될 수 있다.
- PostgreSQL의 NULL 검사, AND·OR 우선순위, ORDER BY 정렬 기준을 정확히 구분해야 한다.
추가로 복습할 내용
- Struct의 크기와 복사 비용
- Struct 배열과 Class 배열의 실제 GC·성능 측정
- Greedy의 탐욕적 선택 속성과 최적 부분 구조
- 작업 선택 알고리즘의 C# 코드 및 시간 복잡도
- Adapter의 외부 SDK 예외 처리
- TCP의 Length-Prefix 부분 수신 처리
- WebSocket 재접속과 UDP 패킷 유실 처리
- PostgreSQL의 NULL 3값 논리와 복합 WHERE 조건
이번 학습에서는 자료구조와 SQL의 개념을 다시 설명할 수 있었지만, Greedy의 최적 부분 구조는 추가 설명이 필요했다.
앞으로는 잘못 이해한 개념을 다시 설명받는 데서 끝내지 않고, 일정 시간이 지난 후에도 힌트 없이 설명할 수 있는지 확인하려고 한다.
본문의 코드와 SQL은 학습 목적의 예시이며 별도의 컴파일·실행·성능 측정 결과는 포함하지 않았다. 게임 서버 사례는 가상의 상황이며 회사 내부 소스 코드나 고객 데이터는 포함하지 않았다.
'서버 공부 > 매일학습' 카테고리의 다른 글
| Day 10 — 오늘 배운 게임 서버 개발 지식 5가지 (0) | 2026.11.12 |
|---|---|
| Day 9 — 오늘 배운 게임 서버 개발 지식 5가지 (0) | 2026.11.11 |
| Day 8 — 오늘 배운 게임 서버 개발 지식 5가지 (0) | 2026.11.10 |
| Day 7 — 오늘 배운 게임 서버 개발 지식 5가지 (0) | 2026.11.09 |
| Day 6 — 오늘 배운 게임 서버 개발 지식 5가지 (0) | 2026.11.07 |
