<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>나만의 작은 도서관</title>
    <link>https://pledge24.tistory.com/</link>
    <description>세월이 흐르면서  내 머리 속 기억이 오래가지 않는다는 게 느껴지더라구요... </description>
    <language>ko</language>
    <pubDate>Wed, 5 Aug 2026 21:02:34 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>pledge24</managingEditor>
    <image>
      <title>나만의 작은 도서관</title>
      <url>https://tistory1.daumcdn.net/tistory/6345417/attach/99f748eb57594832a9ad8b720d6e9dc5</url>
      <link>https://pledge24.tistory.com</link>
    </image>
    <item>
      <title>[언리얼] 라이브 코딩(Live Coding)이란?</title>
      <link>https://pledge24.tistory.com/625</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;라이브 코딩(Live Coding)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브 코딩은 &lt;b&gt;언리얼 에디터를 켠 상태&lt;/b&gt;에서 C++ 코드를 컴파일하고 그 &lt;b&gt;결과를 에디터에 즉시 반영하는 기능&lt;/b&gt;이다. UE5에선 기본적으로 활성화되어 있으며, Ctrl + Alt + F11 단축키를 누르거나 에디터 창에서 오른쪽 아래에 위치한 아이콘을 눌러 실행한다.(아래 사진 참고)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nMUy4/dJMcajoCyrG/WHHWYnHzw1ReBtfmCFEE8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nMUy4/dJMcajoCyrG/WHHWYnHzw1ReBtfmCFEE8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nMUy4/dJMcajoCyrG/WHHWYnHzw1ReBtfmCFEE8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnMUy4%2FdJMcajoCyrG%2FWHHWYnHzw1ReBtfmCFEE8k%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;759&quot; height=&quot;440&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;기존의 문제점 - 소스 코드를 적용하려면 에디터를 껐다 켜야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJb6l0/dJMcagS34ZJ/Llu9R4uKT7z6Xh83wkXPt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJb6l0/dJMcagS34ZJ/Llu9R4uKT7z6Xh83wkXPt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJb6l0/dJMcagS34ZJ/Llu9R4uKT7z6Xh83wkXPt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJb6l0%2FdJMcagS34ZJ%2FLlu9R4uKT7z6Xh83wkXPt0%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;791&quot; height=&quot;416&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&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 style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;언리얼 에디터 &amp;ldquo;프로세스&amp;rdquo;가 DLL(동적 링크 라이브러리) 파일로 컴파일된 게임 코드(&amp;ldquo;UE5Editor-MyGame.dll&amp;rdquo;같은)를 자신의 프로세스 메모리에 올린 상태로 돌아가기 때문.&lt;/b&gt; &lt;/span&gt;게임 DLL이 에디터 프로세스 메모리 공간에서 돌아가고 있는데 이걸 갱신 또는 교체하려고 하면 OS가 막기 때문에(윈도에서 프로세스가 DLL을 로드하는 순간 &amp;ldquo;사용 중인 파일&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;문제점을 해결한 기술 &amp;ldquo;핫 리로드(Hot Reload)&amp;rdquo; - 하지만 하자가 있는&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/camFdT/dJMcadPuYBG/h0nvJekOAqbajRYkNcVPs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/camFdT/dJMcadPuYBG/h0nvJekOAqbajRYkNcVPs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/camFdT/dJMcadPuYBG/h0nvJekOAqbajRYkNcVPs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcamFdT%2FdJMcadPuYBG%2Fh0nvJekOAqbajRYkNcVPs0%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;687&quot; height=&quot;516&quot; data-origin-width=&quot;687&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&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;이 문제점을 해결하기 위해 UE4에 등장한 기술이 바로 &lt;b&gt;&quot;핫 리로드(Hot Reload)&quot;&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;핫 리로드는 이름과 달리 기존 DLL을 갱신하는 방식은 아니고, &lt;b&gt;새 DLL 파일을 &amp;ldquo;추가로&amp;rdquo; 로드하는 방식&lt;/b&gt;이다. 예를 들어 기존 DLL 파일의 이름이 &amp;ldquo;UE4Editor-MyGame-1234.dll&amp;rdquo;이었다면 새 DLL 파일을 &amp;ldquo;UE4Editor-MyGame-5678.dll&amp;rdquo;와 같이 넘버링만 달리하여 에디터 메모리에 추가로 올리는 것이다. 그런 다음 엔진이 들고 있던 클래스 참조들을 새 DLL 쪽으로 갈아 끼워서 마치 리로드 된 것처럼 작동하는 것이다.&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 style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;부작용이 많았다.&lt;/b&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;핫 리로드는 엔진과 살아있는 객체들이 기존 DLL 내부를 깊게 참조하고 있는 상황에서 이미 생성된 객체들의 클래스 정보를 런타임에 새 버전으로 연결하려고 한다. 그런데 이때 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;전부 연결하지 못하고 일부분만 연결되어 어떤 건 기존 DLL을, 어떤 건 새 DLL을 가리키는 어정쩡한 상태가 되면서 1) 변경이 반영이 안 되거나, 2) 에셋이 깨지거나, 3) 심하면 파일이 오염되는 문제가 발생&lt;/b&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;게다가 핫 리로드는 dll이 계속 쌓이는 불편함도 있다. 기존 DLL 파일은 지워지지 않고 그대로 남아있기 때문에 시간이 좀 지난 뒤 DLL 폴더를 들어가면 쌓인 DLL 파일로만 몇십 GB를 차지하고 있는 꼬락서니도 볼 수 있다.&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;이렇듯 핫 리로드 방식은 굉장히 문제가 많았기 때문에 결국 UE5에 오면서 deprecated 되었다.&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;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;그래서 등장한 기술 &amp;ldquo;라이브 코딩(Live Coding)&amp;rdquo;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXiCEb/dJMcabRHa7o/vphzngdcTLKKYuxJKKiWu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXiCEb/dJMcabRHa7o/vphzngdcTLKKYuxJKKiWu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXiCEb/dJMcabRHa7o/vphzngdcTLKKYuxJKKiWu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXiCEb%2FdJMcabRHa7o%2FvphzngdcTLKKYuxJKKiWu1%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;844&quot; height=&quot;424&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;424&quot;/&gt;&lt;/span&gt;&lt;/figure&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;UE5에서 핫 리로드가 deprecated 되면서 대체재로 나온 기술이 바로 &lt;b&gt;&amp;ldquo;라이브 코딩(Live Coding)&amp;rdquo;&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;라이브 코딩은 DLL을 교체하지 않는다.&lt;/b&gt; 대신 바뀐 부분이 있다면 해당 부분만 컴파일해서 메모리에 덧붙인 다음, 기존 DLL에 있던 원래 함수의 맨 앞에 &amp;ldquo;이제부터는 저기로 가리&amp;rdquo;는 점프(jump) 명령 한 줄을 심어둔다. 이렇게 하면 기존 DLL을 계속 사용하면서 바뀐 내용을 그대로 반영할 수 있다.&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;이런 작동 방식은 기존 핫 리로드에서 발생했던 대부분의 문제를 해결해 준다. 핫 리로드에서 발생했던 문제의 대부분이 새 DLL로 교체할 때 참조가 제대로 교체되지 않았던 것이 원인이었는데, 라이브 코딩은 DLL 교체 작업 자체가 없기 때문에 함수 주소가 바뀌거나 클래스 레이아웃이 밀리거나 객체가 깨지는 등의 문제는 발생하지 않는다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4UvOJ/dJMcafT97jt/yHGOdT4UTZmceVLtv17znk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4UvOJ/dJMcafT97jt/yHGOdT4UTZmceVLtv17znk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4UvOJ/dJMcafT97jt/yHGOdT4UTZmceVLtv17znk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4UvOJ%2FdJMcafT97jt%2FyHGOdT4UTZmceVLtv17znk%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;832&quot; height=&quot;394&quot; data-origin-width=&quot;832&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;라이브 코딩의 한계&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째, 구조 변경 시 사용이 불가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브 코딩은 함수 본문을 변경하는 건 잘 되지만, 구조를 변경했을 땐 작동하지 않는다. 멤버 변수를 새로 추가하거나, UPROPERTY 매크로 내부를 바꾸거나, 새 함수를 선언하는 등 헤더 파일 자체를 수정하면 라이브 코딩으로 커버할 수 없다는 것이다. 이때는 어쩔 수 없이 기존 방식처럼 에디터를 껐다가 다시 켜야 한다.(세상 답답하지 않을 수 없다)&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 style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;라이브 코딩은 그저 &amp;ldquo;함수 구현부에 대한 주소를 리다이렉팅&amp;rdquo; 할 뿐, 기존 DLL을 그대로 사용하기 때문이다.&lt;/b&gt;&lt;/span&gt; 기존 DLL을 사용하는 이상 &amp;ldquo;이미 만들어진 객체의 크기와 구조&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;b&gt;둘째, 디버그가 이상해진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 한계에서 언급했듯 라이브 코딩은 기존 DLL 위에서 돌아간다. 여기서 패치를 덧대어 리다이렉팅을 하는 방식으로 돌아가는데 이런 방식이 디버거를 붙인 상태에서 어긋나는 모습을 보여준다. 인라인 함수, 템플릿처럼 컴파일 타임에 코드가 펼쳐지는 경우 특히 문제가 생긴다고 한다.&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;라이브 코딩 사용 시 워크플로우는 어떻게?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;849&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckAwxl/dJMcaglcmlX/CTrQTvNGP3mq0w4ysqLxtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckAwxl/dJMcaglcmlX/CTrQTvNGP3mq0w4ysqLxtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckAwxl/dJMcaglcmlX/CTrQTvNGP3mq0w4ysqLxtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckAwxl%2FdJMcaglcmlX%2FCTrQTvNGP3mq0w4ysqLxtk%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;849&quot; height=&quot;504&quot; data-origin-width=&quot;849&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&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;hellip; 그냥 작업 속도가 빠르다) 쓰긴 할 텐데 정확히 어떤 식으로 써야 할지 좀 헷갈린다. 그래서 찾아보니 위와 같은 워크플로우로 진행하면 된다고 한다.&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;왼쪽 경로, 즉 cpp 파일에서 함수 본문만 고친 경우 라이브 코딩을 사용하면 된다. 게임플레이 로직, 수치 조정, 리플렉션 함수가 아닌 것들만 수정했다면 에디터를 끄지 않고 라이브 코딩으로 에디터에 반영시킬 수 있다. 구조만 잡히면 대부분의 반복 작업이 이 cpp를 바꾸는데 시간을 쓰기 때문에 이런 작업의 경우 라이브 코딩이 빛을 발한다고 할 수 있다.&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;오른쪽 경로, 즉 헤더를 건드렸거나 리플렉션과 관련된 요소(UPROPERTY 멤버 추가, UFUNCTION 시그니처 변경 등)를 바꾼 경우, 에디터를 끄고 컴파일해야 한다. 컴파일의 경우 IDE에서 해도 되고, 에디터에게 맡겨도 되는데(컴파일 안 한 상태로 다시 키면 재빌드 할 거냐고 뜬다) IDE에서 컴파일하는 게 더 좋은 습관이기 때문에 웬만하면 IDE로 컴파일하는 게 좋다.&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;결론 및 요약&lt;/b&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;/li&gt;
