<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>will-big.log</title>
        <link>https://velog.io/</link>
        <description>개발자가 되고싶어요</description>
        <lastBuildDate>Sun, 12 Apr 2026 09:52:54 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>will-big.log</title>
            <url>https://images.velog.io/images/will-big/profile/6b6ecda0-a32f-40f6-91d2-3cd1b56455b5/social.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. will-big.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/will-big" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[C 언어로 Lox 인터프리터 만들기 8]]></title>
            <link>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-8</link>
            <guid>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-8</guid>
            <pubDate>Sun, 12 Apr 2026 09:52:54 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>이전 챕터에서 <code>clox</code>는 문자열(<code>ObjString</code>)을 힙에 할당할 수 있게 되었습니다. 하지만 여기서 몇 가지 불편함이 생깁니다.</p>
<p>앞으로 변수를 구현하면, 변수 이름(문자열)으로 값을 저장하고 조회해야 합니다. 만약 변수들을 배열에 순서대로 넣어둔다면, 변수를 찾을 때마다 처음부터 끝까지 하나씩 비교해야 합니다. 변수가 100개면 최악의 경우 100번 비교 — O(n)입니다.</p>
<p>또 다른 문제는 문자열 비교입니다. <code>&quot;hello&quot; == &quot;hello&quot;</code>를 판단하려면 현재는 <code>memcmp</code>로 문자 하나하나를 비교해야 합니다. 문자열이 길수록 느려집니다.</p>
<p>이 두 문제를 한 번에 해결하는 자료구조가 해시 테이블입니다. <code>key</code>를 숫자로 변환해서 배열 인덱스로 직접 접근하면 평균 <code>O(1)</code>에 조회할 수 있고, 이 위에 <code>string interning</code>을 구현하면 문자열 비교도 포인터 비교 한 번으로 끝낼 수 있습니다.</p>
<h2 id="해시-테이블의-기본-원리">해시 테이블의 기본 원리</h2>
<p>해시 테이블의 핵심은 단순합니다. <code>key</code>를 숫자로 변환해서 배열의 인덱스로 사용하는 것입니다.</p>
<p>예를 들어 변수 이름이 <code>a</code>~<code>z</code> 하나뿐인 언어라면, <code>&#39;c&#39; - &#39;a&#39; = 2</code>로 바로 <code>배열[2]</code>에 접근할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/2a040400-e0c4-4927-9734-70342bc12163/image.png" alt=""></p>
<p>현실에서는 <code>key</code> 공간이 훨씬 크기 때문에, 해시 함수로 얻은 값을 모듈로(<code>%</code>) 연산으로 배열 크기에 맞춥니다.</p>
<pre><code class="language-c">index = hash(key) % capacity</code></pre>
<p>여기서 중요한 개념이 <code>load factor</code>(적재율)입니다.</p>
<pre><code>load factor = 엔트리 수 / 버킷 수</code></pre><p><code>load factor</code>가 높으면 충돌이 잦아지고, 낮으면 메모리가 낭비됩니다. <code>clox</code>에서는 <code>0.75</code>를 임계값으로 사용하여, 이를 넘으면 배열을 확장합니다.</p>
<h2 id="충돌-처리">충돌 처리</h2>
<p>서로 다른 <code>key</code>가 같은 인덱스에 매핑되는 것을 충돌(collision)이라 합니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/5a061c1c-8dd1-49d2-848e-a2ef1fedc242/image.png" alt=""></p>
<p>충돌을 처리하는 방식은 크게 두 가지입니다.</p>
<h3 id="separate-chaining">Separate Chaining</h3>
<p>각 버킷이 연결 리스트를 가지는 방식입니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/7b19de14-0d38-48f0-99ec-18c0b1f34849/image.png" alt=""></p>
<p>구현이 직관적이지만, 리스트 노드가 메모리에 흩어져서 캐시 효율이 떨어집니다.</p>
<h3 id="open-addressing">Open Addressing</h3>
<blockquote>
<p><code>clox</code>가 선택한 방식입니다.</p>
</blockquote>
<p>모든 엔트리를 하나의 연속 배열에 저장합니다. 충돌이 나면 배열 안에서 다음 빈 칸을 찾습니다. 그 중 가장 단순한 방식이 <code>linear probing</code> — 다음 칸, 그 다음 칸 순서대로 확인하는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/cc14ae9e-305b-47a0-bd2d-2375e971f84c/image.png" alt=""></p>
<p><code>clox</code>가 <code>open addressing</code>을 선택한 이유는 CPU 캐시 효율 때문입니다. 현대 CPU는 메모리를 캐시 라인(보통 64바이트) 단위로 가져옵니다. 연속 배열은 한 번에 여러 엔트리를 캐시에 올릴 수 있지만, 연결 리스트는 노드마다 포인터를 따라가야 하므로 캐시 미스가 잦아집니다.</p>
<h3 id="탐색-원리">탐색 원리</h3>
<blockquote>
<p>밀려난 엔트리를 어떻게 다시 찾는가?</p>
</blockquote>
<p><code>open addressing</code>에서 흥미로운 점은, 엔트리가 원래 위치에서 밀려났다는 정보를 따로 기록하지 않는다는 것입니다.</p>
<p>대신 삽입할 때와 같은 탐색 순서를 반복하면 반드시 찾을 수 있습니다. 해시값에서 시작해서 빈 칸을 만날 때까지 순서대로 확인하면, 있으면 반드시 만나고, 빈 칸을 만나면 &quot;없다&quot;고 판단할 수 있습니다.</p>
<pre><code>&quot;cake&quot;를 찾을 때 (hash(&quot;cake&quot;) % cap = 2):

[2] &quot;bagel&quot;  → 내가 찾는 key가 아님 → 다음
[3] &quot;jam&quot;    → 아님 → 다음
[4] &quot;cake&quot;   → 찾았다!</code></pre><h3 id="tombstone--삭제-후에도-탐색-체인-유지하기"><code>tombstone</code> — 삭제 후에도 탐색 체인 유지하기</h3>
<blockquote>
<p>그냥 비우면 왜 안 되는가?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/will-big/post/9beadbf7-b2f1-4bb8-b191-6d2caab3e329/image.png" alt=""></p>
<p>엔트리를 그냥 비우면 탐색 체인이 끊어져서, 뒤에 밀려나 있는 엔트리를 찾을 수 없게 됩니다.</p>
<p>해결책은 <code>tombstone</code>(묘비)입니다. 삭제된 자리를 빈칸으로 만드는 대신, &quot;여기는 삭제되었지만 탐색은 계속해라&quot;라는 특수 표식을 남깁니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/919190ab-9d7a-4462-a4ef-2775c8a2c061/image.png" alt=""></p>
<p><code>clox</code>에서는 <code>key = NULL, value = BOOL_VAL(true)</code>로 표현합니다. 빈 칸(<code>value</code>가 <code>NIL</code>)과 <code>tombstone</code>(<code>value</code>가 <code>NIL</code>이 아님)을 구분하는 것이 핵심입니다.</p>
<pre><code class="language-c">bool tableDelete(Table* table, ObjString* key) {
    if (table-&gt;count == 0) return false;

    Entry* entry = findEntry(table-&gt;entries, table-&gt;capacity, key);
    if (entry-&gt;key == NULL) return false;

    // tombstone 배치: key를 비우고 value에 표식을 남긴다
    entry-&gt;key = NULL;
    entry-&gt;value = BOOL_VAL(true);
    return true;
}</code></pre>
<p><code>tombstone</code>은 배열을 리사이징(rehashing)할 때 자연스럽게 정리됩니다. 살아있는 엔트리만 새 배열에 옮기므로 <code>tombstone</code>은 사라집니다. 이런 방식을 지연 삭제라고 합니다.</p>
<h2 id="해시-함수--fnv-1a">해시 함수 — FNV-1a</h2>
<p>좋은 해시 함수는 세 가지 조건을 만족해야 합니다.</p>
<ol>
<li>결정성: 같은 입력이면 항상 같은 결과</li>
<li>균등성: 출력이 배열 전체에 골고루 분산</li>
<li>속도: 해시 테이블의 모든 연산이 해시 계산을 거치므로, 느리면 의미가 없음</li>
</ol>
<p><code>clox</code>는 FNV-1a 알고리즘을 사용합니다.</p>
<pre><code class="language-c">static uint32_t hashString(const char* key, int length) {
    uint32_t hash = 2166136261u;  // FNV offset basis
    for (int i = 0; i &lt; length; i++) {
        hash ^= (uint8_t)key[i];  // XOR로 현재 바이트 반영
        hash *= 16777619;          // 곱셈으로 비트를 전체 범위에 확산
    }
    return hash;
}</code></pre>
<p>XOR은 입력 바이트의 비트 패턴을 해시값에 반영하고, 곱셈은 비트를 전체 범위로 퍼뜨려 균등 분산을 만듭니다. 이 두 연산의 조합이 중요합니다. XOR만 쓰면 <code>&quot;ab&quot;</code>와 <code>&quot;ba&quot;</code>의 해시가 같아지고(순서 구분 불가), 곱셈만 쓰면 비슷한 입력이 비슷한 출력을 만들어 분산이 나빠집니다.</p>
<p>문자열은 불변(immutable)이므로, 생성 시점에 해시를 한 번만 계산해서 <code>ObjString</code> 구조체의 <code>hash</code> 필드에 저장해둡니다. 이후 해시 테이블에서 사용할 때마다 재계산할 필요가 없습니다.</p>
<h2 id="string-interning">String Interning</h2>
<blockquote>
<p>같은 문자열은 메모리에 하나만 존재하게 합니다.</p>
</blockquote>
<p>이번 챕터에서 가장 인상적이었던 부분입니다.</p>
<p>현재 구현에서 <code>&quot;hello&quot;</code>가 코드에 두 번 나오면 <code>ObjString</code>이 두 개 생깁니다. 주소가 다르니까 <code>==</code>(포인터 비교)로는 같다고 판단할 수 없습니다.</p>
<p><code>String interning</code>은 VM에 전역 문자열 테이블(<code>vm.strings</code>)을 두고, 새 문자열을 만들 때마다 이미 같은 문자열이 있는지 확인합니다.</p>
<pre><code class="language-c">ObjString* copyString(const char* chars, int length) {
    uint32_t hash = hashString(chars, length);

    // 이미 같은 문자열이 있으면 기존 객체를 반환
    ObjString* interned = tableFindString(&amp;vm.strings, chars, length, hash);
    if (interned != NULL) return interned;

    // 없으면 새로 만들고 테이블에 등록
    char* heapChars = ALLOCATE(char, length + 1);
    memcpy(heapChars, chars, length);
    heapChars[length] = &#39;\0&#39;;
    return allocateString(heapChars, length, hash);
}</code></pre>
<p>이렇게 하면 같은 내용의 문자열은 항상 같은 포인터를 가지게 됩니다. 결과적으로 문자열 비교가 <code>memcmp</code> <code>O(n)</code>에서 포인터 비교 <code>O(1)</code>로 바뀝니다.</p>
<pre><code class="language-c">// interning 전: 문자열 내용을 하나씩 비교
case VAL_OBJ: {
    ObjString* aString = AS_STRING(a);
    ObjString* bString = AS_STRING(b);
    return aString-&gt;length == bString-&gt;length &amp;&amp;
           memcmp(aString-&gt;chars, bString-&gt;chars, aString-&gt;length) == 0;
}</code></pre>
<pre><code class="language-c">// interning 후: 포인터 비교 한 번
case VAL_OBJ: return AS_OBJ(a) == AS_OBJ(b);</code></pre>
<p>여기서 <code>tableFindString()</code>이 <code>findEntry()</code>와 다른 점이 있습니다. <code>findEntry()</code>는 포인터 비교(<code>entry-&gt;key == key</code>)를 하지만, <code>tableFindString()</code>은 <code>length</code> → <code>hash</code> → <code>memcmp</code> 순서로 문자열 내용을 비교합니다. interning이 완료되기 전에 호출되는 함수이므로, 같은 내용의 문자열이 다른 객체로 존재할 수 있기 때문입니다. 비용이 낮은 비교부터 해서 불일치를 빨리 걸러내는 설계입니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/b134d504-f57d-46c0-a751-288f298c04ba/image.png" alt=""></p>
<p>위 이미지는 <code>&quot;hello&quot; + &quot; &quot; + &quot;hello&quot;</code> 를 입력한 상태입니다. 두번째 <code>&quot;hello&quot;</code>를 연산할 때 <code>tableFindString()</code> 함수에서 테이블에 존재하는 <code>&quot;hello&quot;</code>문자열을 찾고 이를 반환하는 부분을 보여줍니다.</p>
<h2 id="마치며">마치며</h2>
<p>해시 테이블은 자주 등장하는 자료구조 중 하나입니다. 하지만 &quot;<code>O(1)</code>로 찾을 수 있다&quot;는 결론만 알고, 그 안에서 충돌 처리, <code>load factor</code> 관리, 캐시 효율을 위한 설계 선택이 어떻게 이루어지는지는 직접 구현해보지 않으면 알기 어렵습니다.</p>
<p>특히 <code>string interning</code>은 이번 챕터에서 가장 큰 수확이었습니다. 평소 언리얼 엔진의 <code>FName</code>이 왜 빠른지 개념적으로만 이해하고 있었는데, 직접 해시 테이블을 만들고 그 위에 interning을 구현해보니 &quot;같은 문자열 → 같은 포인터 → 비교가 <code>O(1)</code>&quot;이라는 흐름이 구체적으로 와닿았습니다. <code>FName</code>은 모든 이름 문자열을 글로벌 테이블에 interning해서, <code>FName(&quot;Weapon&quot;) == FName(&quot;Weapon&quot;)</code>이 문자열 비교가 아닌 정수 비교로 동작합니다. 게임처럼 매 프레임 이름을 비교해야 하는 상황에서 큰 성능 차이를 만드는 기법이라는 것을 직접 구현해보면서 체감할 수 있었습니다.</p>
<p>언리얼의 <code>TMap</code>이 <code>open addressing</code>을 사용하는 이유도, 캐시 효율이라는 맥락에서 이해할 수 있게 되었습니다.</p>
<p><code>open addressing</code>에서 &quot;밀려난 엔트리를 어떻게 다시 찾는가?&quot;라는 의문이 있었는데, 답은 &quot;기록하지 않는다 — 삽입 때와 같은 순서로 탐색하면 보장된다&quot;였습니다. 이 단순하면서도 우아한 원리가 <code>tombstone</code>이라는 장치와 맞물려 완성되는 과정이 인상적이었습니다.</p>
<hr>
<p>이미지 출처 및 참고: <a href="https://craftinginterpreters.com/hash-tables.html">Crafting Interpreters - Chapter 20: Hash Tables</a>
GitHub: <a href="https://github.com/Will-Big/clox">Will-Big/clox</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C 언어로 Lox 인터프리터 만들기 7]]></title>
            <link>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-7</link>
            <guid>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-7</guid>
            <pubDate>Mon, 23 Mar 2026 04:57:13 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>Ch18까지는 숫자, bool, nil 같이 <strong>크기가 고정된 값</strong>만 다뤘다.
이번 챕터에서는 <strong>가변 길이 데이터인 문자열</strong>을 추가한다.</p>
<p>문자열이 왜 특별한가? 지금까지의 Value는 이렇게 생겼다.</p>
<pre><code class="language-c">typedef union {
    bool boolean;
    double number;
} as;</code></pre>
<p><code>bool</code>은 1바이트, <code>double</code>은 8바이트 — 크기가 컴파일 타임에 고정이라 Value 안에 직접 담을 수 있었다.</p>
<p>하지만 문자열은 다르다.</p>
<pre><code>&quot;hi&quot;             → 2글자
&quot;hello, world!&quot;  → 14글자</code></pre><p>길이가 런타임에 결정되므로 Value 안에 직접 담을 수 없다.
그래서 <strong>Value에는 포인터만 넣고, 실제 데이터는 힙에 할당</strong>한다.</p>
<hr>
<h2 id="obj--힙-객체의-베이스-타입">Obj — 힙 객체의 베이스 타입</h2>
<p>문자열 외에도 앞으로 함수, 클래스 등 다양한 힙 객체가 생긴다.
이것들을 하나의 Value로 다루기 위해 공통 베이스 타입 <code>Obj</code>를 도입한다.</p>
<pre><code class="language-c">// object.h
typedef enum {
    OBJ_STRING,
} ObjType;

struct Obj {
    ObjType type;
    struct Obj* next;
};</code></pre>
<p><code>type</code> 필드는 이 포인터가 실제로 어떤 객체인지 나타내는 태그다.
<code>next</code>는 VM이 할당한 모든 객체를 링크드 리스트로 연결하기 위한 포인터다. 나중에 GC(가비지 컬렉터)가 이 리스트를 순회해 메모리를 해제한다.</p>
<pre><code>vm.objects → [ObjString &quot;world&quot;] → [ObjString &quot;hello&quot;] → NULL</code></pre><hr>
<h2 id="objstring--c의-구조체-상속">ObjString — C의 구조체 상속</h2>
<p>C에는 상속이 없지만 <strong>구조체 레이아웃</strong>을 이용해 흉내낼 수 있다.</p>
<blockquote>
<p>C 스펙 보장: 구조체의 첫 번째 필드는 구조체 자체와 메모리 주소가 동일하다.</p>
</blockquote>
<pre><code class="language-c">struct ObjString {
    Obj obj;       // 반드시 첫 번째 필드
    int length;
    char* chars;
};</code></pre>
<p>메모리 레이아웃:</p>
<pre><code>ObjString 주소: 0x3000
┌──────────────────────────────┐
│ Obj obj   (ObjType type)     │ ← 0x3000  (Obj*와 주소 동일)
│           (Obj* next)        │
│ int length                   │ ← 0x3008
│ char* chars                  │ ← 0x300C
└──────────────────────────────┘</code></pre><p><code>ObjString*</code>를 <code>Obj*</code>로 캐스팅해도 같은 주소를 가리키므로 안전하다.
책에서는 이 패턴을 <strong>&quot;struct inheritance&quot;</strong> 라고 부른다.</p>
<hr>
<h2 id="value-시스템-확장">Value 시스템 확장</h2>
<pre><code class="language-c">// value.h
typedef struct Obj Obj;           // 전방 선언
typedef struct ObjString ObjString;

typedef enum {
    VAL_BOOL,
    VAL_NIL,
    VAL_NUMBER,
    VAL_OBJ,    // ← 추가
} ValueType;

typedef struct {
    ValueType type;
    union {
        bool boolean;
        double number;
        Obj* obj;    // ← 추가
    } as;
} Value;</code></pre>
<p>접근 매크로:</p>
<pre><code class="language-c">#define IS_OBJ(value)     ((value).type == VAL_OBJ)
#define AS_OBJ(value)     ((value).as.obj)
#define OBJ_VAL(value)    ((Value){VAL_OBJ, {.obj = (Obj*)(value)}})</code></pre>
<hr>
<h2 id="편의-매크로">편의 매크로</h2>
<p><code>object.h</code>에 문자열 전용 매크로를 추가한다.</p>
<pre><code class="language-c">#define OBJ_TYPE(value)     (AS_OBJ(value)-&gt;type)
#define IS_STRING(value)    isObjType(value, OBJ_STRING)
#define AS_STRING(value)    ((ObjString*)AS_OBJ(value))
#define AS_CSTRING(value)   (((ObjString*)AS_OBJ(value))-&gt;chars)</code></pre>
<p><code>IS_STRING</code>을 매크로 대신 <code>inline</code> 함수로 구현한 이유:</p>
<pre><code class="language-c">// 매크로였다면 pop()이 두 번 호출됨 💥
#define IS_STRING(v)  (IS_OBJ(v) &amp;&amp; AS_OBJ(v)-&gt;type == OBJ_STRING)
IS_STRING(pop())

// inline 함수는 인자를 한 번만 평가
static inline bool isObjType(Value value, ObjType type) {
    return IS_OBJ(value) &amp;&amp; AS_OBJ(value)-&gt;type == type;
}</code></pre>
<hr>
<h2 id="문자열-할당--copystring-vs-takestring">문자열 할당 — copyString vs takeString</h2>
<p>문자열을 힙에 올리는 함수가 두 가지다.</p>
<pre><code class="language-c">// 소스코드 리터럴용: 새 힙 메모리에 복사
ObjString* copyString(const char* chars, int length) {
    char* heapChars = ALLOCATE(char, length + 1);
    memcpy(heapChars, chars, length);
    heapChars[length] = &#39;\0&#39;;
    return allocateString(heapChars, length);
}

// 문자열 연결 결과용: 이미 힙에 있으니 소유권만 인수
ObjString* takeString(char* chars, int length) {
    return allocateString(chars, length);
}</code></pre>
<table>
<thead>
<tr>
<th></th>
<th>copyString</th>
<th>takeString</th>
</tr>
</thead>
<tbody><tr>
<td><strong>사용처</strong></td>
<td>소스코드 리터럴 <code>&quot;hello&quot;</code></td>
<td>문자열 연결 <code>&quot;a&quot; + &quot;b&quot;</code> 결과</td>
</tr>
<tr>
<td><strong>메모리</strong></td>
<td>새로 할당 후 복사</td>
<td>기존 버퍼 소유권 인수</td>
</tr>
<tr>
<td><strong>이유</strong></td>
<td>원본 소스 버퍼는 나중에 사라질 수 있음</td>
<td>concatenate()가 이미 새 버퍼를 만들었음</td>
</tr>
</tbody></table>
<p>할당 후 포인터 구조:</p>
<pre><code>Value { type: VAL_OBJ, as.obj: 0x3000 }
                               ↓
                   ObjString { chars: 0x2000 }
                               ↓
                   char[] &quot;hello\0&quot;</code></pre><hr>
<h2 id="컴파일러--문자열-토큰-파싱">컴파일러 — 문자열 토큰 파싱</h2>
<p>Pratt Parser 규칙 테이블에 문자열 핸들러를 등록한다.</p>
<pre><code class="language-c">// compiler.c
[TOKEN_STRING] = {string, NULL, PREC_NONE},

static void string() {
    emitConstant(OBJ_VAL(copyString(parser.previous.start + 1,
                                    parser.previous.length - 2)));
}</code></pre>
<p>Scanner가 <code>&quot;hello&quot;</code>를 토큰으로 만들면 따옴표가 포함된다.</p>
<pre><code>Token { start: → &quot;hello&quot;, length: 7 }</code></pre><ul>
<li><code>start + 1</code> → 여는 <code>&quot;</code> 건너뜀</li>
<li><code>length - 2</code> → 양쪽 <code>&quot;</code> 2글자 제외</li>
</ul>
<hr>
<h2 id="vm--문자열-연결">VM — 문자열 연결</h2>
<p><code>OP_ADD</code>를 타입에 따라 분기하도록 수정한다.</p>
<pre><code class="language-c">case OP_ADD: {
    if (IS_STRING(peek(0)) &amp;&amp; IS_STRING(peek(1))) {
        concatenate();
    } else if (IS_NUMBER(peek(0)) &amp;&amp; IS_NUMBER(peek(1))) {
        double b = AS_NUMBER(pop());
        double a = AS_NUMBER(pop());
        push(NUMBER_VAL(a + b));
    } else {
        runtimeError(&quot;Operands must be two numbers or two strings.&quot;);
        return INTERPRET_RUNTIME_ERROR;
    }
    break;
}

static void concatenate() {
    ObjString* b = AS_STRING(pop());
    ObjString* a = AS_STRING(pop());

    int length = a-&gt;length + b-&gt;length;
    char* chars = ALLOCATE(char, length + 1);
    memcpy(chars, a-&gt;chars, a-&gt;length);
    memcpy(chars + a-&gt;length, b-&gt;chars, b-&gt;length);
    chars[length] = &#39;\0&#39;;

    ObjString* result = takeString(chars, length);
    push(OBJ_VAL(result));
}</code></pre>
<hr>
<h2 id="메모리-해제">메모리 해제</h2>
<p>프로그램 종료 시 링크드 리스트를 순회하며 모든 객체를 해제한다.</p>
<pre><code class="language-c">void freeObjects() {
    Obj* object = vm.objects;
    while (object != NULL) {
        Obj* next = object-&gt;next;  // 먼저 저장
        freeObject(object);        // 그 다음 해제
        object = next;
    }
}

