์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

Unity C# Job System๊ณผ Burst Compiler๋ฅผ ํ™œ์šฉํ•œ ๋Œ€๊ทœ๋ชจ NPC ๊ธธ์ฐพ๊ธฐ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ NativeArray ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ

๊ฒŒ์ž„๊ต์ˆ˜ 2026. 9. 6. 13:01
๋ฐ˜์‘ํ˜•

๐ŸŒ English Abstract

This technical article explores how to eliminate main thread performance bottlenecks during large-scale NPC pathfinding in Unity by combining the C# Job System and Burst Compiler. It provides a step-by-step troubleshooting guide for resolving NativeArray memory leaks, managing native container lifecycles, and handling edge cases in multi-threaded environments. Readers will gain actionable engineering insights into profiling, job dependency management, and high-performance data-oriented programming.

์˜คํ”ˆ์›”๋“œ ๊ฒŒ์ž„์ด๋‚˜ ๋Œ€๊ทœ๋ชจ RTS(Real-Time Strategy) ์žฅ๋ฅด๋ฅผ ๊ฐœ๋ฐœํ•  ๋•Œ ๊ฐ€์žฅ ํ”ํžˆ ์ง๋ฉดํ•˜๋Š” ๊ธฐ์ˆ ์  ๋‚œ์ œ ์ค‘ ํ•˜๋‚˜๋Š” ์ˆ˜๋ฐฑ์—์„œ ์ˆ˜์ฒœ ๊ฐœ์— ๋‹ฌํ•˜๋Š” NPC(Non-Player Character)์˜ ์‹ค์‹œ๊ฐ„ ๊ธธ์ฐพ๊ธฐ ์—ฐ์‚ฐ(Pathfinding Computation)์ž…๋‹ˆ๋‹ค. ์œ ๋‹ˆํ‹ฐ์˜ ๊ธฐ๋ณธ ์‹œ์Šคํ…œ์ธ NavMeshAgent๋Š” ์†์‰ฌ์šด ์‚ฌ์šฉ์„ฑ์„ ์ œ๊ณตํ•˜์ง€๋งŒ, ๋‹จ์ผ ์Šค๋ ˆ๋“œ ๊ตฌ์กฐ์— ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐœ์ฒด ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚˜๋ฉด ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ(Main Thread Bottleneck)๊ณผ ํ”„๋ ˆ์ž„ ๋“œ๋กญ(Frame Drop)์„ ์œ ๋ฐœํ•ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ํ•œ๊ณ„๋ฅผ ๊ทน๋ณตํ•˜๊ธฐ ์œ„ํ•ด ์œ ๋‹ˆํ‹ฐ์˜ C# Job System๊ณผ Burst Compiler๋ฅผ ๋„์ž…ํ•˜์—ฌ ์—ฐ์‚ฐ์„ ๋ฐ์ดํ„ฐ ์ง€ํ–ฅ ๊ตฌ์กฐ(Data-Oriented Design)๋กœ ์ „ํ™˜ํ•˜๋Š” ๊ธฐ๋ฒ•์ด ๋„๋ฆฌ ํ™œ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ ํ™˜๊ฒฝ์œผ๋กœ ์ „ํ™˜ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋น„๊ด€๋ฆฌํ˜• ๋ฉ”๋ชจ๋ฆฌ(Unmanaged Memory)์ธ NativeArray์˜ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ๋กœ ์ธํ•ด ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜(Memory Leak)๋‚˜ ๋™์‹œ์„ฑ ์ถฉ๋Œ(Race Condition) ๋ฌธ์ œ๊ฐ€ ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ๋Š” ์‹ค์ œ ํ˜„์—… ์—ฐ๋™ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๊ธฐ์ˆ ์  ๋ณ‘๋ชฉ์„ ๋ถ„์„ํ•˜๊ณ , ์ด๋ฅผ ์ตœ์ ํ™”ํ•œ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ์‚ฌ๋ก€๋ฅผ ๊นŠ์ด ์žˆ๊ฒŒ ๋‹ค๋ฃน๋‹ˆ๋‹ค.



1. ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ ์›์ธ๊ณผ ๋ฐ์ดํ„ฐ ์ง€ํ–ฅ ํŒจ๋Ÿฌ๋‹ค์ž„ ์ „ํ™˜

๊ธฐ์กด์˜ ๊ฐ์ฒด ์ง€ํ–ฅ ๋ฐฉ์‹(Object-Oriented Programming, OOP)์œผ๋กœ ๊ตฌํ˜„๋œ A* ๊ธธ์ฐพ๊ธฐ ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ๋‹จ์ผ ํ”„๋ ˆ์ž„ ๋‚ด์—์„œ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์˜ CPU ์ž์›์„ ๊ณผ๋„ํ•˜๊ฒŒ ์ ์œ ํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜๋ฐฑ ๋ช…์˜ NPC๊ฐ€ ๋™์‹œ์— ๊ธธ์ฐพ๊ธฐ๋ฅผ ์š”์ฒญํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ์ค‘์ฒฉ๋˜์–ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

  • ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜(Garbage Collection, GC) ๋ถ€ํ•˜: ๋งค ํ”„๋ ˆ์ž„ ๋…ธ๋“œ ํƒ์ƒ‰์„ ์œ„ํ•ด ์ƒ์„ฑ๋˜๋Š” C# ํด๋ž˜์Šค ๊ฐ์ฒด(Node Object)๋“ค์ด ํž™ ๋ฉ”๋ชจ๋ฆฌ(Heap Memory)์— ํ• ๋‹น๋˜์–ด ์ฃผ๊ธฐ์ ์ธ GC ์ŠคํŒŒ์ดํฌ๋ฅผ ์œ ๋ฐœํ•ฉ๋‹ˆ๋‹ค.
  • L1/L2 ์บ์‹œ ๋ฏธ์Šค(Cache Miss): ์ฐธ์กฐ ํƒ€์ž…(Reference Type) ๋ฐ์ดํ„ฐ๋“ค์ด ๋ฉ”๋ชจ๋ฆฌ์ƒ์— ํŒŒํŽธํ™”๋˜์–ด ๋ฐฐ์น˜๋˜๋ฏ€๋กœ, CPU ์บ์‹œ ํšจ์œจ์ด ๊ธ‰๊ฒฉํžˆ ๋–จ์–ด์ง‘๋‹ˆ๋‹ค.
  • ์ฝ”์–ด ํ™œ์šฉ ๋ถˆ๊ท ํ˜•: ๋‹ค์ค‘ ์ฝ”์–ด(Multi-Core) ํ™˜๊ฒฝ์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ํ•˜๋‚˜๋งŒ 100% ๊ฐ€๋™๋˜๊ณ  ๋‚˜๋จธ์ง€ ์ž‘์—… ์Šค๋ ˆ๋“œ(Worker Thread)๋Š” ์œ ํœด ์ƒํƒœ์— ๋น ์ง‘๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ๊ตฌ์กฐ์ฒด(Struct) ์ค‘์‹ฌ์˜ ์—ฐ์†๋œ ๋ฉ”๋ชจ๋ฆฌ ๋ฐฐ์—ด์„ ๊ตฌ์„ฑํ•˜๊ณ , ์—ฐ์‚ฐ ๋กœ์ง์„ C# Job System์œผ๋กœ ๊ฒฉ๋ฆฌํ•˜์—ฌ ์ž‘์—… ์Šค๋ ˆ๋“œ(Worker Threads)๋กœ ๋ถ„์‚ฐ์‹œ์ผœ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— LLVM ๊ธฐ๋ฐ˜์˜ Burst Compiler๋ฅผ ๊ฒฐํ•ฉํ•˜๋ฉด CIL(Common Intermediate Language) ์ฝ”๋“œ๊ฐ€ ์ตœ์ ํ™”๋œ ์‹ฌ๋“œ(SIMD, Single Instruction Multiple Data) ์–ด์…ˆ๋ธ”๋ฆฌ ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜๋˜์–ด ์ˆ˜์‹ญ ๋ฐฐ ์ด์ƒ์˜ ์—ฐ์‚ฐ์†๋„ ํ–ฅ์ƒ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2. C# Job System & Burst Compiler ๊ธฐ๋ฐ˜ ๊ธธ์ฐพ๊ธฐ ๊ตฌ์กฐ ์„ค๊ณ„

