Post

2026-07-30 TIL (108일차)

2026-07-30 TIL (108일차)

A* 길찾기 / RAII와 스마트 포인터 / TCP vs UDP

모의면접때 부족했던 부분을 다시 공부했습니다.


Part 1. A* 알고리즘

1. A*가 필요한 이유

알고리즘탐색 방식한계
BFS가까운 순서로 전방위 확장가중치(지형 이동 비용)를 반영 못 함
다익스트라미확정 정점 중 g가 최소인 것 확장목표 방향을 몰라 반대편까지 균등하게 퍼짐
A*g + h가 최소인 것 확장휴리스틱 설계에 따라 최적성이 좌우됨

다익스트라는 목표가 어디인지 모르기 때문에 시작점을 중심으로 원형으로 퍼집니다. 목표가 특정 한 곳임을 알고 있다면 그 정보를 쓰는 게 당연히 유리하고, 그 정보를 넣은 것이 A*입니다.

2. 핵심 수식

\[f(n) = g(n) + h(n)\]
의미성질
$g(n)$시작점부터 n까지의 실제 누적 비용이미 확정된 과거 정보
$h(n)$n부터 목표까지의 예상 비용(휴리스틱)추정치, 설계자가 정함
$f(n)$n을 경유하는 총 예상 비용이 값이 최소인 노드를 먼저 확장
  • $h(n) = 0$ → 다익스트라와 완전히 동일해집니다. (A*는 다익스트라의 일반화)
  • $g(n) = 0$ → 목표 방향으로만 달리는 Greedy Best-First Search. 매우 빠르지만 최단 경로가 아닙니다.
  • 즉 A*는 “이미 쓴 비용”과 “남은 비용 추정”의 균형을 잡는 알고리즘입니다.

3. 동작 절차

  1. 시작 노드를 Open List에 넣고 g=0, f=h(start)로 초기화
  2. Open List에서 f가 최소인 노드를 꺼냄 → 이 노드를 current로 확정
  3. current가 목표면 종료 → parent를 역추적해 경로 복원
  4. currentClosed List로 이동
  5. 인접 노드마다 tentative_g = g[current] + cost(current, neighbor) 계산
    • 기존 g[neighbor]보다 작으면 갱신하고 parent[neighbor] = current 기록 후 Open List에 push
  6. Open List가 빌 때까지 2번으로 반복 → 비면 경로 없음
목록역할자료구조이유
Open List아직 확장하지 않은 후보우선순위 큐(최소 힙)f 최소를 $O(\log n)$에 추출
Closed List확장을 끝낸 노드해시셋 / 불리언 배열재방문 여부 조회가 $O(1)$

“왜 그 자료구조인가”를 설명할 수 있어야 합니다. Open List에 vector를 쓰고 매번 최소를 선형 탐색하면 $O(n)$이 되어 전체 성능이 무너집니다.

4. 격자 기반 구현 (C++)

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
struct Node {
    int idx;      // y * W + x
    float f;
    bool operator>(const Node& o) const { return f > o.f; }  // 최소 힙용
};

std::vector<int> AStar(int startIdx, int goalIdx, int W, int H,
                       const std::vector<bool>& blocked)
{
    const int INF = std::numeric_limits<int>::max();
    std::vector<float> g(W * H, INF);
    std::vector<int>   parent(W * H, -1);
    std::vector<bool>  closed(W * H, false);

    auto heuristic = [&](int a, int b) {           // 맨해튼 (4방향 이동)
        int ax = a % W, ay = a / W, bx = b % W, by = b / W;
        return static_cast<float>(std::abs(ax - bx) + std::abs(ay - by));
    };

    std::priority_queue<Node, std::vector<Node>, std::greater<Node>> open;
    g[startIdx] = 0.f;
    open.push({startIdx, heuristic(startIdx, goalIdx)});

    const int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};

    while (!open.empty()) {
        Node cur = open.top(); open.pop();
        if (closed[cur.idx]) continue;             // ★ stale 항목 스킵
        if (cur.idx == goalIdx) break;
        closed[cur.idx] = true;

        int cx = cur.idx % W, cy = cur.idx / W;
        for (int d = 0; d < 4; ++d) {
            int nx = cx + dx[d], ny = cy + dy[d];
            if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
            int nIdx = ny * W + nx;
            if (blocked[nIdx] || closed[nIdx]) continue;

            float tentative = g[cur.idx] + 1.f;    // 이동 비용(지형별로 가변 가능)
            if (tentative < g[nIdx]) {             // 더 좋은 경로 발견
                g[nIdx] = tentative;
                parent[nIdx] = cur.idx;
                open.push({nIdx, tentative + heuristic(nIdx, goalIdx)});
            }
        }
    }

    std::vector<int> path;                          // 경로 복원
    if (parent[goalIdx] == -1 && startIdx != goalIdx) return path;  // 실패
    for (int at = goalIdx; at != -1; at = parent[at]) path.push_back(at);
    std::reverse(path.begin(), path.end());
    return path;
}

