로그인게임하기
현장 강의록

[현장 강의록 4] 통신 규약과 직렬화, 패킷 설계

2026.10.11

현장 강의록 · 애니메이션고 2022 · 4편

온라인 게임은 서로 떨어진 컴퓨터가 약속대로 말을 주고받아 굴러갑니다. 이 수업에서는 그 약속인 통신 규약, 객체를 줄 세워 보내는 직렬화, 주고받을 말의 모양인 패킷, 그리고 쓰레드를 쓸 때 조심할 점을 배웠습니다.

수업 정보

2022년 1학기 한국애니메이션고등학교 2학년 「게임 개발과 미들웨어」 4강 「네트워크 프로그래밍 #1」에서 가르친 내용입니다.

PROTOCOL

통신 규약

기다림

Blocking / NonBlocking

Blocking 은 전송이 끝날 때까지 프로그램이 멈춰 기다립니다. NonBlocking 은 기다리지 않고 다음 일을 합니다. 디스크에 파일을 쓸 때가 NonBlocking 의 예입니다.

약속

Transfer Protocol

HTTP, TCP, UDP, FTP 처럼 용도에 따라 정한 통신 약속입니다. 이름 끝에 TP 가 붙는 표준이 많지만, 게임 전용으로 만든 약속도 프로토콜이라고 부릅니다.

연결

Listen / Accept

통신하려면 누군가 연결을 기다려야 합니다(Listen). TCP 는 상대의 연결을 받아들인(Accept) 뒤에 대화가 시작됩니다.

흐름

Network Streaming

직렬화한 데이터를 멀리 있는 상대에게 흘려보내는 것입니다. 파일을 읽어 내려가는 File Streaming 과 같은 모양입니다.

단위

Packet

약속을 지키며 오가는 가장 작은 단위입니다. 받는 쪽이 패킷의 시작과 끝을 알아야 말을 끊어 읽을 수 있습니다.

현장

게임 업계에서는

TCP 와 UDP 를 모두 씁니다. 보통은 멀티 쓰레드 TCP 를 쓰고, 전용 서버나 P2P 같은 곳에는 UDP 를 씁니다.

TCP UDP

TCP 와 UDP

왼쪽은 흔들다리를 건너 소포를 건네고 영수증을 받는 전령, 오른쪽은 빠르게 날아가다 두 마리가 구름 속으로 빗나가는 종이새 떼
TCP 는 받았는지 확인하고, UDP 는 던지고 확인하지 않습니다.

TCP

  • 연결을 맺고 통로를 만든 뒤 통신합니다
  • 잃어버리는 데이터가 없고 순서도 지켜집니다
  • Blocking 성격이라 느립니다

UDP

  • 연결 없이 목적지 쪽으로 던집니다
  • 받았는지, 순서가 맞는지 보장하지 않습니다
  • 빠릅니다

TCP 의 가장 큰 약점은 기다림입니다. 한 사람과 주고받는 동안 다른 사람이 기다려야 하면 서버가 멈춥니다. 그래서 접속한 사람(Peer)마다 쓰레드를 하나씩 붙여 동시에 처리하는 멀티 쓰레드 TCP 를 씁니다.

ASYNC

동기와 비동기

동기 (Synchronized)

  • 일을 맡기고 결과가 올 때까지 기다립니다
  • 결과를 받은 뒤 다음 단계로 갑니다
  • 예: 상점에서 값을 치르고 아이템을 가방에 옮긴 뒤 결과를 알려 줄 때

비동기 (Asynchronized)

  • 일을 맡기고 기다리지 않고 다음 일을 합니다
  • 비동기로 일하려면 쓰레드가 필요합니다
  • 예: 기사가 검으로 치면서 동시에 방패로 상대의 검을 막을 때

상점 구매는 순서가 중요합니다. 값을 치르기 전에 아이템이 들어오면 안 됩니다. 전투는 동시성이 중요합니다. 공격과 방어가 차례를 기다리면 어색합니다. 같은 게임 안에서도 일의 성격에 따라 동기와 비동기를 골라 씁니다.

SERIAL

직렬화, 객체를 줄 세워 보내기

받침대 위 에드윈 인형이 하트 · 방패 · 이름 구슬로 풀려 유리관을 지나 반대편에서 같은 인형으로 다시 만들어지는 그림
객체를 줄지어 풀어 보내고, 받는 쪽에서 다시 조립합니다.
  1. Serialization객체를 앞에서 뒤로 읽을 수 있게 줄 세움
  2. Network Streaming줄 선 데이터를 물 흐르듯 보냄
  3. Deserialization받은 줄을 다시 객체로

디스크와 메모리의 데이터는 원래 줄지어 놓여 있습니다. 읽을 때는 커서가 맨 앞에서 맨 뒤로 움직이며 차례로 읽습니다. 이렇게 앞에서부터 차례로 읽을 수 있는 모양으로 데이터를 만드는 것이 직렬화입니다. Unity 의 SerializeField 가 같은 말에서 왔습니다.

보내는 쪽과 받는 쪽은 약속(프로토콜)으로 "몇 번째 자리에 무엇이 있는지" 를 맞춥니다. 받는 쪽이 줄을 풀어 객체로 되돌리는 과정이 역직렬화이고, 약속대로 했다면 처음 보낸 객체와 똑같은 객체가 만들어집니다.

File Serialization설명용 예시 — 4강 11~12쪽 실습
[Serializable]
public class Character
{
    public int ID;
    public string Name;
    public int AttackPoint;
    public int HealthPoint;

    public void Serialize(BinaryWriter writer)       // 정한 순서대로 쓴다
    {
        writer.Write(ID);
        writer.Write(Name);
        writer.Write(AttackPoint);
        writer.Write(HealthPoint);
    }