๋Œ€๊ทœ๋ชจ ๊ธธ์ฐพ๊ธฐ๋ฅผ ๋ณ‘๋ ฌํ™”ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋จผ์ € ๊ทธ๋ฆฌ๋“œ(Grid) ์ •๋ณด์™€ ์ด๋™ ๊ฒฝ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณ ์„ฑ๋Šฅ ์ปจํ…Œ์ด๋„ˆ์ธ NativeArray ๋ฐ NativeParallelHashMap์œผ๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Burst Compiler์˜ ์ œ์•ฝ ์‚ฌํ•ญ์ƒ, ๊ด€๋ฆฌํ˜• ๊ฐ์ฒด(Managed Object)์ธ Class๋‚˜ Reference ํƒ€์ž…์€ Job ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ์˜ค์ง ๋ธ”๋ฆฌํ„ฐ๋ธ” ํƒ€์ž…(Blittable Types)๊ณผ ๊ตฌ์กฐ์ฒด๋งŒ ํ™œ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

A* ์•Œ๊ณ ๋ฆฌ์ฆ˜์šฉ Native Parallel Job ๊ตฌํ˜„ ์˜ˆ์‹œ

using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

[BurstCompile(FloatMode = FloatMode.Fast, FloatPrecision = FloatPrecision.Standard, CompileSynchronously = true)]
public struct PathfindingJob : IJobParallelFor
{
    [ReadOnly] public NativeArray<float3> StartPositions;
    [ReadOnly] public NativeArray<float3> TargetPositions;
    [ReadOnly] public NativeArray<bool> GridObstacleMap;
    public int GridWidth;
    public int GridHeight;

    // ๊ธธ์ฐพ๊ธฐ ๊ฒฐ๊ณผ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•  ๊ธฐ๋ณธ ์ปจํ…Œ์ด๋„ˆ (์ธ๋ฑ์Šค๋ณ„ ์ฐธ์กฐ)
    [NativeDisableParallelForRestriction]
    public NativeArray<UnsafeList<float3>> CalculatedPaths;

    public void Execute(int index)
    {
        float3 start = StartPositions[index];
        float3 target = TargetPositions[index];

        // A* Pathfinding ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์—ฐ์‚ฐ ์ˆ˜ํ–‰ (Value Type ๊ธฐ๋ฐ˜ ์ž‘์„ฑ)
        // [SIMD ์ตœ์ ํ™” ์ ์šฉ ๋Œ€์ƒ ๊ตฌ๊ฐ„]
        UnsafeList<float3> path = CalculatedPaths[index];
        path.Clear();

        // ๋…ธ๋“œ ํƒ์ƒ‰ ๋กœ์ง ์ฒ˜๋ฆฌ...
        path.Add(start);
        path.Add(target);
    }
}

์œ„ ์ฝ”๋“œ์—์„œ [BurstCompile] ์†์„ฑ์€ ์ปดํŒŒ์ผ๋Ÿฌ์—๊ฒŒ ํ•ด๋‹น ๊ตฌ์กฐ์ฒด๋ฅผ ๊ณ ์„ฑ๋Šฅ ๊ธฐ๊ณ„์–ด๋กœ ๋ณ€ํ™˜ํ•˜๋ผ๋Š” ์ง€์‹œ๋ฅผ ๋‚ด๋ฆฝ๋‹ˆ๋‹ค. FloatMode.Fast ์˜ต์…˜์„ ํ™œ์„ฑํ™”ํ•˜๋ฉด ๋ถ€๋™ ์†Œ์ˆ˜์  ์—ฐ์‚ฐ์˜ ์—„๊ฒฉ์„ฑ์„ ์•ฝ๊ฐ„ ์™„ํ™”ํ•˜๋Š” ๋Œ€์‹  ๋ ˆ์ง€์Šคํ„ฐ ํ™œ์šฉ์„ฑ์„ ๊ทน๋Œ€ํ™”ํ•˜์—ฌ ์—ฐ์‚ฐ ์†๋„๋ฅผ ๋Œ€ํญ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.