구현 포인트 3가지

  • if (closed[cur.idx]) continue; — STL 우선순위 큐는 decrease-key를 지원하지 않아 같은 노드가 여러 번 push됩니다. 이 한 줄로 오래된 항목을 걸러내야 하며, 없으면 성능이 크게 나빠집니다. (다익스트라와 동일한 패턴)
  • parent 배열이 없으면 비용만 알고 경로는 모릅니다. 게임에서는 이 복원된 배열이 실제 AI 이동 포인트가 됩니다.
  • 이동 비용을 1.f 고정이 아니라 지형별 값으로 바꾸면 그대로 가중 그래프 탐색이 됩니다. (늪 3배, 도로 0.5배 등)

5. 휴리스틱 — A*의 성패를 결정하는 부분

두 가지 조건

조건수식만족하면
허용성 (Admissible)$h(n) \le$ 실제 최단 비용최적해 보장
일관성 (Consistent)$h(n) \le cost(n, n’) + h(n’)$Closed List 노드를 재방문할 필요 없음 → 구현 단순화

핵심은 절대 과대평가하지 않는 것입니다. 과대평가하면 실제로는 더 짧은 경로를 “비싸 보인다”고 판단해 건너뛰기 때문에 최단 경로가 아닌 답이 나옵니다. 반대로 과소평가는 안전하지만, 0에 가까울수록 다익스트라처럼 느려집니다.

정리: $h$가 작으면 정확하지만 느리고, 크면 빠르지만 부정확합니다. 실제 비용에 최대한 가깝게, 단 넘지 않게 설계하는 것이 목표입니다.

이동 방식별 올바른 휴리스틱

이동 방식휴리스틱수식
4방향 (상하좌우)맨해튼$\lvert dx \rvert + \lvert dy \rvert$
8방향 (대각선 허용)옥타일$(dx+dy) + (\sqrt{2}-2)\min(dx,dy)$
자유 방향 (NavMesh)유클리드$\sqrt{dx^2 + dy^2}$
어떤 방향이든 1턴체비셰프$\max(\lvert dx \rvert, \lvert dy \rvert)$

흔한 실수: 8방향 이동인데 맨해튼을 쓰면 과대평가가 됩니다. 대각선 1칸의 실제 비용은 $\sqrt{2} \approx 1.41$인데 맨해튼은 2로 추정하기 때문입니다. → 최적성이 깨집니다.

튜닝 기법

  • Tie-breaking: $h$에 1.001배를 곱하면 동일한 f값 후보가 줄어 탐색 노드 수가 크게 감소합니다. (최적성을 미세하게 희생)
  • Weighted A*: $f = g + w \cdot h$ ($w > 1$). 최적성을 포기하고 속도를 얻습니다. 실시간 게임에서 “충분히 좋은 경로”면 되는 경우 자주 씁니다.

6. 복잡도와 변형

  • 시간 복잡도는 휴리스틱 품질에 의존합니다. 최악에는 다익스트라와 같은 $O(E \log V)$, 휴리스틱이 완벽하면 경로 길이에 비례합니다.
  • 메모리가 실질적 병목입니다. Open/Closed List가 노드 수만큼 커지므로 대형 맵에서는 아래 변형을 씁니다.
변형개선 포인트
JPS (Jump Point Search)균일 격자에서 대칭 경로를 건너뛰어 노드 수를 극적으로 감소
HPA*맵을 구역으로 나눠 추상 레벨에서 먼저 탐색 (계층적)
IDA*반복 심화로 메모리를 $O(깊이)$로 축소
D* Lite맵이 변할 때 전체 재계산 없이 증분 갱신 (동적 장애물)
Flow Field목표가 하나이고 유닛이 수백 개면 역방향 BFS로 방향 필드를 한 번 만들어 공유 → 유닛당 $O(1)$