&lt;li&gt;핫 리로드라는 비슷한 역할을 하는 기능도 있는데 UE5로 넘어온 지금 라이브 코딩이 권장된다.&lt;/li&gt;
&lt;li&gt;라이브 코딩은 함수 구현부를 수정할 때만 유효하다. 헤더 파일을 수정했다면 에디터를 껐다 켜자.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Unreal Engine</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/625</guid>
      <comments>https://pledge24.tistory.com/625#entry625comment</comments>
      <pubDate>Thu, 14 May 2026 23:07:19 +0900</pubDate>
    </item>
    <item>
      <title>[Network] TCP 더 자세히 알아보기</title>
      <link>https://pledge24.tistory.com/624</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;이전 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글은 아래 링크의 확장입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pledge24.tistory.com/286&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://pledge24.tistory.com/286&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778684059694&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Network] TCP vs UDP&quot; data-og-description=&quot;목차TCP, TCP 통신 과정UDPTCP와 UDP 작동 비교 예제TCP와 UDP 비교표 신뢰할 수 있는 전송 프로토콜: TCP TCP(Transmission Control Protocol)는 호스트 간 접속과 송수신의 신뢰성이 보장되는 환경에서 통신하기 &quot; data-og-host=&quot;pledge24.tistory.com&quot; data-og-source-url=&quot;https://pledge24.tistory.com/286&quot; data-og-url=&quot;https://pledge24.tistory.com/286&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AsmJc/dJMb8SpMR4o/RKfeV4lGZyx3O3a3iPiel0/img.jpg?width=800&amp;amp;height=546&amp;amp;face=0_0_800_546,https://scrap.kakaocdn.net/dn/bisjsH/dJMb8UHT4Tc/0i4nU0znHhFO9kuShiBnL0/img.jpg?width=800&amp;amp;height=546&amp;amp;face=0_0_800_546,https://scrap.kakaocdn.net/dn/bCTWL2/dJMb8U8Yv0I/Z50JFVON5mxCoyZUVDfq9K/img.png?width=3334&amp;amp;height=1221&amp;amp;face=0_0_3334_1221&quot;&gt;&lt;a href=&quot;https://pledge24.tistory.com/286&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://pledge24.tistory.com/286&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AsmJc/dJMb8SpMR4o/RKfeV4lGZyx3O3a3iPiel0/img.jpg?width=800&amp;amp;height=546&amp;amp;face=0_0_800_546,https://scrap.kakaocdn.net/dn/bisjsH/dJMb8UHT4Tc/0i4nU0znHhFO9kuShiBnL0/img.jpg?width=800&amp;amp;height=546&amp;amp;face=0_0_800_546,https://scrap.kakaocdn.net/dn/bCTWL2/dJMb8U8Yv0I/Z50JFVON5mxCoyZUVDfq9K/img.png?width=3334&amp;amp;height=1221&amp;amp;face=0_0_3334_1221');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Network] TCP vs UDP&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;목차TCP, TCP 통신 과정UDPTCP와 UDP 작동 비교 예제TCP와 UDP 비교표 신뢰할 수 있는 전송 프로토콜: TCP TCP(Transmission Control Protocol)는 호스트 간 접속과 송수신의 신뢰성이 보장되는 환경에서 통신하기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;pledge24.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;순서 보장을 위한 TCP의 시퀀스 번호(SEQ)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 데이터의 전송 순서를 보장하는 기능이 있다. 네트워크를 타고 오면서 순서가 뒤섞여 버려도 수신 측에서 기존 순서대로 재정렬할 수 있어야 한다는 것이다.&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;그렇다면 TCP는 재정렬을 어떻게 하는 걸까?&lt;/b&gt; 바로 헤더에 적힌 시퀀스 번호(이하 SEQ)를 활용하는 것이다. 보내는 쪽의 TCP가 시퀀스 번호가 증가하는 방향으로 세그먼트를 만든다면, 받는 쪽의 TCP가 각 세그먼트에 적힌 시퀀스 번호를 기준으로 오름차순 정렬만 하면 전송 순서를 맞출 수 있게 된다.&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;SEQ(Sequence Number) 필드 값 결정 원리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zxUpZ/dJMcahEnlz7/iDxjehzRGJc5kjpKf0FU2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zxUpZ/dJMcahEnlz7/iDxjehzRGJc5kjpKf0FU2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zxUpZ/dJMcahEnlz7/iDxjehzRGJc5kjpKf0FU2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzxUpZ%2FdJMcahEnlz7%2FiDxjehzRGJc5kjpKf0FU2K%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;594&quot; height=&quot;351&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;351&quot;/&gt;&lt;/span&gt;&lt;/figure&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;시퀀스 번호는 1씩 증가?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시퀀스 번호는 TCP 헤더 중 Sequence Number라는 32bit 크기의 필드에 저장된다. 송신 측에서 각 세그먼트의 Sequence Number 필드에 값을 설정하면, 수신 측에서 이를 읽어 정렬하는 것이다. 그렇다면 시퀀스 번호는 세그먼트를 만들 때마다 1씩 증가시키는 걸까?&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;그렇지 않다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 SEQ값에 좀 더 &lt;b&gt;&amp;ldquo;의미 있는&lt;/b&gt;&amp;rdquo; 값을 넣기 위해 0, 1, 2, &amp;hellip; 처럼 단순히 1씩 증가시키는 방식을 사용하지 않는다. SEQ는 &lt;b&gt;바이트 오프셋&lt;/b&gt;으로, 해당 호스트가 보낸 바이트 중 몇 번째 바이트인지에 대한 정보를 담고 있다. 예를 들어, 호스트 A가 전송한 세그먼트가 SEQ=401이라면, 해당 세그먼트는 &amp;ldquo;호스트 A가 전송한 401번째 바이트로 시작하는 페이로드를 담고 있음&quot;을 의미하게 된다. (이 설명은 ISN를 0으로 설정한 설명으로, 실제랑은 약간 다름. ISN에 대한 내용은 뒤에 나옴)&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;들쭉날쭉한 SEQ 증가량&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 각 세그먼트의 SEQ 증가량은 이전 세그먼트의 페이로드 크기에 영향을 받아 들쭉날쭉 할 수도 있다. 물론 TCP는 MSS(Maximum Segment Size, 이더넷 환경에서는 보통 1460바이트)를 초과하는 크기의 페이로드를 만들 수 없기 때문에 말도 안 되게 들쭉날쭉한 경우는 생기지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTKL9G/dJMcafGB65z/2zTvkMIy15ETMIgJkrXnC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTKL9G/dJMcafGB65z/2zTvkMIy15ETMIgJkrXnC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTKL9G/dJMcafGB65z/2zTvkMIy15ETMIgJkrXnC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTKL9G%2FdJMcafGB65z%2F2zTvkMIy15ETMIgJkrXnC1%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;802&quot; height=&quot;382&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;페이로드 길이에 대한 정보는 왜 TCP 헤더에 없을까?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 헤더를 자세히 보면 이상하게도 페이로드 길이에 대한 정보가 없다. 길이를 알아야 페이로드를 추출할 텐데 왜 길이 정보가 없는 걸까? 그 이유는 IP 헤더의 TotalLength 필드로 페이로드 길이를 구할 수 있기 때문. TCP는 IP 위에서 동작하는 프로토콜이기 때문에 수신 측에선 항상 IP 헤더의 필드인 TotalLength 값을 얻을 수 있다. 그렇게 얻은 값을 통해 페이로드 길이를 계산하면 된다. 계산 방식은 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ynWnn/dJMcadIGthw/06Ten5iknkRKMmL9bdsCNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ynWnn/dJMcadIGthw/06Ten5iknkRKMmL9bdsCNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ynWnn/dJMcadIGthw/06Ten5iknkRKMmL9bdsCNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FynWnn%2FdJMcadIGthw%2F06Ten5iknkRKMmL9bdsCNk%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;735&quot; height=&quot;340&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;두 호스트는 SEQ를 공유할까? 그렇지 않다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;하나의 TCP로 연결된 두 호스트는 SEQ도 공유하지 않을까?&amp;rsquo; 하고 생각했었다. 그런데 그렇지는 않았다. &lt;span style=&quot;color: #ef6f53;&quot;&gt;&lt;b&gt;TCP 연결은 사실 하나의 양방향 스트림이 아닌 두 개의 단방향 스트림이 합쳐진 형태로 봐야 한다고 한다.&lt;/b&gt;&lt;/span&gt; 그래서 &lt;b&gt;하나의 TCP안에 (호스트 A &amp;rarr; 호스트 B) 방향의 단방향 스트림과 (호스트 B &amp;rarr; 호스트 A) 방향의 단방향 스트림이 별도로 존재하며 각각이 독립적인 통신을 한다.&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;&lt;b&gt;따라서 SEQ도 독립적으로 적용된다.&lt;/b&gt; 만약 호스트 A가 일방적으로 데이터를 보냈다면, 호스트 B의 SEQ는 증가하지 않고, 호스트 A의 SEQ만 잔뜩 증가되어 있을 거라는 것이다.&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;ISN(Initial Sequence Number)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 SEQ는 ISN이라는 초기값이 존재한다. 즉, 데이터를 전송하지 않은 초기 시점에 SEQ값이 0이 아닐 수 있다는 것이다. ISN이 결정되는 시점은 서로 연결하는 시점인 3-way handshake이며, 클라이언트가 SYN 패킷에 자신의 ISN을 실어 보내고, 서버가 SYN-ACK에 자신의 ISN을 실어 보내는 식으로 ISN 정보를 공유한다. 예시는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y355t/dJMcaa6no0f/b3JgA8r3riicDs81wQ7K21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y355t/dJMcaa6no0f/b3JgA8r3riicDs81wQ7K21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y355t/dJMcaa6no0f/b3JgA8r3riicDs81wQ7K21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy355t%2FdJMcaa6no0f%2Fb3JgA8r3riicDs81wQ7K21%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;793&quot; height=&quot;621&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;/figure&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;ISN가 0 또는 1에서 난수값으로 바뀐 이유?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날에는 ISN의 값을 0 또는 1로 설정해 두었다. 그런데 이게&lt;b&gt; Sequence Number Prediction Attack&lt;/b&gt;이라는 공격을 가능하게 했기 때문에 RFC 6528 이후로는 0 또는 1 대신, 해시 기반의 난수를 ISN으로 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 나온 Sequence Number Prediction Attack에 대해 잠깐 설명하자면, 공격자(Attacker)가 SEQ를 추측해서 가짜 세그먼트를 끼워 넣는 방식을 말한다. 모든 TCP의 시퀀스 번호가 0 또는 1로 시작하니 대충 때려 맞추는 게 가능하다는 아이디어에서 나온 공격 방식이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;받았음을 알리는 TCP의 ACK&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 전송한 데이터가 반드시 도착함을 보장하는 프로토콜이다. 이를 위해 TCP는 받은 데이터가 있으면 반드시 보낸 대상에게 &lt;b&gt;&amp;lsquo;받았음을 알리는 ACK&amp;rsquo;&lt;/b&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;ACK(Acknowledgement Number) 필드 값 결정 원리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zxUpZ/dJMcahEnlz7/iDxjehzRGJc5kjpKf0FU2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zxUpZ/dJMcahEnlz7/iDxjehzRGJc5kjpKf0FU2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zxUpZ/dJMcahEnlz7/iDxjehzRGJc5kjpKf0FU2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzxUpZ%2FdJMcahEnlz7%2FiDxjehzRGJc5kjpKf0FU2K%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;594&quot; height=&quot;351&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;351&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SEQ와 동일하게 ACK도 TCP 헤더에 존재하는데, Acknowledgement Number(이하 ACK)라는 32bit 크기의 필드에 저장되어 있다. 데이터 수신 측에서 ACK 필드에 값을 설정해 전송하면, 데이터 송신 측에서 이를 읽어 전송한 데이터가 정상적으로 수신되었음을 인지하게 된다. 그렇다면 ACK 값은 어떻게 결정되는 걸까?&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 style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;TCP에서 ACK의 필드 값은 다음에 받아야 할 바이트 오프셋으로 설정된다.&lt;/b&gt;&lt;/span&gt; 예를 들어, 데이터 수신 측이 데이터 송신 측에게 보내는 세그먼트가 ACK=501이라면, &lt;b&gt;&amp;ldquo;500번째 바이트까지 잘 받았으니, 501번째 바이트부터 달라&amp;rdquo;&lt;/b&gt;는 의미가 된다. (SEQ 단어를 활용해서 다시 말하면 &amp;ldquo;SEQ=500까지 수신완료. 다음 SEQ=501.&amp;rdquo;이 된다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/df8qnh/dJMcaaFjUjH/mQkqxAZMssSE7XSokFJvp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/df8qnh/dJMcaaFjUjH/mQkqxAZMssSE7XSokFJvp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/df8qnh/dJMcaaFjUjH/mQkqxAZMssSE7XSokFJvp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdf8qnh%2FdJMcaaFjUjH%2FmQkqxAZMssSE7XSokFJvp1%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;758&quot; height=&quot;497&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 최적화를 위한 ACK 활용 방식 변화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ACK를 기다리지 않겠다 - 슬라이딩 윈도우(Sliding Window)&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;477&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Alcl9/dJMcafGB65N/O1kHkivoKvtmh3FDkuklYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Alcl9/dJMcafGB65N/O1kHkivoKvtmh3FDkuklYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Alcl9/dJMcafGB65N/O1kHkivoKvtmh3FDkuklYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAlcl9%2FdJMcafGB65N%2FO1kHkivoKvtmh3FDkuklYk%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;781&quot; height=&quot;477&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;477&quot;/&gt;&lt;/span&gt;&lt;/figure&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;TCP의 naive 한 모델인 Stop-and-wait(위 그림에서 왼쪽)는 전송한 데이터의 ACK가 나에게 도착할 때까지 다음 데이터를 보내지 않고 기다리는 방식이다. 이 방식은 전송한 세그먼트 개수 : ACK 개수 = 1 : 1이며, 송신 측 입장에서 RTT만큼 기다려야 다음 데이터를 전송할 수 있기 때문에 RTT가 길어지면 대부분의 시간을 ACK를 기다리는 시간으로 채워지면서 처리량(throughput)이 낮아진다는 문제가 있다. 예를 들어 패킷 전송 시간이 1ms, RTT(Round Trip Time)가 100ms라면, 1ms동안 보내고, 99ms를 그냥 놀게 되는 꼴이다.&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;이 문제의 근본적인 원인은 ACK를 기다리는 데에 있기 때문에 문제를 해결하려면 ACK를 기다리지 않고 세그먼트를 연달아 전송하는 방식이 필요했다. 그래서 나온 방식이 바로 &lt;b&gt;&amp;ldquo;슬라이딩 윈도우&amp;rdquo;&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;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;슬라이딩 윈도우 ?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg0SFy/dJMcaaZzNxs/ffH1XXW5Fbc56cDhylnRM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg0SFy/dJMcaaZzNxs/ffH1XXW5Fbc56cDhylnRM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg0SFy/dJMcaaZzNxs/ffH1XXW5Fbc56cDhylnRM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg0SFy%2FdJMcaaZzNxs%2FffH1XXW5Fbc56cDhylnRM0%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;660&quot; height=&quot;336&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&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;슬라이딩 윈도우의 작동 방식은 간단하다. ACK 대기 없이 전송할 수 있는 최대 바이트 개수인 &amp;ldquo;윈도우&amp;rdquo;를 설정하고, ACK를 받아 수신이 확정된 바이트만큼 윈도우를 오른쪽으로 슬라이딩하는 것이다. SEQ와 ACK는 바이트 오프셋을 사용한다고 했다. 슬라이딩 윈도우도 이를 활용하는 것이고, 예를 보여주면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;1284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdRHPF/dJMcabc5pyT/hzZYcNeNoZQKni2gzPK75K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdRHPF/dJMcabc5pyT/hzZYcNeNoZQKni2gzPK75K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdRHPF/dJMcabc5pyT/hzZYcNeNoZQKni2gzPK75K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdRHPF%2FdJMcabc5pyT%2FhzZYcNeNoZQKni2gzPK75K%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;725&quot; height=&quot;1024&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;1284&quot;/&gt;&lt;/span&gt;&lt;/figure&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;슬라이딩 윈도우 적용으로 인한 처리량 극대화&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리량 극대화는 슬라이딩 윈도우를 사용하는 가장 큰 이유이자 가장 본질적인 장점이다. Stop-and-wait 모델이 ACK를 받을 때까지 걸리는 RTT 시간 동안 기다릴 수밖에 없는 반면, 슬라이딩 윈도우는 BDP(Bandwidth-Delay Product, 일반적으로 대역폭 &amp;times; RTT) 이상으로 윈도우 크기를 맞춰놓고 데이터를 전송하면 네트워크 파이프를 가득 채워 전송할 수 있게 되는, 즉, 처리량을 극대화시킬 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgI29O/dJMcabc5pza/fVoaNbIIW18mlYkgk8juNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgI29O/dJMcabc5pza/fVoaNbIIW18mlYkgk8juNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgI29O/dJMcabc5pza/fVoaNbIIW18mlYkgk8juNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgI29O%2FdJMcabc5pza%2FfVoaNbIIW18mlYkgk8juNK%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;853&quot; height=&quot;706&quot; data-origin-width=&quot;853&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;유실에 강한 ACK - 누적 ACK(Cumulative ACK)&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdslDE/dJMcabEb5pI/xdRoqg5JkoZ749144j1z10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdslDE/dJMcabEb5pI/xdRoqg5JkoZ749144j1z10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdslDE/dJMcabEb5pI/xdRoqg5JkoZ749144j1z10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdslDE%2FdJMcabEb5pI%2FxdRoqg5JkoZ749144j1z10%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;813&quot; height=&quot;507&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 슬라이딩 윈도우 방식을 사용한 TCP 통신에서 발생한 상황이다. 송신 측 호스트가 Seg 1, 2, 3, 4를 연달아 보냈고, 수신 측 호스트가 수신 완료에 대한 사실을 알리기 위해 ACK 1, 2, 3, 4를 보냈지만 ACK 1, 2가 네트워크에서 유실된 상태. 이 상황에서 TCP는 어떻게 복원해야 할까?&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;기존의 방식 vs 누적 ACK&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 방식이었다면 송신측은 ACK 1, 2가 도착하지 않았기 때문에 Seg 1, 2가 정상적으로 도착하지 않았다고 인지한다.(Seg X가 정상적으로 도착했음을 알리는 역할이 ACK X니까) 그래서 실제론 Seg 1, 2가 수신측에 정상적으로 도착했음에도 불구하고 송신 측에서 이를 알 턱이 없기 때문에 Seg 1, 2를 재전송하게 된다.&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;그럼 누적 ACK라는 건 뭔가 다를까? 일단 &lt;b&gt;누적 ACK&lt;/b&gt;이란 &amp;ldquo;특정 세그먼트를 정상적으로 수신했음&amp;rdquo;을 알리는 기존 방식과 달리 &lt;b&gt;&amp;ldquo;N번째 바이트까지 정상 수신. N+1번째 바이트 수신 필요&amp;rdquo;를 알리는 방식&lt;/b&gt;이다. 예를 들어, 누적 ACK 방식에서 세그먼트가 ACK=401이라면, &lt;b&gt;&amp;ldquo;400번째 바이트까지 전부 수신 완료. 이제 401번 바이트부터 받아야 한다.&amp;rdquo;&lt;/b&gt;는 의미가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황에 대입하면 ACK 1, 2가 네트워크 상에서 유실되었음에도 ACK 4인 ACK=401 하나가 &amp;ldquo;Seg 1~4 정상 수신&amp;rdquo;이라는 사실을 한꺼번에 증명하게 되는데, 이는 앞서 ACK 1,2가 유실되었음에도 Seg 1~4의 정상 수신 했음을 알리는 결과를 얻게 된다.&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;누적 ACK의 장점들&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwnbGf/dJMcadooNbS/ZcQJ9hfc9QqWjI4w9MlPt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwnbGf/dJMcadooNbS/ZcQJ9hfc9QqWjI4w9MlPt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwnbGf/dJMcadooNbS/ZcQJ9hfc9QqWjI4w9MlPt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwnbGf%2FdJMcadooNbS%2FZcQJ9hfc9QqWjI4w9MlPt1%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;795&quot; height=&quot;240&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&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;누적 ACK를 사용함으로써 얻는 장점을 정리해 보면 크게 3가지 정도 있다.&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;첫째, ACK 유실에 강하다는 점이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황에서 보았듯, ACK도 결국 IP 패킷이라 네트워크 상에서 손실될 수 있다. 누적 ACK를 사용하면 뒤쪽 ACK가 앞쪽 ACK의 결과를 전부 커버하기 때문에 네트워크 상에서 ACK 몇 개가 사라져도 제대로 &amp;ldquo;정상 수신 했음&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;b&gt;둘째, ACK 트래픽을 감소시키기 용이하다는 점이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'앞쪽 ACK가 유실되어도 뒤쪽 ACK가 전달되기만 하면 된다'는 건 일부러 ACK를 빼먹고 안 보내도 된다는 말이 된다. 실제로 TCP는 &amp;lsquo;delayed ACK&amp;rsquo;라고 모든 세그먼트마다 ACK를 보내지 않고 모아서 한 번만 보내는 방식을 사용하여 ACK 트래픽을 줄인다. (생각보다 많이 모아서 보내진 않았다. RFC 1122/5681를 기준으로 최대 2개 세그먼트마다 1번의 ACK라고 하니 )&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;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식의 경우, ACK 하나가 세그먼트 하나의 수신 완료 정보를 가지고 있었기 때문에 &amp;lsquo;Seg X는 수신 완료 되었는가?&amp;rsquo;에 대한 정보를 저장한 비트맵이 필요했다. 반면 누적 ACK의 경우, 단순하게 수신한 ACK값 하나만 있으면 되기 때문에 비트맵 같은 건 사용하지 않아도 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;누적 ACK의 약점을 보완한 두 가지 메커니즘 - 빠른 재전송(Fast Retransmit), SACK(Selected ACK)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누적 ACK에 대한 장점을 늘어놓자마자 약점을 이야기하는 건 좀 그렇지만 누적 ACK은 &amp;ldquo;중간이 비면 약하다&amp;rdquo;는 약점이 있다. 이 약점을 보완하기 위해 등장한 메커니즘이 &lt;b&gt;빠른 재전송&lt;/b&gt;과 &lt;b&gt;SACK&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;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;빠른 재전송(Fast Retransmit)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNuyX7/dJMcai4kajd/VQTVkoPtr7zo09kTM4moZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNuyX7/dJMcai4kajd/VQTVkoPtr7zo09kTM4moZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNuyX7/dJMcai4kajd/VQTVkoPtr7zo09kTM4moZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNuyX7%2FdJMcai4kajd%2FVQTVkoPtr7zo09kTM4moZk%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;766&quot; height=&quot;576&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;빠른 재전송이 나타나게 된 배경&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황은 delayed ACK를 사용하지 않는 누적 ACK가 적용된 TCP 통신 시나리오이다. delayed ACK를 사용하지 않아 수신 측이 각 세그먼트마다 ACK를 보내고 있는데, Seg 2의 유실로 Seg 3, Seg 4, Seg 5의 응답으로 ACK=101을 보내고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누적 ACK는 ACK 유실에 강한 거지 Seg 유실에 강한 건 아니기 때문에 Seg가 유실되면 기존 지침대로 RTO 타임아웃에 의해 재전송이 이루어진다.&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;문제는 RTO 타임아웃 시간이 RTT의 평균과 분산을 고려하여 보수적으로 결정되기 때문에 수백 ms ~ 수 초라는 굉장히 긴 시간으로 설정되어 재전송이 한참 뒤에 실행된다는 것이다. 그래서 온전히 RTO에 맡겨 재전송을 하게 되면 고작 패킷 하나가 손실되었다는 이유로 연결 전체가 수백 ms씩 멈춰버리게 된다. 이를 해결하기 위해 타임아웃보다 빠르게 Seg 유실을 알아챌 수 있는 다른 방식이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1jHzM/dJMcadPt2tt/iElhnS9Q8HkzQb3IsKiMv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1jHzM/dJMcadPt2tt/iElhnS9Q8HkzQb3IsKiMv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1jHzM/dJMcadPt2tt/iElhnS9Q8HkzQb3IsKiMv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1jHzM%2FdJMcadPt2tt%2FiElhnS9Q8HkzQb3IsKiMv0%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;802&quot; height=&quot;312&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ldquo;빠른 재전송(Fast Retransmit)&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;b&gt;빠른 재전송은 중복 ACK가 일정 횟수 이상, 즉, 중복 ACK가 3번 더 도착했을 때 재전송을 해버리는 메커니즘이다.&lt;/b&gt; 위 상황을 기준으로 하면, ACK 101이 Seg 2 유실 이후 중복 ACK가 3번 도착하는데, 중복 ACK가 1, 2번 더 도착할 때는 재전송을 하지 않고 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;정확히 3번 더 도착했을 때 재전송을 해버리는 것이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 여기서 &lt;b&gt;&amp;ldquo;왜 하필 3번 더냐?&amp;rdquo;&lt;/b&gt; 의문이 들 수 있는데, 2개로는 부족하기 때문이다. 네트워크 상에서 패킷이 뒤섞인 채로 도착하는 일은 흔하다. 그래서 유실이 아니라 단순히 늦게 도착하는 경우일 수도 있는데 중복 ACK가 1~2개 더 도착했다는 이유로 재전송을 하면 멀쩡한 데이터를 또 보내는 꼴이 되어버리기 때문이다. 그래서 &lt;b&gt;&amp;ldquo;뒤섞여 도착한 경우&amp;rdquo;&lt;/b&gt;와 &lt;b&gt;&amp;ldquo;손실&amp;rdquo;&lt;/b&gt;을 구분하기 위해 통계적으로, 그리고 경험적으로 결정된 타협점이 바로 3번이었던 것이다.(RFC 5681에 명시된 규칙)&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;결론적으로, 3번의 중복 ACK를 받은 경우 어지간한 상황에서 Seg 유실인 상황이며, 이때 해당 Seg를 재전송하면 기존 RTO 기반 재전송 방식보다 훨씬 빠르게 재전송되기 때문에 &amp;ldquo;빠른 재전송&amp;rdquo;은 누적 ACK의 &amp;ldquo;패킷 손실을 늦게 알아챈다&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;SACK(Selective ACK)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;397&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YHBmR/dJMcaciKXN5/HRdQFFiAEv13l9jUvnYng1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YHBmR/dJMcaciKXN5/HRdQFFiAEv13l9jUvnYng1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YHBmR/dJMcaciKXN5/HRdQFFiAEv13l9jUvnYng1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYHBmR%2FdJMcaciKXN5%2FHRdQFFiAEv13l9jUvnYng1%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;820&quot; height=&quot;397&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;397&quot;/&gt;&lt;/span&gt;&lt;/figure&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;빠른 재전송이 누적 ACK의 &amp;ldquo;패킷 손실을 늦게 알아챈다&amp;rdquo;는 약점을 보완해 준다면, SACK는 &amp;ldquo;어떤 패킷을 손실했는지 정확히 모른다&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;위 그림과 같이 Seg 2, 4가 동시에 유실되고, Seg 1, 3, 5, 6이 도착했다고 가정해 보자. 수신자는 Seg 1, 3, 5, 6에 대한 ACK를 송신자에게 전송해야 하는데 Seg 2가 유실되었기 때문에 Seg 3, 5, 6이 도착했음에도 불구하고 보낼 수 있는 정보는 ACK=101 하나뿐이다. 이 세그먼트를 송신자가 여러 번 받아봤자 Seg 3, 5, 6이 정상적으로 도착했음을 알 수 있는 방법은 없다는 것이다. 그래서 송신자는 어쩔 수 없이 Seg 2, 3, 4, 5, 6을 전부 재전송(Go-Back-N)하게 된다.&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;SACK는 이 문제, 그러니까 &lt;b&gt;&amp;ldquo;유실이 발생한 세그먼트 뒤에 정상적으로 수신한 세그먼트를 알 수 없어 받은 세그먼트를 송신자가 재전송하는 문제&amp;rdquo;&lt;/b&gt;를 해결한다. 방식은 TCP 헤더의 옵션 필드를 활용해서 이 정보를 추가로 알려주는 방식이다. ACK 필드는 그대로 두되, 옵션 영역에 &lt;b&gt;&amp;ldquo;이미 받아둔 비연속 구간들의 좌표&amp;rdquo;&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;SACK 방식의 TCP 헤더를 송신자가 수신하게 되면 송신자는 즉시 &lt;b&gt;&quot;101-200과 301-400, 이 두 구간만 재전송하면 된다&quot;고&lt;/b&gt;&amp;nbsp;추론할 수 있다. 누적 ACK만 있었을 때는 &quot;101 이후를 전부 다시 보낸다(Go-Back-N)&quot;는 보수적 선택을 해야 했지만, SACK 덕분에 &lt;b&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;결론적으로 SACK는 중간 세그먼트가 유실된 상황에서 수신자가 이미 받은 세그먼트에 대한 정보를 TCP 헤더 옵션 필드에 추가함으로써 &amp;ldquo;어떤 패킷을 손실했는지 정확히 모른다&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;SACK 유의사항&lt;/b&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;이러한 SACK에 대해 몇 가지 짚어둘 디테일이 있다.&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;첫 번째로 SACK는 3-way handshake에서 합의가 되어야 사용할 수 있다는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SACK는 3-way handshake의 SYN 패킷에 SACK-Permitted라는 옵션을 실어 보내고, 양쪽 모두 동의해야 사용가능한 상태로 되기 때문에 통신 도중 갑작스레 SACK 방식으로 쓰겠다고 해봤자 허용되지 않는다.&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;두 번째로 양쪽 OS가 모두 SACK를 지원해야 한다는 것이다 .&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SACK는 한쪽이라도 OS가 지원하지 않는다면 연결은 누적 ACK로만 동작하게 된다. 물론 요즘 OS는 기본으로 활성화되어 있긴 하다.&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;세 번째로 SACK 블록은 최대 4개로 제한된다는 것이다 .&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 옵션 필드는 무한정 늘어나지 않고 총길이가 40바이트로 제한되어 있다. 수신한 세그먼트 정보를 담는 SACK 블록 하나당 8바이트를 차지하므로 SACK 블록만으로 채우면 8*5 = 40이므로 5개를 넣을 수 있는 공간인데, 실제로는 4개가 최대이다.(RFC 2018 기준) 다만 실무에서는 보통 Timestamps 옵션(10바이트 + 2바이트 패딩 = 12바이트)을 같이 쓰기 때문에 Timestamps를 사용하면 40 - 12 = 28바이트가 남고, 여기서 SACK 헤더 2바이트와 블록당 8바이트임을 고려하면 (28 - 2) / 8 = 3개가 실질적 한계이다.&lt;/p&gt;</description>
      <category>Common/CS-네트워크</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/624</guid>
      <comments>https://pledge24.tistory.com/624#entry624comment</comments>
      <pubDate>Wed, 13 May 2026 23:52:36 +0900</pubDate>
    </item>
    <item>
      <title>[C++] 가끔 C 배열을 쓰고 싶을 때를 위한 글</title>
      <link>https://pledge24.tistory.com/623</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;나는 vector가 있는데 왜 불편한 C 배열을 쓰고 싶어 할까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;vector라는 좋은 게 있는데 왜 C 배열을 쓰고 싶어지는 이유는 크게 두 가지 정도 있는 것 같다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;첫 번째는 C 배열이 vector보다 가독성이 높다는 점&lt;/b&gt;이다. 개인적으로 vector&amp;lt;int&amp;gt;와 같이 적는 것보단 int arr[5]와 같이 적는 게 훨씬 눈에 잘 들어왔다. 그래서 vector &amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;와 같이 꺾쇠 지옥에 시달리다 보면 int arr[5][5]와 같은 문법이 그리워지곤 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;두 번째는 고정된 크기임을 명시하고 싶을 때&lt;/b&gt;이다. vector는 기본적으로 크기 조절을 막을 수 있는 방법이 없다. 그래서 고정된 크기로 쓸 vector는 계속 머릿속으로 &lt;b&gt;&amp;lsquo;이 vector는 고정 크기다&amp;rsquo;&lt;/b&gt;라는 생각을 하고 있어야 되는데 이게 너무 귀찮았다. 물론 std::array를 쓰면 해결되는 문제이다. 그럼에도 C 배열을 쓰고 싶어 하는 건 std::array도 std::vector와 똑같이 첫 번째 이유에 걸리기 때문이다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;hellip;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 내용들은 혹여나 C 배열을 쓰게 될 때 참고하기 위해 작성해 둔 내용이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배열 초기화&lt;/h3&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;int arr[5];		// -858993460 -858993460 -858993460 -858993460 -858993460
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;std::vector와 달리 &lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;C 배열을 초기화를 하지 않았을 때 0이 아닌 쓰레기 값이 들어있으므로&lt;/span&gt;&lt;/b&gt;(단, 전역 배열과 정적 배열은 제외), 반드시 초기화해줘야 한다.&lt;/li&gt;
&lt;li&gt;C++에서 C 배열을 초기화하는 방법은 다양하다. C++로 올라오면서 C++ 방식으로 초기화하는 방식이 생겼기 때문. 우선 C 방식부터 정리하자면 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C 방식으로 배열 초기화하기&lt;/h3&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;/b&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;/li&gt;
&lt;li&gt;중괄호 안에 넣은 초기값의 개수에 따라 다르게 동작하는데, 정리하면 아래와 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초기값 0개(안 넣음)&lt;/b&gt;: C++에서 새로 생긴 방식으로, 전부 0으로 초기화한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;초기값 1개&lt;/b&gt;: 전부 넣은 초기값으로 초기화한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;초기값 2개 이상&lt;/b&gt;: 해당 값들로 초기화. 남는 자리는 0으로 초기화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;// 1. 전부 같은 값으로 초기화
int arr[5] = {};  // 0 0 0 0 0