3. NativeArray ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜์™€ ์•ˆ์ „์„ฑ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

C# Job System์—์„œ ๊ฐ€์žฅ ์น˜๋ช…์ ์ด๊ณ  ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋Š” ๋น„๊ด€๋ฆฌํ˜• ๋ฉ”๋ชจ๋ฆฌ์˜ ํ•ด์ œ ๋ˆ„๋ฝ์œผ๋กœ ์ธํ•œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜(Memory Leak)์ž…๋‹ˆ๋‹ค. C#์˜ ๊ธฐ๋ณธ GC๋Š” Native Container(NativeArray, NativeList ๋“ฑ)์˜ ๋‚ด๋ถ€ ๋ฒ„ํผ๋ฅผ ์ž๋™์œผ๋กœ ํ•ด์ œํ•ด์ฃผ์ง€ ์•Š์œผ๋ฏ€๋กœ, ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ƒ๋ช…์ฃผ๊ธฐ(Lifecycle)๋ฅผ ๊ด€๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 1: Allocator ์„ ํƒ ๋ฏธ์Šค๋กœ ์ธํ•œ ์˜ค๋ฒ„ํ—ค๋“œ ๋ฐ ๋ˆ„์ˆ˜

Native Container ์ƒ์„ฑ ์‹œ ์ง€์ •ํ•˜๋Š” Allocator ํ• ๋‹น ์ •์ฑ…์€ ์„ฑ๋Šฅ๊ณผ ์•ˆ์ „์„ฑ์— ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ๋ฏธ์นฉ๋‹ˆ๋‹ค.

ํ• ๋‹น์ž ์ข…๋ฅ˜ (Allocator) ์ˆ˜๋ช… ๋ฒ”์œ„ (Lifespan) ํŠน์ง• ๋ฐ ๊ถŒ์žฅ ์šฉ๋„
Allocator.Temp 1 ํ”„๋ ˆ์ž„ ์ด๋‚ด ๊ฐ€์žฅ ๋น ๋ฆ„. Job์— ์ „๋‹ฌํ•  ์ˆ˜ ์—†์œผ๋ฉฐ ๋‹จ์ผ ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ๋งŒ ์‚ฌ์šฉ ํ›„ ์ฆ‰์‹œ ํ•ด์ œ.
Allocator.TempJob ์ตœ๋Œ€ 4 ํ”„๋ ˆ์ž„ Job์— ์ „๋‹ฌํ•˜๋Š” ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ์šฉ. ์Šค๋ ˆ๋“œ ์•ˆ์ „ํ•˜๋ฉฐ ์ž‘์—… ์™„๋ฃŒ ํ›„ ๋ฐ˜๋“œ์‹œ ํ•ด์ œ ํ•„์š”.
Allocator.Persistent ์˜๊ตฌ์  (์ˆ˜๋™ ํ•ด์ œ ์ „๊นŒ์ง€) ๊ธด ์ˆ˜๋ช…์„ ๊ฐ€์ง„ ๋งต ๋ฐ์ดํ„ฐ, ๊ทธ๋ฆฌ๋“œ ์ •๋ณด ์ €์žฅ์šฉ. ํ•ด์ œ ๋ˆ„๋ฝ ์‹œ ๋ˆ„์ˆ˜ ๋ฐœ์ƒ ์œ„ํ—˜ ๋†’์Œ.