7. 언리얼 NavMesh와의 관계

NavMesh는 이동 가능 영역을 볼록 폴리곤으로 미리 분할해 둔 그래프입니다. 폴리곤이 정점, 인접 관계가 간선, 중심 간 거리가 가중치입니다.

  1. A*로 폴리곤 시퀀스(코리도) 탐색 — 어떤 폴리곤들을 지나갈지 결정
  2. Funnel / String Pulling으로 경로 평활화 — 폴리곤 중심을 잇는 지그재그를 실제 최단 직선으로 당김

타일 그리드보다 유리한 이유는 넓은 평지를 폴리곤 하나로 표현해 노드 수가 훨씬 적고, 임의 방향 이동이 자연스럽다는 점입니다.

  • 전역/지역 2계층: A*는 전역 경로만 담당하고, 다른 유닛과의 실시간 충돌은 RVO / Detour Crowd가 처리합니다.
  • 동적 장애물: NavModifier 또는 런타임 리빌드로 처리하되 비용이 크므로 지역 회피와 병용합니다.
  • AI가 100마리일 때: ① 경로 요청 Time-slicing ② 워커 스레드 비동기화 ③ 리더 경로 공유 + 오프셋(플로킹) ④ HPA* ⑤ 거리·화면 여부에 따른 갱신 주기 LOD ⑥ RTS라면 Flow Field.

Part 2. RAII와 스마트 포인터

1. RAII란 무엇인가

Resource Acquisition Is Initialization — 자원의 획득을 객체의 초기화(생성자) 에, 해제를 소멸자에 묶는 기법입니다. 결과적으로 자원의 생명주기가 객체의 스코프와 일치하게 됩니다.

C++에는 GC가 없지만, 대신 스택 객체의 소멸자가 스코프를 벗어날 때 반드시 호출된다는 강력한 보장이 있습니다. RAII는 이 보장을 자원 관리에 활용하는 것입니다.

왜 필요한가 — 수동 관리의 3가지 실패

1
2
3
4
5
6
7
8
9
void Process() {
    Resource* r = new Resource();

    if (!Validate()) return;      // ① 조기 반환 → 누수
    DoWork();                     // ② 예외 발생 → delete 미실행, 누수
    // ③ 유지보수 중 return 추가 → 누수

    delete r;
}
1
2
3
4
5
6
void Process() {
    auto r = std::make_unique<Resource>();   // 획득 = 초기화

    if (!Validate()) return;   // 자동 해제
    DoWork();                  // 예외가 나도 스택 되감기(unwinding) 중 소멸자 호출
}                              // 스코프 종료 = 해제

핵심: RAII의 진짜 가치는 편의성이 아니라 예외 안전성(Exception Safety) 입니다. 예외가 발생하면 delete문은 실행되지 않지만 소멸자는 반드시 호출됩니다.

표준 라이브러리의 RAII 예시

자원RAII 래퍼
힙 메모리unique_ptr / shared_ptr / vector / string
뮤텍스 락lock_guard / unique_lock / scoped_lock
파일 핸들ifstream / ofstream
스레드jthread (C++20)
  • Rule of Zero: 자원을 직접 관리하지 않고 RAII 타입에 위임하면 복사·이동·소멸자를 아예 작성할 필요가 없습니다. 가장 좋은 상태입니다.
  • Rule of Five: 직접 관리해야 한다면 소멸자, 복사 생성자, 복사 대입, 이동 생성자, 이동 대입 다섯 개를 모두 정의해야 합니다.

2. 세 가지 스마트 포인터

종류소유권복사오버헤드용도
unique_ptr단독 소유불가 (이동만)0 (raw pointer와 동일)기본 선택
shared_ptr공유 소유가능 (참조 카운트 증가)포인터 2개 크기 + 원자적 카운트소유자가 여럿일 때만
weak_ptr소유하지 않음(관찰)가능카운트 조작 없음순환 참조 해결, 캐시

unique_ptr — 기본값으로 삼을 것

1
2
3
auto p = std::make_unique<Actor>(args);   // new 직접 호출 금지
auto q = std::move(p);                    // 소유권 이전, p는 nullptr
// auto r = p;                            // ❌ 컴파일 에러 — 복사 불가가 곧 안전장치
  • 런타임 오버헤드가 0입니다. 크기도 raw pointer와 같고, 소멸자 호출은 컴파일 타임에 결정됩니다. 즉 성능을 이유로 raw pointer를 고집할 근거가 없습니다.
  • 배열은 std::unique_ptr<T[]>delete[]를 호출합니다. (다만 실무에선 vector가 나음)
  • 커스텀 deleter로 FILE*, 소켓, API 핸들 등 메모리가 아닌 자원도 감쌀 수 있습니다.

