<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jisub's log</title>
        <link>https://velog.io/</link>
        <description>게임 개발을 좋아하는 컴공생</description>
        <lastBuildDate>Wed, 12 Aug 2026 09:40:51 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. jisub's log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jisub_shim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Unity] FixedUpdate에 대한 고찰]]></title>
            <link>https://velog.io/@jisub_shim/Unity-FixedUpdate%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</link>
            <guid>https://velog.io/@jisub_shim/Unity-FixedUpdate%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</guid>
            <pubDate>Wed, 12 Aug 2026 09:40:51 GMT</pubDate>
            <description><![CDATA[<p>Unity를 쓰면 &quot;물리 처리는 FixedUpdate에 넣어라&quot;는 말을 많이 듣습니다.
그런데 왜 그래야 하는지, FixedUpdate가 정확히 언제 몇 번 불리는지는 뭉뚱그려 알고 넘어가기 쉽습니다.</p>
<p>결국 Unity의 Transform 세계와 물리 엔진의 세계는 별개이고, FixedUpdate는 그 둘을 잇는 창구다. 라는 점을 기억하고 아래 내용을 봐주시면 감사하겠습니다.</p>
<h2 id="fixedupdate는-한-프레임에-0번일-수도-여러-번일-수도-있다">FixedUpdate는 한 프레임에 0번일 수도, 여러 번일 수도 있다.</h2>
<p>Unity는 FixedUpdate를 &quot;프레임마다 한 번&quot;이 아니라 누산기(accumulator) 방식으로 실행합니다.</p>
<pre><code>// 엔진 내부, 의사코드
accumulator += Mathf.Min(realDeltaTime, Time.maximumDeltaTime);

while (accumulator &gt;= Time.fixedDeltaTime)
{
    FixedUpdate();                        // 내 스크립트
    PhysicsSimulate(Time.fixedDeltaTime); // 엔진 물리 적분
    //
    // 여기서 OnCollisionXXX, OnTriggerXXX 발생
    //
    accumulator -= Time.fixedDeltaTime;
}

Update();
LateUpdate();
Render();</code></pre><p>while 루프를 통해 밀린 시간이 있으면 따라잡을 때까지 반복합니다.</p>
<p>fixedDeltaTime의 기본값은 0.02초, 즉 50Hz입니다. fps별로 실제 호출 횟수를 계산하면 아래와 같습니다.</p>
<table>
<thead>
<tr>
<th>프레임률</th>
<th>프레임 시간</th>
<th>프레임 당 FixedUpdate</th>
<th>1초간 Update</th>
<th>1초간 FixedUpdate</th>
</tr>
</thead>
<tbody><tr>
<td>200fps</td>
<td>5ms</td>
<td>0, 0, 0, 1 반복</td>
<td>200회</td>
<td><strong>50회</strong></td>
</tr>
<tr>
<td>60fps</td>
<td>16.7ms</td>
<td>대부분 1, 가끔 0</td>
<td>60회</td>
<td><strong>50회</strong></td>
</tr>
<tr>
<td>25fps</td>
<td>40ms</td>
<td>항상 2</td>
<td>25회</td>
<td><strong>50회</strong></td>
</tr>
</tbody></table>
<p>Update 횟수는 기기마다 제각각이지만 FixedUpdate 횟수는 어느 기기에서든 50으로 고정됩니다.</p>
<h3 id="물리는-별도-쓰레드가-아니다">물리는 별도 쓰레드가 아니다.</h3>
<p>여기서 헷갈리기 쉬운 부분은 FixedUpdate는 프레임과 무관하게 돈다는 말이 비동기로 돈다는 뜻이 아닙니다.</p>
<p>실행 순서는 확실히 정해져 있습니다. 같은 메인 쓰레드에서 FixedUpdate(n번) -&gt; Update -&gt; LateUpdate 순으로 순차 실행됩니다. 분리된 것은 실행 시간이 아니라 실행 빈도입니다. 별도의 쓰레드가 아닌 별도의 클럭(시계)으로 돌아간다고 생각하는게 맞습니다.</p>
<h3 id="왜-이렇게-만들었을까">왜 이렇게 만들었을까?</h3>
<p>물리 적분은 <code>속도 += 가속도 * dt</code> 같은 누적 계산이라 dt가 달라지면 결과 궤적이 달라집니다.
만약 물리를 프레임마다 한 번씩 가변 dt로 돌린다면</p>
<ul>
<li>144fps 게이밍 pc -&gt; 점프 높이 3.02m</li>
<li>30fps 노트북 -&gt; 점프 높이 2.87m</li>
</ul>
<p>같은 조작인데 컴퓨터 성능에 따라 결과가 달라지게 됩니다.
그러나 dt를 항상 0.02로 고정하면 어떤 기기에서든 계산이 똑같이 재현됩니다. 이것이 목적입니다.</p>
<hr>
<h2 id="한-프레임의-실제-구조">한 프레임의 실제 구조</h2>
<p>Unity 6는 프레임 구조를 API로 열어놨습니다.
PlayerLoop.GetCurrentPlayerLoop()를 재귀적으로 출력하면 엔진이 한 프레임에 실제로 실행하는 것들이 전부 나옵니다.</p>
<pre><code>using System.Text;
using UnityEngine;
using UnityEngine.LowLevel;

public class PlayerLoopProbe : MonoBehaviour
{
    [ContextMenu(&quot;현재 프레임 루프 구조 출력&quot;)]
    private void Dump()
    {
        var sb = new StringBuilder();
        Append(sb, PlayerLoop.GetCurrentPlayerLoop(), 0);
        Debug.Log(sb.ToString());
    }

    private static void Append(StringBuilder sb, PlayerLoopSystem system, int depth)
    {
        if (system.type != null)
            sb.Append(&#39; &#39;, (depth - 1) * 2).AppendLine(system.type.Name);

        if (system.subSystemList == null) return;

        foreach (var sub in system.subSystemList)
            Append(sb, sub, depth + 1);
    }
}</code></pre><p>출력을 요약하면 아래와 같습니다.</p>
<pre><code>Initialization
EarlyUpdate
FixedUpdate                          &lt;- 이 블록 전체가 0~N회 반복
  ScriptRunBehaviourFixedUpdate      &lt;- 내 FixedUpdate
  PhysicsFixedUpdate                 &lt;- 물리 시뮬레이션
  ScriptRunDelayedFixedFrameRate     &lt;- WaitForFixedUpdate 코루틴
PreUpdate
  PhysicsUpdate
  NewInputUpdate                     &lt;- Input System
Update
  ScriptRunBehaviourUpdate           &lt;- 내 Update
  ScriptRunDelayedDynamicFrameRate   &lt;- yield null / WaitForSeconds
PreLateUpdate
  ScriptRunBehaviourLateUpdate       &lt;- 내 LateUpdate
PostLateUpdate
  FinishFrameRendering
  PlayerSendFrameComplete            &lt;- 프레임 끝</code></pre><p>여기서 ScriptRunBehaviourFixedUpdate(내 FixedUpdate)의 다음 줄이 PhysicsFixedUpdate입니다.
뒤에서 다시 다루겠지만, 이 인접성이 물리는 FixedUpdate 안에서 다뤄야 하는 가장 직접적인 근거입니다.</p>
<hr>
<h2 id="maximum-allowed-timestep">Maximum Allowed Timestep</h2>
<p>FixedUpdate가 무거우면 이런 일이 일어날 수 있습니다.
<code>프레임이 느려짐 -&gt; 다음 프레임에 FixedUpdate 더 많이 호출 -&gt; 프레임이 더 느려짐 -&gt; 더 많이 호출 -&gt; ...(반복)</code>
이걸 막는게 Edit &gt; Project Settings &gt; Time의 Maximum Allowed Timestep입니다. 위에서 확인했던 누적 시간(accumulator)을 이 값에서 잘라버립니다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/1a1ff059-cdbb-4f89-acda-205a2a033657/image.png" alt=""></p>
<blockquote>
<p>한 프레임 최대 호출 수 = maximumDeltaTime ÷ fixedDeltaTime = 0.3333 ÷ 0.02 ≈ 16회</p>
</blockquote>
<h3 id="그럼-초당-50회가-깨지는-것-아닌가">그럼 초당 50회가 깨지는 것 아닌가?</h3>
<p>맞습니다. 깨집니다. 다만 여기서 발생하는 게 어떤 종류의 오류인지 구분하는 것이 중요합니다.
시간이 잘려나가도 실행되는 각 스텝의 dt는 여전히 정확히 0.02입니다. 스텝을 크게 늘려서 따라잡는 게 아니라 스텝 자체를 버립니다.
만약 스텝을 늘렸다면 벽을 관통하는 터널링 현상, 스프링 발산 등의 물리 문제가 전부 일어나게 됩니다. Unity는 그걸 안하기 때문에 물리 계산 자체는 언제나 정상입니다.</p>
<p>대신 게임 시간이 실제 시간보다 뒤처집니다. 버려진 시간만큼 Time.time이 실제 시계보다 느려지고, 결과적으로 슬로우모션이 됩니다.
실제로 문제가 되는 것들은 아래와 같습니다.</p>
<ul>
<li>오디오 싱크 - 오디오 자체는 DSP 클럭으로 실제 시간대로 흐릅니다. 게임만 느려지면 리듬 게임 노트가 음악과 어긋납니다.</li>
<li>멀티플레이 - 서버 시간과 클라이언트 시간이 벌어집니다.</li>
<li>제한시간 - Time.time 기준 60초 타이머가 실제로는 63초(예시)가 됩니다.</li>
</ul>
<h3 id="그럼에도-이-설계가-옳은-이유">그럼에도 이 설계가 옳은 이유</h3>
<p>클램프가 없다고 상상해보면 명확합니다. 디버깅 중 브레이크 포인트에 10초 멈췄다가 재개하면 누산기에 10초가 쌓여 있으니 500번 연속 실행됩니다. 화면은 얼어붙고 캐릭터는 순간이동합니다. 알트탭에서도 같은 일이 벌어집니다.</p>
<p>선택지는 둘 뿐입니다.</p>
<ol>
<li>시간을 버린다 (Unity) -&gt; 게임이 잠깐 느려진다</li>
<li>전부 따라잡는다 -&gt; 게임이 멈춘다
프레임이 감당 못하는 상황에서 실시간성을 포기하고 반응성을 지키는 쪽을 선택한 것입니다.</li>
</ol>
<hr>
<h2 id="입력을-fixedupdate에서-읽으면-안되는-이유">입력을 FixedUpdate에서 읽으면 안되는 이유</h2>
<p>Input.GetKeyDown 같은 플래그는 프레임 단위로 세팅되고 리셋됩니다. 물리 루프와 주기가 다릅니다.</p>
<pre><code>// 절대 이렇게 하지 말 것
void FixedUpdate()
{
    if (Input.GetKeyDown(KeyCode.Space))
        _rb.AddForce(Vector3.up * jumpPower, ForceMode.Impulse);
}</code></pre><ul>
<li>해당 프레임에서 FixedUpdate가 2번 돌면 -&gt; 플래그가 두 번 다 true -&gt; 점프력 2배</li>
<li>해당 프레임에서 0번 돌면 -&gt; 다음 프레임에 플래그가 리셋됨 -&gt; 점프 입력 씹힘
이런 경우, 무작위로 발생하고 재현이 어려워서 가끔 점프가 안되는 유령 버그가 됩니다.
올바른 패턴은 Update에서 받아서 FixedUpdate에서 소비하는 것입니다.<pre><code>private bool _jumpQueued;
</code></pre></li>
</ul>
<p>void Update()
{
    // 입력 수집은 프레임 단위로
    if (Input.GetKeyDown(KeyCode.Space)) _jumpQueued = true;
}</p>
<p>void FixedUpdate()
{
    if (!_jumpQueued) return;</p>
<pre><code>_rb.AddForce(Vector3.up * jumpPower, ForceMode.Impulse);
_jumpQueued = false;   // 소비한 쪽에서 내린다</code></pre><p>}</p>
<pre><code>플래그를 Update에서 내리면 소비되기 전에 지워질 수 있기에 조심해야 합니다.

-----------
## AddForce를 Update에서 부르면 안되는 이유
Rigidbody는 힘을 누산기에 모아뒀다가 물리 스텝에서 한 번에 적용하고 비웁니다.
그러나 Update에서 AddForce를 부르면 물리 스텝 사이에 몇 번 불렸느냐가 그대로 결과에 곱해집니다.</code></pre><p>// 프레임률에 따라 결과가 달라진다
void Update()
{
    _rb.AddForce(Vector3.forward * 10f);
}</p>
<pre><code>PC의 성능에 따라 결과가 달라질 수 있습니다.
Time.deltaTime을 곱해도 해결되지 않습니다. 힘 누산기가 비워지는 시점이 프레임이 아니라 물리 스텝이라서, 애초에 곱해야 할 dt가 프레임의 dt가 아니기 때문입니다.
FixedUpdate에 넣으면 호출 1회당 물리 스텝 1회가 보장되므로 이 문제가 원천적으로 사라집니다.

-------------------
## Transform 세계와 물리 세계는 별도다.
여기가 이 글의 결론입니다.
![](https://velog.velcdn.com/images/jisub_shim/post/670dabc3-6d81-401e-b020-f2c8d99956ab/image.png)

내 스크립트가 사는 곳과 실제 물리 계산이 일어나는 곳은 별도입니다. 두 세계는 항상 일치하지 않습니다.
PhysicsFixedUpdate 단계에서 맞춰질 뿐입니다.

그래서 FixedUpdate에서 하는 일은 &quot;물리 세계에 주문서를 넣는 것&quot;이고, 주문이 처리되는 건 바로 다음 줄의 시뮬레이션 단계입니다.

### AutoSyncTransforms
두 세계의 동기화는 양방향인데 비대칭입니다.
- PhysX -&gt; Transform: 시뮬레이션이 끝나면 항상 자동으로 반영
- Transform -&gt; PhysX: 최신 Unity 프로젝트에서는 자동으로 안 함

즉, transform.position을 바꿔도 PhysX는 여전히 옛날 위치를 알고 있습니다.
</code></pre><p>transform.position = newPos;</p>
<p>// PhysX 세계는 아직 갱신 전 -&gt; 옛 위치 기준으로 판정된다
if (Physics.Raycast(transform.position, Vector3.down, out var hit, 1f))
    Debug.Log(hit.collider.name);   // 엉뚱한 결과</p>
<pre><code>
해결책은 둘 중 하나입니다.</code></pre><p>Physics.SyncTransforms();   // 강제로 밀어넣기 (비쌈)
_rb.MovePosition(newPos);   // 애초에 물리 세계에 명령하기</p>
<p>```
기본값이 꺼져 있는 이유는 성능입니다. Transform을 건드릴 때마다 PhysX에 밀어넣으면 오브젝트가 많을 때 비용이 큽니다.
대신 개발자가 두 세계의 존재를 알고 있어야 한다는 부담을 지우긴 합니다.</p>
<hr>
<h2 id="rigidbody-interpolate">Rigidbody-Interpolate</h2>
<p>물리는 50Hz, 화면은 144Hz라고 해봅시다. 그러면 렌더링되는 프레임 중 상당수는 물리가 갱신되지 않은 프레임이라 오브젝트가 같은 자리에 멈춰 있다가 툭 튀는 것처럼 보입니다.
해결은 Rigidbody의 Interpolate 옵션입니다.</p>
<table>
<thead>
<tr>
<th>설정</th>
<th>동작</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>None</code></td>
<td>보정 없음</td>
<td>대부분의 소품 (비용 절약)</td>
</tr>
<tr>
<td><code>Interpolate</code></td>
<td>직전 두 물리 상태 사이를 보간. <strong>항상 한 스텝 뒤처짐</strong></td>
<td><strong>플레이어·카메라 추적 대상</strong></td>
</tr>
<tr>
<td><code>Extrapolate</code></td>
<td>속도로 미래를 예측. 충돌 시 파고들었다 튐</td>
<td>특수한 경우만</td>
</tr>
</tbody></table>
<p>보통 플레이어와 카메라가 따라다니는 대상에만 켜고, 나머지는 끕니다. 전부 켜면 그 자체가 비용이기 때문입니다.
그리고 이건 카메라를 LateUpdate에 두는 것과 짝입니다. 물리 캐릭터를 따라가는 카메라는 LateUpdate + 대상 Interpolate 둘 다 있어야 안 떨립니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>FixedUpdate에 넣는 기준은 &quot;Rigidbody를 건드리는가&quot; 입니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>어디에</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td><code>Rigidbody.AddForce</code> / <code>linearVelocity</code></td>
<td><strong>FixedUpdate</strong></td>
<td>누산기·결정론</td>
</tr>
<tr>
<td><code>Rigidbody.MovePosition</code> / <code>MoveRotation</code></td>
<td><strong>FixedUpdate</strong></td>
<td>다음 스텝까지 쓸어가며 이동</td>
</tr>
<tr>
<td>물리 이동용 지면 <code>Raycast</code></td>
<td><strong>FixedUpdate</strong></td>
<td>판정과 적용을 같은 시점에</td>
</tr>
<tr>
<td><code>CharacterController.Move</code></td>
<td><strong>Update</strong></td>
<td>PhysX 다이내믹스가 아님</td>
</tr>
<tr>
<td><code>transform</code> 직접 이동 (Rigidbody 없음)</td>
<td><strong>Update</strong></td>
<td>물리와 무관</td>
</tr>
<tr>
<td>입력 수집</td>
<td><strong>Update</strong></td>
<td>프레임 단위 이벤트</td>
</tr>
<tr>
<td>카메라 추적</td>
<td><strong>LateUpdate</strong></td>
<td>대상 이동이 끝난 뒤</td>
</tr>
<tr>
<td>UI, 타이머, 애니메이션 파라미터</td>
<td><strong>Update</strong></td>
<td></td>
</tr>
</tbody></table>
<p>FixedUpdate는 물리 엔진과 대화하는 창구라고 생각하면 정확합니다. 물리 엔진과 할 얘기가 없으면 들어갈 이유가 없습니다.</p>
<p>결국 요약하면,</p>
<ol>
<li>호출 횟수가 가변이다 -&gt; 그래서 입력을 여기서 읽으면 안된다.</li>
<li>힘은 스텝 단위로 누적, 소비된다. -&gt; 그래서 AddForce를 Update에서 부르면 안된다.</li>
<li>물리 세계와 Transform 세계는 별개다 -&gt; 그래서 transform.position 직접 수정이 위험하다.</li>
</ol>
<p>로 정리할 수 있겠습니다.</p>
<p>[참고자료]
<a href="https://docs.unity3d.com/kr/2022.3/Manual/ExecutionOrder.html">https://docs.unity3d.com/kr/2022.3/Manual/ExecutionOrder.html</a>
<a href="https://www.youtube.com/watch?v=QtmGT-22PqA">https://www.youtube.com/watch?v=QtmGT-22PqA</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[게임 제작 연합 동아리 BRIDGE 후기 (+ 지원 팁)]]></title>
            <link>https://velog.io/@jisub_shim/%EA%B2%8C%EC%9E%84-%EC%A0%9C%EC%9E%91-%EC%97%B0%ED%95%A9-%EB%8F%99%EC%95%84%EB%A6%AC-BRIDGE-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@jisub_shim/%EA%B2%8C%EC%9E%84-%EC%A0%9C%EC%9E%91-%EC%97%B0%ED%95%A9-%EB%8F%99%EC%95%84%EB%A6%AC-BRIDGE-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 20 Jul 2026 11:02:26 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jisub_shim/post/2a4367f1-4ca4-4168-86fc-09ff899aceeb/image.jpg" alt=""></p>
<p>얼마 전 BRIDGE 동아리의 26-1학기의 정규 총회가 마무리되었다.
어느덧 브릿지에서 활동한지도 1년 반이라는 시간이 흘렀다.</p>
<p>처음에 지원할 때는 정말 떨리고 간절한 마음이었는데, 당시 후기나 활동 글을 찾아보며 많은 도움을 받았던 기억이 있다.
최근 지원을 고민하시는 분들을 위해 서칭을 해보니, 최신 후기글이 다소 부족한 것 같아 내 경험을 토대로 솔직한 이야기를 적어보려고 한다.</p>
<blockquote>
<ul>
<li>저는 프로그래머이기 때문에 프로그래머 기준으로 작성하는 점 양해부탁드립니다.</li>
</ul>
</blockquote>
<h2 id="bridge-란">BRIDGE 란?</h2>
<p>브릿지는 여러 지역의 대학생들이 모여 팀을 이루고 게임을 제작하도록 도와주는 동아리이다.
기획, 프로그래밍, 아트, 사운드 총 4개의 직군으로 구성되어 있다.
총회는 격주마다 진행되며 다양한 네트워킹과 스터디를 지원한다.</p>
<p>이번 신입 모집에서는 대학생만 받지만 졸업 유예나 수료 등도 지원을 받기 때문에 졸업이 아니라면 크게 문제없이 지원 가능하다.
(추가로 한번 들어오면 졸업 후에도 활동이 가능하기 때문에 졸업예정자라고 해서 크게 걱정하지 않아도 된다!)</p>
<p>게임 개발에 관심과 열정이 있는 대학생이라면 꼭 지원해보는 것을 추천한다!</p>
<p>BRIDGE 블로그 링크: <a href="https://bridgegames.tistory.com">https://bridgegames.tistory.com</a></p>
<h2 id="서류">서류</h2>
<p>대학생 동아리인 만큼 어렵게 생각할 필요는 없다.</p>
<p>서류는 크게 자기소개, 지원동기, 활동다짐로 나눌 수 있다. 여기서 가장 중요한 것은 본인이 선택한 직군에 대한 열정을 어필하는 것이다.
나 역시 지원할 당시 그동안 해온 활동들을 정리하고, 커뮤니케이션과 협업에 문제가 없는 사람이라는 점을 강조하여 작성했다.
또 브릿지 블로그에 올라온 활동 기록들을 둘러보고, 동아리에 관심이 있는 티를 잔뜩 냈던 기억이 있다.</p>
<p>포트폴리오는 개인적으로 솔직함이 정말 중요한 것 같다.
화려하고 어려운 스택과 기술의 단순 나열보다 비교적 난이도는 쉽더라도 자신의 역할을 명확히하고 이 프로젝트를 진행하면서 어떤 것을 배우고 느꼈는지를 적는 것이 더 좋다고 생각한다. (물론 기술적 깊이에 자신이 있다면 이러한 경험을 적극적으로 어필해도 좋다!)</p>
<p>프로젝트를 진행해야 하기에 기본적인 게임 개발 경험을 필요로 하지만, 정말 기본적인 경험일 뿐 엄청난 무언가를 바라는 것이 아니기에 부담가지지 않았으면 한다!</p>
<h2 id="인터뷰">인터뷰</h2>
<p>서류를 통과하면 인터뷰 연락이 오는데, 보통 강남역 윙스터디에서 2대 2로 진행이 된다.</p>
<p>인터뷰를 준비한다면 아래 사항은 꼭 체크하자!</p>
<ol>
<li>자기소개 &amp; 지원동기 준비하기</li>
<li>BRIDGE 티스토리 카페에서 어떤 활동을 하는지 확인하기</li>
<li>자신이 작성한 포트폴리오 숙지하기</li>
</ol>
<p>사실 나는 인터뷰할 때 직군 관련 질문이 가장 무서웠다.
내가 잘 대답할 수 있을까..? 내가 모르는 질문이 나오면 어떡하지..? 와 같은 걱정을 계속해서 했던 기억이 있다.</p>
<p>그러나 너무 걱정하지 않아도 괜찮다.
어차피 본인의 포트폴리오에 작성한 내용 숙지만 제대로 했다면 충분히 무리없이 대답할 수 있는 수준이다.
그리고 다른 브릿지 후기에서도 볼 수 있듯이 인터뷰 보는 사람도 같은 동아리 사람이기에.. 너무 걱정하지 않아도 된다!</p>
<h2 id="활동-후기">활동 후기</h2>
<p>동아리 활동을 돌아보면 정말 많은 것을 얻고 배웠다고 자부할 수 있다.</p>
<p>많은 분이 제대로 된 팀 프로젝트 경험을 원해서 동아리에 가입하곤 한다.
나 역시 제대로 만든 게임을 직접 정식 출시까지 이끌어보고 싶다는 목표 하나를 품고 들어왔다.</p>
<p>그리고 지금은 브릿지에서 준비해 온 프로젝트의 정식 출시를 눈앞에 두고 있다.
(<del>현재 텀블벅 펀딩도 진행중인데 많관부...</del> <a href="https://tumblbug.com/pengzle">https://tumblbug.com/pengzle</a>)</p>
<p>하지만 단순 프로젝트 경험만이 브릿지의 전부는 아니다.</p>
<p>게임 개발이라는 공통의 열정을 가진 사람들이 모인 만큼, 게임 업계에 관심이 많은 수많은 사람들을 만날 수 있다.</p>
<ol>
<li>격주 단위의 정규 총회</li>
<li>먼저 취업하신 선배님들로 부터 조언을 얻을 수 있는 네트워킹 행사</li>
<li>기업 탐방, 게임잼, 컨퍼런스와 같은 외부 행사
등등
적극적으로 참여한다면 네트워킹 기회를 많이 얻을 수 있다.</li>
</ol>
<p>또한 내 부족한 역량을 채울 수 있는 스터디 활동도 적극적으로 지원 및 운영되고 있다.
나 역시 컴퓨터 그래픽스 스터디, C++ 스터디 등에 직접 참여하고 운영도 해보았다.
사실 지금 어려운 시기인만큼 함께 할 동료가 간절한 상황이었는데, 스터디를 하면서 서로 유대감도 생기고 혼자 공부했다면 얻지 못했을 수많은 정보도 얻을 수 있었다.</p>
<p>이외에도 프로젝트 팀장, 운영진, 스터디장 등 다양한 주도적인 경험도 얼마든지 쌓을 수 있다.</p>
<p>그리고... 게임을 좋아하는 회원분들이 많다보니 같이 게임도 하고, 디스코드에서 같이 작업도 하고 친해질 기회가 정말 정말 많이 열려있다.
돌아보면 힘든 시기에 같이 힘낼 수 있는 동료가 생긴 것이 가장 큰 장점인 것 같다.</p>
<hr>
<p>게임 개발에 열정을 가지신 모든 분들을 응원합니다!
다음에 BRIDGE에서 뵐 수 있으면 좋겠습니다!</p>
<p>이상으로 후기를 마치겠습니다.
감사합니다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2025 넥슨 게임잼 [재밌넥] 참가 후기 및 회고]]></title>
            <link>https://velog.io/@jisub_shim/2025-%EB%84%A5%EC%8A%A8-%EA%B2%8C%EC%9E%84%EC%9E%BC-%EC%9E%AC%EB%B0%8C%EB%84%A5-%EC%B0%B8%EA%B0%80-%ED%9B%84%EA%B8%B0-%EB%B0%8F-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@jisub_shim/2025-%EB%84%A5%EC%8A%A8-%EA%B2%8C%EC%9E%84%EC%9E%BC-%EC%9E%AC%EB%B0%8C%EB%84%A5-%EC%B0%B8%EA%B0%80-%ED%9B%84%EA%B8%B0-%EB%B0%8F-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 21 Jul 2025 10:43:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jisub_shim/post/c3138b31-75aa-4cba-91ff-cd2f032202da/image.png" alt="">
이번에 넥슨에서 주최한 재밌넥 게임잼 행사에 다녀왔다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/9efe2fe1-07a0-4af5-81c6-ffc99cc27d00/image.png" alt="">
예전부터 정말 참여해보고 싶었던 행사였고, 운좋게 기회를 얻게 되어 좋은 경험을 하게 되었다.
행사에 참여하면서 배운 것과 느낀 것이 많다보니, 바로 후기를 작성해본다!</p>
<hr>
<h3 id="무슨-일이-있었는가">무슨 일이 있었는가?</h3>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/17ebe162-c04a-4cca-9bda-85eabe36ebf7/image.jpg" alt="">
게임잼은 넥슨코리아 본사에서 진행되었다.
평소에 정말 좋아했던 넥슨이었기에, 설레는 마음이 가득했고 잘해보고 싶다는 열정이 가득했다.
복지에 대해 잠깐 말하면, 역시 대기업이라 너무 너무 좋았다..! 시설뿐만 아니라 일단 밥이 엄청 맛있었고, 특히 샤워실이 너무나도 좋았다..ㅎㅎ
나도 &#39;언젠가 이런 환경에서 일하고 싶다&#39; 라는 상상을 많이 했던 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/efa4ef87-c3e9-4e6f-9b57-79e79439f203/image.jpg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/dee7c07d-e378-4605-9bc7-df1cd6fa05c6/image.jpg" alt="">
OT는 넥슨 건물 안 1994홀에서 진행되었고, OT와 동시에 주제발표가 이루어졌다.
사실 이전 주제가 단풍, 섬이었어서 이번에 &#39;with 민트로켓&#39;이다보니, 데이브 더 다이버와 연관된 심플한 주제를 예상했었다. (바다, 스시, 낚시 등등)
그러나 이번 게임잼 주제는 예상과 다르게 &#39;다른 장르의 결합&#39; 이었다..!
참신한 게임을 좋아하는 나에게는 반가운 소식이었지만, 준비해왔던 것과 다른 기획자분들은 많이 당황했었을 것 같았다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/1d9a8a76-9e40-4e46-ada2-d2e1a50d7d64/image.jpg" alt="">
이후 팀빌딩은 피그잼을 통해 이루어졌고, 작업실에서 2박 3일간 게임 개발을 진행했다.
많은 분들이 거의 잠도 안자고, 개발에 몰두했다. 개발 과정에서 여러가지 일이 있었지만, 팀원분들이 정말 다들 실력자였고 열심히 해주어서 나도 더 몰입해서 열심히 할 수 있었던 것 같다..ㅎㅎ
그리고 중간중간 넥슨에서 준비한 게임이나 이벤트들도 있어서 행사를 더욱 재밌게 즐길 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/fed78d61-e853-45f4-807b-e788277368b7/image.png" alt="">
시연 때는 역시나 재밌는 게임들이 정말 많았다!
그리고 우리 팀은 퍼즐 + 건설 시뮬레이션 장르로 게임 개발을 했다.
2048 퍼즐 게임의 매커니즘을 이용하여 마나를 이동 및 업그레이드하고 이를 설치한 건물에 주입하고 땅을 개척하여 최종 목적지에 도달시켜 목표를 달성하는 게임이었다.
비록 수상은 못했지만, 정말 열심히 만들었기에 시연할 때 뿌듯했고 재밌게 즐겨주셨던 분들께 너무 감사했다.</p>
<h3 id="경험-그리고-배우고-느낀-점">경험, 그리고 배우고 느낀 점</h3>
<p>사실 수상을 목표로 했기에 좋았던 만큼 아쉬움도 조금 남았다.
요새 프로젝트를 하면서 개발에 대한 자신감도 생겼고, 무엇보다 지난 시간 동안의 성장을 증명하고 싶었다.
결론적으로 플레이 빌드는 뽑아냈지만, 튜토리얼 개발은 실패하고 제출하게 되었다.</p>
<p>항상 게임잼을 할 때, 고민해왔던 것이 있다.
제한된 시간 내에 코드 구조를 우선 시 하는가, 아니면 하드코딩을 감수하더라도 빠르게 구현 해야하는가이다.</p>
<p>이번 게임잼에서 이에 대한 정답은 없다는 것을 느꼈다. 요약하면 시간, 팀 역량 등 상황에 맞게 능동적으로 개발해야한다.
규모가 작고 시간이 충분하다면 크게 문제가 되지 않는다. 그러나 대부분의 상황에서는 최대한 코어 시스템과 수정이 예상되는 부분, 그렇지 않은 부분을 구분해 능동적으로 구현하는 게 중요하다고 생각한다.
나는 기획자에게 &quot;이 부분 확장 가능성있나요?&quot;와 같은 질문을 자주 했는데 도움이 되었다고 생각한다.</p>
<p>프로젝트에서는 업무 편중, 기획 변경, 깃 충돌, 예기치 못한 버그 등 다양한 이슈가 발생할 수 있음을 늘 인식하고 있어야 한다.
이번 게임잼에서도 쉽지 않았지만 나름의 방식대로 대처하려고 노력했다.</p>
<p>게임잼의 특성 상 단기간에 많은 기능을 구현해야 하다보니, 기능 구현에 있어서 AI를 활용했다.
그러나 단순히 독립된 로직을 출력하는 것을 넘어, 연출, 건물, 온도 등 여러가지 연계되는 기능들까지 고려해야 했기에 예상치 못한 상황들이 적지않게 나왔다.</p>
<p>사실 AI를 사용함에 있어서 엄격한 검증 작업은 필수적이라는 것을 너무나도 잘 알고 있었지만, 시간 압박으로 인해 이를 간과했고 결과적으로 디버깅에 많은 시간을 빼앗겼다.
수면 부족으로 인한 피로도가 판단력을 흐린 점도 원인이었다. 효율적인 개발을 위해서는 AI 출력물에 대한 최소한의 검증 파이프라인은 유지해야 함을 다시 한 번 깨닫게 되는 계기가 되었다.</p>
<p>발표 때는 심사위원분들께 &quot;플레이 방법을 직관적으로 인지하기 어렵다&quot;는 피드백을 받았다.
규칙이 복잡한 게임일수록 튜토리얼이 역할을 충실히 수행해야 한다.
그러나 시연장에서 직접 설명해 드리면 된다는 안일한 생각으로 컨텐츠 추가를 우선시했던 점은 명백한 판단 미스였다.
이번 경험을 통해 아무리 좋은 아이디어와 시스템을 갖추었더라도, 플레이어에게 제대로 전달되지 않는다면 의미가 반감된다는 점을 깊이 반성하게 되었다.</p>
<p>마지막 시상식에서 민트로켓 황재익 대표이사님께서 “객관적으로 판단할 수 있는 사람이 되라”는 조언을 해주신 것이 기억에 남는다.
단순히 객관화의 중요성을 아는 것에서 그치는 것이 아니라, 스스로 이를 꾸준히 인지하고 실천하려는 노력이 필요하다는 점을 느꼈다.</p>
<p>객관화는 결코 쉽지 않다. 누구나 자신이 만든 결과물을 바라볼 때 주관적인 보정이 들어가기 마련이다. 내가 만드는 게임이 나에게는 재미있을지 몰라도, 다른 사람에게는 다르게 다가올 수 있음을 항상 경계해야 한다. 또한 내 실력과 역량 역시 과대평가하거나 과소평가하지 않고, 객관적으로 바라보기 위해 지속적으로 노력해야 한다.</p>
<p>이번 게임잼은 내 역량을 다시 돌아보고 점검하며, 새롭게 리프레시할 수 있는 좋은 기회가 되었다.</p>
<p>마지막으로 함께 고생한 팀원들, 그리고 좋은 기회를 주신 넥슨과 민트로켓에 정말 감사드립니다!</p>
<blockquote>
<p>&#39;철저한 객관화를 통해 언젠가는 정말 정말 재밌는 게임을 만들어보자!&#39;
<img src="https://velog.velcdn.com/images/jisub_shim/post/2f38e53a-0a2f-4c4f-85e0-5e1e1e55c7ee/image.jpg" alt=""></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity] Unity에서 안 건드린 scale이 변한다면?]]></title>
            <link>https://velog.io/@jisub_shim/Unity-SetParent%EC%99%80-scale%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</link>
            <guid>https://velog.io/@jisub_shim/Unity-SetParent%EC%99%80-scale%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</guid>
            <pubDate>Fri, 16 May 2025 14:39:25 GMT</pubDate>
            <description><![CDATA[<p>이번에 프로젝트를 하다가, UI 오브젝트의 scale을 조정하지도 않았는데 자꾸 scale이 달라지는 현상을 발견했다. 
처음엔 버그인 줄 알고 디버그 로그를 찍어가며 누가 scale을 바꾸는지 추적했지만, 알고보니 원인은 <code>Transform.SetParent()</code>의 동작 방식에 있었다.</p>
<h3 id="유니티-transform의-scale">유니티 Transform의 Scale</h3>
<p>Unity의 Transform의 Scale은 <code>World Scale = Parent World Scale * Child Local Scale</code>와 같은 구조로 동작한다.
즉, 어떤 오브젝트를 <code>SetParent()</code>로 부모에 붙이는 순간, 부모의 scale에 맞춰 자식의 localScale이 자동으로 조정되는 것이 일반적이다.</p>
<p><code>SetParent()</code>에는 <code>worldPositionStays</code> 라는 매개변수가 있다.
이 값을 true로 설정하면(디폴트가 true), Unity는 자식 오브젝트가 월드 좌표계 기준으로 localScale을 강제로 조정한다.</p>
<p>예를 들어 부모가 <code>(0.5, 0.5, 0.5)</code>라면, 자식의 <code>localScale</code>은 <code>(2, 2, 2)</code>처럼 자동으로 바뀌게 된다.
그래야 월드 기준으로 동일한 크기를 유지할 수 있기 때문이다.
이러한 문제때문에 SetParent()를 사용할 때는 부모의 scale을 잘 고려해야하고, 되도록이면 scale을 1로 유지하는게 바람직하다.</p>
<h3 id="canvas-scaler의-scale-with-screen-size-모드">Canvas Scaler의 Scale With Screen Size 모드</h3>
<p>문제를 꼬이게 만든 것은 바로 <code>Canvas Scaler</code>의 <code>Scale With Screen Size</code> 모드 였다.
이것은 설정에 따라 해상도에 맞춰 Canvas 자체의 scale을 자동으로 조정한다.
예를들어 <code>Reference Resolution</code>을 설정해두면 Rect Transform의 Width와 Height를 설정해둔 해상도 값으로 맞춰두고, 설정 해상도 대비 현재 화면 해상도의 비율값만큼 해당 canvas의 scale에 적용한다. 이렇게 <code>Canvas Scaler</code>의 <code>Scale With Screen Size</code>는 scale을 조정하는 방식으로 해상도를 자동으로 맞춰준다.
아래 스크린샷을 보면 현재 해상도가 QHD일 때, <code>Reference Resolution</code>을 FHD로 설정해두면 scale이 1.333333이 되는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/d3d92071-06c0-4a5b-b17b-9894105191b4/image.png" alt=""></p>
<h3 id="이-둘의-환장의-콜라보">이 둘의 환장(?)의 콜라보</h3>
<p>그럼 다시 본론으로 돌아가 보자.
무엇이 문제였냐면…
이 프로젝트에서는 미리 만들어 둔 UI 오브젝트 프리팹을 필요한 만큼 Instantiate해서, SetParent()를 통해 UI Canvas 안으로 넣는 과정에서 발생했다.
이 과정에서 Canvas Scaler의 Scale With Screen Size 모드는 이미 화면 해상도에 따라 Canvas의 scale을 조정하고 있기 때문에, SetParent는 그걸 모른 채 다시 scale을 건드려버리는 상황이 생긴다.</p>
<p>즉,</p>
<ul>
<li>Canvas는 해상도에 맞춰서 scale을 조정해놓았고,</li>
<li>Unity는 SetParent()를 하면서 또 scale을 조정하려고 든다.</li>
</ul>
<p>결과적으로,</p>
<ul>
<li>월드 상에서는 자식 오브젝트의 scale이 변하지 않은 것처럼 보여야 하는데,</li>
<li>실제로는 localScale이 예상치 못한 값으로 바뀌어버리고, 화면상의 크기도 미묘하게 틀어지게 된다.</li>
</ul>
<p>두 시스템 모두 &quot;잘 보이게 하겠다&quot;고 나섰지만, 서로 조율 없이 각자 판단대로 scale을 만지다 보니 오히려 화면 출력이 꼬이는 아이러니한 상황이 생긴 것이다.</p>
<h3 id="해결방안">해결방안</h3>
<p>이런 현상을 방지하기위해 2가지 방안을 찾았다.</p>
<p>첫번째는 SetParent의 <code>worldPositionStays</code> 매개변수를 false로 해주는 것이다.</p>
<p>두번째는 월드에 생성하고 Canvas의 자식으로 넣는 과정에서 문제가 생기는 것이므로, 그냥 아예 생성 자체를 Canvas의 자식으로 바로 넣어버리면 문제가 해결된다.</p>
<p>물론 Instantiate을 안하고 미리 씬에 배치해두고 활성/비활성화하는 방법도 있었다. 그러나 상황에 따라 매번 생성해야하는 개수가 달라지고, 성능 또한 위 두 방법도 스테이지 시작 전 초기화 단계에서만 실행되기에 크게 차이가 날 것이라고 생각하지 않아서, 유지보수 편의성을 위해 미리 씬에 배치하는 방법은 채택하지 않았다.</p>
<h2 id="교훈">교훈</h2>
<p>SetParent()를 사용할 때, 부모의 scale을 조심하자. (되도록이면 1로 유지)</p>
<p>특히 Canvas Scaler의 Scale With Screen Size 모드와 함께 사용할 땐 더더욱 주의하자.</p>
<p>Unity의 &quot;도와주려고 해주는 일&quot;이 꼭 내가 원하는 일은 아닐 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C#] Enumerator 열거자]]></title>
            <link>https://velog.io/@jisub_shim/C-Enumerator-%EC%97%B4%EA%B1%B0%EC%9E%90</link>
            <guid>https://velog.io/@jisub_shim/C-Enumerator-%EC%97%B4%EA%B1%B0%EC%9E%90</guid>
            <pubDate>Mon, 24 Mar 2025 07:16:45 GMT</pubDate>
            <description><![CDATA[<h2 id="enumerator">Enumerator</h2>
<p>.NET에서 제공하는 <code>IEnumerator</code> 인터페이스를 구현한 객체를 <strong>Enumerator(열거자)</strong>라고 한다.<br><code>System.Collections</code>에 있는 컬렉션들은 <code>IEnumerable</code>을 상속받아 구현되어 있다.<br>따라서 <code>List</code>, <code>Array</code> 등의 컬렉션 클래스는 <code>foreach</code>문을 사용할 수 있다.  </p>
<hr>
<h3 id="ienumerator">IEnumerator</h3>
<h4 id="어떻게-요소들을-열거할-것인가">&quot;어떻게 요소들을 열거할 것인가?&quot;</h4>
<pre><code class="language-cs">namespace System.Collections
{
    public interface IEnumerator
    {
        object Current { get; }

        bool MoveNext();
        void Reset();
    }
}</code></pre>
<p>IEnumerator는 컬렉션을 반복할 때 사용한다.</p>
<h4 id="열거자를-구현하기-위해-다음과-같은-멤버가-필요하다">열거자를 구현하기 위해 다음과 같은 멤버가 필요하다.</h4>
<ol>
<li>현재 요소를 반환하는 <code>Current</code> 읽기 전용 프로퍼티</li>
<li>다음 요소로 이동하고 이동이 성공하면 true, 아니면 false를 반환하는 <code>MoveNext()</code> 메서드</li>
<li>인덱스를 초기 상태(-1)로 되돌리는 <code>Reset()</code> 메서드</li>
</ol>
<h3 id="ienumerable">IEnumerable</h3>
<h4 id="요소들을-열거할-수-있는가">&quot;요소들을 열거할 수 있는가?&quot;</h4>
<pre><code class="language-cs">namespace System.Collections
{
    public interface IEnumerable
    {
        IEnumerator GetEnumerator();
    }
}</code></pre>
<p><code>IEnumerable</code>에 선언된 <code>GetEnumerator()</code> 메서드를 통해 <code>IEnumerator</code>를 반환한다.</p>
<p>추가적으로 <code>IEnumertor</code> 또는 <code>IEnumerable</code>을 반환하는 모든 메서드는 <code>ref</code>, <code>out</code> 키워드 사용이 불가하다. 또한 람다 함수에 사용할 수도 없다.</p>
<p>또한 제네릭을 사용해서 열거하는 값의 타입을 지정해줄 수 있다. 타입을 지정안하면 object 타입으로 넘어가 박싱/언박싱이 일어나게 되어 조심할 필요가 있다.</p>
<h2 id="foreach">foreach</h2>
<p>foreach 순회는 아래 3단계를 따른다.  </p>
<ol>
<li><code>GetEnumerator()</code> 메서드를 호출하여 <code>IEnumerator</code> 객체를 얻는다.</li>
<li><code>MoveNext()</code>를 먼저 호출하여 다음 요소로 이동한 후, <code>Current</code>를 반환한다.</li>
<li>2를 반복하며 <code>MoveNext()</code>가 <code>false</code>를 반환할 때까지 순회한다.</li>
</ol>
<p>즉, <code>foreach</code> 문을 사용하려면 다음 조건을 만족해야 한다.</p>
<ol>
<li><p>순회하려는 객체는 <code>GetEnumerator()</code> 메서드를 가지고 있어야 한다.</p>
<ul>
<li><code>IEnumerable</code>을 상속하면 기본적으로 이 조건을 만족한다.  </li>
<li>하지만 <code>IEnumerable</code>을 상속하지 않더라도 <code>GetEnumerator()</code>를 직접 구현하면 <code>foreach</code> 사용 가능하다.</li>
</ul>
</li>
<li><p><code>GetEnumerator()</code>의 반환 타입은 <code>Current</code> 프로퍼티(읽기 전용)와 <code>MoveNext()</code> 메서드를 가지고 있어야 한다.</p>
<ul>
<li>즉, <code>IEnumerator</code> 인터페이스를 구현해야 한다.</li>
</ul>
</li>
</ol>
<p>이를 구현하면 아래와 같은 코드로 <code>foreach</code>문이 작동한다.</p>
<pre><code class="language-cs">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

class Enumerator : IEnumerable, IEnumerator
{
    private int[] items = { 1, 2, 3, 4, 5 };
    private int index = -1;
    public object Current { get { return items[index]; } }

    public IEnumerator GetEnumerator()
    {
        Reset();
        return this;
    }

    public bool MoveNext()
    {
        index++;
        return items.Length &gt; index;
    }

    public void Reset()
    {
        index = -1;
    }
}
public class Practice : MonoBehaviour
{
    void Start()
    {
        Enumerator enumerator = new Enumerator();

        foreach (var item in enumerator)
        {
            Debug.Log(item);
        }
    }
}</code></pre>
<p>추가적으로 <code>foreach</code>문은 아래와 같은 코드로 치환 가능하다.</p>
<pre><code class="language-cs">void Start()
{
    IEnumerable enumerable = new Enumerator();
    IEnumerator enumerator = enumerable.GetEnumerator();
    while (enumerator.MoveNext())
    {
        int current = (int)enumerator.Current;
        Debug.Log(current);
    }
}</code></pre>
<p>이렇게 IEnumerator와 IEnumerable 인터페이스를 활용하여 컬렉션의 내부 구조를 몰라도 순회할 수 있게 해주는 디자인 패턴을 &#39;반복자(iterator) 패턴&#39; 이라고 한다.
지금까지 설명한 예제 코드들은 설명의 편의성을 위해 제네릭을 사용하지 않았지만, 실제로는 <code>IEnumerator&lt;T&gt;</code>, <code>IEnumerable&lt;T&gt;</code>와 같은 제네릭 타입을 사용하는 것이 일반적이다. 그렇지 않으면 값 타입을 순회할 때 박싱(Boxing)이 발생해 추가적인 성능 비용이 발생할 수 있다.
추가적으로 유의해야할 점은 foreach 문을 사용할 때마다 IEnumerator 객체가 생성되기 때문에, 과도하게 사용할 경우 성능 저하로 이어질 수 있다는 것이다. (단, 예제 코드처럼 this를 반환하는 특수 케이스는 성능 문제 없음)</p>
<h2 id="yield">yield</h2>
<p>C#에서 <code>yield</code> 키워드를 사용하면 C# 컴파일러는 해당 메서드를 반복자 패턴으로 변환하여 <code>IEnumerable</code> 또는 <code>IEnumerator</code> 인터페이스를 구현하는 코드를 생성한다. 이를 통해 <code>yield return</code>을 호출할 때마다 상태를 저장하고, 다음 호출에서 중단된 위치부터 실행을 재개할 수 있도록 한다.</p>
<pre><code class="language-cs">class Enumerator
{
    private int[] items = {1,2,3,4,5 };

    public IEnumerator GetEnumerator()
    {
        yield return items[0];
        yield return items[1];
        yield return items[2];
        yield return items[3];
        yield return items[4];
    }

}
public class Practice : MonoBehaviour
{
    void Start()
    {
        Enumerator enumerator = new Enumerator();
        foreach(var item in enumerator)
        {
            Debug.Log(item);
        }
    }
}</code></pre>
<p>이렇게 <code>yield return</code> 을 사용하여 한 번 호출될 때마다 하나씩 값을 반환하고, 지연 호출이 가능해진다.
(참고로, 컴파일러는 이러한 yield 메서드를 자동으로 상태 머신(state machine) 클래스로 변환해준다고 한다.)</p>
<h2 id="마무리">마무리</h2>
<p>지금까지 IEnumerator, IEnumerable 그리고 foreach문이 동작하는 방식과, yield 키워드를 활용한 반복자 패턴에 대해 살펴보았다. 핵심은 컬렉션의 내부 구조를 몰라도 일관된 방식으로 요소들을 순회할 수 있도록 해주는 것이 반복자 패턴의 목적이라는 점이다.</p>
<p>결론적으로, 반복자 패턴은</p>
<ol>
<li>코드의 가독성과 유지보수성을 크게 높여주지만,</li>
<li>메모리 할당과 성능 측면에서는 신중하게 사용할 필요가 있다.</li>
</ol>
<p>&quot;언제 사용하고, 언제 주의할 것인가?&quot; 에 대한 감각을 익히는 것이 중요한 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C#] object, Boxing/Unboxing]]></title>
            <link>https://velog.io/@jisub_shim/C-object-BoxingUnboxing</link>
            <guid>https://velog.io/@jisub_shim/C-object-BoxingUnboxing</guid>
            <pubDate>Wed, 12 Mar 2025 16:01:45 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jisub_shim/post/7e888745-c854-42ed-87e0-734499de01bc/image.png" alt=""></p>
<h2 id="object-자료형">object 자료형</h2>
<p>C#에는 int, long, char 등 다양한 자료형이 있다. 이 중 object는 모든 데이터를 다룰 수 있는 최상위 자료형이다. C#에서 모든 자료형은 object를 상속하므로, object는 모든 자료형의 부모라고 볼 수 있다.</p>
<pre><code class="language-cs">object _float = 1.23f;
object _int = 1;
object _string = &quot;Hello&quot;; // string은 원래 참조 타입이므로 Boxing 아님
object _bool = true;</code></pre>
<h2 id="박싱boxing과-언박싱unboxing">박싱(Boxing)과 언박싱(Unboxing)</h2>
<p>object는 참조 타입이다. 따라서 object 변수에 값 타입을 할당하면 스택(stack)에는 주소값이 저장되고, 힙(heap)에는 실제 데이터가 저장된다. 이렇게 값 타입을 참조 타입으로 변환하는 과정을 박싱(Boxing)이라고 한다.</p>
<pre><code class="language-cs">object _object = 1; // Boxing
int _int = (int)_object; // Unboxing</code></pre>
<p>반대로 힙에 있는 데이터를 다시 스택으로 가져오는 과정을 언박싱(Unboxing)이라고 한다. 즉, 원래 주소값을 담고 있던 자리에 데이터를 직접 복사하는 것이다. 이때 힙에 남아있는 데이터는 가비지가 되고 GC가 해제할 때까지 기다려야 한다.</p>
<p>따라서 박싱/언박싱은 힙 메모리 할당과 데이터 복사 비용이 크기 때문에 성능상 문제가 있다. 이러한 문제로 object는 잘 쓰이지 않는다고 한다.</p>
<h3 id="그렇다면-object는-어디에-쓰일까">그렇다면 object는 어디에 쓰일까?</h3>
<p>object는 모든 자료형의 조상이기 때문에 다형성(Polymorphism)을 지원한다. 즉, 모든 타입을 object로 다룰 수 있기 때문에 다양한 상황에서 활용될 여지가 있다.</p>
<p>예를들어,</p>
<pre><code class="language-cs">public abstract class ObjectPolymorphismExample
{
    // 추상화는 하고 싶은데 필드 타입이 명확하지 않은 경우 (ex. 퀘스트 시스템)
    public abstract object value { get; }

    ...중략...
}</code></pre>
<p>이처럼 object 타입을 사용하면 다양한 자료형의 값을 하나의 부모 타입으로 다룰 수 있어, 여러 상황에서 유용하게 활용될 수 있다. </p>
<p>물론 제네릭(Generic)을 사용하면 타입이 컴파일 타임에 고정되므로 형변환이 필요 없고, 안전성을 확보할 수 있다. 또한 박싱/언박싱 문제도 피할 수 있다. 따라서 C# 2.0 버전 이상이라면 제네릭을 사용하는 것이 성능과 안전성 측면에서 더 좋은 선택이긴 하다.</p>
<p>그러나 애초에 힙에 할당된 참조 타입 변수들은 박싱/언박싱이 일어나지 않기도 하고, 값 타입을 박싱/언박싱 하더라도 매 프레임마다 일어나는 것이 아니라면 성능에 미치는 영향은 미미하다. 무엇보다 제네릭을 사용하면 이를 담는 컬렉션(배열, 리스트) 등을 만든다고 했을 때, 타입이 달라지면 </p>
<pre><code class="language-cs">public abstract class ObjectPolymorphismExample&lt;T&gt;
{
    public abstract T value { get; }

    ...중략...
}</code></pre>
<h3 id="systemobject의-주요-메서드">System.Object의 주요 메서드</h3>
<p>C#에서 모든 자료형은 System.Object 클래스를 상속하며, 아래와 같은 기본 메서드를 사용할 수 있다.</p>
<pre><code class="language-cs">namespace System
{
    public class Object
    {
        public Object();

        ~Object();

        public static bool Equals(Object objA, Object objB);
        public static bool ReferenceEquals(Object objA, Object objB);
        public virtual bool Equals(Object obj);
        public virtual int GetHashCode();
        public Type GetType();
        public virtual string ToString();
        protected Object MemberwiseClone();
    }
}</code></pre>
<h4 id="public-static-bool-equalsobject-obja-object-objb">public static bool Equals(Object objA, Object objB);</h4>
<p>두 객체(objA와 objB)가 동일한지 비교하는 정적 메서드</p>
<blockquote>
<p>기본적으로 객체의 참조를 비교하지만, 오버라이드하여 값 비교를 할 수 있다.</p>
</blockquote>
<h4 id="public-static-bool-referenceequalsobject-obja-object-objb">public static bool ReferenceEquals(Object objA, Object objB);</h4>
<p>두 객체가 같은 참조를 가리키는지 확인하는 정적 메서드</p>
<blockquote>
<p>이 메서드는 객체의 값이 아닌 참조 자체를 비교한다. 즉 두 객체가 모두 동일한 메모리 위치를 가리키는지를 확인한다.
추가적으로 둘 다 null이면 true를 반환한다.</p>
</blockquote>
<h4 id="public-virtual-bool-equalsobject-obj">public virtual bool Equals(Object obj);</h4>
<p>두 객체가 동일한지 비교하는 인스턴스 메서드</p>
<blockquote>
<p>기본적으로 객체의 참조를 비교하지만, 오버라이드하여 값 비교를 할 수 있다.</p>
</blockquote>
<h4 id="public-virtual-int-gethashcode">public virtual int GetHashCode();</h4>
<p>객체의 해시 코드를 반환하는 메서드</p>
<blockquote>
<p>해시 코드는 객체를 비교하는 데 사용되는 값으로, 기본적으로 참조에 기반한 해시 값을 반환한다. 이 메서드도 오버라이드하여 객체의 값에 기반한 해시 코드를 구현할 수 있다.</p>
</blockquote>
<h4 id="public-type-gettype">public Type GetType();</h4>
<p>객체의 런타임 타입을 반환하는 메서드</p>
<blockquote>
<p>객체가 어떤 타입인지 알 수 있다.</p>
</blockquote>
<h4 id="public-virtual-string-tostring">public virtual string ToString();</h4>
<p>객체의 문자열 표현을 반환하는 메서드</p>
<blockquote>
<p>기본적으로 클래스 이름을 반환하지만, 오버라이드해서 객체의 구체적인 데이터를 문자열로 반환할 수 있다.</p>
</blockquote>
<h4 id="protected-object-memberwiseclone">protected Object MemberwiseClone();</h4>
<p>객체의 얕은 복사본을 생성하는 메서드</p>
<blockquote>
<p>객체의 필드 값을 복사하지만, 참조형 필드는 복사하지 않고 같은 참조를 유지한다.</p>
</blockquote>
<h3 id="커스텀-자료형들은-따로-object를-상속받은게-없는데-어떻게-위와-같은-함수들을-사용할-수-있는-걸까">커스텀 자료형들은 따로 Object를 상속받은게 없는데 어떻게 위와 같은 함수들을 사용할 수 있는 걸까?</h3>
<p>C#에서 모든 객체는 System.Object 클래스를 암묵적으로 상속받기 때문에, 위와 같은 메서드를 사용할 수 있다.
암묵적 상속은 C#에서 명시적으로 상속하지 않아도 모든 클래스가 자동으로 System.Object 클래스를 상속받는 것처럼, 언어 자체가 내부적으로 처리하는 방식이다. 즉, 개발자가 명시적으로 상속을 선언하지 않아도, 특정 클래스는 특정 부모 클래스의 기능을 자동으로 상속받는 개념이다.
이 과정은 언어 수준에서 제공되는 기본적인 기능으로, 개발자가 따로 이를 구현하거나 명시할 필요는 없다.</p>
<h2 id="마무리">마무리</h2>
<p>C#의 object 타입은 잘 쓰이지는 않지만 모든 자료형들의 조상이기 때문에 배워두면 유용하다. 또한 다형성을 지원하여 가끔 사용되기도 한다. 그러나 박싱/언박싱은 성능 문제를 야기할 수 있기 때문에 신중히 사용하거나, 대체 가능하다면 제네릭을 이용하는 편이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[OOP] SOLID 원칙]]></title>
            <link>https://velog.io/@jisub_shim/OOP-SOLID-%EC%9B%90%EC%B9%99</link>
            <guid>https://velog.io/@jisub_shim/OOP-SOLID-%EC%9B%90%EC%B9%99</guid>
            <pubDate>Tue, 25 Feb 2025 12:08:58 GMT</pubDate>
            <description><![CDATA[<h2 id="solid-원칙이란">SOLID 원칙이란?</h2>
<p>SOLID 원칙은 객체 지향 프로그래밍에서 코드 설계를 이해하기 쉽고 유연하게 만들고 유지보수를 용이하도록 만드는 5가지 원칙을 의미한다.
이 원칙을 통해 게임 시스템의 유지보수성과 확장성을 높일 수 있다.</p>
<h2 id="단일-책임-원칙-srp-single-responsibility-principle">단일 책임 원칙 (SRP, Single Responsibility Principle)</h2>
<p>SRP는 클래스, 모듈은 오직 하나의 책임을 가져야 한다는 원칙이다. 즉, 클래스는 오직 하나의 변경 이유만 가져야 하며, 이를 통해 코드의 유지 보수성과 가독성을 높일 수 있다.
이러한 설계 원칙을 기반으로 하나의 큰 클래스를 만들기보다는 여러 개의 작은 클래스를 만드는 편이 좋다.
유니티의 컴포넌트들도 각각 한 가지 책임을 올바르게 수행한다. 예를들면 Renderer는 화면에 표시되는 방식을 제어하고, Rigidbody는 물리 시뮬레이션과 상호작용하는 하나의 책임을 수행한다.</p>
<h3 id="srp를-위반한-경우">SRP를 위반한 경우</h3>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/abb887ad-9e51-4a0a-a083-b552d7ed9ce9/image.png" alt=""></p>
<pre><code class="language-cs">public class UnrefactoredPlayer : MonoBehaviour
{
 [SerializeField] private string inputAxisName;
 [SerializeField] private float positionMultiplier;
 private float yPosition;
 private AudioSource bounceSfx;
 private void Start()
 {
 bounceSfx = GetComponent&lt;AudioSource&gt;();
 }
 private void Update()
 {
 float delta = Input.GetAxis(inputAxisName) * Time.deltaTime;
 yPosition = Mathf.Clamp(yPosition + delta, -1, 1);
 transform.position = new Vector3(transform.position.x, yPosition *
 positionMultiplier, transform.position.z);
 }
 private void OnTriggerEnter(Collider other)
 {
 bounceSfx.Play();
 }
}</code></pre>
<p>Player 컴포넌트 안에 Audio, Input, Movement를 모두 구현하면 각각에 해당하는 책임들을 하나의 클래스 안에서 모두 수행하게 된다. 이는 SRP를 잘 지키지 못한 예시이며, 규모가 커질수록 유지보수가 어려워지게 될 가능성이 크다.</p>
<h3 id="srp에-기반한-설계-방법">SRP에 기반한 설계 방법</h3>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/c8a443cd-4f2e-4e15-b9a8-2d64710968cf/image.png" alt=""></p>
<pre><code class="language-cs">[RequireComponent(typeof(PlayerAudio), typeof(PlayerInput),
typeof(PlayerMovement))]
public class Player : MonoBehaviour
{
 [SerializeField] private PlayerAudio playerAudio;
 [SerializeField] private PlayerInput playerInput;
 [SerializeField] private PlayerMovement playerMovement;
 private void Start()
 {
 playerAudio = GetComponent&lt;PlayerAudio&gt;();
 playerInput = GetComponent&lt;PlayerInput&gt;();
 playerMovement = GetComponent&lt;PlayerMovement&gt;();
 }
}
public class PlayerAudio : MonoBehaviour
{
 …
}
public class PlayerInput : MonoBehaviour
{
 …
}
public class PlayerMovement : MonoBehaviour
{
 …
} </code></pre>
<p>이 스크립트에서 필요한 기능들은 각 클래스에서 독립적으로 수행한다.
Player 클래스는 단순히 각 컴포넌트를 보유하고, 직접 기능을 수행하지 않는다.</p>
<p>그러면 이러한 의문이 떠오를 수 있다. 
<strong><em>&#39;단순히 각 컴포넌트를 보유만 할거면 Player는 무슨 필요가 있지?&#39;</em></strong></p>
<p>Player 클래스가 존재해야하는 이유는 이들을 조립하고 조정하는데에 있다. 즉, 플레이어라는 개념을 하나로 묶고 여러 시스템을 연계하는 역할을 한다.
이렇게 하면 각 클래스가 하나의 책임만 가지게 되어 변경이 필요할 때 서로 영향을 주지 않는다.
Movement, Input, Audio와 같은 비즈니스 클래스들이 서로 직접적인 의존 관계를 가지지 않도록 Player라는 중심 엔터티를 만든다고 생각하면 된다.</p>
<blockquote>
<p>PlayerInput → PlayerMovement 직접 호출 ❌
PlayerInput → Player → PlayerMovement 호출 ⭕</p>
</blockquote>
<p>이렇게 하면 SRP를 지키면서 유지보수성과 확장성을 높일 수 있다.</p>
<h2 id="개방-폐쇄-원칙-ocp-open-closed-principle">개방 폐쇄 원칙 (OCP, Open-Closed Principle)</h2>
<p>OCP는 클래스가 확장에는 열려있고 수정에는 닫혀있어야 한다는 원칙이다. 즉, 새로운 기능을 만든다고 했을 때 기존의 클래스를 수정하는 것이 아닌 새로운 클래스를 만들어 확장 가능해야 한다는 것이다.</p>
<h3 id="ocp를-위반한-경우">OCP를 위반한 경우</h3>
<pre><code class="language-cs">public class AreaCalculator
{
 public float GetRectangleArea(Rectangle rectangle)
 {
 return rectangle.width * rectangle.height;
 }
 public float GetCircleArea(Circle circle)
 {
 return circle.radius * circle.radius * Mathf.PI;
 }
}
public class Rectangle
{
 public float width;
 public float height;
}
public class Circle
{
 public float radius;
} 
</code></pre>
<p>해당 클래스에서 더 많은 모형을 추가하려면 각 모형에 대한 매서드를 생성해야 한다. 초반에는 괜찮을 수 있지만 만약 모형을 앞으로 수십개 더 추가해야 한다면 AreaCalculator 클래스는 감당할 수 없을 만큼 커지게 될 것이다. 이러한 경우 OCP를 위반했다고 볼 수 있다.</p>
<h3 id="ocp에-기반한-설계-방법">OCP에 기반한 설계 방법</h3>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/7056446a-bbac-45b7-9df1-e76e49badd9d/image.png" alt=""></p>
<pre><code class="language-cs">public abstract class Shape
{
 public abstract float CalculateArea();
} </code></pre>
<p>Shape 추상 클래스를 정의하면 클래스 확장에 유연하게 설계 가능하다.</p>
<pre><code class="language-cs">public class Rectangle : Shape
{
 public float width;
 public float height;
 public override float CalculateArea()
 {
 return width * height;
 }
}
public class Circle : Shape
{
 public float radius;
 public override float CalculateArea()
 {
 return radius * radius * Mathf.PI;
 }
}</code></pre>
<p>이렇게 모형에 대한 클래스를 만들고,</p>
<pre><code class="language-cs">public class AreaCalculator
{
 public float GetArea(Shape shape)
 {
 return shape.CalculateArea();
 }
}
public class AreaCalculator : MonoBehaviour
{
 private void Start()
 {
 Debug.Log(GetArea(new RectAngle { width = 2, height = 3 }));
 Debug.Log(GetArea(new Circle { radius = 3 }));
 }
 public float GetArea(Shape shape)
 {
 return shape.CalculateArea();
}</code></pre>
<p>AreaCalculator 클래스를 단순화 할 수 있다.</p>
<p>이렇게하면 AreaCalculator 클래스는 직접적인 수정없이 Shape 클래스를 적절히 구현하는 것으로 기능 확장이 가능해진다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/d6c561e8-9f2e-44c9-9799-af33825c256b/image.png" alt=""></p>
<p>이러한 설계는 디버깅하기도 좋다. 새로운 모형에서 오류가 발생해도 AreaCalculator를 검사할 필요가 없다. 기존 코드는 변경 없이 유지되기 때문에, 새롭게 만든 코드만 확인하면 된다. 즉, 새로운 기능을 더 유연하게 추가 확장할 수 있다.</p>
<h2 id="리스코프-치환-원칙lsp-liskov-substitution-principle">리스코프 치환 원칙(LSP, Liskov Substitution Principle)</h2>
<p>LSP는 자식 클래스는 항상 부모 클래스를 대체할 수 있어야한다는 원칙이다. OOP에서 상속을 통해 기능을 추가할 수 있다. 하지만 조심해서 사용하지 않으면 오히려 복잡도가 높아질 수 있다.</p>
<h3 id="lsp를-위반한-경우">LSP를 위반한 경우</h3>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/e3f89030-4c98-4142-bbe5-04e5fdd77a41/image.png" alt="">
위와 같이 Vehicle을 상속받는 Car와 Truck 클래스가 있다고 해보자. 여기까지는 크게 문제가 없다. 그러나 Train을 추가한다면 어떨까? 
<img src="https://velog.velcdn.com/images/jisub_shim/post/2a7ebf18-b91a-4b44-b3ab-debc6c31d10e/image.png" alt=""></p>
<p>Train은 철도 위에서 직진, 후진만 할 뿐 좌회전, 우회전을 하지 않는다. 즉, Vehicle을 상속받았지만 TurnRight()와 TurnLeft()는 아무런 기능을 하지않는 일이 발생한다. 이러한 경우를 리스코프 치환 원칙을 위반했다고 한다.</p>
<h3 id="lsp를-준수하기-위한-몇-가지-방안">LSP를 준수하기 위한 몇 가지 방안</h3>
<p><strong>1. 자식 클래스를 만들 때 메서드를 비워두는 일이 생기면 안된다.</strong>
    NotImplementedException은 LSP를 위반했다는 의미이며, 메서드를 비워두는 경우도 마찬가지이다. 자식 클래스가 부모 클래스처럼 동작하지 않는다면 오류나 예외가 명시적으로 보이지 않더라도 LSP를 준수하지 않은 것이다.</p>
<p><strong>2. 추상화를 단순하게 유지한다.</strong>
    부모 클래스에 들어가는 로직이 많을수록 LSP를 위반할 확률도 커진다. 부모 클래스는 자식 클래스의 일반적인 기능만 표현해야 한다.</p>
<p><strong>3. 자식 클래스는 부모 클래스와 동일한 공용 멤버를 가져야 한다.</strong>
    공용 멤버는 호출 시 동일한 시그니처와 동작을 취해야 한다.</p>
<p><strong>4. 클래스 계층 구조를 수립할 때 클래스 API를 고려해야 한다.</strong>
대상을 모두 Vehicle로 간주하더라도 Car와 Train은 각각 서로 다른 부모 클래스로부터 상속하는 편이 더 나을 수 있다. 실질적으로 분류가 항상 클래스 계층 구조와 일치하지는 않는다.</p>
<p><strong>5. 상속보다는 합성을 우선시 한다.</strong>
상속을 통한 기능 전달 대신, 특정한 동작을 캡슐화할 수 있도록 인터페이스나 별도의 클래스를 만드는 편이 좋다. 그런 다음 적절히 조합하여 다양한 기능의 합성물을 만든다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/e6ca6b97-55bb-45d0-96a5-59111cd75815/image.png" alt=""></p>
<h3 id="lsp를-잘-지킨-경우">LSP를 잘 지킨 경우</h3>
<p>Vehicle 클래스를 삭제한 다음 대부분의 기능은 인터페이스로 옮긴다.</p>
<pre><code class="language-cs">public interface ITurnable
{
 public void TurnRight();
 public void TurnLeft();
}
public interface IMovable
{
 public void GoForward();
 public void Reverse();
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/9508cb47-223b-46e0-afb2-7b359877b934/image.png" alt="">
RoadVehicle 클래스와 RailVehicle 클래스를 만들면 LSP를 더 철저하게 지킬 수 있다. Car와 Train은 해당하는 부모 클래스로부터 상속한다.</p>
<p>이러한 방법에서는 기능이 상속 대신 인터페이스를 통해 실행된다. Car와 Train은 더 이상 같은 부모 클래스를 공유하지 않으며, 이로인해 자식 클래스인 Car, Train은 각각의 부모 클래스를 대체할 수 있게 된다. 즉, LSP를 준수한다.</p>
<h2 id="인터페이스-분리-원칙isp-interface-segregation-principle">인터페이스 분리 원칙(ISP, Interface Segregation Principle)</h2>
<p>ISP는 반드시 객체가 자신에게 필요한 기능만을 가지도록 인터페이스를 분리하여 제한하는 원칙이다. 불필요한 상속이나 구현을 최대한 방지함으로써 객체의 불필요한 책임을 제거한다. 다시 말해, 인터페이스의 규모가 커지지 않도록 설계 해야 한다.</p>
<pre><code class="language-cs">{
 public float Health { get; set; }
 public int Defense { get; set; }

 public void Die();
 public void TakeDamage();
 public void RestoreHealth();

 public float MoveSpeed { get; set; }
 public float Acceleration { get; set; }

 public void GoForward();
 public void Reverse();
 public void TurnLeft();
 public void TurnRight();

 public int Strength { get; set; }
 public int Dexterity { get; set; }
 public int Endurance { get; set; }
}</code></pre>
<p>다양한 유닛이 있는 전략 게임을 만들 때 위와 같이 각 유닛에는 체력, 속도 등 다양한 스탯과 동작이 존재한다.
부술 수 있는 통이나 상자 등 파괴 가능한 프랍을 만든다고 가정해보자. 비록 움직이지 않는 프랍이지만 체력이라는 개념이 필요하다. 또한 상자나 통에는 게임 내의 다른 유닛에 부여된 능력 중 대부분이 부여되지 않은 것이다.
이 때, 너무 많은 메서드를 부여하는 인터페이스 한 개를 만드는 대신, 여러 개의 작은 인터페이스로 분할하여 필요한 요소만 선택해 사용하도록 할 수 있다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/dfcb32eb-b839-4b3a-b1ee-e380fa87e652/image.png" alt=""></p>
<pre><code class="language-cs">public class ExplodingBarrel : MonoBehaviour, IDamageable, IExplodable
{
 ...
}
public class EnemyUnit : MonoBehaviour, IDamageable, IMovable, IUnitStats
{
 ...
}</code></pre>
<p>이러한 방식으로 인터페이스를 분리하여 오브젝트가 게임 환경과 더 유연한 방식으로 상호 작용 가능해진다. </p>
<h2 id="의존성-역전-원칙dip-dependency-inversion-principle">의존성 역전 원칙(DIP, Dependency Inversion Principle)</h2>
<p>DIP는 상위 수준의 모듈이 하위 수준의 모듈에서 어떠한 것도 직접 가져오면 안된다는 원칙이다. 이를 위해 양측 모두 추상화에 의존해야 한다.
DIP는 클래스 간의 결합도를 줄이는 데 도움이 될 수 있다. 애플리케이션에서 클래스와 시스템을 만들 때 자연스럽게 일부는 상위 수준이 되고 일부는 하위 수준이 된다. 보통 상위 수준 클래스는 하위 수준 클래스에 의존해서 작업을 수행하는데, SOLID 원칙에서는 이를 바꿔야 한다고 강조한다.
플레이어가 문을 트리거해서 여는 로직을 개발한다고 가정해보자.</p>
<h3 id="dip를-위반한-경우">DIP를 위반한 경우</h3>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/6470567f-f44b-4a53-8db1-9384c9f3d9d6/image.png" alt=""></p>
<pre><code class="language-cs">{
 public Door door;
 public bool isActivated;
 public void Toggle()
 {
 if (isActivated)
 {
 isActivated = false;
 door.Close();
 }
 else
 {
 isActivated = true;
 door.Open();
 }
 }
}
public class Door : MonoBehaviour
{
 public void Open()
 {
 Debug.Log(“The door is open.”);
 }
 public void Close()
 {
 Debug.Log(“The door is closed.”);
 }
} </code></pre>
<p>Switch는 Toggle()을 호출해서 문을 여닫을 수 있다. 이 코드는 작동하기는 하지만 Door에서 직접 Switch로 연결되는 종속성이 발생한다는 문제가 있다. Switch 로직을 통해 Door 외의 조명, 전원 등을 토글하는 경우에 사용되려면 어떻게 해야할까? Switch 클래스에 해당 객체를 참조하여 해당하는 메서드를 추가할 수도 있지만, 그러면 OCP를 위반하게 되고, 기능을 확장할 때마다 원본 코드를 수정해야 한다.</p>
<h3 id="dip에-기반한-설계-방법">DIP에 기반한 설계 방법</h3>
<p>이러한 문제도 역시 추상화로 해결 가능하다. 클래스 사이에 ISwitchable 이라는 인터페이스를 삽입하면 된다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/4ccaaa4f-5a4f-479a-9ee7-98c8b60c25ea/image.png" alt=""></p>
<pre><code class="language-cs">public class Switch : MonoBehaviour
{
 public ISwitchable client;
 public void Toggle()
 {
 if (client.IsActive)
 {
 client.Deactivate();
 }
 else
 {
 client.Activate();
 }
 }
} </code></pre>
<p>이렇게하면 Switch는 문에 직접 의존하지 않고 ISwitchable 클라이언트에 의존하게 된다. 이전에는 Switch가 Door에만 작동했으나, 이제는 ISwitchable을 구현하는 모든 요소에 동작한다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/14338af4-9030-4015-84ae-5275140f3c07/image.png" alt=""></p>
<p>인터페이스를 도입하면서 저수준 모듈이 인터페이스를 구현하는 구조로 변경되었다. 이렇게 되면 구체적인 구현이 아닌 추상화에 의존하게 된다. 즉, 저수준 모듈이 고수준 모듈의 요구에 맞춰 동작하는 구조가 되어 의존성이 역전된다. 이를 통해 결합도를 낮춰 프로젝트를 간편하게 확장할 수 있다.</p>
<h2 id="마무리">마무리</h2>
<p>OOP의 기본이 되는 SOLID 원칙을 정리해 보았다. 
절대적인 법칙은 아니지만, 이를 참고하면 유지보수성이 높고 깔끔한 코드를 작성하는 데 좋은 가이드라인이 될 것이다.</p>
<p>하지만 원칙에 얽매여 억지로 적용하기보다, KISS 원칙을 떠올리며 코드를 단순하게 유지하는 것이 더 중요하다.
확장할 필요가 없거나 한 번 구현 후 수정할 일이 거의 없는 기능이라면, 불필요한 추상화가 오히려 코드의 복잡도를 증가시킬 수도 있다.</p>
<p>결국, SOLID 원칙은 하나의 도구일 뿐, 무조건 적용해야 하는 법칙이 아니다.
따라서 필요할 때 유기적으로 활용하는 것이 가장 효과적이라고 생각한다.</p>
<h2 id="레퍼런스">레퍼런스</h2>
<p><a href="https://unity.com/kr/resources/design-patterns-solid-ebook">https://unity.com/kr/resources/design-patterns-solid-ebook</a>
<a href="https://www.youtube.com/watch?v=J6F8plGUqv8">https://www.youtube.com/watch?v=J6F8plGUqv8</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity] Assembly Definition]]></title>
            <link>https://velog.io/@jisub_shim/Unity-Assembly-Definition</link>
            <guid>https://velog.io/@jisub_shim/Unity-Assembly-Definition</guid>
            <pubDate>Thu, 30 Jan 2025 13:59:07 GMT</pubDate>
            <description><![CDATA[<h2 id="어셈블리-정의-assembly-definition">어셈블리 정의 (Assembly Definition)</h2>
<p>프로젝트 규모가 커질수록 컴파일 시간이 길어지는 경우가 많다. 이러한 문제를 개선할 수 있는 방법 중 하나는 바로 &#39;어셈블리 정의&#39;이다.
유니티의 어셈블리 정의(Assembly Definition)는 C# 스크립트의 컴파일 단위를 나누는 기능이다. 기본적으로 모든 C# 스크립트는 Assembly-CSharp.dll에 포함되지만, 어셈블리 정의를 사용하면 여러 개의 어셈블리로 분리할 수 있다. 
단, 예외적으로 Editor 폴더 아래의 스크립트들은 Assembly-Csharp-Editor.dll로 컴파일한다.</p>
<h2 id="어셈블리-정의의-장점">어셈블리 정의의 장점</h2>
<ol>
<li><p><strong>컴파일 속도 최적화</strong></p>
<ul>
<li>변경된 어셈블리만 다시 컴파일하기 때문에 불필요한 전체 컴파일 방지할 수 있다.<br>
</li>
</ul>
</li>
<li><p><strong>모듈화된 코드 관리</strong></p>
<ul>
<li>UI, Core, Player 등 관련 코드들을 폴더 단위로 분리하여 어셈블리 정의하면 유지보수 관점에서 용이하다.<br>
</li>
</ul>
</li>
<li><p><strong>의존성(Dependency) 설정 가능</strong></p>
<ul>
<li>특정 어셈블리만 참조하도록 설정하여 불필요한 코드는 접근 차단할 수 있다.<br>
</li>
</ul>
</li>
<li><p><strong>테스트 코드 분리 가능</strong></p>
<ul>
<li>Editor 전용 코드와 Runtime 코드를 따로 관리 가능하다. 빌드 시 Editor 코드는 제외된다.</li>
</ul>
</li>
</ol>
<h2 id="어셈블리-정의하는-법">어셈블리 정의하는 법</h2>
<ol>
<li><strong>새 어셈블리 정의 파일(.asmdef) 생성</strong><ul>
<li>&#39;Assets → Create → Assembly Definition&#39; 경로를 통해 .asmdef 파일을 생성한다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/a8cb573b-ae74-4f5a-b1af-e9f0aa1f4ace/image.png" alt="">
<img src="https://velog.velcdn.com/images/jisub_shim/post/e491ddff-b094-4209-8ee4-91933da2019c/image.png" alt=""></li>
</ul>
</li>
</ol>
<br>

<ol start="2">
<li><strong>필요한 참조 추가 (Assembly Definition References 설정)</strong><ul>
<li>다른 어셈블리를 참조하려면, 해당 .asmdef 파일에서 Assembly Definition References 항목을 설정해야 한다.</li>
<li>참조하려는 .asmdef 파일을 선택하여 추가할 수 있다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/22ab9e00-397a-4443-92aa-0bd92ede9183/image.png" alt=""></li>
</ul>
</li>
</ol>
<br>

<ol start="3">
<li><strong>파일을 해당 폴더에 배치하면 적용 완료</strong><ul>
<li>적용하면 해당 폴더와 그 하위 폴더에 있는 스크립트들은 해당 어셈블리로 묶이게 된다.</li>
</ul>
</li>
</ol>
<h2 id="어셈블리-정의-사용-시-주의점">어셈블리 정의 사용 시 주의점</h2>
<ol>
<li><p><strong>런타임 성능에는 영향을 주지 않는다.</strong></p>
<ul>
<li>따라서 런타임 성능 최적화를 목적으로 사용하는 것은 적절하지 않다.</li>
</ul>
</li>
</ol>
<ol start="2">
<li><p><strong>어셈블리 간 순환 참조 방지해야 한다.</strong></p>
<ul>
<li><p>유니티에서 .asmdef 파일을 사용할 때, 서로 참조(순환 참조)하는 두 개의 어셈블리는 동시에 컴파일할 수 없다. A가 B를 컴파일하기 전에 필요로 하고, B가 A를 컴파일하기 전에 필요로 하기 때문에 서로를 먼저 컴파일해야 하는 상황이 발생할 수 있기 때문이다.</p>
</li>
<li><p>OOP 관점에서도 상호 참조가 되어버리면 커플링으로 서로 강하게 묶여버려서 모듈화의 의미가 사라지게 된다. 이렇게 의존성이 꼬여 있으면 한쪽을 수정할 때 다른 쪽도 수정해야하는 경우가 많아 유지보수가 어려워진다. </p>
</li>
</ul>
</li>
</ol>
<ol start="3">
<li><p><strong>너무 세분화하면 관리가 어려워질 수 있다.</strong></p>
<ul>
<li>어셈블리를 지나치게 세분화하면 설정이 복잡해지기 때문에 부담이 커질 수 있다.</li>
</ul>
</li>
</ol>
<h2 id="마무리">마무리</h2>
<p>어셈블리 정의는 컴파일 최적화 기법이다. 주로 규모가 큰 프로젝트에서 컴파일 속도를 줄이고, 코드 모듈화를 돕는 역할을 한다. 또, 자주 변경되는 코드와 변경이 적은 코드를 분리하면 빌드 속도도 빨라질 수 있다.
프로젝트 규모와 상황에 맞춰 적절하게 사용하면 개발 효율과 유지보수 측면에서 유용하게 사용할 수 있을 것 같다.</p>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://www.youtube.com/watch?v=I_2a2wRSF1o">https://www.youtube.com/watch?v=I_2a2wRSF1o</a></p>
<p><a href="https://docs.unity3d.com/kr/2023.2/Manual/ScriptCompilationAssemblyDefinitionFiles.html">https://docs.unity3d.com/kr/2023.2/Manual/ScriptCompilationAssemblyDefinitionFiles.html</a></p>
<p><a href="https://learn.microsoft.com/ko-kr/dotnet/standard/assembly/">https://learn.microsoft.com/ko-kr/dotnet/standard/assembly/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Univ] 게임과 인문학 'IDOL']]></title>
            <link>https://velog.io/@jisub_shim/%EA%B2%8C%EC%9E%84%EA%B3%BC-%EC%9D%B8%EB%AC%B8%ED%95%99-IDOL</link>
            <guid>https://velog.io/@jisub_shim/%EA%B2%8C%EC%9E%84%EA%B3%BC-%EC%9D%B8%EB%AC%B8%ED%95%99-IDOL</guid>
            <pubDate>Sun, 22 Dec 2024 16:28:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>서울과학기술대학교 &#39;게임과 인문학&#39; 에서 연출로 참여한 게임 &#39;IDOL&#39;이 완성되었습니다!</strong></p>
</blockquote>
<p>게더타운과 트와인을 이용하여 만든 메타버스 속 게임으로 &#39;무대 사고로 얼굴에 화상을 입은 아이돌&#39;의 이야기를 담았습니다.</p>
<h3 id="기본-설명">기본 설명</h3>
<ol>
<li>비밀번호를 입력할 때, 눈 모양 아이콘을 누른 후 입력하시면 됩니다.</li>
<li>오브젝트는 X키를 눌러 상호작용 할 수 있습니다.</li>
<li>비밀번호를 열고 나가면 항상 오브젝트가 있는데, 꼭 X키로 상호작용해서 <strong>트와인 대화를 감상</strong>해주시면 감사하겠습니다. (중요!)</li>
<li>주인공이 되었다 생각하고 플레이 하면 더욱 몰입할 수 있습니다.</li>
</ol>
<h3 id="작품-링크">작품 링크</h3>
<p>아래 링크로 플레이 하실 수 있습니다.
후기까지 남겨주신다면 정말 감사하겠습니다!😊</p>
<p><a href="https://app.gather.town/invite?token=dTPKJb9URdepA0gwbCuh">[6조] &#39;IDOL&#39; 플레이 하러가기</a></p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/c76fab71-c61f-48a3-be2b-8c1461f26b19/image.png" alt=""></p>
<h3 id="다른-조-작품-링크">다른 조 작품 링크</h3>
<p>저희 조 이외의 다른 작품 링크도 공유합니다!</p>
<p><a href="https://app.gather.town/invite?token=zi0DpX9eT_KT_qiprEs9">[1조] 환승역전</a>
<a href="https://app.gather.town/invite?token=Zt6s0HUETiaKFphiu22n">[2조] Oh, D</a>
<a href="https://app.gather.town/invite?token=15K490htTleYfZsd5KhC">[3조] 크리스마스 팔레트</a>
<a href="https://app.gather.town/invite?token=DvWNb8dASG-uR720j1Hm">[4조] The door to the inside</a>
<a href="https://app.gather.town/invite?token=1Pdm2QGPRh6ILHSAdCNI">[5조] Project HIKI</a>
<a href="https://app.gather.town/app/UqiKQhfUunAZUGFI/In%20Dream">[7조] XXX&#39;s Game of Life</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity] Rect Transform과 해상도]]></title>
            <link>https://velog.io/@jisub_shim/RectTransformAndResolution</link>
            <guid>https://velog.io/@jisub_shim/RectTransformAndResolution</guid>
            <pubDate>Wed, 16 Oct 2024 13:47:05 GMT</pubDate>
            <description><![CDATA[<h2 id="rect-transform-이란">Rect Transform 이란?</h2>
<p>UI에 한정해서 사용하는 컴포넌트입니다.</p>
<p>기본적으로 Transform을 상속을 받고 있어서, Transform의 필드나 함수들을 가지고 있습니다.</p>
<h3 id="anchor">Anchor</h3>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/1ec9b6ad-d8fb-484f-950e-0365af18d9a1/image.png" alt=""></p>
<ul>
<li>Anchor는 4개의 삼각형(핀)의 형태로 존재합니다.<ul>
<li>위 사진처럼 Anchor가 모여있으면, Anchor의 위치가 UI 좌표계의 원점이 됩니다.</li>
</ul>
</li>
</ul>
<h3 id="pivot">Pivot</h3>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/368ddfcb-22de-4725-802d-c7769326b894/image.png" alt=""></p>
<ul>
<li>UI 오브젝트의 중점이라고 생각하면 좋습니다.</li>
</ul>
<h3 id="ui의-position">UI의 Position</h3>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/443fd463-23aa-4747-a04f-66dd206a727d/image.png" alt="">
<img src="https://velog.velcdn.com/images/jisub_shim/post/3a60c557-cb6a-4cf6-85d2-fe20598b2dec/image.png" alt=""></p>
<p>즉, UI 오브젝트(Box)의 Position은 Anchor를 원점으로 하여, 중점인 Pivot까지의 거리라고 볼 수 있습니다.</p>
<h3 id="anchor는-나눌-수-있다">Anchor는 나눌 수 있다.</h3>
<p>Anchor는 아래 사진처럼 나눌 수 있습니다.</p>
<p>이를 <strong>Stretch</strong>라고 표현합니다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/23cc2383-dead-4777-9708-a715555cc1a6/image.png" alt=""></p>
<p>이렇게 나누게 되면 Rect Transform의 Position 정보가 (x, y, z)에서 (Left, Right, Top, Bottom)의 형태로 바뀌게 됩니다. 각 원소들은 각각 해당 부분과 떨어져있는 거리입니다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/eee46a30-5555-4295-91a1-ca41c8865e32/image.png" alt=""></p>
<p><strong>위에 작성한 내용은 이 영상에 더 자세하게 나와있으니 참고하면 좋을 것 같습니다.</strong></p>
<p><a href="https://www.youtube.com/watch?v=A0prWX3afwg">참고 영상: 베르 유튜브</a></p>
<ul>
<li><strong>만약 Pivot이 움직이지 않는다면?</strong></li>
</ul>
<p>Scene창 좌측 상단의 이 부분을 Center → Pivot으로 바꿔주면 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/2bdb2bf0-363c-4673-a18c-cea9cf453e40/image.png" alt=""></p>
<h2 id="부모-자식-관계일-때의-rect-transform">부모-자식 관계일 때의 Rect Transform</h2>
<p>아래 이미지를 보면, White Box는 부모, Red Box는 자식입니다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/a0202b18-6b0a-430d-bdbc-61be7b9d613d/image.png" alt=""></p>
<blockquote>
<p>White Box : 부모 
Red Box : 자식</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/4de32b65-d637-46a7-94a1-da046877eb55/image.png" alt=""></p>
<p>White Box는 Canvas의 자식이므로 Anchor는 Canvas의 Pivot에 맞춰져 있습니다.</p>
<p>기본적으로 자식의 Anchor는 부모의 Pivot 위치에 설정되며, 부모의 위치가 바뀌면 자식도 그에 맞게 이동합니다.</p>
<p>그러나 지금의 경우에는 부모의 <strong>크기</strong>를 변경해도 자식의 크기는 변하지 않습니다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/90ae78be-1a64-4b93-87f7-8075eb7f9c32/image.png" alt=""></p>
<p>부모의 크기가 변하더라도 자식의 크기는 변하지 않는데, 자식의 크기도 함께 변경되도록 하려면 어떻게 해야 할까요?</p>
<h3 id="anchor-설정으로-부모-자식-크기-동기화">Anchor 설정으로 부모-자식 크기 동기화</h3>
<p>정답은 자식의 Anchor를 조정하는 것입니다. </p>
<p>부모 크기에 따라 자식의 크기도 함께 변하게 하려면, 자식의 Anchor를 부모의 가장자리에 설정해 주면(Stretch)됩니다. Anchor를 &quot;박아둔다&quot;고 생각하면 이해가 쉬운데, 이렇게 하면 부모의 크기가 변할 때 자식도 같이 커지게 됩니다.</p>
<h4 id="자식-anchor를-부모의-가장자리에-박아두고">자식 Anchor를 부모의 가장자리에 박아두고</h4>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/84df2292-d79e-4b43-8c6d-31382c553013/image.png" alt=""></p>
<h4 id="자식-anchor를-부모의-가장자리에-박아두고부모의-크기를-조정하면-자식의-크기도-자동으로-조정">자식 Anchor를 부모의 가장자리에 박아두고부모의 크기를 조정하면 자식의 크기도 자동으로 조정</h4>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/228f016a-5712-4e94-b70a-03bea998c6e1/image.png" alt=""></p>
<h3 id="해상도에-따른-대응-방법">해상도에 따른 대응 방법</h3>
<p>여기까지는 부모-자식 간의 크기 비율을 동기화하는 방법이었지만, 해상도가 바뀌어도 UI가 적절히 대응하는 방법은 따로 필요합니다. (사실 방법은 동일합니다.)</p>
<p>그 이유는 Canvas의 크기(해상도)가 달라져도 Canvas의 자식인 WhiteBox의 Anchor는 정중앙에 박혀있기 때문에, 크기 변화가 없기 때문입니다.</p>
<h4 id="fhd">FHD</h4>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/72ad00e2-7436-4c63-aec5-37955650430b/image.png" alt=""></p>
<h4 id="qhd">QHD</h4>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/1e354a80-bb67-46cc-b176-61487cd91e87/image.png" alt=""></p>
<h4 id="640x360">640x360</h4>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/d3b349dc-4393-452e-a3c3-dc9e07fa2173/image.png" alt=""></p>
<p>이 문제를 해결하려면 자식인 White Box의 Anchor도 Stretch해 주어야 합니다. 아래 이미지처럼 자식의 Anchor를 부모의 모서리에 맞춰 Stretch하면, 해상도가 달라져도 자식 UI 요소가 부모 UI 요소의 크기 변화에 맞춰 자동으로 조정됩니다.
<img src="https://velog.velcdn.com/images/jisub_shim/post/07b7bc1b-ed1a-444a-b586-055f5e18b48c/image.png" alt="">
이 방식으로 해상도가 변경되더라도 UI 비율이 유지되는 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/jisub_shim/post/6b0ed35c-2041-43b2-ac0d-1c1ef25038e2/image.gif" alt="">
<strong>이처럼 Rect Transform에서 Anchor와 Pivot의 설정을 적절히 활용하면 해상도에 맞춰 자동으로 조정되는 UI를 구현할 수 있습니다.</strong></p>
]]></description>
        </item>
    </channel>
</rss>