์‹ค๋ฌด ์›์ธ ๋ถ„์„: ๋งค ํ”„๋ ˆ์ž„ ๊ธธ์ฐพ๊ธฐ๋ฅผ ์Šค์ผ€์ค„๋งํ•˜๋ฉด์„œ Allocator.TempJob์œผ๋กœ ์ƒ์„ฑํ•œ NativeArray๋ฅผ Job์ด ์ข…๋ฃŒ๋˜๊ธฐ ์ „์— Dispose()ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, ํ”„๋ ˆ์ž„์ด ์ง€์—ฐ๋˜์–ด 4ํ”„๋ ˆ์ž„ ์ œํ•œ์„ ์ดˆ๊ณผํ•˜๋ฉด ์œ ๋‹ˆํ‹ฐ์˜ ์•ˆ์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ(Safety System)์ด ๊ฒฝ๊ณ ๋ฅผ ์ถœ๋ ฅํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์œ ์‹ค๋ฉ๋‹ˆ๋‹ค.

ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 2: JobHandle ์˜์กด์„ฑ ์—ฐ๊ฒฐ๊ณผ Deferred Dispose ํŒจํ„ด

๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ Job์˜ ์™„๋ฃŒ๋ฅผ JobHandle.Complete()๋กœ ์ฆ‰์‹œ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๊ฒƒ์€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋Œ€๊ธฐ ๋ณ‘๋ชฉ(Main Thread Stall)์„ ์ผ์œผํ‚ค๋ฏ€๋กœ ๋น„๋™๊ธฐ ํ๋ฆ„์„ ๊นจ๋œจ๋ฆฝ๋‹ˆ๋‹ค. ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๋ฅผ ๋ธ”๋กœํ‚นํ•˜์ง€ ์•Š๊ณ  ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ํ•ด์ œํ•˜๋ ค๋ฉด ์˜์กด์„ฑ ๊ธฐ๋ฐ˜ ํ•ด์ œ(Deferred Dispose) ํŒจํ„ด์„ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

public class PathfindingManager : MonoBehaviour
{
    private NativeArray<float3> startPositions;
    private NativeArray<float3> targetPositions;
    private JobHandle pathfindingJobHandle;

    public void RequestPathfinding(Vector3[] starts, Vector3[] targets)
    {
        // ์ด์ „ ์ž‘์—…์ด ์ง„ํ–‰ ์ค‘์ด๋ผ๋ฉด ์™„๋ฃŒ ๋ณด์žฅ
        pathfindingJobHandle.Complete();

        int count = starts.Length;
        // NativeArray ํ• ๋‹น (TempJob ์‚ฌ์šฉ)
        startPositions = new NativeArray<float3>(count, Allocator.TempJob);
        targetPositions = new NativeArray<float3>(count, Allocator.TempJob);

        // ๋ฐ์ดํ„ฐ ๋ณต์‚ฌ
        for (int i = 0; i < count; i++)
        {
            startPositions[i] = starts[i];
            targetPositions[i] = targets[i];
        }

        PathfindingJob job = new PathfindingJob
        {
            StartPositions = startPositions,
            TargetPositions = targetPositions
        };

        // Job ์Šค์ผ€์ค„๋ง
        pathfindingJobHandle = job.Schedule(count, 32);

        // [์ค‘์š” Solution] Job ์™„๋ฃŒ ํ›„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋Œ€๊ธฐ ์—†์ด ์ž๋™์œผ๋กœ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ํ•ด์ œ๋˜๋„๋ก ์ฒด์ด๋‹
        pathfindingJobHandle = startPositions.Dispose(pathfindingJobHandle);
        pathfindingJobHandle = targetPositions.Dispose(pathfindingJobHandle);
    }

    private void OnDestroy()
    {
        // ์”ฌ ์ „ํ™˜ ๋˜๋Š” ๊ฐ์ฒด ํŒŒ๊ดด ์‹œ ๋‚จ์•„์žˆ๋Š” ํ•ธ๋“ค ๊ฐ•์ œ ์ข…๋ฃŒ ๋ฐ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€
        pathfindingJobHandle.Complete();
    }
}