shared_ptr — 비용을 알고 써야 함

1
2
auto a = std::make_shared<Texture>();   // 제어 블록 + 객체를 한 번에 할당
auto b = a;                             // 참조 카운트 2

내부적으로 객체 포인터 + 제어 블록 포인터 두 개를 가지며, 제어 블록에 강한 참조 카운트와 약한 참조 카운트가 있습니다.

  • make_shared를 쓰는 이유: ① 할당이 1회로 줄어 캐시에 유리 ② 생성 중 예외가 나도 누수 없음. 단, weak_ptr이 하나라도 살아 있으면 제어 블록이 유지되어 객체 메모리도 함께 붙잡혀 있습니다. 큰 객체 + 오래 사는 weak_ptr 조합에서는 오히려 shared_ptr<T>(new T)가 나을 수 있습니다.
  • 참조 카운트는 원자적 연산이라 멀티스레드에서 비용이 있습니다. 루프에서 shared_ptr을 값으로 복사하면 카운트가 계속 오르내리므로, 읽기만 할 때는 const&로 받아야 합니다.
  • 카운트 자체는 스레드 안전하지만 가리키는 객체의 데이터는 스레드 안전하지 않습니다. 자주 나오는 함정입니다.

weak_ptr — 순환 참조 끊기

1
2
3
4
5
6
7
8
struct Node {
    std::shared_ptr<Node> child;
    std::weak_ptr<Node>   parent;   // ← shared_ptr이면 카운트가 0이 안 됨
};

if (auto p = node->parent.lock()) {   // 사용 시점에 승격
    p->DoSomething();                 // 살아 있으면 유효한 shared_ptr
}                                     // 죽었으면 nullptr

부모와 자식이 서로 shared_ptr을 들면 카운트가 서로 1씩 남아 영원히 해제되지 않습니다. 소유권이 없는 방향(보통 역방향 참조, 옵저버, 캐시)은 weak_ptr로 잡습니다.

3. 함수 인터페이스 가이드

의도시그니처
소유권 이전void f(std::unique_ptr<T> p)
소유권 공유void f(std::shared_ptr<T> p)
그냥 사용만 (null 불가)void f(T& t)
그냥 사용만 (null 허용)void f(T* t)
스마트 포인터 자체를 재지정void f(std::unique_ptr<T>& p)

자원을 소유하지 않는 함수에 shared_ptr을 넘기는 것은 불필요한 카운트 조작 + 의도 왜곡입니다. “이 함수는 수명에 관여하지 않는다”를 시그니처로 표현하세요.

4. 흔한 실수

  • ❌ 같은 raw pointer로 shared_ptr두 번 만들기 → 제어 블록이 두 개 생겨 이중 해제
  • ❌ 멤버 함수에서 shared_ptr<T>(this) 반환 → 위와 같은 이중 해제. enable_shared_from_this + shared_from_this()를 써야 합니다.
  • get()으로 얻은 포인터를 delete
  • ❌ 순환 참조 방치 → 조용한 누수 (가장 찾기 어려움)
  • ❌ 성능 걱정으로 unique_ptr 회피 → 오버헤드가 0이므로 근거 없음

5. 언리얼에서의 대응 관계 (중요)

언리얼은 표준 스마트 포인터를 쓰지 않고, UObject인지 아닌지에 따라 관리 방식이 완전히 갈립니다.

대상관리 방법
UObject 파생GC(가비지 컬렉션) 가 담당. UPROPERTY()로 선언해 참조를 GC에 알림
UObject 약한 참조TWeakObjectPtr<T> — 파괴되면 자동으로 무효화, IsValid() 체크
UObject 포인터 필드 (UE5)TObjectPtr<T> — 에디터에서 지연 로딩·접근 추적 지원
일반 C++ 객체TSharedPtr / TSharedRef / TWeakPtr / TUniquePtr
  • UObjectstd::shared_ptr이나 TSharedPtr을 쓰면 안 됩니다. GC와 참조 카운트가 이중으로 수명을 관리해 크래시가 납니다.
  • UPROPERTY()를 빼면 GC가 참조를 모르기 때문에 살아 있어야 할 객체가 수거되어 댕글링 포인터가 됩니다. 원인 찾기 어려운 크래시의 대표 원인입니다.
  • TSharedRefnull이 될 수 없는 버전이라, null 체크가 불필요한 곳에 쓰면 의도가 코드로 드러납니다.
  • RAII 사고방식은 언리얼에서도 유효합니다. FScopeLock, FScopedSlowTask 등이 같은 패턴입니다.