// 2. 초기값이 1개 -&amp;gt; 해당 값으로 전부 초기화
int arr[5] = {0}; // 0 0 0 0 0

// 3. 초기값이 2개 이상 -&amp;gt; 부분 초기화 + 나머지는 0으로
int arr[5] = {1, 2}; // 1 2 0 0 0

// 추가. 딱 하나만 초기값을 넣고 싶다면,
// 어쩔 수 없이 아래와 같이 작성해야한다.
int arr[5] = {};
arr[0] = 3;&lt;/code&gt;&lt;/pre&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;b&gt;두 번째 방법. memset 함수&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C에는 memXXX 시리즈가 있다. 이 시리즈 중 &lt;b&gt;memset은 지정한 범위 내의 메모리의 값을 설정하는 함수로&lt;/b&gt;, 배열의 선언과 초기화를 따로 해야 하는 경우(즉, 첫 번째 방법을 사용할 수 없는 경우) 사용한다.&lt;/li&gt;
&lt;li&gt;memset은 &lt;b&gt;&amp;ldquo;memory.h&amp;rdquo;, &amp;ldquo;string.h&amp;rdquo;&lt;/b&gt; 등 여러 헤더 중 하나를 include 하여 사용할 수 있는데 &lt;b&gt;C++ 에선 &amp;lt;cstring&amp;gt; 헤더를 추가하여 사용하는 것을 권장한다&lt;/b&gt;고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;cstring&amp;gt;
int arr[5];
memset(arr, 0, sizeof(arr)); // 0 0 0 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C++ 방식으로 초기화하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C++ 방식으로 초기화하고 싶다면, std::fill을 사용하면 된다. std::fill &amp;lt;algorithm&amp;gt; 헤더에 있으며, 사용 방법은 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;int arr[5];

// 전체를 0으로 초기화
std::fill(std::begin(arr), std::end(arr), 0);

// [0, 1) 범위를 7로 초기화
std::fill(std::begin(arr), std::begin(arr) + 1, 7);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드를 보면 std::begin이랑 std::end 함수를 사용하여 배열 포인터를 반복자(iterator)로 바꿔주는 작업이 있는데, 이는 어디까지나 C++ 방식에 맞추기 위한 거고, C 배열의 경우 (연속된 메모리 컨테이너이기 때문에) 반복자로 안 바꿔줘도 똑같이 작동한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;#include &amp;lt;algorithm&amp;gt;

int arr[5];
// [0, 1) 범위를 7로 초기화
std::fill(arr, arr + 1, 7);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C 배열은 인자로 전달하면 포인터로 퇴화(decay)한다.&lt;/h3&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;/b&gt; std::vector나 std::array처럼 STL 컨테이너들은 함수로 전달할 때 기존과 동일한 데이터를 가지고 있는 반면, &lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;C 배열은 넘겨지는 순간 포인터로 퇴화하면서 배열의 크기 정보를 잃어버린다.&lt;/span&gt;&lt;/b&gt; 즉, 내가 아무리 배열 타입으로 매개변수 타입을 설정해도, 배열의 크기를 함수 내에서 알 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void func1(int arr[])
{
    // int arr[]로 타입을 설정했음에도 실제론 int* 포인터라,
    // arr의 크기가 5인지 알 수 없다.
    // sizeof(arr)은 포인터의 크기만 반환한다.
}

void func2(int arr[5])
{
    // 5와 같이 적어줘도 소용없다. 
    // 5는 주석과 같은 역할일 뿐, arr의 크기가
    // 5임을 넘겨주진 않는다.
    // func1과 동일하게 int* 포인터를 받을 뿐이다.
}

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    func1(arr);
    func2(arr);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그래서 C 배열을 함수로 넘겨주는 경우, 배열의 크기에 대한 매개변수를 따로 전달해줘야 한다. &lt;b&gt;윈도우 라이브러리에서 배열 크기를 따로 보내주는 이유도 이에 기반한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void func3(int arr[], int N)
{
}

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int N = sizeof(arr) / sizeof(int);
    func3(arr, N);
}&lt;/code&gt;&lt;/pre&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;참조(&amp;amp;)를 사용하면 퇴화하지 않는다.&lt;/b&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void func(int (&amp;amp;arr)[5]) {
    // 이제 sizeof(arr)는 20(4*5)바이트가 나온다.
    std::cout &amp;lt;&amp;lt; &quot;참조 사용 시 sizeof(arr): &quot; &amp;lt;&amp;lt; sizeof(arr) &amp;lt;&amp;lt; std::endl;
}
&lt;/code&gt;&lt;/pre&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;템플릿 함수의 경우 배열 크기가 자동 추론된다.&lt;/b&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template &amp;lt;std::size_t N&amp;gt;
void printArray(int (&amp;amp;arr)[N]) { // N이 자동으로 결정됨
    // N이 5로 자동 추론됨.
    for(int i = 0; i &amp;lt; N; ++i) {
        std::cout &amp;lt;&amp;lt; arr[i] &amp;lt;&amp;lt; &quot; &quot;;
    }
}

int arr[] = {1, 2, 3, 4, 5};
printArray(arr);
&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C 배열은 복사 대입이 안된다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C++에서 해왔던 대로 vector &amp;lt;int&amp;gt; v1 = v2; 와 같은 복사 대임을 C 배열에도 적용하면 컴파일 에러가 난다. 이유는 명확하게도 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;C 배열에는 복사 대입 기능을 제공하지 않기 때문&lt;/b&gt;&lt;/span&gt;. 그래서 아래와 같은 코드는 오류가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = arr; // 복사를 기대했지만 돌아온건 컴파일 에러다.
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C 배열 초기화 시 반드시 중괄호가 우변에 와야 하며, 만약 초기화가 끝난 시점에서 &lt;b&gt;arr2 = arr1;과 같이 작성한다 해도 복사가 아닌 포인터 대입으로 취급&lt;/b&gt;되고, &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;이렇게 취급된 포인터 대입 시도마저 메모리상 고정된 위치를 가리켜야 할 C 배열 변수에 주소를 변경하려는 구문이 되므로 컴파일 에러가 발생한다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int arr1[5] = {1, 2, 3, 4, 5};
int arr2[5] = {0, 0, 0, 0, 0};