์œ„ ์ฝ”๋“œ์™€ ๊ฐ™์ด NativeArray.Dispose(JobHandle inputDeps) ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ์ง€์ •ํ•œ JobHandle์˜ ์—ฐ์‚ฐ์ด ๋๋‚˜๋Š” ์ฆ‰์‹œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ์ œ์–ดํ•˜์— ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ํ•ด์ œ๋ฉ๋‹ˆ๋‹ค. ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ Complete()๋ฅผ ๋™๊ธฐ์ ์œผ๋กœ ํ˜ธ์ถœํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง€๋ฏ€๋กœ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ๋ณ‘๋ชฉ์„ ์™„์ „ํžˆ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 3: ์˜ˆ์™ธ ์ƒํ™ฉ(Edge Cases) ๋ฐ ๋„๋ฉ”์ธ ๋ฆฌ๋กœ๋“œ ์ฒ˜๋ฆฌ

๊ฐœ๋ฐœ ๊ณผ์ • ๋ฐ ์—๋””ํ„ฐ ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ฃผ์š” ์—ฃ์ง€ ์ผ€์ด์Šค์™€ ๊ทธ ๋Œ€์ฒ˜ ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. ์—๋””ํ„ฐ ํ”Œ๋ ˆ์ด ๋ชจ๋“œ ์ค‘๋‹จ ์‹œ ๋ˆ„์ˆ˜: ์œ ๋‹ˆํ‹ฐ ์—๋””ํ„ฐ์—์„œ Play ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ •์ง€ํ•  ๋•Œ, Execution Order์— ์˜ํ•ด OnDestroy()๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๊ณ  ๋„๋ฉ”์ธ ๋ฆฌ๋กœ๋“œ(Domain Reload)๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด Persistent ํ• ๋‹น์ž๊ฐ€ ๋ˆ„์ˆ˜๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด AppDomain.CurrentDomain.DomainUnload ์ด๋ฒคํŠธ์— ํ•ด์ œ ๋กœ์ง์„ ๋“ฑ๋กํ•˜๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
  2. ๋น„๋™๊ธฐ ๊ธธ์ฐพ๊ธฐ ์ทจ์†Œ ์š”์ฒญ: NPC๊ฐ€ ์ด๋™ ์ค‘ ์‚ฌ๋งํ•˜๊ฑฐ๋‚˜ ํƒ€๊ฒŸ์ด ํŒŒ๊ดด๋œ ๊ฒฝ์šฐ, ์‹คํ–‰ ์ค‘์ธ JobHandle์„ ์ฆ‰์‹œ ์ค‘๋‹จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ JobHandle.Complete()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ž‘์—…์„ ๋๋‚ธ ๋’ค ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐฐ์—ด์„ ๋น„์›Œ์•ผ ๋™์‹œ ์•ก์„ธ์Šค ์—๋Ÿฌ(InvalidOperationException)๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  3. ์•ˆ์ „ ๊ฒ€์‚ฌ ์‹œ์Šคํ…œ ํ™œ์šฉ: Project Settings -> Player -> Scripting Define Symbols์— ENABLE_UNITY_COLLECTIONS_CHECKS ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ช…์‹œํ•˜์—ฌ, ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐœ์ƒ ์ง€์ ์˜ ์ฝœ์Šคํƒ(Callstack)์„ ํŠธ๋ž˜ํ‚นํ•˜๋„๋ก ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.


4. ์„ฑ๋Šฅ ์ธก์ • ๋ฐ ํ”„๋กœํŒŒ์ผ๋ง ๋น„๊ต ๋ถ„์„