Part 3. TCP vs UDP

1. 위치와 역할

둘 다 전송 계층(Transport Layer) 프로토콜로, IP가 제공하는 “최선 노력 전달(Best-effort)” 위에서 애플리케이션 간 통신(포트 단위) 을 담당합니다. IP는 패킷의 도착·순서·중복을 보장하지 않으며, 그 신뢰성을 TCP가 얹고, UDP는 얹지 않습니다.

2. 핵심 비교

항목TCPUDP
연결연결 지향 (핸드셰이크 필요)비연결형
신뢰성재전송으로 전달 보장보장 없음 (유실 허용)
순서보장보장 없음
중복 제거안 함
전송 단위바이트 스트림 (경계 없음)데이터그램 (경계 유지)
흐름 제어슬라이딩 윈도우없음
혼잡 제어있음 (네트워크 상황에 따라 속도 조절)없음
헤더 크기20바이트 이상8바이트
속도·지연상대적으로 느림, 지연 변동 큼빠름, 지연 일정
1:N 전송불가 (1:1)브로드캐스트 / 멀티캐스트 가능

바이트 스트림 vs 데이터그램이 실무에서 가장 자주 문제를 일으킵니다. TCP는 send를 3번 해도 상대가 recv 1번에 뭉쳐 받을 수 있어(TCP는 메시지 경계를 보존하지 않음) 반드시 길이 헤더 기반 파싱이 필요합니다. UDP는 보낸 단위 그대로 도착합니다.

3. TCP의 연결 관리

3-way handshake (연결 수립)

1
2
3
4
Client                          Server
   │──────── SYN (seq=x) ────────▶│   내 초기 순서번호는 x
   │◀──── SYN-ACK (seq=y,ack=x+1)─│   확인했고, 내 초기 순서번호는 y
   │──────── ACK (ack=y+1) ──────▶│   너의 번호도 확인

왜 3번인가: 양쪽이 서로의 초기 순서번호(ISN)를 교환하고 확인해야 합니다. 2번이면 서버는 자기 SYN이 도달했는지 알 수 없고, 지연된 옛 SYN이 도착해 잘못된 연결이 열릴 수 있습니다.

4-way handshake (연결 종료)

1
2
3
4
   │──────── FIN ───────▶│
   │◀─────── ACK ────────│   "종료 요청 받았음"
   │◀─────── FIN ────────│   (남은 데이터 다 보낸 뒤)
   │──────── ACK ───────▶│   → TIME_WAIT 상태로 대기

왜 4번인가: TCP는 전이중(full-duplex)이라 방향별로 따로 닫아야 합니다. 한쪽이 끝냈어도 반대쪽은 아직 보낼 데이터가 남아 있을 수 있습니다(Half-Close).

  • TIME_WAIT: 마지막 ACK가 유실됐을 때 재전송에 응답하고, 옛 패킷이 새 연결에 섞이는 것을 막기 위해 일정 시간(보통 2×MSL) 대기합니다. 서버가 짧은 연결을 대량 처리하면 포트가 고갈되는 원인이 됩니다.

4. TCP가 신뢰성을 만드는 방법

메커니즘동작
순서번호 / ACK바이트 단위 번호 부여, 수신측이 누적 확인 응답
재전송 타이머 (RTO)ACK가 안 오면 재전송. RTT 측정으로 동적 조정
빠른 재전송중복 ACK 3개를 받으면 타이머 만료 전에 즉시 재전송
SACK받은 구간을 선택적으로 알려 필요한 부분만 재전송
슬라이딩 윈도우(흐름 제어)수신 버퍼 여유만큼만 보내 수신자를 보호
혼잡 제어Slow Start → 혼잡 회피(AIMD) → 유실 시 윈도우 축소, 네트워크를 보호