arr2 = arr1; // 컴파일 에러: &quot;array type 'int [5]' is not assignable&quot;
&lt;/code&gt;&lt;/pre&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;b&gt;그럼 C 배열 복사는 어떻게 해야 할까?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;memcpy를 사용하면 된다. memcpy는 이름 그대로 메모리를 복사하는 함수로, 아래와 같이 사용하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;cstring&amp;gt;
// arr1의 내용을 arr2에 복사.
std::memcpy(arr2, arr1, sizeof(arr1));
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C++ 방식으로 처리하고 싶다면 std::copy를 사용해도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;#include &amp;lt;algorithm&amp;gt;
// arr1의 내용을 arr2에 복사.
std::copy(std::begin(arr1), std::end(arr1), std::begin(arr2));
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>C++/문법 및 메소드(STL)</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/623</guid>
      <comments>https://pledge24.tistory.com/623#entry623comment</comments>
      <pubDate>Mon, 12 Jan 2026 17:30:32 +0900</pubDate>
    </item>
    <item>
      <title>[C++][STL Algorithm] vector 부분 복사에 유용한 std::copy(feat. inserter 계열 클래스)</title>
      <link>https://pledge24.tistory.com/622</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;std::copy란?&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;template &amp;lt;class InputIt, class OutputIt&amp;gt;
OutputIt copy(InputIt first, InputIt last, OutputIt d_first);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;std::copy란 지정한 범위의 메모리를 복사하여 다른 위치에 붙여 넣는 함수로,&lt;/b&gt; 3개의 iterator를 인자로 받으며 [first, last) 범위의 메모리를 복사하여 d_first부터 붙여 넣는다.&lt;/li&gt;
&lt;li&gt;다른 STL Algorithm 함수들과 동일하게 &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;last 위치는 범위에 포함시키지 않기 때문에&lt;/b&gt;&lt;/span&gt; std::copy의 last 위치의 메모리가 복사 범위에 들어가지 않음을 유의해야 한다.&lt;/li&gt;
&lt;li&gt;std::copy를 사용할 수 있는 범위가 굉장히 넓지만, 제목에서 알 수 있듯 &lt;b&gt;자주 사용되는 상황은 vector 부분 복사&lt;/b&gt;이다. 예제는 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; v1 = {1, 2, 3, 4, 5};

int first = 1;
int last = 3;

vector&amp;lt;int&amp;gt; v2(last-first+1);
std::copy(v1.begin()+first, v1.begin()+last+1, v2.begin());

for(const int&amp;amp; elem : v2)
{
	cout &amp;lt;&amp;lt; elem &amp;lt;&amp;lt; ' ';
}
cout &amp;lt;&amp;lt; '\\n';

// 실행 결과
2 3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;std::copy는 반복자(iterator) 기능이 있는 모든 클래스에서 사용 가능하다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;std::copy는 컨테이너에 종속된 함수가 아니라 iterator를 중점으로 로직이 돌아가기 때문에 iterator 기능을 넣어놓은 클래스라면 사용이 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;STL의 컨테이너(단, queue, stack과 같은 컨테이너 어댑터는 제외)들은 iterator를 지원하기 때문에 전부 사용이 가능하다.&lt;/b&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;b&gt;시퀀스 컨테이너:&lt;/b&gt; std::vector, std::list, std::deque, std::array, std::forward_list 등.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;연관/무순서 컨테이너:&lt;/b&gt; std::set, std::map, std::unordered_set 등 (주로 이 컨테이너들의 데이터를 다른 곳으로 복사할 때 사용).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;C 스타일 배열:&lt;/b&gt; 일반 배열의 포인터도 반복자의 역할을 하므로 사용 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기타:&lt;/b&gt; std::string, std::valarray 등.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;for문 방식과 비교했을 때의 장점&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// 1. 기본 방식
{
    vector&amp;lt;int&amp;gt; v2;
    for(int i = first; i &amp;lt;= last; i++)
    {
        int num = v1[i];
        v2.push_back(num);
    }
}