๋™์ผํ•œ ๋งต ํ™˜๊ฒฝ์—์„œ 2,000๊ฐœ์˜ NPC๋ฅผ ๋Œ€์ƒ์œผ๋กœ ๊ธฐ์กด NavMeshAgent ๋ฐฉ์‹๊ณผ Burst + C# Job System ๋ณ‘๋ ฌ ๊ธธ์ฐพ๊ธฐ ๋ฐฉ์‹์˜ ํ”„๋ ˆ์ž„ ์†Œ์š” ์‹œ๊ฐ„์„ ์œ ๋‹ˆํ‹ฐ ํ”„๋กœํŒŒ์ผ๋Ÿฌ(Unity Profiler)๋กœ ๋น„๊ต ๊ฒ€์ฆํ•œ ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ตฌ๋ถ„ (2,000๊ฐœ ๊ฐœ์ฒด ์—ฐ์‚ฐ) ๋ฉ”์ธ ์Šค๋ ˆ๋“œ CPU ์‹œ๊ฐ„ GC Alloc (ํ”„๋ ˆ์ž„๋‹น) ํ‰๊ท  ํ”„๋ ˆ์ž„๋ ˆ์ดํŠธ(FPS)
๊ธฐ์กด ๋‹จ์ผ ์Šค๋ ˆ๋“œ (NavMesh/OOP) 42.5 ms 184.2 KB ~18 FPS (์‹ฌ๊ฐํ•œ ๋ž™)
Job System + Burst Compiler 1.8 ms 0 B (Zero GC) 140+ FPS (๋งค์šฐ ์›ํ™œ)

๋ฒค์น˜๋งˆํฌ ๊ฒฐ๊ณผ, C# Job System๊ณผ Burst Compiler๋ฅผ ๋„์ž…ํ•จ์œผ๋กœ์จ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์—ฐ์‚ฐ ์‹œ๊ฐ„์ด ์•ฝ 95% ์ด์ƒ ๊ฐ์†Œํ•˜์˜€์œผ๋ฉฐ, ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜ ํ• ๋‹น(GC Alloc)์„ 0 Bytes๋กœ ์ œ์–ดํ•˜์—ฌ ํ”„๋ ˆ์ž„ ๋“œ๋กญ ํ˜„์ƒ์„ ์™„๋ฒฝํžˆ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก  ๋ฐ ํ•ต์‹ฌ ์š”์•ฝ

๋Œ€๊ทœ๋ชจ NPC ๊ธธ์ฐพ๊ธฐ ์—ฐ์‚ฐ์—์„œ ๋ณ‘๋ชฉ์„ ์™„๋ฒฝํžˆ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์—ฐ์‚ฐ ์ž์ฒด์˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋ฐ์ดํ„ฐ ์ง€ํ–ฅ์ ์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜๋Š” ์ž‘์—…์ด ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ C# Job System๊ณผ Burst Compiler๋Š” ๊ฐ•๋ ฅํ•œ ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์ œ๊ณตํ•˜์ง€๋งŒ, ๋น„๊ด€๋ฆฌํ˜• ๋ฉ”๋ชจ๋ฆฌ์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์—„๊ฒฉํ•˜๊ฒŒ ์ œ์–ดํ•ด์•ผ๋งŒ ์•ˆ์ •์„ฑ์„ ๋‹ด๋ณดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ๋‹จ์ˆœํ™”: Job ๋‚ด๋ถ€๋กœ ์ „๋‹ฌํ•˜๋Š” ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋Š” NativeContainer ๋ฐ Value Type ๊ตฌ์กฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ๋น„๋™๊ธฐ ํ•ด์ œ ์ฒด์ด๋‹: NativeArray.Dispose(JobHandle)์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์˜ ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์ œ๋กœ(Zero)ํ™”ํ•˜์„ธ์š”.
  • ๋ˆ„์ˆ˜ ์ถ”์  ์‹œ์Šคํ…œ ์œ ์ง€: ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ๋Š” ์œ ๋‹ˆํ‹ฐ์˜ Collection Safety Check ๊ธฐ๋Šฅ์„ ํ•ญ์ƒ ํ™œ์„ฑํ™”ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ์กฐ๊ธฐ์— ๋ฐœ๊ฒฌํ•˜๊ณ  ์กฐ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ณธ ๊ฐ€์ด๋“œ์—์„œ ์ œ์‹œํ•œ ํŒจํ„ด๊ณผ ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ธฐ๋ฒ•์„ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•œ๋‹ค๋ฉด, ๋Œ€๊ทœ๋ชจ ๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด ๊ฒŒ์ž„์ด๋‚˜ ๋ณต์žกํ•œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ™˜๊ฒฝ์—์„œ๋„ ์ตœ์ ์˜ ํ”„๋ ˆ์ž„ ์œ ์ง€๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•