static void freeObject(Obj* object) {
    switch (object-&gt;type) {
        case OBJ_STRING: {
            ObjString* string = (ObjString*)object;
            FREE_ARRAY(char, string-&gt;chars, string-&gt;length + 1);
            FREE(ObjString, object);
            break;
        }
    }
}</code></pre>
<p><code>ObjString</code>은 <code>chars</code> 버퍼와 <code>ObjString</code> 자체, 두 번 해제한다.</p>
<hr>
<h2 id="실행-결과">실행 결과</h2>
<p><img src="https://velog.velcdn.com/images/will-big/post/430be2e8-8ccb-498a-bf44-72054283ae1b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/will-big/post/101b68a5-7c59-4181-9f9f-ca891992c561/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/will-big/post/636ca047-cc99-4ba8-8911-6e4379429029/image.png" alt=""></p>
<h2 id="마치며">마치며</h2>
<p>이번 포스트부터는 claude code를 활용하여 함께 코드를 이해하고 포스트를 작성하고 있다. clox의 규모가 커지며 한번에 알아보기 어려웠던 실행 흐름을 어느정도 잡아주고 코드가 기존과 비교하여 추가된 부분을 중점적으로 리뷰해주기 때문에 확실히 이해하기 쉬워졌다.</p>
<p>참고 자료: <a href="https://craftinginterpreters.com/strings.html">Cratring Interpreters - strings</a>
Github: <a href="https://github.com/Will-Big/clox">Will-big/clox</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] 행렬로 점과 직선 사이의 거리 구하기]]></title>
            <link>https://velog.io/@will-big/%ED%96%89%EB%A0%AC%EB%A1%9C-%EC%A0%90%EA%B3%BC-%EC%A7%81%EC%84%A0-%EC%82%AC%EC%9D%B4%EC%9D%98-%EA%B1%B0%EB%A6%AC-%EA%B5%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@will-big/%ED%96%89%EB%A0%AC%EB%A1%9C-%EC%A0%90%EA%B3%BC-%EC%A7%81%EC%84%A0-%EC%82%AC%EC%9D%B4%EC%9D%98-%EA%B1%B0%EB%A6%AC-%EA%B5%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 01 Feb 2026 07:12:49 GMT</pubDate>
            <description><![CDATA[<h2 id="글을-작성하게-된-계기">글을 작성하게 된 계기</h2>
<p>취업 준비를 시작하며 코딩 테스트를 치르게 되었습니다. 운 좋게 면접 기회도 있었는데, 당시 면접에서 &quot;원과 직선의 충돌 여부를 확인할 수 있는 방법&quot;을 물었습니다. 저는 원의 중심에서 직선까지의 거리를 구하면 된다고 답변했고, 면접관께서는 실제로 그 거리를 구하는 방법을 알고 있는지 다시 물으셨습니다.</p>
<p>컴퓨터가 대부분 계산해주다 보니 직접 계산하는 수학을 하지 않은 지 오래되었고, 공식도 거의 잊어버린 상태였습니다. 그래서 저는 &quot;공식의 존재를 알고 있기 때문에 검색을 통해 확인하여 충분히 구현 가능합니다&quot;라고 답변했습니다.</p>
<p>당시에는 &#39;그런 거 굳이 외울 필요 있나&#39;라며 속으로 대수롭지 않게 넘겼지만, 결과적으로 면접에서는 떨어졌습니다. 원인은 정확히 알 수 없지만, 이러한 대답 또한 감점 요인이 되지 않았을까 싶습니다.</p>
<p>하지만 결정적으로 이 글을 작성하게 된 계기는 코딩 테스트였습니다. 모 회사의 코딩 테스트에서 점과 직선 사이의 거리를 구하는 문제가 출제되었습니다. 막상 시험장에서 맞닥뜨리니 공식도 기억나지 않았고, 그것을 유도하는 과정조차 떠올리지 못했습니다. &#39;분명 아는 건데...&#39;라는 생각으로 머리가 하얘졌고, 남은 시간 동안 노력했지만 결국 풀이를 이끌어내지 못한 채 시험이 끝났습니다.</p>
<p>코딩 테스트가 끝나고 스스로 많이 아쉬웠고, 자책하기도 했습니다. 제가 생각하기에도 기초적인 수학 지식인데, 이런 것조차 의존한다면 과연 &#39;회사 입장에서 뽑고 싶은 인재일까&#39;라는 생각이 들었습니다.</p>
<p>하지만 저는 공식을 무작정 외우는 것을 좋아하지 않습니다. 수학을 열심히 하던 고등학교 시절부터 지금까지, 공식만을 외우는 과정은 제가 그것을 진정으로 익혔다는 느낌을 주지 못했습니다.</p>
<p>그래서 이번 경험을 계기로, 공식보다는 <strong>점과 직선 사이의 거리를 구하는 과정</strong>에 대해 생각해보기로 했습니다.</p>
<hr>
<h2 id="연립방정식-유도-과정">연립방정식 유도 과정</h2>
<p>아래 그림과 같이 점 $P(x_0, y_0)$이 주어지고, 직선의 방정식 $y = ax + b$를 알고 있다고 가정합니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/0d6dbebe-7f8c-4cd2-8b5f-d2f54e30b76c/image.png" alt=""></p>
<p>직선의 기울기를 알고 있기 때문에, 점 $(x_0, y_0)$에서 직선까지 수선의 발을 내릴 때 생기는 직선의 기울기 또한 구할 수 있습니다. 두 직선은 직교하므로 두 직선의 기울기의 곱은 반드시 $-1$이 됩니다. 따라서 수선의 기울기는 $-\frac{1}{a}$입니다.</p>
<p>수선의 방정식은 다음과 같이 표현됩니다:</p>
<p>$$y - y_0 = -\frac{1}{a}(x - x_0)$$</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/73f12ec3-0d5f-49e0-8d1e-423d0517872f/image.png" alt=""></p>
<p>이 과정을 통해 두 개의 방정식을 얻었습니다:</p>
<ol>
<li>$y = ax + b$ (주어진 직선)</li>
<li>$y - y_0 = -\frac{1}{a}(x - x_0)$ (수선)</li>
</ol>
<p>이제 이 연립방정식의 해를 구하면 두 직선이 교차하는 지점, 즉 수선의 발을 구할 수 있습니다. 교차점을 구한다면 점과 점 사이의 거리를 통해 점과 직선 사이의 거리를 얻게 됩니다.</p>
<hr>
<h2 id="컴퓨터적-관점에서의-문제">컴퓨터적 관점에서의 문제</h2>
<p>사람 기준으로 생각하면 방정식이 두 개인 연립방정식은 해를 구하기 상당히 쉬운 편입니다. 하지만 컴퓨터적인 측면에서 이를 다시 생각해보면 다음과 같은 문제가 있었습니다:</p>
<ol>
<li><strong>컴퓨터는 방정식을 행렬로 이해합니다.</strong> 사람이 풀이하는 방식과 다르게 접근해야 합니다.</li>
<li><strong>범용성을 고려해야 합니다.</strong> 현재는 방정식과 변수가 각각 2개이지만, 개수가 증감할 수 있다는 가정하에 범용적으로 작성해야 합니다.</li>
</ol>
<p>결국 행렬로 접근해야 한다는 것을 깨닫고, 컴퓨터에서 연립방정식을 계산할 수 있는 방법을 찾아보기로 했습니다.</p>
<hr>
<h2 id="컴퓨터에서-연립방정식의-계산">컴퓨터에서 연립방정식의 계산</h2>
<p>연립방정식을 컴퓨터로 푸는 방법은 크게 세 가지가 있습니다: <strong>가우스 소거법</strong>, <strong>역행렬 곱</strong>, <strong>LU 분해</strong>. 각 방법의 특성을 구현 관점에서 비교하면 다음과 같습니다.</p>
<h4 id="1-가우스-소거법-gaussian-elimination">1. 가우스 소거법 (Gaussian Elimination)</h4>
<ul>
<li><strong>직접 구현 난이도</strong>: 중간</li>
<li><strong>수치 안정성</strong>: 좋음 (피벗팅 사용 시)</li>
<li><strong>계산 복잡도</strong>: $O(n^3)$</li>
</ul>
<h4 id="2-역행렬-곱-inverse-matrix-multiplication">2. 역행렬 곱 (Inverse Matrix Multiplication)</h4>
<ul>
<li><strong>직접 구현 난이도</strong>: 쉬움</li>
<li><strong>수치 안정성</strong>: 나쁨</li>
<li><strong>계산 복잡도</strong>: $O(n^3)$ + 추가 곱셈</li>
<li><strong>문제점</strong>:<ul>
<li>부동소수점 오차 누적</li>
</ul>
</li>
</ul>
<h4 id="3-lu-분해-lu-decomposition">3. LU 분해 (LU Decomposition)</h4>
<ul>
<li><strong>직접 구현 난이도</strong>: 중상</li>
<li><strong>수치 안정성</strong>: 좋음 (피벗팅 사용 시)</li>
<li><strong>계산 복잡도</strong>: $O(n^3)$</li>
<li><strong>장점</strong>: 같은 행렬 $A$로 여러 $b$ 벡터를 풀 때 효율적</li>
</ul>
<p>여기서는 <strong>구현이 비교적 쉽고 부동소수점의 영향을 덜 받는 가우스 소거법</strong>으로 연립방정식을 해결해보겠습니다.</p>
<hr>
<h2 id="가우스-소거법이란">가우스 소거법이란?</h2>
<p>연립방정식을 <strong>상삼각행렬</strong>로 만든 뒤, 아래에서 위로 대입하여 해를 구하는 방법입니다.</p>
<ul>
<li>상삼각행렬: 주대각선을 기준으로 대각항의 위쪽 항들의 값이 모두 <code>0</code>인 경우
<img src="https://velog.velcdn.com/images/will-big/post/6205cad9-afd1-4f67-b970-183f5eef6dc5/image.png" alt=""></li>
</ul>
<h3 id="예시-2개-변수의-연립방정식">예시: 2개 변수의 연립방정식</h3>
<p>$$
\begin{cases}
2x + 3y = 8 \
x - y = -1
\end{cases}
$$</p>
<h4 id="step-1-확장-행렬로-표현">Step 1: 확장 행렬로 표현</h4>
<p>$$
\left[\begin{array}{cc|c}
2 &amp; 3 &amp; 8 \
1 &amp; -1 &amp; -1
\end{array}\right]
$$</p>
<h4 id="step-2-전방-소거-forward-elimination">Step 2: 전방 소거 (Forward Elimination)</h4>
<p><strong>목표</strong>: 왼쪽 아래를 0으로 만들기
첫 번째 행을 이용해 두 번째 행의 $x$를 제거합니다:</p>
<p>$$
R_2 \leftarrow R_2 - \frac{1}{2} R_1
$$</p>
<p>$$
\left[\begin{array}{cc|c}
2 &amp; 3 &amp; 8 \
0 &amp; -2.5 &amp; -5
\end{array}\right]
$$</p>
<h4 id="step-3-후방-대입-back-substitution">Step 3: 후방 대입 (Back Substitution)</h4>
<p>아래부터 위로 올라가며 해를 구합니다:
<strong>2번째 식</strong>: 
$$
-2.5y = -5 \quad \Rightarrow \quad y = 2
$$</p>
<p><strong>1번째 식</strong>:
$$
2x + 3(2) = 8 \quad \Rightarrow \quad 2x = 2 \quad \Rightarrow \quad x = 1
$$</p>
<p><strong>답</strong>: $x = 1, , y = 2$</p>
<p>복잡해 보이지만 결국 <strong>&quot;한 식을 다른 식에서 빼서 변수를 하나씩 제거&quot;</strong>하는 방법입니다.</p>
<hr>
<h2 id="가우스-소거법-c-구현-및-설명">가우스 소거법 C++ 구현 및 설명</h2>
<h3 id="구현">구현</h3>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;cmath&gt;

using namespace std;

vector&lt;double&gt; gaussianElimination(vector&lt;vector&lt;double&gt;&gt; A, vector&lt;double&gt; b)
{
    int n = A.size();

    // AX = b -&gt; AX|b 형태로 변환
    for (int i = 0; i &lt; n; i++)
    {
        A[i].push_back(b[i]);
    }

    // 전방 소거
    for (int i = 0; i &lt; n; i++)
    {
        // 피벗 설정
        int maxRow = i;
        for (int j = i + 1; j &lt; n; j++)
        {
            if (abs(A[j][i]) &gt; abs(A[maxRow][i]))
                maxRow = j;
        }

        // 행 교환
        swap(A[i], A[maxRow]);

        // 아래 행들을 0으로 만들기
        for (int j = i + 1; j &lt; n; j++)
        {
            double factor = A[j][i] / A[i][i];
            for (int k = i; k &lt;= n; k++)
            {
                A[j][k] -= factor * A[i][k];
            }
        }
    }

    // 후방 대입
    vector&lt;double&gt; x(n);
    for (int i = n - 1; i &gt;= 0; i--)
    {
        x[i] = A[i][n];
        for (int j = i + 1; j &lt; n; j++)
        {
            x[i] -= A[i][j] * x[j];
        }
        x[i] /= A[i][i];
    }

    return x;
}

int main()
{
    vector&lt;vector&lt;double&gt;&gt; A = { {2, 3}, {1, -1} };
    vector&lt;double&gt; b = { 8, -1 };

    vector&lt;double&gt; solution = gaussianElimination(A, b);

    cout &lt;&lt; &quot;해: x = &quot; &lt;&lt; solution[0] &lt;&lt; &quot;, y = &quot; &lt;&lt; solution[1] &lt;&lt; endl;

    return 0;
}</code></pre>
<hr>
<h3 id="설명">설명</h3>
<h4 id="1-확장-행렬-생성">1. 확장 행렬 생성</h4>
<pre><code class="language-cpp">for (int i = 0; i &lt; n; i++)
{
    A[i].push_back(b[i]);
}</code></pre>
<p>연립방정식 $AX = b$를 확장 행렬 $[A|b]$ 형태로 변환합니다. 각 행의 끝에 $b$ 값을 추가하여 행렬과 우변을 함께 처리할 수 있게 만듭니다.</p>
<p>예시: 
$$
\begin{bmatrix} 2 &amp; 3 \ 1 &amp; -1 \end{bmatrix}, 
\begin{bmatrix} 8 \ -1 \end{bmatrix}
\quad \Rightarrow \quad
\left[\begin{array}{cc|c} 2 &amp; 3 &amp; 8 \ 1 &amp; -1 &amp; -1 \end{array}\right]
$$</p>
<h4 id="2-전방-소거-forward-elimination">2. 전방 소거 (Forward Elimination)</h4>
<h4 id="2-1-피벗-선택">2-1. 피벗 선택</h4>
<pre><code class="language-cpp">int maxRow = i;
for (int j = i + 1; j &lt; n; j++)
{
    if (abs(A[j][i]) &gt; abs(A[maxRow][i]))
        maxRow = j;
}
swap(A[i], A[maxRow]);</code></pre>
<p><strong>피벗을 정하는 이유</strong></p>
<ul>
<li>현재 열($i$번째)에서 <strong>절댓값이 가장 큰 원소</strong>를 피벗으로 선택합니다</li>
<li>0으로 나누는 오류를 방지하고 수치적 안정성을 높입니다</li>
<li>작은 값으로 나누면 부동소수점 오차가 크게 증폭되므로, 큰 값을 피벗으로 사용하여 오차를 최소화합니다</li>
</ul>
<p>예시: 
$$
\left[\begin{array}{cc|c} 2 &amp; 3 &amp; 8 \ 1 &amp; -1 &amp; -1 \end{array}\right]
\quad \Rightarrow \quad
\left[\begin{array}{cc|c} 2 &amp; 3 &amp; 8 \ 1 &amp; -1 &amp; -1 \end{array}\right]
$$
(이 경우 $|2| &gt; |1|$이므로 교환 불필요)</p>
<h4 id="2-2-하부-행-소거">2-2. 하부 행 소거</h4>
<pre><code class="language-cpp">for (int j = i + 1; j &lt; n; j++)
{
    double factor = A[j][i] / A[i][i];
    for (int k = i; k &lt;= n; k++)
    {
        A[j][k] -= factor * A[i][k];
    }
}</code></pre>
<p><strong>상삼각 행렬을 만드는 과정:</strong></p>
<ul>
<li><code>factor</code>: $i$번째 행을 몇 배 해서 빼야 $j$번째 행의 $i$번째 원소가 0이 되는지 계산</li>
<li>$i$번째 피벗 행 아래의 모든 행에서 $i$번째 열의 값을 0으로 만듭니다</li>
<li><code>k &lt;= n</code>인 이유: 확장된 우변 $b$ 값까지 함께 처리해야 하므로</li>
</ul>
<p>예시: 
$$
\text{factor} = \frac{A[1][0]}{A[0][0]} = \frac{1}{2} = 0.5
$$
$$
R_2 \leftarrow R_2 - 0.5 \times R_1
$$
$$
\left[\begin{array}{cc|c} 2 &amp; 3 &amp; 8 \ 1 &amp; -1 &amp; -1 \end{array}\right]
\quad \Rightarrow \quad
\left[\begin{array}{cc|c} 2 &amp; 3 &amp; 8 \ 0 &amp; -2.5 &amp; -5 \end{array}\right]
$$</p>
<h4 id="3-후방-대입-back-substitution">3. 후방 대입 (Back Substitution)</h4>
<pre><code class="language-cpp">vector&lt;double&gt; x(n);
for (int i = n - 1; i &gt;= 0; i--)
{
    x[i] = A[i][n];
    for (int j = i + 1; j &lt; n; j++)
    {
        x[i] -= A[i][j] * x[j];
    }
    x[i] /= A[i][i];
}</code></pre>
<p><strong>맨 아래 행부터 위로 올라가며 해를 구합니다:</strong></p>
<ol>
<li><code>x[i] = A[i][n]</code>: 우변 값으로 초기화</li>
<li><code>x[i] -= A[i][j] * x[j]</code>: 이미 구한 $x[j]$ 값들을 대입하여 뺍니다</li>
<li><code>x[i] /= A[i][i]</code>: 계수로 나누어 최종 해를 구합니다</li>
</ol>
<p>예시:
$$
\left[\begin{array}{cc|c} 2 &amp; 3 &amp; 8 \ 0 &amp; -2.5 &amp; -5 \end{array}\right]
$$</p>
<p><strong>$i = 1$ (두 번째 행):</strong>
$$
x[1] = \frac{-5}{-2.5} = 2
$$</p>
<p><strong>$i = 0$ (첫 번째 행):</strong>
$$
x[0] = \frac{8 - 3 \times x[1]}{2} = \frac{8 - 3 \times 2}{2} = \frac{2}{2} = 1
$$</p>
<p><strong>최종 해</strong>: $x = 1, y = 2$</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>연립방정식의 해를 구하는 과정은 복잡하고, 시간 복잡도 또한 $O(n^3)$입니다. 비록 $n$이 방정식의 개수이기 때문에 점과 직선의 거리를 구하는 경우($n=2$)에는 큰 부담이 되지 않지만, <strong>코딩 테스트 환경에서는 점과 직선 사이의 거리 공식을 직접 사용하는 것이 훨씬 효율적</strong>입니다.</p>
<p>오늘의 교훈: <strong>기초 수학을 잊지 말자!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C 언어로 Lox 인터프리터 만들기 6]]></title>
            <link>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-6</link>
            <guid>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-6</guid>
            <pubDate>Mon, 01 Dec 2025 05:46:36 GMT</pubDate>
            <description><![CDATA[<p>지난 시간까지는 <code>double</code> 타입의 숫자를 다룰 수 있는 기본적인 형태를 만들었습니다. 이번에는 <code>true</code>, <code>false</code>, <code>nil</code> 과 같은 숫자 외에 프로그래밍에 필요한 데이터 타입을 정의합니다.</p>
<p>이번 시간의 목표는 앞서 말한 데이터 타입들의 정의와 이를 이용한 비교 연산(<code>&lt;</code>, <code>&gt;</code>, <code>==</code>)과 논리 연산(<code>!</code>)을 수행할 수 있도록 확장하는 것입니다.</p>
<h2 id="value-구조체의-변경-union"><code>Value</code> 구조체의 변경: <code>union</code></h2>
<p>가장 근본적인 변화는 데이터를 담는 그릇인 <code>Value</code> 타입의 정의입니다.</p>
<pre><code class="language-c">// value.h - 이전
typedef double Value;</code></pre>
<p>기존에는 모든 값을 숫자로만 취급했기에 <code>double</code> 그 자체였습니다.</p>
<pre><code class="language-c">// value.h - 현재
typedef enum {
    VAL_BOOL,
    VAL_NIL,
    VAL_NUMBER,
} ValueType;

typedef struct {
    ValueType type; // 값의 종류를 나타내는 태그
    union {
        bool boolean;
        double number;
    } as; // 실제 데이터
} Value;</code></pre>
<p>이제는 값이 숫자인지, 불리언인지, 혹은 <code>nil</code>인지 구분해야 합니다. C 언어는 정적 타입 언어이므로, 동적 타입인 Lox 의 값을 담기 위해 <code>union</code>을 도입했습니다.</p>
<p><code>Value</code>는 타입을 외부에 전달하기 위한 <code>type</code>과 실제 값을 가지고 있는 <code>as</code>를 가지고 있습니다.</p>
<p>이를 안전하게 다루기 위해 값을 확인(<code>IS_BOOL</code>)하고, C 언어의 기본 타입으로 변환(<code>AS_BOOL</code>)하고, 다시 Lox 값으로 포장(<code>BOOL_VAL</code>)하는 매크로들도 대거 추가되었습니다. 아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-c">// vm.c
static bool isFalsey(Value value) {
    return IS_NIL(value) || (IS_BOOL(value) &amp;&amp; !AS_BOOL(value));
}</code></pre>
<h3 id="값-동일-비교">*값 동일 비교</h3>
<p>메모리에 적힌 두 값이 다른지 가장 빠르게 비교하는 방법은 무엇일까요? 아마도 답은 메모리를 직접 비교(<code>memcmp</code>)하는 것일 겁니다. 비교 함수를 호출할 필요도 없이 말입니다. 하지만 clox에서는 불가능했습니다. 아래는 clox의 값이 같은지 검사하는 코드입니다.</p>
<pre><code class="language-c">// value.c
bool  valuesEqual ( Value  a , Value  b ) {
   if ( a . type != b . type ) return  false ;
   switch ( a . type ) {
     case  VAL_BOOL :    return  AS_BOOL ( a ) == AS_BOOL ( b );
     case  VAL_NIL :     return  true ;
     case  VAL_NUMBER : return  AS_NUMBER ( a ) == AS_NUMBER ( b );
     default :          return  false ; // 도달할 수 없음. 
  } 
}</code></pre>
<p>이렇게 했던 이유는 아래 그림을 보며 설명하겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/ee60ecb0-0540-4eba-b727-90b94f0de3a0/image.png" alt="">이미지 출처: <a href="https://craftinginterpreters.com/types-of-values.html">Crafting Interpreters</a></p>
<p>C 언어에서는 사용하지 않는 비트에 대해서 동일한 값이 들어있음을 보장하지 않습니다. 그리고 <code>Value</code>의 특성상 패딩으로 인해 빈 메모리 공간은 존재합니다. 이 패딩 안의 값은 동일한 타입이라도 다른 값이 들어있을 수 있어 <code>memcmp</code>를 사용할 수 없었습니다. 저자는 수 많은 디버깅을 통해 이 결과를 알아냈지만 우리는 그 결과를 쉽게 받아들일 수 있음에 큰 감사를 보냅니다.</p>
<h2 id="명령어opcode와-컴파일러의-확장">명령어(<code>OpCode</code>)와 컴파일러의 확장</h2>
<p>새로운 타입이 생겼으니, 이를 처리할 새로운 명령어도 필요합니다. 산술 연산만 있던 <code>Chunk</code>에 논리 및 비교 연산이 추가되었습니다.</p>
<pre><code class="language-c">// chunk.h
typedef enum
{
    OP_CONSTANT,
    OP_NIL,
    OP_TRUE,
    OP_FALSE,
    OP_EQUAL,
    OP_GREATER,
    OP_LESS,
    ...
    OP_NOT,
    OP_NEGATE,
    OP_RETURN,
} OpCode;</code></pre>
<ul>
<li>리터럴 생성: <code>OP_NIL</code>, <code>OP_TRUE</code>, <code>OP_FALSE</code></li>
<li>비교 연산: <code>OP_EQUAL</code> (<code>==</code>), <code>OP_GREATER</code> (<code>&gt;</code>), <code>OP_LESS</code> (<code>&lt;</code>)</li>
<li>논리 부정: <code>OP_NOT</code> (<code>!</code>)</li>
</ul>
<pre><code class="language-c">// compiler.c
static void literal()
{
    switch (parser.previous.type) {
        case TOKEN_FALSE: emitByte(OP_FALSE); break;
        case TOKEN_NIL: emitByte(OP_NIL); break;
        case TOKEN_TRUE: emitByte(OP_TRUE); break;
        default: return; // Unreachable.
    }
}</code></pre>
<p><code>number()</code>가 숫자를 상수 테이블에 넣었다면, <code>literal()</code>은 고정된 값(<code>true</code>, <code>false</code>, <code>nil</code>)에 대응하는 특정 명령어를 바로 내보냅니다. 이 함수를 통해 상수 테이블을 쓰지 않고 직접 스택에 푸시합니다.</p>
<p>비교 연산(<code>&gt;</code>, <code>&lt;</code>)은 결과로 <code>VAL_BOOL</code> 타입의 값을 스택에 푸시합니다.</p>
<h3 id="파싱-규칙rules-업데이트">파싱 규칙(<code>rules</code>) 업데이트</h3>
<p>프랫 파서의 <code>rules</code> 테이블에 새로운 토큰들을 등록했습니다. 이제 <code>!</code>는 <code>unary</code>로, 비교 연산자들은 <code>binary</code>로 연결되어 우선순위에 따라 파싱됩니다.</p>
<pre><code class="language-c">// compiler.c
ParseRule rules[] =
{
    [TOKEN_BANG]          = {unary,    NULL,   PREC_NONE},        // !
    [TOKEN_BANG_EQUAL]    = {NULL,     binary, PREC_EQUALITY},    // !=
    [TOKEN_EQUAL]         = {NULL,     NULL,   PREC_NONE},
    [TOKEN_EQUAL_EQUAL]   = {NULL,     binary, PREC_EQUALITY},
    [TOKEN_GREATER]       = {NULL,     binary, PREC_COMPARISON},
    [TOKEN_GREATER_EQUAL] = {NULL,     binary, PREC_COMPARISON},
    [TOKEN_LESS]          = {NULL,     binary, PREC_COMPARISON},
    [TOKEN_LESS_EQUAL]    = {NULL,     binary, 
    ...
    [TOKEN_EOF]           = {NULL,     NULL,   PREC_NONE},
};</code></pre>
<h2 id="vm-타입-검사와-런타임-에러">VM: 타입 검사와 런타임 에러</h2>
<p>가장 큰 변화는 가상 머신(<code>vm.c</code>)에 있습니다. 이전에는 무조건 <code>pop()</code>해서 더하거나 빼면 됐지만, 이제는 &quot;숫자가 아닌 것에 뺄셈을 시도하는지&quot; 감시해야 합니다.</p>
<h3 id="타입-안전성-확보">타입 안전성 확보</h3>
<p><code>BINARY_OP</code> 매크로가 단순히 연산만 하는 것에서, 피연산자의 타입을 검사하는 로직으로 변경되었습니다.</p>
<pre><code class="language-c">// vm.c
#define BINARY_OP(valueType, op) \
    do { \
        if (!IS_NUMBER(peek(0)) || !IS_NUMBER(peek(1))) { \
            runtimeError(&quot;Operands must be numbers.&quot;); \
            return INTERPRET_RUNTIME_ERROR; \
        } \
        // ... 연산 수행 
    } while (false) \</code></pre>
<p>이제 <code>false + 5</code> 같은 코드를 실행하면 <code>VM</code>이 <code>Operands must be numbers.</code> 라는 런타임 에러를 뱉고 안전하게 종료됩니다.</p>
<h3 id="falsey의-개념-nil-과-false"><code>Falsey</code>의 개념 (<code>nil</code> 과 <code>false</code>)</h3>
<p>Lox 언어에서 &quot;거짓&quot;으로 취급되는 것은 <code>nil</code>과 <code>false</code> 두 가지뿐입니다. 이를 처리하기 위한 앞서 <code>Value</code> 구조체 설명에서 나왔던 <code>isFalsey</code> 함수가 추가되었고, <code>OP_NOT</code> (<code>!</code>) 연산에서 이를 활용합니다.</p>
<h2 id="실행-결과">실행 결과</h2>
<p>이제 clox는 다음과 같은 복합적인 식을 이해하고 실행할 수 있습니다.</p>
<pre><code class="language-cpp">&gt; !(5 - 4 &gt; 1)
true</code></pre>
<ol>
<li><p><code>5 - 4</code>를 계산하여 <code>1</code>을 얻습니다.</p>
</li>
<li><p><code>1 &gt; 1</code>을 비교하여 <code>false</code>를 얻습니다.</p>
</li>
<li><p><code>!false</code>를 수행하여 최종적으로 <code>true</code>가 출력됩니다.</p>
</li>
</ol>
<h2 id="마치며">마치며</h2>
<p>이번 시간은 지난 포스팅보다 쉬웠습니다. 그리고 좀 더 &#39;굴러가는&#39; 듯한 느낌을 주는 과정이었습니다. 하지만 모든 과정이 지난 포스팅의 <strong>&#39;재귀 함수를 통해 인터프리터가 작동하는 원리&#39;</strong> 를 이해하는 것을 기조로 하고 있습니다.</p>
<p>재미있던 점은, C 언어의 특성으로 인해 <code>memcmp</code>로 빠르게 가능할 것이라 여겼던 동일 비교 연산이 그렇지 않았다는 것입니다. 패딩의 여백 부분의 동일함을 보장되지 않아 어쩔 수 없이 <code>AS_BOOL</code>, <code>AS_NUMBER</code> 와 같은 매크로 연산이 불가피했던 것은 몰랐던 내용이었습니다.</p>
<p>현재는 숫자와 연산자로 이루어진 식을 이해하고 결과를 도출하는 인터프리터를 만들었습니다. 다음 시간에는 문자열을 상수 컨테이너에 넣고, 정상적으로 해제하는 구조를 만들어보겠습니다.</p>
<p>참고 자료: <a href="https://craftinginterpreters.com/types-of-values.html">Cratring Interpreters - types of values</a>
Github: <a href="https://github.com/Will-Big/clox">Will-big/clox</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C 언어로 Lox 인터프리터 만들기 5]]></title>
            <link>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-5</link>
            <guid>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-5</guid>
            <pubDate>Mon, 10 Nov 2025 03:58:27 GMT</pubDate>
            <description><![CDATA[<p>이 챕터의 목표는 우리가 쓴 Lox 코드(예: <code>-1 + 2 * (3 / 4)</code>)를 컴퓨터가 알아들을 수 있는 저수준 명령어, 즉 <strong>바이트코드(bytecode)</strong>로 번역하는 &#39;컴파일러&#39;를 만드는 것입니다.</p>
<p>이전 챕터에서 만든 &#39;스캐너(Scanner)&#39;가 코드를 &#39;토큰(Token)&#39;이라는 단어 조각(예: <code>-</code>, <code>1</code>, <code>+</code>, <code>2</code>...)으로 자르는 역할이었다면, 이번 챕터의 &#39;컴파일러&#39;는 이 토큰들을 조합해서 실제 &#39;실행 가능한 명령어 묶음(Chunk)&#39;을 만듭니다.</p>
<p>하지만 <code>main.c</code>, <code>compiler.c</code>, <code>vm.c</code>, <code>scanner.c</code> 등 여러 파일에 흩어져 있는 코드들이 정확히 &#39;어떻게&#39; 상호작용해서 결과를 내는지 한눈에 파악하기 어렵습니다.</p>
<p>그래서 오늘은 사용자가 터미널에 <code>-(1 + 2)</code>를 입력하는 순간부터 최종 결과인 <code>-3</code>이 출력되기까지, 코드의 흐름을 한 단계씩 따라가 보려고 합니다.</p>
<p>이전과 달라진 코드가 많이 존재하지만 이를 하나하나 짚기보단 현 단계까지 완성된 코드를 기반으로 어떻게 컴파일러가 코드를 해석하고 결과를 도출하는지 확인해 보겠습니다.</p>
<h3 id="단일-패스-컴파일">단일 패스 컴파일</h3>
<p>일반적인 컴파일러 패스는 <strong>파싱을 통한 의미 파악</strong>과 이를 바탕으로 <strong>동일한 의미를 가지는 저수준 명령어 출력</strong>으로 나뉩니다. 하지만 이 챕터의 컴파일러는 코드를 읽는 동시에 바이트코드를 바로 생성하는 &#39;단일 패스(Single Pass)&#39; 방식을 사용합니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/3786da34-e46d-4990-991e-d0c63e0712b7/image.png" alt=""></p>
<p>이미지 출처: <a href="https://craftinginterpreters.com/compiling-expressions.html">Cratring Interpreters</a></p>
<p>저자의 말을 빌려 설명하자면, 코드를 보는 하나의 구멍이 존재하고 컴파일러는 그 구멍을 통해 즉각적으로 해석하며 바이트코드로 변환하는 것을 의미합니다. 이는 구문을 이해하기 위해 주변 맥락을 많이 필요로하지 않아야 한다는 것입니다.</p>
<p>이 방식은 C언어로 구현하기 더 간단하고 메모리도 적게 쓴다는 장점이 있습니다.</p>
<h3 id="흐름-분석">흐름 분석</h3>
<p><code>-(1+2)</code> 를 컴파일하는 과정을 통해 코드 흐름을 알아보겠습니다. 가장 먼저 파서가 첫번째 토큰을 가리키게 합니다. 파서는 현재 토큰(<code>cur</code>), 이전 토큰(<code>pre</code>)을 추적하며 필요에 따라 이전 토큰을 활용합니다.</p>
<p><code>Source</code>: 소스 코드
<code>Tokens</code>: 소스 코드를 기반으로 생성된 토큰
<code>Parser</code>: 파서가 가리키는 토큰
<code>Byte-c</code>: 파싱을 통해 생성된 바이트 코드
<code>Consts</code>: 상수 배열</p>
<pre><code class="language-c">Source: &quot;-(1+2)&quot;
Tokens:   -   (   1   +   2   )  &lt;EOF&gt;
Parser:   ^
          |
         cur

Byte-c: []
Consts: []</code></pre>
<p>표현식의 첫번째 토큰을 스캔합니다. 이는 현재 토큰을 적절한 값으로 초기화 하기 위함입니다.</p>
<pre><code class="language-c">Source: &quot;-(1+2)&quot;
Tokens:   -   (   1   +   2   )  &lt;EOF&gt;
Parser:   ^   ^
          |   |
         pre cur

Byte-c: []
Consts: []</code></pre>
<p>이후 본격적인 파싱이 시작됩니다. 모든 연산자 토큰은 우선 순위를 가집니다. 예를 들어 <code>=</code> &lt; <code>+</code>,<code>-</code> &lt; <code>*</code>,<code>/</code> 와 같습니다. 마지막에 결과값을 가져 오기 위해 우선 순위가 가장 낮은 <code>=</code> 을 표현식의 시작 연산자로 정합니다. 현재는 계산만 가능 하기 때문에 생략되어 있지만 소스 코드가 <code>print(-(1+2));</code> 와 같으며 그 결과를 출력하기 위한 과정이라고 생각하면 됩니다.</p>
<p>이후 다음 토큰을 추가로 스캔하여 현재 토큰과 이전 토큰을 갱신합니다. 토큰 갱신은 아래 함수에서 이루어집니다. 이제 <code>pre</code> 토큰부터 검사합니다.</p>
<p><code>pre</code> 토큰은 <code>-</code> 입니다. <code>-</code> 는 일반적으로는 이항 연산자로 활용되지만 소스 코드의 상황처럼 전위 연산자로 쓰일 수 있습니다. <code>pre</code> 토큰에서 유효한 토큰이 검출되었으니 처리합니다.</p>
<p>다시 처음에 <code>=</code> 을 표현식의 시작 연산자로 정했던 것처럼 <code>-</code> 이후의 표현식을 <strong>부분 표현식</strong>처럼 취급해서 <code>-</code> 연산자로 시작합니다. 시작하는 함수를 표현하면 아래와 같습니다. 여기서 <code>pre_token_processing()</code>, <code>cur_token_processing()</code> 이 내부적으로 다시 <code>start_parsing_with()</code> 을 호출하는 <strong>재귀 함수</strong>의 형태를 가지고 있습니다. </p>
<pre><code class="language-c">func start_parsing_with(&#39;=&#39;)
    move_parser()
    pre_token_processing()

    while(&#39;=&#39; &lt;= cur_token_precedence)
        move_parser()
        in_token_processing()
    end while
end func

// 함수명은 이해를 돕기 위해 현재 맥락에 직관적인 이름으로 대체하였습니다.
// 실제 함수명은 아래와 같습니다.
// start_parsing_with(): parsePrecedence()
// move_parser(): advance()
// pre_token_processing(): prefix()
// in_token_processing(): infix() 
// 자세한 사항은 깃헙을 참조 바랍니다.</code></pre>
<p>즉, <code>pre_token_processing(&#39;-&#39;)</code> 이 내부적으로 <code>start_parsing_with(&#39;-&#39;)</code> 를 호출한 것입니다. 각 함수는 모두 전역 변수로 파서를 공유하고 있어서 재귀적으로 진행하더라도 표현식을 끝까지 처리할 수 있게 됩니다.</p>
<p><code>pre_token_processing(&#39;-&#39;)</code>이 호출되면, 실제 코드에서는 <code>unary()</code> 함수가 실행됩니다. <code>unary</code>의 임무는 간단합니다.</p>
<ul>
<li><p>자신의 피연산자(<code>operand</code>)를 파싱한다. (<code>-</code> 뒤에 오는 대상)</p>
</li>
<li><p>피연산자 파싱이 끝나면, &quot;부호를 바꿔라&quot;라는 <code>OP_NEGATE</code> 명령어를 붙인다.</p>
</li>
</ul>
<p>피연산자를 파싱하기 위해, <code>unary</code>는 다시 한번 <code>start_parsing_with</code>을 <strong>재귀 호출</strong>합니다. 이때, 자신(<code>unary</code>)의 우선순위인 <code>PREC_UNARY(=&#39;-&#39;)</code>를 넘겨줍니다.</p>
<pre><code class="language-c">Source: &quot;-(1+2)&quot;
Tokens:   -   (   1   +   2   )  &lt;EOF&gt;
Parser:       ^   ^
                |   |
             pre cur

Call Stack: [ start_parsing_with(&#39;=&#39;) | start_parsing_with(&#39;-&#39;) ]

Byte-c: []
Consts: []</code></pre>
<p><code>start_parsing_with(&#39;-&#39;)</code> 가 시작되고, <code>move_parser()</code> 를 호출하여 다음 토큰을 봅니다. <code>pre -&gt; (</code>, <code>cur -&gt; 1</code>이 됩니다.</p>
<p>이제 <code>pre_token_processing(&#39;(&#39;)</code>이 실행될 차례입니다. 이는 <code>grouping()</code> 함수에 해당합니다.</p>
<p><code>grouping</code> 함수의 임무도 간단합니다.</p>
<ul>
<li><p>괄호 안의 표현식을 처음부터 다시 파싱한다.</p>
</li>
<li><p>파싱이 끝나면, <code>)</code>(닫는 괄호)가 오는지 확인한다.</p>
</li>
</ul>
<p>괄호 안의 <code>1+2</code>는 완전한 새 표현식이므로, <code>grouping</code>은 다시 <code>start_parsing_with(&#39;=&#39;)</code> 을 재귀 호출합니다.</p>
<pre><code class="language-c">Source: &quot;-(1+2)&quot;
Tokens:   -   (   1   +   2   )  &lt;EOF&gt;
Parser:           ^   ^
                  |   |
                 pre cur
Call Stack: [ start_parsing_with(&#39;=&#39;) | start_parsing_with(&#39;-&#39;) | start_parsing_with(&#39;=&#39;) ]

Byte-c: []
Consts: []</code></pre>
<p>이제 3단계의 재귀 호출이 쌓였습니다. 이제 맨 위 <code>start_parsing_with(&#39;=&#39;)</code>가 <code>move_parser()</code> 를 호출합니다. <code>pre -&gt; 1</code>, <code>cur -&gt; +</code>가 됩니다.</p>
<p><code>pre_token_processing(&#39;1&#39;)</code> 가 호출됩니다.
<code>pre_token_processing(&#39;1&#39;)</code> 함수는 <a href="https://github.com/Will-Big/clox/blob/cffbb8955843285a545655eed8a59f4be21ec086/compiler.c#L263">배열 정보</a>를 통해 숫자 토큰에 속하는 <a href="https://github.com/Will-Big/clox/blob/cffbb8955843285a545655eed8a59f4be21ec086/compiler.c#L218">전처리 함수</a>로 연결됩니다. 전처리 함수는 <code>number()</code> 함수에 해당합니다. 함수에서는 아래와 같은 작업을 수행합니다.</p>
<ul>
<li><p><code>&quot;1&quot;</code> 문자열을 숫자 <code>1.0</code>으로 변환합니다.</p>
</li>
<li><p><code>1.0</code>을 상수 배열(<code>Consts</code>)에 추가합니다. (<code>Consts[0] = 1.0</code>)</p>
</li>
<li><p>&quot;상수 배열 <code>0</code>번 인덱스의 값을 스택에 밀어 넣어라&quot;라는 <code>OP_CONSTANT</code> 명령어를 <code>Byte-c</code>에 추가합니다.</p>
</li>
</ul>
<pre><code class="language-c">Source: &quot;-(1+2)&quot;
Tokens:   -   (   1   +   2   )  &lt;EOF&gt;
Parser:               ^   ^
                      |   |
                     pre cur
Call Stack: [ start_parsing_with(&#39;=&#39;) | start_parsing_with(&#39;-&#39;) | start_parsing_with(&#39;=&#39;) ]
Byte-c: [OP_CONSTANT, 0]
Consts: [1.0]</code></pre>
<p><code>number()</code>가 리턴되고, <code>start_parsing_with(&#39;=&#39;)</code>의 <code>while</code> 루프로 돌아옵니다. <code>while(&#39;=&#39; &lt;= cur_token_precedence [+] )</code>를 검사합니다. <code>+</code> 연산자의 우선순위(<code>PREC_TERM</code>)는 <code>=</code> (<code>PREC_ASSIGNMENT</code>)보다 높으므로, 루프가 실행됩니다. (연산자 우선 순위는 <a href="https://github.com/Will-Big/clox/blob/cffbb8955843285a545655eed8a59f4be21ec086/compiler.c#L13">여기</a>를 참조)</p>
<p>루프 안에서 <code>move_parser()</code> 를 호출합니다. <code>pre -&gt; +</code>, <code>cur -&gt; 2</code> 가 됩니다. <code>cur_token_processing(&#39;+&#39;)</code> (즉, <code>binary()</code> 함수)가 호출됩니다.</p>
<p><code>binary</code> 함수는 이항 연산자(<code>Infix</code>)를 처리합니다. 임무는 이렇습니다.</p>
<ul>
<li><p><code>+</code>의 오른쪽 피연산자(<code>2</code>)를 파싱한다.</p>
</li>
<li><p>파싱이 끝나면, &quot;스택 위의 두 값을 더해라&quot;라는 <code>OP_ADD</code> 명령어를 붙인다.</p>
</li>
</ul>
<p>오른쪽 피연산자를 파싱하기 위해, <code>binary</code> 는 <code>start_parsing_with(&#39;+&#39;)</code>를 재귀 호출합니다. 이때 <code>+</code> 보다 한 단계 높은 <code>* or /</code> 우선순위를 넘겨줍니다. 이렇게 하는 이유는 자신보다 우선순위가 높은 연산자들을 재귀 호출을 통해 먼저 계산하도록 하는 것입니다.</p>
<pre><code class="language-c">Source: &quot;-(1+2)&quot;
Tokens:   -   (   1   +   2   )  &lt;EOF&gt;
Parser:                   ^   ^
                          |   |
                         pre cur
Call Stack: [ start_parsing_with(&#39;=&#39;) | start_parsing_with(&#39;-&#39;) | start_parsing_with(&#39;=&#39;) | start_parsing_with(&#39;* or /&#39;) ]

Byte-c: [OP_CONSTANT, 0]
Consts: [1.0]</code></pre>
<p>4단계 재귀입니다. <code>start_parsing_with(&#39;* or /&#39;)</code> 가 <code>move_parser()</code> 를 호출합니다. <code>pre -&gt; 2</code>, <code>cur -&gt; )</code> 가 됩니다.</p>
<p><code>pre_token_processing(&#39;2&#39;)</code> (즉, <code>number()</code>)가 호출됩니다. <code>Consts</code> 배열 <code>1</code>번 인덱스에 <code>2.0</code>이 추가되고, <code>Byte-c</code>에 <code>OP_CONSTANT</code>, <code>1</code>이 추가됩니다.</p>
<pre><code class="language-c">Source: &quot;-(1+2)&quot;
Tokens:   -   (   1   +   2   )  &lt;EOF&gt;
Parser:                       ^   ^
                              |   |
                             pre cur
Call Stack: [ start_parsing_with(&#39;=&#39;) | start_parsing_with(&#39;-&#39;) | start_parsing_with(&#39;=&#39;) | start_parsing_with(&#39;* or /&#39;) ]

Byte-c: [OP_CONSTANT, 0, OP_CONSTANT, 1]
Consts: [1.0, 2.0]</code></pre>
<p><code>number()</code>가 리턴되고, <code>start_parsing_with(&#39;* or / &#39;)</code>의 <code>while</code> 루프로 돌아옵니다. <code>cur -&gt; )</code>입니다. <code>)</code>의 우선순위는 <code>* or /</code> 보다 낮습니다. <code>while</code> 루프는 종료되고, <code>start_parsing_with(&#39;* or /&#39;)</code>가 리턴됩니다. (4단계 재귀 종료)</p>
<p>이제 <code>binary()</code> 함수로 돌아옵니다. 오른쪽 피연산자(<code>2</code>) 파싱이 끝났습니다. <code>binary</code>는 자신의 임무인 <code>OP_ADD</code> 명령어를 <code>Byte-c</code>에 추가합니다.</p>
<pre><code class="language-c">Byte-c: [OP_CONSTANT, 0, OP_CONSTANT, 1, OP_ADD]
Consts: [1.0, 2.0]</code></pre>
<p><code>binary()</code>가 리턴됩니다. (3단계 <code>while</code> 루프의 <code>in_token_processing</code> 종료)</p>
<p>다시 <code>start_parsing_with(&#39;=&#39;)</code>의 <code>while</code> 루프로 돌아옵니다. <code>cur</code> 는 여전히 <code>)</code> 입니다. <code>)</code> 의 우선순위는 <code>=</code> 보다 낮으므로 <code>while</code> 루프가 종료됩니다. <code>start_parsing_with(&#39;=&#39;)</code>가 리턴됩니다. (3단계 재귀 종료)</p>
<p>이제 <code>grouping()</code> 함수로 돌아옵니다. 괄호 안의 표현식(<code>1+2</code>) 파싱이 모두 끝났습니다. <code>grouping</code>의 마지막으로 <code>cur</code> 가 <code>)</code> 인지 확인합니다. 문제가 없다면 <code>move_parser()</code> 를 호출하여 <code>)</code>를 소비합니다.</p>
<pre><code class="language-c">Source: &quot;-(1+2)&quot;
Tokens:   -   (   1   +   2   )  &lt;EOF&gt;
Parser:                           ^    ^
                          |    |
                         pre  cur
Call Stack: [ start_parsing_with(&#39;=&#39;) | start_parsing_with(&#39;-&#39;) ]

Byte-c: [OP_CONSTANT, 0, OP_CONSTANT, 1, OP_ADD]
Consts: [1.0, 2.0]</code></pre>
<p><code>grouping()</code> 이 리턴됩니다.</p>
<p>이제 <code>start_parsing_with(&#39;-&#39;)</code> 로 돌아옵니다. <code>pre_token_processing(&#39;(&#39;)</code> (즉, <code>grouping()</code>)이 리턴되었습니다. <code>while</code> 루프를 검사합니다. <code>cur</code> 는 <code>&lt;EOF&gt;</code> 입니다. 우선순위가 가장 낮으므로 <code>while</code> 루프가 종료됩니다. <code>start_parsing_with(&#39;-&#39;)</code> 가 리턴됩니다. (2단계 재귀 종료)</p>
<p><code>unary()</code> 함수로 돌아옵니다. 피연산자 <code>(1+2)</code>의 파싱이 모두 끝났습니다. <code>unary</code>는 마지막으로 <code>OP_NEGATE</code> 명령어를 <code>Byte-c</code>에 추가합니다.</p>
<pre><code class="language-c">Byte-c: [OP_CONSTANT, 0, OP_CONSTANT, 1, OP_ADD, OP_NEGATE]
Consts: [1.0, 2.0]</code></pre>
<p><code>unary()</code>가 리턴됩니다.</p>
<p>최초에 호출했던 <code>start_parsing_with(&#39;=&#39;)</code> 로 돌아옵니다. <code>pre_token_processing(&#39;-&#39;)</code> (즉, <code>unary()</code>)가 리턴되었습니다. <code>while</code> 루프를 검사합니다. <code>cur</code> 는 <code>&lt;EOF&gt;</code> 입니다. 우선순위가 낮으므로 <code>while</code> 루프가 종료됩니다. <code>start_parsing_with(&#39;=&#39;)</code>가 리턴됩니다. (1단계 재귀 종료)</p>
<p>모든 표현식 파싱 호출이 끝났습니다. 표현식이 종료되었기 때문에 최종적으로 <code>cur</code> 가 <code>&lt;EOF&gt;</code> 인지 확인합니다.</p>
<p>마지막으로 <code>OP_RETURN</code> 을 <code>Byte-c</code> 에 추가합니다.</p>
<pre><code class="language-c">// 최종 결과
Byte-c: [OP_CONSTANT, 0, OP_CONSTANT, 1, OP_ADD, OP_NEGATE, OP_RETURN]
Consts: [1.0, 2.0]</code></pre>
<h3 id="바이트-코드-실행-과정">바이트 코드 실행 과정</h3>
<p>이것이 <code>-(1+2)</code>가 바이트코드로 컴파일되는 재귀 과정입니다. 이 바이트코드를 <code>VM</code>이 실행하면 다음과 같습니다.</p>
<ol>
<li><p><code>OP_CONSTANT, 0</code>: 스택에 <code>1.0</code>을 푸시. 
 (스택: <code>[1.0]</code>)</p>
</li>
<li><p><code>OP_CONSTANT, 1</code>: 스택에 <code>2.0</code>을 푸시. 
 (스택: <code>[1.0, 2.0]</code>)</p>
</li>
<li><p><code>OP_ADD</code>: <code>1.0</code>과 <code>2.0</code>을 팝하여 더하고, 결과 <code>3.0</code>을 푸시. 
 (스택: <code>[3.0]</code>)</p>
</li>
<li><p><code>OP_NEGATE</code>: <code>3.0</code>을 팝하여 부호를 바꾸고, 결과 <code>-3.0</code>을 푸시. 
 (스택: <code>[-3.0]</code>)</p>
</li>
<li><p><code>OP_RETURN</code>: <code>-3.0</code>을 팝하여 출력.
 (스택: <code>empty</code>)</p>
</li>
</ol>
<p>이처럼 프랫 파서는 재귀 호출 스택을 이용하여, 코드를 작성하는 순서와 다르게 <code>VM</code>이 실행해야 하는 순서로 바이트코드를 재정렬합니다.</p>
<h3 id="마치며">마치며</h3>
<p>여태와는 다르게 이번 코드는 방대하며, 재귀적인 호출이 많았기에 이해하기 어려웠습니다. 그래서 지속적인 디버깅과 예제를 활용하여 다양한 상황에서 어떻게 파싱이 적용되는지 확인해보며 실행되는 과정을 지켜보고 분석하는 과정이 오래 걸렸습니다. 다음에는 <code>Lox</code> 언어에서 활용되는 값의 유형(<code>type</code>)에 대해 알아보겠습니다.</p>
<p>참고 자료: <a href="https://craftinginterpreters.com/compiling-expressions.html">Cratring Interpreters - compiling expressions</a>
Github: <a href="https://github.com/Will-Big/clox">Will-big/clox</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C 언어로 Lox 인터프리터 만들기 4]]></title>
            <link>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-4</link>
            <guid>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-4</guid>
            <pubDate>Mon, 13 Oct 2025 06:08:58 GMT</pubDate>
            <description><![CDATA[<p>지난 기록에서는 가상 머신(VM)의 기본 구조를 만들고, 직접 작성한 바이트코드를 실행하는 단계까지 진행했습니다. 하지만 진정한 인터프리터라면 사용자가 작성한 소스 코드를 직접 해독할 수 있는 능력이 필수적입니다.</p>
<p>이번 개발 기록에서는 <strong>스캐너(Scanner)</strong>를 구현한 과정을 공유하고자 합니다. 스캐너는 소스 코드라는 텍스트를 인터프리터가 이해할 수 있는 최소 단위인 <strong>토큰(Token)</strong>으로 분해하는, 언어 처리의 가장 기본적인 단계입니다.</p>
<h3 id="스캐너의-역할">스캐너의 역할</h3>
<p>인터프리터가 <code>&quot;var a = 10;&quot;</code>과 같은 코드를 만났을 때, 이것을 단순한 텍스트가 아닌 <code>var</code> 키워드, <code>a</code>라는 이름, <code>=</code> 연산자, <code>10</code>이라는 숫자, 그리고 <code>;</code> 기호의 조합으로 인식하게 만드는 것이 바로 스캐너의 역할입니다.</p>
<p>이 스캐닝 단계를 거치면, 위 코드는 아래와 같은 토큰들의 흐름(Stream)으로 변환됩니다.</p>
<p><code>TOKEN_VAR</code> <code>TOKEN_IDENTIFIER</code> <code>TOKEN_EQUAL</code> <code>TOKEN_NUMBER</code> <code>TOKEN_SEMICOLON</code></p>
<p>이렇게 생성된 토큰 스트림은 인터프리터의 다음 단계인 파서(Parser)로 전달되어, 코드의 문법적 구조를 분석하는 데 사용됩니다.</p>
<h3 id="clox-스캐너-구현-과정">Clox 스캐너 구현 과정</h3>
<p>본격적으로 <code>scanner.c</code>와 <code>scanner.h</code> 파일에 스캐너를 구현한 과정을 단계별로 설명하겠습니다.</p>
<h4 id="1-토큰token의-정의">1. 토큰(<code>Token</code>)의 정의</h4>
<p>가장 먼저 <code>scanner.h</code> 파일에 Lox 언어에 필요한 모든 종류의 토큰을 <code>enum</code>으로 정의했습니다. 한 글자로 된 괄호나 연산자부터, <code>!=</code> 나 <code>==</code> 처럼 두 글자로 이루어진 토큰, 그리고 숫자, 문자열, 식별자 및 모든 키워드가 여기에 포함됩니다.</p>
<pre><code class="language-c">// scanner.h
typedef enum 
{
    // Single-character tokens.
    TOKEN_LEFT_PAREN, TOKEN_RIGHT_PAREN,
    // ...
    // Keywords.
    TOKEN_AND, TOKEN_CLASS, TOKEN_ELSE, TOKEN_FALSE,
    // ...
    TOKEN_ERROR, TOKEN_EOF
} TokenType;</code></pre>
<p>다음으로, 각 토큰이 담게 될 정보를 <code>Token</code> 구조체로 설계했습니다. 토큰의 종류(<code>type</code>)는 물론, 오류 메시지 출력이나 디버깅을 위해 소스 코드 내 위치를 알려주는 줄 번호(<code>line</code>) 정보도 필요합니다.</p>
<p>여기서 중요한 설계 결정은 토큰의 실제 텍스트, 즉 렉심(lexeme)을 처리하는 방식이었습니다. 렉심을 위해 매번 새로운 메모리를 할당하여 문자열을 복사하는 대신, <strong>원본 소스 코드 문자열의 시작 위치를 가리키는 포인터(<code>start</code>)와 그 길이(<code>length</code>)</strong>만을 저장하기로 했습니다. C언어 환경에서는 이 방식이 불필요한 메모리 할당과 해제를 줄여주므로 훨씬 효율적이며 관리 또한 용이합니다.</p>
<pre><code class="language-c">// scanner.h
typedef struct
{
    TokenType type;
    const char* start;
    int length;
    int line;
} Token;</code></pre>
<h4 id="2-키워드와-식별자의-구분">2. 키워드와 식별자의 구분</h4>
<p>스캐너 구현에서 가장 흥미로운 부분 중 하나는 키워드(<code>var</code>, <code>if</code>, <code>for</code> 등)와 일반 식별자(사용자가 정의한 변수 이름 등)를 구분하는 로직이었습니다. 예를 들어 <code>&#39;for&#39;</code>는 키워드이지만, <code>&#39;forest&#39;</code>는 식별자로 처리되어야 합니다.</p>
<p>만약 단어가 <code>&#39;f&#39;</code>로 시작했다면, 두 번째 글자가 <code>&#39;a&#39;</code>인지(<code>&#39;false&#39;</code>), <code>&#39;o&#39;</code>인지(<code>&#39;for&#39;</code>), <code>&#39;u&#39;</code>인지(<code>&#39;fun&#39;</code>)를 다시 <code>switch</code> 문으로 확인하는 방식으로 탐색을 이어갑니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/3a2651ff-57a0-4cd6-90b1-c3f587836aa4/image.png" alt=""></p>
<p>이미지 출처: <a href="https://craftinginterpreters.com/scanning-on-demand.html">Crafting Interpreters</a></p>
<p>이를 해결하기 위해 <code>identifierType</code> 함수 내에서 위 이미지와 같은 Trie(트라이) 자료구조와 유사한 아이디어를 적용했습니다.</p>
<pre><code class="language-c">static TokenType identifierType()
{
    switch (scanner.start[0])
    {
        case &#39;a&#39;: return checkKeyword(1, 2, &quot;nd&quot;, TOKEN_AND);
        // ...
        case &#39;f&#39;:
            if (scanner.current - scanner.start &gt; 1)
            {
                switch (scanner.start[1])
                {
                    case &#39;a&#39;: return checkKeyword(2, 3, &quot;lse&quot;, TOKEN_FALSE);
                    case &#39;o&#39;: return checkKeyword(2, 1, &quot;r&quot;, TOKEN_FOR);
                    case &#39;u&#39;: return checkKeyword(2, 1, &quot;n&quot;, TOKEN_FUN);
                }
            }
            break;
        // ...
        case &#39;w&#39;: return checkKeyword(1, 4, &quot;hile&quot;, TOKEN_WHILE);
    }

    return TOKEN_IDENTIFIER;
}</code></pre>
<h4 id="3-scantoken-함수">3. <code>scanToken()</code> 함수</h4>
<p>스캐너의 모든 동작은 <code>scanner.c</code> 파일에 정의된 <code>scanToken()</code> 함수를 중심으로 이루어집니다. 이 함수는 호출될 때마다 소스 코드에서 다음 토큰 하나를 정확히 인식하여 반환하는 역할을 수행합니다. </p>
<p>내부적으로는 다음과 같은 여러 도우미 함수들을 활용하여 코드를 한 글자씩 탐색합니다.</p>
<ul>
<li><p><code>isAtEnd()</code>: 소스 코드의 마지막에 도달했는지 검사합니다.</p>
</li>
<li><p><code>advance()</code>: 현재 문자를 반환하고 다음 문자로 포인터를 이동시킵니다. <code>scanner.current++</code> 실행 후 <code>scanner.current[-1]</code>을 반환하는 포인터 연산을 활용합니다.</p>
</li>
<li><p><code>peek()</code> &amp; <code>peekNext()</code>: 포인터를 이동시키지 않고 현재 또는 바로 다음 문자를 확인합니다.</p>
</li>
<li><p><code>match(char)</code>: 현재 문자가 특정 문자와 일치하는지 확인하고, 일치할 경우에만 포인터를 이동시킵니다. 이 함수 덕분에 <code>!=</code>, <code>==</code> 와 같이 한 글자 또는 두 글자로 구성될 수 있는 토큰들을 간결하게 처리할 수 있습니다.</p>
</li>
</ul>
<p><code>scanToken</code> 함수의 전체적인 로직은 <code>advance()</code>로 문자 하나를 읽어온 뒤 <code>switch</code> 문으로 분기하여, 해당 문자에 맞는 토큰을 생성(<code>makeToken</code>)하는 구조로 되어 있습니다.</p>
<pre><code class="language-c">// scanner.c
Token scanToken()
{
    scanner.start = scanner.current;

    if (isAtEnd()) return makeToken(TOKEN_EOF);

    char c = advance();
    if (isAlpha(c)) return identifier();
    if (isDigit(c)) return number();

    switch (c)
    {
        case &#39;(&#39;: return makeToken(TOKEN_LEFT_PAREN);
        // ...
        case &#39;!&#39;:
            return makeToken(match(&#39;=&#39;) ? TOKEN_BANG_EQUAL : TOKEN_BANG);
        // ...
        case &#39;&quot;&#39;: return string();
    }

    return errorToken(&quot;Unexpected character.&quot;);
}</code></pre>
<p>먼저 <code>identifier()</code> 함수는 알파벳이나 밑줄(<code>_</code>)로 시작하는 단어를 발견하면, 알파벳이나 숫자가 끝날 때까지 모든 문자를 읽어들입니다.</p>
<p>그 후 <code>identifierType()</code> 함수가 호출되어, 스캔된 단어의 첫 글자를 기준으로 <code>switch</code> 문을 통해 분기합니다.</p>
<p>만약 단어가 <code>&#39;f&#39;</code>로 시작했다면, 두 번째 글자가 <code>&#39;a&#39;</code>인지(<code>&#39;false&#39;</code>), <code>&#39;o&#39;</code>인지(<code>&#39;for&#39;</code>), <code>&#39;u&#39;</code>인지(<code>&#39;fun&#39;</code>)를 다시 <code>switch</code> 문으로 확인하는 방식으로 탐색을 이어갑니다.</p>
<p>마지막으로 <code>checkKeyword()</code> 함수에서 단어의 전체 길이와 나머지 문자열의 내용이 정확히 일치하는지를 <code>memcmp</code> 함수를 통해 최종적으로 검사합니다. 이 모든 조건이 충족되면 해당 키워드 토큰 타입을 반환하고, 그렇지 않으면 일반 <code>TOKEN_IDENTIFIER</code>로 최종 판정합니다.</p>
<p>이러한 접근 방식은 수많은 문자열 비교 함수(<code>strcmp</code>)를 호출하는 것보다 훨씬 효율적으로 키워드를 판별할 수 있게 해줍니다.</p>
<h3 id="마치며">마치며</h3>
<p>이번 구현을 통해 clox 인터프리터는 소스 코드를 읽고 의미 있는 단위로 이해하는 첫 번째 단계를 완성했습니다. 인터프리터(컴파일러)가 키워드를 인식하는 방식으로 간단하지만 효율적인 방법인 Trie 자료구조를 활용하여 만들며 스캐너가 동작하는 원리를 파악하였습니다.</p>
<p>이제 스캐너가 잘게 나눈 토큰들이 준비되었습니다. 다음 단계는 이 토큰들을 조합하여 코드의 문법적인 구조를 파악하는 <strong>파서(Parser)</strong>를 구현하는 것입니다.</p>
<p>참고자료: <a href="https://craftinginterpreters.com/scanning-on-demand.html">Crafting Interpreters - Scanning on Demand</a>
Github: <a href="https://github.com/Will-Big/clox">Will-Big/clox</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C 언어로 Lox 인터프리터 만들기 3]]></title>
            <link>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-3</link>
            <guid>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-3</guid>
            <pubDate>Tue, 30 Sep 2025 07:03:33 GMT</pubDate>
            <description><![CDATA[<p>이전까지는 추후 작성될 Lox 코드를 실행 가능한 형태로 저장하는 명령어 자료형, <code>Chunk</code>를 만드는 과정이었습니다. 이제 <code>Chunk</code>를 실행 할 수 있는 <code>VM</code>(가상 머신)을 만들 차례입니다.</p>
<h3 id="vm의-구성">VM의 구성</h3>
<p><code>VM</code>은 명령어, 즉 <code>Chunk</code>를 실행하기 위한 장치입니다. 정확히는 <code>Chunk::code</code>를 실행합니다. 따라서 현재 실행할 <code>Chunk</code>와 그 안에서 실행하고 있는 바이트 코드의 위치를 알고 있어야 하며, 계산과 임시 값을 추적할 수 있어야 합니다. 이는 아래와 같은 구조를 가짐으로써 가능합니다.</p>
<pre><code class="language-c">typedef struct
{
    Chunk* chunk;
    uint8_t* ip;
    Value stack[STACK_MAX];
    Value* stackTop;
} VM;</code></pre>
<p><code>ip</code>는 <code>Chunk::code</code> 배열 중 어느 인덱스를 참조하고 있는지 나타내는 &#39;명령어 포인터&#39;입니다. 성능을 위해 인덱스가 아닌 실제 메모리 주소를 참고하고 있습니다.</p>
<p><code>stack</code>은 <code>VM</code>이 계산의 결과와 값을 관리할 수 있도록 하는 자료형입니다. 값의 반환은 후입선출의 형태를 따르기 때문에 쉬운 구현을 위해 스택을 사용하였습니다.</p>
<h3 id="인터프리터-실행-흐름">인터프리터 실행 흐름</h3>
<p>지금까지 작성한 코드를 기반으로 Lox 인터프리터의 실행 흐름을 분석하면,</p>
<ol>
<li>소스 코드(Lox) 작성</li>
<li>어휘 분석(토큰 분리)</li>
<li>구문 분석(문장 구조 생성)</li>
<li>바이트 코드(<code>Chunk</code>) 생성</li>
<li>인터프리터(<code>VM</code>) 실행</li>
</ol>
<p>위의 순서를 가지고 있습니다. 하지만 1 ~ 3은 구현하지 않은 상태이며 인위적으로 <code>main.c</code>에서 주입하고 있습니다.</p>
<p>현재는 5(인터프리터 실행)를 구현하기 위해 <code>VM</code>의 최소 기능중 하나인 값을 관리하고 순서에 따라 명령을 시행하는 기능을 구현하고 있습니다.</p>
<p>이 페이지에서 최종적으로 실행하려는 가상의 소스 코드는 다음과 같습니다.</p>
<pre><code class="language-python">return -((1.2 + 3.4) / 5.6) </code></pre>
<p>위 가상의 소스 코드를 C 언어를 활용해 <code>Chunk</code> 로 만들어 넣는 과정은 아래와 같습니다.</p>
<pre><code class="language-c">int main(int argc, const char* argv[])
{
    initVM();
    Chunk chunk;
    initChunk(&amp;chunk);

    int constant = addConstant(&amp;chunk, 1.2); // 1.2
    writeChunk(&amp;chunk, OP_CONSTANT, 123);
    writeChunk(&amp;chunk, constant, 123);

    constant = addConstant(&amp;chunk, 3.4);    // 3.4
    writeChunk(&amp;chunk, OP_CONSTANT, 123);
    writeChunk(&amp;chunk, constant, 123);

    writeChunk(&amp;chunk, OP_ADD, 123); // +

    constant = addConstant(&amp;chunk, 5.6); // 5.6
    writeChunk(&amp;chunk, OP_CONSTANT, 123);
    writeChunk(&amp;chunk, constant, 123);

    writeChunk(&amp;chunk, OP_DIVIDE, 123); // /

    writeChunk(&amp;chunk, OP_NEGATE, 123); // -

    writeChunk(&amp;chunk, OP_RETURN, 123); // return

    disassembleChunk(&amp;chunk, &quot;Test Chunk&quot;);

    interpret(&amp;chunk);
    freeChunk(&amp;chunk);
    freeVM();

    return 0;
}</code></pre>
<h3 id="실행-예">실행 예</h3>
<p>바이트 코드를 실행하기 위한 함수, <code>run()</code>은 <code>interpret()</code> 안에서 실행됩니다. <code>interpret()</code>은 <code>Chunk</code>와 <code>VM</code>을 연결하는 역할을 하며 <code>VM</code> 객체는 전역으로 선언되어 있습니다.</p>
<p><code>run()</code>은 <code>VM</code>에 연결된 <code>Chunk</code>를 실행하며 <code>VM</code> 객체 안의 <code>stack</code>을 활용합니다. <code>main.c</code>에 사용한 <code>writeChunk</code> 순서에 의존하여 아래 이미지와 같은 스택을 구성합니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/f7ae3e5b-4e92-49a8-b858-a91919d2241b/image.png" alt=""></p>
<p><code>stack</code>에 <code>1.2</code>, <code>3.4</code>를 넣고, <code>OP_ADD</code> 연산자를 만나면 <code>pop()</code>을 두 번 한 후, 값을 연산하고 그 값을 다시 <code>push()</code>하는 방식으로 <code>stackTop</code>을 움직이며 값을 관리합니다. 다른 이항 연산자도 이와 같은 형식을 가집니다. 자세한 사항은 하단 Github의 <code>vm.c</code>로 확인할 수 있습니다. </p>
<p><code>OP_NEGATE</code>는 단항 연산자기 때문에 아래와 같은 연산 방식을 사용합니다.</p>
<pre><code class="language-c">case OP_NEGATE:
{
    push(-pop());
    break;
}</code></pre>
<p><code>OP_NEGATE</code> 연산자의 피연산자 값을 <code>pop()</code> 하고 단항 연산자를 적용한 후 다시 값을 <code>push()</code> 하여 스택에 값을 유지할 수 있습니다.</p>
<p>다양한 연산자들을 사용하여 처음 목표했던 식의 결과 값을 리턴하는 과정을 나타내면 아래 이미지와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/46ede8e6-28bc-4c03-a054-0533621e59e6/image.png" alt=""></p>
<ul>
<li><code>0000 ~ 0009</code>은 <code>offset</code>으로 바이트 코드 배열의 인덱스에 해당합니다.</li>
<li><code>123</code>은 소스 코드의 줄 번호에 해당합니다.<ul>
<li><code>|</code>은 위 소스 코드와 같은 줄 번호임을 의미합니다.</li>
</ul>
</li>
<li><code>OP_CODE</code>는 명령어에 해당합니다.</li>
<li><code>[ NUM ]</code>은 스택에 저장된 값입니다.</li>
<li><code>0, 1, 2</code>는 <code>offset</code>으로 상수 풀의 인덱스에 해당합니다.</li>
<li><code>'1.2', '3.4', '5.6'</code>은 상수 값으로 상수 풀의 해당 인덱스에 저장된 값을 의미합니다.</li>
</ul>
<h3 id="마치며">마치며</h3>
<p><code>VM</code>이 어떻게 동작하는지를 살펴보며 내부적으로 스택을 활용하는 과정을 구현해보았습니다. 인터프리터는 현재 연산 값을 보관하고 다음 연산에 활용하기 위해 스택을 필요로 합니다. 그리고 이는 연산 뿐이 아니라 일반적인 프로그래밍에서도 마찬가지로 활용됩니다. 다음에는 소스코드로부터 토큰을 만드는 스캐너(어휘 분석기)를 만들어 보도록 하겠습니다.</p>
<p>참고자료: <a href="https://craftinginterpreters.com/a-virtual-machine.html">Crafting Interpreters - A Virtual Machine</a>
Github: <a href="https://github.com/Will-Big/clox">Will-Big/clox</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ C 언어로 Lox 인터프리터 만들기 2]]></title>
            <link>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</link>
            <guid>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</guid>
            <pubDate>Fri, 26 Sep 2025 13:20:59 GMT</pubDate>
            <description><![CDATA[<p>우리가 만드는 VM은 <code>123</code> 같은 숫자나 <code>"hello world"</code> 같은 문자열, 즉 &#39;값&#39;을 다룰 수 있어야 합니다. 사용자가 생성 가능한 모든 코드는 바이트 코드로 저장되기 때문에 상수 값 또한 이와 같은 형식으로 저장되어야 합니다.</p>
<p>바이트 코드로 저장하기 위해서, 이전에 만들었던 <code>Chunk</code> 구조체를 활용합니다. </p>
<pre><code class="language-c">typedef struct
{
    int count;
    int capacity;
    uint8_t* code;
} Chunk;</code></pre>
<p><code>print 10;</code>과 같은 코드가 있다면 아래 그림과 같이 저장되는 형식입니다. <code>code</code> 배열에 명령어와 인자들이 바이트 코드로 변환되어 순차적으로 저장됩니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/5276dfde-4d9e-4ab7-9ca7-0af39d1895d8/image.png" alt=""></p>
<p>하지만 모든 데이터에 대해 위 그림같이 명령어 뒤에 상수를 놓는 것은 불가능한 작업입니다. <code>print "hello, world";</code>의 경우, 상수에 해당하는 문자열의 크기가 가변적이기 때문에 바이트의 할당량을 예측할 수 없습니다. </p>
<p><img src="https://velog.velcdn.com/images/will-big/post/e882ccf5-f52f-4bf7-9aff-aac75922fd8e/image.png" alt=""></p>
<p>그래서 다음 바이트 코드의 인덱스를 VM에서 알 수 없게 됩니다. 이 문제는 상수 전용 풀을 생성하고 바이트 코드안에 상수가 존재하는 인덱스를 작성하여 해결 할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/will-big/post/f4f7c7e7-9d34-4e25-99ea-9dfd6bc26acf/image.png" alt=""></p>
<h4 id="마치며">마치며</h4>
<p>명령어와 인자는 청크의 <code>code</code>에 저장되며 크기는 바이트 단위입니다. 명령어에 필요한 인자 크기가 항상 단일 바이트일 이유는 없지만 VM에서 크기를 예측할 수 있어야 정상적인 실행이 가능합니다. 따라서 가변적인 값을 고려하여 상수 풀을 만들고 이를 참조하는 방식으로 바이트 코드를 생성할 수 있습니다.</p>
<p>참고자료 : <a href="https://craftinginterpreters.com/chunks-of-bytecode.html#constants">Crafting Interpreters - constants</a>
Github : <a href="https://github.com/Will-Big/clox">Will-Big/clox</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[C 언어로 Lox 인터프리터 만들기 1]]></title>
            <link>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@will-big/C-%EC%96%B8%EC%96%B4%EB%A1%9C-Lox-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Thu, 25 Sep 2025 08:21:06 GMT</pubDate>
            <description><![CDATA[<p><a href="https://craftinginterpreters.com/chunks-of-bytecode.html">Crafting Interpreters</a> 라는 웹 교재를 활용하여 C 언어를 기반으로 동작하는 <strong>Lox</strong> 언어의 <strong>인터프리터</strong>를 만들고 있습니다.</p>
<p>코드에 대한 설명과 전체 구조는 웹 교재를 통해 확인할 수 있으므로 이 페이지에서는 다루지 않습니다. 주로 교재를 따라 만들어보며 생겼던 의문이나 다시 정리하고 싶은 내용들을 위주로 작성할 계획입니다.</p>
<p>이번 정리는 인터프리터의 이해를 위한 VM(가상 머신)과 Chunk에 대한 기초를 다룹니다.</p>
<h3 id="vm과-chunk의-관계">VM과 Chunk의 관계</h3>
<p>Lox 인터프리터의 최종 목표는 사용자가 작성한 Lox 코드를 실행하는 것입니다. 이 과정은 크게 두 단계로 나뉩니다.</p>
<p>컴파일: 사람이 이해하는 Lox 코드(var a = 1;)를 기계(VM)가 이해하는 저수준 명령어, 즉 <strong>바이트코드(Bytecode)</strong>로 번역합니다.</p>
<p>실행: <strong>가상 머신(VM)</strong>이라는 엔진이 이 바이트코드를 한 줄씩 읽어 실행합니다.</p>
<p>여기서 Chunk는 바로 그 바이트코드가 담기는 &#39;명령서 뭉치&#39; 역할을 합니다. 컴파일러가 Lox 코드를 번역하여 Chunk라는 명령서 뭉치를 만들면, VM이 그걸 가져다가 실행하는 구조입니다.</p>
<h3 id="chunk-구조체-파헤치기">Chunk 구조체 파헤치기</h3>
<p>사용자가 얼마나 긴 코드를 짤지 미리 알 수 없으므로, 이 명령서 뭉치는 필요에 따라 자동으로 길어지는 기능이 있어야 합니다. 그래서 Chunk 구조체는 다음과 같은 세 가지 핵심 멤버로 구성됩니다.</p>
<pre><code class="language-c">typedef struct
{
    int count;
    int capacity;
    uint8_t* code;
} Chunk;</code></pre>
<p><code>int count</code>: 현재 사용 중인 바이트의 개수입니다.
<code>int capacity</code>: 현재 할당된 메모리의 총 용량입니다.
<code>uint8_t* code</code>: 실제 명령어 바이트들이 저장되는 메모리 공간입니다.</p>
<p>새로운 명령어를 추가할 때마다 공간이 부족하면(<code>count == capacity</code>) 메모리를 재할당(<code>realloc</code>)합니다. 이와 관련된 함수는 웹 교재를 참고하거나 글 하단의 Github - memory 소스 코드를 통해 확인할 수 있습니다.</p>
<h4 id="마치며">마치며</h4>
<p>현재는 기본 명령어 단위인 Chunk만 의 구조만 설계한 상태입니다. 각 명령어를 분해하기 위한 <code>disassemble</code> 관련 함수도 존재합니다. 앞으로 교재를 진행하며 VM, 가비지 컬렉터, 상수와 변수에 대한 처리를 구현하며 정리할 예정입니다.</p>
<p>Github : <a href="https://github.com/Will-Big/clox">Will-Big/clox</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] std::vector<bool> 의 진실]]></title>
            <link>https://velog.io/@will-big/stdvectorbool-%EC%9D%98-%EC%A7%84%EC%8B%A4</link>
            <guid>https://velog.io/@will-big/stdvectorbool-%EC%9D%98-%EC%A7%84%EC%8B%A4</guid>
            <pubDate>Mon, 15 Sep 2025 06:33:59 GMT</pubDate>
            <description><![CDATA[<p>알고리즘 문제를 풀다가 흥미로운 경험을 했습니다. 저는 습관적으로 <code>vector<T&gt;</code>를 즐겨 쓰고, 반복문도 <code>range-based for</code>를 자주 사용합니다. 그런데 <code>vector<bool&gt;</code>을 사용했을 때는 문제가 생겼습니다.</p>
<h3 id="1-range-based-for에서의-오류">1. range-based for에서의 오류</h3>
<p>  <img src="https://velog.velcdn.com/images/will-big/post/1dda078c-4b0d-4b02-a46e-9a80961f769a/image.png" alt=""></p>
<p>다른 타입에서는 잘 돌아가던 코드가 <code>vector<bool&gt;</code>에서는 컴파일 에러를 냈습니다. 이유는 <code>vector<bool&gt;</code>이 일반적인 <code>vector<T&gt;</code>와 다르게 동작하기 때문입니다.</p>
<p><code>vector<bool&gt;</code>은 <code>bool</code> 값을 비트 단위로 압축 저장하기 때문에, 원소를 <code>bool&</code>로 반환할 수 없습니다. 대신 <code>vector<bool&gt;::reference</code>라는 프록시 객체를 반환하는데, 이게 진짜 참조가 아니기 때문에 범위 기반 <code>for</code>문이나 함수 인자 전달에서 문제가 발생합니다.</p>
<h3 id="2-실행-시간-차이">2. 실행 시간 차이</h3>
<p>또 한 번은 알고리즘 문제 풀이 후 채점 결과를 보면서 발생하였습니다.
제 코드와 다른 사람의 코드가 시간 복잡도 상으론 동일했지만, 제 코드는 실행 시간이 눈에 띄게 느렸습니다.
  <img src="https://velog.velcdn.com/images/will-big/post/32760030-e134-47b7-be20-c98600aad3ba/image.png" alt="">
<img src="https://velog.velcdn.com/images/will-big/post/68b67a44-a9db-437f-baf9-8c8a84ca3cfd/image.png" alt=""></p>
<p>코드를 비교 했을 때 가장 큰 차이는 </p>
<ul>
<li>저는 <code>std::vector<bool&gt;</code>을 사용했고,</li>
<li>다른 사람은 <code>bool[n]</code> 배열을 사용했습니다.</li>
</ul>
<p><code>vector<bool&gt;</code>은 매 접근마다 비트 마스크 연산과 프록시 객체 생성이 들어가므로, 단순 메모리 참조만 하는 <code>bool[]</code>보다 훨씬 느릴 수밖에 없습니다. 반복 접근이 많은 BFS, DFS 같은 문제에선 이 차이가 성능에 직접적인 영향을 줍니다.</p>
<h3 id="3-결론">3. 결론</h3>
<p>저는 이 두 가지 경험을 통해 <code>vector<bool&gt;</code>이 일반적인 <code>vector<T&gt;</code>와는 다른 점을 느낄 수 있었습니다. 특히 알고리즘에서는 유의미하기 때문에 따라서 상황에 맞는 적절한 자료구조를 선택하는 것이 필요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] static_cast 를 통한 객체 캐스팅]]></title>
            <link>https://velog.io/@will-big/C-staticcast-%EB%A5%BC-%ED%86%B5%ED%95%9C-%EA%B0%9D%EC%B2%B4-%EC%BA%90%EC%8A%A4%ED%8C%85</link>
            <guid>https://velog.io/@will-big/C-staticcast-%EB%A5%BC-%ED%86%B5%ED%95%9C-%EA%B0%9D%EC%B2%B4-%EC%BA%90%EC%8A%A4%ED%8C%85</guid>
            <pubDate>Fri, 07 Mar 2025 05:03:01 GMT</pubDate>
            <description><![CDATA[<p>최근 주변에서 <strong>static_cast</strong> 를 포인터나 참조, 혹은 r-value 가 아닌 <strong>실제 객체</strong>에 사용했을 때 어떻게 동작하는지에 대한 질문이 들어왔습니다. 제게 “static_cast 가 실행될 때 임시 객체가 생성된다”라는 설명을 들었는데, 이에 대해 자세히 정리해보고자 합니다.</p>
<h2 id="1-실제-객체에-static_cast-를-사용하면-무슨-일이-일어날까">1. 실제 객체에 static_cast 를 사용하면 무슨 일이 일어날까?</h2>
<p>C++에서 <strong>static_cast</strong> 를 실제 객체에 적용하면, 대상 타입의 임시 객체(temporary object)가 새로 생성됩니다.<br>즉,  </p>
<ul>
<li><strong>복사 생성자</strong> 또는 변환 연산자를 통해 원본 객체의 해당 부분(예를 들어, 파생 클래스의 Base 부분)만 복사합니다.</li>
<li>이때, 파생 클래스에서 추가된 멤버들은 복사되지 않으며, 그 결과 <strong>객체 슬라이싱</strong>(object slicing)이 발생하게 됩니다.</li>
</ul>
<p>아래 예시 코드를 통해 이를 살펴볼 수 있습니다:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
using namespace std;

class Base {
public:
    int base_val;
    Base(int val = 0) : base_val(val) {}

    virtual void show() const {
        cout &lt;&lt; &quot;Base: &quot; &lt;&lt; base_val &lt;&lt; &quot;\n&quot;;
    }
};

class Derived : public Base {
public:
    int derived_val;
    Derived(int base, int derived) : Base(base), derived_val(derived) {}

    // Derived 클래스의 show()는 파생 클래스의 추가 정보를 출력합니다.
    void show() const override {
        cout &lt;&lt; &quot;Derived: &quot; &lt;&lt; base_val &lt;&lt; &quot;, &quot; &lt;&lt; derived_val &lt;&lt; &quot;\n&quot;;
    }
};

int main() {
    Derived d(1, 2);

    // 객체를 값으로 static_cast 하면, 새로운 Base 객체(임시 객체)가 생성되고,
    // Derived의 추가 멤버(derived_val)는 잘려나갑니다.
    Base b = static_cast&lt;Base&gt;(d);
    cout &lt;&lt; &quot;After object static_cast (객체 슬라이싱 발생):\n&quot;;
    b.show();  // Base::show() 호출, 결과: &quot;Base: 1&quot;

    // 포인터나 참조를 static_cast 하면, 원본 객체를 가리키므로 슬라이싱이 발생하지 않습니다.
    Base&amp; b_ref = static_cast&lt;Base&amp;&gt;(d);
    cout &lt;&lt; &quot;After reference static_cast (슬라이싱 없음):\n&quot;;
    b_ref.show();  // Derived::show() 호출, 결과: &quot;Derived: 1, 2&quot;

    return 0;
}</code></pre>
<p>위 코드에서 <code>Base b = static_cast&lt;Base&gt;(d);</code> 는 Derived 객체 <code>d</code> 의 Base 부분만을 복사하여 임시 객체를 만들고, 그 결과로 객체 슬라이싱이 발생합니다. 반면, <code>Base&amp; b_ref = static_cast&lt;Base&amp;&gt;(d);</code> 는 단순히 원본 객체를 참조하기 때문에, 가상 함수 호출 시 실제 객체인 Derived 의 함수가 호출됩니다.</p>
<h2 id="2-복사-생성자를-삭제하면-어떻게-될까">2. 복사 생성자를 삭제하면 어떻게 될까?</h2>
<p>만약 객체의 복사 생성자가 명시적으로 삭제되었다면, <strong>static_cast</strong> 를 통해 값 변환(즉, 임시 객체 생성을 요구하는 변환)을 수행할 때 컴파일 오류가 발생합니다.<br>즉, <strong>static_cast</strong> 로 객체를 값으로 변환하려고 하면 내부적으로 복사 생성자(또는 이동 생성자)가 호출되는데, 복사 생성자가 삭제된 경우 그 호출이 불가능하여 컴파일러가 에러를 내게 됩니다.</p>
<p>예를 들어보면:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
using namespace std;

class Base {
public:
    int base_val;
    Base(int val = 0) : base_val(val) {}

    // 복사 생성자를 명시적으로 삭제
    Base(const Base&amp;) = delete;

    virtual void show() const {
        cout &lt;&lt; &quot;Base: &quot; &lt;&lt; base_val &lt;&lt; &quot;\n&quot;;
    }
};

class Derived : public Base {
public:
    int derived_val;
    Derived(int base, int derived) : Base(base), derived_val(derived) {}

    void show() const override {
        cout &lt;&lt; &quot;Derived: &quot; &lt;&lt; base_val &lt;&lt; &quot;, &quot; &lt;&lt; derived_val &lt;&lt; &quot;\n&quot;;
    }
};

int main() {
    Derived d(1, 2);

    // 아래 코드는 컴파일 에러를 발생시킵니다.
    // static_cast 를 사용하면 임시 객체 생성을 위해 복사 생성자가 호출되는데, 
    // 복사 생성자가 삭제되어 있으므로 컴파일 되지 않습니다.
    // Base b = static_cast&lt;Base&gt;(d);

    // 반면, 참조를 이용한 캐스팅은 임시 객체 생성을 요구하지 않으므로 정상적으로 작동합니다.
    Base&amp; b_ref = static_cast&lt;Base&amp;&gt;(d);
    b_ref.show();  // 올바르게 Derived::show() 호출, 결과: &quot;Derived: 1, 2&quot;

    return 0;
}</code></pre>
<p>이처럼 복사 생성자가 삭제된 클래스에 대해 객체를 값으로 static_cast 하려고 하면 복사 생성자가 필요하기 때문에 컴파일 오류가 발생합니다.<br>반면, 포인터나 참조를 사용한 캐스팅은 객체 복사가 일어나지 않으므로 이러한 문제가 발생하지 않습니다.</p>
<h2 id="3-객체-슬라이싱과-가상-함수-포인터virtual-function-pointer의-메모리-레이아웃">3. 객체 슬라이싱과 가상 함수 포인터(virtual function pointer)의 메모리 레이아웃</h2>
<p>객체 슬라이싱이 발생하면, 파생 클래스의 추가 데이터는 새로 생성된 임시 객체에 복사되지 않습니다. 이와 함께 <strong>가상 함수 포인터(vptr)</strong> 의 관리에도 중요한 변화가 있습니다.</p>
<ul>
<li><p><strong>메모리 레이아웃 측면</strong>:  
일반적으로 C++ 객체의 메모리 레이아웃은 <strong>vptr</strong>(만약 가상 함수가 있다면)와 데이터 멤버들로 구성됩니다.  </p>
<ul>
<li><strong>Derived 객체</strong>는 자신만의 vptr(보통 Derived 클래스의 vtable을 가리킴)과 추가 멤버들을 포함합니다.</li>
<li>하지만, 객체 슬라이싱을 통해 <strong>Base 임시 객체</strong>가 생성되면, 이 객체는 Base 클래스의 복사 생성자를 통해 만들어집니다.</li>
</ul>
</li>
<li><p><strong>가상 함수 호출과 vptr의 역할</strong>:  
임시 Base 객체는 <strong>Base 타입의 객체</strong>로서 생성되기 때문에,  </p>
<ul>
<li>생성 과정에서 Base 클래스의 생성자(또는 복사 생성자)가 실행되고,  </li>
<li>이에 따라 vptr은 Base 클래스의 vtable을 가리키도록 초기화됩니다.</li>
</ul>
<p>결과적으로, 임시 객체에서는 가상 함수 호출 시 <strong>Base 클래스</strong>의 구현이 사용됩니다.<br>즉, Derived 객체였던 원본과는 달리, 임시 객체는 오직 Base 클래스의 특성만을 반영하게 됩니다.</p>
</li>
</ul>
<p>이러한 메모리 레이아웃 관리 방식은 C++ 컴파일러가 <strong>객체의 타입 안전성</strong>을 보장하기 위해 내부적으로 처리하는 부분이며, 개발자가 직접 개입할 수 없는 구현 세부사항입니다.</p>
<h2 id="4-c-이와-같이-설계된-이유">4. C++ 이와 같이 설계된 이유</h2>
<p>C++는 <strong>값(value) 세만틱스</strong>를 기본 원칙으로 채택하고 있습니다. 이 설계 철학에는 몇 가지 중요한 이유가 있습니다.</p>
<ul>
<li><p><strong>명시적 복사와 불변성</strong>:  
객체를 값으로 전달하거나 복사할 때, 원본 객체와는 독립적인 새로운 객체가 생성되는 것이 일반적입니다.<br>이는 프로그램의 다른 부분에서 발생할 수 있는 부작용(side effect)을 줄이고, 객체의 불변성을 유지하는 데 기여합니다.</p>
</li>
<li><p><strong>객체 슬라이싱의 명시성</strong>:  
파생 클래스의 추가 멤버나 특성을 의도적으로 사용하지 않고, 기본 클래스의 부분만 필요할 때 객체 슬라이싱이 발생하도록 하여,<br>개발자가 슬라이싱의 결과를 명확하게 인지하고 처리할 수 있도록 합니다.</p>
</li>
<li><p><strong>안전한 다형성 구현</strong>:  
만약 원본 객체의 다형적 특성을 그대로 유지하고 싶다면, 포인터나 참조를 사용하도록 유도합니다.<br>이렇게 함으로써, 개발자가 의도한 경우에만 다형성을 활용할 수 있게 하여, 예기치 않은 동작을 방지합니다.</p>
</li>
</ul>
<p>즉, <strong>static_cast</strong> 가 임시 객체를 생성하여 슬라이싱을 발생시키는 설계는,  </p>
<ul>
<li><strong>값 복사 시 명확한 독립 객체 생성</strong>  </li>
<li><strong>개발자에게 명시적 의도 전달</strong>  </li>
<li><strong>다형적 동작의 선택적 활용</strong><br>등의 C++ 설계 철학과 부합합니다.</li>
</ul>
<h2 id="결론">결론</h2>
<p>실제 객체에 <strong>static_cast</strong> 를 사용하면,  </p>
<ul>
<li>대상 타입에 맞는 <strong>임시 객체</strong>가 생성되고,  </li>
<li>이 과정에서 <strong>객체 슬라이싱</strong>이 발생하여 파생 클래스의 추가 멤버와 가상 함수 정보가 손실됩니다.</li>
</ul>
<p>이때, 새로 생성된 임시 객체의 <strong>가상 함수 포인터(vptr)</strong> 는 Base 클래스의 vtable을 가리키도록 설정되어, 가상 함수 호출 시 Base 클래스의 구현이 실행됩니다.<br>이러한 설계는 C++의 <strong>값 세만틱스</strong>와 <strong>명시적 복사</strong> 원칙에 기반한 것으로, 개발자에게 보다 명확한 의도 표현과 안전한 다형성 사용을 유도합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] delete[] 연산자는 어떻게 배열을 지울 수 있을까?]]></title>
            <link>https://velog.io/@will-big/C-delete-%EC%97%B0%EC%82%B0%EC%9E%90%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B0%B0%EC%97%B4%EC%9D%84-%EC%A7%80%EC%9A%B8-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@will-big/C-delete-%EC%97%B0%EC%82%B0%EC%9E%90%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B0%B0%EC%97%B4%EC%9D%84-%EC%A7%80%EC%9A%B8-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Thu, 13 Feb 2025 10:23:17 GMT</pubDate>
            <description><![CDATA[<p>Effective C++을 보며 <code>new</code>와 <code>delete</code> 연산자에 대해 공부하다 보면, <code>delete[]</code> 연산자가 단순히 포인터를 넘겨주는 것이 아니라 내부에 저장된 배열의 길이(혹은 요소 개수)를 이용해 전체 배열을 올바르게 해제한다는 내용을 접하게 됩니다. 그런데 어떻게 <code>delete[]</code>는 배열의 길이를 알 수 있을까요?</p>
<h3 id="new와-delete의-내부-동작">new[]와 delete[]의 내부 동작</h3>
<p>C++에서 <code>new[]</code> 연산자로 동적 배열을 할당하면, 컴파일러는 단순히 필요한 메모리 크기만을 할당하는 것이 아니라, 추가적인 메타데이터를 함께 저장합니다.
이 메타데이터에는 일반적으로 배열에 할당된 요소의 개수 또는 배열의 길이와 같은 정보가 포함됩니다.</p>
<p>예를 들어, 아래와 같이 배열을 할당한다고 가정해봅니다.</p>
<pre><code class="language-cpp">MyClass* arr = new MyClass[3];</code></pre>
<p>이때 실제로 메모리에는 다음과 같은 구조로 저장될 가능성이 있습니다.</p>
<pre><code class="language-cpp">// memory layout
| meta data (len=3) | arr[0] | arr[1] | arr[2] |</code></pre>
<blockquote>
<p>이 메타데이터의 위치와 저장 방식은 구현체에 따라 다를 수 있지만, 많은 컴파일러에서 배열의 앞부분에 저장됩니다.</p>
</blockquote>
<p>그렇다면 <code>delete[]</code> 연산자는 무엇을 할까요?
<code>delete[]</code> 연산자는 전달받은 포인터를 기준으로, 배열 앞에 숨겨진 메타데이터를 읽어 배열의 길이를 알아내고,
각 요소에 대해 적절히 소멸자(destructor)를 호출한 후 메모리 전체를 해제합니다.</p>
<h3 id="왜-일반-delete로는-안-되는가">왜 일반 delete로는 안 되는가?</h3>
<p>만약 <code>new[]</code>로 할당한 배열에 대해 단순히 <code>delete</code>를 사용하면,
메타데이터에 저장된 배열 길이 정보를 참조하지 않고 단일 객체만 해제하려고 시도하게 됩니다.
이 경우, 다음과 같이 요소의 소멸자가 올바르게 호출되지 않아 정의되지 않은 동작(Undefined Behavior)이 발생할 수 있습니다.</p>
<pre><code class="language-cpp">MyClass* arr = new MyClass[3];
delete arr;  // 잘못된 사용: 첫 번째 요소만 소멸자 호출되고 나머지는 무시됨</code></pre>
<p>따라서 동적 배열을 해제할 때는 반드시 <code>delete[]</code> 를 사용해야 합니다.</p>
<h3 id="결론">결론</h3>
<p><code>new[]</code> 연산자: 동적 배열 할당 시 내부에 배열의 길이 등 메타데이터를 함께 저장합니다.
<code>delete[]</code> 연산자: 전달받은 포인터의 앞부분에 저장된 메타데이터를 참조해 배열의 길이를 알아내고,
각 요소의 소멸자를 호출한 후 메모리를 해제합니다.
이 메커니즘은 구현 세부 사항이므로, 프로그래머는 단지 올바른 대응(<code>delete[]</code>)을 사용하기만 하면 됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] nullptr 로 멤버 함수 호출하기]]></title>
            <link>https://velog.io/@will-big/C-nullptr-%EB%A1%9C-%EB%A9%A4%EB%B2%84-%ED%95%A8%EC%88%98-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@will-big/C-nullptr-%EB%A1%9C-%EB%A9%A4%EB%B2%84-%ED%95%A8%EC%88%98-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 09 Feb 2025 10:30:03 GMT</pubDate>
            <description><![CDATA[<p><code>C++</code>에서 멤버 함수를 호출하면 컴파일러는 암시적으로 호출 대상 객체의 주소인 <code>this</code> 포인터를 전달합니다.
따라서 만약 객체 포인터가 <code>nullptr</code>인 경우, 즉 유효하지 않은 주소를 갖고 있을 때 멤버 함수를 호출하면 정의되지 않은 동작(Undefined Behavior, UB) 이 발생할 수 있습니다.
이 글에서는 구체적으로 다음과 같은 경우에 대해 살펴보겠습니다.</p>
<ol>
<li>멤버 변수를 접근하는 멤버 함수 호출</li>
<li>멤버 변수를 접근하지 않는 멤버 함수 호출</li>
<li><code>static</code> 멤버 함수 호출</li>
<li>멤버 변수를 접근하지 않는 가상 함수 호출</li>
<li>멤버 변수를 접근하는 가상 함수 호출</li>
</ol>
<h3 id="1-멤버-변수에-접근하는-멤버-함수-호출">1. 멤버 변수에 접근하는 멤버 함수 호출</h3>
<p>멤버 함수 내에서 멤버 변수에 접근하는 경우, <code>this</code> 포인터가 <code>nullptr</code>이면 즉시 <code>nullptr</code> 역참조가 발생하여 <code>segmentation fault</code> 등 런타임 에러가 발생합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct MyClass {
    int value = 42;

    void accessMember() {
        // 멤버 변수에 접근하므로, this가 nullptr면 런타임 에러 발생!
        std::cout &lt;&lt; &quot;value: &quot; &lt;&lt; value &lt;&lt; std::endl;
    }
};

int main() {
    MyClass* ptr = nullptr;
    // ptr-&gt;accessMember() 호출 시 nullptr를 역참조 → UB 발생
    ptr-&gt;accessMember();
    return 0;
}</code></pre>
<h3 id="2-멤버-변수를-접근하지-않는-멤버-함수-호출">2. 멤버 변수를 접근하지 않는 멤버 함수 호출</h3>
<p>멤버 함수 내부에서 멤버 변수나 <code>this</code>를 사용하지 않는 경우, 실행 결과가 문제없이 보일 수 있습니다.
하지만 표준상 이 경우에도 여전히 <code>this</code> 포인터는 암시적으로 전달되므로 정의되지 않은 동작(UB)임에 주의해야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct MyClass {
    void noAccess() {
        // 멤버 변수를 사용하지 않음.
        std::cout &lt;&lt; &quot;Hello from noAccess()&quot; &lt;&lt; std::endl;
    }
};

int main() {
    MyClass* ptr = nullptr;
    // 정상적으로 출력될 수도 있으나, UB에 해당합니다.
    ptr-&gt;noAccess();
    return 0;
}</code></pre>
<h3 id="3-static-멤버-함수-호출">3. static 멤버 함수 호출</h3>
<p><code>static</code> 멤버 함수는 클래스에 속한 일반 함수와 같아서, <code>this</code> 포인터가 전달되지 않습니다.
따라서 객체의 유효성 여부와 <strong>무관하게</strong> 호출할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct MyClass {
    static void staticFunc() {
        std::cout &lt;&lt; &quot;Hello from staticFunc()&quot; &lt;&lt; std::endl;
    }
};

int main() {
    MyClass* ptr = nullptr;
    // ptr을 통해 호출해도, 내부적으로 this를 사용하지 않으므로 안전합니다.
    ptr-&gt;staticFunc();
    // 권장하는 호출 방법: MyClass::staticFunc();
    return 0;
}</code></pre>
<h3 id="4-멤버-변수를-접근하지-않는-가상-함수-호출">4. 멤버 변수를 접근하지 않는 가상 함수 호출</h3>
<p>가상 함수는 런타임에 호출할 함수 주소를 결정하기 위해 <code>vtable lookup</code> 을 수행합니다.
비록 가상 함수의 본문 내에서 멤버 변수를 접근하지 않더라도, <code>vtable lookup</code> 과정에서 암시적으로 전달되는 <code>this</code> 포인터가 필요합니다.
따라서 <code>this</code>가 <code>nullptr</code>인 경우, <code>vtable</code> 조회 시 <code>nullptr</code> 역참조가 발생하여 정의되지 않은 동작(UB)이 발생할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct MyClass {
    virtual void virtualNoAccess() {
        // 멤버 변수에 접근하지 않지만, 호출 시 vtable lookup을 위해 this가 필요합니다.
        std::cout &lt;&lt; &quot;Hello from virtualNoAccess()&quot; &lt;&lt; std::endl;
    }
};

int main() {
    MyClass* ptr = nullptr;
    // vtable lookup 시 nullptr 역참조 → UB 발생
    ptr-&gt;virtualNoAccess();
    return 0;
}</code></pre>
<blockquote>
<p>추가 설명:
4번의 경우, &quot;명시적으로&quot; 오류 메시지가 출력되거나 즉시 크래시가 발생한다고 보장할 수 없습니다.
실제로 시스템이나 컴파일러에 따라 동작이 달라질 수 있지만, UB임에는 변함이 없습니다.</p>
</blockquote>
<h3 id="5-멤버-변수를-접근하는-가상-함수-호출">5. 멤버 변수를 접근하는 가상 함수 호출</h3>
<p>이 경우는 1번과 4번의 문제가 모두 복합되어 발생합니다.
즉, <code>vtable lookup</code> 과정에서 <code>this</code> 포인터가 필요하고, 그 이후 멤버 변수 접근으로 인해 추가적인 <code>nullptr</code> 역참조가 일어납니다.
따라서 매우 위험하며, 정의되지 않은 동작입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct MyClass {
    int value = 100;

    virtual void virtualAccess() {
        // vtable lookup 이후, 멤버 변수 접근 → UB 발생
        std::cout &lt;&lt; &quot;value: &quot; &lt;&lt; value &lt;&lt; std::endl;
    }
};

int main() {
    MyClass* ptr = nullptr;
    // vtable 조회와 멤버 변수 접근 모두 문제를 일으킵니다.
    ptr-&gt;virtualAccess();
    return 0;
}</code></pre>
<h3 id="결론">결론</h3>
<p><code>nullptr</code> 객체에서 <code>non-static</code> 멤버 함수를 호출하는 것은 멤버 변수에 접근 여부와 관계없이 UB를 발생시킬 수 있습니다.
특히 가상 함수 호출은 내부적으로 <code>vtable lookup</code>을 위해 <code>this</code> 포인터를 사용하므로, 멤버 변수를 사용하지 않더라도 UB에 해당합니다.</p>
<h4 id="안전한-코딩-습관">안전한 코딩 습관</h4>
<ul>
<li>객체 포인터의 유효성 검사:
멤버 함수를 호출하기 전에 항상 포인터가 <code>nullptr</code> 인지 확인합니다.</li>
<li><code>static</code> 함수 활용:
객체 상태와 무관하게 동작해야 하는 함수라면,
<code>static</code> 멤버 함수나 전역 함수를 사용하는 방법을 고려해보세요.</li>
<li>명시적 호출:
가상 함수의 특정 구현을 호출하고자 한다면, 명시적으로 클래스 이름을 사용하여 호출하는 방법도 있으나, 기본적으로 호출 대상 객체가 유효한지 항상 확인해야 합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C++] 가상함수와 메모리 레이아웃]]></title>
            <link>https://velog.io/@will-big/C-%EA%B0%80%EC%83%81%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@will-big/C-%EA%B0%80%EC%83%81%ED%95%A8%EC%88%98</guid>
            <pubDate>Tue, 19 Sep 2023 05:03:15 GMT</pubDate>
            <description><![CDATA[<p>C++에서 가상 함수(virtual function)는 상속과 다형성을 지원하기 위한 메커니즘이며, 기반 클래스에서 선언되고 파생 클래스에서 재정의(override)될 수 있습니다. 가상 함수는 <code>virtual</code> 키워드를 사용하여 선언됩니다.</p>
<h3 id="기본-문법">기본 문법</h3>
<pre><code class="language-cpp">class Shape {
public:
    virtual void draw() {
        cout &lt;&lt; &quot;Drawing a shape&quot; &lt;&lt; endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        cout &lt;&lt; &quot;Drawing a circle&quot; &lt;&lt; endl;
    }
};</code></pre>
<h3 id="동작-원리">동작 원리</h3>
<p>가상 함수는 vtable(virtual table)이라는 특별한 테이블을 사용하여 동적으로 바인딩됩니다. 객체의 타입을 실행 시간에 결정하여 적절한 함수를 호출합니다.</p>
<h3 id="사용-예">사용 예</h3>
<pre><code class="language-cpp">Shape* shape = new Circle();
shape-&gt;draw(); // 출력: &quot;Drawing a circle&quot;</code></pre>
<p>기반 클래스 포인터(<code>shape</code>)를 사용하여 파생 클래스(<code>Circle</code>)의 <code>draw</code> 메서드를 호출할 수 있습니다. 이렇게 하면 다양한 <code>Shape</code> 파생 클래스를 동일한 인터페이스로 처리할 수 있습니다.</p>
<h3 id="가상포인터와-가상함수-테이블">가상포인터와 가상함수 테이블</h3>
<p>가상함수는 C++에서 다형성을 구현할 때 사용되며, 가상포인터는 가상함수 테이블을 가리킵니다. 객체가 생성될 때, 가상포인터는 해당 클래스의 가상함수 테이블을 가리키게 설정됩니다. 가상함수가 호출될 때, 가상포인터를 통해 올바른 함수를 찾아 실행합니다. 이렇게 해서 런타임 다형성이 구현됩니다.</p>
<pre><code class="language-cpp">[Base Class Object]
+------------------+
|  vptr            | ----&gt; [Base VTable]
+------------------+       +------------------+
|  Base members    |       |   Base::func1()  |
+------------------+       +------------------+
                           |   Base::func2()  |
                           +------------------+

[Derived Class Object]
+------------------+
|  vptr            | ----&gt; [Derived VTable]
+------------------+       +------------------+
|  Base members    |       | Derived::func1() |
+------------------+       +------------------+
|  Derived members |       | Derived::func2() |
+------------------+       +------------------+</code></pre>
<ul>
<li><code>vptr</code>: 가상포인터, 객체가 생성될 때 설정됩니다.</li>
<li><code>VTable</code>: 가상함수 테이블, 가상함수들의 주소를 저장합니다.</li>
</ul>
<p><code>vptr</code>는 객체마다 고유하며, 각 객체의 <code>vptr</code>는 해당 객체의 타입에 따른 <code>VTable</code>을 가리킵니다. 반면에 <code>VTable</code>은 클래스 당 하나만 생성되어 해당 클래스의 모든 객체에 의해 공유됩니다. 즉, 같은 클래스의 객체들은 같은 <code>VTable</code>을 사용하게 됩니다.
가상함수를 호출할 때, <code>vptr</code>를 사용해 적절한 <code>VTable</code>을 찾고, 그 테이블에서 함수의 주소를 찾아 실행합니다. 이렇게 런타임 다형성이 가능해집니다.</p>
<pre><code class="language-cpp">// Assuming SomeFunction() is a virtual functions, this call

ptr-&gt;SomeFunction();

// will be tranformed into something like this:

(*ptr-&gt;__vptr[n])(ptr)</code></pre>
<p><code>ptr-&gt;SomeFunction();</code>이라는 가상함수 호출은 컴파일러에 의해 내부적으로 다른 형태로 변환됩니다. 이 변환된 형태가 <code>(*ptr-&gt;__vptr[n])(ptr)</code>입니다.</p>
<ol>
<li><code>ptr-&gt;__vptr</code>: <code>ptr</code>이 가리키는 객체의 가상포인터(<code>vptr</code>)를 의미합니다. 이 <code>vptr</code>는 해당 객체의 가상함수 테이블(VTable)을 가리킵니다.</li>
<li><code>ptr-&gt;__vptr[n]</code>: <code>vptr</code>가 가리키는 VTable에서 <code>n</code>번째 위치에 있는 함수의 주소를 가져옵니다. <code>n</code>은 <code>SomeFunction()</code>의 VTable 내 위치입니다.</li>
<li><code>(*ptr-&gt;__vptr[n])(ptr)</code>: <code>n</code>번째 위치에 있는 함수를 호출합니다. <code>ptr</code>는 <code>this</code> 포인터로 전달됩니다.</li>
</ol>
<p>이렇게 하면 런타임에 적절한 함수가 호출됩니다. 이는 다형성을 구현하는 데 필수적인 메커니즘입니다.</p>
<h3 id="typeid-연산자와-가상함수">typeid 연산자와 가상함수</h3>
<p><code>typeid</code> 연산자와 가상함수는 C++의 런타임 타입 정보(RTTI) 시스템에서 밀접한 관계를 가집니다. 가상함수가 있는 클래스의 객체에 <code>typeid</code>를 사용하면, 런타임에 객체의 실제 타입을 확인할 수 있습니다.</p>
<ol>
<li>가상함수가 있는 클래스: 객체의 <code>vptr</code> (가상 포인터)를 통해 가상함수 테이블(VTable)에 접근하고, VTable에 저장된 RTTI 정보를 사용하여 객체의 실제 타입을 알아냅니다.</li>
<li>가상함수가 없는 클래스: <code>typeid</code>는 컴파일 타임에 객체의 타입을 결정합니다. 이 경우에는 VTable과 RTTI가 필요하지 않습니다.</li>
</ol>
<p><code>typeid</code>를 사용하여 런타임에 타입을 확인할 때, 가상함수와 VTable이 없으면 다형성을 지원하지 않는 클래스의 객체에 대해서는 정적 타입만을 반환합니다. 가상함수와 VTable이 있는 경우에는 런타임에 객체의 실제 타입을 확인할 수 있습니다.</p>
<pre><code class="language-cpp">SomeObject:                 VTable
  +--------+          +---------------+ 
  | __vptr |----+     | offset_to_top | -2
  +--------+    |     +---------------+
  |   p    |    |     |      RTTI     | -1
  +--------+    |     +---------------+
  |   q    |    +---&gt; |     ~Foo()    | 0
  +--------+          +---------------+
                      | SomeFunction  | 1
                      +---------------+</code></pre>
<p>음수 인덱스에 RTTI 정보가 저장되는 것은 구현 세부사항이므로 모든 컴파일러에서 보장되지는 않습니다. 그러나 많은 C++ 컴파일러 구현에서는 이러한 방식으로 동작합니다.</p>
<h3 id="단일-상속">단일 상속</h3>
<pre><code class="language-cpp">class Base {
public:
    virtual ~Base() {}
    virtual void func1() {}
};

class Derived : public Base {
public:
    virtual ~Derived() {}
    virtual void func1() override {}
    virtual void func2() {}
};</code></pre>
<pre><code class="language-cpp">[Base Class]
+------------------+
|  vptr            | ----&gt; [Base VTable]
+------------------+       +------------------+
|  Base members    |       |  Base::~Base()   |
+------------------+       +------------------+
                           |  Base::func1()   |
                           +------------------+

[Derived Class]
+------------------+
|  vptr            | ----&gt; [Derived VTable]
+------------------+       +--------------------+
|  Base members    |       | Derived::~Derived()|
+------------------+       +--------------------+
| Derived members  |       |  Derived::func1()  |
+------------------+       +--------------------+
                           |  Derived::func2()  |
                           +--------------------+</code></pre>
<ul>
<li><code>vptr</code>: 가상 포인터, 객체의 VTable을 가리킵니다.</li>
<li><code>VTable</code>: 가상함수와 가상 소멸자의 주소를 저장합니다.</li>
</ul>
<ol>
<li><code>Base</code> 클래스의 객체는 <code>Base VTable</code>을 가리키는 <code>vptr</code>를 가집니다.</li>
<li><code>Derived</code> 클래스의 객체는 <code>Derived VTable</code>을 가리키는 <code>vptr</code>를 가집니다.</li>
</ol>
<p><code>Derived::func1()</code>은 <code>Base::func1()</code>을 오버라이딩하므로, <code>Derived VTable</code>에서는 <code>Derived::func1()</code>의 주소가 저장됩니다.</p>
<p><code>Derived::func2()</code>는 <code>Derived</code> 클래스만의 고유한 가상함수이므로, <code>Derived VTable</code>에 추가됩니다. 이렇게 해서 다형성이 구현됩니다.</p>
<h3 id="다중-상속">다중 상속</h3>
<pre><code class="language-cpp">class Base0 {
public:
    virtual void func0() {}
};

class Base1 {
public:
    virtual void func1() {}
};

class Derived : public Base0, public Base1 {
public:
    virtual void func0() override {}
    virtual void func1() override {}
};</code></pre>
<p>이 예시에서 <code>Derived</code> 클래스는 <code>Base0</code>과 <code>Base1</code>을 상속받습니다. 따라서 <code>Derived</code> 객체는 두 개의 <code>vptr</code>를 가지며, 각각은 해당 기반 클래스의 <code>VTable</code>을 가리킵니다. 이렇게 다중 상속에서는 복잡한 메모리 레이아웃과 <code>VTable</code> 구조를 가질 수 있습니다.</p>
<pre><code class="language-cpp">[Derived Class]
+------------------+       +------------------+
|  vptr to Base0   | ----&gt; | Derived::func0() |
+------------------+       +------------------+
|  vptr to Base1   | ----&gt; | Derived::func1() |
+------------------+       +------------------+
|  Derived members |
+------------------+</code></pre>
<ul>
<li>각 기반 클래스마다 별도의 <code>vptr</code>가 필요합니다.</li>
<li>각 기반 클래스마다 별도의 <code>VTable</code>이 필요합니다.</li>
<li>객체를 특정 기반 클래스 타입으로 변환할 때 주소 조정이 필요할 수 있습니다.</li>
</ul>
<h4 id="다중-상속에서-주소-조정">다중 상속에서 주소 조정</h4>
<pre><code class="language-cpp">// For this:
Derived* d = new Derived;
Base1* b = d;

// the compiler will transform the code to (via vtable):
Base1* b = d + sizeof(Base0);</code></pre>
<p>다중 상속 상황에서, <code>Derived</code> 클래스의 객체를 <code>Base1</code> 클래스의 포인터로 변환하려면, 포인터의 주소를 <code>Base0</code>의 크기만큼 조정해야 합니다. 이런 조정은 런타임에 이루어지며, <code>function_thunk</code>가 이 역할을 합니다.</p>
<pre><code class="language-cpp">// then for a virtual function call needing pointer adjustment:
ptr-&gt;function();

// becomes:
ptr-&gt;__function_thunk(ptr);

__function_thunk:
    ptr += offset;
    function(ptr);</code></pre>
<p><code>function_thunk</code>는 &quot;thunk&quot; 함수의 한 예입니다. Thunk는 일반적으로 런타임에 주소나 포인터의 조정이 필요할 때 사용되는 작은 코드 조각입니다. 이 경우, <code>function_thunk</code>는 실제 가상 함수(<code>function</code>)를 호출하기 전에 필요한 포인터 조정을 수행합니다. 처리되는 객체의 유형이 컴파일 타임에 알려지지 않기 때문에 이러한 변환은 모두 런타임에 발생합니다.</p>
<h4 id="다중-상속-객체의-가상함수-테이블">다중 상속 객체의 가상함수 테이블</h4>
<pre><code class="language-cpp">                         +----------------+
                         | offset_to_top  |
Derived:                 +----------------+
  +-------------+        |  Derived RTTI  |
  | _vptr_Base0 |---+    +----------------+
  +-------------+   +--&gt; | Base0 virtuals |
  |     ...     |        +----------------+
  +-------------+        |      ...       |
  | _vptr_Base1 |---+    +----------------+
  +-------------+   |    | offset_to_top  |
  |     ...     |   |    +----------------+
  +-------------+   |    |  Derived RTTI  |
                    |    +----------------+
                    +--&gt; | Base1 virtuals |
                         +----------------+
                         |      ...       |
                         +----------------+</code></pre>
<p>다중 상속을 사용하는 <code>Derived</code> 클래스의 경우, 각 기반 클래스(<code>Base0</code>, <code>Base1</code> 등)의 VTable에는 해당 기반 클래스의 가상 함수들에 대한 정보가 들어가고, 추가로 <code>Derived</code>에서 오버라이드한 가상 함수에 대한 정보도 들어갈 수 있다는 점입니다.</p>
<p>예를 들어, <code>Derived</code> 클래스가 <code>Base0</code>의 <code>func0()</code>을 오버라이드하고, <code>Base1</code>의 <code>func1()</code>을 오버라이드했다면, <code>Derived</code> 클래스의 <code>Base0</code>과 <code>Base1</code>에 대한 VTable은 각각 <code>Derived::func0()</code>과 <code>Derived::func1()</code>에 대한 정보를 포함할 것입니다.</p>
<p>이렇게 각 VTable에는 <code>Derived</code> 클래스에서 오버라이드한 함수에 대한 정보가 들어가므로, 어떤 기반 클래스의 포인터로도 <code>Derived</code> 객체의 적절한 함수를 빠르게 호출할 수 있습니다.</p>
<h3 id="가상-상속">가상 상속</h3>
<p>가상 상속(virtual inheritance)의 주된 목적은 다중 상속을 사용할 때 &quot;다이아몬드 문제&quot;를 해결하는 것입니다. 다이아몬드 문제란, 두 개 이상의 클래스가 같은 부모 클래스를 상속받을 때, 그 부모 클래스의 멤버가 중복으로 존재하는 문제를 말합니다.</p>
<pre><code>    ClassA
    /     \
ClassB   ClassC
    \     /
    ClassD</code></pre><p>예를 들어, <code>ClassA</code>를 상속받는 <code>ClassB</code>와 <code>ClassC</code>가 있고, 이 두 클래스를 다시 상속받는 <code>ClassD</code>가 있다면, <code>ClassD</code>는 <code>ClassA</code>의 두 개의 인스턴스를 가지게 됩니다. 이렇게 되면, <code>ClassA</code>의 멤버에 접근할 때 어떤 인스턴스의 멤버에 접근해야 하는지 모호해집니다.</p>
<p>이 경우, <code>ClassD</code>의 메모리 레이아웃은 다음과 같이 두 개의 <code>ClassA</code> 인스턴스를 포함하게 됩니다.</p>
<pre><code>ClassD: [ ClassB [ ClassA ] ] [ ClassC [ ClassA ] ]</code></pre><p>가상 상속을 사용하면, 이러한 문제를 해결할 수 있습니다. 가상 상속을 통해 <code>ClassB</code>와 <code>ClassC</code>가 <code>ClassA</code>를 상속받으면, <code>ClassD</code>는 <code>ClassA</code>의 단 하나의 인스턴스만을 가지게 됩니다. 즉, <code>ClassA</code>의 멤버는 <code>ClassD</code> 내에서 단일 인스턴스로만 존재하게 되어, 모호함이 해결됩니다.</p>
<p>가상 상속을 사용하면, <code>ClassD</code>의 메모리 레이아웃은 다음과 같이 단 하나의 <code>ClassA</code> 인스턴스만을 포함하게 됩니다.</p>
<pre><code>ClassD: [ ClassB ] [ ClassC ] [ ClassA ]</code></pre><p>여기서 <code>[ ClassB ]</code>와 <code>[ ClassC ]</code>는 <code>ClassA</code>를 가상으로 상속받기 때문에, <code>ClassA</code>의 단일 인스턴스가 <code>ClassD</code>에 직접 포함됩니다. 이렇게 하면 <code>ClassA</code>의 멤버에 대한 모호함이 해결됩니다.</p>
<p>가상 기본 클래스가 하나만 있는 간단한 경우를 고려해 보겠습니다.</p>
<pre><code class="language-cpp">class Base {
public:
    ~Base();
    int p { 5 };
};

class Derived : virtual public Base {
public:
    ~Derived();
    int q { 7 };
};

Derived d;</code></pre>
<p>Derived 의 실제 메모리 레이아웃은 아래와 같습니다.</p>
<pre><code>(lldb) p sizeof(d)
(unsigned long) $0 = 16
(lldb) x/4w &amp;d
0x7fff5fbffb68: 0x00001028 0x00000001 0x00000007 0x00000005
                |                   | |        | |        |
                ..................... .......... ..........
                   virtual pointer        q          p</code></pre><p>위의 간략화된 메모리 레이아웃의 특성과 같이 부모 클래스의 비정적 데이터 멤버가 파생 클래스의 멤버 메모리뒤에 배치되는 것을 확인할 수 있습니다. </p>
<p>객체의 메모리 레이아웃은 컴파일 시점에 결정되며, 이는 컴파일러와 플랫폼, 그리고 클래스의 상속 구조에 따라 달라집니다. 가상 상속을 사용하는 경우, 일반적으로 가상 베이스 클래스의 멤버 변수들은 파생 클래스의 메모리 레이아웃에서 가장 아래에 위치하게 됩니다. 이는 가상 상속의 특성 때문에 가상 베이스 클래스의 멤버 변수들이 단 한 번만 인스턴스화되어야 하기 때문입니다.</p>
<blockquote>
<p>생성자 호출 순서와 메모리 레이아웃은 별개의 문제입니다. 생성자는 객체가 생성될 때 초기화 작업을 수행하는 역할을 하며, 메모리 레이아웃은 컴파일러가 결정합니다. 따라서, 생성자가 호출되는 순서와는 무관하게, 각 클래스의 멤버 변수는 컴파일러가 결정한 메모리 레이아웃에 따라 적재됩니다</p>
</blockquote>
<h4 id="vttvirtual-table-table">VTT(Virtual Table Table)</h4>
<p>VTT(Virtual Table Table)는 C++의 가상 상속에서 중요한 역할을 하는 데이터 구조입니다. VTT는 각 클래스의 생성자와 소멸자가 호출될 때 해당 클래스의 vptr(virtual pointer)가 올바른 vtable을 가리키도록 도와줍니다. 이는 다중 상속과 가상 상속이 복합적으로 이루어진 복잡한 상속 구조에서 특히 중요합니다.</p>
<p>VTT(Virtual Table Table)의 역할을 이해하기 위해 간단한 예를 들어보겠습니다. 다음과 같은 클래스 구조를 가정해봅시다.</p>
<pre><code class="language-cpp">class Base {
public:
    virtual void func() { /* ... */ }
};

class Derived1 : virtual public Base {
public:
    virtual void func() override { /* ... */ }
};

class Derived2 : virtual public Base {
public:
    virtual void func() override { /* ... */ }
};

class MostDerived : public Derived1, public Derived2 {
public:
    virtual void func() override { /* ... */ }
};</code></pre>
<p>이 경우, <code>MostDerived</code> 객체를 생성할 때 다음과 같은 과정이 일어납니다.</p>
<ol>
<li><code>Base</code>의 생성자가 호출됩니다. 이 때, <code>Base</code>의 vptr는 <code>Base</code>의 vtable을 가리킵니다.</li>
<li><code>Derived1</code>의 생성자가 호출됩니다. <code>Derived1</code>의 vptr는 <code>Derived1</code>의 vtable을 가리키도록 변경됩니다.</li>
<li><code>Derived2</code>의 생성자가 호출됩니다. <code>Derived2</code>의 vptr는 <code>Derived2</code>의 vtable을 가리키도록 변경됩니다.</li>
<li>마지막으로 <code>MostDerived</code>의 생성자가 호출됩니다. <code>MostDerived</code>의 vptr는 <code>MostDerived</code>의 vtable을 가리키도록 변경됩니다.</li>
</ol>
<p>이 과정에서 VTT는 각 단계에서 어떤 vtable을 가리켜야 하는지 정보를 제공합니다. 즉, 생성자와 소멸자가 호출될 때마다 VTT를 참조하여 vpointer를 올바른 vtable로 설정합니다.</p>
<p>VTT(Virtual Table Table)가 없다면, 다중 상속과 가상 상속이 복합적으로 사용될 때 문제가 발생할 수 있습니다. 예를 들어, 위와 같이 <code>MostDerived</code> 객체를 생성하는 상황에서  <code>Derived1</code>이 먼저 생성되었다고 가정하면, <code>Base</code> 클래스의 vptr(virtual pointer)는 <code>Derived1</code>의 vtable을 가리킬 것입니다.</p>
<p>이후에 <code>Derived2</code>의 생성자가 호출되면, <code>Derived2</code>의 vptr도 설정되어야 하지만, <code>Base</code>의 vptr는 이미 <code>Derived1</code>의 vtable을 가리키고 있을 것입니다. 이 상태에서 <code>MostDerived</code>의 생성자가 호출되면, <code>Base</code>의 vptr가 어떤 vtable을 가리켜야 할지 모호해집니다.</p>
<p>이러한 문제는 VTT를 사용함으로써 해결됩니다. VTT는 객체의 생성과 소멸 과정에서 각 클래스의 vptr가 올바른 vtable을 가리키도록 도와줍니다. </p>
<pre><code class="language-cpp">MostDerived 객체 메모리 레이아웃:

+-------------------+
| MostDerived::vptr | ----&gt; MostDerived::vtable
+-------------------+
| Derived1::vptr    | ----&gt; Derived1::vtable
+-------------------+
| Derived2::vptr    | ----&gt; Derived2::vtable
+-------------------+
| Base::vptr        | ----&gt; MostDerived::vtable (가상 상속 때문)
+-------------------+
| MostDerived 멤버  |
+-------------------+
| Derived1 멤버     |
+-------------------+
| Derived2 멤버     |
+-------------------+
| Base 멤버         |
+-------------------+</code></pre>
<p><code>MostDerived</code> 객체가 생성되는 과정에서 <code>Base</code>의 생성자가 호출되면, 이 시점에서 <code>Base</code>의 vptr는 <code>MostDerived</code>의 vtable을 가리키게 설정됩니다. 이렇게 하면, <code>MostDerived</code> 객체를 통해 <code>Base</code>의 가상 함수를 호출할 때 올바른 함수가 실행됩니다.</p>
<p>따라서, <code>MostDerived</code> 객체 내에서 <code>Base</code>의 vptr는 <code>MostDerived</code>의 vtable을 가리키게 됩니다. 이는 VTT(Virtual Table Table)를 통해 관리될 수 있으며, 이로 인해 다중 상속과 가상 상속이 복합적으로 사용될 때도 가상 함수 호출이 올바르게 작동합니다. 이렇게 하면, <code>Base</code>의 vptr는 최종적으로 <code>MostDerived</code>의 vtable을 가리키게 됩니다.</p>
<p>따라서, VTT가 없으면 다중 상속과 가상 상속이 복합적으로 사용될 때 vptr 설정이 제대로 이루어지지 않아, 가상 함수 호출 등에서 문제가 발생할 수 있습니다.</p>
<p>이렇게 VTT는 복잡한 상속 구조에서도 객체가 올바르게 생성되고 소멸될 수 있도록 도와줍니다. 이는 컴파일러가 자동으로 관리하는 부분이므로 일반적으로 개발자가 직접 다루지는 않습니다.</p>
<h3 id="참고자료">참고자료</h3>
<ul>
<li><a href="https://anderberg.me/2016/06/26/c-virtual-functions/">https://anderberg.me/2016/06/26/c-virtual-functions/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C/C++] Event 구현하기]]></title>
            <link>https://velog.io/@will-big/Event-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@will-big/Event-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 02 Sep 2023 16:13:24 GMT</pubDate>
            <description><![CDATA[<h2 id="설명하기-앞서">설명하기 앞서</h2>
<p>내 코드에서는 Event 를 관리하는 클래스를 Event System 이라고 이름은 붙였지만 Event Manager 와 같이 다양하게 불릴 뿐 역할은 모두가 동일하다고 본다. 이 포스트를 작성하기 전까지 C# 에도 Event 가 있는 줄 몰랐는데 포스트 작성을 위해 Event 관련 자료를 찾아보니 C# 의 기능까지 확인하게 되었다. 내가 필요한 기능만 적당히 구현해놓은 Event System 과는 달리 좀더 세분화되고 사용하기 좋게 되어있던 것 같아서 내가 만든 Event System 을 개선할때 참고하려고 한다. (참고로 Microsoft C++ 에도 <a href="https://learn.microsoft.com/ko-kr/cpp/cpp/event?view=msvc-170"><code>__event</code></a> 라는 키워드가 존재하는데 아직 공부한 영역이 아니라서 자세히 보진 않았지만 비슷한 기능을 하는 듯 하다.)</p>
<blockquote>
<p>처음 구현해본 Event System 로써 아직 보완할 점이 상당히 많이 보인다. 멀티스레드로도 사용할 수 있을 것 같고, 사용 편의성을 더욱 늘릴 필요가 있을 것 같으며(C# 스타일의 이벤트 처럼) 외부에서 이벤트를 등록/제거 하기 쉽게 만들 필요가 있는 것 같다(현재는 자신의 클래스에 국한하여 이벤트를 처리하는 방식). 여유가 있다면 이 형태에서 점점 개선하여 사용할 예정이다.</p>
</blockquote>
<h2 id="event-란--사용하는-이유">Event 란 + 사용하는 이유</h2>
<p>내가 받아들인 Event 는 핵심만 말하자면 특정한 Event 가 발생했을 때, 그 Event 를 구독한 객체가 그에 맞는 행동을 하는 것이다. 여기서 Event 는 개별적으로 구분이 가능한 무엇이든 상관이 없다. 내 엔진의 특성상 <code>wstring</code>을 사용할 일이 많아서 <a href="https://en.cppreference.com/w/cpp/string/basic_string_view"><code>wstring_view</code></a> 객체로 사용하였으나 <code>int</code> , <code>string</code> 등 고유한 값을 가지고 있는 것이면 무엇이든 가능하다 (사실 고유하지 않아도 되지만 그러면 의도치 않은 일이 벌어질 수 있을테니).
그렇다면 <em>&#39;Event 를 왜 쓰는걸까?&#39;</em> 라는 의문이 생길 수 있다. 내가 받아들인 Event 의 가장 큰 장점은, 클래스간의 결합도를 낮추는 것이라고 생각한다.
단적인 예를 들어보겠다. 똑같은 방식으로 생성된 몬스터 100 마리가 있을 때, 그 중 68 번째로 생성된 몬스터에게 특별한 의미를 부여하려고 한다. 그 몬스터가 사망했을 때 플레이어에게 500 골드를 주는 것이다. 가장 직관적으로 구현한다고 하면 68번째 생성된 몬스터를 그것을 관리하는 시스템 차원의 클래스에서 직접 들고 있다가, 그 몬스터가 죽었는지 계속 확인하면서 죽는다면 우리가 원하는 행위를 수행하는 것이다. 글로는 쉽게 설명했지만 직접 구현하다보면 이런 생각이 들며 머리가 아파오기 시작할 것 같다. <em>&#39;굳이 이 한마리 때문에 이걸 멤버 변수로 들고 있어야 한다고?&#39;</em> 게다가 필드에 몬스터가 이 한 종류 뿐이라는 보장도 없다. 몬스터의 종류는 수도 없이 많을텐데 그리고 발생하는 행위가 모두 같다는 보장도 없기 때문에 개별적인 모든 것을 위해서 멤버변수를 하나하나 들고 있자니 벌써 하기가 싫어진다.
이런 문제를 Event 로 쉽게 해결이 가능하다. 객체가 Event 를 받았을 때, 해야할 일을 객체에게 <strong>등록</strong> 해주면 되기 때문이다. 위의 예를 보자면, 68 번째 몬스터에게 <strong>&quot;사망시 플레이어에게 500골드 지급&quot;</strong> 이라는 함수를 구현해서 넣어주는 것이다. <strong>등록</strong> 이라는 표현을 사용했는데, 이에 대한 구현 방식은 정말 개인의 자유인 것 같다. 본론으로 돌아가서, 위와 같이 Event 를 사용한다면 관리자 클래스에서 몬스터 객체를 개별적으로 알 필요가 없게 되어 클래스간 결합도가 떨어지는 결과를 얻을 수 있다. 당연히 플레이어에게 500골드를 주는 행위 조차도 Event 로 처리한다면 몬스터가 플레이어도 알 필요가 없기에 같은 맥락으로 볼 수 있다.</p>
<h2 id="코드-및-설명">코드 및 설명</h2>
<h3 id="코드-진행-방식">코드 진행 방식</h3>
<blockquote>
</blockquote>
<ol start="0">
<li>특정 <code>EventID</code> 를 받았을 때 반응하고 싶은 클래스가 <code>EventHandler</code> 클래스를 다중 상속을 받은 후, 자신의 <strong>멤버 함수</strong>를 <code>EventFunction</code> 시그니처 <code>std::function&lt;void(std::any)&gt;</code> 에 맞춰서 특정 <code>EventID</code> 에 반응하는 함수를 <code>EventHandler::MakeListenerInfo</code> 함수의 인자로 넣는다.</li>
<li><code>EventID</code> 를 발행하고 싶은 객체가 <code>EventSystem</code> 의 싱글톤 객체에 접근하여 <code>EventSystem::PublishEvent</code> 을 사용한다.</li>
<li><code>EventSystem::m_CustomEvents</code> 에 <strong>(1)</strong> 에서 발행된 <code>Event</code> 객체가 <strong>push_back</strong> 되고, 해당 프레임의 마지막(실행 주기 상 <code>EventSystem</code> 이 가장 마지막에 위치함)에 <code>EventSystem::Update -&gt; EventSystem::ProcessEvent</code> 을 통해 <code>Event</code> 객체에 대한 처리를 한다. 기본적으로 <code>m_CustomEvents</code> 컨테이너는 선입선출의 형태(<strong>Queue</strong>)를 가지기 위해 먼저 들어온 <code>Event</code> 객체부터 처리하지만, <strong>지연 시간</strong>이 존재하는 <code>Event</code> 객체는 해당 시간이 만료될 때 까지 나중에 들어온 <code>Event</code> 객체보다 늦게 처리된다.</li>
<li>객체의 실행 조건(<strong>지연 시간 만료</strong>)이 만족하면 <code>EventSystem::ProcessEvent</code> 에는 내부적으로 <code>EventSystem::DispatchEvent</code> 가 실행되는데, 이 함수가 실제로 <code>EventFunction</code> 을 실행하는 부분이다.</li>
<li><code>EventSystem::DispatchEvent</code> 에서 <code>EventFunction</code> 이 실행되면, <strong>(0)</strong> 에서 등록한 함수가 실행된다. (실제 코드는 사용 예시 참조)</li>
</ol>
<h3 id="eventsystemh">EventSystem.h</h3>
<pre><code class="language-cpp">#pragma once

#include &lt;functional&gt;
#include &lt;queue&gt;
#include &lt;map&gt;
#include &lt;string_view&gt;
#include &lt;any&gt;

namespace McCol
{
    class EventHandler;

    struct Event
    {
        std::wstring_view EventID;                        // 이벤트명
        std::any Parameter;                                // 매개변수(필요시 적절한 캐스팅으로 사용)
        float DelayedTime;                                // 지연 시간(기본값 0)

        Event(std::wstring_view id, std::any param, const float&amp; delayed)
            : EventID(id), Parameter(param), DelayedTime(delayed) {}
    };

    struct ListenerInfo
    {
        const EventHandler* Listener = nullptr;                // 이벤트를 받을 객체(dangling 포인터를 방지하기 위한 객체 검사용, 실제 사용 X)
        std::function&lt;void(std::any)&gt; EventFunction;        // 이벤트를 받을 시 수행할 함수

        ListenerInfo(const EventHandler* listener, const std::function&lt;void(std::any)&gt;&amp; func)
            : Listener(listener), EventFunction(func) {}
    };

    class EventSystem
    {
    private:
        EventSystem();
        ~EventSystem() = default;

    private:
        static EventSystem* m_Instance;
        std::vector&lt;Event&gt; m_CustomEvents;                        // 사용자 정의 이벤트
        std::multimap&lt;std::wstring_view, ListenerInfo&gt; m_Listeners;        // 이벤트 구독자 정보 컨테이너

    public:
        static EventSystem* GetInstance()
        {
            if (m_Instance == nullptr)
                m_Instance = new EventSystem;

            return m_Instance;
        }

        void Initialize();
        void Update(const float&amp; deltaTime);
        void Finalize();

    public:
        // 이벤트 발행
        void PublishEvent(std::wstring_view evtID, const std::any&amp; param = nullptr, const float&amp; delayed = 0.0f);
        void Subscribe(std::wstring_view evtID, const ListenerInfo&amp; listenerInfo);            // 이벤트 구독

        void Unsubscribe(std::wstring_view evtID, const EventHandler* listener);            // 해당 이벤트 구독 해지
        void RemoveListener(const EventHandler* listener);                                    // 해당 리스너가 구독한 모든 구독 취소
        void RemoveListenersAtEvent(std::wstring_view evtID);                                // 해당 이벤트 모든 구독자 제거
        void RemoveAllEvents();                                                                // 모든 이벤트 제거
        void RemoveAllSubscribes();                                                            // 모든 구독 취소

    private: // 내부 함수
        void ProcessEvent(const float&amp; deltaTime);                                            // 이벤트 큐 확인
        void DispatchEvent(const Event&amp; evt);                                                // 이벤트 처리
        bool CheckSubscribe(std::wstring_view evtID, const EventHandler* listener);            // 이벤트 중복 구독 확인
    };
}</code></pre>
<blockquote>
<p><strong>Event</strong></p>
</blockquote>
<ul>
<li><code>EventSystem</code> 안에서만 사용되는 구조체로, 구성요소는 식별하기 위한 <code>EventID</code> 값, <code>Event</code> 를 발행하는 객체가 발행할 때 해당 <code>Event</code> 를 받는 객체에서 필요로 하는 인자를 전달하기 위한 <code>Parameter</code> , 원하는 시간 뒤에 수행하기 위한 <code>DelayedTime</code> 이다.</li>
<li>세가지 멤버 변수 모두 <code>Event</code> 객체를 이루는 필수적인 요소이기 때문에 생성자로 모두 받고 있다.</li>
</ul>
<blockquote>
<p><strong>ListenerInfo</strong></p>
</blockquote>
<ul>
<li>마찬가지로 <code>EventSystem</code> 안에서만 사용되는 구조체. 하지만 생성은 <code>Event</code> 를 구독하는 객체가 직접하고 <code>EventSystem</code> 이 그걸 넘겨받는 형식이다. 구성요소는 <code>Event</code> 를 받을 객체의 주소값 <code>Listener</code> , 특정 <code>Event</code> 를 받을 시 수행하는 함수 <code>EventFunction</code> (문법 참조 : <a href="https://en.cppreference.com/w/cpp/utility/functional/function">std::function</a>) 이 있다.</li>
<li><code>Listener</code> 는 함수를 수행하는데 사실 필요가 없지만, <code>dangling pointer</code> 를 방지하기 위해 저장한다. 내가 찾아본 바에 의하면 <code>std::function</code> 에는 함수 주인의 정보를 알 수 없었다. 따라서 주인의 주소값 <code>Listener</code>  을 통해 <code>std::function</code> 과 생명 주기를 동일시 하기 위해 사용한다. 즉 주인이 사라지면 연결된 <code>std::function</code> 도 함께 삭제하기 위함이다. 실제로 사용되는 방식은 <code>EventHandler.h</code> 의 소멸자를 확인하길 바란다.</li>
<li><code>EventFunction</code> 의 타입으로 <code>std::function</code> 을 사용한 이유는 함수를 하나의 객체처럼 다루기 위해서이다.  <code>EventSystem</code> 에서 함수를 관리 할 때 함수에 수행 명령을 내리는 것은 <code>EventSystem</code> 이기 때문에 하나의 객체처럼 저장할 수 있어야 하는데 이 방법을 위한 적절한 C++ 문법이 <code>std::function</code> 이었다.</li>
<li>형태가 <code>std::function&lt;void(std::any)&gt;</code> 인 이유 : 내 <code>EventSystem</code>  특성상 해당 <code>EventID</code> 를 <strong>Subscribe(구독)</strong> 한 객체가 설정한 함수 <code>EventFunction</code> 이 수행되었을 때, 객체는 그 <code>EventID</code>  를 알 필요가 없다. 이미 등록한 함수 <code>EventFunction</code> 이 수행되었다는 것 부터가 그 함수에 대응하는 <code>EventID</code> 가 <strong>Publish(발행)</strong> 되었다는 뜻이기 때문이다. (<strong>Subscribe</strong>, <strong>Publish</strong> 에 대한 설명은 EventSystem.cpp 에서 추가 후술) 그렇다면 인자가 굳이 <code>std::any</code> 일 필요가 있나 싶겠지만 <code>EventFunction</code> 중 특정 함수는 매개변수를 반드시 필요로 하는 함수가 있을 수 있기 때문에 그 매개변수를 <code>std::any</code> 로 지정하여 어떤 타입이든 올 수 있게 한 것이다. 하지만 인자로 들어 올 수 있는 것은 하나 뿐이기에 인자로 넘겨야 할 것이 여러개라면 구조체화 해서 넘겨야 한다는 단점이 있다. 반환형이 <code>void</code> 인 이유는 단순하다. 내가 생각하기에 <code>EventFunction</code> 이 무언가를 반환할 이유가 없었고 함수 수행은 해당 객체가 하지만 수행 명령을 내리는 객체는 <code>EventSystem</code> 이기 때문이다.</li>
</ul>
<blockquote>
<p><strong>EventSystem Member Variable</strong></p>
</blockquote>
<ul>
<li><code>m_Instance</code> : <code>Event</code> 를 <strong>Subscribe</strong> 하거나 <strong>Publish</strong> 하는 행위는 프로그램 전체에서 광범위하게 일어나기 때문에 사용 편의성을 위해서 Singleton Pattern 을 사용하였다.</li>
<li><code>m_CustonEvents</code> : 사용자가 정의한 <code>Event</code> 가 저장되는 컨테이너. <code>Event</code> 는 <strong>ProcessEvent</strong> 라는 내부 함수가 수행될 때 마다 모두 비워지는게 맞기 때문에 처음에는 <code>std::vector</code> 가 아닌 <code>std::queue</code> 였으나 지연 처리 <code>DelayedTime</code> 기능을 추가하게 되면서 특정 <code>Event</code> 만 처리되어야 했고, <strong>pop</strong> 방식을 사용할 수 없게 되었기 때문에 변경하게 되었다.</li>
<li><code>m_Listeners</code> : 구독자(<strong>Subscribe</strong> 한 객체들) 정보를 담는 컨테이너(<a href="https://en.cppreference.com/w/cpp/container/multimap">std::multimap</a>). 구독자 정보이기 때문에 <code>multimap&lt;std::wstring_view, ListenerInfo&gt;</code> 타입을 갖고 있는데 여기서 <code>wstring_view</code> 는 <code>Event::EventID</code> 이다. </li>
<li>각 멤버 함수에 대한 개별적인 설명은 cpp 에서 후술.</li>
</ul>
<h3 id="eventsystemcpp">EventSystem.cpp</h3>
<pre><code class="language-cpp">#include &quot;pch.h&quot;
#include &quot;EventSystem.h&quot;

McCol::EventSystem* McCol::EventSystem::m_Instance = nullptr;

McCol::EventSystem::EventSystem()
{
}

void McCol::EventSystem::Initialize()
{
}

void McCol::EventSystem::Update(const float&amp; deltaTime)
{
    ProcessEvent(deltaTime);
}

void McCol::EventSystem::Finalize()
{
    RemoveAllEvents();
    RemoveAllSubscribes();

    SAFE_DELETE(m_Instance)
}

void McCol::EventSystem::PublishEvent(std::wstring_view evtID, const std::any&amp; param, const float&amp; delayed)
{
    m_CustomEvents.push_back({evtID, param, delayed});
}

void McCol::EventSystem::Subscribe(std::wstring_view evtID, const ListenerInfo&amp; listenerInfo)
{
    // 유효성 및 중복 검사
    if(listenerInfo.Listener == nullptr || CheckSubscribe(evtID, listenerInfo.Listener))
    {
        return;
    }

    m_Listeners.emplace(evtID, listenerInfo);
}

void McCol::EventSystem::Unsubscribe(std::wstring_view evtID, const EventHandler* listener)
{
    auto [first, last] = m_Listeners.equal_range(evtID);

    for(auto&amp; it = first; it != last;)
    {
        if(it-&gt;second.Listener == listener)
        {
            it = m_Listeners.erase(it);
        }
        else
        {
            ++it;
        }
    }
}

void McCol::EventSystem::RemoveListener(const EventHandler* listener)
{
    // info : EventHandler 를 상속받은 객체가 소멸될 경우 자동으로 호출되는 함수
    // info : listener 의 구독목록을 받아온다면 성능상 개선의 여지가 있음, 현재는 O(N)
    for(auto it = m_Listeners.begin(); it != m_Listeners.end();)
    {
        if(it-&gt;second.Listener == listener)
        {
            it = m_Listeners.erase(it);
        }
        else
        {
            ++it;
        }
    }
}

void McCol::EventSystem::RemoveListenersAtEvent(std::wstring_view evtID)
{
    auto [first, last] = m_Listeners.equal_range(evtID);

    for (auto&amp; it = first; it != last;)
    {
        it = m_Listeners.erase(it);
    }
}

void McCol::EventSystem::RemoveAllEvents()
{
    while(!m_CustomEvents.empty())
    {
        m_CustomEvents.clear();
    }
}

void McCol::EventSystem::RemoveAllSubscribes()
{
    m_Listeners.clear();
}

void McCol::EventSystem::ProcessEvent(const float&amp; deltaTime)
{
    std::vector&lt;McCol::Event&gt; tempEvents = m_CustomEvents;
    m_CustomEvents.clear();

    for (auto&amp; evt : tempEvents)
    {
        evt.DelayedTime -= deltaTime;

        if (evt.DelayedTime &lt;= 0)
        {
            DispatchEvent(evt);
        }
        else
        {
            m_CustomEvents.push_back(evt);
        }
    }
}

void McCol::EventSystem::DispatchEvent(const McCol::Event&amp; evt)
{
    auto [first, last] = m_Listeners.equal_range(evt.EventID);

    for (auto&amp; it = first; it != last; ++it)
    {
        // 이벤트 함수 호출
        // EventFunction : 해당 이벤트를 구독한 Listener 가 실행할 함수
        // evt.Parameter : 해당 이벤트에 대한 매개변수
        it-&gt;second.EventFunction(evt.Parameter);
    }
}

bool McCol::EventSystem::CheckSubscribe(std::wstring_view evtID, const EventHandler* listener)
{
    auto [first, last] = m_Listeners.equal_range(evtID);

    for (auto&amp; it = first; it != last; ++it)
    {
        if (it-&gt;second.Listener == listener)
        {
            return true;
        }
    }

    return false;
}
</code></pre>
<blockquote>
<p><strong>EventSystem Member Function</strong></p>
</blockquote>
<ul>
<li><code>EventSystem::EventSystem</code> : 생성자에서는 딱히 해줄 일이 없다.</li>
<li><code>EventSystem::Initialize</code> : 초기화에서도 딱히 해줄 일이 없다. (시스템 관련 함수의 모양새를 맞추기 위해 있는 함수)</li>
<li><code>EventSystem::Update</code> : 매 프레임(실행 주기)마다 이벤트 처리 함수 <code>ProcessEvent</code> 를 수행한다.</li>
<li><code>EventSystem::Finalize</code> : 시스템이 종료(<strong>Finalize</strong>)된다는 것은 게임이 끝난 다는 것이기 때문에 모든 이벤트를 삭제하고, 모든 구독자를 삭제한다. 이후 싱글톤 객체를 삭제한다. <code>SAFE_DELETE</code> 는 단순히 들어온 포인터를 <code>delete</code> 하고 <code>nullptr</code> 로 바꾸어주는 매크로(<code>pch.h</code> 에 정의됨). (소멸자에서 처리해도 되지만 시스템 관련 함수의 모양새를 맞추기 위해 있는 함수)</li>
<li><code>EventSystem::PublishEvent</code> : 외부에서 싱글톤 객체에 접근하여 사용하는 함수로 <code>Event</code> 를 <strong>발행</strong>할 때 사용한다. 인자로 <strong>식별값(ID), 파라미터, 지연 시간</strong>을 가지며 <strong>지연 시간</strong>은 기본 값으로 <code>0.0f</code> 를 갖는다. 인자는 모두 <code>Event</code> 객체의 생성자에 그대로 들어간다. 생성된 <code>Event</code> 객체는 <code>m_CustomEvents</code> 에 <strong>push_back</strong> 으로 들어가기 때문에 복사되어 들어간다. 복사라는 단점이 있음에도 불구하고 <strong>emplace_back</strong> 대신 <strong>push_back</strong> 을 사용한 이유는 참조 형식으로 받아온 <code>param</code> 객체의 수명이 불분명하기 때문에 <code>Event</code> 가 실제로 수행 될 때 까지 <code>param</code> 객체가 존재한다는 보장이 없기 때문이다.</li>
<li><code>EventSystem::Subscribe</code> : 외부에서 싱글톤 객체에 접근하여 사용하는 함수로 <code>Event</code> 를 <strong>구독</strong>할 때 사용한다. 중복 구독할 이유는 없기 때문에 유효성과 중복 검사를 해주고 문제가 없다면 <code>m_Listeners</code> 에 넣어준다.</li>
<li><code>EventSystem::ProcessEvent</code> : 지연 시간을 확인하여 적절한 <code>Event</code> 객체들을 처리하는 함수. <strong><a href="https://en.cppreference.com/w/cpp/language/range-for">range-based for loop</a></strong> 을 사용하여 순회하는데, 순회 함과 동시에 조건이 만족하는 <code>Event</code> 객체는 <code>EventSystem::DispatchEvent</code> 로 넘겨서 실행하게 된다. 실행 도중 <code>Event</code> 를 생성하는 <code>EventFunction</code> 이 존재 할 수 있으므로 <code>EventSystem::Pusblish</code> 를 통해 <code>m_CustonEvents</code> 에 새로운 <code>Event</code> 객체가 추가 될 수 있다. 이로 인해 <strong>for loop</strong> 순회에 문제가 생기게 된다. 따라서 <code>m_CustomEvents</code> 를 <code>tempEvents</code> 에 복사하여 생성하고 <code>m_CustomEvents</code> 는 <strong>clear</strong> 를 통해 비워준다. 그 뒤 <code>tempEvents</code> 를 순회하며 <code>DelayedTime</code> 을 <code>deltaTime</code> 만큼  빼주고 <code>DelayedTime</code> 의 값을 확인해가며 <strong>DispatchEvent</strong> 의 수행 여부를 결정한다. 수행하지 않는다면 다시 <code>m_CustomEvents</code> 에 넣어주어 다음 프레임에 같은 행위를 반복하도록 한다. 함수가 종료되면 <code>tempEvents</code> 는 수명이 만료되어 삭제되고 <code>m_CustomEvents</code> 에는 아직 지연 시간이 남은 <code>Event</code> 객체만 유지되게 된다.</li>
<li><code>EventSystem::DispatchEvent</code> : <code>m_Listeners</code> 의 <strong>equal_range</strong> 를 사용하여 <strong>key</strong> 값(<code>evt.EventID</code>) 이 일치하는 <code>std::pair&lt;wstring_view, ListenerInfo&gt;</code> 객체를 가르키는 <code>iterator</code> 를 <code>[first, last]</code> 형태로 가져온다. 이후 <code>first ~ last</code> 를 순회하며 해당 <code>iterator.second</code> 의 <code>EventFunction</code> 에 매개변수로 들어온 <code>evt.Parameter</code> 를 넣어주고 실행한다.</li>
<li>이외 함수 : <code>EventSystem.h</code> 함수 주석 참조</li>
</ul>
<h3 id="eventhandlerh">EventHandler.h</h3>
<pre><code class="language-cpp">#pragma once
#include &lt;functional&gt;
#include &quot;EventSystem.h&quot;

namespace McCol
{
    class EventHandler
    {
    public:
        // EventHandler 로 등록된 모든 이벤트를 제거
        virtual ~EventHandler()
        {
            EventSystem::GetInstance()-&gt;RemoveListener(this);
        }

        // EventSystem 에 등록하기 위한 Callable 생성
        template &lt;typename T&gt;
        ListenerInfo MakeListenerInfo(void (T::* func)(std::any))
        {
            // 유효성 검사
            static_assert(std::is_base_of_v&lt;EventHandler, T&gt;, &quot;T must be derived from EventHandler&quot;);
            // Callable 반환 (인자 설명은 ListenerInfo 구조체 참조)
            return ListenerInfo(this, [this, func](std::any handler) { (static_cast&lt;T*&gt;(this)-&gt;*func)(handler); });
        }
    };
}
</code></pre>
<blockquote>
<p><strong>EventHandler Member Function</strong></p>
</blockquote>
<ul>
<li><code>EventHandler::~EventHandler</code> : <code>EventSystem</code> 싱글톤 객체에 접근하여 이 객체로 등록된 모든 <code>ListenerInfo</code> 를 제거한다.</li>
<li><code>EventHandler::MakeListenerInfo</code> : <code>ListenerInfo</code> 객체를 생성한다. <code>[this, func](std::any handler) { (static_cast&lt;T*&gt;(this)-&gt;*func)(handler); }</code> 부분은 람다식으로 <strong>[ ]</strong> 캡처 부분에서 <code>this</code> 는 함수를 수행하는 객체를, <code>func</code> 는 수행되는 멤버 함수를 나타내며 <code>(std::any handler)</code> 는 <code>handler</code> 라는 <code>std::any</code> 타입의 매개변수를 받아오는 것을 의미한다. 그리고 이를 이용하여 람다식의 본문인 <code>{ (static_cast&lt;T*&gt;(this)-&gt;*func)(handler); }</code> 부분을 작성한다. 본문은 <code>this</code> 를 <code>T*</code> 로 캐스팅하여 사용하는데 그 이유는 <code>MakeListenerInfo</code> 함수가 템플릿 함수이기 때문이다. <code>this</code> 는 <code>EventHandler</code> 객체를 가르키므로 <code>func</code> 를 알 수가 없기 때문에 이를 캐스팅하여 <code>func</code> 를 수행할 수 있도록 한다. 또한 매개변수로 <code>handler</code> 를 넣어준다. 이 함수가 템플릿 함수인 이유는 어떤 클래스던 간에 해당 클래스가 <code>Event</code> 를 구독할 필요가 있다면 <code>EventHandler</code> 클래스를 상속받아 <code>MakeListenerInfo</code> 를 사용할 수 있도록 하기 위해서이다.</li>
</ul>
<h2 id="사용-예시">사용 예시</h2>
<pre><code class="language-cpp">#include &quot;EventSystem.h&quot;
#include &lt;iostream&gt;

class Player : public McCol::EventHandler
{
public:
    Player()
    {
        McCol::EventSystem::GetInstance()-&gt;Subscribe(L&quot;PlayerDied&quot;, MakeListenerInfo(&amp;Player::OnPlayerDied));
    }

    void OnPlayerDied(std::any data)
    {
        int lives = std::any_cast&lt;int&gt;(data);
        std::wcout &lt;&lt; L&quot;Player died with &quot; &lt;&lt; lives &lt;&lt; L&quot; lives remaining.&quot; &lt;&lt; std::endl;
        if (lives &lt;= 0)
        {
            McCol::EventSystem::GetInstance()-&gt;PublishEvent(L&quot;GameOver&quot;);
        }
    }
};

class Game : public McCol::EventHandler
{
public:
    Game()
    {
        McCol::EventSystem::GetInstance()-&gt;Subscribe(L&quot;GameOver&quot;, MakeListenerInfo(&amp;Game::OnGameOver));
    }

    void OnGameOver(std::any data)
    {
        running = false;
    }

    void Run()
    {
        int lives = 3;
        running = true;
        while (running)
        {
            McCol::EventSystem::GetInstance()-&gt;Update(0.0f);
            // 게임 로직 (생략)
            // ...
            lives--;
            McCol::EventSystem::GetInstance()-&gt;PublishEvent(L&quot;PlayerDied&quot;, lives);
        }
        std::wcout &lt;&lt; L&quot;Game Over!&quot; &lt;&lt; std::endl;
    }

private:
    bool running;
};

int main()
{
    McCol::EventSystem::GetInstance()-&gt;Initialize();

    Player player;
    Game game;

    game.Run();

    McCol::EventSystem::GetInstance()-&gt;Finalize();

    return 0;
}</code></pre>
<blockquote>
<p>직접 사용한 코드를 가져오려고 했으나 해당 이벤트를 처리를 이해하는데 수반되는 코드가 많기 때문에 이벤트의 처리 과정을 보여주는데는 적합하지 않다고 판단되어 따로 만들었다. 편의상 이곳의 <code>EventSystem::Update</code> 의 인자로 <code>0.0f</code> 를 주었지만 원래는 지연 시간 처리를 위해 게임 엔진상의 실제 델타 타임을 주는게 옳다. 위에서 언급한 <strong>코드 진행 방식</strong>에 기반하여 설명하자면 <code>Player</code> 가 구독한 <code>EventID</code> 를 <code>Game</code> 에서 발행하는 형식이다. 포스트의 도입부에서 이야기 했던 <code>Event</code> 의 필요성에서 나온 사용 방향과는 조금 다르지만 이렇게도 사용할 수 있고 다른 방식으로도 얼마든지 응용이 가능하다. 모두 같은 결론을 도출해 낼 수 있는데, <strong>객체간 의존성</strong>이 적어진다는 것이다!</p>
</blockquote>
<blockquote>
</blockquote>
<ol start="0">
<li><code>Player</code> 생성자에서 <code>L&quot;PlayerDied&quot;</code> 를 구독하고 실행할 함수를 지정한다.</li>
<li><code>Game</code> 생성자에서 <code>L&quot;GameOver&quot;</code> 를 구독하고 실행할 함수를 지정한다.</li>
<li>매 실행 주기(프레임)마다 <code>Game::Run</code> 이 실행되고 <code>lives</code> 값이 줄어들며 이에 해당하는 <code>L&quot;PlayerDied&quot;</code> 를 발행하며 인자로 남은 <code>lives</code> 를 넘겨준다.</li>
<li><code>Player::OnPlayerDied</code> 에서 이 함수가 수행 될 때마다 인자로 받은 남은 <code>lives</code> 를 출력한다.</li>
<li>출력 도중 <code>lives</code> 가 <code>if (lives &lt;= 0)</code> 조건을 만족하면 <code>L&quot;GameOver&quot;</code> 를 발행한다.</li>
<li><code>Game::OnGameOver</code> 함수가 <code>L&quot;GameOver&quot;</code> 를 받고 자신이 속한 객체의 <code>running</code> 을 <code>false</code> 로 변경하여 게임을 종료한다.</li>
</ol>
<h2 id="참고-자료">참고 자료</h2>
<p>Youtube : <a href="https://www.youtube.com/watch?v=2E98LkckmYk&amp;t=2396s&amp;ab_channel=BruceRiggs">C++ - Event System</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 정적 링크와 동적 링크]]></title>
            <link>https://velog.io/@will-big/TIL-%EC%A0%95%EC%A0%81-%EB%A7%81%ED%81%AC%EC%99%80-%EB%8F%99%EC%A0%81-%EB%A7%81%ED%81%AC</link>
            <guid>https://velog.io/@will-big/TIL-%EC%A0%95%EC%A0%81-%EB%A7%81%ED%81%AC%EC%99%80-%EB%8F%99%EC%A0%81-%EB%A7%81%ED%81%AC</guid>
            <pubDate>Wed, 07 Jun 2023 06:44:50 GMT</pubDate>
            <description><![CDATA[<p>공부 중 정적 링크(Static Link)와 동적 링크(Dynamic Link)의 차이에 대해 갈피를 잡기가 어려워서 조금 더 찾아보았다.
ChatGPT 를 통해 습득한 정보라 정확하지 않을 수 있지만 내가 이해한 내용은 아래와 같다.</p>
<h3 id="정적-링크-예시">정적 링크 예시</h3>
<pre><code class="language-cpp">// MyClass.h
class MyClass {
private:
    int m_garbage1;
    int m_garbage2;

public:
    void MyFunction();
    void GarbageFunction1();
    void GarbageFunction2();
};

// main.cpp
#include &quot;MyClass.h&quot;

int main() {
    MyClass obj;
    obj.MyFunction();
    return 0;
}</code></pre>
<h3 id="동적-링크-예시">동적 링크 예시</h3>
<pre><code class="language-cpp">#include &lt;opencv2/opencv.hpp&gt;

int main() {
    cv::Mat image = cv::imread(&quot;image.jpg&quot;, cv::IMREAD_COLOR);
    cv::imshow(&quot;Image&quot;, image);
    cv::waitKey(0);
    return 0;
}</code></pre>
<p>정적 링크는 컴파일 단계에서 <strong>내가 필요한 코드가 있는 파일 전체를</strong> 포함하는 것이다.
동적 링크는 런타임 단계에서 <strong>내가 필요한 코드 부분만</strong> 포함하는 것이다.</p>
<p>위 코드로 정적 링크의 예를 들면 다음과 같다.
나는 <em>MyClass</em> 의 <em>MyFunction</em> 만 실행하고 싶은데 <em>MyFunction</em> 뿐만 아니라 MyClass 에 있는 모든 변수나 함수(<em>m</em>garbage_, <em>GarbageFunction</em>)들이 포함되어 메모리의 낭비가 발생한다.</p>
<p>동적 링크는 정적 링크와 반대 되는 상황이라고 생각하면 된다. 내가 opencv 를 사용하지 않기 때문에 opencv 가 일반적으로 동적 링크 되는지는 알지 못하지만 동적 링크로 사용된다고 가정하겠다. <em>cv::imread</em> 할 때, <em>cv::imshow</em> 할 때, <em>cv::waitKey</em> 를 수행할 때 마다 각각 함수 실행에 필요한 코드 영역들이 메모리 상에 로드된 후 수행이 끝나면 해제되는 것이 동적 링크가 가지는 차이점이다. 동적 링크는 운영체제 단계에서 관리하므로 정확히 언제 해제하는지 알 수 없지만 아마도 더 이상 자주 사용되지 않는다고 판단되는 시점에 알아서 메모리에서 해제하는 것으로 보인다.</p>
<h3 id="항상-이득일까">항상 이득일까?</h3>
<p>동적 링크가 항상 이득일지 의문이 들어서 ChatGPT 에게 물어보았다.
<img src="https://velog.velcdn.com/images/will-big/post/7175d567-998a-43dd-badc-23016c02aca3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/will-big/post/345c4e2a-9ed7-469c-ab8a-dd2199867880/image.png" alt=""></p>
<p>결론적으로 말하자면 완벽한 이득은 아니지만 그럼에도 불구하고 정적 링크를 사용하였을 때 얻는 이득이 동적 링크를 사용하였을 때에 비해 미미하기 때문에 동적 링크를 사용하는 것이 이득이라고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[게임인재원] 1학기를 마치며]]></title>
            <link>https://velog.io/@will-big/%EA%B2%8C%EC%9E%84%EC%9D%B8%EC%9E%AC%EC%9B%90-1%ED%95%99%EA%B8%B0%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0</link>
            <guid>https://velog.io/@will-big/%EA%B2%8C%EC%9E%84%EC%9D%B8%EC%9E%AC%EC%9B%90-1%ED%95%99%EA%B8%B0%EB%A5%BC-%EB%A7%88%EC%B9%98%EB%A9%B0</guid>
            <pubDate>Tue, 06 Jun 2023 08:57:48 GMT</pubDate>
            <description><![CDATA[<h1 id="5기-프로그래밍-기록">5기 프로그래밍 기록</h1>
<p> 재밌었고 힘들었던 게임인재원 1학기를 마치고 첫 방학이 끝나간다. 간략하게 1학기(1Q)를 마치면서 느꼈던 것들을 정리하고자 한다. 23년 3월부터 5월 말까지 게임인재원에서 보낸 3개월에 대한 후기를 적어본다.</p>
<p> 처음 왔을 때는 대부분이 이곳의 정보를 잘 모르고 왔기 때문에 무엇을 가르쳐주는지도 잘 모르고 어떤 교육과정이 있는지도 잘 모르는 상태였다. 하지만 수업을 들으면서 느껴지는 교수님, 강사님들의 자신감과 강의력이 이곳을 계속 다녀도 괜찮겠다는 확신을 주어서 3월부터 5월 말까지 계속 달려왔던 것 같다. 사실 이곳에 대한 정보는 오래전부터 알고 있었지만 그때는 지금보다 더 정보도 없었고 내 생각도 어려서 두려운 마음에 지원하지 않았다. 시간이 흘러 어느정도 생각이 정립되고, 미래에 대한 불안감과 나의 실력이 사회가 요구하는 기준치에 한참 부합하지 못한다는 것을 깨달아 이곳에 지원하게 되었다. 우연찮게도 혼자 지원하려 했던 이곳에 친한 친구와 함께 등록하게 되었는데 그 또한 지금 생각해보면 정말 좋은 결과를 가져다 주었다고 생각한다.</p>
<p> 한 학기 동안 얕고 넓게 배웠다고 생각한다. 누군가는 &#39;깊게 배우지 않았느냐&#39;, 또는 &#39;좁게 배우지 않았느냐&#39;라고 말할지도 모르지만 내 생각은 그렇지 않다. 얕다의 의미는 기존에 내가 알던 부분에서는 아직까지 내가 아예 모를정도로 깊게까지는 나아가지 않았고 간간이 헷갈리더라도 과거에 공부했던 내용이기에 어렵지 않았다는 뜻이다. 넓다고 생각한 이유는 내가 공부하지 않았던 영역(기획, 아트)에 대해서는 조금씩 맛을 본 느낌이었기 때문이다. 결코 수준에 대한 평가가 아니며 전공자로써 느끼는 1 학기 수업에 대한 감상일 뿐이니 오해하지 않기를 바란다. 이것이 게임인재원에서 요구하는 1 학기의 적절한 학습상인지는 모르겠으나 2 학기에는 더, 3 학기에는 훨씬 더 어려워질 것이라는 교수님의 말씀 또한 새겨 들으며... 만족하며 학기를 마무리 지었다.</p>
<p> 사실 내가 생각하는 게임인재원의 꽃은, 대학교에서 쉽게 배울 수 없는 게임에 특화된 수업이나 현업에서 일하던 분들을 만나 학습자에게 부족한 부분을 지적해주는 그런 것들이 아니다. 다른 사람의 의견을 빌리자면 학습 분위기가 좋은 곳이라고도 했지만, 나에게 이곳이 좋은 이유가 무엇인지 묻는다면 매 학기말에 하는 프로젝트라고 말할 것이다. 단순히 무언가를 계획을 잡고 만드는 거에 그치는 것이 아니라 게임에 필요한 3 분야 기획, 아트, 플밍이 모두 뭉쳐 하나로 만드는데 큰 의미가 있기 때문이다.</p>
<p> <img src="https://velog.velcdn.com/images/will-big/post/872fe8b3-fa49-4e59-b71b-9ea047d8f0b7/image.png" alt="게임인재원 WinAPI 프로젝트 시연회"></p>
<p> 1 학기도 당연하게도 프로젝트로 마무리가 된다. 1 학기 동안 배운 것과 WinAPI 를 사용하여 3 개 학과가 모여 게임을 하나 만든다. 물론 WinAPI 를 직접적으로 사용하는 것은 프로그래밍 뿐이지만 그로 인해 많은 것들에 제한점이 있었고 자연스럽게 다른 학과들과 함께 이에 맞춰서 진행하는 프로젝트가 되었다. WinAPI 사용에 있어서 개인적으로 느꼈던 가장 큰 불편한점은 GPU를 사용할 수 없다는 것이었다. 잘 아는것은 아니지만 GDI(WinAPI에 쓰이는 그래픽 디바이스 인터페이스)는 CPU 만을 사용하기 때문에 우리가 생각하는 게임 그래픽에 적합한 출력 방법을 가진 장치는 아니라고 한다. 따라서 CPU 가 그만큼 과부하를 받는 것이고 프로그래밍 입장에서 프레임 걱정을 많이 해야했다. 사실 현재 하는 프로젝트가 많은 연산을 요구하는 게임이 아니기 때문에 프레임이 쉽게 떨어질 일은 없었지만 몇몇 WinAPI 함수의 사용은 주의를 필요로 했기 때문에 마냥 쉽게 받아들일 것은 아니었다고 생각한다.</p>
<p> 우연히 프로그래밍 팀의 팀장을 맡게 되었는데 생각보다 힘들었다. 먼저 나의 수준이 높지 않아서 팀원들의 역량을 파악하는게 어려웠고, 때문에 역할 배분에서 적지 않은 고생을 했다. 나름대로 계획을 짜서 진행했음에도 불구하고 앞서 말한 문제 때문에 계획이 틀어지는 경우도 자주 있었고 당연히 기획에서 의도한 모든 것들을 해낼 수는 없었다. 개개인의 실력 부족도 이유라고 할 수 있겠지만 잘못된 역할 분배로 인한 시간 낭비로 소모되는 기간이 적지 않았던게 주된 이유라고 생각이 들어서 함께 했던 프로그래밍 팀원과 기획 팀원 모두에게 미안한 마음이 있었다.</p>
<p> 개인적으로 아쉬운 점도 있었다. 평소에 기능에 대해 최대한 구상을 한 후에 코드를 짜는 게 깔끔한 코드가 나온다고 생각하는 편인데, 프로젝트 기간에는 그럴 여유가 많이 없어서 닥치는대로 기능을 구현하고 나중에 추가적인 부분이 있다면 끼워 맞추는 식으로 진행했다. 그러다 보니 어느샌가 코드는 괴물이 되어있었고 확장성도 매우 떨어지는 코드가 되어버렸다. 마치 만화에서 보이는 물이 새는 배수관처럼 한 곳을 막으면 다른 곳이 터지는 일도 비일비재했다. </p>
<p> 또한 프레임워크에 대해 공부하지 않고 프레임워크를 만들려고 하다 보니 인터넷에 나와 있는 쉬운 정보를 무작정 따라하게 되었고 내 코드가 아니다 보니 완전히 이해하지 않고 쓰는 기능들도 종종 있었다. 때문에 제작하고 있는 게임에 적합하지 않는 부분도 몇몇 있었고 고치기 힘들었던 부분도 정말 많았다. 다음에도, 그리고 그 다음에도 나는 프로그래밍을 담당할 것이고 당연히 다시 프레임워크를 만들게 될텐데 어떻게든 나만의 프레임워크에 대한 정립을 마무리 짓는것이 올해의 목표이다. 절대 쉽게 될거라고 생각하지 않기에 천천히 길게 보며 공부할 예정이다.</p>
<p> 아쉬웠던 점과는 별개로 프로그래밍 팀, 아트 팀, 기획 팀 모두가 제 역할을 잘해주었고 만족할만한 결과물이 나왔다고 생각한다. 기획에서 나왔던 많은 기믹들을 일부 구현하지 못해서 아쉬웠지만 그 아쉬움이 없어질만큼 깔끔했던 레벨 디자인과 게임의 전체적인 느낌과 너무 잘 어우러지는 아트가 만나 좋은 게임을 만들 수 있는 토대가 되었다. 프로그래밍 팀 또한 그에 부합하기 위해 모두가 노력했고 기분 좋은 프로젝트로 끝마칠 수 있어서 정말 좋았다.</p>
<p>!youtube[zIlGfcm2qls]</p>
<p>나에게 정말 큰 경험이 되었던 프로젝트였다. 내 손길이 온전히 들어간 게임을 만들 수 있는 기회가 얼마나 될까 싶기도 하고, 현재 하는 공부가 재밌기에 앞으로도 열심히 하고 싶다. 끝으로 정말 좋았던 게임인재원이지만 한가지 아쉬웠던 점이 있다. 바로 홍보가 정말 안되어있다는 것이다. 사실 이런 글을 적는 성격이 아니지만 이 글을 보고 열정을 가진 많은 사람들이 이곳에 왔으면 좋겠기에 후기를 남겨보았다. 자주는 안되더라도 한 학기마다 최소 하나씩은 후기를 남겨보려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C/C++] compare 함수를 공부하며]]></title>
            <link>https://velog.io/@will-big/CC-compare-%ED%95%A8%EC%88%98%EB%A5%BC-%EA%B3%B5%EB%B6%80%ED%95%98%EB%A9%B0-</link>
            <guid>https://velog.io/@will-big/CC-compare-%ED%95%A8%EC%88%98%EB%A5%BC-%EA%B3%B5%EB%B6%80%ED%95%98%EB%A9%B0-</guid>
            <pubDate>Mon, 17 Apr 2023 06:52:44 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-cpp">int compare1(const void* a, const void* b)
{
    return strcmp(*(const char**)a, *(const char**)b);
}

int compare2(const void* a, const void* b)
{
    int A = *(int*)a;
    int B = *(int*)b;

    if (A &lt; B)
        return -1;
    else if (A &gt; B)
        return 1;
    else
        return 0;
}

int main(void)
{
    const char* aList[5] = { &quot;abb&quot;, &quot;aaa&quot;, &quot;ddd&quot;, &quot;eee&quot;, &quot;ccc&quot; };
    int nList[5] = { 4,3,2,5,1 };

    qsort(aList, 5, sizeof(const char*), compare1);
    qsort(nList, 5, sizeof(int), compare2);

    for (size_t i = 0; i &lt; 5; i++)
    {
        cout &lt;&lt; aList[i] &lt;&lt; endl;
        cout &lt;&lt; nList[i] &lt;&lt; endl;
    }

    return 0;
}</code></pre>
<p>처음 위 코드를 작성했을 때 compare1 에서 굳이 번거롭게 void* -&gt; const char* 로 직접 변환하면 될 것을 ** 로 변환하고 다시 역참조를 하는지 이해가 안갔다. 주변에 자문을 구해봤지만 잘 이해가 안가서 다른 변수를 인자로 받는 코드를 만들어 보았고 그것이 compare2 다.</p>
<p>compare2 로 만들면서 느꼈던 것은 당연히 * 로 변환하고 역참조를 해야한다고 생각했다는 것이다. compare1 에서 주장했던 것과 같은 논리를 해보자면 void* 로 들어온 값을 억지로 int 로 변환한다는 것인데 포인터로 들어온 자료형을 갑자기 int 로 바꾼다는 발상 자체가 말이 안된다는 것이다. </p>
<p>포인터를 상당히 오랜만에 다시 공부하게 되어서 헷갈리는 상황이 많이 발생하는데 종종 포인터에 국한하지 않고 다른 자료형으로 생각해보면 납득이 가는 경우가 꽤 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity] 2022-2 게임소프트웨어 팀프로젝트 - The Worker]]></title>
            <link>https://velog.io/@will-big/Unity-2022-%EA%B2%8C%EC%9E%84%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-The-Worker</link>
            <guid>https://velog.io/@will-big/Unity-2022-%EA%B2%8C%EC%9E%84%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-The-Worker</guid>
            <pubDate>Tue, 24 Jan 2023 16:28:36 GMT</pubDate>
            <description><![CDATA[<p>2022년에 있었던 팀프로젝트 The Worker 중 내가 맡았던 백엔드에 대한 코드와 설명이다. 원래 주석이 적지 않았지만 인코딩 이슈로 인해 삭제했다. 다음엔 주석을 영어로 다는 것도 고려해야겠다. 이외의 코드도 좀 있지만 내가 주로 작업한 코드는 아래 코드가 전체 코드라고 봐도 무방하다. 백엔드만 담당하였기에 시연 영상에 보이는 클라이언트와 관련된 부분들은 팀원들이 작업한 내용이다. 비록 클라이언트에 비하면 했던 것은 적었지만 개인적으로 의미 있었던 팀프로젝트라고 생각한다.</p>
<h2 id="시연-영상">시연 영상</h2>
<p><a href="https://youtu.be/FgzSE4eazn8"><img src="https://velog.velcdn.com/images/will-big/post/e0fa26bd-5614-485b-b233-0a4dc410dc30/image.png" alt=""></a> (이미지클릭)</p>
<h2 id="코드">코드</h2>
<pre><code class="language-cs">using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BackEnd;
using LitJson;
using UnityEngine;
using Random = UnityEngine.Random;

// DB Function Naming Rule
// DB -&gt; User = Get...
// User -&gt; DB = Update...

public class BackendManager : MonoBehaviour
{
    private static BackendManager instance = null;

    private void Awake()
    {
        if (instance is null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    public static BackendManager Instance
    {
        get
        {
            if (instance is null)
                return null;

            return instance;
        }
    }

    public Dictionary&lt;string, RankItem&gt; GetRank(int stageNum)
    {
        var rankItemList = new Dictionary&lt;string, RankItem&gt;();
        var uuid = GetRankId(stageNum);

        Debug.Log(&quot;Before GetDetailedRank&quot;);

        foreach (var id in uuid)
        {
            rankItemList.Add(id.Key, GetDetailedRank(id.Value));
        }

        Debug.Log(&quot;After GetDetailedRank&quot;);

        return rankItemList;
    }

    private Dictionary&lt;string, string&gt; GetRankId(int stageNum)
    {
        // BlockTypeCount, BlockLength, AveargeStep
        var rankTableItemList = new List&lt;RankTableItem&gt;();
        string[] rankTypeArray = { $&quot;S{stageNum}_BL&quot;, $&quot;S{stageNum}_BTC&quot;, $&quot;S{stageNum}_AS&quot; };
        var rankId = new Dictionary&lt;string, string&gt;();

        var bro = Backend.URank.User.GetRankTableList();

        if (bro.IsSuccess())
        {
            JsonData rankTableListJson = bro.FlattenRows();

            for (int i = 0; i &lt; rankTableListJson.Count; i++)
            {
                RankTableItem rankTableItem = new RankTableItem();


                rankTableItem.rankType = rankTableListJson[i][&quot;rankType&quot;].ToString();
                rankTableItem.date = rankTableListJson[i][&quot;date&quot;].ToString();
                rankTableItem.uuid = rankTableListJson[i][&quot;uuid&quot;].ToString();
                rankTableItem.order = rankTableListJson[i][&quot;order&quot;].ToString();
                rankTableItem.isReset = rankTableListJson[i][&quot;isReset&quot;].ToString() == &quot;true&quot; ? true : false;
                rankTableItem.title = rankTableListJson[i][&quot;title&quot;].ToString();
                rankTableItem.table = rankTableListJson[i][&quot;table&quot;].ToString();
                rankTableItem.column = rankTableListJson[i][&quot;column&quot;].ToString();

                if (rankTableListJson[i].ContainsKey(&quot;rankStartDateAndTime&quot;))
                {
                    rankTableItem.rankStartDateAndTime =
                        DateTime.Parse(rankTableListJson[i][&quot;rankStartDateAndTime&quot;].ToString());
                    rankTableItem.rankEndDateAndTime =
                        DateTime.Parse(rankTableListJson[i][&quot;rankEndDateAndTime&quot;].ToString());
                }

                if (rankTableListJson[i].ContainsKey(&quot;extraDataColumn&quot;))
                {
                    rankTableItem.extraDataColumn = rankTableListJson[i][&quot;extraDataColumn&quot;].ToString();
                    rankTableItem.extraDataType = rankTableListJson[i][&quot;extraDataType&quot;].ToString();
                }

                rankTableItemList.Add(rankTableItem);
                Debug.Log(rankTableItem.ToString());
            }

            foreach (var type in rankTypeArray)
            {
                var result = rankTableItemList.Find(a =&gt; a.title.Equals(type));

                if (result is null)
                {
                    Debug.Log(&quot;No Rank Table Item Error&quot;);
                    break;
                }

                Debug.Log($&quot;GetRankId Success. {type} : {result.uuid}&quot;);
                rankId.Add(type, result.uuid);
            }
        }

        return rankId;
    }

    private RankItem GetDetailedRank(string uuid)
    {
        RankItem rankItem = new RankItem();

        var bro = Backend.URank.User.GetMyRank(uuid);

        if (bro.IsSuccess())
        {
            Debug.Log(&quot;@GetDetailedRank Start&quot;);
            JsonData rankListJson = bro.GetFlattenJSON();
            string extraName = string.Empty;

            rankItem.gamerInDate = rankListJson[&quot;rows&quot;][0][&quot;gamerInDate&quot;].ToString();

            if (rankListJson[&quot;rows&quot;][0][&quot;nickname&quot;] != null)
                rankItem.nickname = rankListJson[&quot;rows&quot;][0][&quot;nickname&quot;].ToString();
            else
                rankItem.nickname = &quot;Unknown&quot;;

            rankItem.score = rankListJson[&quot;rows&quot;][0][&quot;score&quot;].ToString();
            rankItem.index = rankListJson[&quot;rows&quot;][0][&quot;index&quot;].ToString();
            rankItem.rank = rankListJson[&quot;rows&quot;][0][&quot;rank&quot;].ToString();
            rankItem.totalCount = rankListJson[&quot;totalCount&quot;].ToString();

            if (rankListJson[&quot;rows&quot;][0].ContainsKey(rankItem.extraName))
            {
                rankItem.extraData = rankListJson[&quot;rows&quot;][0][rankItem.extraName].ToString();
            }

            Debug.Log(rankItem.ToString());
        }

        return rankItem;
    }

    public Dictionary&lt;string, Dictionary&lt;string, List&lt;string&gt;&gt;&gt; GetOtherUserCode(int stageNum)
    {
        var topThreeCodes = new Dictionary&lt;string, Dictionary&lt;string, List&lt;string&gt;&gt;&gt;(); // &lt;rankType, Dict&lt;nickname, code&gt;&gt;
        var rankId = GetRankId(stageNum);

        foreach (var id in rankId)
        {
            var bro = Backend.URank.User.GetRankList(id.Value, 3, 0);

            if (bro.IsSuccess() == false)
            {
                Debug.Log(&quot;GetOtherUserCode Error : &quot; + bro);
                return null;
            }

            var rankListJson = bro.GetFlattenJSON();
            var userCodeDict = new Dictionary&lt;string, List&lt;string&gt;&gt;();

            for (int i = 0; i &lt; rankListJson[&quot;rows&quot;].Count; i++)
            {
                var gamerRank = rankListJson[&quot;rows&quot;][i][&quot;rank&quot;].ToString(); // key
                var gamerNickname = rankListJson[&quot;rows&quot;][i][&quot;nickname&quot;].ToString(); // value
                var gamerCode = rankListJson[&quot;rows&quot;][i][&quot;UserCode&quot;].ToString(); // value

                var list = new List&lt;string&gt;();
                list.Add(gamerNickname);
                list.Add(gamerCode);

                userCodeDict.Add(gamerRank, list);
                // Debug.Log($&quot;{id.Key}, {i + 1} : {rankListJson[&quot;rows&quot;][i][&quot;UserCode&quot;]}&quot;);
            }


            topThreeCodes.Add(id.Key, userCodeDict);
        }

        return topThreeCodes;
    }

    public void UpdateScore(int stageNum, int blockLength, int blockTypeCount, float averageStep, string userCode)
    {

        var rankId = GetRankId(stageNum);
        var tableName = $&quot;STAGE_{stageNum}&quot;;
        var rowInDate = string.Empty;

        Param param = new Param();
        param.Add(&quot;BlockLength&quot;, blockLength);
        param.Add(&quot;BlockTypeCount&quot;, blockTypeCount);
        param.Add(&quot;AverageStep&quot;, averageStep);
        param.Add(&quot;UserCode&quot;, userCode);

        SendQueue.Enqueue(Backend.GameData.GetMyData, tableName, new Where(), bro =&gt;
        {
            if (bro.IsSuccess() == false)
            {
                Debug.Log(bro);
                return;
            }

            if (bro.GetReturnValuetoJSON()[&quot;rows&quot;].Count &lt;= 0)
            {
               SendQueue.Enqueue(Backend.GameData.Insert, tableName, param, callback =&gt;
                {
                    if (callback.IsSuccess() == false)
                    {
                        Debug.Log(callback);
                        Debug.Log(&quot;UpdateScore-Insert Error&quot;);
                        return;
                    }

                    if (callback.FlattenRows().Count &gt; 0)
                        rowInDate = callback.GetInDate();

                    foreach (var item in rankId) // Dic &lt;rankType , uuid&gt;
                    {
                        InnerUpdate(item.Value, tableName, rowInDate, param);
                    }
                });
            }
            else
            {
               SendQueue.Enqueue(Backend.GameData.Update, tableName, new Where(), param, callback =&gt;
                {
                    if (callback.IsSuccess() == false)
                    {
                        Debug.Log(callback);
                        Debug.Log(&quot;UpdateScore-Update Error&quot;);
                        return;
                    }

                    rowInDate = bro.GetInDate();        

                    foreach (var item in rankId) // Dic &lt;rankType , uuid&gt;
                    {
                        InnerUpdate(item.Value, tableName, rowInDate, param);
                    }
                });
            }
        });


    }

    private void InnerUpdate(string uuid, string tableName, string rowIndate, Param param)
    {
        SendQueue.Enqueue(Backend.URank.User.UpdateUserScore, uuid, tableName, rowIndate, param, bro =&gt;
        {
            if (bro.IsSuccess() == false)
            {
                Debug.Log(bro);
                Debug.Log(&quot;UpdateScore-UpdateUserScore Error&quot;);
            }
        });
    }

    public (bool isValid, string message) CreateNickname(string nickname)
    {
        var bro = Backend.BMember.CreateNickname (nickname);

        return (bro.IsSuccess(), bro.GetMessage());
        // Error cases -&gt; https://developer.thebackend.io/unity3d/guide/bmember/nickname/ 
    }

    public void UpdateMyInfo(UserInfo userInfo)
    {
        Param param = Param.Parse(userInfo);
        var tableName = &quot;USER_INFO&quot;;

        SendQueue.Enqueue(Backend.GameData.GetMyData, tableName, new Where(), bro =&gt;
        {
            if (bro.IsSuccess() == false)
            {
                Debug.Log(bro);
                return;
            }

            if (bro.GetReturnValuetoJSON()[&quot;rows&quot;].Count &lt;= 0)
            {
                SendQueue.Enqueue(Backend.GameData.Insert, tableName, param, callback =&gt;
                {
                    if (callback.IsSuccess() == false)
                    {
                        Debug.Log(callback);
                        Debug.Log(&quot;UpdateMyInfo-Insert Error&quot;);
                        return;
                    }
                    Debug.Log(&quot;UpdateMyInfo-Insert Success&quot;);
                });
            }
            else
            {
               SendQueue.Enqueue(Backend.GameData.Update, tableName, new Where(), param, callback =&gt;
                {
                    if (callback.IsSuccess() == false)
                    {
                        Debug.Log(callback);
                        Debug.Log(&quot;UpdateMyInfo-Update Error&quot;);
                        return;
                    }
                    Debug.Log(&quot;UpdateMyInfo-Update Success&quot;);
                });
            }
        });
    }

    public UserInfo GetMyInfo()
    {
        var bro = Backend.GameData.GetMyData(&quot;USER_INFO&quot;, new Where(), 1);

        if (bro.IsSuccess() == false)
        {
            Debug.Log(&quot;IsSuccess Error&quot;);
            Debug.Log(bro);
            return null;
        }
        if (bro.FlattenRows()[&quot;rows&quot;].Count &lt;= 0)
        {
            Debug.Log(&quot;No data&quot;);
            Debug.Log(bro);
            return null;
        }

        var myInfoJson = bro.FlattenRows();

        var userInfo = JsonMapper.ToObject&lt;UserInfo&gt;(myInfoJson.ToJson());
        Debug.Log(userInfo.ToString());

        return userInfo;
    }

    public string CreateRandomNickname()
    {
        var frontWord = new[] {&quot;신나는&quot;, &quot;멋진&quot;, &quot;행복한&quot;,&quot;슬픈&quot;,&quot;우울한&quot;,&quot;재수없는&quot;,&quot;고생하는&quot;,&quot;귀여운&quot;, &quot;오른손이 커다란&quot;, &quot;밥을 좋아하는&quot;};
        var backWord = new[] {&quot;바지&quot;, &quot;옷걸이&quot;, &quot;얼음&quot;, &quot;코끼리&quot;, &quot;개미핥기&quot;, &quot;빨대&quot;, &quot;가방&quot;, &quot;눈꺼풀&quot;, &quot;의자&quot;, &quot;팔꿈치&quot;};

        var randFront = Random.Range(0, frontWord.Length);
        var randBack = Random.Range(0, backWord.Length);

        return frontWord[randFront] + backWord[randBack];
    }
}</code></pre>
<h2 id="목표-선정-및-피드백">목표 선정 및 피드백</h2>
<p><strong>1.시작</strong></p>
<pre><code>목표

- GPGS 로그인

활동

- 안드로이드 빌드 이슈 해결
- GPGS 이슈 해결
- 로그인 구현 시도</code></pre><p><strong>2.중간 발표</strong></p>
<pre><code>목표

- GPGS 로그인
- 뒤끝 SDK 활용 → 유저 정보 저장
- 랭킹 생성 / 갱신

    → 팀원 피드백 : 랭킹 랜덤 조회 → 상위 3명 조회로 변경 요청


활동

- GPGS 로그인 활성화
- 뒤끝 서버 유저 정보 저장 완료</code></pre><p><strong>3.팀 발표</strong></p>
<pre><code>목표

- 랭킹 생성 / 갱신
- 랭킹 조회
- 유저 정보 생성 / 갱신
- 유저 정보 조회

활동

- 랭킹 조회에 대한 피드백 반영
- 각종 이슈 해결</code></pre><p><strong>4.최종</strong></p>
<pre><code>목표

- 기존 목표 기능 보완

활동

- 기존 목표 기능 추가 발생 이슈 해결
- 다른 팀원의 이슈 해결 도움</code></pre><p><strong>5.느낀점</strong></p>
<blockquote>
<p>백엔드 개발은 처음이었는데, 처음부터 끝까지 디버깅과의 전쟁이었다고 생각이 듦. GPGS 특성상 구글 플레이 콘솔에 Android App Build 파일(이하 aab)을 업로드 하지 않으면 해당 버전을 실행 조차 할 수 없어서 디버깅하는데 상당한 시간을 소요함. 특히나 안드로이드 빌드는 컴퓨터의 성능에 의해 시간이 많이 좌지우지 되어 불편함이 많았음. 백엔드의 특성상 클라이언트가 요구하는 객체만 넘겨주면 되기 때문에 적절한 객체를 약속하고 그 객체가 올바르게 반환 될 때까지 스스로 디버깅하면서 작업하는 일의 반복이기 때문에 상대적으로 팀원들과의 소통이 적었고 이로 인해 야기되는 불편한 점이 종종 있었음.  예를 들면 스테이지의 인덱스 번호를 0부터 시작할지, 1부터 시작할지 정도나 반환되는 객체가 복잡해져서 이를 다시 설명해야되는 경우 등 몇몇 불편한 경우를 겪고 소통의 중요성을 체감함.</p>
</blockquote>
<p><strong>6.개선점</strong></p>
<blockquote>
<p>디버깅 및 기능 변경 요구를 거치면서 코드에 수정을 거듭하게 되었는데, 이로 인해 시각적으로 보기에 안좋아진 코드들이 생기게 되었음. 다음부턴 디자인 패턴을 고려하며 수정하더라도 기존의 코드를 재사용할 수 있게끔 만들고 싶음.
팀 프로젝트 초반에는 느끼지 못했지만 후반에 데드라인이 가까워짐에 따라 소통의 부재가 아쉬웠음. 다음에는 좀 더 적극적으로 소통에 참여하기로 다짐함.
이번 프로젝트에서는 주로 API 를 다루었기 때문에 짜여진 코드를 잘 조립해서 원하는 모양을 만들어내는 것에 그쳤지만 다음에 다시 백엔드를 맡게 된다면 서버만 얻은 채로 직접 밑바닥부터 개발해보고 싶음.</p>
</blockquote>
<h2 id="구현-세부사항">구현 세부사항</h2>
<h3 id="1-구현-방식">1. 구현 방식</h3>
<p>서버의 주요 기능은 <code>Assets/Scripts/Backends/BackendManager.cs</code> 에 구현 됨.</p>
<p>싱글톤 패턴으로 <code>BackendManger.Instance.함수명</code> 으로 사용 가능.</p>
<p>각 함수가 리턴하는 객체의 사용 여부에 따라 객체를 반드시 함수 실행 이후에 사용해야 한다면 동기 방식으로, 실행 순서에 관계없다면 SendQueue(비동기와 유사) 방식으로 사용함.</p>
<p>동기 방식 적용 함수</p>
<p><code>GetRankId</code>, <code>GetDetailedRank</code>, <code>GetOtherUserCode</code>, <code>CreateNickname</code>, <code>GetMyInfo</code></p>
<p>주로 서버로부터 클라이언트가 객체를 전달받을 때 적용</p>
<p>SendQueue 방식 적용 함수</p>
<p><code>UpdateScore</code>, <code>InnerUpdate</code>, <code>UpdateMyInfo</code></p>
<p>주로 클라이언트가 서버로 객체를 전달할 때 적용</p>
<h3 id="2-구현-기능">2. 구현 기능</h3>
<ul>
<li><p>유저 가입</p>
<p>  GPGS + 뒤끝 서버 SDK 사용</p>
<p>  <code>Assets/Scripts/Backends/GoogleLogin.cs</code> 참조</p>
<ol>
<li><p>유저가 사용중인 플레이스토어 아이디로 자동으로 회원 가입 시도</p>
</li>
<li><p>회원 가입 요청이 정상적으로 이루어 졌다면 <code>Social.localUser.Authenticate((bool success))</code></p>
<ol>
<li><p>기존 회원의 가입 요청의 경우</p>
<p> <code>BackendManager.Instance.GetMyInfo()</code> 로 서버에서 로드</p>
<ol start="2">
<li><p>신규 회원의 가입 요청의 경우</p>
<p>랜덤한 닉네임을 생성한 후 기본 정보를 서버에 업데이트 및 로드 </p>
<p><code>UpdateMyInfo(NewuserInfo)</code> : 새로 생성된 정보를 USER_INFO 테이블에 저장함</p>
<p><code>CreateNickname(nickname)</code> : 새로 생성된 닉네임을 뒤끝 서버에 전달함</p>
</li>
</ol>
</li>
</ol>
</li>
</ol>
</li>
</ul>
<pre><code>생성된 유저 테이블은 위의 `DB - 유저 테이블` 이미지와 같은 형식</code></pre><ul>
<li><p>유저(자신) 정보 조회</p>
<p>  뒤끝 서버 SDK 사용</p>
<p>  <code>Assets/Scripts/Backends/BackendManager.cs</code> 참조</p>
<p>  <code>GetMyInfo</code> 로 사용 가능하며 <code>USER_INFO</code> 테이블에서 유저 고유 값(uuid)에 맞는 정보를 가져와 클라이언트에 로드함.</p>
</li>
</ul>
<ul>
<li><p>랭킹 생성 / 갱신</p>
<p>  뒤끝 서버 SDK 사용</p>
<p>  <code>Assets/Scripts/Backends/BackendManager.cs</code> 참조</p>
<p>  <code>UpdateScore</code>  로 사용 가능하며 생성과 갱신의 기능을 동시에 담당할 수 있게끔 SDK 를 변형하여 적용함.</p>
<ol>
<li><p>클리어한 스테이지 정보를 서버로부터 가져옴(<code>GetRankId</code>)</p>
</li>
<li><p>서버에 자신의 데이터가 있는지 검사(<code>Backend.GameData.GetMyData</code>)</p>
<ol>
<li><p>정보가 없다면</p>
<p> <code>Backend.GameData.Insert</code> → <code>InnerUpdate</code>  를 통해 정보를 삽입함</p>
</li>
<li><p>정보가 존재한다면</p>
<p>  <code>Backend.GameData.Update</code> → <code>InnerUpdate</code> 를 통해 정보를 갱신함</p>
</li>
</ol>
</li>
</ol>
</li>
</ul>
<pre><code>생성(삽입)과 갱신을 하나의 함수에서 처리한 이유

두 기능의 공통점은 정보를 바꾼다는 것인데, 뒤끝 서버에서 제공하는 함수는 같은 역할을 할 수 있는 하나의 함수가 없기 때문에 개발의 편의를 위해 두 기능을 하나의 함수로 묶음

`InnerUpdate` 의 역할

내부적으로 `Backend.URank.User.UpdateUserScore` 를 SendQueue 방식으로 수행함</code></pre><ul>
<li><p>랭킹 조회</p>
<p>  뒤끝 서버 SDK 사용</p>
<p>  <code>Assets/Scripts/Backends/BackendManager.cs</code> 참조</p>
<ol>
<li><p>자신의 랭킹 조회</p>
<p> <code>GetRank</code> 로 사용 가능</p>
<p> <code>Dictionary&lt;string, RankItem&gt;</code> 형태로 반환하며 <code>key</code>값(분야)에 따른 자신의 순위를 <code>RankItem.rank</code> 로 조회 가능</p>
</li>
<li><p>상위 3명의 코드 조회</p>
<p> <code>GetOtherUserCode</code> 로 사용 가능</p>
<p> <code>Backend.URank.User.GetRankList</code> 의 파라미터를 적절하게 입력하여 분야별로 1등부터 상위 3명의 랭킹 정보를 가져옴</p>
<p> 가져온 <code>Json</code> 타입 정보 중 필요한 정보만 분리하여 클라이언트에게 전달함</p>
<p> 전달하는 객체 : <code>Dictionary&lt;string, Dictionary&lt;string, List&lt;string&gt;&gt;&gt;</code></p>
<p> → &lt; 분야 , &lt; 랭킹(인덱스) , 코드리스트 &gt; &gt;</p>
</li>
</ol>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity] 더블 탭(멀티 탭) 대쉬]]></title>
            <link>https://velog.io/@will-big/Unity-%EB%8D%94%EB%B8%94-%ED%83%AD-%EB%8C%80%EC%89%AC</link>
            <guid>https://velog.io/@will-big/Unity-%EB%8D%94%EB%B8%94-%ED%83%AD-%EB%8C%80%EC%89%AC</guid>
            <pubDate>Wed, 11 Jan 2023 10:57:32 GMT</pubDate>
            <description><![CDATA[<h3 id="발생한-문제">발생한 문제</h3>
<p>유니티에서 더블 클릭을 구현하기가 여간 귀찮다. 방법이야 정말 여러개가 있지만 업데이트 문에서 감지한다던가 <em>UniRx</em> 를 써서 감지한다던가 하는 것도 가능하지만 성능상의 단점, 구현의 한계 등으로 몇번 시도해보다가 다른 방향으로 틀게 되었다. 이 문제의 핵심은 대쉬 구현이 아닌 <strong>키보드 더블 탭</strong> 구현이다. 그리고 이번도 저번과 마찬가지로 <em>New Input System</em> 을 활용한다.</p>
<h3 id="해결방법">해결방법</h3>
<p><img src="https://velog.velcdn.com/images/will-big/post/b4ba1e82-b571-4923-9545-fe26544c2f8b/image.png" alt=""></p>
<p>Actions - Dash 의 Binding Properties 를 보면 Interactions - Multi Tap 기능이 있다. 간략히 설명을 하자면 다음과 같다. 상세 정보는 <a href="https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Interactions.html#multitap">링크</a>를 확인</p>
<blockquote>
<p>Tap Count - 상호작용을 위해 필요한 탭 수
Max Tap Spacing - 탭 사이의 입력이 없어도 되는 시간
Max Tap Duration - 탭의 최대 입력 시간
Press Point - (정확한 정보 확인시 추가)</p>
</blockquote>
<p>몇번 테스트 해본 결과 아주 만족스러운 디폴트 값이었기에 그냥 쓰기로 했다. 기존의 New Input System 과 마찬가지로 SendMessage 방식으로 작동하기에 OnDash 함수를 구현해주고 코루틴으로 애니메이션과 쿨다운을 넣었다.</p>
<p>더블 탭은 유니티에서 구현해주었으니 우리는 대쉬만 구현하면 된다.</p>
<blockquote>
<p>작동방식</p>
</blockquote>
<ol>
<li>New Input System -&gt; OnDash() 실행</li>
<li>대쉬가 가능하면 애니메이션을 실행하고 코루틴을 시작</li>
<li>대쉬 불가능 처리 및 현재 gravity 저장 후 순간 속도 증가</li>
<li>대쉬 애니메이션이 끝나면 현재 위치에 따라 적절한 애니메이션으로 변경</li>
<li>대쉬 전 변수들을 복구 후에 특정 시간이 지나면 대쉬 가능 처리</li>
</ol>
<h3 id="코드">코드</h3>
<pre><code class="language-cs">    private void OnDash() // 1
    {
        if (isAbleDash)
        {     // 2
            anim.Play(&quot;Dash&quot;, -1, 0);
            StartCoroutine(DashStart());
        }
    }

    private IEnumerator DashStart()
    {
        Debug.Log(&quot;Dash!&quot;);
        Debug.Log(&quot;Dash Cool Time Start&quot;);

        // 3
        isAbleDash = false;
        isDashing = true; // isDashing 동안 사용자의 입력을 받지 않음
        var originalGravity = rigid.gravityScale;
        rigid.gravityScale = 0f;
        rigid.velocity = new Vector2(transform.localScale.x * dashPower, 0);

        // 4
        while (anim.GetCurrentAnimatorStateInfo(0).normalizedTime &lt;= 1.0f)
        {
            yield return null;
        }

        if (isGrounded)
            anim.Play(&quot;Idle&quot;, -1, 0);
        else
            StartCoroutine(JumpAnimation());

        // 5
        isDashing = false;
        rigid.gravityScale = originalGravity;
        rigid.velocity = new Vector2(0, 0);
        yield return new WaitForSeconds(dashCooldown); // 1초
        isAbleDash = true;

        Debug.Log(&quot;Dash Cool Time Finished&quot;);
    }</code></pre>
<br/>
<br/>

<h3 id="실행-결과">실행 결과</h3>
<p><img src="https://velog.velcdn.com/images/will-big/post/1d133fb4-29ff-4f49-a4ba-0a68e240c033/image.gif" alt=""></p>
<p><img src="https://velog.velcdn.com/images/will-big/post/fb0f8996-fa0e-4ce1-8bc7-25f22cddc385/image.gif" alt=""></p>
<p>다른 실행 장면을 각기 녹화한것이라 결과의 모습이 다르지만 잘 되는 걸 확인 할 수 있다.</p>
]]></description>
        </item>
    </channel>
</rss>