// 2. std::copy() 사용 방식
{
    vector&amp;lt;int&amp;gt; v2(last-first+1);
    std::copy(v1.begin()+first, v1.begin()+last+1, v2.begin());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1번 방식과 2번 방식 둘 다 v1의 first부터 last까지의 원소를 v2에 복사하는 코드이다. 1번 방식인 for문 활용 코드도 코드 길이가 길지 않기 때문에 충분히 좋은 방식이지만, &lt;b&gt;2번 방식인 std::copy() 활용 코드가&lt;/b&gt; 한 줄의 코드로 복사를 하는 코드임을 명시할 수 있기 때문에 1번 방식보다 &lt;b&gt;가독성이 높고&lt;/b&gt; &lt;b&gt;코드 길이도 짧다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의 사항: 붙여 넣을 위치에 메모리가 부족하면 오류 발생!&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;{
    vector&amp;lt;int&amp;gt; v2;
    std::copy(v1.begin()+first, v1.begin()+last+1, v2.begin());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;std::copy는 복사 붙여 넣기를 할 뿐, 붙여 넣을 위치에 메모리가 부족했을 때 알아서 메모리를 확보하지는 않는다.&lt;/b&gt;&lt;/span&gt; 그렇기 때문에 위 코드처럼 메모리를 충분히 확보하지 않은 벡터 v2의 begin()을 그대로 3번째 인자에 넣게 되면 오류가 발생하게 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그렇다면 매번 std::copy를 쓸 때마다 붙여 넣을 위치에 충분한 메모리가 확보되었는지 확인해야 할까?&lt;/b&gt; 이러한 확인 작업은 매우 귀찮기도 하고, 놓치기도 쉽기 때문에 최대한 안 하고 싶을 텐데, &lt;b&gt;이럴 때는 알아서 메모리 공간을 확보하는 inserter 계열의 클래스를 같이 사용하면 된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;// 2-1. std::copy() + back_inserter 사용 방식
// back_inserter를 인자로 넘겨주면 미리 메모리 확보를 하지 않아도 된다!
{
    vector&amp;lt;int&amp;gt; v2;
    std::copy(v1.begin()+first, v1.begin()+last+1, back_inserter(v2));
}&lt;/code&gt;&lt;/pre&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;inserter 계열의 클래스?&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// std::back_inserter
template&amp;lt;class Container&amp;gt;
std::back_insert_iterator&amp;lt;Container&amp;gt; back_inserter(Container&amp;amp; c)
{
    return std::back_insert_iterator&amp;lt;Container&amp;gt;(c);
}

// std::front_inserter
template&amp;lt;class Container&amp;gt;
std::front_insert_iterator&amp;lt;Container&amp;gt; front_inserter( Container&amp;amp; c )
{
    return std::front_insert_iterator&amp;lt;Container&amp;gt;(c);
}

// std::inserter
template&amp;lt;class Container&amp;gt;
std::insert_iterator&amp;lt;Container&amp;gt; inserter(Container&amp;amp; c, typename Container::iterator i)
{
    return std::insert_iterator&amp;lt;Container&amp;gt;(c, i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;inserter 계열의 클래스는 컨테이너에 새로운 원소를 삽입할 때 사용하는 특수한 출력 반복자 어댑터(Output Iterator Adapter)로&lt;/b&gt;, 3가지 종류가 있으며, 각각 아래와 같다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;std::back_inserter&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 맨 뒤에 원소를 삽입한다.&lt;/li&gt;
&lt;li&gt;내부에서 push_back(value)을 호출하기 때문에, push_back 멤버 함수가 존재하는 컨테이너만 사용가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 가능 컨테이너: std::vector, std::deque, std::list, std::string 등&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;std::front_inserter&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 맨 앞에 원소를 삽입한다.&lt;/li&gt;
&lt;li&gt;내부에서 push_front(value)를 호출하기 때문에, push_front 멤버 함수가 존재하는 컨테이너만 사용가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 가능 컨테이너&lt;/b&gt;: std::deque, std::list, std::forward_list 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;std::inserter(일반 삽입자)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너의 특정 위치에 원소를 삽입한다.&lt;/li&gt;
&lt;li&gt;내부에서 insert(it, value)를 호출하기 때문에, insert 멤버 함수가 존재하는 컨테이너만 사용가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 가능 컨테이너: 거의 모든 STL 컨테이너&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;특징: 연관 컨테이너(set, map)의 경우, 삽입 위치 인자인 it을 힌트로만 사용하며 실제로는 정렬된 위치에 삽입된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건 복사 std::copy_if&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;template &amp;lt;class InputIt, class OutputIt, class UnaryPredicate&amp;gt;
OutputIt copy_if(InputIt first, InputIt last, OutputIt d_first,
                 UnaryPredicate pred);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복사를 하되, 조건에 맞는 요소만 복사하고 싶다면 std::copy_if를 사용하면 된다. 4개의 인자를 전달해야 하며, 앞에 3개는 std::copy와 동일하게 넣은 다음 마지막 인자에 &lt;b&gt;&amp;ldquo;단항 서술자(UnaryPredicate)&amp;rdquo;&lt;/b&gt;를 넣어주면 된다. 예제는 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; v2;
std::copy_if(v1.begin(), v1.end(), back_inserter(v2), [](const int&amp;amp; elem){
    // 짝수만 복사
    return elem % 2 == 0;
});

for(const int&amp;amp; elem : v2)
{
	cout &amp;lt;&amp;lt; elem &amp;lt;&amp;lt; ' ';
}
cout &amp;lt;&amp;lt; '\\n';

// 실행 결과
2 4
&lt;/code&gt;&lt;/pre&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;단항 서술자(UnaryPredicate)?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;STL 알고리즘에 인자로 Predicate 타입이 자주 나오는데, C++에서 Predicate는 반환 타입이 bool인 함수로, Predicate 계열인 단항 서술자는 인자가 한 개인 Predicate를 의미한다. 즉, 단항 서술자는 &lt;b&gt;&amp;ldquo;인자가 한 개이고, bool 타입을 반환하는 functor&amp;rdquo;를 의미한다&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>C++/문법 및 메소드(STL)</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/622</guid>
      <comments>https://pledge24.tistory.com/622#entry622comment</comments>
      <pubDate>Fri, 9 Jan 2026 18:31:41 +0900</pubDate>
    </item>
    <item>
      <title>[TIL][C++] 251114 MMO 서버 개발 137일차: [언리얼] 다시 정리하는 액터의 Life Cycle, Broadcast한 델리게이트와 바인딩된 함수들은 언제 실행되는가?, 호출된 이후에 Tick이 실행될 가능성?</title>
      <link>https://pledge24.tistory.com/619</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] 다시 정리하는 액터의 Life Cycle&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1409&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnwChg/dJMcafSvdOS/FKo6u3cvVlWR808TAIQI81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnwChg/dJMcafSvdOS/FKo6u3cvVlWR808TAIQI81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnwChg/dJMcafSvdOS/FKo6u3cvVlWR808TAIQI81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnwChg%2FdJMcafSvdOS%2FFKo6u3cvVlWR808TAIQI81%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;1409&quot; height=&quot;282&quot; data-origin-width=&quot;1409&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&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;액터의 생명 주기(Life Cycle)이란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레벨에 Actor가 생성, 초기화, 플레이, 소멸되는 전체과정&lt;/li&gt;
&lt;li&gt;각 단계마다 특정 이벤트 함수들이 자동으로 호출된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;액터 생성 단계&lt;/b&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;Construction Script&lt;/b&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;/li&gt;
&lt;li&gt;에디터에서 액터의 속성(Property)을 수정할 때마다 호출되므로, 이곳에 게임플레이 로직을 작성하거나, 무거운 작업을 추가하면 안 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 액터의 속성은 에디터에서 액터의 디테일 패널에 노출되는 값들이 대표적이다. 따라서, 액터를 레벨에 배치한 다음 액터를 드래그로 이동시키면 조금만 이동해도 수십 번 Construction Script가 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;채워 넣는 로직 종류
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액터의 초기 설정과 구성을 자동화할 때 사용한다.&lt;/li&gt;
&lt;li&gt;에디터는 Construction Script가 실행된 후의 결과를 표시하므로, Construction Script에 작성한 로직의 결과를 에디터에서 시각적 피드백으로 받을 수 있다.&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;b&gt;Event BeginPlay&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;월드에 배치된&amp;rdquo; 액터는 게임이 Play상태로 진입했을 때, 또는 Play상태인 게임에서 액터가 스폰될 때 딱 한 번만 호출&lt;/li&gt;
&lt;li&gt;액터의 모든 초기화 작업이 완료된 시점과 액터가 레벨에서 Tick을 돌기 시작하는 시점 사이에 호출된다고 보면 된다.&lt;/li&gt;
&lt;li&gt;채워 넣는 로직 종류
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레벨에 등장한 시점에서 처리할 로직.&lt;/li&gt;
&lt;/ul&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;액터 루프 단계&lt;/b&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;Event Tick&lt;/b&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;/li&gt;
&lt;li&gt;Delta Seconds 매개변수를 제공&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;b&gt;Event Possessed(액터가 Pawn 또는 Pawn의 하위 클래스인 경우)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플레이어 컨트롤러(APlayerController)가 해당 액터를 대상으로 Possess()를 호출했을 때 호출됨.&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;b&gt;Event UnPossessed(액터가 Pawn 또는 Pawn의 하위 클래스인 경우)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플레이어 컨트롤러(APlayerController)가 해당 액터를 대상으로 UnPossess()를 호출했을 때 호출됨.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;액터 제거 단계&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/823Wb/dJMcahQjNOy/tOAZFwAEOwqxnPH3Agqk21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/823Wb/dJMcahQjNOy/tOAZFwAEOwqxnPH3Agqk21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/823Wb/dJMcahQjNOy/tOAZFwAEOwqxnPH3Agqk21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F823Wb%2FdJMcahQjNOy%2FtOAZFwAEOwqxnPH3Agqk21%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;754&quot; height=&quot;399&quot; data-origin-width=&quot;754&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Event EndPlay&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액터가 월드에서 제거되거나 파괴될 때(Ex. Destroy() 호출, 레벨 전환, PIE 종료) 호출되는 함수.&lt;/li&gt;
&lt;li&gt;Destroy() 함수가 호출되는 시점에 매우 빠르게 그다음으로 호출되는 함수&lt;/li&gt;
&lt;li&gt;이 시점부터 액터는 더 이상 게임 로직에 참여하지 않으며, 자신이 소유한 리소스를 해제하기에 가장 적절한 게임플레이 단계의 마지막 순간이다.&lt;/li&gt;
&lt;li&gt;채워 넣는 로직 종류
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;b&gt;Event Destroyed&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액터가 Destroy() 함수 호출 이후, 액터의 실질적 파괴가 일어나기 직전에 발생하는 이벤트.&lt;/li&gt;
&lt;li&gt;블루프린트에서 발동하는 이벤트.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C++에서 AActor::Destroyed() 내부에서 호출됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;소멸 직전의 마무리 정리 작업 코드가 들어간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;void AActor::Destroyed()
{
	RouteEndPlay(EEndPlayReason::Destroyed); // EndPlay 호출

	ReceiveDestroyed(); // BP의 Event Destroy임
	OnDestroyed.Broadcast(this);
}

// 결론
// AActor::Destroyed() -&amp;gt; EndPlay() -&amp;gt; BP's Destroyed
// AActor::EndPlay -&amp;gt; UObject::BeginDestroy -&amp;gt; UObject::FinishDestroy
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이벤트 호출 순서&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7F2VI/dJMcafEXYpD/b0itKwPmT8IUdMgtXOBoKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7F2VI/dJMcafEXYpD/b0itKwPmT8IUdMgtXOBoKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7F2VI/dJMcafEXYpD/b0itKwPmT8IUdMgtXOBoKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7F2VI%2FdJMcafEXYpD%2Fb0itKwPmT8IUdMgtXOBoKK%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;1632&quot; height=&quot;156&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] 딱 한 번만 호출되는 노드 Do Once&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBpE7b/dJMcaiaCvEL/a390VPrtqkPowg2WwffEP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBpE7b/dJMcaiaCvEL/a390VPrtqkPowg2WwffEP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBpE7b/dJMcaiaCvEL/a390VPrtqkPowg2WwffEP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBpE7b%2FdJMcaiaCvEL%2Fa390VPrtqkPowg2WwffEP0%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;225&quot; height=&quot;138&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Tick에 넣으면 한 번만 발동한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AActor의 소멸자 사용은 안전할까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UObject에서 파생된 클래스들은 자동으로 언리얼의 GC에 의해 수명 주기가 관리된다. 따라서 UObject 계열의 클래스는 직접적인 소멸자 사용을 지양해야 한다.&lt;/li&gt;
&lt;li&gt;소멸자 대신, GC가 객체를 정리하기 전에 호출하는 BeginDestroy()와 같은 특화된 UE함수를 사용하여 정리 작업을 수행하는 것이 옳다.&lt;/li&gt;
&lt;li&gt;AActor 또한 UObject의 하위 클래스이므로 같은 규칙이 적용된다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UObject 계열의 클래스가 아닌 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TSharedPtr이나 TWeakPtr 등을 사용하면 된다. UObject가 아닌 클래스를 UE의 스마트 포인터로 묶은 다음, 해당 클래스의 소멸자에 코드를 작성한다면, 이는 문제가 되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Broadcast 한 델리게이트와 바인딩된 함수들은 언제 실행되는가?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;즉시, 그리고 동기적으로 실행된다&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Broadcast()가 호출되는 순간, 프로그램의 실행 흐름은 해당 지점에서 멈추고, 델리게이트에 바인딩된 함수들을 순차적으로 모두 실행한다.(단, 바인딩된 함수들의 실행 순서는 보장 X) 모든 함수의 실행이 완료되면, Broadcast() 다음 코드로 실행 흐름이 넘어간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] Destroy가 호출된 이후에 Tick이 실행될 가능성?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;Destroy가 언제 호출되었는가?&amp;rdquo;가 중요하다.&lt;/li&gt;
&lt;li&gt;대부분의 경우 UWorld::Destroy함수는 월드가 Play 상태일 때 호출되므로, Destroy를 호출하는 객체의 Tick이 액터의 Tick보다 먼저 호출되는가? 와 연관이 있다.&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;b&gt;사전 지식: UWorld::Destory() 함수는 비동기 함수이다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파괴한다는 이름을 가진 함수지만, 실제로 파괴는 그 즉시 일어나지 않는다. Destroy는 파괴할 액터를 &amp;ldquo;Pending Kill&amp;rdquo; 상태로 플래그만 세워놓고, 메모리 해제나 컴포넌트 제거는 이루어지지 않는다.&lt;/li&gt;
&lt;li&gt;World&amp;rarr;Tick() 과정에서 액터 리스트를 순회하며 액터의 Tick을 호출하는데, 이때 PendingKill 상태인 액터는 Tick 호출 대상에서 제외된다. 그래서 같은 프레임 안에서 벌어지는 일은 아래와 같이 나뉜다.&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;b&gt;Destroy 예시: 액터 A의 Tick()에서 액터 B를 Destroy 하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 액터의 Tick은 World의 Tick() 함수에 의해 &lt;b&gt;순차적으로&lt;/b&gt; 호출된다.&lt;/li&gt;
&lt;li&gt;만약 World가 액터 B의 Tick을 먼저 호출했다면, B는 해당 프레임에서 방해 없이 그대로 Tick을 돈다.&lt;/li&gt;
&lt;li&gt;반대로 World가 액터 A의 Tick을 먼저 호출했다면, B는 PendingKill 상태가 되어 같은 프레임 안에서 예정되어 있던 Tick은 건너뛴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 액터 A가 먼저 호출된 경우
1. World의 TickGroup에서 A의 ExcuteTick호출
2. A의 AActor::Tick // B를 Destroy
3. World의 TickGroup에서 B의 ExcuteTick호출
4. B는 pending kill 상태이므로 Tick 호출 안 함
&lt;/code&gt;&lt;/pre&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;b&gt;Destroy 예시: 액터 A의 Delay 이후 노드에서 본인을 Destroy 하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Delay노드는 Latent Action에 해당하며, Latent Action이 발동되는 시점은 액터의 Tick 내부이다. 정확히는 AActor::Tick에서 실행되는 것이며, AActor::Tick 내부에서 BP의 Tick &amp;rarr; Latent Action 순으로 실행되므로, 같은 프레임 내에서 Tick 함수와 Destroy가 호출되는 것이 가능하다.&lt;/li&gt;
&lt;li&gt;만약 AActor의 하위 클래스이고, 하위 클래스의 Tick의 최상단에 Super::Tick으로 작성했다면, 하위 클래스 Tick에 작성한 로직이 가장 늦게 실행되므로 하위 클래스의 Tick은 Destroy가 호출된 이후에 호출된다. 따라서 순서는 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. AActor::Tick
2. BP의 Tick 이벤트
3. Delay노드 뒤에 연결된 노드(Latent Action 발동) // 본인을 Destroy!
4. AActor의 하위 클래스 Tick(Super::Tick을 최상단에 배치한 경우) // pending kill 상태로 진입
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우 AActor의 하위 클래스의 Tick은 Pending Kill 상태로 진입하며, EndPlay가 호출된 이후의 시점이므로, 만약 EndPlay에서 메모리를 날려버렸다면, Tick에서 해당 메모리에 접근하는 순간 크래시가 난다. 이런 상황을 방지하기 위해 AActor의 하위 클래스의 Tick에서 Super::Tick을 최상단에 두었다면, 아래와 같이 pending kill 상태일 때는 Tick을 실행하지 않도록 처리해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// PendingKill 상태인지 확인하는 코드
if (IsPendingKill() || IsActorBeingDestroyed())
    return;
    
// PendingKill 상태라면 IsValid는 false를 반환한다.(이게 좀 더 추천됨)
if (!IsValid(this)) return; 
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Today I Learn</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/619</guid>
      <comments>https://pledge24.tistory.com/619#entry619comment</comments>
      <pubDate>Sat, 15 Nov 2025 02:42:49 +0900</pubDate>
    </item>
    <item>
      <title>[TIL][C++] 251113 MMO 서버 개발 136일차: [언리얼] BeginPlay()에 대해서&amp;hellip;, 에디터 환경 유무에 따른 BeginPlay() 호출 순서 차이, 언리얼은 네트워크 구조가 기본으로 세팅된다, ULocalPlayer, FWorldContext</title>
      <link>https://pledge24.tistory.com/618</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] BeginPlay()에 대해서&amp;hellip;&lt;/h3&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;BeginPlay() 함수란?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BeginPlay는 액터(AActor)가 월드(UWorld)에서 처음으로 활성화되고 시뮬레이션되기 시작할 때 단 한 번 호출되는 이벤트 함수이다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BeginPlay()가 호출되는 정확한 시점?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 액터를 두고 BeginPlay가 호출되는 시점을 정확하게 표현하자면 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;액터의 모든 초기화 작업이 완료되었으며(부착된 컴포넌트 포함), 월드에 해당 액터가 등록되어 인지할 수 있는 시점.&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;레벨에 배치된 액터의 BeginPlay(), 게임 도중에 스폰된 액터의 BeginPlay()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언리얼에서 이 둘의 BeginPlay() 호출 시점은 조금 다르다.&lt;/li&gt;
&lt;li&gt;레벨의 배치된 액터의 경우 초기화 작업이 완료되어도 곧바로 BeginPlay()를 호출되지 않고, 레벨이 로드된 이후에 첫 프레임에 호출된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왜 BeginPlay 호출이 뒤로 밀리 나면, BeginPlay의 Play는 게임이 시작된 상태, 즉, 레벨이 완전히 로드가 된 이후에서만 가능한 상태이기 때문. 액터는 레벨에 종속되기 때문에 본인을 소유한 레벨보다 먼저 Play 상태가 되는 것은 말이 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;반면, 이미 플레이 상태인 게임에서 액터를 SpawnActor와 같은 함수로 스폰했다면, 레벨은 이미 Play상태이기 때문에 엔진은 최대한 빨리 BeginPlay를 호출한다. SpawnActor의 경우 동기 함수이기 때문에, 반환 시점에서 BeginPlay는 이미 호출된 경우가 많다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&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;/li&gt;
&lt;li&gt;게임 도중에 스폰된 액터는 만들어진 즉시 호출&lt;/li&gt;
&lt;/ul&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;렌더링과 BeginPlay()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그렇다면 BeginPlay()가 끝난 직후의 시점은 어떨까? 해당 액터가 레벨에서 눈에 보일까?&lt;/li&gt;
&lt;li&gt;결론부터 말하자면 그렇지 않다. 엔진은 한 프레임(틱)내에서 렌더링은 로직을 다 처리한 뒤 마지막 부분에서 처리되는 작업이기 때문에, BeginPlay()가 막 끝난 시점에서 액터는 눈에 보이지 않는다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BeginPlay와 Tick 함수의 첫 번째 호출은 같은 틱(프레임)에서 호출되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BeginPlay()는 액터의 &lt;b&gt;준비 단계&lt;/b&gt;를 담당하고, Tick()은 &lt;b&gt;실제 게임 루프의 실행 단계&lt;/b&gt;를 담당하므로, &lt;b&gt;BeginPlay()가 먼저 완료된 후&lt;/b&gt;에 첫 번째 Tick()이 발생한다.&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;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에디터 환경 유무에 따른 BeginPlay() 호출 순서 차이&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 액터는 BeginPlay() 함수를 가질 수 있다. 그런데 궁금한 점이 있다. 게임 플레이 도중에 스폰된 액터는 그렇다 쳐도, 레벨에 배치된 액터들의 경우 레벨이 로드된 이후 첫 프레임에 동시에 호출되는데 순서는 어떻게 될까?&lt;/li&gt;
&lt;li&gt;기본적으로 BeginPlay()들끼리의 순서는 딱히 없다고 보는 것이 일반적이다. 누가 먼저 호출되고 누가 나중에 호출되고는 보장하지 않기 때문. 하지만, 이 말은 반은 맞고 반은 틀린 말이다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특수 액터 타입에 대한 BeginPlay 호출 순서 보장&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언리얼에서 액터는 모든 게임 오브젝트의 기반이 되는 그릇의 역할을 하기 때문에, 오만가지가 액터로 만들어져 있다. 그래서 월드에서 시스템 역할을 맡는 애들도 전부 액터로 만들어져 있다. 대표적으로 GameMode와 GameMode의 프로퍼티들이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;489&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mITyS/dJMcaap6GeO/6fbscwW5u3h5vNElho6VIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mITyS/dJMcaap6GeO/6fbscwW5u3h5vNElho6VIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mITyS/dJMcaap6GeO/6fbscwW5u3h5vNElho6VIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmITyS%2FdJMcaap6GeO%2F6fbscwW5u3h5vNElho6VIk%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;873&quot; height=&quot;489&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;489&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GameMode와 GameMode의 프로퍼티들과 같은 특수한 액터들은 BeginPlay의 호출 순서가 보장된다. 그런데 이 호출 순서가 특이하게도 &amp;ldquo;에디터 환경인가? 릴리즈 된 StandAlone(NetMode의 StandAlone 아님) 프로그램 환경인가?&amp;rdquo;에 따라 달라진다.&lt;/li&gt;
&lt;li&gt;각각의 BeginPlay 함수의 호출 순서를 보면 아래와 같다.&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;b&gt;에디터 환경&lt;/b&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;레벨에 배치된 Actor&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GameMode&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Level&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GameState&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PlayerController&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PlayerState&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HUD&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pawn&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;빌드된 게임 (Standalone 상태)&lt;/b&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;Pawn&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GameMode&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GameState&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PlayerState&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PlayerController&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HUD&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Level&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레벨에 배치된 Actor&lt;/b&gt;&lt;/li&gt;
&lt;/ol&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;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에디터 환경과 StandAlone 환경은 서로 다른 방식으로 레벨에 배치된 액터를 스폰하기 때문. 에 StandAlone의 경우 디스크에서 직접 액터의 데이터를 가져오는 반면, 에디터 환경의 경우 미리 에디터가 들고 있는 액터를 복제해서 스폰한다. 그래서 에디터 환경인 경우 다른 액터들보다(심지어 레벨보다도) 빠르게 BeginPlay가 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언리얼은 네트워크 구조가 기본으로 세팅된다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NetMode를 StandAlone으로 했다는 것은 네트워크를 사용하지 않는 싱글플레이어 게임을 만들겠다는 건데, 이상하게도 네트워크 관련 객체들이 플레이할 때마다 생기는 것을 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;573&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kQaws/dJMcafybtyg/fApcDVPyTEIpbtQpk1E2m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kQaws/dJMcafybtyg/fApcDVPyTEIpbtQpk1E2m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kQaws/dJMcafybtyg/fApcDVPyTEIpbtQpk1E2m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkQaws%2FdJMcafybtyg%2FfApcDVPyTEIpbtQpk1E2m0%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;573&quot; height=&quot;155&quot; data-origin-width=&quot;573&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;li&gt;NetMode를 StandAlone으로 하면 이렇게 생성되는 네트워크 전용 객체들은 안에 텅텅 빈 상태로 껍데기만 남아있는다고 한다. 있는 거 자체가 불편하긴 하지만 문제가 되지는 않는다고 하니 뭐&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ULocalPlayer&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔진의 플레이어 표현(월드가 아님!). GameInstance가 아래에 존재한다.&lt;/li&gt;
&lt;li&gt;엔진에서 관리되기 때문에 월드가 파괴되는 것이 ULocalPlayer의 생명주기에 영향을 주지 않는다.&lt;/li&gt;
&lt;li&gt;보통은 1개만 있다. 한 게임에 동시에 여러 플레이어를 조종하는 구조라면 여러 개 있을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H62nh/dJMcabWQ2fF/zKYdK6kscG1vSkocxWm4EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H62nh/dJMcabWQ2fF/zKYdK6kscG1vSkocxWm4EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H62nh/dJMcabWQ2fF/zKYdK6kscG1vSkocxWm4EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH62nh%2FdJMcabWQ2fF%2FzKYdK6kscG1vSkocxWm4EK%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;671&quot; height=&quot;283&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ULocalPlayer는 UPlayer를 상속받는 클래스.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;역할?&lt;/b&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;APlayerController 생성 및 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ULocalPlayer는 자신의 APlayerController를 소유하거나 이를 생성한다.&lt;/li&gt;
&lt;li&gt;네트워크 세션 연결 시, ULocalPlayer 단위로 세션에 참여하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;입력(Input) 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 장치와(키보드, 패드 등)와 플레이어를 매핑&lt;/li&gt;
&lt;li&gt;ULocalPlayer가 여러 개면 각 ULocalPlayer가 별도의 매핑을 하고, 별도의 입력을 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;렌더링 뷰(Viewport) 제어&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 ULocalPlayer는 FSceneView 또는 UPlayerCameraManager를 통해 자신만의 카메라 시점을 가진다.&lt;/li&gt;
&lt;li&gt;화면을 나누는 방식(SplitScrren Layout)도 여기서 제어된다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;관계 요약&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;UGameInstance
 ├─ ULocalPlayer (플레이어 1)
 │   ├─ APlayerController
 │   ├─ UPlayerCameraManager
 │   └─ 입력 및 뷰 관리
 ├─ ULocalPlayer (플레이어 2)
 │   └─ ...

UGameInstance
 └── TArray&amp;lt;ULocalPlayer*&amp;gt; LocalPlayers
       └── APlayerController (각 LocalPlayer에 대응)
            └── APawn / ACharacter
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 각 클라이언트 연결마다 UPlayer 타입을 가지는데, 그냥 가지지는 않고, 하위 클래스인 UNetConnection으로 가지고 있는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UPlayer는 직접 사용되지 않고, 두 가지 구체 타입으로 나뉩니다:&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;ULocalPlayer&lt;/b&gt;: 클라이언트(또는 listen 서버의 로컬 클라이언트)에서 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UNetConnection&lt;/b&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;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WorldContext(FWorldContext)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게임 인스턴스에 존재하는 특정 World 인스턴스 및 이와 관련된 모든 콘텍스트 데이터를 관리하는 &amp;ldquo;구조체&amp;rdquo;&lt;/li&gt;
&lt;li&gt;쉽게 말해, 하나의 언리얼 엔진 애플리케이션 (프로그램)이 실행될 때, 여러 개의 독립적인 '세계(World)'를 가질 수 있는데, 이 각각의 세계에 대한 중요한 정보와 상태를 묶어서 관리하는 '주소록' 또는 '콘텍스트'라고 생각하면 된다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 기능 및 역할&lt;/b&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;UWorld 인스턴스 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 활성화된 UWordl 인스턴스를 관리한다. ( 포인터 타입으로 가지고 있음 UWorld*)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다중 월드 지원&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게임인스턴스는 활성화된 UWorld는 유일하지만, UWorld 인스턴스 자체는 여러 개를 동시에 가질 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PIE (Play In Editor):&lt;/b&gt; 에디터에서 플레이할 때, 각 클라이언트(및 서버)마다 별도의 월드 인스턴스를 가집니다. 각 PIE 세션은 고유한 WorldContext를 가집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;독립 실행형 게임:&lt;/b&gt; 메인 게임 월드가 있고, 때로는 별도의 로딩 화면이나 메뉴용 월드를 가질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;콘텍스트 제공 (Mode/Type):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WorldContext는 해당 월드가 어떤 목적으로 사용되고 있는지에 대한 콘텍스트 정보를 담고 있다. 예를 들어, EWorldContextType 열거형을 통해 &lt;b&gt;이 월드가 게임 월드 (Game)인지, 에디터 유틸리티 (Editor)용인지, PIE 클라이언트 (PIEClient)인지, 아니면 PIE 서버 (PIEServer)인지 등을 알려준다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엔진에 접근 기능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔진의 특정 시스템은 WorldContext를 통해 해당 월드에 해당하는 올바른 콘텍스트를 파악하고 접근한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Today I Learn</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/618</guid>
      <comments>https://pledge24.tistory.com/618#entry618comment</comments>
      <pubDate>Thu, 13 Nov 2025 23:44:42 +0900</pubDate>
    </item>
    <item>
      <title>[TIL][C++] 251112 MMO 서버 개발 135일차: [언리얼] LoadMap 안에서 일어나는 과정을 알아보자</title>
      <link>https://pledge24.tistory.com/617</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] LoadMap 안에서 일어나는 과정을 알아보자&lt;/h3&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;===LoadMap전 엔진 초기화 과정(처음 켰을때 한 번만 진행)===&lt;/b&gt;&lt;b&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;&lt;b&gt;1. GEngine::Start&lt;/b&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{
	SCOPED_BOOT_TIMING(&quot;GEngine-&amp;gt;Start()&quot;);
	GEngine-&amp;gt;Start();
}
&lt;/code&gt;&lt;/pre&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;2. GEngine::Init()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GameInstance, GameViewportClient, LocalPlayer를 생성 및 초기화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ogLP8/dJMcacBr4QC/kjb1d0Pn0MZrKJOnAatWCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ogLP8/dJMcacBr4QC/kjb1d0Pn0MZrKJOnAatWCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ogLP8/dJMcacBr4QC/kjb1d0Pn0MZrKJOnAatWCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FogLP8%2FdJMcacBr4QC%2Fkjb1d0Pn0MZrKJOnAatWCk%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;812&quot; height=&quot;480&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&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;LoadMap을 기점으로 생성되는 객체&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;489&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzfd9P/dJMb995NykZ/cysu9hB2tKm07PJ9bZoiU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzfd9P/dJMb995NykZ/cysu9hB2tKm07PJ9bZoiU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzfd9P/dJMb995NykZ/cysu9hB2tKm07PJ9bZoiU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzfd9P%2FdJMb995NykZ%2Fcysu9hB2tKm07PJ9bZoiU1%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;873&quot; height=&quot;489&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;489&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LoadMap 이후에 생성되는 모든 객체는 UWorld에 종속되며, 레벨 전환 시 UWorld가 파괴되기 때문에 이에 종속된 객체들도 전부 파괴된다. 따라서, 위 사진에서 UEngine::LoadMap아래에 있는 모든 객체는 파괴된다고 볼 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요약: LoadMap 후에 생성된 객체들은 해당 레벨에서만 유효하다.&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;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LoadMap 첫 번째 과정. 맵 데이터 메모리에 올리기(Load)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우리가 흔히 Map이라고 부르는 레벨(ULevel)은 *. umap 파일 형식으로 되어있다.&lt;/li&gt;
&lt;li&gt;이 파일에는 World, Level, Actor를 저장하고 있으며, 데이터는 직렬화된 상태로 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xovL8/dJMb99LuIWg/OXRjc6Ip0kZutWzWcYIHxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xovL8/dJMb99LuIWg/OXRjc6Ip0kZutWzWcYIHxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xovL8/dJMb99LuIWg/OXRjc6Ip0kZutWzWcYIHxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxovL8%2FdJMb99LuIWg%2FOXRjc6Ip0kZutWzWcYIHxK%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;787&quot; height=&quot;103&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;103&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u8yXT/dJMcafLH9hb/1oBOA4meNKPP2PVHuozn20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u8yXT/dJMcafLH9hb/1oBOA4meNKPP2PVHuozn20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u8yXT/dJMcafLH9hb/1oBOA4meNKPP2PVHuozn20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu8yXT%2FdJMcafLH9hb%2F1oBOA4meNKPP2PVHuozn20%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;500&quot; height=&quot;378&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h3 data-ke-size=&quot;size23&quot;&gt;LoadMap 두 번째 과정. UWorld 초기화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리에 UWorld를 올렸으니 초기화를 해야 한다. (객체를 만들고 Init 메서드를 부르는 과정)&lt;/li&gt;
&lt;li&gt;GameInstance가 소유하고 있는 WorldContext에 새롭게 만든 UWorld 객체를 넣어 실행할 월드를 교체한다. (이 과정에 의해 UWorld와 GameInstance가 서로에 대해 알게 되어 UWorld에서 GI에, GI에서 UWorld에 접근이 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vDDeM/dJMcaap6dsK/V7LjYTIFucVk3FKCiT3zj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vDDeM/dJMcaap6dsK/V7LjYTIFucVk3FKCiT3zj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vDDeM/dJMcaap6dsK/V7LjYTIFucVk3FKCiT3zj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvDDeM%2FdJMcaap6dsK%2FV7LjYTIFucVk3FKCiT3zj1%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;1074&quot; height=&quot;151&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;151&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행할 World가 교체되었으니 GC의 대상으로 두기 위해 AddToRoot로 World를 등록하고, InitWorld를 호출하여 물리 / 내비게이션(Navigation) / AI / 오디오 같은 월드 시스템을 초기화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;965&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cORy11/dJMb99SgmIB/7i6LQAXKWPpTIgeRQ14Ca1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cORy11/dJMb99SgmIB/7i6LQAXKWPpTIgeRQ14Ca1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cORy11/dJMb99SgmIB/7i6LQAXKWPpTIgeRQ14Ca1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcORy11%2FdJMb99SgmIB%2F7i6LQAXKWPpTIgeRQ14Ca1%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;965&quot; height=&quot;447&quot; data-origin-width=&quot;965&quot; data-origin-height=&quot;447&quot;/&gt;&lt;/span&gt;&lt;/figure&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;World는 GI에게 AGameMode 생성&lt;/b&gt;을 요청하고, 생성에 성공했다면 Map을 완전히 가져온다(Loading)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kD3PD/dJMcagcMABv/fBSaMWwsUVH5NN9cGpkwP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kD3PD/dJMcagcMABv/fBSaMWwsUVH5NN9cGpkwP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kD3PD/dJMcagcMABv/fBSaMWwsUVH5NN9cGpkwP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkD3PD%2FdJMcagcMABv%2FfBSaMWwsUVH5NN9cGpkwP0%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;1033&quot; height=&quot;479&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRvFyX/dJMcaf50Y5A/FGnoAekLUmCKL8fcVlS3dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRvFyX/dJMcaf50Y5A/FGnoAekLUmCKL8fcVlS3dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRvFyX/dJMcaf50Y5A/FGnoAekLUmCKL8fcVlS3dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRvFyX%2FdJMcaf50Y5A%2FFGnoAekLUmCKL8fcVlS3dK%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;1033&quot; height=&quot;479&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h3 data-ke-size=&quot;size23&quot;&gt;LoadMap 세 번째 과정. InitiailizeActorsForPlay() 호출&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InitiailizeActorsForPlay() 함수는 게임 플레이를 시작하기 위해 **&amp;ldquo;월드 내 모든 액터의 초기 상태를 설정&amp;rdquo;**한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 함수를 거치게 되면 모든 액터의 Component들이 UWorld에 등록되며, 액터는 PostLoad가, 컴포넌트들은 InitializeComponent가 호출된다.&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LoadMap 네 번째 과정. GameMode::InitGame() 호출&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;InitGame은 &lt;b&gt;GameMode를 초기화하며, 내부에서 AGameSession를 생성하게 된다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잠깐 말하는 거지만, 모든 액터는 항상 UWorld를 통해서 스폰한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btUvmG/dJMcac2wpxN/bDkqkGCJ5eisVHJ621hzBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btUvmG/dJMcac2wpxN/bDkqkGCJ5eisVHJ621hzBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btUvmG/dJMcac2wpxN/bDkqkGCJ5eisVHJ621hzBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtUvmG%2FdJMcac2wpxN%2FbDkqkGCJ5eisVHJ621hzBK%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;703&quot; height=&quot;393&quot; data-origin-width=&quot;703&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h3 data-ke-size=&quot;size23&quot;&gt;LoadMap 다섯 번째 과정. 월드에 존재하는 각 Level의 Actor 초기화&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;129&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F0bU5/dJMcac2wpxR/aOkdCDDp98CdJFxAsqxhf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F0bU5/dJMcac2wpxR/aOkdCDDp98CdJFxAsqxhf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F0bU5/dJMcac2wpxR/aOkdCDDp98CdJFxAsqxhf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF0bU5%2FdJMcac2wpxR%2FaOkdCDDp98CdJFxAsqxhf1%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;592&quot; height=&quot;129&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;129&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 map에 여러 Level을 넣지 않았다면(SubLevel을 만들지 않았다면) 월드에 존재하는 레벨은 Persistent Level 하나만 존재한다. SubLevel을 만들었다면 여러 레벨이 한 map에 존재하게 되는 거고. 이러한 경우에 맞추기 위해 for문을 돌면서 모든 레벨을 초기화해 준다. &lt;b&gt;이때 사용하는 함수는 ULevel::RoutActorInitialize() 함수이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ULevel::RoutActorInitialize() 내부&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fall-through 방식이라는 게 사용되어 있다. fall-through이란 의도적으로 break문을 안 넣어서 현재 case문에서 문제가 없을 때 다음 case문으로 넘어가게끔 작성한 방식이다. 즉, 이전 case문에서 문제가 없었다면 이에 따른 Enum값을 변경하고, 다음 case문에서 변경된 Enum을 가지면 실행하는 방식. 예시를 들면 아래와 같다.
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;void func()
{
	// State는 클래스 멤버 변수
	// EMyEnum State = EMyEnum::FirstState;
	
	switch(State)
	{
		case EMyEnum::FirstState:
		{
			if(/**문제 있으면 진입*/)
				return;
				
			State = EMyEnum::SecondState;
		}
		
		case EMyEnum::SecondState:
		{
			if(/**문제 있으면 진입*/)
				return;
				
			State = EMyEnum::ThridState;
		}
		
		//...
	}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ULevel::RoutActorInitialize() 내부에서는 이러한 fall-through을 while문을 돌면서 각 Actor에 대해 처리하도록 설계되어 있다. (여기서 처리되는 액터들은 레벨에 포함된 모든 액터들이다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;539&quot; data-origin-height=&quot;571&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAEyEp/dJMcac2wpx8/4k1GbZFQv7c5kQLUQdoU6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAEyEp/dJMcac2wpx8/4k1GbZFQv7c5kQLUQdoU6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAEyEp/dJMcac2wpx8/4k1GbZFQv7c5kQLUQdoU6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAEyEp%2FdJMcac2wpx8%2F4k1GbZFQv7c5kQLUQdoU6K%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;539&quot; height=&quot;571&quot; data-origin-width=&quot;539&quot; data-origin-height=&quot;571&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 과정을 성공적으로 끝까지 통과한 액터들은 InitializeComponent 삼총사가 호출된 상태가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GameMode도 일단은 Level에 포함된 Actor이다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물론 GameMode도 일단은 Level에 포함된 Actor이기 때문에 GameMode도 이 과정을 거친다.&lt;/li&gt;
&lt;li&gt;그래서 GameMode의 PreInitializeComponents도 여기서 호출되는데, &lt;b&gt;이때 GameState 객체와 GameNetworkManager 객체가 생성&lt;/b&gt;된다. 추가로, GameState는 InitGameState() 호출에 의해 초기화까지 진행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mAZzX/dJMcahisog1/PWkggro9qOERJ8qPSScQfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mAZzX/dJMcahisog1/PWkggro9qOERJ8qPSScQfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mAZzX/dJMcahisog1/PWkggro9qOERJ8qPSScQfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmAZzX%2FdJMcahisog1%2FPWkggro9qOERJ8qPSScQfK%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;1215&quot; height=&quot;474&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 시점에 도달했다면 LoadMap의 대부분이 처리된 상태이다. 모든 액터가 로드 및 초기화되었고, 월드가 Play 상태로 되었기 때문.&lt;/li&gt;
&lt;li&gt;게임의 전체 상태를 관리하는 액터들도 전부 있는 상태. 위 과정에서 중간중간에 생성한 객체들이 있었는데, 이것들이 전부 이에 해당한다. 생성된 애들을 보면 아래와 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AGameModeBase
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UWorld 초기화(두 번째 과정)할 때 UWorld::SetGameMode()로 생성됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AGameSession
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GameMode 초기화(네 번째 과정)할 때 GameMode::InitGame()에 의해 생성됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AGameNetworkManager / AGameStateBase
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ULevel::RoutActorInitialize() 호출(다섯 번째 과정)할 때 AGameModeBase::PreInitializeComponents()에 의해 생성됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이제 World는 완전히 초기화가 되었으며, 게임을 대표하는 게임 프레임워크가 생겼다.&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LoadMap 여섯 번째 과정. ULocalPlayer::SpawnPlayActor()&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이제 로컬 플레이어에 대해서 처리한다. ULocalPlayer는 GI에서 관리하기 때문에 GI에서 가져온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI0oCR/dJMcahisog7/kv7TKfyLNnithzlPZVTnw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI0oCR/dJMcahisog7/kv7TKfyLNnithzlPZVTnw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI0oCR/dJMcahisog7/kv7TKfyLNnithzlPZVTnw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI0oCR%2FdJMcahisog7%2Fkv7TKfyLNnithzlPZVTnw1%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;617&quot; height=&quot;201&quot; data-origin-width=&quot;617&quot; data-origin-height=&quot;201&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드처럼 각 ULocalPlayer마다 SpawnPlayActor를 호출하게 되는데, &lt;b&gt;ULocalPlayer::SpawnPlayActor내부에선 아래와 같이 PlayerController를 생성하게 된다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqBw4t/dJMcagcMABz/YAHjoG7LchJ2iSVh90atAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqBw4t/dJMcagcMABz/YAHjoG7LchJ2iSVh90atAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqBw4t/dJMcagcMABz/YAHjoG7LchJ2iSVh90atAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqBw4t%2FdJMcagcMABz%2FYAHjoG7LchJ2iSVh90atAK%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;1128&quot; height=&quot;682&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h3 data-ke-size=&quot;size23&quot;&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;/li&gt;
&lt;li&gt;UWorld::SpawnPlayerActor &amp;rarr; GameMode::PreLogin()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PreLogin은 로그인 요청을 승인하거나 거부하는 역할을 담당한다.&lt;/li&gt;
&lt;li&gt;로그인을 승인시킨 플레이어가 로컬이라면, 해당 플레이어를 게임에 추가하기 위해 AGameMode::Login() 함수가 호출된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;당연히 GameMode는 서버만 가지고 있으므로 호출되는 위치는 서버이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Login함수는 PlayerController를 만들어 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eJq3y3/dJMcahW3XH5/3eVBySYAOdYuc1xllveTBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eJq3y3/dJMcahW3XH5/3eVBySYAOdYuc1xllveTBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eJq3y3/dJMcahW3XH5/3eVBySYAOdYuc1xllveTBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeJq3y3%2FdJMcahW3XH5%2F3eVBySYAOdYuc1xllveTBK%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;800&quot; height=&quot;472&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물론 게임 플레이를 위해 World를 가져온 후, APlayerController를 생성하므로, 액터 생성 과정을 거치게 된다. 즉, PlayerController는 PostInitializecomponents가 호출된다.&lt;/li&gt;
&lt;li&gt;그리고 이어서 컨트롤러는 &lt;b&gt;AController::InitPlayerState()를 호출하여 내부에서 APlayerState를 생성한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFddR5/dJMcahQipqJ/0k2Mk7Gk0vPgvjCJEyqVk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFddR5/dJMcahQipqJ/0k2Mk7Gk0vPgvjCJEyqVk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFddR5/dJMcahQipqJ/0k2Mk7Gk0vPgvjCJEyqVk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFddR5%2FdJMcahQipqJ%2F0k2Mk7Gk0vPgvjCJEyqVk0%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;855&quot; height=&quot;440&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Login() 함수 처리가 끝나면, World는 통신을 위해 역할(SetRole)과 레플리케이션(SetReplicates)을 세팅하고, 완성된 Player객체를 연결한다.&lt;/li&gt;
&lt;li&gt;모든 작업이 완료되면 GameMode::PostLogin 함수가 호출되며 이 플레이어가 참가한 결과로 발생해야 하는 모든 설정을 처리할 수 있는 기회가 제공된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로, GameMode는 새 PC에 대한 Pawn 생성을 PostLogin에서 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GameMode의 관전자 처리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플레이어가 관전해야 함을 나타내도록 PlayerState를 구성하거나, 처음에 모든 플레이어가 관전자로 시작하도록 GameMode를 구성할 수도 있다.&lt;/li&gt;
&lt;li&gt;관전자로 처리할 경우, GameMode는 Pawn을 생성하지 않고(관전 자니까!), 대신 날라댕기는 SpectatorPawn을 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GameMode의 플레이어 처리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PostLogin다음으로 RestartPlayer를 호출한다.&lt;/li&gt;
&lt;li&gt;RestartPlayer는 PC가 주어지면, 새 Pawn이 생성되어야 하는 위치를 나타내는 APlayerStart를 찾는다. &amp;rarr; GetDefaultPawnClassForController를 통해 생성되어야 할 새 Pawn을 스폰 &amp;rarr; 스폰된 폰은 SetPawn()을 통해 PC에 폰을 연결하고, FinishRestartPlayer를 통해 RestartPlayer가 완료되었음을 알린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마지막 단계: World::BeginPlay()&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Engine은 World::BeginPlay()가 호출한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;59&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k8EKa/dJMcafro9u4/GB2soVFp2toJIeAwzGdGek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k8EKa/dJMcafro9u4/GB2soVFp2toJIeAwzGdGek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k8EKa/dJMcafro9u4/GB2soVFp2toJIeAwzGdGek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk8EKa%2FdJMcafro9u4%2FGB2soVFp2toJIeAwzGdGek%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;944&quot; height=&quot;59&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;59&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;World는 BeginPlay()에서 GameMode의 StartPlay()를 호출 + 월드가 BeginPlay를 했음을 알리는 OnWorldBeginPlay 델리게이트를 Broadcast&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1ovhl/dJMcadf3wRp/EiNQvV4kf4rBeWBjkGXWs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1ovhl/dJMcadf3wRp/EiNQvV4kf4rBeWBjkGXWs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1ovhl/dJMcadf3wRp/EiNQvV4kf4rBeWBjkGXWs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1ovhl%2FdJMcadf3wRp%2FEiNQvV4kf4rBeWBjkGXWs0%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;542&quot; height=&quot;250&quot; data-origin-width=&quot;542&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GameMode::StartPlay는 GameState에게 BeginPlay를 알리고,&lt;/li&gt;
&lt;li&gt;GameState는 모든 Actor들에게 BeginPlay를 알린다. (이때 Actor들의 BeginPlay가 호출된다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bORt80/dJMcahvZy3y/q7g9EavLpp0HiFmnKui251/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bORt80/dJMcahvZy3y/q7g9EavLpp0HiFmnKui251/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bORt80/dJMcahvZy3y/q7g9EavLpp0HiFmnKui251/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbORt80%2FdJMcahvZy3y%2Fq7g9EavLpp0HiFmnKui251%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;301&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이렇게 BeginPlay까지 처리가 전부 끝나면 LoadMap의 처리는 끝나게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 자료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://openmynotepad.tistory.com/114&quot;&gt;https://openmynotepad.tistory.com/114&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://itnerd.tistory.com/41&quot;&gt;https://itnerd.tistory.com/41&lt;/a&gt;&lt;/p&gt;</description>
      <category>Today I Learn</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/617</guid>
      <comments>https://pledge24.tistory.com/617#entry617comment</comments>
      <pubDate>Thu, 13 Nov 2025 00:45:05 +0900</pubDate>
    </item>
    <item>
      <title>[TIL][C++] 251111 MMO 서버 개발 134일차: [언리얼] GameMode와 내부 Property들의 역할</title>
      <link>https://pledge24.tistory.com/616</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[언리얼] GameMode와 내부 Property들의 역할&lt;/h2&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;b&gt;시작하기 앞서&amp;hellip;&lt;/b&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;GameMode와 내부에 존재하는 Property들은 언리얼 네트워크 시스템을 밀접한 관련이 있다.&lt;/b&gt; 즉, 서버가 따로 없는 싱글플레이어 게임인 경우 GameMode 대부분의 기능이 사용되지 않으므로, 아래의 설명들은 멀티플레이어 게임을 기준으로 설명한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;게임 모드(GameMode)란?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;136&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU3kn1/dJMcacBr3BW/HexGd4f6FCjuOqW7zPsVjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU3kn1/dJMcacBr3BW/HexGd4f6FCjuOqW7zPsVjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU3kn1/dJMcacBr3BW/HexGd4f6FCjuOqW7zPsVjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU3kn1%2FdJMcacBr3BW%2FHexGd4f6FCjuOqW7zPsVjK%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;136&quot; height=&quot;217&quot; data-origin-width=&quot;136&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;/figure&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;게임의 규칙을 결정하고 정보를 처리하는 액터(Actor).&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버 전용 클래스&lt;/b&gt;이며, &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;클라이언트에는 존재하지 않는다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;언리얼에서 제공하는 게임 모드 클래스는 AGameModeBase와 AGameMode가 있으며, 현재 시점(UE5.3)에서 아래와 같은 역할을 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AGameModeBase&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AGameModeBase는 모든 GameMode의 베이스 클래스.&lt;/li&gt;
&lt;li&gt;4.14 이전에는 AGameMode가 해당 역할을 했지만, 지금은 AGameModeBase의 파생 클래스가 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AGameMode&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AGameModeBase 보다 다양한 기능이 들어있는 AGameModeBase의 파생 클래스.&lt;/li&gt;
&lt;li&gt;맵 진입(EnteringMap), 시작 대기 중(WatingToStart), 진행 중(InProgress), 경기 후 대기(WatingPostMatch) 등 경기 상태에 대한 부가 함수를 지원한다.&lt;/li&gt;
&lt;/ul&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;게임 모드에서 결정하는 규칙(Rule)이란?&lt;/b&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;진행할 게임의 전반적인 규칙을 의미한다. 게임의 전반적인 규칙에 대해 LOL로 예를 들면 아래와 같은 규칙들을 의미한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 팀에 팀원은 5명. 레드팀과 블루팀이 있다.&lt;/li&gt;
&lt;li&gt;캐릭터는 각 팀의 우물에서 스폰되며, 시작 시 500 골드(맞나?)를 받는다.&lt;/li&gt;
&lt;li&gt;상대 진영의 넥서스를 먼저 파괴하거나, 상대가 항복을 하면 승리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대충 이런 식으로 게임 모드에서 정하는 규칙은 튜토리얼에서 이야기할 법한 게임 규칙이라 보면 된다. 이를 정리해서 주요 기능을 알아보면 아래와 같다.&lt;/li&gt;
&lt;li&gt;주요 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플레이어가 게임에 진입하는 방식 처리(Ex. 게임 시작 시 스폰 위치 결정)&lt;/li&gt;
&lt;li&gt;액터 간의 상호작용 발생 시 처리하는 방식 처리&lt;/li&gt;
&lt;li&gt;최대 플레이어 또는 관전자 수 조절&lt;/li&gt;
&lt;li&gt;게임 플레이 도중 일시 정지(Pause) 처리&lt;/li&gt;
&lt;li&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;클라이언트 측은 절대 게임 모드가 존재하거나 접근하려고 하면 안 된다.&lt;/b&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;/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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;여러 게임 모드의 활용&lt;/b&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;게임 모드를 활용하면 같은 맵에서 여러 규칙으로 진행되는 게임들을 만들 수 있다. 맵에서 사용하는 게임 모드를 교체하면 다른 게임 규칙이 적용되기 때문에, 원하는 방식에 맞게 AGameMode(또는 AGameModeBase)를 상속받아 게임 모드를 만든다면, 게임 형식, 미션 유형, 승리 조건 등을 바꿔 데스매치, 폭파 임무, 정령 전 등 다양한 게임을 즐길 수 있다.&lt;/li&gt;
&lt;li&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;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;게임 모드의 프로퍼티(Property)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MLsGo/dJMcacuGwir/94gKP7EhfJ84R0mILylwrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MLsGo/dJMcacuGwir/94gKP7EhfJ84R0mILylwrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MLsGo/dJMcacuGwir/94gKP7EhfJ84R0mILylwrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMLsGo%2FdJMcacuGwir%2F94gKP7EhfJ84R0mILylwrk%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;594&quot; height=&quot;393&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디폴트 폰 클래스(APawn)&lt;/li&gt;
&lt;li&gt;HUD 클래스(AHUD)&lt;/li&gt;
&lt;li&gt;플레이어 컨트롤러 클래스(APlayerController)&lt;/li&gt;
&lt;li&gt;게임 스테이트 클래스(AGameState)&lt;/li&gt;
&lt;li&gt;플레이어 스테이트 클래스(APlayerState)&lt;/li&gt;
&lt;li&gt;관전자 클래스(ASpectatorPawn)&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;디폴트 폰 클래스(APawn)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;123&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPkeSS/dJMcaiuTUsf/PtlC4yhK5HJ0St2akPWBB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPkeSS/dJMcaiuTUsf/PtlC4yhK5HJ0St2akPWBB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPkeSS/dJMcaiuTUsf/PtlC4yhK5HJ0St2akPWBB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPkeSS%2FdJMcaiuTUsf%2FPtlC4yhK5HJ0St2akPWBB1%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;123&quot; height=&quot;216&quot; data-origin-width=&quot;123&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;폰(Pawn)은 컨트롤러로 조종할 수 있는 액터(Actor)를 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;게임 플레이 모드에 진입하면 컨트롤러에 빙의(Possess)된 디폴트 폰이 스폰된다. 디폴트 폰은 빙의된 컨트롤러의 종류에 따라 플레이어가 직접 조종할 수도, AI가 조종할 수도 있다.&lt;/li&gt;
&lt;li&gt;게임에 존재하는 모든 플레이어가 같은 몸뚱이(?)를 사용한다면 디폴트 폰 클래스를 사용해도 무방하지만, 플레이어마다 생김새부터 선택한 캐릭터의 메커니즘까지 크게 다르다면, 디폴트 폰 클래스를 비워두고 따로 생성하는 로직을 갖게 된다.&lt;/li&gt;
&lt;/ul&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;b&gt;HUD 클래스(Head-Up Display, AHUD)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;137&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b443i7/dJMcaiuTUsh/N9tRGlBQQZksnx45uWvpH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b443i7/dJMcaiuTUsh/N9tRGlBQQZksnx45uWvpH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b443i7/dJMcaiuTUsh/N9tRGlBQQZksnx45uWvpH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb443i7%2FdJMcaiuTUsh%2FN9tRGlBQQZksnx45uWvpH1%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;137&quot; height=&quot;216&quot; data-origin-width=&quot;137&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qP2lH/dJMcaiuTUsF/tN19Yt34vAAdHZ65kR2oKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qP2lH/dJMcaiuTUsF/tN19Yt34vAAdHZ65kR2oKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qP2lH/dJMcaiuTUsF/tN19Yt34vAAdHZ65kR2oKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqP2lH%2FdJMcaiuTUsF%2FtN19Yt34vAAdHZ65kR2oKk%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;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&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;HUD는 플레이어 화면 가장 앞에 표시되는 UI로&lt;/b&gt;, 킬로그, Hp 바 등 게임 내 오브젝트에 가려지지 않는 UI를 표현할 때 사용한다.&lt;/li&gt;
&lt;li&gt;전통적으로 HUD는 유저와의 상호작용(Ex. 클릭, 호버)을 하지 않는 UI를 의미하기 때문에, AHUD 클래스가 표시하는 UI들도 유저와의 상호작용이 안된다. 그런데 시간이 지나면서 상호작용이 가능한 HUD가 필요해지기도 했고, UE4에 AHUD보다 좋은 UMG(Unreal Montion Graphic)라는 새로운 UI 제작 툴이 개발되면서, UMG로 HUD를 만드는 것이 추세가 되었다. 따라서, 요즘에는 GameMode에 AHUD 클래스 프로퍼티를 비워두고 UMG로 만든 HUD 위젯을 PlayerController에 넣어놓고 BeginPlay에 뷰포트에 추가하는 방식으로 진행한다.&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;플레이어 컨트롤러 클래스(APlayerController)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;135&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RuQr4/dJMcadG7ZKX/9trYyQckQ0Msno2Hgf0c2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RuQr4/dJMcadG7ZKX/9trYyQckQ0Msno2Hgf0c2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RuQr4/dJMcadG7ZKX/9trYyQckQ0Msno2Hgf0c2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRuQr4%2FdJMcadG7ZKX%2F9trYyQckQ0Msno2Hgf0c2K%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;135&quot; height=&quot;211&quot; data-origin-width=&quot;135&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;/figure&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;플레이어의 입력(Input)과 카메라, UI, 서버 통신을 담당하는 플레이어 대표 객체.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;클라이언트, 서버 양쪽에 존재하지만 권한은 다르다.&lt;/li&gt;
&lt;li&gt;주요 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 측 PC: 입력을 처리하고, HUD/UI를 제어한다.&lt;/li&gt;
&lt;li&gt;서버 측 PC: 클라이언트의 명령을 수신하고 검증한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PlayerController는 네트워크에서 플레이어의 세션 단위를 나타내며, RPC(Remote Procedure Call)를 통해 서버와 클라이언트 간 요청을 주고받는다.&lt;/li&gt;
&lt;/ul&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;b&gt;게임 상태 클래스(AGameState)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;130&quot; data-origin-height=&quot;217&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLR9GG/dJMcai2JQXU/38UhXiZAKrLV7s2xkDVTx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLR9GG/dJMcai2JQXU/38UhXiZAKrLV7s2xkDVTx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLR9GG/dJMcai2JQXU/38UhXiZAKrLV7s2xkDVTx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLR9GG%2FdJMcai2JQXU%2F38UhXiZAKrLV7s2xkDVTx0%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;130&quot; height=&quot;217&quot; data-origin-width=&quot;130&quot; data-origin-height=&quot;217&quot;/&gt;&lt;/span&gt;&lt;/figure&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;현재 진행 중인 게임의 상태 데이터를 관리하는 &amp;ldquo;공유&amp;rdquo; 컨테이너 객체&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;GameMode때와 똑같이 AGameStateBase와 AGameState가 있는데, AGameStateBase는 기본 상태를, AGameState는 좀 더 고수준의 게임 정보(Ex. 스코어, 플레이어 목록 등)를 포함한다.&lt;/li&gt;
&lt;li&gt;AGameState는 서버에서 생성되며, 모든 클라이언트에게 복제(Replicate)된다.&lt;/li&gt;
&lt;li&gt;주요 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replicated 변수를 통해 클라이언트와 동기화&lt;/li&gt;
&lt;li&gt;현재 매치 상태(MatchState) 관리&lt;/li&gt;
&lt;li&gt;연결된 모든 APlayerState 목록 관리(PlayerArray)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AGameState가 저장하는 상태 데이터 예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게임 플레이 시간&lt;/li&gt;
&lt;li&gt;접속한 플레이어 목록&lt;/li&gt;
&lt;li&gt;각 플레이어의 상태(APlayerState)&lt;/li&gt;
&lt;li&gt;게임 시작 여부&lt;/li&gt;
&lt;li&gt;현재 GameMode의 기반 클래스&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;플레이어 상태 클래스(APlayerState)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;129&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D9U7s/dJMcaaDDqoG/oIe0eoy2pwq5EShWfpeyfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D9U7s/dJMcaaDDqoG/oIe0eoy2pwq5EShWfpeyfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D9U7s/dJMcaaDDqoG/oIe0eoy2pwq5EShWfpeyfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD9U7s%2FdJMcaaDDqoG%2FoIe0eoy2pwq5EShWfpeyfk%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;129&quot; height=&quot;212&quot; data-origin-width=&quot;129&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&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;각 플레이어의 상태 데이터를 관리하는 &amp;ldquo;공유&amp;rdquo; 컨테이너 객체&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;APlayerState는 서버에서 생성되며, 모든 클라이언트에게 복제(Replicate)된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 클라이언트는 모든 플레이어의 APlayerState를 가지고 있는다. 예를 들어, 게임에 참여 중인 다른 플레이어가 5명이라면, 각 클라이언트는 본인 APlayerState까지 포함해서 총 6개의 APlayerState가 존재하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;APlayerController가 &amp;ldquo;플레이어의 의식&amp;rdquo;이라면, APlayerState는 &amp;ldquo;플레이어의 신분증&amp;rdquo;과 같은 역할이다.&lt;/li&gt;
&lt;li&gt;APlayerState가 저장하는 상태 데이터 예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플레이어 점수&lt;/li&gt;
&lt;li&gt;플레이어 이름&lt;/li&gt;
&lt;li&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;b&gt;APlayerState에 좀 더 알아보기: 캐릭터에 종속되지 않는 게임 정보는 어떻게?&lt;/b&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;/b&gt; 물론 원본 데이터는 서버가 가지고 있어 데이터 자체가 사라지진 않겠지만 Pawn 클래스 안에서 관리한다면(또는 부착된 전용 ActorComponent에서 관리한다면), Pawn을 리스폰할 때마다 불필요한 복제가 발생한다. 그래서 &amp;ldquo;캐릭터 인스턴스의 생명주기로부터 자유로우면서 소유한 클라이언트에게만 복제되는 것&amp;rdquo;을 원했다.&lt;/li&gt;
&lt;li&gt;후보군은 여러 개 있다. &lt;b&gt;1) 맘에 들지 않지만 가능은 한 ActorComponent 부착방식, 2) 소유한 클라이언트에게만 복제되는 APlayerController, 3) 모든 클라이언트에게 복제되는 APlayerState 등이 있다. 그럼에도 APlayerState가 가장 적합하다고 생각하는 이유는 아래와 같다.&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ActorComponent 방식은 위에서 말했듯 비효율적이다.&lt;/li&gt;
&lt;li&gt;APlayerController는 복제에 대해선 안전하지만, 플레이어의 &amp;ldquo;제어(Control)&amp;rdquo;를 하기 위한 객체이므로 맥락상 데이터를 저장하는 공간으로 쓰는 건 좀 그렇다.&lt;/li&gt;
&lt;li&gt;APlayerState는 이름부터 플레이어의 상태를 저장하는 객체이다. Pawn과 상관없이 접속한 플레이어의 정보를 담기에 적합한 위치이다.&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;b&gt;APlayerState의 문제점: 모든 클라이언트에게 내 APlayerState가 복제된다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내 APlayerState는 현재 게임 내 존재하는 모든 플레이어의 클라이언트에 복제가 된다. 비공개 데이터를 다른 클라이언트가 가지고 있다면, 이건 문제가 있다.&lt;/li&gt;
&lt;li&gt;다행히도 APlayerState에서 소유한 클라이언트에게만 복제하는 방식이 존재한다. 아래와 같이 DOREPLIFETIME_CONDITION(..., GoldAmount, COND_OwnerOnly)를 사용했다면, 이 변수의 값은 해당 APlayerState를 소유한 클라이언트에게만 복제된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;UPROPERTY(ReplicatedUsing = OnRep_PrivateData, ReplicatedCondition = COND_OwnerOnly)
FString PrivateData;

DOREPLIFETIME_CONDITION(..., GoldAmount, COND_OwnerOnly)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 복제는 논리적으로 막을 수 있다는 것. 하지만 해당 변수 자체가 없는 상태로 APlayerState를 만들지는 않는다. 그래서 다른 클라이언트에 존재하는 내 APlayerState는 값이 없는 빈 깡통인 상태인 비공개 변수들이 존재하게 된다.&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;관전자 클래스(Spectator Class)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;129&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/or56B/dJMcahQin31/kPMGjpMIQvCKecPD3djBS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/or56B/dJMcahQin31/kPMGjpMIQvCKecPD3djBS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/or56B/dJMcahQin31/kPMGjpMIQvCKecPD3djBS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2For56B%2FdJMcahQin31%2FkPMGjpMIQvCKecPD3djBS1%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;129&quot; height=&quot;213&quot; data-origin-width=&quot;129&quot; data-origin-height=&quot;213&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex. Free Cam, 각 플레이어 시점 등등&amp;hellip;&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GameMode 관련 클래스 관계도&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKHdIh/dJMcafStO3r/sTXsH1RfM3YV6Rirm5j7O0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKHdIh/dJMcafStO3r/sTXsH1RfM3YV6Rirm5j7O0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKHdIh/dJMcafStO3r/sTXsH1RfM3YV6Rirm5j7O0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKHdIh%2FdJMcafStO3r%2FsTXsH1RfM3YV6Rirm5j7O0%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;728&quot; height=&quot;435&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;간단하게 알아보는 플레이어 입장 과정&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버가 새로 연결된 플레이어의 APlayerController 초기화&lt;/li&gt;
&lt;li&gt;플레이어가 월드에 입장하기 위한 세팅 시작&lt;/li&gt;
&lt;li&gt;폰 초기화&lt;/li&gt;
&lt;li&gt;플레이어 컨트롤러를 폰에 빙의시킴&lt;/li&gt;
&lt;li&gt;-완-&lt;/li&gt;
&lt;/ol&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;b&gt;간단하게 알아보는 게임 진행 중 과정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[클라이언트 입력] &amp;rarr; APlayerController &amp;rarr; (서버 통신)
                                      &amp;darr;
                                AGameMode (게임 규칙)
                                      &amp;darr;
                                AGameState (현재 상태)
                                      &amp;darr;
                       APlayerState (플레이어별 데이터)
                                      &amp;darr;
                               APawn (물리적 표현)                         
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가로 알면 좋은 네트워크 관련 객체들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AGameSession&lt;/b&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;/li&gt;
&lt;li&gt;서버 전용 클래스이며, 클라이언트에는 존재하지 않는다.&lt;/li&gt;
&lt;li&gt;주요 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플레이어 접속 허용/거부 (ApproveLogin)&lt;/li&gt;
&lt;li&gt;최대 플레이어 수 관리 (MaxPlayers)&lt;/li&gt;
&lt;li&gt;세션 시작/종료 관리(HandleMatchHasStarted, HandleMatchHasEnded)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AGameNetworkManager&lt;/b&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;/li&gt;
&lt;li&gt;서버 성능과 네트워크 트래픽을 관리하는 엔진 내부 관리자 역할을 한다.&lt;/li&gt;
&lt;li&gt;서버 전용 클래스이며, 클라이언트에는 존재하지 않는다.&lt;/li&gt;
&lt;li&gt;주요 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트의 Ping, 패킷 손실, 속도 제한 모니터링&lt;/li&gt;
&lt;li&gt;서버의 대역폭 제한, 업데이트 빈도 조절&lt;/li&gt;
&lt;li&gt;딜레이가 심한 클라이언트 감지 및 제어(치트 감지, 움직임 예측에 적용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Today I Learn</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/616</guid>
      <comments>https://pledge24.tistory.com/616#entry616comment</comments>
      <pubDate>Wed, 12 Nov 2025 02:32:41 +0900</pubDate>
    </item>
    <item>
      <title>[TIL][C++] 251110 MMO 서버 개발 133일차: [언리얼] SpawnActor에 따른 BeginPlay() 호출 시점, BP의 BeginPlay가 C++ BeginPlay보다 먼저 호출되는 이유</title>
      <link>https://pledge24.tistory.com/615</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] SpawnActor에 따른 BeginPlay() 호출 시점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이전에도 한 번 다룬 적이 있는데, 새롭게 알게 되거나 정리된 내용이 많이 생겨서 다시 다루려고 한다.&lt;/li&gt;
&lt;li&gt;SpawnActor의 경우, 시점에 따라 BeginPlay()가 호출되는 타이밍이 다르다. 거두절미하고 결과만 이야기하자면 아래와 같다.&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;BeginPlay() 호출 시점&lt;/b&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;게임 플레이 전(Ex. LoadMap()을 호출한 경우)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpawnActor() 함수의 반환 시점에 BeginPlay()가 호출되지 않고, &lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;레벨이 시작될 때 AActor::BeginPlay()가 호출된다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;게임 플레이 중(Ex. 레벨이 열려있는 상황)&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 style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;SpawnActor() 함수가 스폰한 액터는 즉시 BeginPlay()까지 호출한다.&lt;/b&gt;&lt;/span&gt; (이에 대해서 이야기가 좀 갈린다. 어디선 SpawnActor를 호출한 프레임의 끝 또는 다음 프레임 시작 부분에 BeginPlay()가 스폰한 액터들이 한 번에 호출된다고도 한다. 그러면서도 런타임엔 그 즉시 실행되는 것처럼 보인다나 뭐라나&amp;hellip; 암튼 애매한 부분이 많아서 그냥 즉시 호출한다고 생각하는 게 편하다)&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다시 다루는 직후 초기화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 이야기를 다시 꺼내게 된 사유는 SpawnActor() 함수 호출 직후에 내부 데이터를 초기화하는 경우, 의도된 순서대로 작동하지 않았기 때문이다. 저번에도 이 방식이 잘못되었다는 것을 알았지만 이번에 한 번 더하면서 확실해졌다. 상황은 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;OutMonster = GetWorld()-&amp;gt;SpawnActor&amp;lt;AMonster&amp;gt;(MonsterBPClass, Location, Rotation);
if(!OutMonster)
	OutMonster-&amp;gt;SetDefaultMonsterData(MonsterData);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpawnActor다음에 반환된 액터를 초기화하는 코드이다. 결론적으로 이 코드가 잘못된 것은 대부분의 경우에서 AActor::BeginPlay()가 초기화 함수(여기선 SetDefault 어쩌고&amp;hellip;)보다 먼저 호출된다는 것이다. 이는 비단 C++ 뿐만 아니라 블루프린트에서도 똑같이 적용된다. 즉, 결론은 아래와 같다.&lt;/li&gt;
&lt;/ul&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;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;게임 플레이 중 SpawnActor로 스폰한 액터의 BeginPlay는 다음 코드 라인(C++), 연결된 다음 노드(BP)보다 먼저 호출된다&lt;/span&gt;&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] BP의 BeginPlay가 C++ BeginPlay보다 먼저 호출되는 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SpawnActor호출 시 BeginPlay가 언제 호출되는지 알아보려고 Log를 찍어보던 도중, 이상한 부분이 발견되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;LogBlueprintUserMessages: [BP_Beginner_RangedMinion_C_0] 5
LogTemp: ACreature::BeginPlay TestNum: 6
LogTemp: AMonster::BeginPlay TestNum: 7
LogTemp: AMonster::SetDefaultMonsterData TestNum: 8
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 로그는 몬스터 스폰 시 각 BeginPlay마다 찍히는 것으로, 각 UE_LOG마다 ++TestNum를 넣어 출력마다 1씩 TestNum이 증가하도록 만든 것이다. 이에 따라 BeginPlay 호출 순서가 아래와 같다는 것을 알 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. BP::BeginPlay() 호출
2. ACreature::BeginPlay() 호출
3. AMonster::BeginPlay() 호출
4. Monster-&amp;gt;SetDefaultMonsterData() 호출 // SpawnActor 다음 코드
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SetDefaultMonsterData에 대해서는 위에서 다뤘으니 위치가 이상하지 않다는 것을 알게 되었는데, BP의 BeginPlay가 가장 먼저 호출되는 것은 이해가 가지 않았다. 최하위 클래스인 BP의 BeginPlay가 가장 먼저 호출되었으니 말이다. &amp;lsquo;이러면 안 되는 게 아닌가?&amp;lsquo; 싶었다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론: 이게 정상적인 출력이었다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나는 당연히 BP의 BeginPlay가 C++의 BeginPlay를 오버라이드한다고 생각했다. 하지만 그렇지 않았다. AActor에 가보면 이 둘은 서로 다른 거였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// AActor.h의 일부
protected:
	/** Event when play begins for this actor. */
	UFUNCTION(BlueprintImplementableEvent, meta=(DisplayName = &quot;BeginPlay&quot;))
	ENGINE_API void ReceiveBeginPlay();

	/** Overridable native event for when play begins for this actor. */
	ENGINE_API virtual void BeginPlay();
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드는 AActor.h의 일부이다. ReceiveBeginPlay가 뭔가 싶겠지만 이게 바로 BP를 만들면 Event Graph에 기본으로 있는 세 이벤트 중 하나인 BeginPlay 이벤트이다. 그리고 우리가 C++에서 BeginPlay를 정의할 때 사용하는 함수는 그 아래에 있는 함수이다. 즉, 정리하자면 아래와 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ReceiveBeginPlay(): BP의 BeginPlay 이벤트&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BeginPlay(): C++의 BeginPlay 함수&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결국 BP에서 사용하는 BeginPlay와 C++에서 사용하는 BeginPlay는 서로 다른 것이다!&lt;/b&gt; 그리고 이 BP의 BeginPlay(ReceiveBeginPlay 함수)는 AActor::BeginPlay 안에서 아래와 같이 마지막 부분에서 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;void AActor::BeginPlay()
{
	// 많고 많은 코드들이 지나고...

	ReceiveBeginPlay();

	ActorHasBegunPlay = EActorBeginPlayState::HasBegunPlay;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이렇게 AActor의 BeginPlay에서 BP의 BeginPlay 이벤트를 호출하는 구조 때문에 BP가 최하위 클래스임에도 불구하고 AActor의 하위 C++ 클래스들보다 먼저 호출되는 것이었다.&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 이렇게 만들었을까?&lt;/b&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;/li&gt;
&lt;/ul&gt;</description>
      <category>Today I Learn</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/615</guid>
      <comments>https://pledge24.tistory.com/615#entry615comment</comments>
      <pubDate>Tue, 11 Nov 2025 01:37:55 +0900</pubDate>
    </item>
    <item>
      <title>[TIL][C++] 251107 MMO 서버 개발 132일차: 블루프린트는 오버로딩을 지원하지 않는다, TSubclassOf에서 들어있는 클래스 정보 가져오기, 기타 등등...</title>
      <link>https://pledge24.tistory.com/614</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] 블루프린트는 오버로딩을 지원하지 않는다.&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C++함수는 오버로딩을 지원하기 때문에 아래와 같이 작성하는 것이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;AMonster* SpawnMonster(int32 TemplateId, const FTransform&amp;amp; Transform);
AMonster* SpawnMonster(int32 TemplateId, const FVector&amp;amp; Location, const FRotator&amp;amp; Rotation);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 두 SpawnMonster는 서로 다른 시그니처를 가진 함수이기 때문에 내부적으로 다른 함수로 취급하게 된다. 그런데 이런 오버로딩 기능이 블루프린트에서는 지원하지 않는다. 따라서, 아래와 같은 코드는 오류가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;UFUNCTION(BlueprintCallable, Category=&quot;Spawn&quot;)
AMonster* SpawnMonster(int32 TemplateId, const FTransform&amp;amp; Transform);

UFUNCTION(BlueprintCallable, Category=&quot;Spawn&quot;)
AMonster* SpawnMonster(int32 TemplateId, const FVector&amp;amp; Location, const FRotator&amp;amp; Rotation);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] TSubclassOf에서 들어있는 클래스 정보 가져오기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TsubclassOf는 내부적으로 UClass* 포인터를 들고 있다. 이 포인터를 통해 저장된 클래스 정보에 접근할 수 있다. UClass*는 Get() 함수를 통해 얻을 수 있다.&lt;/li&gt;
&lt;li&gt;참고로 BP 클래스인 경우 일반적으로 이름 뒤에 '_C'가 붙습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// TSubclassOf&amp;lt;AActor&amp;gt;타입
if(MonsterBPClass)
{
	UClass* ClassRef = MonsterBPClass.Get();
	
	// 1) 클래스 이름 반환 UClass::GetName()
	// FString을 반환한다.
	FString ClassName = ClassRef-&amp;gt;GetName();
	
	// 예: &quot;BP_Goblin_C&quot;
  UE_LOG(LogTemp, Warning, TEXT(&quot;Class Name: %s&quot;), *ClassName);   
  
  // 2) 클래스 파일 전체 경로 반환 UClass::GetFullName()
  // 똑같이 FString을 반환한다.
  FString FullClassName = ClassRef-&amp;gt;GetFullName();
  
  // 예: &quot;BlueprintGeneratedClass /Game/Blueprints/BP_Goblin.BP_Goblin_C&quot;
  UE_LOG(LogTemp, Warning, TEXT(&quot;Full Class Name: %s&quot;), *FullClassName);   
  
  // 3) 클래스 파일 상대 경로 반환 UClass::GetPathName()
  // 똑같이 FString을 반환한다.
  FString ClassPathName = MonsterBPClass-&amp;gt;GetPathName();
    
  // 예: &quot;/Game/Blueprints/BP_Goblin.BP_Goblin_C&quot;
  UE_LOG(LogTemp, Warning, TEXT(&quot;Class Path Name: %s&quot;), *ClassPathName);
}
&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[언리얼] 기타 알게 된 내용&lt;/h3&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;UPROPERTY 매크로에 Category 지정자를 사용할 때, 값을 큰따옴표 없이 적는 것은 무슨 의미일까?&lt;/b&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;UPROPERTY(Category = MyCategory) // 문제없음
UPROPERTY(Category = My Category) // 문제 있음
UPROPERTY(Category = &quot;My Category&quot;) // 문제 없음
&lt;/code&gt;&lt;/pre&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;meta = (AllowPrivateAccess = &quot;true&quot;)는 무슨 용도일까?&lt;/b&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;TPS 템플릿을 C++로 받으면 아래와 같이 InputAction의 UPROPERTY가 설정되어 있는 것을 볼 수 있는데,&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;private:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = &quot;true&quot;))
	UInputAction* JumpAction;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = &quot;true&quot;))
	UInputAction* MoveAction;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = &quot;true&quot;))
	UInputAction* LookAction;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 meta = (AllowPrivateAccess = &quot;true&quot;)는 &amp;ldquo;C++ 클래스의 private로 선언된 변수를 BP에서 접근 가능&amp;rdquo;하도록 하는 용도이다.&lt;/li&gt;
