<?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>Fri, 16 May 2025 14:39:25 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] 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>
    공용 멤버는 호출 시 동일한 서명(Signature)과 동작을 취해야 한다.</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>이러한 방식으로 인터페이스를 분리하여 오브젝트가 게임 환경과 더 유연한 방식으로 상호 작용 가능해진다. 
ISP는 LSP와 유사하게 상속보다 합성을 우선시한다. 이는 시스템을 분리하고 간편하게 수정 및 확장하는데 도움이 된다.</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>