    public static Character Deserialize(BinaryReader reader)   // 같은 순서대로 읽는다
    {
        return new Character
        {
            ID = reader.ReadInt32(),
            Name = reader.ReadString(),
            AttackPoint = reader.ReadInt32(),
            HealthPoint = reader.ReadInt32(),
        };
    }
}

쓰는 순서와 읽는 순서가 하나라도 어긋나면 이름 자리에 공격력이 들어갑니다. 직렬화에서 가장 중요한 것은 양쪽이 같은 약속을 지키는 일입니다.

SECURE

암호화와 압축

보안

암호화

패킷을 빼앗겼을 때 내용을 읽을 수 있으면 해킹 피해로 이어집니다. 암호화로 내용을 감추고, 받는 쪽에서 복호화합니다.

양

패킷 압축

보낼 양이 클 때 줄여 네트워크를 아낍니다. 파일 압축과 같은 알고리즘이지만 메모리 안에서 하므로 훨씬 빠릅니다.

비용

대가

보낼 때와 받을 때마다 암호화 · 복호화, 압축 · 해제를 해야 하므로 CPU 와 메모리를 씁니다.

암호화와 압축은 반드시 필요한 구현이지만 공짜가 아닙니다. 어떤 패킷에 적용할지 고르는 것이 설계입니다.

PACKET

패킷 설계

패킷은 클래스로 정의합니다. 공통 부모 GamePacket 을 두고, 주고받을 말마다 자식 클래스를 만들었습니다.

게임 패킷 클래스GamePacketToString()GamePacket_SendLogin아이디비밀번호GamePacket_SendLogin_Response유저 ID닉네임로그인 성공 여부서버 점검 중 여부GamePacket_BroadcastChat유저 닉네임채널채팅 내용GamePacket_SendChat유저 ID채널채팅 내용
로그인 요청 · 응답(초록)과 채팅 보내기 · 퍼뜨리기(빨강) 패킷이 GamePacket 을 상속합니다.
패킷방향담는 값
SendLogin클라이언트 → 서버아이디, 비밀번호
SendLogin_Response서버 → 클라이언트유저 ID, 닉네임, 로그인 성공 여부, 서버 점검 중 여부
SendChat클라이언트 → 서버유저 ID, 채널, 채팅 내용
BroadcastChat서버 → 모든 클라이언트유저 닉네임, 채널, 채팅 내용

SendChat 은 유저 ID 를, BroadcastChat 은 닉네임을 담는다는 차이를 보세요. 서버는 ID 를 받아 닉네임으로 바꿔 퍼뜨립니다. 받는 사람에게 필요한 것은 ID 가 아니라 화면에 띄울 이름이기 때문입니다.

실습: C# Serializable 클래스
  1. GamePacket 클래스를 만듭니다.
  2. PacketTypes enum 에 SendChat, SendChat_Response 를 선언합니다.
  3. ChatTypes enum 에 ToAll, ToUser 를 선언합니다.
  4. 모든 타입에 [Serializable] 을 붙입니다.
GamePacket.cs설명용 예시 — 위 실습의 답
[Serializable] public enum PacketTypes { SendChat, SendChat_Response }
[Serializable] public enum ChatTypes { ToAll, ToUser }

[Serializable]
public class GamePacket
{
    public PacketTypes Type;
    public override string ToString() => Type.ToString();
}

[Serializable]
public class GamePacket_SendChat : GamePacket
{
    public long UserID;
    public ChatTypes Channel;
    public string Message;
}
THREAD

쓰레드를 쓸 때 조심할 것

문제

공유 메모리 문제

쓰레드 여럿이 같은 데이터를 동시에 건드리면 값이 엉킵니다. 예: 재화가 빠지기 전에 아이템 구매가 연달아 들어오는 경우

해결

Critical Section

공유 데이터를 쓰는 구간에는 한 번에 쓰레드 하나가 들어가게 막습니다. C# 에서는 lock 키워드를 씁니다.

주의

Dead Lock

두 쓰레드가 서로의 일이 끝나기를 기다리며 영원히 멈추는 현상입니다. 잠금 구간을 잘못 짜면 생깁니다.

lock설명용 예시 — 재화 차감과 아이템 지급을 한 구간으로
private readonly object goldLock = new object();

public bool Buy(Item item)
{
    lock (goldLock)                 // 이 안에는 한 번에 쓰레드 하나가 들어온다
    {
        if (gold < item.Price) return false;
        gold -= item.Price;
        inventory.Add(item);
        return true;
    }
}

lock 이 없으면 두 쓰레드가 동시에 gold < item.Price 를 통과해 돈이 하나 값으로 아이템 두 개를 살 수 있습니다. 공유 데이터를 바꾸는 코드는 언제나 "동시에 두 번 들어오면?" 을 먼저 물어봐야 합니다.

NOW

지금 우리 서버에서는

패킷을 클래스로 정의하고 공용 라이브러리로 나눠 쓰는 방식은 우리 C# 서버 라이브러리 ElServer 에서도 그대로입니다(ElServer 리뷰). 실제 서비스에서 네트워크가 끊겼을 때 무엇을 바꿨는지는 구조 읽기 6편에 정리했습니다.

정리

  1. TCP 는 확인하고, UDP 는 던집니다. 게임은 용도에 따라 둘 다 씁니다.
  2. 직렬화는 순서의 약속입니다. 쓰는 순서와 읽는 순서가 같아야 같은 객체가 돌아옵니다.
  3. 공유 데이터는 lock 으로. 동시에 두 번 들어오는 경우를 먼저 생각합니다.

다음 편에서는 소켓을 열고 JSON 으로 패킷을 주고받아 채팅 프로그램을 만듭니다.

← 개발 노트 목록