&lt;li&gt;왜 하위 클래스인 BP에서 상위 C++ 클래스의 priavate 변수에 접근할 수 있도록 허용하는가? 의문이 들기는 하는데, 아래와 같은 이유로 사용한다고 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;C++ 캡슐화 유지: C++ 코드 내는 private로 유지하여 C++ 코드에서 직접 접근하여 변경하는 것을 막고 싶을 때(BP만 제외하고)&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;UPROPERTY()에서 EditAnywhere, BlueprintReadOnly를 동시에 사용하면 정확히 어떤 효과가 나는가?&lt;/b&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;해당 변수는 에디터에서 수정 가능, But 블루프린트 스크립트에서 쓰기 불가능(읽기 전용)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에디터에서의 효과 EditAnywhere: 디테일 패널에 해당 변수가 노출됨 + 해당 변수가 속한 클래스의 어떤 인스턴스에서든(Ex. 레벨에 배치된 인스턴스 or BP 기본값) 쓰기 작업 가능&lt;/li&gt;
&lt;li&gt;BP에서의 효과(BlueprintReadOnly): 블루프린트에서 읽는 것만 가능하고 쓰는 것은 안됨. 즉, Get만 가능하고 Set은 안된다.&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;
&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;블루프린트 노드에서 UE_LOG 역할을 하는 노드는?&lt;/b&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;따로 있는 게 아니라 똑같이 Print String 노드를 사용하면 된다. 이때 Development Only를 열어서 Print to Screen만 끄고 Print to Log만 켜주면 된다. 그러면 뷰포트에는 출력이 안되고 Output Log에만 해당 로그가 출력된다. (UE_LOG처럼)&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;데이터 테이블 순회 방법 재정리(?)&lt;/b&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;/b&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;Get Data Table Row Names 노드 사용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DT에 있는 모든 행의 이름이 FName 배열에 넣어져 반환됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;For Each Loop 노드 연결&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반복문을 돈다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Get Data Table Row&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;행의 이름(FName)으로 행을 반환받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;C++ 버전&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;TArray&amp;lt;FMonsterData*&amp;gt; AllRows;
static const FString Context(TEXT(&quot;LoadMonsterAsset&quot;));