게임에서 TCP가 불리한 이유

  • Head-of-Line Blocking: 패킷 1개가 유실되면 그 뒤에 이미 도착한 패킷들도 순서 보장 때문에 애플리케이션에 전달되지 않습니다. 재전송에 최소 1 RTT가 걸리므로 그 시간 동안 캐릭터가 멈춥니다. 실시간 게임에서는 오래된 위치 정보를 정확히 받는 것보다 최신 정보를 빨리 받는 것이 중요하므로 이 특성이 치명적입니다.
  • 혼잡 제어의 부작용: 유실을 혼잡 신호로 해석해 전송 속도를 스스로 줄입니다. 무선 환경의 일시적 유실에도 반응해 불필요하게 느려집니다.
  • Nagle + Delayed ACK: 작은 패킷을 모아 보내는 Nagle 알고리즘과 지연 ACK가 겹치면 수십~수백 ms 지연이 생깁니다. → 게임·실시간 통신에서는 TCP_NODELAY로 Nagle을 끕니다.

5. UDP와 “자체 신뢰성”

UDP 헤더는 출발지 포트 / 목적지 포트 / 길이 / 체크섬 8바이트뿐입니다. 그래서 필요한 신뢰성만 골라 직접 구현할 수 있습니다.

필요한 것직접 구현 방식
유실 감지자체 시퀀스 번호 부여
확인 응답ACK 비트필드(최근 32개 수신 여부를 정수 하나로)
순서 보정시퀀스 기준 정렬 + 오래된 패킷은 버림
중요 데이터만 보장채널 분리 — Reliable / Unreliable 채널
끊김 은폐클라이언트 예측(Prediction) + 보간(Interpolation) + 서버 보정

핵심 사고방식은 “모든 걸 보장하지 않고, 보장이 필요한 것만 골라 보장한다” 입니다. 위치 정보는 유실돼도 다음 패킷이 최신 상태를 담고 있으므로 재전송할 필요가 없습니다.

6. 언리얼 네트워킹과의 연결

  • 언리얼의 NetDriver(IpNetDriver)는 UDP 기반이며, 위 신뢰성 계층을 엔진이 자체 구현합니다.
  • RPC 지정자: Reliable은 도착과 순서를 보장하지만 큐가 밀리면 연결이 끊길 수 있어 꼭 필요한 이벤트(사망, 결제, 상태 전이)에만 씁니다. Unreliable은 매 프레임 갱신되는 값(이펙트, 잦은 위치 갱신)에 적합합니다.
  • 프로퍼티 리플리케이션은 “최신 상태만 맞으면 된다”는 원리로 동작합니다. 중간 값이 유실돼도 최종 값이 도착하면 동기화가 맞습니다. 이게 재전송보다 상태 동기화가 우월한 대표 사례입니다.
  • 대역폭 관리는 NetUpdateFrequency, NetCullDistanceSquared, 우선순위(Priority)로 조절합니다.

7. 선택 기준

용도선택이유
로그인, 결제, 인벤토리TCP한 번의 유실도 허용 불가
채팅, 매치메이킹TCP순서와 전달 보장 필요, 지연에 둔감
캐릭터 위치·입력 동기화UDP최신성이 정확성보다 중요
음성 채팅(VoIP)UDP한 프레임 유실보다 지연이 치명적
실시간 대전UDP + 자체 신뢰성선택적 보장
스트리밍(HLS/DASH)TCP(HTTP)버퍼링으로 지연을 흡수
  • QUIC / HTTP3: UDP 위에 신뢰성·암호화·멀티플렉싱을 재구현한 프로토콜입니다. 스트림별로 독립적이라 TCP의 Head-of-Line Blocking을 해결하고, 연결 수립도 더 빠릅니다. “UDP는 신뢰성이 없어서 못 쓴다”가 아니라 “신뢰성을 애플리케이션이 설계한다” 는 흐름을 보여주는 사례입니다.

오늘의 한 줄 정리

  • A*: 다익스트라 + 목표 방향 정보. 휴리스틱은 과대평가하지 않는 선에서 최대한 정확하게.
  • RAII: 자원 수명을 스코프에 묶어 예외 상황에서도 누수가 없게 만드는 것. 기본은 unique_ptr, 공유가 진짜 필요할 때만 shared_ptr, 순환은 weak_ptr.
  • TCP/UDP: 신뢰성을 프로토콜이 주느냐, 애플리케이션이 설계하느냐의 차이. 실시간 게임은 후자를 택한다.
This post is licensed under CC BY 4.0 by the author.