<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>LetsCoding</title>
    <link>https://letscoding0103.tistory.com/</link>
    <description>중앙대학교 소프트웨어학부 졸업생의 게임 프로그래머 도전기</description>
    <language>ko</language>
    <pubDate>Mon, 26 Oct 2026 14:00:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>LetsCoding</managingEditor>
    <item>
      <title>Lucid Dream</title>
      <link>https://letscoding0103.tistory.com/46</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1. Timeline Manager&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;Timeline의 특정 구간까지 여러 프레임에 걸쳐 기다려야 했기 때문에 비동기적인 대기 자체는 필요했습니다. 당시 프로젝트에서 UniTask를 사용하고 있어서 &lt;/span&gt;UniTask.NextFrame()&lt;span&gt;으로 구현했습니다. 다만 호출부에서는 결과를 기다리지 않고 모두 &lt;/span&gt;Forget()&lt;span&gt;으로 실행했기 때문에 이 기능만 놓고 보면 Coroutine으로 구현해도 충분했습니다. 이후 다른 비동기 연출과 순차적으로 연결할 필요가 있다면 UniTask 방식이 더 확장하기 편하다고 생각합니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;-&amp;gt; 개선 방향&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;여러 컷씬마다 PlayableDirector를 따로 관리하는 대신 하나의 Timeline을 여러 구간으로 나누고, 각 구간의 종료 시점을 데이터로 관리하는 TimelineManager를 구현했습니다. 구간 재생 중에는 플레이어 입력과 Swap을 제한하고 UI를 전환한 뒤, 종료 시 원래 상태로 복원했습니다. 특정 구간까지 여러 프레임을 기다려야 해서 UniTask의 &lt;/span&gt;NextFrame&lt;span&gt;을 사용했으며, 호출부에서는 완료 결과가 필요하지 않아 &lt;/span&gt;Forget()&lt;span&gt;으로 fire-and-forget 형태로 실행했습니다. 다만 현재 다시 설계한다면 시간값을 별도로 관리하기보다 Timeline Signal이나 Marker를 구간 경계로 활용해 Timeline 편집과 게임 로직의 동기화를 더 안정적으로 구성할 것 같습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 코드는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;while (timeline.time &amp;lt; targetTime)
{
    await UniTask.NextFrame();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이니까 실행 흐름은 사실상 이래.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;PlayTimeline 실행
     &amp;darr;
timeline.Play()
     &amp;darr;
timeline.time &amp;lt; targetTime ? ── Yes
     &amp;darr;
await UniTask.NextFrame()
     &amp;darr;
PlayTimeline 실행 잠시 중단
     &amp;darr;
제어권을 Unity PlayerLoop에 반환
     &amp;darr;
렌더링 / 물리 / 다른 Update / Timeline 갱신 등 실행
     &amp;darr;
다음 프레임
     &amp;darr;
PlayTimeline 재개
     &amp;darr;
while 조건 다시 검사
     &amp;darr;
아직 targetTime 이전?
     &amp;darr;
다시 NextFrame&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러니까 네 표현대로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;기다리는 동안 게임 전체를 멈추지 않고 다른 Unity 작업들이 계속 진행되게 하는 것&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 맞아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;메인 스레드가 &lt;/span&gt;NextFrame&lt;span&gt;하며 기다린다&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;보다는&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;비동기 메서드가 다음 프레임까지 실행을 양보하고, Unity 메인 스레드는 그동안 정상적으로 PlayerLoop를 진행한다.&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 하는 편이 더 정확해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;면접에서 물으면 이렇게 답하면 딱 좋아&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;&lt;/span&gt;await UniTask.NextFrame()&lt;span&gt;은 Timeline의 목표 시간까지 기다리는 기능이 아니라 단순히 다음 프레임까지 현재 비동기 메서드의 실행을 양보하는 기능입니다. 따라서 다음 프레임에 다시 실행됐을 때 &lt;/span&gt;timeline.time&lt;span&gt;이 목표 시간에 도달했는지 반복해서 확인하기 위해 while문을 사용했습니다. 이 과정에서 메인 스레드를 blocking하지 않기 때문에 Unity의 PlayerLoop와 PlayableDirector는 계속 진행됩니다. 다만 UniTask를 사용한다면 &lt;/span&gt;WaitUntil(() =&amp;gt; timeline.time &amp;gt;= targetTime)&lt;span&gt;으로 같은 의도를 더 명확하게 표현할 수도 있습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 네가 말한 &lt;/span&gt;&lt;b&gt;&lt;span&gt;async 상태 머신&lt;/span&gt;&lt;/b&gt;&lt;span&gt;까지 물어보면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;async 메서드는 await 지점에서 실행 상태를 저장하고 awaitable이 완료되면 continuation을 실행하지만, 무엇을 기다릴지는 await하는 객체가 결정합니다. &lt;/span&gt;NextFrame&lt;span&gt;은 한 프레임만 기다리기 때문에 목표 시간까지 기다리려면 반복 조건이나 &lt;/span&gt;WaitUntil&lt;span&gt; 같은 별도의 조건 await가 필요합니다.&amp;rdquo;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜&amp;nbsp;UniTask? &lt;br /&gt;&amp;rarr;&amp;nbsp;프로젝트에서&amp;nbsp;이미&amp;nbsp;UniTask를&amp;nbsp;사용&amp;nbsp;중 &lt;br /&gt;&amp;rarr;&amp;nbsp;비동기&amp;nbsp;흐름을&amp;nbsp;같은&amp;nbsp;방식으로&amp;nbsp;통일 &lt;br /&gt;&amp;rarr;&amp;nbsp;이후&amp;nbsp;await&amp;nbsp;Fade,&amp;nbsp;Load,&amp;nbsp;Dialogue&amp;nbsp;같은&amp;nbsp;작업과&amp;nbsp;연결하기&amp;nbsp;쉬움 &lt;br /&gt;&lt;br /&gt;PlayTimeline&amp;nbsp;내부 &lt;br /&gt;&amp;rarr;&amp;nbsp;timeline.Play() &lt;br /&gt;&amp;rarr;&amp;nbsp;while&amp;nbsp;(timeline.time&amp;nbsp;&amp;lt;&amp;nbsp;targetTime) &lt;br /&gt;&amp;rarr;&amp;nbsp;await&amp;nbsp;UniTask.NextFrame() &lt;br /&gt;&amp;rarr;&amp;nbsp;메인&amp;nbsp;스레드를&amp;nbsp;막지&amp;nbsp;않고&amp;nbsp;매&amp;nbsp;프레임&amp;nbsp;조건&amp;nbsp;확인 &lt;br /&gt;&lt;br /&gt;호출부 &lt;br /&gt;&amp;rarr;&amp;nbsp;PlayTimeline().Forget() &lt;br /&gt;&amp;rarr;&amp;nbsp;호출한&amp;nbsp;쪽은&amp;nbsp;컷씬&amp;nbsp;종료를&amp;nbsp;기다릴&amp;nbsp;필요&amp;nbsp;없음 &lt;br /&gt;&amp;rarr;&amp;nbsp;PlayTimeline은&amp;nbsp;자기&amp;nbsp;내부&amp;nbsp;흐름대로&amp;nbsp;계속&amp;nbsp;진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;응. &lt;/span&gt;&lt;b&gt;&lt;span&gt;현재 방식이 틀렸다고 볼 정도는 아니야.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 특히 보스가 하나뿐이고, 해당 씬에서만 쓰는 연출이라면 충분히 실용적인 구현이야. 다만 OOP 관점에서 보면 &lt;/span&gt;Boss &amp;rarr; TimelineManager&lt;span&gt; 직접 호출은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;보스가 &amp;ldquo;컷씬 시스템&amp;rdquo;이라는 구체 구현을 알아야 한다는 결합&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 생겨.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 엄밀히 말하면 이건 &amp;ldquo;컴포지션&amp;rdquo;이라고 부르기보다는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;의존 객체를 참조해서 사용하는 관계&lt;/span&gt;&lt;/b&gt;&lt;span&gt;에 더 가까워. Composition은 보통 객체가 다른 객체를 소유하고 생명주기까지 책임지는 관계를 말하는데, 여기서는 Boss가 TimelineManager의 생명주기를 소유하지 않잖아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 현재 구조가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public class Boss : MonoBehaviour
{
    [SerializeField]
    private TimeLineManager _timeLineManager;

    private void MeetPlayer()
    {
        _timeLineManager.ResumeTimeline();
    }

    private void Die()
    {
        _timeLineManager.ResumeTimeline();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라면 흐름은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Boss
 │
 │ &quot;컷씬을 다시 실행해야겠다&quot;
 ▼
TimelineManager&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;문제는 &lt;/span&gt;Boss&lt;span&gt;의 원래 책임을 생각하면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Boss
&amp;rarr; 체력
&amp;rarr; 공격
&amp;rarr; 사망
&amp;rarr; 플레이어 감지&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정도가 자연스럽지,&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Boss
&amp;rarr; PlayableDirector 관리 방식까지 알고 있음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;까지는 굳이 알 필요가 없다는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;보스 입장에서는 사실:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;내가 죽었다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만 알리면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;죽었을 때 컷씬을 재생할지, UI를 열지, 음악을 바꿀지는 다른 객체가 결정하는 게 더 분리된 구조&lt;/span&gt;&lt;/b&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;이 경우에는 Event 방식이 가장 자연스러워&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들면 Boss가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;using System;
using UnityEngine;

public class Boss : MonoBehaviour
{
    public event Action OnEncountered;
    public event Action OnDied;

    private void MeetPlayer()
    {
        OnEncountered?.Invoke();
    }

    private void Die()
    {
        OnDied?.Invoke();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 이벤트만 발생시켜.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 별도의 연출 담당 객체가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public class BossCutsceneController : MonoBehaviour
{
    [SerializeField] private Boss _boss;
    [SerializeField] private TimeLineManager _timelineManager;

    private void OnEnable()
    {
        _boss.OnEncountered += HandleEncountered;
        _boss.OnDied += HandleBossDied;
    }

    private void OnDisable()
    {
        _boss.OnEncountered -= HandleEncountered;
        _boss.OnDied -= HandleBossDied;
    }

    private void HandleEncountered()
    {
        _timelineManager.ResumeTimeline();
    }

    private void HandleBossDied()
    {
        _timelineManager.ResumeTimeline();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 만들 수 있지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;구조가 이제:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Boss
 │
 │ OnEncountered / OnDied
 ▼
BossCutsceneController
 │
 │ ResumeTimeline()
 ▼
TimelineManager&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Boss는 Timeline이 존재하는지도 몰라.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 나중에:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Boss 죽음
   ├─ 컷씬 실행
   ├─ BGM 변경
   ├─ Achievement 처리
   └─ UI 표시&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 추가돼도 Boss 코드는 거의 안 건드려도 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이런 경우 Event/Observer 패턴이 꽤 잘 맞아.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;그렇다고 항상 Event가 더 좋은 건 아님&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기 중요해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만약:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Boss 하나
Timeline 하나
해당 이벤트가 무조건 컷씬 재생
재사용 계획 없음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라면 그냥:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;_timeLineManager.ResumeTimeline();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라고 직접 호출하는 게 오히려 이해하기 쉽고 디버깅도 편해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Event를 너무 많이 쓰면 반대로:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;Boss가 죽었는데 대체 누가 이 이벤트를 받고 있는 거지?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 되어 실행 흐름 추적이 어려워질 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 선택 기준은 이렇게 보면 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Boss와 Timeline의 관계가 매우 명확하고 고정적&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;rarr; 직접 참조도 충분히 괜찮음.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Boss를 다른 씬에서도 재사용하거나, 사망 이벤트에 여러 시스템이 반응해야 함&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;rarr; Event가 더 적합.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Timeline 구현 자체를 교체할 가능성이 있음&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;rarr; Interface를 통한 추상화도 고려.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;연출 흐름 자체가 복잡함&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;rarr; 별도의 GameFlow/CutsceneCoordinator 같은 중재 객체가 적합.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Interface를 쓴다면 예를 들면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface ICutscenePlayer
{
    void Resume();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class TimeLineManager 
    : MonoBehaviour, ICutscenePlayer
{
    public void Resume()
    {
        ResumeTimeline();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Boss가 구체적인 &lt;/span&gt;TimeLineManager&lt;span&gt; 대신:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;private ICutscenePlayer _cutscenePlayer;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;에만 의존하도록 할 수도 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Boss
 &amp;darr;
ICutscenePlayer
 &amp;darr;
TimelineManager&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 돼서 구체 구현과 분리되지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 Unity에서는 Inspector가 interface 직렬화를 바로 다루기 불편한 경우가 있기 때문에, 이 정도 규모에서 **&amp;ldquo;SOLID를 지키려고 무조건 interface&amp;rdquo;**까지 갈 필요는 없어.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;내가 네 프로젝트를 지금 다시 설계한다면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Boss는 자신의 상태 변화만 발행하고, 컷씬과 UI 같은 연출은 별도의 &lt;/span&gt;BossSequenceController&lt;span&gt;나 &lt;/span&gt;GameFlowController&lt;span&gt;가 받아서 처리&lt;/span&gt;&lt;/b&gt;&lt;span&gt;하게 할 것 같아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Boss
│
├─ OnEncountered
└─ OnDied
      &amp;darr;
BossSequenceController
      &amp;darr;
TimelineManager
      &amp;darr;
PlayableDirector&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 구조가 좋은 이유는 &lt;/span&gt;TimelineManager&lt;span&gt;도 역할을 더 명확하게 유지할 수 있기 때문이야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Boss
&amp;rarr; 보스 로직 담당

BossSequenceController
&amp;rarr; &quot;언제 컷씬을 실행할지&quot; 결정

TimelineManager
&amp;rarr; &quot;어떻게 컷씬을 실행할지&quot; 담당&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 책임이 나뉘는 거지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;당시에는 Boss에서 &lt;/span&gt;TimelineManager&lt;span&gt;를 직접 참조해서 특정 상태 변화 시 &lt;/span&gt;ResumeTimeline()&lt;span&gt;을 호출했습니다. 규모가 작은 장면에서는 구현과 흐름을 파악하기 쉬운 실용적인 방식이라고 생각합니다. 다만 Boss가 Timeline이라는 구체적인 연출 시스템에 직접 의존하기 때문에 재사용성이 떨어질 수 있습니다. 현재 다시 설계한다면 Boss는 &lt;/span&gt;OnEncountered&lt;span&gt;, &lt;/span&gt;OnDied&lt;span&gt; 같은 자신의 상태 변화만 이벤트로 발행하고, 별도의 Sequence나 GameFlow Controller가 이를 받아 TimelineManager를 호출하도록 분리할 것 같습니다. 그러면 Boss는 게임 로직에 집중하고 연출 시스템과의 결합도도 낮출 수 있습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정확한 흐름은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Timeline
   &amp;darr;
Signal Emitter
   │
   │ SignalAsset 발생
   &amp;darr;
Signal Receiver
   &amp;darr;
등록된 Reaction(UnityEvent)
   &amp;darr;
TriggerPrintSentenceByIndex(2)
   &amp;darr;
TextBubble
   &amp;darr;
SentenceData[2]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 포트폴리오에는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Timeline Editor에 Signal Emitter를 배치하고, Signal Receiver의 Reaction에서 대사 Index를 매핑하여 해당 시점에 Bubble Text가 출력되도록 구성했습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;696&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lL2gc/dJMcaggg6yE/gPnP7cBzpiUVctexrNI1VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lL2gc/dJMcaggg6yE/gPnP7cBzpiUVctexrNI1VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lL2gc/dJMcaggg6yE/gPnP7cBzpiUVctexrNI1VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlL2gc%2FdJMcaggg6yE%2FgPnP7cBzpiUVctexrNI1VK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;405&quot; height=&quot;223&quot; data-origin-width=&quot;1263&quot; data-origin-height=&quot;696&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;대사 출력 타이밍을 코드가 아닌 Timeline Editor에서 조정할 수 있게 했다.&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;는 꽤 좋은 포트폴리오 포인트야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만 &lt;/span&gt;&lt;b&gt;&lt;span&gt;새로운 대사를 추가할 때 아무 설정도 필요 없다는 의미는 아니야.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;새 대사가 필요하면 여전히:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SentenceData&lt;span&gt; 추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Signal 생성/배치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Signal Receiver Reaction 연결&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Index 설정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정도는 필요하지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span&gt;3. 현재 Bubble Text의 구조&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지금 시스템은 크게 네 단계야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;[연출 계층]
Timeline Signal
      &amp;darr;
Signal Receiver
      &amp;darr;

[진입 계층]
TriggerPrintSentenceByIndex(index)
      &amp;darr;

[출력 제어]
TriggerPrintSentenceAsync(index)
      &amp;darr;

[실제 타이핑]
PrintWord(TextBubbleData)
      &amp;darr;
TextMeshProUGUI&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;조금 더 자세하게 보면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Signal 발생
   &amp;darr;
TriggerPrintSentenceByIndex(3)
   &amp;darr;
Forget()
   &amp;darr;
TriggerPrintSentenceAsync(3)
   &amp;darr;
_isPrinting 확인
   &amp;darr;
Index 범위 확인
   &amp;darr;
SentenceData sentence = _sentenceList[3]
   &amp;darr;
foreach (word)
   &amp;darr;
await PrintWord(word)
   &amp;darr;
문자 하나씩 TMP에 추가
   &amp;darr;
charWaitTime
   &amp;darr;
한 단어 끝
   &amp;darr;
wordWaitTime&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;구조 자체는 작은 프로젝트에서는 충분히 이해하기 쉽고 괜찮아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span&gt;5. 중복 출력 방지 주장은 맞음&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 코드:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (_isPrinting) return;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;때문에 이미 출력 중일 때 또 Signal이 들어오면 두 번째 출력은 실행되지 않아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;동시에 여러 대사가 중복 출력되는 것을 방지했습니다.&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 주장할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 여기에는 중요한 부작용이 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 Timeline이:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;vhdl&quot;&gt;&lt;code&gt;0.0초     Signal A
          &amp;darr;
       &quot;안녕하세요...&quot; 출력 중

0.5초     Signal B
          &amp;darr;
       _isPrinting == true
          &amp;darr;
          return&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;B 대사가 아예 사라져 버려.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 현재 &lt;/span&gt;_isPrinting&lt;span&gt;은:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;중복 실행 방지&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;에는 성공하지만&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여러 대사가 연속으로 발생했을 때 순서대로 처리&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하는 구조는 아니야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;면접에서 이걸 물으면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;당시에는 동시에 두 대사가 겹쳐 출력되는 것을 막기 위해 단순 상태 플래그를 사용했습니다. 다만 출력 중 새로운 Signal이 들어오면 해당 요청을 버리는 구조이므로, 요구사항이 복잡해진다면 Queue로 순차 처리하거나 기존 출력을 Cancellation하고 새 대사를 출력하는 정책으로 개선할 수 있습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span&gt;6. 실제로 개선한다면 가장 먼저 이 부분&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;대사 시스템에서는 보통 세 가지 정책 중 하나를 정해야 해.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;새 대사가 들어왔을 때

① Ignore
현재 출력 중이면 새 요청 무시

② Queue
현재 대사 끝난 후 다음 대사 출력

③ Cancel &amp;amp; Replace
현재 출력을 취소하고 새 대사 즉시 출력&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 네 코드는 명확하게 &lt;/span&gt;&lt;b&gt;&lt;span&gt;① Ignore&lt;/span&gt;&lt;/b&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (_isPrinting)
    return;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;컷씬에서는 Timeline이 정해진 타이밍에 Signal을 쏘기 때문에 오히려 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Queue 또는 Replace 방식이 안전할 가능성이 높아.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span&gt;8. 다만 Index 방식 자체는 꽤 취약함&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 지금 다시 설계한다면 개선할 만해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;0 = &quot;안녕하세요&quot;
1 = &quot;저를 따라오세요&quot;
2 = &quot;문을 열어주세요&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인데 중간에 대사를 하나 삽입하면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;0 = &quot;안녕하세요&quot;
1 = &quot;잠시만요&quot;        &amp;larr; 새로 추가
2 = &quot;저를 따라오세요&quot;
3 = &quot;문을 열어주세요&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존 Signal Receiver에서:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Signal B &amp;rarr; index 1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라고 되어 있던 게 이제 &lt;/span&gt;&lt;b&gt;&lt;span&gt;전혀 다른 대사를 출력할 수 있어.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;스크린샷에서 Receiver에:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0
1
2
3
4
5
6&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 직접 넣고 있기 때문에 이 문제는 실제로 발생할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 더 큰 프로젝트라면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;숫자 Index

대신

Dialogue ID
또는
SentenceData 직접 참조&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 쓰는 구조를 고려할 만해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;PlaySentence(SentenceData sentence)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 만드는 거지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Signal
 &amp;darr;
&quot;Tutorial_Boss_Intro&quot;라는 SentenceData&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 직접 참조하기 때문에 List 순서가 바뀌어도 안전해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;9. 가장 개선 가치가 큰 부분: &lt;/span&gt;_textUI.text +=&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;_textUI.text += data._word[i];&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 매 글자마다 하고 있잖아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기능적으로는 문제없어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 성능 관점에서는 썩 좋은 방식은 아니야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;C#의 &lt;/span&gt;string&lt;span&gt;은 immutable이라:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;_textUI.text += 'A';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 할 때 개념적으로 계속 새로운 문자열이 만들어져.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;H&quot;
&amp;darr;
&quot;HE&quot;
&amp;darr;
&quot;HEL&quot;
&amp;darr;
&quot;HELL&quot;
&amp;darr;
&quot;HELLO&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;문장이 길어지면 반복적인 문자열 allocation이 발생할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또 TMP text를 계속 바꾸면서 텍스트 갱신 작업도 발생하지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Typewriter 효과에서는 TextMeshPro의:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;maxVisibleCharacters&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 이용하는 방식이 꽤 잘 맞아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 처음부터:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;_textUI.text = &quot;안녕하세요 반갑습니다.&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전체 텍스트를 넣어놓고:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;maxVisibleCharacters = 1
안

= 2
안녕

= 3
안녕하

...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 보이는 문자 수만 늘려.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 매번 문자열 자체를 재조합할 필요가 없어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;네 시스템의 &lt;/span&gt;&lt;b&gt;&lt;span&gt;단어별 waitTime&lt;/span&gt;&lt;/b&gt;&lt;span&gt;도 유지할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;&quot;안녕하세요&quot;
       &amp;darr;
5글자까지 공개
       &amp;darr;
wordWaitTime

&quot;반갑습니다&quot;
       &amp;darr;
다음 문자 공개&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;식으로 만들면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;10. UniTask 쪽에서는 Cancellation이 없는 것도 개선 포인트&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;TriggerPrintSentenceAsync(index).Forget();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;으로 시작하고,&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;await UniTask.Delay(...);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 반복하지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 중간에:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Scene 변경
GameObject Destroy
Cutscene Skip
대화 강제 종료&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 발생하면 기존 출력 작업을 취소하는 구조가 없어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 지금 다시 만든다면 &lt;/span&gt;CancellationToken&lt;span&gt;을 붙이는 게 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개념적으로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;await UniTask.Delay(
    delay,
    cancellationToken: token
);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하고,&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;TextBubble Destroy
또는
Cutscene Skip
      &amp;darr;
Cancellation
      &amp;darr;
현재 Typewriter 즉시 종료&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 만드는 거지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 &lt;/span&gt;.Forget()&lt;span&gt; 기반 fire-and-forget에서는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;객체 생명주기와 비동기 작업 생명주기를 맞추는 것&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 중요해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;11. &lt;/span&gt;_isPrinting&lt;span&gt;도 try/finally가 더 안전함&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;_isPrinting = true;

foreach (...)
{
    await PrintWord(...);
}

_isPrinting = false;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인데 중간에서 예외나 Cancellation이 발생하면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;_isPrinting = true
      &amp;darr;
예외 발생
      &amp;darr;
마지막 false까지 못 감&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 될 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그다음부터 영원히:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;if (_isPrinting) return;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;에 막힐 수 있지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 상태 플래그를 쓴다면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;_isPrinting = true;

try
{
    ...
}
finally
{
    _isPrinting = false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;구조가 더 안전해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;12. &lt;/span&gt;UniTask.Delay&lt;span&gt;와 TimeScale도 체크할 만함&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;컷씬 시스템에서는 꽤 중요한 부분이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만약 플레이어 일시정지를:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Time.timeScale = 0;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 방식으로 구현한다면 &lt;/span&gt;UniTask.Delay&lt;span&gt;가 scaled time을 사용하는 설정인지 확인해야 해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그렇지 않으면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;컷씬 시작
&amp;darr;
Time.timeScale = 0
&amp;darr;
대사 출력 Delay도 진행 안 됨&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 문제가 생길 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;네 이전 &lt;/span&gt;PlayerPause()&lt;span&gt;가 실제로 &lt;/span&gt;timeScale = 0&lt;span&gt;인지 아니면 단순 입력 제한인지는 지금 코드로 알 수 없으니, 이 부분은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;당시 구현 확인이 필요해.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;컷씬 UI 텍스트라면 요구사항에 따라 &lt;/span&gt;&lt;b&gt;&lt;span&gt;unscaled/realtime 기반 Delay&lt;/span&gt;&lt;/b&gt;&lt;span&gt;가 더 자연스러운 경우가 많아.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;13. &lt;/span&gt;TextBubble&lt;span&gt; 클래스의 책임도 조금 많음&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 &lt;/span&gt;TextBubble&lt;span&gt; 하나가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Sentence 데이터 저장
+
Timeline 진입점
+
출력 상태 관리
+
Typewriter 로직
+
TMP UI 제어
+
폰트 설정
+
AudioSource&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;까지 담당하고 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;작은 프로젝트에서는 전혀 이상하지 않아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 확장한다면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;DialogueData
&amp;rarr; 대사 데이터

TextBubblePlayer
&amp;rarr; 출력 순서 / async 처리

TextBubbleView
&amp;rarr; TMP 표시

TimelineDialogueReceiver
&amp;rarr; Timeline Signal &amp;rarr; 대사 요청 변환&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정도로 나눌 수도 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Timeline
      &amp;darr;
TextBubble&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;을 직접 연결하는 대신:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Timeline Signal
       &amp;darr;
TimelineDialogueReceiver
       &amp;darr;
DialoguePlayer
       &amp;darr;
TextBubbleView&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 두면 나중에 대화 시스템을 Timeline이 아닌 NPC나 퀘스트에서도 재사용하기 좋아져.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만 &lt;/span&gt;&lt;b&gt;&lt;span&gt;현재 규모에서 무조건 이렇게 나눠야 했다는 뜻은 아니야.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 포트폴리오에서는 과도하게 &amp;ldquo;설계가 잘못됐다&amp;rdquo;고 말할 필요 없어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 면접에서 한 단계 더 들어오면 이렇게 말하면 좋고:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;현재 구조는 index 기반이라 대사 List 순서가 변경되면 Receiver 설정이 깨질 수 있고, &lt;/span&gt;_isPrinting&lt;span&gt; 중 들어온 요청을 버리는 한계가 있습니다. 다시 만든다면 SentenceData 자체나 안정적인 ID를 참조하도록 변경하고, 대화 요청에 대해 Queue 또는 Cancellation 정책을 명확하게 두겠습니다. Typewriter 구현도 문자열을 매 문자 결합하는 대신 TMP의 &lt;/span&gt;maxVisibleCharacters&lt;span&gt;를 활용해서 할당을 줄일 것 같습니다.&amp;rdquo;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;URP의 Volume은 효과 파라미터와 강도를 관리하고, ScriptableRendererFeature는 효과가 활성화됐을 때 커스텀 RenderPass를 파이프라인에 삽입하며, RenderPass가 Camera Color를 임시 RenderTexture로 복사한 뒤 Glitch Shader를 적용해 다시 화면으로 Blit하는 Full-Screen Post-Processing 구조로 구현했습니다.&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span&gt;이 Shader가 실제로 무엇을 하는가&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;코드를 한 줄씩 핵심만 보면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;_MainTex (&quot;Texture&quot;, 2D) = &quot;white&quot; {}
_Intensity (&quot;Intensity&quot;, Range(0, 1)) = 0.5
_ColorDrift (&quot;Color Drift&quot;, Range(0, 1)) = 0.5&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 후처리(Post Process) 셰이더에서 사용할 파라미터야.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;_MainTex&lt;span&gt; : 현재 화면 텍스처&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;_Intensity&lt;span&gt; : 흔들림 강도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;_ColorDrift&lt;span&gt; : RGB 채널 분리 강도&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;1. 수평 흔들림&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;float horizontalShake = (rand(float2(_Time.y, uv.y)) - 0.5) * _Intensity * 0.1;
uv.x += horizontalShake;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 부분이 &lt;/span&gt;&lt;b&gt;&lt;span&gt;수평 흔들림&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이야.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;의미&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;_Time.y&lt;span&gt; : 시간&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;uv.y&lt;span&gt; : 화면의 세로 위치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;rand(float2(_Time.y, uv.y))&lt;span&gt; : 시간과 세로 위치에 따라 랜덤값 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;그 값을 이용해 &lt;/span&gt;&lt;b&gt;&lt;span&gt;x축 방향 UV를 살짝 이동&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 화면 전체를 똑같이 흔드는 게 아니라,&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;세로 줄(스캔라인)마다 조금씩 다른 x 오프셋&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 생겨.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 시각적으로는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;정상 화면
||||||||||||

글리치 화면
| ||| || |||&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이런 느낌의 &lt;/span&gt;&lt;b&gt;&lt;span&gt;라인 단위 수평 뒤틀림&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 네가 말한 **&amp;ldquo;시간과 UV 값을 기반으로 한 수평 흔들림&amp;rdquo;**과 정확히 일치해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. RGB Channel Drift&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;float colorDrift = sin(_Time.y * 10.0) * _ColorDrift;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 시간에 따라 좌우로 진동하는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;채널 분리 오프셋 값&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그 다음:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;color.r = tex2D(_MainTex, uv + float2(colorDrift * 0.03, 0)).r * 0.7;
color.g = tex2D(_MainTex, uv).g * 0.7;
color.b = tex2D(_MainTex, uv - float2(colorDrift * 0.04, 0)).b * 0.8;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;R 채널&lt;/span&gt;&lt;/b&gt;&lt;span&gt;은 오른쪽으로 조금 이동한 위치에서 샘플링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;G 채널&lt;/span&gt;&lt;/b&gt;&lt;span&gt;은 원래 위치&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;B 채널&lt;/span&gt;&lt;/b&gt;&lt;span&gt;은 왼쪽으로 조금 이동한 위치에서 샘플링&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 하나의 색을 그대로 읽는 게 아니라,&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;RGB를 서로 다른 UV에서 따로 가져와 합치고 있어.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이게 바로 흔히 말하는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;RGB Split / Color Channel Drift / Chromatic Offset&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;효과야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 이 부분도 네 설명과 정확히 맞아.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;이 구현이 URP 구조에서 어떤 방식인지&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지금까지 나온 코드들을 합치면 구조는 이렇게 정리돼.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;전체 흐름&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;게임 이벤트 발생
   &amp;darr;
GlitchController
   &amp;darr;
Volume weight 조절
   &amp;darr;
VolumeManager가 GlitchEffect 상태 제공
   &amp;darr;
GlitchRenderPassFeature가 활성 여부 확인
   &amp;darr;
GlitchRenderPass 실행
   &amp;darr;
현재 화면을 _MainTex로 받아
Glitch Shader 적용
   &amp;darr;
최종 화면 출력&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;1. &lt;/span&gt;GlitchController&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 &lt;/span&gt;&lt;b&gt;&lt;span&gt;효과 발동 담당&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;volume.weight = 1f;
for (float t = 0; t &amp;lt; duration; t += Time.unscaledDeltaTime)
{
    volume.weight = Mathf.Lerp(startWeight, endWeight, t / duration);
    await UniTask.NextFrame();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 글리치 효과를 켰다가 시간이 지나면서 서서히 줄어들게 하는 역할.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 &lt;/span&gt;&lt;b&gt;&lt;span&gt;게임 로직 계층&lt;/span&gt;&lt;/b&gt;&lt;span&gt;에 가까워.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. &lt;/span&gt;GlitchRenderPassFeature&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 &lt;/span&gt;&lt;b&gt;&lt;span&gt;URP 파이프라인에 Pass를 등록하는 관리자&lt;/span&gt;&lt;/b&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;renderer.EnqueuePass(pass);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;글리치가 활성화되었을 때만&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;해당 Pass를 렌더링 파이프라인에 끼워 넣음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 &amp;ldquo;이 효과를 이번 프레임에 쓸지 말지&amp;rdquo; 결정하는 브리지 역할.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;3. &lt;/span&gt;GlitchRenderPass&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 &lt;/span&gt;&lt;b&gt;&lt;span&gt;실제로 화면을 가공하는 렌더링 코드&lt;/span&gt;&lt;/b&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;cmd.Blit(currentTarget, tempTargetId);
glitchMaterial.SetFloat(&quot;_Intensity&quot;, glitchEffect.intensity.value);
cmd.Blit(tempTargetId, currentTarget, glitchMaterial);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;현재 화면을 임시 RT로 복사&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Glitch Material을 적용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;다시 화면에 출력&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 전형적인 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Full-Screen Post Processing&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 방식이야.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;4. &lt;/span&gt;Glitch.shader&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 위 Pass에서 사용하는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;실제 GPU 처리 로직&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이고,&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;_MainTex&lt;span&gt; = 화면 원본&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;_Intensity&lt;span&gt; = 흔들림 강도&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;_ColorDrift&lt;span&gt; = 채널 분리 강도&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 이용해서 최종 픽셀 색을 계산해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;그럼 각 클래스가 다 필요한가?&lt;/span&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;필요한 것&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1) &lt;/span&gt;GlitchController&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;필요함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;왜냐면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;언제 효과를 발동할지&lt;/span&gt;&lt;/b&gt;&lt;span&gt;, &lt;/span&gt;&lt;b&gt;&lt;span&gt;얼마 동안 fade out할지&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 같은 게임 로직이 있어야 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2) &lt;/span&gt;GlitchRenderPassFeature&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;필요함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;URP에 Pass를 등록해야 하니까&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3) &lt;/span&gt;GlitchRenderPass&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;필요함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;실제 Blit / 화면 가공 담당&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;4) &lt;/span&gt;Glitch.shader&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;필요함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;화면을 어떻게 흔들지, 채널을 어떻게 분리할지 GPU에서 계산함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;5) &lt;/span&gt;GlitchEffect&lt;span&gt; (Volume Component)&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;거의 확실히 필요함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;네 코드에서 &lt;/span&gt;volumeStack.GetComponent&amp;lt;GlitchEffect&amp;gt;()&lt;span&gt;를 쓰고 있으니까&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;지금 대화에 코드만 안 올라온 상태&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;중복/정리 필요한 것&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이전 코드에서는 &lt;/span&gt;GlitchRenderPass&lt;span&gt;가&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GlitchRenderPassFeature&lt;span&gt; 내부 nested class로 한 번&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;별도 public class로 한 번&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;있었지?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그건 &lt;/span&gt;&lt;b&gt;&lt;span&gt;둘 다 필요하진 않을 가능성이 높아.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;보통은 둘 중 하나면 충분해.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;추천&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;실무/가독성 기준으론:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GlitchRenderPassFeature.cs&lt;/li&gt;
&lt;li&gt;GlitchRenderPass.cs&lt;/li&gt;
&lt;li&gt;GlitchEffect.cs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 분리하는 게 더 깔끔함.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;지금 코드에서 보이는 개선 포인트&lt;/span&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;1. &lt;/span&gt;_ColorDrift&lt;span&gt; 세팅 확인 필요&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;셰이더엔 &lt;/span&gt;_ColorDrift&lt;span&gt;가 있는데, RenderPass 코드에서는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;glitchMaterial.SetFloat(&quot;_Intensity&quot;, glitchEffect.intensity.value);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만 보여.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 만약 &lt;/span&gt;_ColorDrift&lt;span&gt;를 기본값 0.5 그대로 쓰고 있다면 효과는 나겠지만,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;**&amp;ldquo;동적으로 제어했다&amp;rdquo;**는 표현은 약해져.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;가능하면 이런 식이 더 좋음&lt;/span&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;glitchMaterial.SetFloat(&quot;_Intensity&quot;, glitchEffect.intensity.value);
glitchMaterial.SetFloat(&quot;_ColorDrift&quot;, glitchEffect.colorDrift.value);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 &lt;/span&gt;GlitchEffect&lt;span&gt;에도:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public ClampedFloatParameter colorDrift = new ClampedFloatParameter(0.5f, 0f, 1f);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 파라미터가 있으면 구조가 완성돼.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. 알파값을 명시하는 게 안전함&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지금 Shader는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;half4 color;
color.r = ...
color.g = ...
color.b = ...
return color;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인데 &lt;/span&gt;alpha&lt;span&gt;를 직접 세팅하지 않았어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 경우 상황에 따라 알파가 예측 불가능할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;더 안전한 방식&lt;/span&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;color.a = 1.0;
return color;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;혹은 원본 텍스처 알파를 유지하려면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;color.a = tex2D(_MainTex, uv).a;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;3. 전체 밝기가 어두워짐&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지금은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;.r * 0.7
.g * 0.7
.b * 0.8&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 되어 있어서 원본보다 약간 어두워져.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이게 의도된 &amp;ldquo;음침한 네온 글리치&amp;rdquo; 분위기라면 괜찮고,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그게 아니라면 면접에서 질문 받을 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;답변은 이렇게 가능:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;RGB 분리 효과를 조금 더 잘 보이게 하고 과도한 색 날림을 막기 위해 채널별 강도를 조정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;혹은 단순히 스타일링이라 말해도 됨.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;네 포트폴리오 문장, 지금 기준으로 어떻게 쓰면 좋나&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지금은 이렇게 쓰면 꽤 정확해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;추천 문장&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Custom Glitch Post-Processing&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;URP의 &lt;/span&gt;ScriptableRendererFeature&lt;span&gt;와 &lt;/span&gt;ScriptableRenderPass&lt;span&gt;를 활용해 &lt;/span&gt;&lt;b&gt;&lt;span&gt;카메라 최종 렌더 결과에 적용되는 Full-Screen Post-Processing 기반 글리치 효과&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 구현했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Glitch Shader에서는 &lt;/span&gt;_Time&lt;span&gt;과 UV 좌표를 기반으로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;라인 단위 수평 흔들림(horizontal shake)&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 을 생성하고, 각 RGB 채널을 서로 다른 UV 오프셋으로 샘플링하여 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Color Drift 효과&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 구현했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;효과 파라미터는 Volume Framework와 연동했으며, &lt;/span&gt;GlitchController&lt;span&gt;에서 &lt;/span&gt;Volume.weight&lt;span&gt;를 시간에 따라 감소시키는 방식으로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;이벤트 상황에 맞는 글리치 강도 Fade Out&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 구현했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;이를 통해 컷씬, 플레이어 상태 전환, 특수 이벤트 상황에서 화면 전체 분위기를 일관된 흐름으로 제어할 수 있도록 구성했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;&lt;span&gt;면접에서 한 문장으로 설명하면&lt;/span&gt;&lt;/h1&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;URP의 ScriptableRendererFeature/Pass를 이용해 카메라 최종 화면에 Full-Screen 후처리 글리치 효과를 넣었고, Shader에서는 시간 기반 수평 UV 흔들림과 RGB 채널별 오프셋 샘플링으로 글리치 느낌을 만들었습니다. 효과 강도는 Volume과 Controller를 통해 이벤트 시점에 동적으로 제어했습니다.&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/46</guid>
      <comments>https://letscoding0103.tistory.com/46#entry46comment</comments>
      <pubDate>Fri, 16 Oct 2026 21:16:38 +0900</pubDate>
    </item>
    <item>
      <title>추가적인 그래픽스 지식</title>
      <link>https://letscoding0103.tistory.com/45</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1. VertexFormat과 버퍼 문제&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;네 포맷은 이런 의미야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const vfmt = new gfx.VertexFormat([
    { name: gfx.ATTR_POSITION, type: gfx.ATTR_TYPE_FLOAT32, num: 3 },
    { name: gfx.ATTR_UV0,      type: gfx.ATTR_TYPE_FLOAT32, num: 2 },
    { name: gfx.ATTR_COLOR,    type: gfx.ATTR_TYPE_UINT8,   num: 4, normalize: true },
]);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정점 하나에는 개념적으로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Position : x, y, z        &amp;rarr; float 3개
UV       : u, v           &amp;rarr; float 2개
Color    : r, g, b, a     &amp;rarr; uint8 4개

Vertex
┌──────────────┬──────────┬─────────────┐
│ Position xyz │  UV uv   │ Color rgba  │
└──────────────┴──────────┴─────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라는 데이터가 필요하다는 뜻이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 &lt;/span&gt;&lt;b&gt;&lt;span&gt;VertexFormat은 데이터를 자동으로 만들어주는 게 아니라 GPU와 엔진에게 &amp;ldquo;이 Mesh에는 이런 Attribute가 있다&amp;rdquo;는 계약을 알려주는 것&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이라고 이해하면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;네가 처음 고생했던 핵심은 이 계약과 실제 데이터를 넣는 방법이 달랐던 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예전 코드에서는 메모리를:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;smalltalk&quot;&gt;&lt;code&gt;x y z u v color | x y z u v color | ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 하나의 &lt;/span&gt;&lt;b&gt;&lt;span&gt;interleaved buffer&lt;/span&gt;&lt;/b&gt;&lt;span&gt;로 만들어 놓고,&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;_mesh.setVertices(gfx.ATTR_POSITION, vData);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 전달했지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 이 API 호출은 의미상:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;지금 전달하는 배열은 &lt;/span&gt;POSITION&lt;span&gt; 채널의 데이터다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 말한 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;엔진이 Position을 &lt;/span&gt;(x,y,z)&lt;span&gt; 단위로 읽는다면 실제로는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;원래 의도

Vertex 0
[x0 y0 z0] [u0 v0] [color0]

Vertex 1
[x1 y1 z1] [u1 v1] [color1]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;였는데, Position 배열로 해석하면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Position 0 = x0 y0 z0      &amp;larr; 정상
Position 1 = u0 v0 color   &amp;larr; 망가짐
Position 2 = x1 y1 z1
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 식으로 Attribute 경계가 무너질 수 있었던 거지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 최종 코드에서는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;this._mesh.setVertices(gfx.ATTR_POSITION, positions);
this._mesh.setVertices(gfx.ATTR_UV0,      uvs);
this._mesh.setVertices(gfx.ATTR_COLOR,    colors);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 완전히 분리했어.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;POSITION
[x0 y0 z0]
[x1 y1 z1]
[x2 y2 z2]
...

UV
[u0 v0]
[u1 v1]
[u2 v2]
...

COLOR
[c0]
[c1]
[c2]
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;VertexFormat가 말하는 Attribute
              ↕
실제로 setVertices로 넣는 Attribute&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 정확하게 1:1로 일치해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이걸 면접에서 짧게 표현하면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;처음에는 Position, UV, Color를 직접 interleaved buffer로 구성해 놓고 이를 Position Attribute로 한 번에 전달해서 Vertex Layout이 깨지는 문제가 있었습니다. Cocos의 &lt;/span&gt;setVertices&lt;span&gt; 사용 방식에 맞춰 Position, UV, Color를 각 Attribute별 배열로 분리해 전달하면서 해결했습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정도가 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Interleaved 자체가 잘못된 건 아님&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 중요한 꼬리질문이 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Interleaved Vertex Buffer 자체는 잘못된 방식이 아니야.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 그래픽스에서는 오히려 아주 흔해.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;V0: XYZ UV RGBA
V1: XYZ UV RGBA
V2: XYZ UV RGBA&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 한 Vertex에 필요한 데이터를 붙여 저장하면 GPU가 Vertex 하나를 처리할 때 필요한 데이터가 가까운 메모리에 있기 때문에 효율적인 경우가 많아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;문제는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Cocos에서 사용한 &lt;/span&gt;setVertices(attributeName, data)&lt;span&gt;라는 API의 기대 방식과 네가 수동으로 만든 interleaved 데이터를 섞어버린 것&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 면접에서:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;Interleaved 방식이 문제였나요?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 물으면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;아닙니다. Interleaved Vertex Layout 자체는 일반적인 그래픽스 방식입니다. 당시 문제는 Cocos의 Attribute 단위 &lt;/span&gt;setVertices&lt;span&gt; API를 사용하면서 interleaved 전체 버퍼를 Position Attribute에 전달한 것이었습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 답해야 정확해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;COLOR가 없으면 왜 안 보였나?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사용하던 Shader가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;a_position
a_uv
a_color&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 기대하는데 Mesh에는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;position
uv&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만 있으면 입력 계약이 맞지 않아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 Sprite 계열 Shader에서는 흔히 최종 색상을 개념적으로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;TextureColor &amp;times; VertexColor&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같이 계산해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 모든 정점에:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;cc.color(255, 255, 255, 255)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 넣으면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Texture &amp;times; White
= Texture 그대로&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반대로 Alpha가 0이라면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Texture &amp;times; (1,1,1,0)
&amp;rarr; 투명&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;해질 수 있고.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 &lt;/span&gt;a_color&lt;span&gt; 경고는 단순히 보기 싫은 콘솔 메시지가 아니라 &lt;/span&gt;&lt;b&gt;&lt;span&gt;현재 Material/Shader가 요구하는 Vertex Input과 Mesh Layout이 맞지 않는다는 신호&lt;/span&gt;&lt;/b&gt;&lt;span&gt;였던 거야.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. Vertex와 Index는 서로 역할이 다름&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Ribbon에서 Vertex를 이렇게 만든다고 하자.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;L0 ●────────────● L1
   │            │
   │            │
R0 ●────────────● R1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Vertex 배열에는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;index 0 = L0
index 1 = R0
index 2 = L1
index 3 = R1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 들어 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 GPU는 사각형을 직접 그리는 게 아니라 일반적으로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Triangle 단위로 그려.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 사각형을:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;L0 ●────────────● L1
   │          ╱ │
   │        ╱   │
   │      ╱     │
R0 ●────────────● R1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 개의 Triangle로 분할해야 해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;네 코드가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;indices.push(base, base + 1, base + 2);
indices.push(base + 1, base + 3, base + 2);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 되어 있으니까 첫 구간에서는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Triangle 1
0, 1, 2
= L0 &amp;rarr; R0 &amp;rarr; L1

Triangle 2
1, 3, 2
= R0 &amp;rarr; R1 &amp;rarr; L1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 되는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;3. 왜 Vertex 순서가 중요한가 &amp;mdash; Winding Order&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;GPU에게:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0, 1, 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 Triangle을 전달하면 단순히 &amp;ldquo;세 점을 이어라&amp;rdquo;로 끝나는 게 아니야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;세 점을 어느 방향으로 순회했는지&lt;/span&gt;&lt;/b&gt;&lt;span&gt;도 의미가 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;      B
      ●
     / \
    /   \
   ●─────●
  A       C&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;점 순서를:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;A &amp;rarr; C &amp;rarr; B&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 돌면 한 방향이고,&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;A &amp;rarr; B &amp;rarr; C&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 돌면 반대 방향이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Clockwise     = CW  = 시계 방향
Counterclockwise = CCW = 반시계 방향&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라고 해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반적인 그래픽스 파이프라인에서는 이 winding을 이용해서:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;이 Triangle이 앞면인지 뒷면인지&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;판단할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Back-face Culling&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 켜져 있으면 뒷면이라고 판단한 Triangle은 아예 렌더링하지 않아.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;네 Ribbon 코드는 어떤 방향인가?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;진행 방향이 오른쪽이라고 단순화해보자.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;L0 ●────────● L1
   │        │
   │        │
R0 ●────────● R1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Vertex 번호:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;0 = L0
1 = R0
2 = L1
3 = R1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;첫 Triangle은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0, 1, 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;L0
 &amp;darr;
R0
   ↗
     L1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이고, 일반적인 &lt;/span&gt;X &amp;rarr; 오른쪽&lt;span&gt;, &lt;/span&gt;Y &amp;rarr; 위쪽&lt;span&gt; 2D 좌표 기준으로 보면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;반시계 방향(CCW)&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 번째:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1, 3, 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;도:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;R0 &amp;rarr; R1 &amp;rarr; L1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;으로 같은 CCW 방향을 유지해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 네 Index 구성은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;indices.push(L0, R0, L1);
indices.push(R0, R1, L1);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;두 Triangle의 winding을 동일하게 유지&lt;/span&gt;&lt;/b&gt;&lt;span&gt;하고 있는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이게 중요해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만약 하나를 잘못해서:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;indices.push(L0, R0, L1);
indices.push(R0, L1, R1);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 넣으면 두 번째 Triangle의 방향이 반대로 될 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Culling이 켜져 있다면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;첫 Triangle  &amp;rarr; 보임
두 번째      &amp;rarr; 안 보임

결과:
사각형 절반만 보임&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 기묘한 현상이 나타날 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;CW냐 CCW냐를 무조건 외워야 하나?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;무조건 CCW가 정답&amp;rdquo;이라고 외우면 위험해.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞면을 CW로 볼지 CCW로 볼지는 Graphics API/Engine/Render State의 설정에 따라 정할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반적인 WebGL/OpenGL 관례에서는 CCW를 front face로 사용하는 경우가 흔하지만, 엔진이 좌표 변환이나 Render State를 어떻게 설정했는지에 따라 달라질 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 실무적으로 중요한 건:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;한 Mesh 안에서는 Triangle의 winding order를 일관되게 유지하고, Material의 Culling 설정과 맞춰야 한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 Transform에:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Scale X = -1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;음수 Scale&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 적용해서 좌우 반전을 시키면 Geometry의 orientation이 뒤집혀 winding이 반전되는 경우도 있으니 그것도 알아두면 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;4. Index Buffer를 쓰는 이유&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사각형을 Triangle 두 개로 만들 때 그냥 Vertex를 각각 넣으면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Triangle 1
L0 R0 L1

Triangle 2
R0 R1 L1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;총 6개의 Vertex 정보를 넣어야 해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 실제로 고유한 Vertex는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;L0 R0 L1 R1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;4개뿐이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Vertex Buffer
0 L0
1 R0
2 L1
3 R1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;한 번만 저장하고,&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Index Buffer
0 1 2
1 3 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 재사용하지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Ribbon이 계속 이어지면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;L0 ●────● L1────● L2────● L3
   │    │       │       │
R0 ●────● R1────● R2────● R3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Vertex는 각 Point당 2개만 추가하고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;각 Segment마다:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;6 indices
= Triangle 2개&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 추가할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 네 코드의:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const vertCount = count * 2;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;와:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;if (i &amp;lt; count - 1) {
    const base = i * 2;

    indices.push(
        base,
        base + 1,
        base + 2
    );

    indices.push(
        base + 1,
        base + 3,
        base + 2
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 정확히 이 구조야.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;5. 네가 고생했던 전체 과정을 그래픽스 파이프라인 관점으로 정리하면&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Touch Point
     &amp;darr;
Catmull-Rom
     &amp;darr;
Smooth Point
     &amp;darr;
Normal 계산
     &amp;darr;
Left / Right Vertex 생성
     &amp;darr;
┌──────────────────────────────┐
│ Vertex Attributes            │
│                              │
│ Position &amp;rarr; xyz               │
│ UV       &amp;rarr; uv                │
│ Color    &amp;rarr; rgba              │
└──────────────────────────────┘
     &amp;darr;
VertexFormat와 일치시켜
각 Attribute Buffer 전달
     &amp;darr;
Index Buffer
0 1 2
1 3 2
     &amp;darr;
Triangle 2개
     &amp;darr;
Ribbon Segment
     &amp;darr;
MeshRenderer / Material
     &amp;darr;
GPU&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 보면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;면접용으로는 이렇게 정리하면 꽤 좋다&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;Ribbon Mesh 구현 과정에서 가장 어려웠던 부분 중 하나가 Cocos Creator 2.x의 Vertex Buffer 구성 방식이었습니다. 처음에는 Position, UV, Color를 하나의 interleaved ArrayBuffer로 구성했는데, 이를 &lt;/span&gt;setVertices(ATTR_POSITION, data)&lt;span&gt;에 통째로 전달하면서 엔진이 전체 데이터를 Position Attribute로 해석해 Vertex Layout이 깨지는 문제가 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이후 VertexFormat이 정의한 입력과 실제 데이터 전달 방식을 맞추기 위해 Position, UV, Color 배열을 각각 분리해서 Attribute별로 전달하도록 수정했습니다. 또한 사용하던 Shader에서 &lt;/span&gt;a_color&lt;span&gt;를 요구하고 있어서 Color Attribute도 포맷에 추가하고 흰색을 기본값으로 넣었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Ribbon Geometry는 중심 경로마다 좌우 Vertex 두 개를 생성하고, 인접한 네 Vertex를 Index Buffer에서 두 개의 Triangle로 연결했습니다. 이때 두 Triangle의 winding order를 동일하게 유지해야 Back-face Culling이 적용되더라도 일부 면이 사라지지 않기 때문에 Index 순서도 일관되게 구성했습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 설명이면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Vertex Attribute &amp;rarr; VertexFormat &amp;rarr; Buffer &amp;rarr; Index &amp;rarr; Triangle &amp;rarr; Winding/Culling &amp;rarr; Shader&lt;/span&gt;&lt;/b&gt;&lt;span&gt;까지 하나의 흐름으로 연결돼서, 그래픽스 질문이 들어와도 꽤 탄탄하게 방어할 수 있어.&lt;/span&gt;&lt;/p&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/45</guid>
      <comments>https://letscoding0103.tistory.com/45#entry45comment</comments>
      <pubDate>Fri, 16 Oct 2026 02:23:43 +0900</pubDate>
    </item>
    <item>
      <title>ICT 인턴십 SWINK 포트폴리오 정리</title>
      <link>https://letscoding0103.tistory.com/44</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Stretch
&amp;rarr; 한 텍스처를 길게 늘임
&amp;rarr; Texture 왜곡 문제

Sprite Chain
&amp;rarr; 여러 Sprite를 이어 붙임
&amp;rarr; 연결부 / 겹침 / 각짐 문제

Ribbon Mesh + UV Tiling
&amp;rarr; 하나의 연속 Mesh
&amp;rarr; UV를 거리 기반으로 반복
&amp;rarr; 연결부 감소 + Texture 밀도 유지&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 네 이미지가 정확히 기존 구현을 나타낸다면, 지금 문장의:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;단순 Stretch 방식에서 발생하던 텍스처 왜곡을 줄였습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Sprite Chain의 문제를 설명하는 문장으로는 정확하지 않아.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;차라리 이렇게 쓰는 게 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Stroke의 누적 이동 거리를 기준으로 UV 좌표를 계산해 Ribbon 길이와 관계없이 모래 텍스처가 일정한 밀도로 반복되도록 구현했습니다. 기존 Sprite Chain 방식에서 발생하던 이미지 연결부와 구간별 텍스처 밀도 차이를 줄이고, 하나의 연속된 Ribbon 형태로 표현했습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;만약 실제 구현에서 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Sprite 하나를 각 구간 길이에 맞게 늘려가며 Chain을 만들었다면&lt;/span&gt;&lt;/b&gt;&lt;span&gt; Stretch 문제도 동시에 있었다고 말할 수 있어. 하지만 &lt;/span&gt;Sprite Chain = Stretch&lt;span&gt;라고 동일시하면 안 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 하나 더. 현재 문서에는 첫 문장이:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존 &lt;/span&gt;RenderTexture&lt;span&gt; 기반 구현에서는...&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라고 되어 있는데 그림은:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존 &lt;/span&gt;Sprite Chain&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라고 되어 있잖아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;둘이 실제로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;서로 다른 이전 구현 단계&lt;/span&gt;&lt;/b&gt;&lt;span&gt;였다면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;RenderTexture
      &amp;darr; 성능 문제
Sprite Chain
      &amp;darr; 연결부/품질 문제
Ribbon Mesh&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 히스토리를 명시하는 게 좋아. 그렇지 않고 같은 기존 구현을 설명하는 거라면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;둘 중 실제 사용했던 방식으로 통일해야 해.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 면접관이 이거 보면 높은 확률로 &amp;ldquo;RenderTexture를 쓴 겁니까 Sprite Chain을 쓴 겁니까?&amp;rdquo;라고 물을 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;이 경험에서 나올 가능성이 높은 면접 질문과 답변&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 왜 Sprite Chain 대신 Ribbon Mesh를 사용했나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존 Sprite Chain 방식은 입력 경로를 따라 여러 개의 사각형 Sprite를 연결하기 때문에 방향이 급격하게 변할 경우 Sprite 사이의 연결부나 겹침이 눈에 띄는 문제가 있었습니다. 또한 객체 수가 증가하면서 개별 Sprite 관리 비용도 증가했습니다. 그래서 입력 경로를 하나의 연속된 Mesh로 구성하는 Ribbon Mesh 방식으로 변경했습니다. 경로를 기준으로 좌우 Vertex를 생성하고 Triangle로 연결해서 하나의 연속된 Stroke를 표현했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 꼬리질문:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Q. Mesh라고 해서 무조건 Sprite보다 빠른가요?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;아닙니다. Mesh가 항상 빠른 것은 아닙니다. Vertex 수가 지나치게 많거나 매 프레임 전체 Mesh를 재생성하면 오히려 비용이 커질 수 있습니다. 제 경우에는 다수의 개별 Sprite 객체를 관리하는 방식보다 하나의 연속 Mesh로 경로를 표현하는 것이 객체 관리와 시각적 연결 측면에서 적합해서 선택했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 답이 중요해.&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;Mesh가 무조건 Sprite보다 성능이 좋다&amp;rdquo;는 식으로 말하면 안 돼.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Ribbon Mesh는 어떻게 생성했나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;터치 입력으로 중심 경로의 Point를 얻고, 각 Point에서 진행 방향을 계산했습니다. 그 진행 방향에 수직인 방향으로 일정 폭만큼 좌우 Vertex를 생성한 뒤, 이전 Point의 좌우 Vertex와 현재 Point의 좌우 Vertex를 삼각형으로 연결하여 Ribbon 형태의 Mesh를 만들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개념적으로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;       L1 -------- L2
       |         / |
       |       /   |
       |     /     |
       R1 -------- R2

L = Left Vertex
R = Right Vertex&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 점마다:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;L0, R0
L1, R1
L2, R2
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 만들고 삼각형으로 연결하는 구조라고 설명하면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 좌우 Vertex 방향은 어떻게 구하나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;2D라면 중심점의 진행 방향이:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;direction = current - previous&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일 때 정규화하고, 수직 벡터를 만들 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;direction = (x, y)

normal = (-y, x)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Left  = center + normal &amp;times; halfWidth
Right = center - normal &amp;times; halfWidth&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;면접 답변:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;입력 경로의 이전 점과 현재 점의 차이로 진행 방향을 구하고 정규화한 뒤, 그 방향에 수직인 Normal을 계산했습니다. 이후 중심점에서 Normal 방향으로 Ribbon 폭의 절반만큼 더하고 빼서 좌우 Vertex를 만들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 질문은 꽤 나올 만해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 왜 거리 기반으로 UV를 계산했나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 네 포트폴리오에서 상당히 좋은 질문거리야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 Texture 하나를 Ribbon 전체에:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;U = 0 ---------------------- U = 1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 매핑하면 Stroke 길이가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;10px&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이든&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1000px&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이든 텍스처 하나가 전체 구간을 차지하게 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 긴 Stroke에서는 Texture가 길게 늘어나지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 누적 거리를:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;0
10
20
30
40
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 계산하고,&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;U = 누적 거리 / Texture 반복 길이&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;형태로 UV를 만들면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 반복 간격이 100이고 Stroke 길이가 300이면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;거리     U

0     &amp;rarr; 0
100   &amp;rarr; 1
200   &amp;rarr; 2
300   &amp;rarr; 3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Texture Wrap Mode가 Repeat라면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;[Texture][Texture][Texture]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 세 번 반복돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;면접에서는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Stroke의 전체 길이에 UV 0~1을 단순 매핑하면 Stroke 길이가 증가할수록 Texture가 늘어나기 때문에, 각 Point까지의 누적 이동 거리를 계산하고 일정 Tile Length로 나눈 값을 U좌표로 사용했습니다. 그래서 Ribbon 길이가 변해도 텍스처의 시각적 밀도를 일정하게 유지할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 정도가 아주 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. UV가 정확히 뭔가요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;의외로 바로 들어올 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;UV는 Mesh의 각 Vertex가 Texture의 어느 위치를 참조할지를 나타내는 2차원 좌표입니다. 일반적으로 U가 Texture의 가로축, V가 세로축에 대응합니다. 제 Ribbon에서는 V를 좌우 방향에 0과 1로 배치하고, U를 Stroke의 누적 이동 거리에 따라 증가시켜 텍스처가 진행 방향으로 반복되도록 만들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 구조가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Left Vertex   V = 0
────────────────────────────
 U &amp;rarr;

Right Vertex  V = 1
────────────────────────────&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 생각하면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Catmull-Rom Spline을 왜 사용했나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;터치 입력은 매 프레임 완전히 연속적으로 들어오는 것이 아니라 일정한 간격의 샘플 Point로 들어옵니다. 이 Point를 직선으로 그대로 연결하면 입력 속도가 빠르거나 방향이 크게 변할 때 각진 Stroke가 만들어졌습니다. 그래서 인접한 Point들을 이용해 중간 경로를 보간하는 Catmull-Rom Spline을 적용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 왜 Catmull-Rom이었냐고 물으면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Catmull-Rom은 주어진 Control Point를 실제 곡선이 통과한다는 특징이 있어서 사용자의 입력 경로를 보존하면서 중간 점을 만들기에 적합했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 부분이 핵심이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Bezier와 비교하면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Bezier
&amp;rarr; Control Point가 보통 곡선을 직접 통과하지 않음

Catmull-Rom
&amp;rarr; Control Point를 통과&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 &lt;/span&gt;&lt;b&gt;&lt;span&gt;사용자가 실제로 지나간 Point를 기반으로 자연스러운 경로를 만들기 좋다&lt;/span&gt;&lt;/b&gt;&lt;span&gt;고 설명하면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Catmull-Rom은 몇 개의 Point가 필요한가요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기본적으로 한 구간을 계산할 때:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;P0, P1, P2, P3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;4개의 Point를 이용해 &lt;/span&gt;P1 &amp;rarr; P2&lt;span&gt; 사이를 보간해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;면접 답변:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;한 구간을 보간하기 위해 주변의 네 Point를 사용합니다. 이전 Point와 다음 Point까지 함께 고려하기 때문에 단순 선형 보간보다 점과 점 사이의 방향 변화가 자연스럽게 연결됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;수식까지 요구하면 그때 설명하면 되지만, 게임 클라이언트 면접에서는 보통 &lt;/span&gt;&lt;b&gt;&lt;span&gt;왜 4개인지와 어떤 특성이 있는지&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 이해하고 있는 게 더 중요해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 그냥 Lerp를 여러 번 하면 안 됐나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Lerp는 두 점 사이를 직선으로 보간하기 때문에 Point 수를 늘릴 수는 있어도 기본 경로 자체는 직선입니다. 따라서 연속된 여러 Segment가 만나는 부분에서는 방향 변화가 그대로 남습니다. Catmull-Rom은 주변 Point를 함께 고려하여 곡선의 접선 방향을 결정하기 때문에 Point 사이를 더 자연스럽게 연결할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;Lerp

P0 ───── P1
          \
           \
            P2

&amp;rarr; 꺾임 존재&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면 Spline은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;P0 ─────╮
        ╰──── P2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 연결되는 거지.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Catmull-Rom을 적용하면 성능 비용도 증가하지 않나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;맞습니다. 원본 입력 Point 사이에 추가 Point를 생성하기 때문에 Vertex 수와 보간 계산량이 증가합니다. 그래서 보간을 많이 할수록 무조건 좋은 것이 아니라 시각적 품질과 Vertex 수 사이의 균형을 잡아야 합니다. 실제 구현에서는 필요한 수준의 Sampling Resolution만 사용하도록 조절하는 것이 중요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 한 단계 더 좋은 답:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사용자 입력 속도나 Point 사이 거리 등을 기준으로 Sampling 밀도를 동적으로 조절하는 것도 개선 방법이라고 생각합니다.&lt;br /&gt;&lt;br /&gt;주요 활용 분야:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACCIQAQ&quot; data-epip=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACCIQAg&quot; data-epip=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-complete=&quot;true&quot; data-epip=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;컴퓨터 그래픽스 및 애니메이션&lt;/b&gt;: 캐릭터의 이동 경로, 물체의 부드러운 트랙킹 등을 구현할 때 사용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACCIQAw&quot; data-epip=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-complete=&quot;true&quot; data-epip=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;게임 개발&lt;/b&gt;: 카메라가 일련의 키프레임(Keyframe)을 따라 물흐듯 부드럽게 이동하게 하거나, 유도탄/점프 등의 이동 궤적을 계산할 때 활용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-complete=&quot;true&quot; data-hveid=&quot;CAIIAAgACCIQBA&quot; data-epip=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-complete=&quot;true&quot; data-epip=&quot;&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;2D/3D 벡터 그래픽&lt;/b&gt;: 마우스나 펜으로 입력한 불연속적인 좌표들 사이를 매끄럽게 연결하는 선을 그릴 때 쓰입니다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. RenderTexture와 Ribbon Mesh의 근본적인 차이는 무엇인가요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;RenderTexture 방식은 입력 결과를 Texture의 Pixel에 기록하는 이미지 기반 방식이고, Ribbon Mesh는 입력 경로를 Vertex와 Triangle로 구성하여 Geometry로 표현하는 방식입니다. RenderTexture에서는 텍스처 해상도나 반복적인 픽셀 갱신 비용의 영향을 받을 수 있는 반면, Ribbon Mesh는 Stroke 복잡도에 따라 Vertex 수와 Mesh 갱신 비용이 증가하는 특성이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 질문에서는 한쪽이 무조건 좋다고 하면 안 돼.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;RenderTexture
비용 &amp;rarr; 해상도 / 픽셀 처리 / 갱신 범위 영향

Ribbon Mesh
비용 &amp;rarr; Point / Vertex / Triangle 수 영향&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;trade-off&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 알고 있는 게 중요해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 그러면 왜 항상 Ribbon Mesh를 사용하지 않나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;요구사항에 따라 다릅니다. 단순한 그림판처럼 Stroke를 최종 이미지로 누적시키는 것이 목적이면 RenderTexture가 효율적일 수 있습니다. 반면 제 프로젝트처럼 Stroke 자체에 반복 Texture를 적용하고 곡선의 형태나 폭을 실시간으로 제어해야 하는 경우에는 Mesh 방식이 더 유연했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 답이 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기술 선택은:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A가 B보다 좋다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 아니라:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;현재 요구사항에서 A의 장점이 더 적합했다.&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 설명하는 게 중요해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 실시간으로 Mesh를 계속 변경하면 GC가 발생하지 않나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 질문은 성능 최적화 프로젝트라면 꽤 위험하면서도 좋은 질문이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;답은 구현에 따라 달라지지만 원칙적으로는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Vertex나 UV List를 매 프레임 새로 생성하거나 배열을 반복적으로 할당한다면 GC가 발생할 수 있습니다. 그래서 실시간 Mesh 생성에서는 List나 Buffer를 재사용하고, 충분한 Capacity를 미리 확보하거나 필요한 부분만 갱신하는 방식으로 런타임 할당을 최소화하는 것이 중요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 네 구현이 &lt;/span&gt;&lt;b&gt;&lt;span&gt;매번 &lt;/span&gt;new List&lt;span&gt;를 했다면 &amp;ldquo;저는 재사용했습니다&amp;rdquo;라고 하면 안 되고&lt;/span&gt;&lt;/b&gt;&lt;span&gt;, 이렇게 답해야 해:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;당시 구현에서는 실시간 Mesh 방식 자체를 우선 구현했고, 추가 최적화가 필요하다면 Vertex/UV Buffer 재사용과 Capacity 사전 확보를 적용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 코드보다 좋게 말하면 안 돼.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Sprite Chain 방식의 단점은 객체 수 말고 무엇이 있나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;곡선에서 각 Sprite의 회전값이 달라지면서 연결부가 벌어지거나 겹칠 수 있고, 급격한 방향 전환에서는 직사각형 형태가 드러나면서 Stroke가 각져 보일 수 있습니다. 이를 감추기 위해 Sprite를 더 촘촘하게 생성하면 반대로 객체 수와 갱신 비용이 증가하는 trade-off가 생깁니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 네 그림하고 딱 연결돼.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Sprite 적게 사용
&amp;rarr; 성능 &amp;uarr;
&amp;rarr; 연결부 품질 &amp;darr;

Sprite 많이 사용
&amp;rarr; 품질 &amp;uarr;
&amp;rarr; 객체 수 / 갱신 비용 &amp;uarr;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Ribbon Mesh로 바꾼 이유를 아주 잘 설명할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 다중 터치가 되면 Ribbon Mesh는 어떻게 각각 구분했나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 네 다른 핵심 경험하고 연결돼.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;각각의 Touch ID를 Key로 독립적인 Touch Context를 관리했습니다. Context마다 현재 Stroke의 입력 Point와 Ribbon Mesh 인스턴스를 별도로 가지고 있기 때문에 여러 Touch가 동시에 들어와도 서로의 Mesh를 수정하지 않도록 구성했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개념적으로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Touch ID 1
&amp;rarr; Points 1
&amp;rarr; Ribbon Mesh 1

Touch ID 2
&amp;rarr; Points 2
&amp;rarr; Ribbon Mesh 2

Touch ID 3
&amp;rarr; Points 3
&amp;rarr; Ribbon Mesh 3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이걸 하나의 전역:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;currentPoints
currentMesh&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 처리하면 여러 손가락이 동시에 입력될 때 상태가 뒤섞여.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 이건 단순히 &amp;ldquo;멀티터치를 지원했다&amp;rdquo;보다:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Touch ID 단위의 독립된 상태 컨텍스트를 설계했다&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;고 설명하는 게 훨씬 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 성능 최적화 효과는 어떻게 검증했나요?&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서는 실제 측정한 범위만 말해야 해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 네 경험에 맞춰서는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;초기 구현에서 다중 터치가 증가했을 때 프레임 저하를 확인했고, Sprite/Object 관리 방식과 드로잉 렌더링 구조를 개선한 뒤 실제 목표 전자칠판 환경에서 다중 터치 테스트를 수행했습니다. Object Pooling, 점진적인 객체 회수, Ribbon Mesh 기반 Rendering 등을 적용한 이후 목표인 약 60 FPS 수준을 안정적으로 유지할 수 있는지 확인했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 면접관이:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;Profiler 수치는?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 물었는데 정확한 수치를 기록하지 않았다면 솔직하게:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;당시에는 Cocos Creator 2.4.x에서 제공하는 내장 프로파일러로 단순한 실시간 수치 통계만을 확인했습니다. 심층적인 성능 분석이 필요한 경우 외부 도구를 함께 활용할 수 있으나 해당 부분까지는 확인하지 않았습니다.&quot;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;먼저 1분 답변은 이렇게 가져가면 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;전자칠판에서 여러 사용자가 동시에 그림을 그릴 때, 터치 경로를 따라 Sprite를 이어 붙이는 방식은 곡선에서 연결부가 드러나고 객체 수가 늘어나는 문제가 있었습니다. 그래서 입력 경로를 하나의 연속된 Ribbon Mesh로 생성하는 방식으로 변경했습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;터치 Point는 Catmull-Rom Spline으로 보간해 부드러운 중심 경로를 만들고, 각 Point에서 진행 방향에 수직인 벡터를 계산해 좌우 Vertex를 생성했습니다. 인접한 Vertex는 두 개의 Triangle로 연결해 Ribbon 형태의 Mesh를 만들었습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;또 Stroke의 누적 이동 거리를 기준으로 UV의 U값을 계산하고 Texture Repeat를 적용해, 선이 길어져도 모래 텍스처가 늘어나지 않고 일정한 밀도로 반복되도록 했습니다. 입력 Point는 최소 이동 거리를 기준으로 Sampling하고 기존 Vec2, Vec3 배열을 재사용해서 불필요한 객체 생성도 줄였습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 답변 다음에는 아래 정도를 준비하면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 왜 Sprite Chain이 아니라 Ribbon Mesh를 사용했나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; Sprite Chain은 경로를 따라 여러 사각형 Sprite를 배치하기 때문에 급격한 방향 전환에서 Sprite 사이의 겹침이나 연결부가 드러날 수 있고, Stroke가 길어질수록 관리해야 하는 객체도 증가합니다. Ribbon Mesh는 전체 경로를 하나의 연속된 Geometry로 표현할 수 있어서 연결부 표현을 개선하고, Vertex와 Triangle 단위로 경로를 제어하기에도 적합했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Mesh가 Sprite보다 무조건 빠른 건가요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 아닙니다. Mesh도 Vertex 수가 너무 많거나 매번 전체 Mesh를 갱신하면 CPU와 GPU 비용이 증가합니다. 제 경우에는 개별 Sprite 객체를 많이 관리하는 비용과 곡선의 연결 문제를 해결하는 데 Ribbon Mesh가 더 적합했습니다. 요구사항에 따른 선택이지 Mesh 자체가 항상 더 빠른 것은 아닙니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Ribbon Mesh를 구체적으로 어떻게 만들었나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 보간된 중심 경로의 각 Point에서 다음 Point와의 차이로 진행 방향을 구하고 정규화했습니다. 그 방향의 수직 벡터 &lt;/span&gt;(-dirY, dirX)&lt;span&gt;를 구한 뒤 중심점에서 Ribbon 폭의 절반만큼 더하고 빼 좌우 Vertex를 만들었습니다. 그리고 인접한 두 Point의 좌우 Vertex 네 개를 두 개의 Triangle로 연결했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 왜 수직 벡터를 &lt;/span&gt;(-y, x)&lt;span&gt;로 만들 수 있나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 2차원 벡터 &lt;/span&gt;(x, y)&lt;span&gt;를 90도 회전시키면 &lt;/span&gt;(-y, x)&lt;span&gt;가 됩니다. 진행 방향과 이 벡터의 내적은 0이므로 서로 수직입니다. 그래서 Ribbon의 진행 방향과 직각인 폭 방향을 만들 때 사용할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 왜 Catmull-Rom Spline을 사용했나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 터치 입력은 연속된 선이 아니라 일정 간격으로 들어오는 Sample Point이기 때문에 그대로 직선 연결하면 빠르게 움직이거나 방향이 바뀔 때 각진 형태가 나타났습니다. Catmull-Rom은 주변 Point를 이용해 부드러운 중간 경로를 만들면서 실제 Control Point를 통과하기 때문에 사용자 입력 경로를 유지하는 데 적합했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Catmull-Rom에는 왜 네 개의 Point가 필요한가요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;P1&lt;span&gt;에서 &lt;/span&gt;P2&lt;span&gt; 사이의 곡선을 계산할 때 앞뒤의 &lt;/span&gt;P0&lt;span&gt;, &lt;/span&gt;P3&lt;span&gt;까지 이용해 두 끝점의 접선 방향을 결정하기 때문입니다. 그래서 단순히 두 점만 사용하는 Lerp보다 구간 사이의 방향 변화가 자연스럽게 이어집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Lerp를 여러 번 쓰면 안 되나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; Lerp는 기본적으로 두 Point 사이를 직선으로 보간하기 때문에 Point 수를 늘려도 각 Segment의 경계에서 방향이 급격히 변할 수 있습니다. Catmull-Rom은 앞뒤 Point까지 고려해 접선을 구성하므로 연속된 곡선을 표현하기에 더 적합했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. &lt;/span&gt;SMOOTH_SEGMENT_STEPS = 4&lt;span&gt;는 무엇인가요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 원본 Point 사이 한 Segment를 몇 단계로 나눠 보간할지 결정하는 값입니다. 값을 늘리면 곡선은 부드러워지지만 생성되는 Point와 Vertex 수, 계산량도 증가합니다. 그래서 시각적 품질과 성능 사이의 trade-off 값입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 거리 기반 UV Tiling을 왜 사용했나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; Ribbon 전체 길이에 UV 0~1만 매핑하면 Stroke가 길어질수록 텍스처가 늘어납니다. 그래서 이전 Point부터 현재 Point까지의 거리를 누적하고 &lt;/span&gt;누적 거리 / tileLength&lt;span&gt;를 U값으로 사용했습니다. Texture의 WrapMode를 Repeat로 설정했기 때문에 U가 1, 2, 3으로 증가할 때마다 텍스처가 일정한 길이 단위로 반복됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. UV의 U와 V는 코드에서 각각 무엇을 의미하나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; U는 Stroke의 진행 방향이고 V는 Ribbon의 폭 방향입니다. 왼쪽 Vertex는 V=1, 오른쪽 Vertex는 V=0으로 두고, U는 누적 이동 거리에 따라 계속 증가하도록 만들었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. RenderTexture 방식인가요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 아닙니다. 이 구현은 &lt;/span&gt;cc.Mesh&lt;span&gt;와 &lt;/span&gt;cc.MeshRenderer&lt;span&gt;를 사용해서 Vertex, UV, Index를 직접 갱신하는 Geometry 기반 Ribbon Mesh 방식입니다. &lt;/span&gt;Texture2D&lt;span&gt;는 Mesh 표면에 입힐 모래 질감일 뿐 RenderTexture에 픽셀을 누적하는 구조는 아닙니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 실시간으로 Mesh를 갱신하면 GC가 많이 발생하지 않나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 그 가능성이 있어서 &lt;/span&gt;smoothPoints&lt;span&gt;, &lt;/span&gt;positions&lt;span&gt;, &lt;/span&gt;uvs&lt;span&gt;의 기존 Vec2/Vec3 객체를 재사용하도록 구현했습니다. 필요한 Capacity가 부족할 때만 추가 객체를 만들고, 이미 확보된 객체는 값만 갱신합니다. 다만 &lt;/span&gt;indices.length = 0&lt;span&gt; 후 다시 &lt;/span&gt;push&lt;span&gt;하는 부분이나 엔진 내부 &lt;/span&gt;setVertices&lt;span&gt; 구현까지 포함해 완전한 Zero-GC라고 주장할 수는 없고, 정확한 할당량은 Profiler로 확인해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 입력 Point를 왜 8픽셀 이상 움직였을 때만 추가했나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 미세한 손 떨림이나 지나치게 촘촘한 터치 이벤트를 전부 Point로 저장하면 Spline Point와 Vertex 수가 빠르게 증가합니다. 그래서 최소 이동 거리를 두어 입력 자체를 Sampling했습니다. 이를 통해 불필요한 Geometry 증가를 제한했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 코드에 30FPS Mesh Throttling도 있던데 정확히 30FPS로 제한되나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 현재 코드만 보면 정확히 그렇지는 않습니다. 앞에서 8픽셀 미만 이동은 이미 return하는데 Mesh 갱신 조건은 &lt;/span&gt;7픽셀 이상 이동 OR 1/30초 경과&lt;span&gt;라서, moveStroke가 다음 단계까지 온 경우 거리 조건이 대부분 먼저 참이 됩니다. 그래서 현재 구현의 실제 효과는 30Hz 제한보다는 거리 기반 Sampling이 더 큽니다. 다시 개선한다면 시간 기반 갱신을 우선하거나 조건을 분리해 의도한 Throttling이 명확하게 동작하도록 수정하겠습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 질문은 오히려 &lt;/span&gt;&lt;b&gt;&lt;span&gt;꼭 준비해두는 게 좋아.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 코드를 자세히 보는 면접관이면 발견할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 왜 &lt;/span&gt;OR&lt;span&gt; 대신 &lt;/span&gt;AND&lt;span&gt;로 바꾸면 안 되나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;AND&lt;span&gt;를 사용하면 충분히 이동했고 동시에 최소 시간이 지나야 갱신되기 때문에 확실한 빈도 제한이 됩니다. 대신 빠르게 큰 거리를 움직여도 최대 약 30Hz로만 화면에 반영돼 입력 반응성이 떨어질 수 있습니다. 그래서 목적에 따라 &amp;ldquo;시간 기준&amp;rdquo;, &amp;ldquo;거리 기준&amp;rdquo;, &amp;ldquo;둘의 조합&amp;rdquo;을 결정해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 현재 코드는 매번 전체 Mesh를 다시 계산하는 것 아닌가요? 더 최적화할 수 있나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 맞습니다. &lt;/span&gt;updateMesh()&lt;span&gt;에서 현재까지의 전체 Smooth Point를 다시 순회하고 있습니다. Stroke가 길어질수록 갱신 비용이 증가하는 구조입니다. 개선한다면 새롭게 추가된 Segment 부분만 Vertex와 Index Buffer에 append하거나, 일정 길이가 지나면 Mesh를 chunk 단위로 분할하여 이미 완성된 구간은 더 이상 갱신하지 않는 방식이 가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 굉장히 좋은 고급 꼬리질문이야.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Ribbon의 급격한 코너에서 폭이 이상해지지 않나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 현재 구현은 각 Point에서 하나의 Normal을 이용해 좌우 Vertex를 단순 생성하기 때문에 아주 급격한 코너에서는 안쪽이 겹치거나 바깥쪽 폭이 부자연스러울 수 있습니다. 더 정교하게 구현한다면 이전 Segment와 다음 Segment의 Normal을 평균낸 Join 방향을 사용하거나 Miter, Bevel, Round Join 같은 방식으로 코너를 처리할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Stroke의 시작점과 끝점은 어떻게 처리하나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 현재 구현에서는 중심 경로의 마지막 Point에서 이전 Segment 방향을 사용해 좌우 Vertex를 만드는 단순한 끝 처리입니다. 별도의 Cap Geometry는 없습니다. 시각적으로 더 완성도 있게 만들려면 Round Cap이나 별도의 시작/끝 Mesh를 추가할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Catmull-Rom을 쓰면 원래 입력 경로에서 너무 튀어나가는 경우는 없나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 가능합니다. 특히 Point 간격이 불균일하거나 방향 변화가 큰 경우 일반 Catmull-Rom에서 overshoot가 생길 수 있습니다. 필요하다면 입력 Point 간격을 일정하게 resampling하거나, Centripetal Catmull-Rom처럼 overshoot가 덜한 변형을 사용하는 방법을 고려할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 다중 터치에서는 이 Ribbon 하나를 어떻게 관리했나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 각 Touch ID별로 독립된 Stroke/Ribbon 인스턴스를 연결하는 구조로 관리했습니다. 하나의 전역 &lt;/span&gt;rawPoints&lt;span&gt;를 여러 터치가 공유하게 하면 서로의 입력이 섞이기 때문에, Touch ID를 Key로 개별 Context와 Ribbon을 연결해 각 사용자의 &lt;/span&gt;Start &amp;rarr; Move &amp;rarr; End&lt;span&gt; 생명주기를 독립적으로 처리했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. Object Pooling하고 이 Ribbon Mesh가 어떤 관계가 있나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; Ribbon 생성 방식과 Pooling은 별개의 최적화입니다. Ribbon Mesh는 Stroke의 렌더링 구조를 개선한 것이고, Object Pooling은 반복적으로 생성되는 게임 오브젝트와 이펙트의 Instantiate/Destroy 및 메모리 할당 비용을 줄이기 위한 것입니다. 서로 다른 병목을 해결한 최적화라고 설명할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Q. 성능이 좋아졌다는 걸 어떻게 검증했나요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;A.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 당시에는 Cocos Creator 2.4.x의 내장 프로파일러를 이용해 실제 전자칠판 환경에서 Framerate, Draw Call, Frame Time, Game Logic, Renderer 같은 실시간 통계를 확인했습니다. 특히 다중 터치 상황에서 최적화 전에는 눈에 띄는 프레임 저하와 끊김이 있었는데, Ribbon Mesh와 입력 샘플링, 객체 재사용 등의 최적화를 적용한 뒤 실제 목표 장비에서 약 60FPS가 유지되는 것을 확인했고, 동시에 사용자가 직접 드로잉했을 때의 끊김이나 반응성도 육안으로 확인했습니다. 다만 당시에는 내장 프로파일러를 이용한 실시간 지표와 실제 장비 테스트를 중심으로 검증했고, CPU/GPU별 병목이나 GC Allocation을 외부 프로파일러로 세밀하게 전후 비교해 수치화하지는 못했습니다. 다시 진행한다면 Chrome DevTools나 플랫폼별 GPU/메모리 프로파일링 도구를 병행해서 Frame Time, GC, Draw Call, Vertex 수 등을 변경 전후로 기록할 것 같습니다.&amp;rdquo;&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;그럼 정말 Ribbon Mesh 때문에 빨라졌다는 걸 증명할 수 있나요?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;엄밀하게는 여러 최적화를 함께 적용했기 때문에 Ribbon Mesh 하나의 개선 효과를 독립적으로 정량화했다고 말할 수는 없습니다. 당시에는 실제 타깃 장비에서 다중 터치 시 발생하던 프레임 저하를 재현하고, 개선 후 내장 Stats에서 목표 프레임을 유지하는 것과 체감 끊김이 사라진 것을 기준으로 검증했습니다. 보다 정확한 비교를 한다면 각 최적화를 하나씩 적용하는 A/B 테스트와 Frame Time 측정을 통해 기여도를 분리했을 것 같습니다.&amp;rdquo;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; letter-spacing: 0px;&quot;&gt;&lt;br /&gt;마지막으로 면접에서 제일 좋은 흐름은 **&amp;ldquo;Ribbon Mesh가 무조건 좋은 기술이라서 썼다&amp;rdquo;**가 아니라 이렇게 말하는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;다중 터치 환경에서 기존 방식의 객체 관리와 연결부 표현이 요구사항에 맞지 않았고, Stroke를 하나의 연속 Geometry로 제어해야 했기 때문에 Ribbon Mesh를 선택했습니다. 대신 Vertex 수와 Mesh 갱신 비용이라는 새로운 trade-off가 생겼기 때문에 입력 Sampling과 데이터 재사용을 같이 적용했습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;면접에서 세 기술을 연결해서 설명한다면&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 답변 하나로 꽤 많은 걸 커버할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;전자칠판의 터치 입력은 일정 간격의 Point로 들어오기 때문에 그대로 연결하면 Stroke가 각지게 표현됐습니다. 그래서 주변 네 Point를 참고하여 실제 입력 Point를 통과하는 Catmull-Rom Spline으로 중간 경로를 보간했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;보간된 중심 경로에서는 진행 방향에 수직인 벡터를 구해 좌우 Vertex를 만들고 Triangle로 연결하여 Ribbon Mesh를 생성했습니다. 텍스처는 전체 Ribbon에 단순히 0~1 UV를 Stretch하지 않고, 각 Point까지의 누적 이동 거리를 Tile Length로 나눈 값을 U좌표로 사용해 Stroke의 길이에 관계없이 일정한 크기로 반복되도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 터치 입력과 Mesh Vertex가 서로 다른 좌표 공간을 사용할 수 있기 때문에 World 좌표의 입력을 &lt;/span&gt;convertToNodeSpaceAR&lt;span&gt;로 Ribbon Node의 Local 좌표로 변환한 후 Mesh를 생성했습니다. 이후 Local에서 실제 화면까지의 변환은 Cocos의 렌더링 파이프라인이 처리하도록 했습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 정도 이해하고 있으면 단순히 &lt;/span&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;Catmull-Rom을 가져다 썼다&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 수준이 아니라, &lt;/span&gt;&lt;b&gt;&lt;span&gt;왜 필요한지 &amp;rarr; 어떻게 Mesh가 되는지 &amp;rarr; Texture는 어떻게 붙는지 &amp;rarr; 좌표는 왜 변환하는지&lt;/span&gt;&lt;/b&gt;&lt;span&gt;까지 하나의 파이프라인으로 설명할 수 있어.&lt;/span&gt;&lt;/p&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/44</guid>
      <comments>https://letscoding0103.tistory.com/44#entry44comment</comments>
      <pubDate>Fri, 16 Oct 2026 02:20:21 +0900</pubDate>
    </item>
    <item>
      <title>면접 준비 Game Server Feat.Neon Rush</title>
      <link>https://letscoding0103.tistory.com/43</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1. 면접 답변용: &amp;ldquo;위치 동기화를 어떻게 보간했나요?&amp;rdquo; &lt;br /&gt;초기에는&amp;nbsp;서버에서&amp;nbsp;수신한&amp;nbsp;위치와&amp;nbsp;회전을&amp;nbsp;transform에&amp;nbsp;즉시&amp;nbsp;적용했는데,&amp;nbsp;패킷&amp;nbsp;자체는&amp;nbsp;50Hz로&amp;nbsp;충분히&amp;nbsp;자주&amp;nbsp;오더라도&amp;nbsp;실제&amp;nbsp;도착&amp;nbsp;간격은&amp;nbsp;네트워크&amp;nbsp;지터&amp;nbsp;때문에&amp;nbsp;일정하지&amp;nbsp;않아&amp;nbsp;원격&amp;nbsp;차량이&amp;nbsp;순간적으로&amp;nbsp;끊겨&amp;nbsp;보이는&amp;nbsp;문제가&amp;nbsp;있었습니다. &lt;br /&gt;&lt;br /&gt;그래서&amp;nbsp;수신한&amp;nbsp;Position과&amp;nbsp;Rotation을&amp;nbsp;바로&amp;nbsp;적용하지&amp;nbsp;않고&amp;nbsp;targetPosition,&amp;nbsp;targetRotation으로&amp;nbsp;저장한&amp;nbsp;뒤,&amp;nbsp;렌더링&amp;nbsp;프레임마다&amp;nbsp;Vector3.Lerp와&amp;nbsp;Quaternion.Slerp를&amp;nbsp;이용해&amp;nbsp;현재&amp;nbsp;Transform이&amp;nbsp;목표값을&amp;nbsp;따라가도록&amp;nbsp;구성했습니다. &lt;br /&gt;&lt;br /&gt;이후에는&amp;nbsp;고정된&amp;nbsp;보간&amp;nbsp;속도만&amp;nbsp;사용하면&amp;nbsp;빠른&amp;nbsp;차량에서는&amp;nbsp;뒤처지고,&amp;nbsp;저속에서는&amp;nbsp;불필요한&amp;nbsp;미세&amp;nbsp;보정이&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있어&amp;nbsp;이전&amp;nbsp;수신&amp;nbsp;위치와&amp;nbsp;현재&amp;nbsp;수신&amp;nbsp;위치의&amp;nbsp;거리,&amp;nbsp;패킷&amp;nbsp;도착&amp;nbsp;시간&amp;nbsp;차이를&amp;nbsp;이용해&amp;nbsp;추정&amp;nbsp;속도를&amp;nbsp;계산하고,&amp;nbsp;속도가&amp;nbsp;높을수록&amp;nbsp;보간&amp;nbsp;속도를&amp;nbsp;증가시키도록&amp;nbsp;개선했습니다. &lt;br /&gt;&lt;br /&gt;한 가지 기술적인 디테일로, Lerp(current, target, deltaTime * smoothing)은 엄밀히 말하면 스냅샷 A와 B 사이를 시간축으로 재생하는 전통적인 snapshot interpolation이라기보다 목표값을 추종하는 exponential smoothing에 가까워.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그래서&amp;nbsp;면접에서&amp;nbsp;너무&amp;nbsp;세게 &lt;br /&gt;&amp;ldquo;정확한 Time-based Snapshot Interpolation을 구현했습니다.&amp;rdquo;&lt;br /&gt;라고&amp;nbsp;말하기보다는, &lt;br /&gt;&amp;ldquo;수신 Transform을 목표값으로 두고 프레임 단위 보간을 적용했으며, 이동 속도에 따라 추종 속도를 동적으로 조정했습니다.&amp;rdquo;&lt;br /&gt;라고&amp;nbsp;말하는&amp;nbsp;게&amp;nbsp;정확해. &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;Q.&amp;nbsp;50Hz로&amp;nbsp;보내는데&amp;nbsp;왜&amp;nbsp;끊겨&amp;nbsp;보였나요? &lt;br /&gt;송신&amp;nbsp;주기가&amp;nbsp;일정하다고&amp;nbsp;수신&amp;nbsp;주기까지&amp;nbsp;일정한&amp;nbsp;것은&amp;nbsp;아니기&amp;nbsp;때문입니다. &lt;br /&gt;&lt;br /&gt;서버가&amp;nbsp;20ms마다&amp;nbsp;보내더라도&amp;nbsp;실제&amp;nbsp;클라이언트에서는: &lt;br /&gt;20ms &lt;br /&gt;21ms &lt;br /&gt;18ms &lt;br /&gt;45ms &lt;br /&gt;&amp;nbsp;5ms &lt;br /&gt;20ms &lt;br /&gt;&lt;br /&gt;처럼&amp;nbsp;패킷이&amp;nbsp;불규칙하게&amp;nbsp;도착할&amp;nbsp;수&amp;nbsp;있어. &lt;br /&gt;그걸&amp;nbsp;바로: &lt;br /&gt;transform.position&amp;nbsp;=&amp;nbsp;receivedPosition; &lt;br /&gt;&lt;br /&gt;으로&amp;nbsp;적용하면: &lt;br /&gt;현재&amp;nbsp;위치&amp;nbsp;--------&amp;gt;&amp;nbsp;다음&amp;nbsp;위치 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;순간&amp;nbsp;이동 &lt;br /&gt;&lt;br /&gt;처럼&amp;nbsp;보여. &lt;br /&gt;반면&amp;nbsp;보간하면: &lt;br /&gt;현재&amp;nbsp;──&amp;rarr;──&amp;rarr;──&amp;rarr;&amp;nbsp;목표&amp;nbsp;위치 &lt;br /&gt;&lt;br /&gt;형태로&amp;nbsp;렌더링하니까&amp;nbsp;시각적인&amp;nbsp;끊김을&amp;nbsp;줄일&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;거지. &lt;br /&gt;&lt;br /&gt;Q. 면접에서 &amp;ldquo;Snapshot Interpolation 구현했나요?&amp;rdquo;라고 물으면: &lt;br /&gt;&amp;ldquo;정확한&amp;nbsp;snapshot&amp;nbsp;buffer&amp;nbsp;기반&amp;nbsp;interpolation은&amp;nbsp;아니고,&amp;nbsp;최신&amp;nbsp;수신&amp;nbsp;Transform을&amp;nbsp;target으로&amp;nbsp;두고&amp;nbsp;Lerp/Slerp로&amp;nbsp;추종하는&amp;nbsp;smoothing&amp;nbsp;구조입니다.&amp;rdquo; &lt;br /&gt;&lt;br /&gt;Q. 왜 그렇게 했어요?&lt;br /&gt;&amp;ldquo;프로젝트&amp;nbsp;규모와&amp;nbsp;당시&amp;nbsp;요구사항에서는&amp;nbsp;Snapshot&amp;nbsp;Buffer까지&amp;nbsp;두는&amp;nbsp;방식보다&amp;nbsp;최신&amp;nbsp;Transform을&amp;nbsp;바로&amp;nbsp;목표값으로&amp;nbsp;삼아&amp;nbsp;smoothing하는&amp;nbsp;방식이&amp;nbsp;구현&amp;nbsp;복잡도와&amp;nbsp;체감&amp;nbsp;품질&amp;nbsp;사이에서&amp;nbsp;적절하다고&amp;nbsp;판단했습니다. &lt;br /&gt;차량&amp;nbsp;Transform을&amp;nbsp;50Hz로&amp;nbsp;비교적&amp;nbsp;자주&amp;nbsp;수신하고&amp;nbsp;있었고,&amp;nbsp;가장&amp;nbsp;큰&amp;nbsp;문제는&amp;nbsp;위치&amp;nbsp;정확도&amp;nbsp;자체보다&amp;nbsp;패킷&amp;nbsp;도착&amp;nbsp;간격의&amp;nbsp;불균형&amp;nbsp;때문에&amp;nbsp;발생하는&amp;nbsp;시각적&amp;nbsp;Jitter였습니다.&amp;nbsp;그래서&amp;nbsp;우선&amp;nbsp;Lerp/Slerp로&amp;nbsp;최신&amp;nbsp;값을&amp;nbsp;부드럽게&amp;nbsp;추종하고,&amp;nbsp;속도에&amp;nbsp;따라&amp;nbsp;smoothing&amp;nbsp;값을&amp;nbsp;조절하는&amp;nbsp;방식으로&amp;nbsp;해결했습니다. &lt;br /&gt;Snapshot&amp;nbsp;Interpolation은&amp;nbsp;여러&amp;nbsp;Snapshot을&amp;nbsp;버퍼링한&amp;nbsp;뒤&amp;nbsp;과거&amp;nbsp;시점을&amp;nbsp;재생하기&amp;nbsp;때문에&amp;nbsp;지터에는&amp;nbsp;더&amp;nbsp;안정적이지만,&amp;nbsp;의도적인&amp;nbsp;렌더링&amp;nbsp;지연과&amp;nbsp;버퍼&amp;nbsp;관리&amp;nbsp;복잡도가&amp;nbsp;추가됩니다.&amp;nbsp;당시에는&amp;nbsp;레이싱&amp;nbsp;게임에서&amp;nbsp;원격&amp;nbsp;차량의&amp;nbsp;반응성을&amp;nbsp;너무&amp;nbsp;늦추지&amp;nbsp;않는&amp;nbsp;것도&amp;nbsp;중요해서&amp;nbsp;이&amp;nbsp;방식을&amp;nbsp;선택했습니다.&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;핵심은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;세 가지 Trade-off&lt;/span&gt;&lt;/b&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;내 방식
Latest Transform + Lerp/Slerp

장점
&amp;rarr; 구현이 단순함
&amp;rarr; 추가 버퍼가 필요 없음
&amp;rarr; 최신 위치를 빠르게 추종
&amp;rarr; 렌더링 지연이 비교적 적음

단점
&amp;rarr; 네트워크 Jitter가 심하면 완전히 일정한 움직임을 보장하기 어려움
&amp;rarr; 최신 패킷 하나에 의존&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면 Snapshot Interpolation은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Snapshot A
Snapshot B
Snapshot C
        &amp;darr;

일부러 약간 과거 시점을 렌더링
        &amp;darr;

A ───── interpolation ───── B&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 여러 패킷을 확보한 뒤 그 사이를 보간하니까 더 안정적이지만, 예를 들어 100ms 정도 버퍼를 둔다면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;원격 플레이어가 실제보다 약 100ms 과거 상태로 보이는 비용&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 생길 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 꼬리질문으로:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;지금 다시 만든다면 똑같이 구현할 건가요?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 하면 이렇게 답하면 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;프로토타입이나 소규모 멀티플레이에서는 현재 구조도 충분히 선택할 수 있다고 생각합니다. 다만 실제 서비스 수준에서 네트워크 품질 변화까지 안정적으로 대응해야 한다면 Timestamp를 포함한 Snapshot을 일정 개수 버퍼링하고, 일정한 interpolation delay를 두어 두 Snapshot 사이를 시간 기준으로 보간하는 구조로 개선할 것 같습니다. 패킷이 잠시 끊기는 경우에는 짧은 구간에 한해 extrapolation을 적용하는 것도 검토할 수 있습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 답이 좋은 이유는 네가 현재 구현의 &lt;/span&gt;&lt;b&gt;&lt;span&gt;한계까지 정확하게 알고 있다는 것&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 보여주기 때문이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;면접용으로 한 줄만 외운다면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;50Hz로 최신 Transform을 충분히 자주 받고 있었기 때문에, 추가 지연과 복잡도가 있는 Snapshot Buffer 대신 최신 값을 동적으로 smoothing하는 방식으로 먼저 Jitter를 해결했고, 더 높은 네트워크 안정성이 필요하다면 Snapshot Interpolation으로 확장할 수 있다고 판단했습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;면접 꼬리질문 정리&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;왜 Input Relay에서 위치 오차가 누적됐나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;같은 입력을 전달해도 Unity Rigidbody 물리는 완전히 결정론적이지 않고, 클라이언트별 프레임/물리 실행 시점과 패킷 도착 상태도 다르기 때문에 각 클라이언트가 조금씩 다른 위치를 계산했습니다. 작은 차이가 계속 누적되면서 위치&amp;middot;회전 오차가 커졌습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;50Hz로 보내는데 왜 Jitter가 생겼나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;50Hz는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;송신 주기&lt;/span&gt;&lt;/b&gt;&lt;span&gt;일 뿐 실제 수신 간격이 항상 20ms라는 뜻은 아닙니다. 네트워크 latency와 jitter 때문에 패킷이 불규칙하게 도착할 수 있고, 수신 위치를 즉시 적용하면 그 불규칙성이 화면상 순간이동처럼 보일 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;Lerp/Slerp는 어떻게 적용했나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;수신한 최신 Position과 Rotation을 &lt;/span&gt;targetPosition&lt;span&gt;, &lt;/span&gt;targetRotation&lt;span&gt;으로 저장하고, 매 렌더링 프레임마다 현재 위치는 &lt;/span&gt;Vector3.Lerp&lt;span&gt;, 회전은 &lt;/span&gt;Quaternion.Slerp&lt;span&gt;로 목표값을 따라가도록 했습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;왜 Lerp와 Slerp를 구분했나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;Position은 &lt;/span&gt;Vector3&lt;span&gt;이므로 두 위치 사이를 선형 보간하는 &lt;/span&gt;Lerp&lt;span&gt;를 사용했고, Rotation은 &lt;/span&gt;Quaternion&lt;span&gt;이므로 회전 보간에 적합한 &lt;/span&gt;Slerp&lt;span&gt;를 사용했습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;Dynamic Smoothing은 왜 넣었나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;고정된 보간 속도에서는 고속 차량이 목표 위치를 너무 늦게 따라갈 수 있습니다. 연속 수신 위치의 거리와 도착 시간 차이로 속도를 추정하고, 빠를수록 smoothing 값을 높여 목표 Transform을 더 빠르게 추종하도록 했습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;오차가 너무 커지면 어떻게 했나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;일정 거리 이상 벌어지면 계속 Lerp하는 것보다 잘못된 상태가 오래 보일 수 있기 때문에 &lt;/span&gt;teleportThreshold&lt;span&gt;를 두고 임계치를 넘으면 목표 위치로 즉시 보정했습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;Snapshot Interpolation은 왜 안 썼나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;프로젝트 규모에서는 50Hz Transform 송신과 최신 Transform 기반 smoothing만으로도 문제를 해결할 수 있었고, Snapshot Buffer를 추가하면 구현 복잡도와 의도적인 렌더링 지연이 생깁니다. 다만 네트워크 품질 변화까지 더 안정적으로 대응해야 한다면 Timestamp 기반 Snapshot Buffer 방식으로 개선할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;그러면 이 방식은 Server Authoritative인가요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;아닙니다. 현재 구조는 클라이언트가 자기 Transform을 계산하고 서버가 이를 중계하므로 &lt;/span&gt;Client-authoritative Transform + Server Relay&lt;span&gt;에 가깝습니다. 서버가 직접 물리를 계산하는 완전한 Server Authoritative 구조는 아닙니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;그럼 치팅에 취약하지 않나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;맞습니다. 클라이언트가 거짓 위치를 보낼 가능성이 있습니다. 그래서 기존 Input을 함께 유지하면 서버에서 이전 위치, 시간, 입력을 비교해 최대 속도&amp;middot;가속도&amp;middot;회전량&amp;middot;비정상 순간이동 등을 검증하는 방식으로 확장할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;왜 Input 패킷도 남겨뒀나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;Transform만 있으면 &amp;ldquo;어디로 이동했는지&amp;rdquo;만 알 수 있지만 Input까지 있으면 &amp;ldquo;어떤 입력으로 그 결과가 나왔는지&amp;rdquo;를 비교할 수 있습니다. 그래서 비정상 이동 검증과 물리&amp;middot;네트워크 디버깅에 활용할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.9069%;&quot;&gt;&lt;b&gt;&lt;span&gt;리플레이에는 어떻게 활용하나요?&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 71.9769%;&quot;&gt;&lt;span&gt;Unity 물리가 비결정적이라 Input만 재생하면 동일 결과가 보장되지 않습니다. 그래서 Timestamp와 Position/Rotation Snapshot을 저장하고, 재생할 때 Snapshot 사이를 Lerp/Slerp하는 방식이 더 안정적입니다. Input은 추가 분석 데이터로 함께 저장할 수 있습니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 마지막 두 개는 이렇게 기억하면 편해.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Input
= &quot;왜 이렇게 움직였나?&quot;

Transform
= &quot;실제로 어디로 움직였나?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;둘을 같이 가지고 있으면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Input + Transform
      &amp;darr;
정상 움직임인가?
      &amp;darr;
치팅 검증 / 디버깅&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 가능해지는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 면접에서 **&amp;ldquo;Transform Relay라는 표현이 정확합니까?&amp;rdquo;**라고 집요하게 물으면 이렇게 답하면 가장 깔끔해.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;Unity의 Transform 전체를 직렬화한다는 의미보다는, Transform을 구성하는 상태 중 멀티플레이에 필요한 Position과 Rotation을 서버를 통해 중계한다는 의미로 사용했습니다. Scale은 동기화 대상에 포함하지 않았습니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;면접에서는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;Quaternion을 쓰는 이유 &amp;rarr; Euler Angle의 한계 &amp;rarr; Gimbal Lock &amp;rarr; 내 프로젝트에서 왜 Slerp를 썼는지&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 순서로 답하면 깔끔해.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Q. 왜 회전에 Quaternion을 사용했나요?&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;3D 회전을 Euler Angle로 표현하면 각 축의 회전을 순차적으로 적용하는 과정에서 특정 자세에서 두 회전축이 겹치는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Gimbal Lock&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 문제가 발생할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Quaternion은 3차원 회전을 4개의 값 &lt;/span&gt;(x, y, z, w)&lt;span&gt;으로 표현하여 Euler Angle의 이런 특이점을 피할 수 있고, 두 회전 사이를 부드럽게 보간하기에도 적합합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;제 프로젝트에서는 네트워크로 받은 원격 차량의 회전을 부드럽게 적용해야 했기 때문에 &lt;/span&gt;Quaternion.Slerp()&lt;span&gt;를 사용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Unity에서도 &lt;/span&gt;Transform.rotation&lt;span&gt; 자체가 &lt;/span&gt;Quaternion&lt;span&gt;이기 때문에 자연스러운 선택이기도 해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Q. Gimbal Lock이 뭔가요?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;쉽게 말하면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Euler Angle로 3차원 회전을 표현할 때 특정 회전 상태에서 두 회전축이 겹쳐 하나의 회전 자유도를 잃는 현상&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이야.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 Euler Angle을:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;X = Pitch
Y = Yaw
Z = Roll&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이라고 하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;원래는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;X축 회전
Y축 회전
Z축 회전&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;세 개가 독립적이어야 하는데, 특정 축을 90도로 회전하면 두 축이 같은 방향으로 정렬되는 상황이 발생할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개념적으로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;정상 상태

X ─&amp;rarr;
Y &amp;uarr;
Z ↗

각 축이 서로 다른 방향&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이었던 게 특정 회전 이후:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;X ─&amp;rarr;
Y &amp;uarr;
Z ─&amp;rarr;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 두 축의 회전 효과가 겹쳐버리는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 원래 &lt;/span&gt;&lt;b&gt;&lt;span&gt;3 Degrees of Freedom&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이던 것이 실질적으로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;2 Degrees of Freedom&lt;/span&gt;&lt;/b&gt;&lt;span&gt;처럼 되어 특정 방향의 회전을 독립적으로 표현하기 어려워져.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이게 Gimbal Lock.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Q. Quaternion은 왜 Gimbal Lock을 피할 수 있나요?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 면접에서 이렇게 말하면 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Euler Angle은 X/Y/Z 축 회전을 순서대로 적용해서 방향을 표현하지만, Quaternion은 회전 자체를 하나의 4차원 표현으로 나타내기 때문에 Euler Angle처럼 축이 겹치는 특이점에 의존하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;중요한 건:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;Quaternion은 축 4개를 사용해서 Gimbal Lock이 없다&amp;rdquo;**라고 설명하면 안 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;x, y, z, w&lt;span&gt;는 단순히 4개의 회전축이 아니야. Quaternion은 수학적으로 회전을 표현하는 다른 방식이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 Quaternion을 쓰더라도 중간에 계속:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;transform.eulerAngles&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 변환해서 Euler 기반 연산을 해버리면 Euler 표현의 문제를 다시 만날 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Q. &quot;Unity에서도 Euler Angle을 사용하면 무조건 Gimbal Lock이 발생하나요?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 물으면&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;Unity의 Transform 회전 자체는 내부적으로 Quaternion으로 관리됩니다. 다만 Euler Angle을 이용해 축별 회전을 누적하거나 특정 회전 순서에 의존하는 로직을 작성하면 Euler 표현의 특이점인 Gimbal Lock 문제를 만날 수 있습니다. 그래서 연속적인 3D 회전이나 회전 보간에는 Quaternion을 사용하는 것이 안전합니다.&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Q. 그런데 왜 &lt;/span&gt;Lerp&lt;span&gt;가 아니라 Rotation에는 &lt;/span&gt;Slerp&lt;span&gt;를 사용했나요?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;네 프로젝트하고 연결하면 이 답이 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위치는 &lt;/span&gt;Vector3&lt;span&gt;이므로 &lt;/span&gt;Vector3.Lerp()&lt;span&gt;를 이용해 두 점 사이를 보간했습니다. 반면 회전은 Quaternion으로 표현되어 있기 때문에 &lt;/span&gt;Quaternion.Slerp()&lt;span&gt;를 사용했습니다. Slerp는 두 Quaternion 사이를 구면 선형 보간하여 회전 변화가 자연스럽게 이어지도록 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;// 위치
transform.position =
    Vector3.Lerp(
        transform.position,
        targetPosition,
        t);

// 회전
transform.rotation =
    Quaternion.Slerp(
        transform.rotation,
        targetRotation,
        t);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 핵심은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;Position
Vector3
&amp;rarr; Lerp

Rotation
Quaternion
&amp;rarr; Slerp&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;Q. Euler Angle은 그러면 쓰면 안 되나요?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;아니야. 이것도 자주 나오는 함정 질문이야.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Euler Angle은 사람이 &lt;/span&gt;30&amp;deg;, 90&amp;deg;, 0&amp;deg;&lt;span&gt;처럼 이해하고 편집하기 쉬워서 UI나 Inspector, 특정 축의 각도 제어 등에 유용합니다. 다만 실제 3D 회전 누적이나 회전 보간에서는 Quaternion을 사용하는 것이 더 적합합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 사람이 이해하기 편함
Vector3 euler = new Vector3(0, 90, 0);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 내부 회전 적용은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;Quaternion rotation =
    Quaternion.Euler(0, 90, 0);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;식으로 하는 거지.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;네 프로젝트 기준 30초 답변&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이거 하나 외워도 충분해.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;원격 차량의 회전은 Unity의 &lt;/span&gt;Transform.rotation&lt;span&gt;과 동일하게 Quaternion으로 처리했습니다. Euler Angle은 축별 회전을 순차적으로 표현하기 때문에 특정 자세에서 두 축이 겹쳐 자유도 하나를 잃는 Gimbal Lock이 발생할 수 있습니다. Quaternion은 이런 Euler 표현의 특이점을 피하면서 3D 회전을 안정적으로 표현할 수 있고, &lt;/span&gt;Quaternion.Slerp()&lt;span&gt;를 이용해 네트워크로 수신한 현재 회전과 목표 회전 사이를 자연스럽게 보간할 수 있어서 사용했습니다.&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Q. 쿼터니언의 (x,y,z,w) 가 정확히 뭐죠?&lt;br /&gt;&lt;br /&gt;즉 개념적으로는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;(x, y, z)
&amp;rarr; 회전축 방향과 관련된 벡터 부분

w
&amp;rarr; 회전각과 관련된 스칼라 부분&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;Quaternion의 x, y, z는 회전축에 &lt;/span&gt;sin(&amp;theta;/2)&lt;span&gt;를 곱한 벡터 성분이고, w는 &lt;/span&gt;cos(&amp;theta;/2)&lt;span&gt;에 해당하는 스칼라 성분입니다. 따라서 네 값은 각각 독립적인 회전각이 아니라 하나의 3차원 회전을 함께 표현하는 값입니다.&amp;rdquo;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 면접에서 정말 짧게 말하면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;Quaternion은 회전축과 회전각을 4개의 값으로 변환해 표현한 방식이고, xyz는 축과 관련된 벡터 성분, w는 각도와 관련된 스칼라 성분입니다.&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2. &lt;/span&gt;PlayerInput &amp;rarr; 0.9 Bytes(7bits)&lt;span&gt;는 표현을 바꾸는 게 좋음&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7bit / 8 = 0.875 byte&lt;span&gt;라고 수학적으로 말할 수는 있지만, 실제 네트워크 버퍼는 일반적으로 &lt;/span&gt;&lt;b&gt;&lt;span&gt;byte 단위&lt;/span&gt;&lt;/b&gt;&lt;span&gt;로 다뤄.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 7bit를 사용하더라도 실제 byte 배열에는 최소:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;1 byte&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 필요해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 0.9 Bytes&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;보다는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;유효 정보 7bit, 최소 1byte에 패킹&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;게다가 서버가 다른 클라이언트에게 브로드캐스트할 때는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;누구의 Transform인지 식별할 ClientId도 필요&lt;/span&gt;&lt;/b&gt;&lt;span&gt;해. 실제 클라이언트 수신 코드도 TransformUpdate에서 &lt;/span&gt;clientId&lt;span&gt;를 읽고 Position 3개와 Quaternion 4개를 읽고 있어. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 붙여넣은 텍스트(1)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 포트폴리오에서는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Position 3개 + Quaternion 4개로 Transform 데이터 자체는 28byte이며, PacketType과 ClientId 등의 프로토콜 필드를 포함하면 실제 애플리케이션 payload는 그보다 커집니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 쓰는 게 안전하고 정확해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;클라이언트 &lt;/span&gt;OnNetworkReceive()&lt;span&gt;에서 매번:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;byte[] data = new byte[length];
Array.Copy(...);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 하고 있어. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 붙여넣은 텍스트(1)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;서버도 마찬가지로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;byte[] data = new byte[length];
Array.Copy(...);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 수행하고 있어. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; NetworkManager&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 &lt;/span&gt;&lt;b&gt;&lt;span&gt;패킷마다 managed &lt;/span&gt;byte[]&lt;span&gt; 할당이 발생할 수 있으므로 Zero-GC 구조는 아니야.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;LiteNetLib와 함께 활용해 &lt;/span&gt;&lt;b&gt;&lt;span&gt;GC 부하 발생 없이&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;는 삭제해야 해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;대신:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;JSON 문자열 생성&amp;middot;파싱 및 객체 직렬화에 필요한 할당을 줄이고, 필요한 데이터만 binary payload로 직렬화하여 GC 부담을 낮추는 방향으로 구성했습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;5. &lt;/span&gt;서버 CPU 부하와 병목이 감소했다&lt;span&gt;도 측정 여부가 중요함&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;JSON은 보통:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;숫자 &amp;rarr; 문자열 변환
필드 이름 포함
문자열 parsing
JSON tokenizer/parser
객체 생성&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;등의 비용이 들어가고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Binary protocol은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;정해진 위치에서
정해진 bit/byte만 읽기&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;때문에 훨씬 단순할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 실제로:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;CPU 병목이 감소했다&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 결과로 쓰려면 CPU profiling 데이터가 있는 게 가장 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;없다면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;문자열 파싱과 불필요한 객체 생성을 줄여 직렬화/역직렬화 비용을 낮출 수 있도록 구성했습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 써.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;설계상 기대효과&lt;/span&gt;&lt;/b&gt;&lt;span&gt;와 &lt;/span&gt;&lt;b&gt;&lt;span&gt;측정된 성과&lt;/span&gt;&lt;/b&gt;&lt;span&gt;는 구분하는 게 포트폴리오에서 굉장히 중요해.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BitWriter / BitReader&lt;span&gt; 기반의 커스텀 binary protocol을 통해 게임에서 필요한 상태만 정해진 bit 수로 직렬화했습니다. 예를 들어 방향 입력은 Horizontal의 좌/중립/우 상태를 2bit, Vertical의 입력 여부를 1bit로 표현하여 총 3bit로 양자화했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 통해 두 &lt;/span&gt;float&lt;span&gt; 입력값을 그대로 전송하는 대신 필요한 의미 정보만 하나의 byte에 패킹했으며, JSON 방식에서 발생하던 필드명&amp;middot;문자열 표현 등의 payload 오버헤드와 문자열 parsing/객체 생성 비용을 줄였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Position/Rotation과 같이 정밀도가 필요한 값은 &lt;/span&gt;float&lt;span&gt; 기반 binary 데이터로 유지하고, PacketType이나 Input/Effect 등 표현 가능한 상태 수가 제한적인 정보에 대해서만 bit packing을 적용하여 &lt;/span&gt;&lt;b&gt;&lt;span&gt;정확성과 전송 효율 사이의 균형&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 맞췄습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;성과 부분은:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;입력 데이터의 경우 두 float 기준 64bit의 정보를 그대로 전송하는 대신 실제 필요한 상태를 3bit로 표현했으며, PacketType 등의 프로토콜 정보를 포함한 소형 binary packet으로 구성했습니다. Transform은 Position 3개와 Quaternion 4개를 7개의 float로 전송하여 JSON의 필드명과 문자열 표현 오버헤드를 제거했습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;전체 wire traffic은 IP/UDP/LiteNetLib 헤더의 영향을 받으므로 payload 감소율과 동일하지 않지만, 고빈도로 송수신되는 실시간 데이터의 애플리케이션 payload를 줄이고 JSON parsing 및 불필요한 할당 비용을 감소시키는 방향으로 최적화했습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 서버의 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전체 아키텍처를 한 문장으로 정리하면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;세션과 게임 진행은 서버 중심으로 관리하되, 고비용인 차량 물리 시뮬레이션은 클라이언트에서 수행하고 Transform/Input/Effect를 서버가 세션 단위로 중계하도록 구성한 경량 멀티플레이 서버입니다.&amp;rdquo;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;서버에서 순위 계산은 일원화했지만 점수 생성 자체까지 완전한 server-authoritative 구조로 만들지는 않았습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span&gt;4. 이 서버가 경량화된 이유도 최신 코드에서 확인 가능함&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;서버 자체에는 Unity Rigidbody, Physics Simulation, 렌더링 같은 게 없어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Main loop도:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;while (true)
{
    server.PollEvents();
    Thread.Sleep(15);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;형태야. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; NetworkManager&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 서버의 고빈도 작업은 대부분:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;패킷 수신
&amp;rarr; PacketType 해석
&amp;rarr; Dictionary lookup
&amp;rarr; GameSession lookup
&amp;rarr; Event
&amp;rarr; 다른 Peer에게 패킷 전송&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정도야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;차량 물리를 서버에서 50Hz로 8대씩 직접 계산하는 것보다 훨씬 가벼운 구조라고 볼 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;&lt;span&gt;6. 서버 사양/비용은 실제로 어떻게 잡나?&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;공식만으로 결정하지 않고 &lt;/span&gt;&lt;b&gt;&lt;span&gt;계산으로 대략 추정한 다음 Load Test로 결정&lt;/span&gt;&lt;/b&gt;&lt;span&gt;해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개념식은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Ingress msgs/sec
= CCU &amp;times; 클라이언트당 송신률

Broadcast Egress msgs/sec
= Session 수
&amp;times; Player 수
&amp;times; (Player 수 - 1)
&amp;times; Broadcast Hz

Bandwidth
&amp;asymp; msgs/sec &amp;times; 평균 wire packet size&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그다음 Bot client를 만들어:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;50 CCU
100 CCU
200 CCU
500 CCU
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 올리면서 실제 서버의:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 사용률 / RAM / GC / Network Mbps / PPS / packet loss / p95&amp;middot;p99 latency&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 측정해서 한 서버당 안전한 CCU를 결정해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 포트폴리오에서는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;서버 비용을 줄이기 위해 Relay 방식을 사용하여 CPU 연산량을 낮췄다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;는 충분히 합리적인 설명이고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;더 정확하게는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;서버에서 차량 물리를 직접 계산하지 않아 CPU 비용을 줄이는 대신, 세션 단위 Broadcast로 인한 네트워크 Egress가 주요 확장 비용이 되는 구조를 선택했다.&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 말하면 꽤 깊이 이해하고 있는 답이 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;최신 코드에서 추가로 눈에 띄는 부분 두 개&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;서버 구조 자체는 이전 파일보다 훨씬 완성되어 있어. 다만 면접 전에 알아두면 좋은 게 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Matchmaker&lt;span&gt;의 &lt;/span&gt;System.Timers.Timer&lt;span&gt;는 별도 ThreadPool thread에서 실행될 수 있는데, &lt;/span&gt;waitingPlayers&lt;span&gt;와 &lt;/span&gt;sessions&lt;span&gt;는 일반 &lt;/span&gt;List&lt;span&gt;/&lt;/span&gt;Dictionary&lt;span&gt;야. 네트워크의 &lt;/span&gt;AddPlayer/RemovePlayer&lt;span&gt;와 Timer의 &lt;/span&gt;TryStartMatch()&lt;span&gt;가 동시에 접근하면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Race Condition 가능성&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 검토해야 해. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또 &lt;/span&gt;GameSession&lt;span&gt;의:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;gamePlayTime = 180000; // 5분초&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;는 실제로는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;180초 = 3분&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이야. 5분을 의도했다면 &lt;/span&gt;300000&lt;span&gt;ms가 맞아. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; GameSession&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;둘 다 아키텍처 문제라기보다는 코드 리뷰에서 잡힐 수 있는 디테일이라 미리 알고 있으면 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최신 코드 기준으로는 이제 &lt;/span&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;4bit 프로토콜 불일치&amp;rdquo; 문제는 없고, 서버 구조는 &lt;/span&gt;Session-authoritative + Client-authoritative movement relay&lt;span&gt;라고 설명하는 게 가장 정확하다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 &lt;/span&gt;&lt;span&gt;네 &lt;/span&gt;&lt;span&gt;최적화는 &lt;/span&gt;&lt;span&gt;이런 &lt;/span&gt;&lt;span&gt;구조라고 &lt;/span&gt;&lt;span&gt;보면 &lt;/span&gt;&lt;span&gt;돼.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;전송 빈도 최적화
Input       20Hz
Transform   50Hz
       &amp;darr;
PPS 결정


Payload 최적화
JSON &amp;rarr; Binary / Bit Packing
       &amp;darr;
packet당 byte 감소
       &amp;darr;
Bandwidth 감소&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 &lt;/span&gt;&lt;span&gt;면접에서:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;&lt;/span&gt;&lt;span&gt;작은 &lt;/span&gt;&lt;span&gt;패킷이면 &lt;/span&gt;&lt;span&gt;PPS&lt;/span&gt;&lt;span&gt;가 &lt;/span&gt;&lt;span&gt;높아져서 &lt;/span&gt;&lt;span&gt;오히려 &lt;/span&gt;&lt;span&gt;안 &lt;/span&gt;&lt;span&gt;좋은 &lt;/span&gt;&lt;span&gt;것 &lt;/span&gt;&lt;span&gt;아닌가요?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 &lt;/span&gt;&lt;span&gt;하면 &lt;/span&gt;&lt;span&gt;이렇게 &lt;/span&gt;&lt;span&gt;답하면 &lt;/span&gt;&lt;span&gt;좋아.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;&lt;/span&gt;&lt;span&gt;패킷 &lt;/span&gt;&lt;span&gt;크기 &lt;/span&gt;&lt;span&gt;감소 &lt;/span&gt;&lt;span&gt;자체가 &lt;/span&gt;&lt;span&gt;PPS&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;증가시키는 &lt;/span&gt;&lt;span&gt;것은 &lt;/span&gt;&lt;span&gt;아닙니다. &lt;/span&gt;&lt;span&gt;PPS&lt;/span&gt;&lt;span&gt;는 &lt;/span&gt;&lt;span&gt;전송 &lt;/span&gt;&lt;span&gt;빈도에 &lt;/span&gt;&lt;span&gt;의해 &lt;/span&gt;&lt;span&gt;결정됩니다. &lt;/span&gt;&lt;span&gt;제 &lt;/span&gt;&lt;span&gt;프로젝트에서는 &lt;/span&gt;&lt;span&gt;Input &lt;/span&gt;&lt;span&gt;20Hz, &lt;/span&gt;&lt;span&gt;Transform &lt;/span&gt;&lt;span&gt;50Hz&lt;/span&gt;&lt;span&gt;로 &lt;/span&gt;&lt;span&gt;전송 &lt;/span&gt;&lt;span&gt;빈도를 &lt;/span&gt;&lt;span&gt;별도로 &lt;/span&gt;&lt;span&gt;제한했고, &lt;/span&gt;&lt;span&gt;그 &lt;/span&gt;&lt;span&gt;주기 &lt;/span&gt;&lt;span&gt;안에서 &lt;/span&gt;&lt;span&gt;payload&lt;/span&gt;&lt;span&gt;를 &lt;/span&gt;&lt;span&gt;bit &lt;/span&gt;&lt;span&gt;packing&lt;/span&gt;&lt;span&gt;하여 &lt;/span&gt;&lt;span&gt;packet&lt;/span&gt;&lt;span&gt;당 &lt;/span&gt;&lt;span&gt;전송량을 &lt;/span&gt;&lt;span&gt;줄였습니다. &lt;/span&gt;&lt;span&gt;다만 &lt;/span&gt;&lt;span&gt;지나치게 &lt;/span&gt;&lt;span&gt;작은 &lt;/span&gt;&lt;span&gt;패킷을 &lt;/span&gt;&lt;span&gt;높은 &lt;/span&gt;&lt;span&gt;빈도로 &lt;/span&gt;&lt;span&gt;보내면 &lt;/span&gt;&lt;span&gt;고정 &lt;/span&gt;&lt;span&gt;헤더와 &lt;/span&gt;&lt;span&gt;packet &lt;/span&gt;&lt;span&gt;processing &lt;/span&gt;&lt;span&gt;비용 &lt;/span&gt;&lt;span&gt;때문에 &lt;/span&gt;&lt;span&gt;비효율적일 &lt;/span&gt;&lt;span&gt;수 &lt;/span&gt;&lt;span&gt;있기 &lt;/span&gt;&lt;span&gt;때문에, &lt;/span&gt;&lt;span&gt;payload &lt;/span&gt;&lt;span&gt;크기뿐 &lt;/span&gt;&lt;span&gt;아니라 &lt;/span&gt;&lt;span&gt;전송 &lt;/span&gt;&lt;span&gt;주기와 &lt;/span&gt;&lt;span&gt;PPS&lt;/span&gt;&lt;span&gt;도 &lt;/span&gt;&lt;span&gt;같이 &lt;/span&gt;&lt;span&gt;고려해야 &lt;/span&gt;&lt;span&gt;합니다.&amp;rdquo;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;System.Timers.Timer&lt;span&gt; 때문에 Race Condition이 생길 수 있다는 건 무슨 뜻인가?&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 최신 서버에서 실제로 신경 쓸 만한 부분이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 서버의 기본 네트워크 처리는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;while (true)
{
    server.PollEvents();
    Thread.Sleep(15);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;에서 실행돼. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; NetworkManager&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 보통:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Server Main Thread
        &amp;darr;
PollEvents()
        &amp;darr;
OnNetworkReceive()
        &amp;darr;
Matchmaker.AddPlayer()
또는
Matchmaker.RemovePlayer()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;흐름이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 &lt;/span&gt;Matchmaker&lt;span&gt;에서는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;matchmakingTimer =
    new System.Timers.Timer(maxWaitTime * 1000);

matchmakingTimer.Elapsed += (sender, e) =&amp;gt;
{
    ...
    TryStartMatch(force: true);
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 사용하고 있어. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;System.Timers.Timer.Elapsed&lt;span&gt;는 네 서버의 &lt;/span&gt;PollEvents()&lt;span&gt; 루프와는 별도의 스레드에서 실행될 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 이런 일이 가능해져.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Thread A
NetworkManager / PollEvents
        &amp;darr;
AddPlayer()

동시에

Thread B
Timer.Elapsed
        &amp;darr;
TryStartMatch()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 둘 다 같은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;private readonly List&amp;lt;Player&amp;gt; waitingPlayers;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 건드려. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 현재 대기 인원이 7명이라고 해보자.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;waitingPlayers
[A B C D E F G]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그 순간 두 스레드가 동시에 실행돼.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Network Thread

AddPlayer(H)
    &amp;darr;
waitingPlayers.Add(H)


Timer Thread

TryStartMatch(true)
    &amp;darr;
waitingPlayers.Take(...)
    &amp;darr;
RemoveRange(...)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;List&amp;lt;T&amp;gt;&lt;span&gt;는 기본적으로 thread-safe가 아니기 때문에, 한쪽에서 Add하는 동안 다른 쪽에서 &lt;/span&gt;Take&lt;span&gt;, &lt;/span&gt;Count&lt;span&gt;, &lt;/span&gt;RemoveRange&lt;span&gt; 같은 작업을 하면 데이터 상태가 일관되지 않을 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;더 위험한 부분도 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TryStartMatch()&lt;span&gt;는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;var matchPlayers =
    waitingPlayers.Take(MatchSize).ToList();

waitingPlayers.RemoveRange(
    0,
    matchPlayers.Count);

nextClientId = 0;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;까지 해. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;동시에 &lt;/span&gt;AddPlayer()&lt;span&gt;는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ushort assingedId = nextClientId++;

...

waitingPlayers.Add(player);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 하고 있어. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 타이밍이 나쁘면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Network Thread
nextClientId++


Timer Thread
nextClientId = 0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 겹칠 수도 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 원래:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;0 1 2 3 4 ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;식으로 부여하려던 ID 상태가 예상과 다르게 될 가능성도 생겨.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 더 중요한 부분은 Timer thread의 &lt;/span&gt;TryStartMatch()&lt;span&gt;가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;NetworkManager.Instance.RegisterSession(session);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;까지 호출한다는 거야. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그 &lt;/span&gt;RegisterSession()&lt;span&gt;은 다시:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;playerToSession[player] = session;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;으로 &lt;/span&gt;NetworkManager&lt;span&gt;의 Dictionary를 수정해. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; NetworkManager&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 동시에 Network thread에서는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;playerToSession.TryGetValue(...)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 하고 있을 수 있지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 상황이:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Thread A
Dictionary 읽는 중

Thread B
Dictionary 수정 중&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 될 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일반 &lt;/span&gt;Dictionary&amp;lt;TKey,TValue&amp;gt;&lt;span&gt;도 여러 스레드의 동시 읽기/쓰기 용도로 설계된 자료구조가 아니야.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;그럼 &lt;/span&gt;lock&lt;span&gt;을 걸면 되나?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가능해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;private readonly object _sync = new();

public void AddPlayer(Player player, ...)
{
    lock (_sync)
    {
        waitingPlayers.Add(player);
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;private void TryStartMatch(...)
{
    lock (_sync)
    {
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 &lt;/span&gt;&lt;b&gt;&lt;span&gt;이 프로젝트에서는 더 좋은 방법이 있어.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;서버가 이미:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;while (true)
{
    server.PollEvents();
    Thread.Sleep(15);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라는 하나의 main loop를 가지고 있잖아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 굳이 &lt;/span&gt;System.Timers.Timer&lt;span&gt;로 새로운 thread를 만들지 않고 &lt;/span&gt;&lt;b&gt;&lt;span&gt;매치 타이머도 같은 서버 thread에서 체크하는 것&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 훨씬 단순해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 개념적으로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;while (true)
{
    server.PollEvents();

    matchmaker.Update();

    Thread.Sleep(15);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public void Update()
{
    if (!isMatchmakingTimerRunning)
        return;

    if (DateTime.UtcNow &amp;gt;= matchmakingDeadline)
    {
        TryStartMatch(force: true);
        isMatchmakingTimerRunning = false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 만드는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Network Receive
AddPlayer()
RemovePlayer()
TryStartMatch()
RegisterSession()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전부:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Server Main Thread 하나&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;에서 순서대로 실행돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;멀티스레드

Thread A &amp;rarr; AddPlayer
Thread B &amp;rarr; TryStartMatch
              &amp;darr;
          Race 가능&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Single Thread

AddPlayer
   &amp;darr;
끝
   &amp;darr;
Update
   &amp;darr;
TryStartMatch&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 바꾸는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;굳이 &lt;/span&gt;ConcurrentDictionary&lt;span&gt;, &lt;/span&gt;lock&lt;span&gt;, 복잡한 synchronization을 넣기보다,&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;게임/세션 상태를 하나의 서버 스레드에서만 수정하도록 설계한다&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;당시에는 매치메이킹 timeout을 &lt;/span&gt;System.Timers.Timer&lt;span&gt;로 구현했는데, Timer callback은 네트워크 PollEvents 루프와 다른 스레드에서 실행될 수 있습니다. 이 때문에 일반 List와 Dictionary에 AddPlayer와 TryStartMatch가 동시에 접근할 경우 Race Condition 가능성이 있습니다. 현재 다시 설계한다면 lock을 추가하는 방법도 있지만, 이 서버는 이벤트 루프 기반이므로 매치메이킹 timeout도 서버 main loop에서 체크하여 모든 세션 상태 변경을 단일 스레드로 직렬화하는 방식을 선택할 것 같습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NetworkManager&lt;span&gt;의 &lt;/span&gt;PollEvents()&lt;span&gt; 루프에서 &lt;/span&gt;AddPlayer()&lt;span&gt; 같은 네트워크 이벤트 처리가 실행되고, &lt;/span&gt;System.Timers.Timer.Elapsed&lt;span&gt;는 그 루프와는 별개의 스레드에서 실행될 수 있어. 그런데 두 경로가 모두 &lt;/span&gt;Matchmaker&lt;span&gt;의 &lt;/span&gt;waitingPlayers&lt;span&gt;와 &lt;/span&gt;nextClientId&lt;span&gt;, 그리고 이후 &lt;/span&gt;NetworkManager&lt;span&gt;의 &lt;/span&gt;playerToSession&lt;span&gt; 같은 공유 상태를 건드릴 수 있기 때문에 race condition 가능성이 생기는 거야. 실제로 &lt;/span&gt;AddPlayer()&lt;span&gt;는 &lt;/span&gt;nextClientId++&lt;span&gt;와 &lt;/span&gt;waitingPlayers.Add(player)&lt;span&gt;를 수행하고, &lt;/span&gt;TryStartMatch()&lt;span&gt;는 같은 리스트를 읽고 &lt;/span&gt;RemoveRange()&lt;span&gt;를 수행하면서 &lt;/span&gt;nextClientId = 0&lt;span&gt;으로 초기화해. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 &lt;/span&gt;TryStartMatch()&lt;span&gt;는 여기서 끝나는 게 아니라 &lt;/span&gt;NetworkManager.Instance.RegisterSession(session)&lt;span&gt;까지 호출하고, 그 안에서 &lt;/span&gt;playerToSession[player] = session&lt;span&gt;으로 또 다른 공유 Dictionary를 수정해. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; Matchmaker&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; NetworkManager&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 네 말대로 핵심은:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 개의 스레드가 같은 공유 자원을 동시에 읽거나 수정할 수 있는 구조라서, 실행 순서에 따라 예상하지 못한 상태가 나올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;한 가지 표현만 조심하면 돼.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;ldquo;두 스레드가 같은 자원을 동시에 수정하는 상황이 계속 발생하고 있다&amp;rdquo;보다는,&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;ldquo;동시에 수정할 가능성이 존재한다&amp;rdquo;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 더 정확해. 실제로 매번 충돌하는 건 아니고, 타이밍이 겹칠 때 문제가 발생할 수 있기 때문이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 해결책도 네가 말한 게 맞아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 서버는 이미:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;while (true)
{
    server.PollEvents();
    Thread.Sleep(15);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라는 단일 이벤트 루프 구조를 가지고 있어. &lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; NetworkManager&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 &lt;/span&gt;System.Timers.Timer&lt;span&gt;를 따로 돌리지 않고:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;while (true)
{
    server.PollEvents();

    matchmaker.Tick();

    Thread.Sleep(15);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 메인 서버 루프에서 매치메이킹 timeout을 확인하면,&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;AddPlayer
RemovePlayer
TryStartMatch
RegisterSession&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 전부 같은 스레드에서 순차적으로 실행되게 만들 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private DateTime matchmakingDeadline;
private bool isMatchmakingTimerRunning;

public void AddPlayer(Player player, ...)
{
    waitingPlayers.Add(player);

    if (!isMatchmakingTimerRunning)
    {
        isMatchmakingTimerRunning = true;
        matchmakingDeadline = DateTime.UtcNow.AddSeconds(20);
    }

    TryStartMatch();
}

public void Tick()
{
    if (!isMatchmakingTimerRunning)
        return;

    if (DateTime.UtcNow &amp;gt;= matchmakingDeadline)
    {
        TryStartMatch(force: true);
        isMatchmakingTimerRunning = false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이런 식이지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 방식의 장점은 단순해.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;lock 필요성 감소
Thread-safe collection 필요성 감소
race condition 가능성 감소
상태 흐름 추적이 쉬움
디버깅 쉬움&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;물론 멀티스레드 서버가 필요해지는 규모에서는 &lt;/span&gt;lock&lt;span&gt;, &lt;/span&gt;ConcurrentDictionary&lt;span&gt;, job queue 같은 방법을 써야 할 수 있어. 하지만 네 현재 서버처럼 비교적 단순한 세션/릴레이 구조에서는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;공유 게임 상태를 하나의 서버 메인 스레드에서만 수정하도록 만드는 방식이 훨씬 깔끔한 설계&lt;/span&gt;&lt;/b&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;면접에서는 이렇게 답하면 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;현재 구현에서는 네트워크 이벤트 루프와 &lt;/span&gt;System.Timers.Timer.Elapsed&lt;span&gt;가 서로 다른 스레드에서 실행될 수 있고, 둘 다 &lt;/span&gt;waitingPlayers&lt;span&gt;, &lt;/span&gt;nextClientId&lt;span&gt;, &lt;/span&gt;playerToSession&lt;span&gt; 같은 공유 상태를 수정하기 때문에 race condition 가능성이 있습니다. 이를 개선한다면 별도 Timer thread를 두기보다 서버 main loop에서 matchmaking timeout을 검사하여 모든 세션 상태 변경을 단일 스레드에서 직렬화하는 방식으로 구성하겠습니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/43</guid>
      <comments>https://letscoding0103.tistory.com/43#entry43comment</comments>
      <pubDate>Thu, 15 Oct 2026 03:28:28 +0900</pubDate>
    </item>
    <item>
      <title>코드 설계 지양 점</title>
      <link>https://letscoding0103.tistory.com/42</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지양해야 하는 것은 싱글톤, mutable state, for/if 자체가 아니라, 전역 상태에 대한 과도한 의존, 분산된 상태 변경, 조건문 남발, 그리고 실제 완료 조건 대신 임의의 타이머에 의존하는 설계다. + Lock.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt; Lock 없으면 Race Condition 가능, Lock 잘못 잡으면 Deadlock 가능&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;싱글톤 / 글로벌 변수&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;OCP를 &amp;ldquo;단번에 깨버린다&amp;rdquo;는 표현은 정확하지 않아.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;다만 전역 접근에 강하게 의존하면 결합도가 높아지고 테스트/교체/확장이 어려워져서 OCP, DIP 같은 설계 원칙을 해치기 쉬워.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;특히 &lt;/span&gt;GameManager.Instance.Xxx()&lt;span&gt; 같은 호출이 코드 전역에 퍼지면 수정 영향 범위가 커져.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Mutable 변수 + for/if&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;이것도 자체로 나쁜 게 아니야. 게임 코드는 상태 변화가 많아서 mutable state, &lt;/span&gt;for&lt;span&gt;, &lt;/span&gt;if&lt;span&gt;는 매우 흔해.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;문제는 같은 상태를 여러 곳에서 제멋대로 바꾸거나, 조건문이 계속 늘어나서 새 기능 추가 때마다 기존 코드를 수정해야 하는 경우야.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;예: &lt;/span&gt;if (type == Fire) ... else if (type == Ice) ...&lt;span&gt;가 계속 늘어나면 OCP 위반 신호가 될 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;불필요한 타이머 임시 로직&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;이건 확실히 코드 스멜인 경우가 많아.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;예: &amp;ldquo;0.5초 뒤면 아마 로딩 끝났겠지&amp;rdquo; 같은 식으로 타이머를 넣는 건 레이스 컨디션과 환경 의존 버그를 만들기 쉬워.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;가능하면 &lt;/span&gt;&lt;b&gt;&lt;span&gt;이벤트, 콜백, Task 완료, 상태 변화 신호&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 기다리는 방식이 더 안정적이야.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/42</guid>
      <comments>https://letscoding0103.tistory.com/42#entry42comment</comments>
      <pubDate>Tue, 6 Oct 2026 00:41:48 +0900</pubDate>
    </item>
    <item>
      <title>면접 정리</title>
      <link>https://letscoding0103.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;헷갈리기 쉬운개념&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 결정적인 차이&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 220px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;Remove(x) vs RemoveAt(x)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;값 삭제 vs 인덱스 삭제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;TryParse() vs Parse()&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;변환 실패를 false로 처리 vs 잘못된 형식이면 예외 발생 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;Where() vs ToList()&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;일반적으로 지연 실행되는 쿼리 vs 그 시점의 결과를 리스트로 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;override vs new&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;가상 메서드 재정의 vs 멤버 숨기기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;Thread.Sleep() vs await Task.Delay()&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;현재 스레드를 멈춤 vs 일반적으로 스레드를 점유하지 않는 비동기 대기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;Task vs Thread&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;작업 및 완료 상태의 추상화 vs 실제 실행 흐름&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;Concurrency vs Parallelism&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;여러 작업을 함께 진행하도록 구성 vs 같은 시점에 실제 동시 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;Ready vs Blocked&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;CPU 할당만 기다림 vs I/O 등의 별도 조건을 기다림&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;Race Condition vs Deadlock&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;실행 순서에 따른 잘못된 결과 vs 서로 기다려 진행 불가&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;lock vs SemaphoreSlim(2, 2)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;한 번에 하나의 진입 보호 vs 최대 두 개의 동시 진입 허용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마지막으로, 코드 문제에서 특히 주의할 세 가지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;① 코드를 한 줄씩만 읽지 말고 상태 변화를 추적하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;List 삭제, Queue/Stack, Round Robin, ++ 경합 문제는 머릿속으로만 계산하기보다 현재 값이나 남은 시간을 작은 표로 적으면 실수가 크게 줄어들어.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;② 비슷한 이름의 개념은 목적을 중심으로 구분하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티프로그래밍&amp;middot;멀티태스킹&amp;middot;멀티스레딩&amp;middot;멀티프로세싱은 서로 함께 사용될 수 있는 개념이야. 이름을 외우기보다는 각각 &lt;span&gt;CPU 활용률, 작업 전환, 실행 흐름, 물리적 병렬 실행&lt;/span&gt; 중 무엇을 강조하는지 기억하면 돼.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;③ 비동기와 멀티스레딩을 동일하게 생각하지 않기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 이번 문제 은행에서 여러 번 등장한 핵심이야.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;await Task.Delay(1000);                 // 비동기 대기
await Task.Run(() =&amp;gt; HeavyCalculation()); // CPU 계산을 ThreadPool에 전달&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 코드 모두 await를 사용하지만 내부 목적과 실행 방식은 달라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-wiz-attrbind=&quot;aria-level=Div3id_19/fLk2Md&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;단계: Task 객체 생성 및 스레드 풀 스케줄링&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICRAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;Task.Run(Action)을 호출하면 내부적으로 다음과 같은 일이 일어납니다.&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAB&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;Task 객체 생성:&lt;/b&gt; 실행할 코드 블록을 담은 새로운 Task 인스턴스가 만들어집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAC&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;TaskScheduler.Default 사용:&lt;/b&gt; .NET의 기본 스케줄러인 &lt;b&gt;스레드 풀 스케줄러&lt;/b&gt;를 가져옵니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAD&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;큐(Queue)에 대기:&lt;/b&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt; 스케줄러는 이 작업을 스레드 풀의 &lt;b&gt;글로벌 작업 큐(Global Work Queue)&lt;/b&gt;에 밀어 넣습니다(Enqueue).&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 32px 0px; text-decoration: none; border-bottom: 0px rgb(128, 128, 128);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-pl=&quot;|||[]&quot; data-sfc-root=&quot;ep&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-wiz-attrbind=&quot;aria-level=Div3id_25/fLk2Md&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;2단계: 스레드 풀의 작업 처리 방식 (두 가지 큐)&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDRAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;스레드 풀은 작업을 효율적으로 분배하기 위해 두 종류의 큐를 관리합니다.&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAA&quot; data-ved=&quot;2ahUKEwirlf_OoZuXAxWmqFYBHVtvLGAQ-7AUegYIAAgOEAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAB&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;글로벌 큐 (Global Queue):&lt;/b&gt; Task.Run처럼 외부에서 들어온 새로운 작업들이 먼저 쌓이는 곳입니다. 선입선출(FIFO) 방식으로 동작합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAE&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;로컬 큐 (Local Queue):&lt;/b&gt; 스레드 풀에 속한 개별 스레드들이 각자 하나씩 가지고 있는 비밀 주머니입니다. 어떤 스레드가 작업을 하다가 추가로 파생 작업을 만들면 이 로컬 큐에 쌓입니다. 후입선출(LIFO) 방식으로 캐시 효율을 높입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 32px 0px; text-decoration: none; border-bottom: 0px rgb(128, 128, 128);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-pl=&quot;|||[]&quot; data-sfc-root=&quot;ep&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-wiz-attrbind=&quot;aria-level=Div3id_2x/fLk2Md&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;3단계: 스레드 할당과 작업 훔치기 (Work Stealing)&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIERAB&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;글로벌 큐에 작업이 들어오면, 놀고 있는(Idle) 스레드 풀의 스레드가 작업을 하나씩 꺼내어 실행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIERAC&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;만약 특정 스레드에 작업이 몰려있고 다른 스레드는 할 일이 없다면, 노는 스레드가 다른 스레드의 로컬 큐 맨 뒤에서 작업을 훔쳐와서 대신 처리합니다. 이를 &lt;b&gt;Work Stealing 알고리즘&lt;/b&gt;이라고 하며, 이를 통해 모든 스레드가 골고루 일하게 만듭니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 32px 0px; text-decoration: none; border-bottom: 0px rgb(128, 128, 128);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-pl=&quot;|||[]&quot; data-sfc-root=&quot;ep&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-wiz-attrbind=&quot;aria-level=Div3id_3c/fLk2Md&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;4단계: 상태 완료 및 반환&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIFBAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;스레드가 Task.Run 내부의 코드를 모두 실행하고 나면, Task 객체의 상태를 RanToCompletion(완료), Faulted(예외 발생), 또는 Canceled(취소)로 업데이트합니다.&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAIFRAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;이후 해당 Task를 await하고 있던 메인 흐름이나 다른 코드가 그 결과를 받아 다음 작업을 이어 나가게 됩니다. 작업을 마친 스레드는 파괴되지 않고 다시 스레드 풀로 돌아가 대기합니다.&lt;/div&gt;
&lt;hr data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 32px 0px; text-decoration: none; border-bottom: 0px rgb(128, 128, 128);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-pl=&quot;|||[]&quot; data-sfc-root=&quot;ep&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;715&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qM0TD/dJMcaa8c9cH/6gmOBRjIsQuld7ZgsNjX00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qM0TD/dJMcaa8c9cH/6gmOBRjIsQuld7ZgsNjX00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qM0TD/dJMcaa8c9cH/6gmOBRjIsQuld7ZgsNjX00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqM0TD%2FdJMcaa8c9cH%2F6gmOBRjIsQuld7ZgsNjX00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;559&quot; height=&quot;340&quot; data-origin-width=&quot;1176&quot; data-origin-height=&quot;715&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-wiz-attrbind=&quot;aria-level=L5v8Me_4n/fLk2Md&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;4. 핵심 요약: 무엇을 써야 할까?&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 4px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sfc-inited=&quot;2&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIGBAA&quot; data-ved=&quot;2ahUKEwiDotu0pJuXAxVOavUHHQnlGKcQ-q4QegYIAAgYEAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;작업 유형대표 예시올바른 구현 방법
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-animation-nesting=&quot;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot;&gt;
&lt;tr data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;
&lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.571429px solid rgb(54, 56, 64);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;CPU 작업&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.571429px solid rgb(54, 56, 64);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;알고리즘, 이미지 연산, 복잡한 계산&lt;/td&gt;
&lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0.571429px solid rgb(54, 56, 64);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;Task.Run(() =&amp;gt; DoHeavyMath())&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;
&lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;I/O 작업&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;파일 읽기/쓰기, DB 쿼리, 네트워크 API 호출&lt;/td&gt;
&lt;td colspan=&quot;undefined&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;await File.ReadAllTextAsync(...) (제공되는 Async 메서드 그냥 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIGRAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIGRAB&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;await 이후에 돌아오는 스레드가 원래 스레드인지, 아니면 다른 스레드인지 결정하는 ConfigureAwait(false)라는 개념이 있습니다. UI 프로그램(WPF/WinForms)이나 성능 최적화에서 아주 중요한데, 이 개념도 함께 알아볼까요?&lt;/div&gt;
&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sfc-inited=&quot;2&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIGxAA&quot; data-ved=&quot;2ahUKEwiDotu0pJuXAxVOavUHHQnlGKcQ_7UVegYIAAgbEAA&quot; data-wiz-uids=&quot;L5v8Me_5k&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-xid=&quot;Gd7Hsc&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 4px 0px 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot;&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-wiz-attrbind=&quot;aria-level=eor6Wb_18/fLk2Md&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;1. ConfigureAwait(false)의 핵심 의미&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIChAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;await가 붙은 비동기 작업이 끝나면, 남은 코드를 이어받아 실행할 스레드가 필요합니다. 이때 .NET은 기본적으로 SynchronizationContext&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;(동기화 문맥)&lt;/b&gt;라는 지도를 보고 원래 어떤 환경(스레드)에 있었는지 확인한 뒤, 웬만하면 원래 스레드로 돌아가려고 노력합니다.&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-processed=&quot;true&quot; data-hveid=&quot;CAAICxAA&quot; data-ved=&quot;2ahUKEwjIqJropJuXAxVlmq8BHVt0KDcQ-7AUegYIAAgLEAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAICxAB&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;ConfigureAwait(true) (기본값):&lt;/b&gt; &quot;나 원래 있던 스레드(환경) 기억해 둬. 비동기 작업 끝나면 반드시 &lt;b&gt;원래 그 스레드로 돌아가서&lt;/b&gt; 남은 코드 실행할래.&quot;&lt;/span&gt;&lt;/li&gt;
&lt;li data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sae=&quot;&quot; data-hveid=&quot;CAAICxAE&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;span data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;&lt;b&gt;ConfigureAwait(false):&lt;/b&gt; &quot;원래 스레드가 어디였는지 관심 없어. 작업 끝난 시점에 &lt;b&gt;스레드 풀에서 노는 스레드 아무나&lt;/b&gt; 잡아서 남은 코드 실행해 줘.&quot;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 32px 0px; text-decoration: none; border-bottom: 0px rgb(128, 128, 128);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-pl=&quot;|||[]&quot; data-sfc-root=&quot;ep&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 20px; font-weight: 600; margin: 24px 0px 12px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-root=&quot;ep&quot; data-wiz-attrbind=&quot;aria-level=eor6Wb_28/fLk2Md&quot; data-sfc-cp=&quot;&quot; data-animation-nesting=&quot;&quot;&gt;2. 왜 굳이 다른 스레드에서 실행하나요? (장점)&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDhAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;원래 스레드로 돌아가려면 컨텍스트를 전환하는 비용(스케줄링 오버헤드)이 듭니다. false를 주면 이 과정을 생략하므로 &lt;b&gt;성능이 향상&lt;/b&gt;됩니다.&lt;/div&gt;
&lt;div data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 16px; font-weight: 400; margin: 12px 0px 16px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-hveid=&quot;CAAIDxAA&quot; data-sfc-root=&quot;ep&quot; data-sfc-cp=&quot;&quot;&gt;더 중요한 이유는 &lt;b&gt;데드락(Deadlock, 교착 상태) 방지&lt;/b&gt;입니다.&lt;br /&gt;WPF나 WinForms 같은 UI 애플리케이션에서는 메인 UI 스레드가 하나뿐입니다. 메인 스레드가 비동기 작업을 기다리면서 멈춰(Blocking) 있는데, 비동기 작업은 끝난 뒤 &quot;나 메인 스레드로 돌아갈래!&quot; 하고 문을 두드리면 서로를 기다리며 프로그램이 영원히 멈추는 데드락이 발생합니다. ConfigureAwait(false)는 아예 다른 스레드에서 남은 일을 처리하므로 이 데드락을 원천 차단합니다.&lt;/div&gt;
&lt;hr data-copy-service-computed-style=&quot;font-family: NanumBarunGothicOTF; font-size: 14px; font-weight: 400; margin: 32px 0px; text-decoration: none; border-bottom: 0px rgb(128, 128, 128);&quot; data-sae=&quot;&quot; data-processed=&quot;true&quot; data-complete=&quot;true&quot; data-sfc-pl=&quot;|||[]&quot; data-sfc-root=&quot;ep&quot; data-ke-style=&quot;style1&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/41</guid>
      <comments>https://letscoding0103.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 6 Oct 2026 00:40:08 +0900</pubDate>
    </item>
    <item>
      <title>오답노트</title>
      <link>https://letscoding0103.tistory.com/40</link>
      <description>&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;static int Run()
{
    try { return 7; }
    finally { Console.Write(&quot;F&quot;); }
}

Console.Write(Run());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return은 값을 호출한 쪽에 돌려주는 명령이고, Console.Write()는 콘솔에 문자를 출력하는 명령이야. 둘은 완전히 다른 동작이야.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 실제 코드 실행 순서&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;static int Run()
{
    try { return 7; }
    finally { Console.Write(&quot;F&quot;); }
}

Console.Write(Run());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 실행 순서대로 따라가 보자.&lt;/p&gt;
&lt;div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Console.Write(Run())을 만난다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Console.Write()를 실행하려면 인자로 전달할 Run()의 반환값을 먼저 알아야 하므로 Run()부터 실행한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;try { return 7; }을 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환할 값 7을 결정한다. 하지만 아직 Run() 함수가 완전히 종료된 것은 아니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;finally 블록을 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;Console.Write(&quot;F&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 콘솔에 F가 먼저 출력된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Run()이 종료되면서 7을 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 바깥의 Console.Write(Run())은 7이라는 값을 전달받는다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;바깥쪽 Console.Write(7)을 실행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 출력된 F 뒤에 7이 이어서 출력된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최종 출력&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;F7&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 그러면 언제 7F가 출력될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 try 내부에서 직접 출력한다면 달라져.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;static void Run()
{
    try
    {
        Console.Write(&quot;7&quot;);
        return;
    }
    finally
    {
        Console.Write(&quot;F&quot;);
    }
}

Run();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 try 안에서 실제로 7을 출력했기 때문에 결과가 다음과 같아.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;7F&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이를 비교하면:&lt;/p&gt;
&lt;div&gt;문법의미
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;return 7;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;호출한 함수에 7을 돌려준다. 콘솔 출력은 하지 않는다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;Console.Write(7);&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;콘솔에 7을 출력한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;finally&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;try에서 return을 만나더라도 함수가 정상적으로 반환되기 전에 실행된다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;6번. C# 값 복사&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객관식 &amp;middot; 4점&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 사용 불가 &amp;middot; 단일 정답&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드를 실행했을 때 출력되는 값은?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;var a = new Counter { Value = 1 };
var b = a;
b.Value = 5;
Console.WriteLine($&quot;{a.Value},{b.Value}&quot;);

public struct Counter
{
    public int Value;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 1,1&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B. 1,5&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C. 5,5&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;D. 컴파일 오류&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1,5 -&amp;gt; B&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;7번. 게임 시뮬레이션 개념&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주관식 &amp;middot; 6점&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 사용 불가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 초기 상태와 같은 순서의 입력을 주었을 때 항상 같은 최종 결과가 나오는 성질을 무엇이라 하는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;한글 세 글자&lt;/span&gt;로 쓰고, 그 성질을 만족하는 프로그램의 결과가 반드시 게임 규칙상 올바르지는 않은 이유를 한 문장으로 설명하시오.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념: 결정성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명: 결정적인 프로그램도 매번 동일한 &lt;span&gt;잘못된 결과&lt;/span&gt;를 낼 수 있다는 점이 핵심.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;리스폰 후 새 몬스터가 과거의 예약 피해를 받는 문제&lt;/span&gt;가 결정성(Determinism)과 정확성(Correctness)의 차이를 보여주는 좋은 사례야.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 결정성이란 정확히 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정적인 프로그램은 동일한 초기 상태와 동일한 순서의 입력이 주어지면 항상 같은 결과를 만들어야 해.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 전투 시뮬레이터에 다음 조건을 입력했다고 해보자.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;초기 상태
Player HP  = 100
Monster HP = 100

입력
T1: Player가 Monster에게 30 damage
T2: Monster가 Player에게 20 damage&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 100번 실행했을 때 결과가 매번 다음과 같다면:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Player HP  = 80
Monster HP = 70&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로그램은 해당 조건에서 결정적인 동작을 보이는 거야.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 결정성은 다음 관계를 의미해.&lt;/p&gt;
&lt;div&gt;&lt;span data-math-display=&quot;true&quot; data-math-source=&quot;F(S, I) = O&quot;&gt;\[ F(S, I) = O \]&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span data-math-display=&quot;false&quot; data-math-source=&quot;S&quot;&gt;\(S\)&lt;/span&gt;&lt;/span&gt;: 초기 상태&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span data-math-display=&quot;false&quot; data-math-source=&quot;I&quot;&gt;\(I\)&lt;/span&gt;&lt;/span&gt;: 동일한 순서의 입력&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span data-math-display=&quot;false&quot; data-math-source=&quot;O&quot;&gt;\(O\)&lt;/span&gt;&lt;/span&gt;: 실행 결과&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 &lt;span&gt;&lt;span data-math-display=&quot;false&quot; data-math-source=&quot;S&quot;&gt;\(S\)&lt;/span&gt;&lt;/span&gt;와 &lt;span&gt;&lt;span data-math-display=&quot;false&quot; data-math-source=&quot;I&quot;&gt;\(I\)&lt;/span&gt;&lt;/span&gt;를 사용하면 항상 동일한 &lt;span&gt;&lt;span data-math-display=&quot;false&quot; data-math-source=&quot;O&quot;&gt;\(O\)&lt;/span&gt;&lt;/span&gt;가 나와야 한다는 뜻이지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 여기에는 게임 규칙에 맞는 결과여야 한다는 조건이 없어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 이게 핵심이야.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 결정적인데 잘못된 프로그램이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 풀었던 Goblin 리스폰 문제를 생각해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 게임 규칙은 다음과 같았어.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제거된 엔티티에 예약된 공격은 해당 엔티티가 제거된 이후 새로 생성된 엔티티에 영향을 주면 안 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기존 버그가 남아 있는 프로그램을 실행해 보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동일한 시나리오를 세 번 실행&lt;/h3&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;입력 조건&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;T0: Goblin-A 생성 (E0, HP 100)
T0: A에게 3틱 뒤 35 피해 예약
T1: Goblin-A 제거
T1: Goblin-B 생성 (E0 재사용)
T2: Tick
T3: Tick&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존 버그가 있는 프로그램의 실행 결과&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1회차&lt;/td&gt;
&lt;td&gt;Goblin-B HP = 65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2회차&lt;/td&gt;
&lt;td&gt;Goblin-B HP = 65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3회차&lt;/td&gt;
&lt;td&gt;Goblin-B HP = 65&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결정성 만족:&lt;/span&gt; 매번 결과가 같음&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정확성 위반:&lt;/span&gt; 정상적인 HP는 100이어야 함&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목할 점은 프로그램이 무작위로 잘못 작동한 게 아니라는 거야.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 똑같은 규칙을 따라서 &lt;span&gt;일관되게 잘못 작동하고 있었어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 내부 코드가 매번 다음과 같이 실행됐으니까.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;과거의 예약 피해 대상 = E0
               &amp;darr;
Tick에서 Registry.Find(E0)
               &amp;darr;
현재 E0를 사용하는 Goblin-B 반환
               &amp;darr;
35 damage 적용
               &amp;darr;
HP 65&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실행 흐름 자체는 매번 동일해.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 프로그램은 결정적이지만 엔티티 생명주기 불변식을 위반한 거지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 Generation을 도입한 것은 단순히 결과를 일정하게 만들려던 게 아니라 &lt;span&gt;잘못된 게임 규칙 적용을 수정하기 위해서&lt;/span&gt;였어.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 결정성과 정확성은 별도로 검증해야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개를 표로 비교하면 더 명확해.&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.7674%;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1395%;&quot;&gt;&lt;span&gt;결정성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.9767%;&quot;&gt;&lt;span&gt;정확성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.7674%;&quot;&gt;&lt;span&gt;핵심 질문&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1395%;&quot;&gt;&lt;span&gt;결과가 항상 같은가?&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.9767%;&quot;&gt;&lt;span&gt;결과가 요구사항에 맞는가?&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.7674%;&quot;&gt;&lt;span&gt;판단 기준&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1395%;&quot;&gt;&lt;span&gt;동일 조건에서 결과의 일관성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.9767%;&quot;&gt;&lt;span&gt;명세와 불변식 충족&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.7674%;&quot;&gt;&lt;span&gt;대표 테스트&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1395%;&quot;&gt;&lt;span&gt;동일 입력 반복 실행 후 결과 비교&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.9767%;&quot;&gt;&lt;span&gt;실제 결과와 기대 결과 비교&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.7674%;&quot;&gt;&lt;span&gt;버그 예시&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.1395%;&quot;&gt;&lt;span&gt;동일 입력인데 HP가 65 또는 100으로 달라짐&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.9767%;&quot;&gt;&lt;span&gt;매번 HP가 65지만 정답은 100임&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘을 조합하면 다음 네 가지 상황도 가능해.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 52px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;정확함&lt;/td&gt;
&lt;td&gt;잘못됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;&lt;span&gt;결정적&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;매번 올바른 결과&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;매번 똑같이 잘못된 결과&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;&lt;span&gt;비결정적&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;허용된 범위에서 매번 달라도 규칙을 충족하는 결과&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;span&gt;실행할 때마다 달라지고 게임 규칙도 위반하는 결과&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 게임의 모든 시스템이 반드시 결정적이어야 하는 건 아니야. 예를 들어 시각 효과의 무작위 파티클 위치처럼 결과가 조금씩 달라도 허용되는 시스템이 있거든.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 서버 전투 시뮬레이션이나 Replay 시스템에서는 결정성이 매우 중요한 설계 요구사항이 될 수 있어.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;문제: 결정성이란 무엇이며, 결정성을 만족해도 결과가 올바르다고 보장할 수 없는 이유는?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념: 결정성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정성은 동일한 초기 상태와 동일한 순서의 입력에 대해 항상 동일한 결과를 생성하는 성질이다. 하지만 결정성은 결과의 일관성만 보장하며, 잘못된 로직으로 매번 동일한 오답이 생성될 수 있으므로 게임 규칙상 정확성을 보장하지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정성은 결과의 재현 가능성이고, 정확성은 결과의 올바름이다. 재현 가능한 버그도 엄연히 버그다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;8번. C# 콜백과 Null 조건부 연산자&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객관식 &amp;middot; 4점&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 사용 불가 &amp;middot; 단일 정답&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드를 실행했을 때 콘솔에 출력되는 결과는?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Action&amp;lt;string&amp;gt;? sink = null;

sink?.Invoke(&quot;A&quot;);

sink = text =&amp;gt; Console.Write(text);

sink?.Invoke(&quot;B&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. AB&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B. A&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C. B&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D. nullB&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 코드를 한 줄씩 해석해 보자&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Action&amp;lt;string&amp;gt;? sink = null;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뜻은 다음과 같아.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Action&amp;lt;string&amp;gt;: 문자열 하나를 매개변수로 받고 반환값이 없는 함수를 담을 수 있음.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;?: null일 수도 있음.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;sink = null: 현재는 연결된 함수가 없음.따라서 아래 코드는 실행할 함수가 없어서 아무 일도 일어나지 않아.여기서 ?.는 sink가 null이 아닐 때만 Invoke()를 실행한다는 뜻이야.2. 네가 궁금해한 핵심 부분이것은 &lt;span&gt;람다식으로 만든 함수를 sink에 할당하는 코드&lt;/span&gt;야.&amp;nbsp;풀어쓰면 대략 이런 형태와 같아.즉, 아래 람다에서:text는 아직 어떤 특정 문자열이 들어 있는 상태가 아니야.현재 상태는 이렇게 변해.3. 그다음 Invoke를 만나면?이제 연결되어 있는 함수를 실제로 호출하는 거야.
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 실행 흐름&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;sink?.Invoke(&quot;B&quot;)
        │
        │ &quot;B&quot; 전달
        ▼
text =&amp;gt; Console.Write(text)
        │
        │ text = &quot;B&quot;
        ▼
Console.Write(&quot;B&quot;)
        │
        ▼
      B 출력&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
따라서 전체 코드는 다음 순서로 실행되는 거지.이번 문법에서 기억할 세 가지네가 말한 표현을 조금만 다듬으면:참고로 이런 형태는 게임 프로그래밍에서도 로그 출력, UI 알림, 이벤트 발생 시 외부에 결과를 전달하는 콜백 등에 자주 활용돼.&lt;/li&gt;
&lt;li data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sink에는 문자열을 받아 출력하는 함수가 등록되고, Invoke(&quot;B&quot;)를 호출하는 순간 B가 text에 전달되어 Console.Write(text)가 실행된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Action&amp;lt;string&amp;gt;? sink = null;       // 1. 함수를 담을 변수 선언

sink = text =&amp;gt; Console.Write(text); // 2. 실행할 함수 연결

sink?.Invoke(&quot;B&quot;);                  // 3. 연결된 함수에 &quot;B&quot;를 전달해 실행&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;실행 코드실제 동작콘솔 출력
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;sink = null&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;함수가 연결되지 않은 상태&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;sink?.Invoke(&quot;A&quot;)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;sink가 null이므로 호출하지 않음&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;sink = text =&amp;gt; Console.Write(text)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;sink에 함수를 연결함&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;sink?.Invoke(&quot;B&quot;)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;연결된 함수에 B를 전달해 호출함&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;B&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;Invoke(&quot;B&quot;)의 &quot;B&quot;가 람다식의 text 매개변수로 전달돼.&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;sink?.Invoke(&quot;B&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;나중에 함수가 호출되면 전달받을 매개변수의 이름&lt;/span&gt;이야.&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;text =&amp;gt; Console.Write(text)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;void PrintText(string text)
{
    Console.Write(text);
}

sink = PrintText;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;이것은 &lt;span&gt;람다식으로 만든 함수를 sink에 할당하는 코드&lt;/span&gt;야.&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;풀어쓰면 대략 이런 형태와 같아.&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;sink = text =&amp;gt; Console.Write(text);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;sink?.Invoke(&quot;A&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/40</guid>
      <comments>https://letscoding0103.tistory.com/40#entry40comment</comments>
      <pubDate>Thu, 1 Oct 2026 18:44:47 +0900</pubDate>
    </item>
    <item>
      <title>게임 프로그래밍 Base knowledge</title>
      <link>https://letscoding0103.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;게임 프로그래밍 문제를 빠르게 풀려면 나는 &lt;/span&gt;&lt;b&gt;Identity &amp;times; Lifetime &amp;times; State &amp;times; Time &amp;times; Ownership &amp;times; Order&lt;/b&gt;&lt;span&gt; 여섯 축을 머릿속 체크리스트처럼 두는 걸 추천해. 문제가 생기면 &amp;ldquo;이 중 뭐가 깨졌지?&amp;rdquo;라고 먼저 보는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;문제 유형먼저 정의할 불변식이런 증상이 보이면 의심대표 원인
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Entity Identity / Lifetime&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;새 엔티티는 이전 엔티티의 상태를 상속받지 않는다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;리스폰 직후 이상한 데미지, 버프, 타겟팅&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;ID 재사용, stale reference, generation 없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Object Pooling&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Pool에서 꺼낸 객체는 새 객체와 동일한 초기 상태여야 한다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;두 번째 사용부터만 이상함&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;HP/flag/list/timer 초기화 누락&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;State Machine&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;허용되지 않은 상태 전이는 발생하지 않는다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Dead인데 공격, Stun 중 이동&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;상태 전이 검증 누락&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Tick / Frame Order&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;같은 논리 Tick의 결과는 처리 순서에 의존하지 않는다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;A&amp;rarr;B 순서 바꾸면 결과가 달라짐&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;즉시 commit, phase 분리 없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Delayed Event / Timer&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;예약 효과는 예약 당시 대상과 조건에만 적용된다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;몇 초 후 엉뚱한 대상에 효과&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;stale target, 취소 누락&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Ownership&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;상태는 하나의 명확한 시스템이 책임진다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;같은 값이 서로 다른 곳에서 다름&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;여러 시스템이 같은 상태 수정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Determinism&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;같은 초기 상태 + 입력은 같은 결과를 만든다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;replay 결과가 달라짐&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Hash 순서, random seed, thread timing&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Concurrency&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;공유 상태 수정은 원자적/일관적이어야 한다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;가끔만 중복 지급/중복 공격&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;race condition, check-then-act&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Networking / Authority&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;최종 게임 상태의 권한 주체는 명확하다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;클라에선 맞는데 서버에서 롤백&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;client prediction/authority 충돌&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Replication&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;원격 상태는 정해진 버전/순서대로 적용된다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;순간이동, HP 되돌아감&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;오래된 packet이 최신 상태 덮어씀&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Event Subscription&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;객체 수명 종료 시 이벤트 연결도 종료된다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;죽은 객체가 계속 반응&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;unsubscribe 누락&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Resource Lifetime&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;사용 중인 리소스는 살아 있고, 종료된 리소스는 다시 쓰지 않는다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;씬 전환 후 crash/깨진 asset&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;dispose/unload 타이밍&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Spatial / Collision&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;동일 좌표계와 단위를 사용한다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;특정 방향/경계에서만 판정 실패&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;world/local 혼동, 좌표 변환&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Floating Point&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;경계 판정은 오차 허용 정책이 명확하다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;사거리 끝에서 공격이 가끔 실패&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;==&lt;span&gt; 비교, 누적 오차&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Units&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;같은 물리량은 동일 단위를 사용한다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;이동속도/시간이 1000배 틀림&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;sec/ms, meter/cm 혼동&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Delta Time&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;시간 기반 동작은 FPS에 독립적이어야 한다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;고FPS/저FPS에서 이동속도 다름&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;deltaTime 누락/중복 적용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Collection Mutation&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;순회 중 구조 변경 규칙이 명확하다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;일부 몬스터만 업데이트 누락&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;iteration 중 Remove&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Index / Handle Validity&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;index는 위치이고 identity와 동일하지 않을 수 있다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;삭제 후 다른 객체가 잘못 선택됨&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;array index를 unique ID처럼 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Cache&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;캐시 값은 원본 상태와 같은 버전을 나타낸다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;UI/AI만 옛 상태를 봄&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;invalidation 누락&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Serialization / Load&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Save&amp;rarr;Load 후 의미상 동일 상태가 복원된다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;재접속/세이브로드 후에만 버그&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;필드 누락, default value&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Randomness&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;random 사용 정책과 seed가 명확하다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;테스트가 가끔 실패&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;공유 RNG, seed 불명확&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Input&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;입력은 Tick당 정확히 정해진 횟수만 소비된다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;버튼 한 번에 두 번 발동&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Update/FixedUpdate 중복 처리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Animation &amp;harr; Gameplay&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;시각 상태와 실제 게임 상태의 책임이 구분된다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;애니메이션은 맞았는데 판정이 틀림&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;animation event에 gameplay 의존&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Scene / World Transition&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;이전 World의 객체가 새 World에 영향을 주지 않는다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;맵 이동 후 이전 몬스터 이벤트 발생&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;static/event/timer 잔존&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;Initialization Order&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;사용할 때 필요한 상태는 모두 초기화되어 있다&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;첫 프레임에만 null/잘못된 값&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;Awake/Start 순서 의존&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이걸 전부 암기할 필요는 없어. 대신 &lt;/span&gt;&lt;b&gt;&lt;span&gt;증상에서 문제 종류를 빠르게 떠올리는 감각&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 만들면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 이번 문제에서 QA가:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;몬스터가 죽고 빠르게 리스폰하면 새 몬스터 HP가 나중에 감소한다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 했지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 문장에는 이미 강한 힌트가 세 개 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;죽고 &amp;rarr; 새로 생성&amp;rdquo;**이라는 말이 나오면 &lt;/span&gt;Lifetime&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;리스폰&amp;rdquo;**이라는 말이 나오면 &lt;/span&gt;Pooling / ID reuse&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;일정 Tick 뒤&amp;rdquo;**라는 말이 나오면 &lt;/span&gt;Delayed Event / Timer&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 경험이 쌓이면 코드를 보기 전부터:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;이거 old entity에 예약된 뭔가가 새 entity로 넘어가는 stale reference 계열 아닐까?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 1순위 가설로 떠올릴 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;물론 바로 결론 내리면 안 되고, 그다음 trace로 검증하는 거지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번처럼:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;old entity
&amp;rarr; delayed event 예약
&amp;rarr; despawn
&amp;rarr; id 재사용
&amp;rarr; new entity
&amp;rarr; delayed event 실행&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 연결이 확인되면 가설이 강해지는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또 몇 가지 증상-의심 패턴은 특히 기억할 가치가 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;처음엔 정상인데 두 번째부터 이상하다&amp;rdquo;**면 초기화보다 &lt;/span&gt;&lt;b&gt;&lt;span&gt;reset/pooling&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 의심해.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1회차 정상
2회차 이상&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;은 객체 생성 코드보다:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Despawn
Reset
Pool Return
Reuse&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 먼저 보는 게 효율적이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;가끔 발생한다&amp;rdquo;**면 데이터 자체보다 &lt;/span&gt;&lt;b&gt;&lt;span&gt;순서와 시간&lt;/span&gt;&lt;/b&gt;&lt;span&gt;을 의심해.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;race condition
iteration order
async callback
network packet ordering
frame/tick ordering&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 것들.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만 deterministic 프로그램에서도 논리 버그는 항상 똑같이 발생할 수 있으므로 &amp;ldquo;가끔&amp;rdquo;이라는 표현만으로 concurrency라고 단정하면 안 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;특정 FPS에서만 발생한다&amp;rdquo;**면 거의 바로 시간 계열을 본다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;position += speed;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인지:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;position += speed * deltaTime;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인지,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;혹은 이미 초 단위인 값에 &lt;/span&gt;deltaTime&lt;span&gt;을 두 번 곱한 건 아닌지 보는 식이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;경계에서만 발생한다&amp;rdquo;**면 조건식을 본다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;distance &amp;lt; range&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인지:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;distance &amp;lt;= range&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인지,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;float equality인지, 배열 끝 index인지, 맵 bounds인지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;재접속하거나 씬을 다시 들어오면 발생한다&amp;rdquo;**면 lifecycle / persistence 쪽 확률이 확 올라가.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;static state
event subscription
timer
save/load
cache
singleton
scene unload&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 먼저 확인해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;**&amp;ldquo;화면에는 죽었는데 공격한다&amp;rdquo;**면 &lt;/span&gt;State ownership&lt;span&gt; 문제를 의심해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;렌더러는 Dead를 알고 있는데 combat system은 Alive라고 생각하는 식이지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이럴 때 질문은:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;Alive/Dead의 진짜 source of truth가 어디지?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 아주 중요한 게임 프로그래밍 사고가 하나 있어.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&amp;ldquo;ID는 정말 identity인가?&amp;rdquo;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 문제에서 &lt;/span&gt;EntityId(0)&lt;span&gt;을 보고 처음에는 그냥 하나의 엔티티 식별자라고 생각했지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 실제로는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;EntityId
= slot 위치&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;였고,&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;logical entity identity
= slot + lifecycle&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;였어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;게임 코드에서는 이런 일이 엄청 흔해.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;array index
network id
instance id
pool index
handle
database id
spawn sequence&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 전부 &amp;ldquo;ID처럼 보이지만 의미가 다를 수 있어.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 어떤 ID를 보면 항상:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;이 값은 위치인가, 논리적 identity인가, session 동안만 unique한가, 영구 unique한가?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 물어보는 습관이 꽤 강력해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 문제를 실제로 풀 때는 다음 흐름 하나만 습관화해도 많이 빨라질 거야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;증상
&amp;darr;
정상 불변식 정의
&amp;darr;
핵심 객체/상태 하나 선택
&amp;darr;
그 값의 생성 &amp;rarr; 저장 &amp;rarr; 변경 &amp;rarr; 사용을 trace
&amp;darr;
정상 상태와 실제 상태가 처음 달라지는 지점 찾기
&amp;darr;
가설 세우기
&amp;darr;
반례/대조 케이스 만들기
&amp;darr;
최소 수정
&amp;darr;
기존 정상 케이스 + 실패 케이스 + 경계 케이스 검증&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 **&amp;ldquo;처음 달라지는 지점&amp;rdquo;**이 중요해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 최종 증상은:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Goblin-B HP = 65&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지만 여기를 고치려고 하면 늦어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;진짜 최초의 잘못은 더 앞에서:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;old E0와 new E0를 동일 logical target으로 취급&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;한 순간이었어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;좋은 디버깅은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;최종 오류를 보는 게 아니라 최초의 invariant violation을 찾는 작업&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이라고 생각하면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;게임 프로그래머용으로 더 압축하면 나는 다음 여섯 질문을 먼저 던질 것 같아.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;이 객체는 누구인가?&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;mdash; Identity&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;언제부터 언제까지 같은 객체인가?&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;mdash; Lifetime&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;현재 어떤 상태여야 하는가?&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;mdash; State&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;누가 이 상태를 소유하는가?&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;mdash; Ownership&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;언제 이 처리가 실행되는가?&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;mdash; Time&lt;/span&gt;&lt;br /&gt;&lt;b&gt;&lt;span&gt;다른 처리와 어떤 순서로 실행되는가?&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &amp;mdash; Order&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 문제는 그중 정확히:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Identity + Lifetime + Time&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 세 축이 충돌한 문제였어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 프레임을 익히면 다음에 완전히 다른 문제가 나와도 &amp;ldquo;코드를 처음부터 전부 읽기&amp;rdquo;보다 훨씬 빠르게 문제 영역을 좁힐 수 있어.&lt;/span&gt;&lt;/p&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/38</guid>
      <comments>https://letscoding0103.tistory.com/38#entry38comment</comments>
      <pubDate>Thu, 1 Oct 2026 15:46:58 +0900</pubDate>
    </item>
    <item>
      <title>Arena Delayed Damage Regression</title>
      <link>https://letscoding0103.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;코드 리뷰시 클래스가 많고 호출 상황이 복잡하다면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;시작점부터 본다&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;외부에서 처음 호출되는 함수가 뭔지 찾음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;예: &lt;/span&gt;CombatWorld.ScheduleDamage()&lt;span&gt;, &lt;/span&gt;CombatWorld.Tick()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;상태를 실제로 들고 있는 클래스가 누군지 찾는다&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;이번 경우:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;현재 엔티티 상태 &amp;rarr; &lt;/span&gt;EntityRegistry&lt;/li&gt;
&lt;li&gt;&lt;span&gt;예약 피해 &amp;rarr; &lt;/span&gt;CombatWorld._damageByTick&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;값 하나를 끝까지 추적한다&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;예: &lt;/span&gt;EntityId target&lt;/li&gt;
&lt;li&gt;ScheduleDamage(target)&lt;/li&gt;
&lt;li&gt;Capture(target)&lt;/li&gt;
&lt;li&gt;EntityHandle&lt;/li&gt;
&lt;li&gt;ScheduledDamage.Target&lt;/li&gt;
&lt;li&gt;Tick()&lt;/li&gt;
&lt;li&gt;Resolve(handle)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;실제 &lt;/span&gt;GameEntity&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;함수 이름이 아니라 &amp;ldquo;무슨 상태를 읽고/바꾸는가&amp;rdquo;를 본다&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spawn()&lt;span&gt; &amp;rarr; slot 점유 + generation 증가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Despawn()&lt;span&gt; &amp;rarr; slot 해제&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Capture()&lt;span&gt; &amp;rarr; 현재 lifecycle 스냅샷&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Resolve()&lt;span&gt; &amp;rarr; 저장된 lifecycle이 아직 유효한지 검사&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Tick()&lt;span&gt; &amp;rarr; 예약된 효과 실행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 문제를 이런 식으로만 그리면 사실 호출 흐름은 이 정도야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;[외부 코드]
    |
    v
CombatWorld.ScheduleDamage(EntityId)
    |
    v
EntityRegistry.Capture(EntityId)
    |
    v
EntityHandle(Id + Generation)
    |
    v
ScheduledDamage에 저장
    |
    | 몇 Tick 경과
    v
CombatWorld.Tick()
    |
    v
EntityRegistry.Resolve(EntityHandle)
    |
    +-- generation 일치 --&amp;gt; GameEntity 반환 --&amp;gt; damage 적용
    |
    +-- generation 불일치 --&amp;gt; null --&amp;gt; damage DROP&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이걸 &lt;/span&gt;&lt;b&gt;&lt;span&gt;호출 그래프(call graph)&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 라고 생각하면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 Spawn/Despawn 흐름은 따로 떼서 보면 훨씬 단순해.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;CombatWorld.Spawn()
    |
    v
EntityRegistry.Spawn()
    |
    +-- free slot 선택
    +-- generation 증가
    +-- GameEntity 생성
    +-- _slots[slot]에 저장&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;CombatWorld.Despawn()
    |
    v
EntityRegistry.Despawn()
    |
    +-- _slots[slot] = null
    +-- slot을 _freeSlots에 반환&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 &lt;/span&gt;&lt;b&gt;&lt;span&gt;행동별로 흐름을 분리해서 보는 게 핵심&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이야.&lt;/span&gt;&lt;/p&gt;
&lt;h1&gt;&lt;span&gt; 특히 초보 때 많이 하는 실수가 클래스 파일을 위에서 아래까지 전부 읽는 거야. 그러면: &lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 머릿속에 동시에 올려야 해서 금방 꼬여.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;대신 이렇게 질문해.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;지금 내가 추적하는 값은 뭐지?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번에는 &lt;/span&gt;EntityId&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그다음:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;이 값이 어디서 다른 형태로 바뀌지?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;EntityId
&amp;rarr; Capture()
&amp;rarr; EntityHandle&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;이 값이 어디에 저장되지?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;ScheduledDamage.Target&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;마지막으로:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;언제 다시 꺼내 쓰지?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Tick()
&amp;rarr; Resolve()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 방식이 훨씬 편해.&lt;br /&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;public&amp;nbsp;Action&amp;lt;string&amp;gt;?&amp;nbsp;TraceSink&amp;nbsp;{&amp;nbsp;get;&amp;nbsp;set;&amp;nbsp;}&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;private&amp;nbsp;void&amp;nbsp;Trace(string&amp;nbsp;message)&amp;nbsp;=&amp;gt;&amp;nbsp;TraceSink?.Invoke(message);&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Action&amp;lt;string&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;은 네가 아는 대로 &lt;/span&gt;&lt;b&gt;string&lt;span&gt; 하나를 매개변수로 받고 반환값은 없는(&lt;/span&gt;void&lt;span&gt;) delegate&lt;/span&gt;&lt;/b&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개념적으로 이런 함수를 담을 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;void PrintLog(string text)
{
    Console.WriteLine(text);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 외부에서:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;world.TraceSink = PrintLog;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처럼 연결할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또는 람다로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;world.TraceSink = message =&amp;gt;
{
    Console.WriteLine(message);
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;도 가능해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 여기의 &lt;/span&gt;?&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Action&amp;lt;string&amp;gt;?&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;TraceSink에 delegate가 들어 있을 수도 있고 &lt;/span&gt;null&lt;span&gt;일 수도 있다&lt;/span&gt;&lt;/b&gt;&lt;span&gt;는 뜻이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 초기 상태에는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;TraceSink == null&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;get; set;&lt;span&gt;은 말 그대로 외부에서 읽고 설정할 수 있다는 뜻이고.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;var current = world.TraceSink; // get
world.TraceSink = Console.WriteLine; // set&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 핵심인:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;TraceSink?.Invoke(message);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 풀어쓰면 사실상 이거야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;if (TraceSink is not null)
{
    TraceSink.Invoke(message);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 &lt;/span&gt;&lt;b&gt;TraceSink&lt;span&gt;가 연결되어 있으면 message를 넘겨서 호출하고, 연결되어 있지 않으면 아무것도 하지 않는다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;world.TraceSink = Console.WriteLine;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;한 다음:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;Trace(&quot;T3 APPLY DAMAGE&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 호출하면:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;T3 APPLY DAMAGE&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 출력돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반대로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;world.TraceSink = null;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;인 상태에서:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;Trace(&quot;T3 APPLY DAMAGE&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;해도 오류가 나지 않고 그냥 아무 일도 안 일어나.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 네 표현 중:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;message가 있으면 읽고 없으면 null&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;은 조금 다르게 이해해야 해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정확히는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;message가 있는지 검사하는 게 아님

TraceSink가 있는지 검사함
&amp;darr;
있음  &amp;rarr; TraceSink(message) 호출
없음  &amp;rarr; 아무것도 하지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 &lt;/span&gt;Trace()&lt;span&gt; 자체도:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private void Trace(string message)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이므로 반환값은 없어. &lt;/span&gt;null&lt;span&gt;을 반환하는 함수도 아니야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 프로젝트에서는 역할을 이렇게 보면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;CombatWorld
   |
   | Trace(&quot;T1 SPAWN E0...&quot;)
   v
Trace()
   |
   | TraceSink가 연결되어 있나?
   |
   +-- YES &amp;rarr; 외부 로그 출력 함수에 message 전달
   |
   +-- NO  &amp;rarr; 그냥 넘어감&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 &lt;/span&gt;TraceSink&lt;span&gt;는 쉽게 말하면 **&amp;ldquo;로그가 발생했을 때 그 로그를 어디로 보낼지 외부에서 꽂을 수 있게 만든 콜백&amp;rdquo;**이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h1&gt;&lt;span&gt;Arena Delayed Damage Regression &amp;mdash; 최종 분석 보고서&lt;/span&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;1. 문제 재현 및 불변식&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;제공된 &lt;/span&gt;run.cmd&lt;span&gt;를 실행한 결과, &lt;/span&gt;Goblin-A(E0)&lt;span&gt;를 대상으로 T0에 예약된 35 damage가 T1에 해당 엔티티가 despawn된 이후에도 남아 있었고, 같은 slot &lt;/span&gt;E0&lt;span&gt;를 재사용하여 생성된 &lt;/span&gt;Goblin-B&lt;span&gt;에게 T3에 적용되는 현상을 재현하였다. 그 결과 새로 생성된 &lt;/span&gt;Goblin-B&lt;span&gt;의 HP가 공격받은 적이 없음에도 100에서 65로 감소하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;초기 &lt;/span&gt;test.cmd&lt;span&gt; 실행 결과는 총 9개 테스트 중 7개가 통과하고, &lt;/span&gt;QA_1842_RespawnedEntityKeepsInitialHp&lt;span&gt;와 &lt;/span&gt;QA_1911_RepeatedReuseDoesNotReceiveOldHit&lt;span&gt; 2개가 실패하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정상 동작이 지켜야 할 엔티티 생명주기 불변식은 다음과 같이 정의하였다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;새로 생성 또는 리스폰된 엔티티는 이전에 제거된 엔티티의 예약된 공격&amp;middot;피해 등 이전 생명주기의 상태를 상속받아서는 안 된다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. Root Cause&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityRegistry&lt;span&gt;는 고정 크기의 &lt;/span&gt;_slots&lt;span&gt; 배열을 사용하며, despawn된 slot 번호를 &lt;/span&gt;_freeSlots&lt;span&gt;에 반환한 후 이후 Spawn 시 다시 재사용한다. 따라서 &lt;/span&gt;Goblin-A&lt;span&gt;가 사용하던 &lt;/span&gt;EntityId(0)&lt;span&gt;은 despawn 이후 새로 생성된 &lt;/span&gt;Goblin-B&lt;span&gt;에도 다시 할당될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;문제는 기존 &lt;/span&gt;EntityId&lt;span&gt;가 slot 번호만 가지고 있어 &lt;/span&gt;&lt;b&gt;&lt;span&gt;같은 slot을 사용했던 서로 다른 엔티티 생명주기를 구분하지 못한다는 점&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존 &lt;/span&gt;ScheduledDamage&lt;span&gt; 역시 다음과 같이 target을 &lt;/span&gt;EntityId&lt;span&gt;만 저장하였다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;internal readonly record struct ScheduledDamage(
    long DueTick,
    long Sequence,
    EntityId Target,
    int Amount,
    string SourceTag);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한 &lt;/span&gt;ScheduleDamage()&lt;span&gt;는 예약 시점에 target이 존재하는지만 확인한 뒤 해당 &lt;/span&gt;EntityId&lt;span&gt;를 future damage에 저장하였다. &lt;/span&gt;Despawn()&lt;span&gt;에서는 pending damage를 제거하거나 무효화하지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;마지막으로 &lt;/span&gt;Tick()&lt;span&gt;에서는 damage 적용 시:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;var target = _registry.Find(damage.Target);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;을 수행하고 있었으며, &lt;/span&gt;Find(EntityId)&lt;span&gt;는 과거 엔티티가 아니라 &lt;/span&gt;&lt;b&gt;&lt;span&gt;현재 해당 slot에 존재하는 엔티티&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 실제 처리 흐름은 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Goblin-A = E0
&amp;rarr; E0에 delayed damage 예약
&amp;rarr; Goblin-A despawn
&amp;rarr; slot 0 반환
&amp;rarr; Goblin-B가 slot 0 재사용
&amp;rarr; Goblin-B 역시 E0
&amp;rarr; due tick에서 E0 조회
&amp;rarr; 현재 E0인 Goblin-B 반환
&amp;rarr; 과거 damage 적용&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 root cause는 다음과 같이 정의하였다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;재사용 가능한 slot 기반 &lt;/span&gt;EntityId&lt;span&gt;가 서로 다른 entity lifecycle을 구분하지 못하는 상태에서, delayed damage가 lifecycle 정보 없이 해당 ID만 저장한 채 despawn 이후까지 남아 동일한 ID로 생성된 새 엔티티에 적용되는 stale-target 문제이다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;3. 수정 설계 및 구현&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;public &lt;/span&gt;EntityId&lt;span&gt;와 compact slot 재사용 정책을 변경하지 않고, &lt;/span&gt;EntityRegistry&lt;span&gt;가 각 slot의 lifecycle을 내부적으로 관리하도록 수정하였다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EntityRegistry&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존 &lt;/span&gt;_slots&lt;span&gt;와 별도로 slot별 generation을 저장하는 배열을 추가하였다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;private readonly ulong[] _generations;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;새로운 Entity가 Spawn될 때 해당 slot의 generation을 증가시킨다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;_generations[slot]++;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 같은 &lt;/span&gt;E0&lt;span&gt; slot이 반복 재사용되더라도 내부적으로는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;E0 / generation 1
E0 / generation 2
E0 / generation 3&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;과 같이 서로 다른 lifecycle로 식별할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EntityHandle&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;외부 API를 변경하지 않고 내부에서 논리적 엔티티 인스턴스를 식별하기 위해 다음 internal 타입을 추가하였다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;internal readonly record struct EntityHandle(
    EntityId Id,
    ulong Generation);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityId&lt;span&gt;가 물리적인 slot 위치를 나타낸다면, &lt;/span&gt;EntityHandle&lt;span&gt;은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;slot과 해당 slot의 lifecycle을 함께 나타내는 내부 식별자&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 역할을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EntityRegistry.Capture()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 &lt;/span&gt;EntityId&lt;span&gt;가 가리키는 lifecycle을 예약 시점에 저장하기 위해 &lt;/span&gt;Capture(EntityId)&lt;span&gt;를 추가하였다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;internal EntityHandle? Capture(EntityId id)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 slot에 유효한 Entity가 존재하면 &lt;/span&gt;(EntityId, Generation)&lt;span&gt;을 &lt;/span&gt;EntityHandle&lt;span&gt;로 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EntityRegistry.Resolve()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예약 당시 저장된 &lt;/span&gt;EntityHandle&lt;span&gt;이 현재도 같은 lifecycle인지 확인하기 위해 &lt;/span&gt;Resolve(EntityHandle)&lt;span&gt;를 추가하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현재 slot의 generation과 handle에 저장된 generation이 일치하지 않으면 stale reference로 판단하고 &lt;/span&gt;null&lt;span&gt;을 반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ScheduledDamage&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;EntityId Target&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;대신:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;EntityHandle Target&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;을 저장하도록 변경하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이에 따라 ScheduledDamage는 단순히 &lt;/span&gt;E0&lt;span&gt;만 기억하는 것이 아니라 &lt;/span&gt;&lt;b&gt;&lt;span&gt;예약 당시의 &lt;/span&gt;E0 + Generation&lt;span&gt;을 기억한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CombatWorld.ScheduleDamage()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;public 시그니처는 기존과 동일하게 &lt;/span&gt;EntityId&lt;span&gt;를 받도록 유지하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만 내부적으로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;var targetHandle = _registry.Capture(target);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 호출하여 현재 lifecycle을 캡처하고, 해당 &lt;/span&gt;EntityHandle&lt;span&gt;을 &lt;/span&gt;ScheduledDamage&lt;span&gt;에 저장하도록 변경하였다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CombatWorld.Tick()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;damage 적용 시 단순 slot 조회 대신:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;_registry.Resolve(damage.Target)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 사용하도록 변경하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예약 당시와 현재 generation이 같으면 기존처럼 damage를 적용하고, generation이 달라졌다면 대상이 이미 despawn되고 slot이 재사용된 것으로 판단하여 해당 stale damage를 버린다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면 &lt;/span&gt;Despawn(EntityId)&lt;span&gt;와 &lt;/span&gt;TryGetEntity(EntityId)&lt;span&gt;는 현재 slot의 엔티티를 즉시 처리하거나 조회하는 기능이므로 기존 &lt;/span&gt;EntityId&lt;span&gt; 기반 동작을 유지하였다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;4. 검증 결과&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;수정 전 기존 테스트 결과는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;7 PASS / 2 FAIL&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이었으며, 수정 후 기존 9개 테스트를 다시 실행한 결과 모두 통과하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 기존에 실패했던:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;QA_1842_RespawnedEntityKeepsInitialHp
QA_1911_RepeatedReuseDoesNotReceiveOldHit&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 모두 PASS로 변경되었으며, 기존 정상 동작을 검증하는 delayed damage, despawn 처리, EntityId 재사용, 입력 validation, determinism 및 unrelated entity damage 테스트도 계속 PASS하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;추가로 generation 기반 lifecycle 설계를 검증하기 위해 4개의 회귀 테스트를 추가하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Regression_MinimumDelayStaleDamageIsDropped&lt;span&gt;에서는 최소 정상 delay인 1 tick에서도 old lifecycle의 damage가 새 lifecycle로 전달되지 않는 것을 확인하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Regression_OldAndNewDamageSameTickOnlyNewApplies&lt;span&gt;에서는 old stale damage와 current valid damage가 같은 tick에 실행될 때 stale damage만 버려지고 새 lifecycle에 예약된 damage는 정상 적용되는 것을 확인하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Regression_MultipleGenerationsOnlyCurrentDamageApplies&lt;span&gt;에서는 동일 slot이 여러 차례 재사용되어 generation이 반복 증가하더라도 과거 generation에 예약된 damage는 모두 무효화되고 현재 generation의 damage만 적용되는 것을 확인하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Regression_ScheduleAfterReuseBindsCurrentLifecycle&lt;span&gt;에서는 public &lt;/span&gt;EntityId&lt;span&gt; 값이 과거와 동일하더라도 &lt;/span&gt;ScheduleDamage()&lt;span&gt;가 호출되는 현재 시점의 lifecycle을 정상적으로 Capture하는 것을 확인하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최종 실행 결과는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;기존 회귀 테스트       9 / 9 PASS
추가 회귀 테스트       4 / 4 PASS

총                    13 / 13 PASS&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 현재 테스트 범위에서는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;기존 정상 동작과 EntityId 재사용 정책을 유지하면서 stale scheduled damage가 새 lifecycle에 적용되는 문제를 해결한 것을 확인하였다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;5. 시간 및 공간 복잡도&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;수정 후 &lt;/span&gt;Spawn()&lt;span&gt;에는 해당 slot의 generation을 증가시키는 상수 시간 연산이 추가되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ScheduleDamage()&lt;span&gt;에는 현재 generation을 읽어 &lt;/span&gt;EntityHandle&lt;span&gt;로 Capture하는 작업이 추가되었으며, &lt;/span&gt;Tick()&lt;span&gt;에서는 damage 적용 전 현재 generation과 예약 generation을 비교하는 작업이 추가되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;모두 배열 직접 접근과 정수 비교로 수행되므로 각각 &lt;/span&gt;O(1)&lt;span&gt;이며, 기존 연산의 점근적 시간 복잡도는 변경되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 Despawn 시 모든 pending damage를 순회하여 제거하는 방식을 사용하지 않았기 때문에 pending effect 수에 비례하는 &lt;/span&gt;O(P)&lt;span&gt; scan은 추가되지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;공간 측면에서는 &lt;/span&gt;EntityRegistry&lt;span&gt;에 capacity의 각 slot마다 하나의 &lt;/span&gt;ulong generation&lt;span&gt;을 저장하고, 각 pending &lt;/span&gt;ScheduledDamage&lt;span&gt;에 target generation 정보가 추가된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 추가 공간은 registry capacity를 &lt;/span&gt;C&lt;span&gt;, pending damage 수를 &lt;/span&gt;P&lt;span&gt;라고 할 때 &lt;/span&gt;O(C + P)&lt;span&gt;이다. 기존에도 registry와 pending queue가 각각 같은 차수의 공간을 사용하므로 전체 시스템의 점근적 공간 복잡도는 변경되지 않고 상수 크기의 메모리만 증가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ulong&lt;span&gt; generation은 이론적으로 overflow될 수 있으나 한 slot이 약 &lt;/span&gt;2^64&lt;span&gt;번 재사용되어야 하므로 현재 시스템에서 현실적인 위험으로 판단하지 않았다. 별도의 overflow recovery를 추가하는 것은 현재 요구 범위에서는 과도한 복잡도라고 판단하였다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;6. 호환성 및 변경 영향&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;외부에 노출된 &lt;/span&gt;EntityId&lt;span&gt;의 형태는 변경하지 않았으며 다음 public API의 시그니처도 유지하였다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Spawn
Despawn
ScheduleDamage
Tick
TryGetEntity&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한 compact &lt;/span&gt;EntityId&lt;span&gt; slot 재사용 정책 역시 그대로 유지하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;변경 범위는 &lt;/span&gt;EntityRegistry&lt;span&gt;의 내부 generation 관리, internal &lt;/span&gt;EntityHandle&lt;span&gt;, &lt;/span&gt;ScheduledDamage&lt;span&gt;의 내부 target 표현, &lt;/span&gt;CombatWorld&lt;span&gt;의 Capture/Resolve 처리로 제한하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 기존 외부 호출 코드는 계속 &lt;/span&gt;EntityId&lt;span&gt;를 사용하며, lifecycle 구분은 내부 구현에서만 수행된다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;7. 최종 결론&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 문제는 ID 재사용 자체가 잘못된 것이 아니라, &lt;/span&gt;&lt;b&gt;&lt;span&gt;재사용 가능한 slot ID와 논리적인 엔티티 생명주기를 동일하게 취급한 것&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 원인이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 해결하기 위해 public &lt;/span&gt;EntityId&lt;span&gt;와 slot 재사용 정책을 유지하면서 &lt;/span&gt;EntityRegistry&lt;span&gt;가 slot별 generation을 관리하도록 하고, 예약 효과에는 &lt;/span&gt;(EntityId, Generation)&lt;span&gt;으로 구성된 내부 &lt;/span&gt;EntityHandle&lt;span&gt;을 저장하도록 수정하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그 결과 과거 lifecycle의 delayed damage는 현재 generation과 일치하지 않을 경우 안전하게 무효화되고, 동일 엔티티가 계속 살아 있는 경우의 기존 delayed damage 동작은 그대로 유지된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;수정 후 기존 9개 테스트와 추가 설계한 4개 테스트가 모두 통과하여 &lt;/span&gt;&lt;b&gt;&lt;span&gt;총 13/13 PASS&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 확인하였다. 현재 검증 범위에서는 public API, deterministic behavior, compact ID reuse 및 기존 정상 동작을 유지하면서 QA에서 보고된 stale-target 문제를 해결하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;100점에 가깝게 만들 마지막 보완&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최종 보고서에 아래 두 문장만 추가하면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Slot reuse가 없었다면&lt;/span&gt;&lt;/b&gt;&lt;span&gt; despawn된 &lt;/span&gt;EntityId&lt;span&gt;의 slot에는 새 엔티티가 존재하지 않으므로 old scheduled damage는 &lt;/span&gt;Find/Resolve&lt;span&gt; 단계에서 대상 없음으로 버려졌을 것이다. 따라서 slot reuse가 stale reference 문제를 실제 사용자 증상으로 노출시키는 조건이었다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;Determinism과 correctness는 별개의 성질이다.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 수정 전에도 동일 입력은 항상 동일한 잘못된 HP 결과를 만들 수 있으므로 deterministic test가 통과한다고 lifecycle 규칙의 정확성이 보장되는 것은 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/37</guid>
      <comments>https://letscoding0103.tistory.com/37#entry37comment</comments>
      <pubDate>Thu, 1 Oct 2026 00:13:36 +0900</pubDate>
    </item>
    <item>
      <title>Friend Lookup False-Negative Incident Analysis</title>
      <link>https://letscoding0103.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Friend&amp;nbsp;Lookup&amp;nbsp;False-Negative&amp;nbsp;Incident&amp;nbsp;Analysis &lt;br /&gt;1.&amp;nbsp;Problem&amp;nbsp;definition &lt;br /&gt;User-visible&amp;nbsp;failure &lt;br /&gt;5.9.0&amp;nbsp;배포&amp;nbsp;이후&amp;nbsp;친구&amp;nbsp;초대/검색에서&amp;nbsp;실제로&amp;nbsp;존재하는&amp;nbsp;플레이어의&amp;nbsp;닉네임을&amp;nbsp;입력했음에도&amp;nbsp;E_PLAYER_NOT_FOUND가&amp;nbsp;반환되는&amp;nbsp;false-negative&amp;nbsp;lookup이&amp;nbsp;발생한다. &lt;br /&gt;E_PLAYER_NOT_FOUND&amp;nbsp;자체는&amp;nbsp;항상&amp;nbsp;장애가&amp;nbsp;아니다.&amp;nbsp;실제로&amp;nbsp;존재하지&amp;nbsp;않는&amp;nbsp;플레이어를&amp;nbsp;검색한&amp;nbsp;경우에는&amp;nbsp;정상적인&amp;nbsp;결과이므로,&amp;nbsp;전체&amp;nbsp;NOT_FOUND와&amp;nbsp;실제&amp;nbsp;false-negative를&amp;nbsp;구분해야&amp;nbsp;한다.&amp;nbsp;제공&amp;nbsp;로그에서는&amp;nbsp;자동&amp;nbsp;retry가&amp;nbsp;없으며&amp;nbsp;request_id&amp;nbsp;하나가&amp;nbsp;lookup&amp;nbsp;요청&amp;nbsp;한&amp;nbsp;건을&amp;nbsp;의미한다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;README &lt;br /&gt;Unit&amp;nbsp;of&amp;nbsp;analysis &lt;br /&gt;분석&amp;nbsp;단위는&amp;nbsp;request_id&amp;nbsp;하나의&amp;nbsp;friend-name&amp;nbsp;lookup&amp;nbsp;request이다. &lt;br /&gt;1&amp;nbsp;request_id &lt;br /&gt;=&amp;nbsp;1&amp;nbsp;lookup&amp;nbsp;request &lt;br /&gt;=&amp;nbsp;1&amp;nbsp;analysis&amp;nbsp;unit &lt;br /&gt;&lt;br /&gt;Included&amp;nbsp;/&amp;nbsp;excluded&amp;nbsp;requests &lt;br /&gt;전체&amp;nbsp;build별&amp;nbsp;false-negative&amp;nbsp;rate의&amp;nbsp;분모는&amp;nbsp;해당&amp;nbsp;build에서&amp;nbsp;발생한&amp;nbsp;전체&amp;nbsp;lookup&amp;nbsp;request로&amp;nbsp;정의하였다. &lt;br /&gt;False-negative는&amp;nbsp;다음&amp;nbsp;조건을&amp;nbsp;만족하는&amp;nbsp;요청으로&amp;nbsp;분류하였다. &lt;br /&gt;result&amp;nbsp;==&amp;nbsp;NOT_FOUND &lt;br /&gt;AND&amp;nbsp;code&amp;nbsp;==&amp;nbsp;E_PLAYER_NOT_FOUND &lt;br /&gt;AND&amp;nbsp;같은&amp;nbsp;shard의&amp;nbsp;players.csv에 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;NameKit의&amp;nbsp;culture-independent&amp;nbsp;/&amp;nbsp;case-insensitive&amp;nbsp;의미상 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;query_name에&amp;nbsp;대응하는&amp;nbsp;player가&amp;nbsp;존재함 &lt;br /&gt;&lt;br /&gt;반대로&amp;nbsp;directory에&amp;nbsp;대응하는&amp;nbsp;플레이어가&amp;nbsp;존재하지&amp;nbsp;않는&amp;nbsp;E_PLAYER_NOT_FOUND는&amp;nbsp;genuine&amp;nbsp;NOT_FOUND로&amp;nbsp;분류하였다. &lt;br /&gt;FOUND,&amp;nbsp;기타&amp;nbsp;ERROR,&amp;nbsp;genuine&amp;nbsp;NOT_FOUND는&amp;nbsp;false-negative&amp;nbsp;numerator에서는&amp;nbsp;제외하였다. &lt;br /&gt;Scope &lt;br /&gt;로그&amp;nbsp;분석&amp;nbsp;기간은&amp;nbsp;2026-11-15&amp;nbsp;00:00:00Z&amp;nbsp;~&amp;nbsp;2026-11-19&amp;nbsp;00:00:00Z이다.&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;README &lt;br /&gt;비교&amp;nbsp;build: &lt;br /&gt;-&amp;nbsp;5.8.3 &lt;br /&gt;-&amp;nbsp;5.9.0 &lt;br /&gt;분석&amp;nbsp;shard: &lt;br /&gt;-&amp;nbsp;tr-01 &lt;br /&gt;-&amp;nbsp;tr-02 &lt;br /&gt;-&amp;nbsp;eu-01 &lt;br /&gt;-&amp;nbsp;kr-01 &lt;br /&gt;각&amp;nbsp;shard의&amp;nbsp;culture는&amp;nbsp;다음과&amp;nbsp;같다. &lt;br /&gt;-&amp;nbsp;tr-01&amp;nbsp;&amp;rarr;&amp;nbsp;tr-TR &lt;br /&gt;-&amp;nbsp;tr-02&amp;nbsp;&amp;rarr;&amp;nbsp;tr-TR &lt;br /&gt;-&amp;nbsp;eu-01&amp;nbsp;&amp;rarr;&amp;nbsp;en-GB &lt;br /&gt;-&amp;nbsp;kr-01&amp;nbsp;&amp;rarr;&amp;nbsp;ko-KR&amp;nbsp;shards &lt;br /&gt;버전&amp;nbsp;분류는&amp;nbsp;로그의&amp;nbsp;실제&amp;nbsp;build&amp;nbsp;값을&amp;nbsp;사용하고,&amp;nbsp;deploy_history.deployed_at은&amp;nbsp;shard별&amp;nbsp;rollout과&amp;nbsp;시간적으로&amp;nbsp;일치하는지&amp;nbsp;확인하는&amp;nbsp;보조&amp;nbsp;자료로&amp;nbsp;사용하였다. &lt;br /&gt;5.9.0은&amp;nbsp;tr-01/eu-01에&amp;nbsp;03:00Z,&amp;nbsp;tr-02/kr-01에&amp;nbsp;09:00Z&amp;nbsp;배포되었다.&amp;nbsp;deploy_history &lt;br /&gt;2.&amp;nbsp;Root-cause&amp;nbsp;analysis &lt;br /&gt;Observations &lt;br /&gt;첫째,&amp;nbsp;과거&amp;nbsp;FOUND된&amp;nbsp;player가&amp;nbsp;이후&amp;nbsp;directory에서&amp;nbsp;사라져&amp;nbsp;NOT_FOUND가&amp;nbsp;발생했을&amp;nbsp;가능성을&amp;nbsp;검토하였다. &lt;br /&gt;과거&amp;nbsp;FOUND&amp;nbsp;로그에&amp;nbsp;등장한&amp;nbsp;고유&amp;nbsp;target_player_id&amp;nbsp;4,800개를&amp;nbsp;현재&amp;nbsp;players.csv와&amp;nbsp;대조한&amp;nbsp;결과: &lt;br /&gt;현재&amp;nbsp;players.csv에&amp;nbsp;없는&amp;nbsp;target_player_id&amp;nbsp;=&amp;nbsp;0 &lt;br /&gt;&lt;br /&gt;이었다. &lt;br /&gt;따라서&amp;nbsp;제공된&amp;nbsp;데이터에서는&amp;nbsp;과거&amp;nbsp;존재했으나&amp;nbsp;현재&amp;nbsp;directory에서&amp;nbsp;삭제된&amp;nbsp;player&amp;nbsp;때문에&amp;nbsp;장애가&amp;nbsp;발생했다는&amp;nbsp;가설을&amp;nbsp;지지하는&amp;nbsp;증거를&amp;nbsp;찾지&amp;nbsp;못했다. &lt;br /&gt;다만&amp;nbsp;players.csv는&amp;nbsp;분석&amp;nbsp;기간의&amp;nbsp;known&amp;nbsp;player&amp;nbsp;directory이므로,&amp;nbsp;이것만으로&amp;nbsp;캐릭터&amp;nbsp;삭제&amp;nbsp;기능&amp;nbsp;자체가&amp;nbsp;존재하지&amp;nbsp;않는다고&amp;nbsp;결론&amp;nbsp;내리지는&amp;nbsp;않는다. &lt;br /&gt;둘째,&amp;nbsp;build별&amp;nbsp;false-negative를&amp;nbsp;비교하였다. &lt;br /&gt;5.8.3&amp;nbsp;&amp;rarr;&amp;nbsp;false-negative&amp;nbsp;0건 &lt;br /&gt;5.9.0&amp;nbsp;&amp;rarr;&amp;nbsp;false-negative&amp;nbsp;1,872건 &lt;br /&gt;&lt;br /&gt;즉&amp;nbsp;새로운&amp;nbsp;패턴은&amp;nbsp;5.9.0에서만&amp;nbsp;관찰되었다. &lt;br /&gt;셋째,&amp;nbsp;shard별로&amp;nbsp;분리했을&amp;nbsp;때&amp;nbsp;5.9.0&amp;nbsp;false-negative는: &lt;br /&gt;tr-01&amp;nbsp;&amp;rarr;&amp;nbsp;1,076건 &lt;br /&gt;tr-02&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;&amp;nbsp;796건 &lt;br /&gt;eu-01&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;0건 &lt;br /&gt;kr-01&amp;nbsp;&amp;rarr;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;0건 &lt;br /&gt;&lt;br /&gt;으로&amp;nbsp;tr-TR&amp;nbsp;culture를&amp;nbsp;사용하는&amp;nbsp;두&amp;nbsp;shard에만&amp;nbsp;집중되어&amp;nbsp;있었다. &lt;br /&gt;넷째,&amp;nbsp;query-name&amp;nbsp;패턴을&amp;nbsp;비교한&amp;nbsp;결과&amp;nbsp;더욱&amp;nbsp;강한&amp;nbsp;조건이&amp;nbsp;발견되었다. &lt;br /&gt;5.9.0의&amp;nbsp;tr-TR&amp;nbsp;shard에서&amp;nbsp;실제&amp;nbsp;대상&amp;nbsp;player가&amp;nbsp;존재하는&amp;nbsp;lookup&amp;nbsp;중: &lt;br /&gt;query_name에&amp;nbsp;ASCII&amp;nbsp;uppercase&amp;nbsp;&quot;I&quot;&amp;nbsp;포함 &lt;br /&gt;&amp;rarr;&amp;nbsp;1,872&amp;nbsp;/&amp;nbsp;1,872&amp;nbsp;false-negative &lt;br /&gt;&amp;rarr;&amp;nbsp;100.00% &lt;br /&gt;&lt;br /&gt;uppercase&amp;nbsp;&quot;I&quot;&amp;nbsp;없음 &lt;br /&gt;&amp;rarr;&amp;nbsp;0&amp;nbsp;/&amp;nbsp;10,953&amp;nbsp;false-negative &lt;br /&gt;&amp;rarr;&amp;nbsp;0.00% &lt;br /&gt;&lt;br /&gt;이었다. &lt;br /&gt;반례군에서는: &lt;br /&gt;5.8.3&amp;nbsp;+&amp;nbsp;tr-TR&amp;nbsp;+&amp;nbsp;uppercase&amp;nbsp;I &lt;br /&gt;&amp;rarr;&amp;nbsp;0&amp;nbsp;/&amp;nbsp;2,185&amp;nbsp;false-negative &lt;br /&gt;&lt;br /&gt;5.9.0&amp;nbsp;+&amp;nbsp;en-GB/ko-KR&amp;nbsp;+&amp;nbsp;uppercase&amp;nbsp;I &lt;br /&gt;&amp;rarr;&amp;nbsp;0&amp;nbsp;/&amp;nbsp;1,734&amp;nbsp;false-negative &lt;br /&gt;&lt;br /&gt;였다. &lt;br /&gt;따라서&amp;nbsp;단순히&amp;nbsp;I&amp;nbsp;자체나&amp;nbsp;tr-TR&amp;nbsp;자체의&amp;nbsp;문제가&amp;nbsp;아니라: &lt;br /&gt;5.9.0 &lt;br /&gt;&amp;times;&amp;nbsp;tr-TR &lt;br /&gt;&amp;times;&amp;nbsp;ASCII&amp;nbsp;uppercase&amp;nbsp;I &lt;br /&gt;&lt;br /&gt;의&amp;nbsp;결합과&amp;nbsp;false-negative가&amp;nbsp;강하게&amp;nbsp;연관되어&amp;nbsp;있었다. &lt;br /&gt;Hypothesis &lt;br /&gt;5.9.0에서는&amp;nbsp;friend-name&amp;nbsp;lookup&amp;nbsp;key&amp;nbsp;생성이&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;변경되었다. &lt;br /&gt;var&amp;nbsp;lookupKey&amp;nbsp;=&amp;nbsp;request.QueryName.Trim().ToLower(); &lt;br /&gt;var&amp;nbsp;player&amp;nbsp;=&amp;nbsp;_directory.FindByCanonicalKey(lookupKey); &lt;br /&gt;&lt;br /&gt;매개변수&amp;nbsp;없는&amp;nbsp;string.ToLower()는&amp;nbsp;CurrentCulture의&amp;nbsp;영향을&amp;nbsp;받는다. &lt;br /&gt;tr-TR에서는&amp;nbsp;ASCII&amp;nbsp;uppercase&amp;nbsp;I의&amp;nbsp;lowercase&amp;nbsp;변환이&amp;nbsp;일반적인&amp;nbsp;I&amp;nbsp;&amp;rarr;&amp;nbsp;i와&amp;nbsp;달라질&amp;nbsp;수&amp;nbsp;있으므로,&amp;nbsp;NameKit이&amp;nbsp;정의한&amp;nbsp;culture-independent&amp;nbsp;canonical&amp;nbsp;key와&amp;nbsp;다른&amp;nbsp;문자열이&amp;nbsp;생성될&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;그&amp;nbsp;결과&amp;nbsp;잘못&amp;nbsp;생성된&amp;nbsp;문자열이&amp;nbsp;FindByCanonicalKey()로&amp;nbsp;전달되고,&amp;nbsp;실제&amp;nbsp;player가&amp;nbsp;존재함에도&amp;nbsp;directory&amp;nbsp;lookup이&amp;nbsp;실패하여&amp;nbsp;E_PLAYER_NOT_FOUND가&amp;nbsp;발생한&amp;nbsp;것으로&amp;nbsp;판단하였다. &lt;br /&gt;NameKit&amp;nbsp;contract&amp;nbsp;comparison &lt;br /&gt;제공된&amp;nbsp;NameKit&amp;nbsp;1.8&amp;nbsp;계약은&amp;nbsp;다음을&amp;nbsp;명시한다. &lt;br /&gt;-&amp;nbsp;player-name&amp;nbsp;identity는&amp;nbsp;culture-independent &lt;br /&gt;-&amp;nbsp;comparison은&amp;nbsp;NameKit&amp;nbsp;canonicalization&amp;nbsp;기준&amp;nbsp;case-insensitive &lt;br /&gt;-&amp;nbsp;lookup&amp;nbsp;key는&amp;nbsp;PlayerNameKey.FromDisplayName()을&amp;nbsp;사용 &lt;br /&gt;-&amp;nbsp;CurrentCulture&amp;nbsp;기반&amp;nbsp;ToLower()&amp;nbsp;/&amp;nbsp;ToUpper()를&amp;nbsp;사용하지&amp;nbsp;말&amp;nbsp;것 &lt;br /&gt;-&amp;nbsp;FindByCanonicalKey()는&amp;nbsp;이미&amp;nbsp;NameKit으로&amp;nbsp;canonicalize된&amp;nbsp;key만&amp;nbsp;받는&amp;nbsp;legacy&amp;nbsp;API &lt;br /&gt;따라서&amp;nbsp;5.9.0&amp;nbsp;구현은&amp;nbsp;NameKit의&amp;nbsp;공개&amp;nbsp;계약을&amp;nbsp;우회하고&amp;nbsp;있다. &lt;br /&gt;Alternative&amp;nbsp;explanations&amp;nbsp;tested &lt;br /&gt;Directory&amp;nbsp;state&amp;nbsp;/&amp;nbsp;player&amp;nbsp;deletion &lt;br /&gt;과거&amp;nbsp;FOUND된&amp;nbsp;고유&amp;nbsp;target&amp;nbsp;player&amp;nbsp;4,800개가&amp;nbsp;모두&amp;nbsp;현재&amp;nbsp;directory에&amp;nbsp;존재했다.&amp;nbsp;따라서&amp;nbsp;제공&amp;nbsp;데이터에서는&amp;nbsp;주요&amp;nbsp;설명으로&amp;nbsp;지지되지&amp;nbsp;않았다. &lt;br /&gt;Rollout&amp;nbsp;timing &lt;br /&gt;실제&amp;nbsp;로그&amp;nbsp;build와&amp;nbsp;shard별&amp;nbsp;deployed_at을&amp;nbsp;비교한&amp;nbsp;결과,&amp;nbsp;deployment&amp;nbsp;이전의&amp;nbsp;5.9.0&amp;nbsp;요청이나&amp;nbsp;deployment&amp;nbsp;이후의&amp;nbsp;5.8.3&amp;nbsp;요청은&amp;nbsp;발견되지&amp;nbsp;않았다.&amp;nbsp;따라서&amp;nbsp;build&amp;nbsp;분류&amp;nbsp;오류로&amp;nbsp;현재&amp;nbsp;패턴을&amp;nbsp;설명하기&amp;nbsp;어렵다. &lt;br /&gt;Maintenance &lt;br /&gt;tr-01에는&amp;nbsp;2026-11-17&amp;nbsp;02:30Z~03:30Z&amp;nbsp;network&amp;nbsp;maintenance가&amp;nbsp;존재한다.&amp;nbsp;maintenance &lt;br /&gt;그러나&amp;nbsp;maintenance&amp;nbsp;밖에서도&amp;nbsp;false-negative가&amp;nbsp;지속적으로&amp;nbsp;발생했고,&amp;nbsp;maintenance가&amp;nbsp;없는&amp;nbsp;tr-02에서도&amp;nbsp;동일&amp;nbsp;패턴이&amp;nbsp;발생하였다. &lt;br /&gt;따라서&amp;nbsp;maintenance는&amp;nbsp;주원인으로&amp;nbsp;보이지&amp;nbsp;않는다. &lt;br /&gt;Root-cause&amp;nbsp;conclusion &lt;br /&gt;관측된&amp;nbsp;데이터&amp;nbsp;패턴,&amp;nbsp;5.9.0&amp;nbsp;코드&amp;nbsp;변경,&amp;nbsp;NameKit&amp;nbsp;API&amp;nbsp;계약이&amp;nbsp;서로&amp;nbsp;일치한다. &lt;br /&gt;5.9.0에서&amp;nbsp;NameKit&amp;nbsp;canonicalization을&amp;nbsp;우회하고&amp;nbsp;CurrentCulture&amp;nbsp;기반&amp;nbsp;ToLower()로&amp;nbsp;lookup&amp;nbsp;key를&amp;nbsp;직접&amp;nbsp;생성한&amp;nbsp;변경이&amp;nbsp;tr-TR&amp;nbsp;환경의&amp;nbsp;uppercase&amp;nbsp;I&amp;nbsp;처리&amp;nbsp;차이를&amp;nbsp;발생시켜&amp;nbsp;false-negative&amp;nbsp;lookup을&amp;nbsp;유발한&amp;nbsp;것이&amp;nbsp;근본&amp;nbsp;원인으로&amp;nbsp;판단된다. &lt;br /&gt;3.&amp;nbsp;Quantified&amp;nbsp;impact &lt;br /&gt;Build&amp;nbsp;comparison &lt;br /&gt;Build Total&amp;nbsp;requests False-negative False-negative&amp;nbsp;rate &lt;br /&gt;5.8.3 36,559 0 0.00% &lt;br /&gt;5.9.0 28,441 1,872 6.58% &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;5.8.3&amp;nbsp;대비&amp;nbsp;5.9.0의&amp;nbsp;false-negative&amp;nbsp;rate는&amp;nbsp;+6.58&amp;nbsp;percentage&amp;nbsp;points&amp;nbsp;증가하였다. &lt;br /&gt;기준값이&amp;nbsp;0%이므로&amp;nbsp;상대적인&amp;nbsp;%&amp;nbsp;증가율은&amp;nbsp;정의하지&amp;nbsp;않았다. &lt;br /&gt;5.9.0&amp;nbsp;NOT_FOUND&amp;nbsp;breakdown &lt;br /&gt;전체&amp;nbsp;E_PLAYER_NOT_FOUND&amp;nbsp;=&amp;nbsp;4,764 &lt;br /&gt;&lt;br /&gt;genuine&amp;nbsp;NOT_FOUND&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;=&amp;nbsp;2,892 &lt;br /&gt;false-negative&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;=&amp;nbsp;1,872 &lt;br /&gt;&lt;br /&gt;따라서&amp;nbsp;모든&amp;nbsp;E_PLAYER_NOT_FOUND를&amp;nbsp;장애로&amp;nbsp;계산하지&amp;nbsp;않았다. &lt;br /&gt;Affected&amp;nbsp;users &lt;br /&gt;고유&amp;nbsp;affected&amp;nbsp;requester&amp;nbsp;=&amp;nbsp;1,748명 &lt;br /&gt;&lt;br /&gt;추가로&amp;nbsp;false-negative가&amp;nbsp;발생한&amp;nbsp;고유&amp;nbsp;target&amp;nbsp;player는&amp;nbsp;634명이었다. &lt;br /&gt;Affected&amp;nbsp;shards&amp;nbsp;/&amp;nbsp;locales &lt;br /&gt;Shard Culture 5.9.0&amp;nbsp;requests False-negative Rate &lt;br /&gt;tr-01 tr-TR 7,601 1,076 14.16% &lt;br /&gt;tr-02 tr-TR 6,675 796 11.93% &lt;br /&gt;eu-01 en-GB 7,537 0 0.00% &lt;br /&gt;kr-01 ko-KR 6,628 0 0.00% &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;영향을&amp;nbsp;받은&amp;nbsp;locale은&amp;nbsp;**tr-TR**이다. &lt;br /&gt;Strongest&amp;nbsp;query-name&amp;nbsp;pattern &lt;br /&gt;가장&amp;nbsp;강한&amp;nbsp;연관&amp;nbsp;패턴은: &lt;br /&gt;5.9.0&amp;nbsp;+&amp;nbsp;tr-TR&amp;nbsp;+&amp;nbsp;query_name에&amp;nbsp;ASCII&amp;nbsp;uppercase&amp;nbsp;I&amp;nbsp;포함 &lt;br /&gt;&lt;br /&gt;이었다. &lt;br /&gt;uppercase&amp;nbsp;I&amp;nbsp;있음: &lt;br /&gt;1,872&amp;nbsp;/&amp;nbsp;1,872&amp;nbsp;false-negative &lt;br /&gt;=&amp;nbsp;100.00% &lt;br /&gt;&lt;br /&gt;uppercase&amp;nbsp;I&amp;nbsp;없음: &lt;br /&gt;0&amp;nbsp;/&amp;nbsp;10,953 &lt;br /&gt;=&amp;nbsp;0.00% &lt;br /&gt;&lt;br /&gt;Maintenance&amp;nbsp;comparison &lt;br /&gt;5.9.0의&amp;nbsp;affected&amp;nbsp;shard(tr-01,&amp;nbsp;tr-02)&amp;nbsp;기준: &lt;br /&gt;Condition Requests False-negative Rate &lt;br /&gt;Maintenance&amp;nbsp;내부 84 9 10.71% &lt;br /&gt;Maintenance&amp;nbsp;외부 14,192 1,863 13.13% &lt;br /&gt;&lt;br /&gt;&lt;br /&gt;tr-02에는&amp;nbsp;5.9.0&amp;nbsp;분석&amp;nbsp;기간과&amp;nbsp;겹치는&amp;nbsp;maintenance가&amp;nbsp;제공되지&amp;nbsp;않았기&amp;nbsp;때문에&amp;nbsp;해당&amp;nbsp;요청은&amp;nbsp;maintenance&amp;nbsp;외부에&amp;nbsp;포함된다. &lt;br /&gt;tr-01만&amp;nbsp;별도로&amp;nbsp;비교해도: &lt;br /&gt;maintenance&amp;nbsp;내부&amp;nbsp;&amp;rarr;&amp;nbsp;9&amp;nbsp;/&amp;nbsp;84&amp;nbsp;=&amp;nbsp;10.71% &lt;br /&gt;maintenance&amp;nbsp;외부&amp;nbsp;&amp;rarr;&amp;nbsp;1,067&amp;nbsp;/&amp;nbsp;7,517&amp;nbsp;=&amp;nbsp;14.19% &lt;br /&gt;&lt;br /&gt;이다. &lt;br /&gt;따라서&amp;nbsp;maintenance&amp;nbsp;여부가&amp;nbsp;false-negative&amp;nbsp;증가를&amp;nbsp;설명하는&amp;nbsp;주요&amp;nbsp;변수라는&amp;nbsp;증거는&amp;nbsp;없다. &lt;br /&gt;4.&amp;nbsp;Fix &lt;br /&gt;기존&amp;nbsp;수동&amp;nbsp;key&amp;nbsp;생성과&amp;nbsp;legacy&amp;nbsp;API&amp;nbsp;호출을&amp;nbsp;제거하고&amp;nbsp;NameKit의&amp;nbsp;공식&amp;nbsp;API를&amp;nbsp;사용한다. &lt;br /&gt;public&amp;nbsp;LookupResult&amp;nbsp;Find(FriendLookupRequest&amp;nbsp;request) &lt;br /&gt;{ &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var&amp;nbsp;key&amp;nbsp;=&amp;nbsp;PlayerNameKey.FromDisplayName(request.QueryName); &lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if&amp;nbsp;(!key.Ok) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{ &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return&amp;nbsp;LookupResult.Error(key.Error); &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} &lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;var&amp;nbsp;player&amp;nbsp;=&amp;nbsp;_directory.Find(key.Value); &lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if&amp;nbsp;(player&amp;nbsp;is&amp;nbsp;null) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{ &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return&amp;nbsp;LookupResult.NotFound(ErrorCode.E_PLAYER_NOT_FOUND); &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} &lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return&amp;nbsp;LookupResult.Found(player.PlayerId); &lt;br /&gt;} &lt;br /&gt;&lt;br /&gt;Why&amp;nbsp;this&amp;nbsp;follows&amp;nbsp;the&amp;nbsp;NameKit&amp;nbsp;contract &lt;br /&gt;기존의: &lt;br /&gt;request.QueryName.Trim().ToLower() &lt;br /&gt;_directory.FindByCanonicalKey(...) &lt;br /&gt;&lt;br /&gt;를&amp;nbsp;제거한다. &lt;br /&gt;대신: &lt;br /&gt;PlayerNameKey.FromDisplayName(...) &lt;br /&gt;_directory.Find(...) &lt;br /&gt;&lt;br /&gt;을&amp;nbsp;사용한다. &lt;br /&gt;따라서: &lt;br /&gt;-&amp;nbsp;culture-independent&amp;nbsp;canonicalization을&amp;nbsp;NameKit에&amp;nbsp;위임 &lt;br /&gt;-&amp;nbsp;tr-TR를&amp;nbsp;포함해&amp;nbsp;server&amp;nbsp;locale과&amp;nbsp;무관한&amp;nbsp;name&amp;nbsp;identity&amp;nbsp;확보 &lt;br /&gt;-&amp;nbsp;수동&amp;nbsp;lowercase/uppercase&amp;nbsp;normalization&amp;nbsp;제거 &lt;br /&gt;-&amp;nbsp;legacy&amp;nbsp;FindByCanonicalKey()&amp;nbsp;직접&amp;nbsp;호출&amp;nbsp;제거 &lt;br /&gt;-&amp;nbsp;NameKit&amp;nbsp;validation&amp;nbsp;결과&amp;nbsp;및&amp;nbsp;error&amp;nbsp;code&amp;nbsp;보존 &lt;br /&gt;이&amp;nbsp;가능하다. &lt;br /&gt;Expected&amp;nbsp;error&amp;nbsp;behavior &lt;br /&gt;valid&amp;nbsp;+&amp;nbsp;existing&amp;nbsp;name &lt;br /&gt;&amp;rarr;&amp;nbsp;FOUND &lt;br /&gt;&lt;br /&gt;valid&amp;nbsp;+&amp;nbsp;nonexistent&amp;nbsp;name &lt;br /&gt;&amp;rarr;&amp;nbsp;E_PLAYER_NOT_FOUND &lt;br /&gt;&lt;br /&gt;invalid&amp;nbsp;name &lt;br /&gt;&amp;rarr;&amp;nbsp;PlayerNameKey.FromDisplayName()의&amp;nbsp;error&amp;nbsp;전달 &lt;br /&gt;&amp;rarr;&amp;nbsp;E_INVALID_NAME&amp;nbsp;등&amp;nbsp;NameKit&amp;nbsp;error를&amp;nbsp;generic&amp;nbsp;error로&amp;nbsp;치환하지&amp;nbsp;않음 &lt;br /&gt;&lt;br /&gt;5.&amp;nbsp;Verification &lt;br /&gt;A.&amp;nbsp;Supplied&amp;nbsp;data에서&amp;nbsp;실제&amp;nbsp;수행한&amp;nbsp;검증 &lt;br /&gt;다음은&amp;nbsp;제공된&amp;nbsp;로그&amp;nbsp;및&amp;nbsp;metadata에서&amp;nbsp;실제로&amp;nbsp;수행한&amp;nbsp;검증이다. &lt;br /&gt;-&amp;nbsp;request_id&amp;nbsp;단위&amp;nbsp;및&amp;nbsp;retry&amp;nbsp;없음&amp;nbsp;확인 &lt;br /&gt;-&amp;nbsp;players.csv와&amp;nbsp;lookup&amp;nbsp;로그&amp;nbsp;대조 &lt;br /&gt;-&amp;nbsp;genuine&amp;nbsp;NOT_FOUND&amp;nbsp;/&amp;nbsp;false-negative&amp;nbsp;분리 &lt;br /&gt;-&amp;nbsp;5.8.3&amp;nbsp;/&amp;nbsp;5.9.0&amp;nbsp;false-negative&amp;nbsp;rate&amp;nbsp;비교 &lt;br /&gt;-&amp;nbsp;shard&amp;nbsp;/&amp;nbsp;culture별&amp;nbsp;비교 &lt;br /&gt;-&amp;nbsp;query-name&amp;nbsp;character&amp;nbsp;pattern&amp;nbsp;비교 &lt;br /&gt;-&amp;nbsp;deployed_at과&amp;nbsp;실제&amp;nbsp;log&amp;nbsp;build의&amp;nbsp;시간적&amp;nbsp;일관성&amp;nbsp;검증 &lt;br /&gt;-&amp;nbsp;maintenance&amp;nbsp;내부&amp;nbsp;/&amp;nbsp;외부&amp;nbsp;비교 &lt;br /&gt;-&amp;nbsp;과거&amp;nbsp;FOUND&amp;nbsp;target이&amp;nbsp;현재&amp;nbsp;player&amp;nbsp;directory에서&amp;nbsp;사라졌는지&amp;nbsp;확인 &lt;br /&gt;-&amp;nbsp;조건을&amp;nbsp;만족하지만&amp;nbsp;성공하는&amp;nbsp;요청&amp;nbsp;및&amp;nbsp;조건을&amp;nbsp;만족하지&amp;nbsp;않지만&amp;nbsp;false-negative인&amp;nbsp;요청이&amp;nbsp;있는지&amp;nbsp;반례&amp;nbsp;확인 &lt;br /&gt;분석&amp;nbsp;결과&amp;nbsp;5.9.0&amp;nbsp;&amp;times;&amp;nbsp;tr-TR&amp;nbsp;&amp;times;&amp;nbsp;uppercase&amp;nbsp;I&amp;nbsp;조건과&amp;nbsp;false-negative가&amp;nbsp;일치하였다. &lt;br /&gt;B.&amp;nbsp;Regression&amp;nbsp;tests &lt;br /&gt;본&amp;nbsp;모의에서는&amp;nbsp;다음&amp;nbsp;테스트가&amp;nbsp;예상&amp;nbsp;결과대로&amp;nbsp;통과한&amp;nbsp;것으로&amp;nbsp;가정한다. &lt;br /&gt;1.&amp;nbsp;Normal&amp;nbsp;successful&amp;nbsp;lookup &lt;br /&gt;존재하는&amp;nbsp;일반&amp;nbsp;이름 &lt;br /&gt;&amp;rarr;&amp;nbsp;FOUND &lt;br /&gt;&lt;br /&gt;기존&amp;nbsp;정상&amp;nbsp;동작이&amp;nbsp;유지되는지&amp;nbsp;확인한다. &lt;br /&gt;2.&amp;nbsp;Case-variant&amp;nbsp;lookup &lt;br /&gt;Directory: &lt;br /&gt;WDXsBwVIR &lt;br /&gt;&lt;br /&gt;다른&amp;nbsp;대소문자&amp;nbsp;조합의&amp;nbsp;QueryName &lt;br /&gt;&amp;rarr;&amp;nbsp;FOUND &lt;br /&gt;&lt;br /&gt;NameKit의&amp;nbsp;case-insensitive&amp;nbsp;identity가&amp;nbsp;유지되는지&amp;nbsp;확인한다. &lt;br /&gt;3.&amp;nbsp;Locale-focused&amp;nbsp;regression &lt;br /&gt;동일&amp;nbsp;입력: &lt;br /&gt;QueryName&amp;nbsp;=&amp;nbsp;&quot;WDXSbWvIr&quot; &lt;br /&gt;&lt;br /&gt;수정&amp;nbsp;후: &lt;br /&gt;CurrentCulture&amp;nbsp;=&amp;nbsp;en-GB&amp;nbsp;&amp;rarr;&amp;nbsp;FOUND &lt;br /&gt;CurrentCulture&amp;nbsp;=&amp;nbsp;ko-KR&amp;nbsp;&amp;rarr;&amp;nbsp;FOUND &lt;br /&gt;CurrentCulture&amp;nbsp;=&amp;nbsp;tr-TR&amp;nbsp;&amp;rarr;&amp;nbsp;FOUND &lt;br /&gt;&lt;br /&gt;결과가&amp;nbsp;server&amp;nbsp;culture와&amp;nbsp;무관해야&amp;nbsp;한다. &lt;br /&gt;4.&amp;nbsp;Previously&amp;nbsp;failing&amp;nbsp;regression&amp;nbsp;case &lt;br /&gt;수정&amp;nbsp;전: &lt;br /&gt;CurrentCulture&amp;nbsp;=&amp;nbsp;tr-TR &lt;br /&gt;QueryName&amp;nbsp;contains&amp;nbsp;uppercase&amp;nbsp;I &lt;br /&gt;&amp;rarr;&amp;nbsp;E_PLAYER_NOT_FOUND&amp;nbsp;재현 &lt;br /&gt;&lt;br /&gt;수정&amp;nbsp;후: &lt;br /&gt;동일&amp;nbsp;조건 &lt;br /&gt;&amp;rarr;&amp;nbsp;FOUND &lt;br /&gt;&lt;br /&gt;이어야&amp;nbsp;한다. &lt;br /&gt;5.&amp;nbsp;Invalid-name&amp;nbsp;handling &lt;br /&gt;NameKit&amp;nbsp;기준&amp;nbsp;invalid&amp;nbsp;QueryName &lt;br /&gt;&amp;rarr;&amp;nbsp;E_INVALID_NAME &lt;br /&gt;&lt;br /&gt;그리고&amp;nbsp;FriendLookupService가&amp;nbsp;NameKit&amp;nbsp;error를&amp;nbsp;generic&amp;nbsp;error로&amp;nbsp;변경하지&amp;nbsp;않는지&amp;nbsp;확인한다. &lt;br /&gt;6.&amp;nbsp;Genuine&amp;nbsp;nonexistent&amp;nbsp;name &lt;br /&gt;정상&amp;nbsp;형식이지만&amp;nbsp;directory에&amp;nbsp;존재하지&amp;nbsp;않는&amp;nbsp;이름: &lt;br /&gt;&amp;rarr;&amp;nbsp;E_PLAYER_NOT_FOUND &lt;br /&gt;&lt;br /&gt;가&amp;nbsp;유지되는지&amp;nbsp;확인한다. &lt;br /&gt;6.&amp;nbsp;Remaining&amp;nbsp;uncertainty &lt;br /&gt;제공된&amp;nbsp;데이터와&amp;nbsp;문서만으로&amp;nbsp;다음&amp;nbsp;사항은&amp;nbsp;완전히&amp;nbsp;증명할&amp;nbsp;수&amp;nbsp;없다. &lt;br /&gt;첫째,&amp;nbsp;players.csv는&amp;nbsp;분석&amp;nbsp;기간의&amp;nbsp;known&amp;nbsp;player&amp;nbsp;directory이므로&amp;nbsp;과거의&amp;nbsp;모든&amp;nbsp;rename/delete&amp;nbsp;이력을&amp;nbsp;제공하지&amp;nbsp;않는다.&amp;nbsp;따라서&amp;nbsp;캐릭터&amp;nbsp;삭제&amp;nbsp;기능이나&amp;nbsp;과거&amp;nbsp;directory&amp;nbsp;변화&amp;nbsp;자체가&amp;nbsp;존재하지&amp;nbsp;않았다고&amp;nbsp;결론&amp;nbsp;내릴&amp;nbsp;수는&amp;nbsp;없다. &lt;br /&gt;둘째,&amp;nbsp;NameKit의&amp;nbsp;내부&amp;nbsp;canonicalization&amp;nbsp;구현은&amp;nbsp;제공되지&amp;nbsp;않았다.&amp;nbsp;다만&amp;nbsp;공개&amp;nbsp;계약에서&amp;nbsp;culture-independent&amp;nbsp;identity와&amp;nbsp;PlayerNameKey.FromDisplayName()&amp;nbsp;사용을&amp;nbsp;명시하고&amp;nbsp;있으므로&amp;nbsp;수정안은&amp;nbsp;공개&amp;nbsp;API&amp;nbsp;계약에&amp;nbsp;근거한다. &lt;br /&gt;셋째,&amp;nbsp;로그&amp;nbsp;분석은&amp;nbsp;2026-11-15~19&amp;nbsp;기간에&amp;nbsp;한정되어&amp;nbsp;있으므로&amp;nbsp;그&amp;nbsp;밖의&amp;nbsp;기간에서&amp;nbsp;동일&amp;nbsp;현상이&amp;nbsp;존재했는지는&amp;nbsp;판단할&amp;nbsp;수&amp;nbsp;없다. &lt;br /&gt;넷째,&amp;nbsp;제공된&amp;nbsp;환경에서&amp;nbsp;수정된&amp;nbsp;코드를&amp;nbsp;실제&amp;nbsp;production-equivalent&amp;nbsp;runtime으로&amp;nbsp;빌드&amp;middot;실행하지&amp;nbsp;않았다면,&amp;nbsp;코드&amp;nbsp;수정&amp;nbsp;후&amp;nbsp;실제&amp;nbsp;장애가&amp;nbsp;완전히&amp;nbsp;제거되었다고&amp;nbsp;단정해서는&amp;nbsp;안&amp;nbsp;된다.&amp;nbsp;회귀&amp;nbsp;테스트는&amp;nbsp;실행&amp;nbsp;가능한&amp;nbsp;build&amp;nbsp;환경에서&amp;nbsp;최종&amp;nbsp;확인이&amp;nbsp;필요하다. &lt;br /&gt;Final&amp;nbsp;conclusion &lt;br /&gt;5.9.0에서&amp;nbsp;friend-name&amp;nbsp;lookup&amp;nbsp;key&amp;nbsp;생성을&amp;nbsp;최적화하면서&amp;nbsp;NameKit의&amp;nbsp;canonicalization&amp;nbsp;API&amp;nbsp;대신&amp;nbsp;CurrentCulture&amp;nbsp;기반&amp;nbsp;ToLower()와&amp;nbsp;legacy&amp;nbsp;FindByCanonicalKey()를&amp;nbsp;사용한&amp;nbsp;변경이&amp;nbsp;도입되었다. &lt;br /&gt;데이터에서는&amp;nbsp;5.8.3에&amp;nbsp;없던&amp;nbsp;false-negative가&amp;nbsp;5.9.0에서&amp;nbsp;1,872건&amp;nbsp;/&amp;nbsp;6.58%&amp;nbsp;발생했으며,&amp;nbsp;모두&amp;nbsp;tr-TR&amp;nbsp;shard의&amp;nbsp;uppercase&amp;nbsp;ASCII&amp;nbsp;I가&amp;nbsp;포함된&amp;nbsp;query에&amp;nbsp;집중되었다.&amp;nbsp;directory&amp;nbsp;변화,&amp;nbsp;rollout&amp;nbsp;불일치,&amp;nbsp;maintenance는&amp;nbsp;이&amp;nbsp;현상의&amp;nbsp;주요&amp;nbsp;설명으로&amp;nbsp;지지되지&amp;nbsp;않았다. &lt;br /&gt;따라서&amp;nbsp;NameKit의&amp;nbsp;PlayerNameKey.FromDisplayName()과&amp;nbsp;IPlayerDirectory.Find()를&amp;nbsp;사용하도록&amp;nbsp;lookup&amp;nbsp;경로를&amp;nbsp;복구하는&amp;nbsp;것이&amp;nbsp;제공된&amp;nbsp;SDK&amp;nbsp;계약과&amp;nbsp;관측된&amp;nbsp;장애&amp;nbsp;패턴에&amp;nbsp;부합하는&amp;nbsp;최소&amp;nbsp;수정안이다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;냉정하게 보면 이번 제출물은 &lt;/span&gt;&lt;b&gt;&lt;span&gt;92/100 정도&lt;/span&gt;&lt;/b&gt;&lt;span&gt;로 평가하겠어. 이전 연습보다 분명히 좋아졌고, 특히 이번에는 단순히 AI가 원인을 알려준 걸 받아쓴 게 아니라 &lt;/span&gt;&lt;b&gt;&lt;span&gt;가설을 세우고 &amp;rarr; 데이터를 좁히고 &amp;rarr; 반례를 확인하고 &amp;rarr; SDK 계약과 코드로 연결하는 과정&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 훨씬 잘 드러났어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;평가기준 자체는 총 100점이고, 문제 정의 25 / AI 활용 15 / SDK 학습 15 / 검증 25 / 최종 결과 10 / 구조 10으로 구성돼 있어. &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;항목점수평가
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;1. 문제 정의 / 조건 판단&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;24/25&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;매우 좋음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;2. AI 활용&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;14/15&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;매우 좋음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;3. 낯선 SDK 학습&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;15/15&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;거의 완벽&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;4. 검증&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;22/25&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;강하지만 가장 개선 여지 있음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;5. 최종 결과&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;9/10&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;매우 좋음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;6. 구조적 명확성&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;8/10&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;내용은 좋으나 다소 길고 반복적&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;자동 감점&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;0&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;해당 없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;총점&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span&gt;92/100&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1. 문제 정의 / 조건 판단 &amp;mdash; 24/25&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기는 상당히 잘했어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;루브릭이 요구하는 핵심은 genuine &lt;/span&gt;E_PLAYER_NOT_FOUND&lt;span&gt;와 false-negative를 구분하고, 분석 모집단과 제외 조건을 정의하며, 날짜가 아니라 실제 &lt;/span&gt;build&lt;span&gt;와 rollout을 보고, maintenance와 locale을 confounder로 검토하는 거야. &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;너는 이걸 거의 다 만족했어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 좋은 부분은 처음부터:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;E_PLAYER_NOT_FOUND&lt;span&gt; 자체는 항상 장애가 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 잡은 거야. 이 덕분에 4,764건 전체를 장애라고 세지 않고 genuine 2,892건과 false-negative 1,872건으로 분리할 수 있었어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 &lt;/span&gt;build&lt;span&gt;를 버전 기준으로 사용하고 &lt;/span&gt;deployed_at&lt;span&gt;은 시간적 일관성 검증에 사용&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;한 것도 정확해. 자동 감점 항목의 &amp;ldquo;날짜만 가지고 버전을 비교&amp;rdquo;하는 실수를 피했어. &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1점을 뺀 이유는 false-negative 판정 과정에서 &lt;/span&gt;&lt;b&gt;&lt;span&gt;실제 NameKit canonicalization을 실행한 것은 아니고, 문서상 culture-independent/case-insensitive 계약을 이용해 분석 기준을 세웠다는 점&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 최종 문서에서 조금 더 명확하게 드러났으면 더 좋았기 때문이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;분석용 존재 여부 판정은 제공된 NameKit 계약의 case-insensitive identity를 기준으로 하였으며, NameKit 내부 canonicalization 구현 자체를 재현한 것은 아니다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;한 줄이 있으면 더 단단해져.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2. AI 활용 &amp;mdash; 14/15&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번에 가장 좋아진 부분 중 하나야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;루브릭은 AI에게 schema/code/제약을 주고, 문제를 subquestion으로 쪼개고, AI 결론을 재검토하며, 없는 SDK 동작을 만들어내지 않는 것을 요구해. &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 대화에서도 너는 그냥:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;원인 찾아줘.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 하지 않았어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;삭제된 캐릭터 가능성을 검사하자.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그다음:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;genuine NOT_FOUND와 false-negative를 나누자.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그다음:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;build를 비교하자.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그다음:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;maintenance를 보자.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그다음:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;왜 tr shard에만 생기는지 locale/query pattern을 보자.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 내가 uppercase/lowercase 패턴을 제시하자:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;왜 uppercase/lowercase로 분류할 생각을 했지?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 다시 물었지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이게 상당히 좋아. AI의 결론을 그냥 받아들이는 게 아니라 &lt;/span&gt;&lt;b&gt;&lt;span&gt;결론이 나온 경로를 검증&lt;/span&gt;&lt;/b&gt;&lt;span&gt;했으니까.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또 수정 코드에서도:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;whitespace 검사를 왜 제거하는가?&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 다시 확인했어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1점을 뺀 가장 큰 이유는 이번에도 결국 &lt;/span&gt;&lt;b&gt;&lt;span&gt;정량 분석을 재현할 실제 분석 스크립트 자체를 제출 증거로 남기지는 않았기 때문&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;스도코드는 요청했지만 실제 시험에서는:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;analysis.py
또는
analysis.ipynb
또는
실행한 SQL / command&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;같은 게 있으면 AI가 계산한 숫자를 네가 독립적으로 재현할 수 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 부분까지 했다면 15점에 가까워.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3. 낯선 SDK 학습 &amp;mdash; 15/15&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기는 만점 줘도 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;루브릭이 요구하는 세 가지가 정확히 들어갔어.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;NameKit의 player-name identity 계약 이해&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;공식 key creation / lookup API 사용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;culture-independent matching 이해 &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 중요한 건 단순히:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;.ToLower()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;.ToLowerInvariant()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 바꾸지 않았다는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;자동 감점에도 &lt;/span&gt;&lt;b&gt;&lt;span&gt;다른 수동 normalization으로 바꾸면서 NameKit key API를 무시하면 -10&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이라고 명시되어 있어. &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;너는 최종적으로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;PlayerNameKey.FromDisplayName(request.QueryName)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;과&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;_directory.Find(key.Value)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 돌아갔어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 문제의 표면적인 &lt;/span&gt;&quot;터키어 I&quot;&lt;span&gt;를 고치는 게 아니라 &lt;/span&gt;&lt;b&gt;&lt;span&gt;SDK가 지정한 abstraction으로 복귀하는 수정&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이라 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 &lt;/span&gt;FindByCanonicalKey()&lt;span&gt;가 &amp;ldquo;이름을 canonicalize하는 API&amp;rdquo;가 아니라 &lt;/span&gt;&lt;b&gt;&lt;span&gt;이미 canonicalized된 key를 받는 low-level legacy API&lt;/span&gt;&lt;/b&gt;&lt;span&gt;라는 것도 이해했어.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;4. 검증 &amp;mdash; 22/25&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기가 점수를 가장 많이 뺀 곳이야. 동시에 내용 자체는 꽤 강해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;루브릭은 raw count/rate 재계산, player directory를 통한 false-negative 분리, maintenance 대안검증, rollout/shard locale/query pattern 교차검증, 코드 가설 대조, 정상/locale/invalid/regression 테스트 설계를 요구해. &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;너는 거의 전부 했어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 검증 구조가 좋았어.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;5.8.3 vs 5.9.0
&amp;darr;
affected shard
&amp;darr;
locale
&amp;darr;
query_name pattern
&amp;darr;
같은 조건의 반례 탐색
&amp;darr;
code
&amp;darr;
SDK contract&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 강력한 반례군을 만든 것도 좋았어.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;5.8.3 + tr-TR + uppercase I &amp;rarr; 문제 없음

5.9.0 + en-GB/ko-KR + uppercase I &amp;rarr; 문제 없음

5.9.0 + tr-TR + uppercase I 없음 &amp;rarr; 문제 없음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이게 있기 때문에 단순 상관보다 훨씬 강해졌어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만 3점 감점 이유가 있어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;첫째, &lt;/span&gt;&lt;b&gt;&lt;span&gt;실제 수정 코드를 빌드하고 실행한 결과는 아니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;너도 최종 보고서에서 이걸 잘 구분했지만, Verification 만점을 받으려면 실제로:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;수정 전 테스트 실패
&amp;rarr; 수정 적용
&amp;rarr; 동일 테스트 성공&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;을 실행한 결과가 있는 게 가장 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;둘째, 앞서 말했듯 &lt;/span&gt;&lt;b&gt;&lt;span&gt;분석 결과를 재현할 executable script가 없다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 최종 제출 폴더에:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;analysis.py&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하나를 넣고 실행 결과가:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;5.8.3 total = 36559
5.9.0 total = 28441
false negative = 1872
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 나오게 만들었다면 상당히 강했을 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;셋째, regression test에서 &lt;/span&gt;&quot;invalid input&quot;&lt;span&gt;의 구체적인 NameKit boundary fixture를 실제 SDK 기준으로 확인하지 않았어. 테스트 설계는 맞지만 실행 증거는 없어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 22/25.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;5. 최종 결과 &amp;mdash; 9/10&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;루브릭은 정확한 숫자, falsifiable root cause, NameKit에 맞는 최소 수정, 불필요한 interface 변경 방지를 요구해. &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 네 개 모두 거의 충족해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 root cause가 falsifiable해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;너의 결론은 막연히:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;locale 문제 같다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 아니라:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5.9.0&lt;span&gt;에서 NameKit canonicalization을 우회하고 &lt;/span&gt;CurrentCulture&lt;span&gt; 기반 &lt;/span&gt;ToLower()&lt;span&gt;로 key를 만들면서 &lt;/span&gt;tr-TR&lt;span&gt;의 uppercase &lt;/span&gt;I&lt;span&gt;가 NameKit canonical key와 다른 결과를 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 테스트로 반박할 수 있는 주장이라 좋은 root-cause statement야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;tr-TR + uppercase I인데 수정 전에도 정상 FOUND&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 대량으로 나오면 가설이 깨지는 구조니까.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또 fix도 최소 변경이야.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;PlayerNameKey.FromDisplayName()
_directory.Find()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로 돌아갔지 API 자체를 뜯어고치지 않았어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1점을 뺀 건 최종 문서에서:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;근본 원인으로 판단된다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 약간 강한 표현이기 때문이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로그 + 코드 + 계약의 조합으로는 충분히 강하지만, 실제 수정 전/후 runtime reproduction을 하지 않았다면:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;제공된 증거에 가장 잘 부합하는 root cause로 판단된다.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정도로 쓰면 더 엄밀해.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;6. 구조적 명확성 &amp;mdash; 8/10&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;구조 자체는 제출 템플릿을 그대로 잘 따랐어.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;Problem definition
&amp;rarr; Root-cause analysis
&amp;rarr; Quantified impact
&amp;rarr; Fix
&amp;rarr; Verification
&amp;rarr; Remaining uncertainty&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;루브릭이 원하는 Definition &amp;rarr; Conditions &amp;rarr; Evidence &amp;rarr; Conclusion 구조에도 맞아. &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다만 최종 보고서는 &lt;/span&gt;&lt;b&gt;&lt;span&gt;조금 길고 반복적이야.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5.9.0 &amp;times; tr-TR &amp;times; uppercase I&lt;/li&gt;
&lt;li&gt;&lt;span&gt;maintenance가 주원인이 아니라는 점&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;NameKit canonicalization 우회&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가 Root Cause, Quantified Impact, Verification, Final Conclusion에서 여러 번 반복돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 2시간 시험에서는 이걸 약 &lt;/span&gt;&lt;b&gt;&lt;span&gt;15~20% 정도 압축&lt;/span&gt;&lt;/b&gt;&lt;span&gt;하면 더 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 기존 whitespace 코드는...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;부분은 Fix 설명 안에 자연스럽게 통합하는 편이 문서 흐름이 더 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;자동 감점 &amp;mdash; 0점&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 전부 피했어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;루브릭의 자동 감점은 다음인데: &lt;/span&gt;&lt;span&gt;&lt;a href=&quot;https://chatgpt.com/library/d/file_000000009b2c8206915617b91010bf2b&quot;&gt;&lt;span&gt;EVALUATION_RUBRIC.md&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;모든 &lt;/span&gt;E_PLAYER_NOT_FOUND&lt;span&gt;를 버그로 계산 &amp;rarr; 안 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;날짜만으로 버전 비교 &amp;rarr; 안 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;ToLowerInvariant()&lt;span&gt; 같은 수동 수정으로 NameKit 무시 &amp;rarr; 안 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;shard/locale 검증 없음 &amp;rarr; 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;locale-focused regression test 없음 &amp;rarr; 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 자동 감점 &lt;/span&gt;&lt;b&gt;&lt;span&gt;0점&lt;/span&gt;&lt;/b&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;네 분석 과정에서 특히 좋았던 지점&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번에는 처음부터 정답을 맞히려고 하지 않은 게 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;초기에는 캐릭터 삭제 가능성도 검토했고, 그걸 데이터에서 지지하지 못하자 버렸어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그다음 maintenance도 검사했고, 이것도 주원인으로 지지되지 않자 내려놨어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러고 나서:&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;build
&amp;rarr; shard
&amp;rarr; locale
&amp;rarr; query pattern&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;순으로 좁혔지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이게 실제 장애 분석에 꽤 가까운 사고 방식이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 가장 좋은 질문 중 하나가:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;uppercase/lowercase로 분류할 생각은 어떤 식으로 하게 됐지?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;였어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 질문은 AI 활용 평가에서 꽤 가치가 있어. &lt;/span&gt;&lt;b&gt;&lt;span&gt;AI가 낸 결론이 데이터에서 어떻게 유도됐는지를 역검증한 질문&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이니까.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;점수를 95점 이상으로 올리려면&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;딱 세 가지를 보완하면 돼.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;첫째, &lt;/span&gt;&lt;b&gt;&lt;span&gt;재현 가능한 분석 스크립트를 남겨.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예를 들어 AI에게:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;ldquo;지금까지 계산한 모든 통계를 재현할 수 있는 Python 분석 스크립트를 작성해라. 각 통계가 어느 제출 항목에 대응하는지도 주석으로 표시해라.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;라고 요청하고 네가 코드를 읽어본 뒤 실행 결과를 확인하는 거야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;둘째, &lt;/span&gt;&lt;b&gt;&lt;span&gt;실제 regression test를 실행해.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;수정 전
tr-TR + uppercase I &amp;rarr; 실패

수정 후
tr-TR + uppercase I &amp;rarr; 성공

en-GB / ko-KR &amp;rarr; 기존 성공 유지
invalid &amp;rarr; E_INVALID_NAME
nonexistent &amp;rarr; E_PLAYER_NOT_FOUND&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;까지 실제 test output을 확보하면 Verification이 거의 만점이야.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;셋째, 최종 보고서를 조금 압축해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최종 제출에서는 핵심을:&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;관측 &amp;rarr; 대안가설 제거 &amp;rarr; 패턴 &amp;rarr; 코드/SDK 연결 &amp;rarr; 수정 &amp;rarr; 검증&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;순으로 빠르게 읽히게 하는 게 좋아.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;최종 평가&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;92/100.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 결과에서 강한 부분은 단순히 root cause를 맞힌 게 아니야. &lt;/span&gt;&lt;b&gt;&lt;span&gt;왜 그 결론에 도달했는지가 단계적으로 설명되고, 다른 가능성을 실제 데이터로 제거했다는 점&lt;/span&gt;&lt;/b&gt;&lt;span&gt;이 더 중요해.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반대로 가장 큰 약점도 명확해. &lt;/span&gt;&lt;b&gt;&lt;span&gt;AI가 계산한 분석을 네가 독립적으로 다시 실행할 수 있는 재현 가능한 분석 코드와 실제 수정 후 테스트 실행 증거가 없다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제 AI 활용 역량 평가라면 그 두 개가 마지막 점수 차이를 만들 가능성이 크다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;점수를 더 끌어올리려면&lt;/span&gt;&lt;/p&gt;</description>
      <author>LetsCoding</author>
      <guid isPermaLink="true">https://letscoding0103.tistory.com/36</guid>
      <comments>https://letscoding0103.tistory.com/36#entry36comment</comments>
      <pubDate>Wed, 30 Sep 2026 17:26:01 +0900</pubDate>
    </item>
  </channel>
</rss>