MonsterDataTable-&amp;gt;GetAllRows&amp;lt;FMonsterData&amp;gt;(Context, AllRows);
for (const FMonsterData* Row : AllRows)
{
    if (!Row)
}
&lt;/code&gt;&lt;/pre&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 style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;언리얼 bRunPhysicsWithNoController의 역할&lt;/b&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;액터(Actor)에 컨트롤러가 연결되어 있지 않은 상태에서 물리 시뮬레이션을 계속 돌릴 건지 여부를 결정하는 플래그. 기본값은 false로 되어있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;true로 설정되었을 때의 효과&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤 중인 캐릭터가 들고 있던 검이 UnPossess 되면, 중력의 영향을 받으며 떨어짐&lt;/li&gt;
&lt;li&gt;플레이어의 캐릭터가 죽어 컨트롤러가 UnPossess 되면 해당 캐릭터가 랙돌이 됨.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rArr; 대충 컨트롤러가 없는 액터의 물리적 상호작용이 유지된다고 보면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Today I Learn</category>
      <author>pledge24</author>
      <guid isPermaLink="true">https://pledge24.tistory.com/614</guid>
      <comments>https://pledge24.tistory.com/614#entry614comment</comments>
      <pubDate>Sat, 8 Nov 2025 23:43:47 +0900</pubDate>
    </item>
  </channel>
</rss>