<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>개발 기록.</title>
        <link>https://velog.io/</link>
        <description>And I'm ready to dive</description>
        <lastBuildDate>Thu, 08 May 2025 15:56:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>개발 기록.</title>
            <url>https://velog.velcdn.com/images/rea-d2dive/profile/887da084-6f55-41cf-b99d-689309e8b082/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 개발 기록.. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/rea-d2dive" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[바이브 코딩에 대한 짧은 체험기 (피그마 플러그인 제작)]]></title>
            <link>https://velog.io/@rea-d2dive/%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9-%EC%B2%B4%ED%97%98%EA%B8%B0-%ED%94%BC%EA%B7%B8%EB%A7%88-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EC%A0%9C%EC%9E%91</link>
            <guid>https://velog.io/@rea-d2dive/%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9-%EC%B2%B4%ED%97%98%EA%B8%B0-%ED%94%BC%EA%B7%B8%EB%A7%88-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EC%A0%9C%EC%9E%91</guid>
            <pubDate>Thu, 08 May 2025 15:56:18 GMT</pubDate>
            <description><![CDATA[<h2 id="intro">Intro</h2>
<p>요즘 Threads에서 IT 분야 종사자들이 나누는 지식과 경험을 자주 찾아 보고 있다.
AI 활용법에 대한 글과 함께 <code>바이브 코딩</code>이라는 용어가 자주 등장하는데, <code>비전공자가 며칠 만에 바이브 코딩으로 앱을 만들다</code>, <code>개발자의 미래가 어둡다</code>와 같은 주제의 글에 눈길이 많이 간다.
SW 개발자로 취업을 준비하는 현 상황에 미래에 대한 불안을 키우는 내용이라 그렇지 않나 싶다.</p>
<p>그래서 바이브 코딩을 직접 체험해봤다. 코딩을 모르는 사람의 입장에서 프롬프트만으로 쓸만한 도구를 만들 수 있을 지 알아보고 싶었다. threads 글에서는 cursor를 많이 사용하길래, cursor pro를 결제하여 바이브 코딩에 활용해봤다.</p>
<p>만들고자 한 것은 피그마 플러그인이다. 이력서를 피그마로 만들어 관리하고 있는데, 하이퍼링크 URL이 달라졌을 때 이를 수정하는 것이 번거로웠다. 그래서 링크를 한 곳에서 관리해주는 플러그인이 있으면 좋겠다는 생각을 했고, cursor로 만들게 되었다.</p>
<h2 id="결과물">결과물</h2>
<p>총 572줄의 html과 243줄의 ts 코드를 생성하여 플러그인을 만들었다.</p>
<p><img src="https://velog.velcdn.com/images/rea-d2dive/post/418baf21-9f52-4196-ad02-9d130930b1b1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rea-d2dive/post/00feb840-08aa-4c56-a7e2-82e3e71fec36/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rea-d2dive/post/8b16db6a-9952-4142-80d3-522710b51852/image.png" alt=""></p>
<p>플러그인으로 링크를 수정하면 기존 텍스트 노드의 하이퍼링크가 변경된다.
사용되는 링크를 한 곳에서 볼 수 있어 나름 쓸 만 했다.</p>
<h2 id="느낀-점">느낀 점</h2>
<p>요구사항은 최대한 구체적으로 작성해야 한다.
어떻게 보면 당연한 말이지만, 구체적이지 않으면 내가 원하는 결과가 나오지 않는다. AI는 내가 말하지 않아도 알아서 해 주는 마법사같은 것이 아니니까.</p>
<p>한번에 많은 일을 시키면 안된다. 요구사항 중 빼먹는 것이 생기고, 퀄리티가 보장이 안된다. 문제가 생기면 문제를 파악하는 것도 어려워진다.</p>
<p>Cursor Pro를 1개월 구독하면 Premium Request 500건이 무료로 주어진다. 요청 제한을 고려하지 않고 chat을 사용한 결과 약 90건의 Premium Request를 사용하여 플러그인을 만들었다. 간단한 프로그램을 만드는 수 시간동안 약 20%의 할당량을 사용한 것이다.
요청을 효율적으로 하는 것이 중요하다고 느꼈다. 요청을 구체적으로, 작게 지시하여 결과의 퀄리티를 높여야 재지시하는 일을 줄일 수 있다. cursor의 경우, 이전 프롬프트를 참고하게 하거나 rules를 사용하여 요청의 효율을 높이는 방법을 추가로 활용할 수 있다.</p>
<p>페어프로그래밍에는 내비게이터와 드라이버가 존재한다. 내비게이터는 지시하는 사람, 드라이버는 지시를 받아 코드를 작성하는 사람이다. cursor 같은 도구는 드라이버로 활용해야 SW의 기능, 코드의 퀄리티, AI 서비스 비용 측면에서 괜찮은 결과를 얻을 수 있는 것 같다는 생각을 해 본다. cursor를 사용하는 우리는 내비게이터로서 개발 주도권을 쥐고 있어야 한다.</p>
<p>코딩을 아예 모르는 사람이 프롬프트만으로 좋은 SW를 만드는 것은 어렵다고 생각한다. 요구사항을 구체적으로 제시하지 못하기 때문이다. 만들고 싶은 기능이 명확해도, 이를 어떻게 구현해야 할 지도 내비게이터로서 명확하게 제시할 수 있을까?</p>
<p>주니어 개발자로서 나는 AI 도구의 등장과 발전과 상관 없이 개발 기본기 및 숙련도를 올리고, 도구에 휘둘리는 사람이 아닌 도구를 휘두르는 사람이 되어야겠다.
AI로 개발에 과감히 뛰어들되, 주도권을 빼앗기지 말자.</p>
<h2 id="개발자로서-ai-도구를-활용할-때-읽어-보면-좋을-아티클">개발자로서 AI 도구를 활용할 때 읽어 보면 좋을 아티클</h2>
<ul>
<li><a href="https://maily.so/josh/posts/2nznwx6gzp5">https://maily.so/josh/posts/2nznwx6gzp5</a></li>
<li><a href="https://haebom.dev/vibecoding?full=1">https://haebom.dev/vibecoding?full=1</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Union-Find 알고리즘의 최적화 기법]]></title>
            <link>https://velog.io/@rea-d2dive/Union-Find-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%98-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B8%B0%EB%B2%95</link>
            <guid>https://velog.io/@rea-d2dive/Union-Find-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%98-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B8%B0%EB%B2%95</guid>
            <pubDate>Wed, 23 Apr 2025 10:00:18 GMT</pubDate>
            <description><![CDATA[<h1 id="단순한-union-find-알고리즘">단순한 Union-Find 알고리즘</h1>
<pre><code class="language-python">def find_parent(a):
    if a == parent[a]:
        return a
    return find_parent(parent[a])

def union_parent(a, b):
    a = find_parent(a)
    b = find_parent(b)
    if a &lt; b:
        parent[b] = a
    else:
        parent[a] = b

v, e = map(int, input().split())
parent = list(range(v+1))

for _ in range(e):
    a, b = map(int, input().split())
    union_parent(a, b)</code></pre>
<h1 id="경로-압축">경로 압축</h1>
<h2 id="문제">문제</h2>
<p>현재 노드의 루트 노드를 find할 때마다 모든 직계 조상 노드를 탐색해야 한다.
(a,b)가 노드 a와 b 사이에 간선이 있음을 의미할 때, (3,4), (2,3), (1,2)가 주어지면 트리가 다음과 같이 구성된다.
<img src="https://velog.velcdn.com/images/rea-d2dive/post/f626b6e8-ecf3-4faa-925a-63d9d444c912/image.png" width="50%" height="50%"></p>
<p>다음으로 (4,5)가 주어지면, 4번 노드는 루트 노드를 찾기 위해 3 -&gt; 2 -&gt; 1을 거쳐 탐색해야 1번 노드를 찾을 수 있다.</p>
<p>경로 압축 기법을 추가하면 이러한 비효율을 줄일 수 있다.</p>
<h2 id="find-재귀-호출하면서-부모-테이블-갱신하기">find 재귀 호출하면서 부모 테이블 갱신하기</h2>
<p>find 함수를 다음과 같이 수정하면 된다.</p>
<pre><code class="language-python">def find_parent(a):
    if a != parent[a]:
        parent[a] = find_parent(parent[a])
    return parent[a]</code></pre>
<p>find 도중에 거쳐가는 non-root internal 노드의 부모도 루트 노드가 되도록 트리가 재구성되는 효과가 있다. 마지막으로, 모든 노드에 대해 find를 해 주면 트리의 깊이가 1이 되도록 완전히 재구성된다.</p>
<h1 id="rank">Rank</h1>
<h2 id="문제-1">문제</h2>
<p>경로 압축을 적용해도, (3,4), (2,3), (1,2)를 순서대로 입력했을 때 트리가 일직선으로, 비효율적으로 구성되는 문제는 피할 수 없다. (<em>트리 구성 이후에 모든 노드에 대해 find 한 번씩 해 주면 트리가 평탄화되기는 한다.</em>)</p>
<p>union으로 서로의 root를 연결하는 작업은 크게 보면 트리와 트리를 이어 붙이는 것이다. 트리의 깊이를 알고 있다면, 깊이가 작은 트리를 깊이가 큰 트리의 루트에 이어 붙여 깊이가 깊어지는 현상을 최소화할 수 있다.</p>
<h2 id="union-시-rank로-부모-선택하기">Union 시 Rank로 부모 선택하기</h2>
<p>union 함수를 다음과 같이 수정하면 된다.</p>
<pre><code class="language-python">def union_parent(a, b):
    a = find_parent(a)
    b = find_parent(b)
    if ranks[a] &lt; ranks[b]:
        a, b = b, a
    elif ranks[a] == ranks[b]:
        ranks[a] += 1
    parents[b] = a</code></pre>
<p><code>rank[i]</code>는 i번째 노드를 루트 노드로 하는 트리의 깊이를 나타낸다.
기존 union 함수에서는 단순히 노드 번호가 작은 것이 부모 노드가 되게 했다면, 변경된 함수에서는 깊이가 작은 트리의 루트 노드가 깊이가 큰 트리의 루트 노드에 붙게 되어 전체 트리의 깊이가 최소한으로 늘어난다.
<img src="https://velog.velcdn.com/images/rea-d2dive/post/c1655542-36eb-4e5b-9c44-f04dd94c4421/image.png" width="50%" height="50%"></p>
<p><img src="https://velog.velcdn.com/images/rea-d2dive/post/a6836f94-5e3b-4122-996d-2fb64ece923e/image.png" alt=""></p>
<h1 id="주의-사항">주의 사항</h1>
<p>경로 압축이나 rank 기법을 도입하더라도, 트리 구성이 종료된 이후에 각 노드에 대해 find를 한 번씩 해 주어야 모든 노드의 부모 노드가 루트 노드로 재설정된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[REST API가 무엇인지 설명해보세요.]]></title>
            <link>https://velog.io/@rea-d2dive/REST-API%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EC%A7%80-%EC%84%A4%EB%AA%85%ED%95%B4%EB%B3%B4%EC%84%B8%EC%9A%94</link>
            <guid>https://velog.io/@rea-d2dive/REST-API%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EC%A7%80-%EC%84%A4%EB%AA%85%ED%95%B4%EB%B3%B4%EC%84%B8%EC%9A%94</guid>
            <pubDate>Wed, 19 Mar 2025 19:02:33 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>TL;DR</p>
</blockquote>
<ul>
<li>REST 아키텍처 스타일을 따르는 API.</li>
<li>REST란 분산 하이퍼미디어 시스템을 위한 아키텍처 스타일로, 시스템이 상호운용성과 확장성을 지니게 하기 위한 조건을 제안한다.</li>
<li>통상적으로 REST API라고 부르는 것들은 REST를 온전히 지키지 않는 경우가 많은 것 같다. REST를 잘 지켰다는 것을 강조하고자 한다면 RESTful API로 부르는 것이 어떨까.</li>
</ul>
<h1 id="그런-rest-api로-괜찮은가">그런 REST API로 괜찮은가</h1>
<p>웹 개발 동아리에 들어가서 스터디할 때, 1~2주차 스터디 커리큘럼에 포함된 내용이었다.
REST API에 대해 조사하면서 <a href="https://tv.naver.com/v/2292653?playlistNo=168686">Naver D2 - 그런 REST API로 괜찮은가</a> 강연 영상을 봤던 기억이 있다.
그때 당시에는 REST API는 고사하고 간단한 HTTP API도 만들어 본 적이 없었던지라, 수박 겉핥기 식으로만 보고 넘겼었다.</p>
<p>면접 스터디를 하면서 &quot;REST API가 무엇인가요?&quot; 라는 질문을 마주하였고, 이에 답하기 위해 22년 봄에 봤던 영상을 25년 봄에 다시 보면서 REST에 대한 개념과 나의 생각을 정리하고자 한다.</p>
<h1 id="rest란">REST란?</h1>
<p><strong>RE</strong>presentational <strong>S</strong>tate <strong>T</strong>ransfer의 약자. <code>분산 하이퍼미디어 시스템을 위한 아키텍처 스타일</code>이다.</p>
<p>그렇다면 <strong>분산 하이퍼미디어 시스템</strong>이란?
하이퍼텍스트는 문서 내에서 링크를 통해 다른 문서로 이동할 수 있는 개념이다.
<strong>하이퍼미디어</strong>는 하이퍼텍스트에 이미지, 영상 등의 요소가 포함된 개념이다.
<strong>분산 시스템</strong>이란 여러 개의 독립된 컴퓨터가 네트워크를 통해 원격으로 연결된 시스템을 의미한다.
따라서, <strong>분산 하이퍼미디어 시스템</strong>은 분산 시스템에서 하이퍼미디어를 공유하는 것으로, <strong>웹</strong>이 대표적이다.</p>
<p>90년대 후반까지만 하더라도, 웹에 사용되던 API는 일관성이 떨어졌다. 제각기 다른 형식의 API를 사용했고, 서버가 달라지면 API도 달라져서 클라이언트 측에서 이를 해석하는데 힘이 들었다. API뿐만 아니라, 웹을 구성하는 다른 요소도 웹을 확장 가능하지 않게 만드는 방향으로 구현되고 있었다.</p>
<p>그래서 로이 필딩은 웹이 일관된 방식으로 사용될 수 있게, 확장 가능하게 하기 위해서 REST 스타일을 만들었다.</p>
<p>REST를 구성하는 스타일에는 다음과 같은 것이 있다.</p>
<ul>
<li><strong>client-server</strong><ul>
<li>클라이언트와 서버의 역할을 분리하고, 서로가 독립적으로 동작할 수 있게 설계되어야 한다.</li>
</ul>
</li>
<li><strong>stateless</strong><ul>
<li>서버는 클라이언트를 기억하지 않는다. 이전 요청과 상관없이 다음 요청을 처리해야 한다. 클라이언트가 서버에 종속되면 확장성에 불리해진다.</li>
<li>서버가 이전 요청을 기억하지 않기 때문에 클라이언트는 매 요청마다 세션을 담아 보내야 한다.</li>
</ul>
</li>
<li><strong>cache</strong></li>
<li><strong>uniform interface</strong></li>
<li><strong>layered system</strong><ul>
<li>클라이언트와 서버 사이에 여러 계층(프록시, 로드밸런서, etc.)을 둘 수 있어야 한다. 클라이언트는 서버 사이에 무엇이 있는지 알 필요가 없게 한다.</li>
</ul>
</li>
<li><strong>code-on-demand</strong> (optional)<ul>
<li>서버가 클라이언트로 코드를 전송하여 실행할 수 있는 개념. javascript 파일을 링크로 거는 일반적인 방식을 말하는 것이 아니고, HTTP 응답에 js 코드를 문자열로 보내는 느낌이다.</li>
</ul>
</li>
</ul>
<p>API를 설계할 때 특히 주목해야 할 요소가 바로 <strong>Uniform Interface</strong>이다.</p>
<h3 id="uniform-interface">Uniform Interface</h3>
<p>4가지 요소가 존재한다.</p>
<ul>
<li>Identification of Resources<ul>
<li>모든 리소스가 고유한 URI를 가져야 한다.</li>
</ul>
</li>
<li>Manipulation of Resources through Representations<ul>
<li>리소스를 조작하는 행위가 표현되어야 한다.</li>
<li>클라이언트가 서버로부터 받은 자원의 표현(JSON 등)을 수정해 다시 전송함으로써 자원의 상태를 간접적으로 변경하는 방식</li>
<li>클라이언트가 서버의 내부 구조를 모르고도 자원을 조작할 수 있게 함</li>
</ul>
</li>
<li>Self-descriptive Messages<ul>
<li>요청과 응답이 자체적으로 필요한 정보를 포함해야 한다.</li>
<li>응답이 어디서 왔는지(Host header), 내용을 어떤 방식으로 해석해야 하는지(Content-Type header) 등의 부가 정보를 포함해야 한다.</li>
<li>클라이언트가 서버의 내부 구조를 모르고도 자원을 조작할 수 있게 함</li>
</ul>
</li>
<li>Hypermedia As The Engine Of Application State (HATEOAS)<ul>
<li>클라이언트가 서버의 상태를 예측하지 않고도, 응답을 보고 가능한 동작을 알 수 있어야 한다.</li>
<li>서버가 응답할 때 현재 리소스와 관련된 추가 링크(Hypermedia)를 포함해야 하며, 클라이언트는 이를 보고 다음 동작을 알 수 있다.</li>
<li>클라이언트가 서버의 내부 구조를 모르고도 자원을 조작할 수 있게 함</li>
</ul>
</li>
</ul>
<p>리소스라는 개념이 나와서 추가로 정리한다.
<strong>리소스</strong>란, <code>웹에서 다룰 수 있는 모든 데이터 대상</code>, <code>식별하고자 하는 무언가</code>를 의미한다. 게시판 서비스에서는 사용자, 게시글, 댓글 등에 해당될 수 있을 것이다. 이들 대상은 고유한 URI로 식별되어야 한다.</p>
<h3 id="rest는-api를-위한-것인가">REST는 API를 위한 것인가?</h3>
<p>아니다.
API는 분산 하이퍼미디어 시스템을 구성하는 하나의 요소이며, REST라는 거대한 스타일을 지키기 위해 고려해야 할 여러 요소 중 하나일 뿐이다.</p>
<p><img src="https://velog.velcdn.com/images/rea-d2dive/post/e8964382-6fd4-40b4-8580-b3706869c622/image.png" alt=""></p>
<h1 id="rest-api란">REST API란?</h1>
<p>REST API란, REST 아키텍처 스타일을 따르도록 설계된 API이다.
API가 REST 스타일을 따르게 하려면 적어도 <code>Identification of Resources</code>, <code>Manipulation of Resources through Representations</code>, <code>Self-descriptive Messages</code>, <code>HATEOAS</code> 는 만족 시켜야 할 것이다.</p>
<p>그러나, 실무적인 의미에서 REST API는 약간 다른 것 같다.
흔히 REST API를 구현한다고 하면, 다음과 같은 규칙을 따른다.</p>
<ul>
<li>URI는 동사보다는 명사로 작성한다.</li>
<li>단일 리소스는 단수 명사를, 여러 개의 리소스는 복수 명사를 사용하여 표현한다.</li>
<li>자원에 대한 행위는 HTTP 메서드(GET, PUT, POST, DELETE 등)로 표현한다.</li>
<li>소문자를 사용하고 합성어의 경우 hyphen(-)을 사용한다.</li>
</ul>
<p>위와 같은 규칙은 Uniform Interface의 <code>Identification of Resources</code>, <code>Manipulation of Resources through Representations</code>을 지키게 도와주고, URI의 가독성을 올려주는 효과가 있다. 하지만, <code>Self-descriptive</code>나 <code>HATEOAS</code>는 고려하지 않는 규칙이므로 REST 스타일을 온전히 지킨 API라고는 말하기 힘들다.</p>
<p>그래서 RESTful이라는 용어가 나오지 않았을까 싶다. 원칙적인 개념과 실무에서 사용되는 개념이 달라서.
REST API를 실무적인 의미에서 통상적으로 지켜지는 관습이나 규칙을 따른 API로 본다면, 이는 REST 스타일을 완전히 지켰다고 보기 힘든 경우가 많으므로, RESTful API가 REST 스타일을 온전히 지킨 API를 의미하는 용어가 되게 한 것이 아닌가.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[The Square-Rectangle Problem를 만나다]]></title>
            <link>https://velog.io/@rea-d2dive/The-Square-Rectangle-Problem%EB%A5%BC-%EB%A7%8C%EB%82%98%EB%8B%A4</link>
            <guid>https://velog.io/@rea-d2dive/The-Square-Rectangle-Problem%EB%A5%BC-%EB%A7%8C%EB%82%98%EB%8B%A4</guid>
            <pubDate>Tue, 21 Jan 2025 18:26:19 GMT</pubDate>
            <description><![CDATA[<p>⟪이펙티브 타입스크립트⟫를 읽던 중, 다음과 같은 코드 조각을 보았다.</p>
<pre><code class="language-ts">interface Square {
  width: number;
}

interface Rectangle extends Square {
  height: number;
}</code></pre>
<p>보통 정사각형은 직사각형의 부분집합으로 본다. 모든 정사각형은 직사각형이기 때문이다.
그런데 위의 코드에서는 직사각형이 정사각형의 서브클래스가 되어 있었다.</p>
<p>...?</p>
<p>정사각형은 width 하나만 있으면 되고, 직사각형은 여기에 더해 height가 있어야 하니, 직사각형이 정사각형을 상속하여 만들어지는 것이 머리로는 이해가 된다만, 현실 세계에서의 부분집합 관계와 반대가 되어 헷갈린다.</p>
<p>정사각형 클래스가 직사각형 클래스의 서브 클래스가 되도록 객체 관계를 모델링해봐야겠다.</p>
<pre><code class="language-ts">class Rectangle {
    protected x: number;
    protected y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    get area() {
        return this.x * this.y
    }
    get width() {
        return this.x
    }
    set width(w: number) {
        this.x = w
    }
    get height() {
        return this.y
    }
    set height(h: number) {
        this.y = h
    }
}

class Square extends Rectangle {
    constructor(x: number) {
        super(x, x)
    }
    set width(w: number) {
        this.x = w
        this.y = w
    }
    set height(h: number) {
        this.y = h
        this.x = h
    }
}</code></pre>
<p>직사각형을 상속하여 정사각형을 만들어 보았다.
그런데 이렇게 하면 또 다른 문제가 발생한다.</p>
<h1 id="the-square-rectangle-problem">The Square-Rectangle Problem</h1>
<p>정사각형은 직사각형의 부분집합이다.
다시 말해, 정사각형은 직사각형처럼 동작해야 한다.</p>
<p>바로 위의 코드에 이어지는 부분이다.</p>
<pre><code class="language-ts">const rec: Rectangle = new Square(20)
rec.height // 20
rec.width // 20
rec.width = 10
rec.area // 100??</code></pre>
<p>정사각형은 직사각형처럼 동작해야 한다.
<code>rec</code>은 직사각형이다. 너비만 20에서 10으로 바꿨는데, 넓이가 200이 아닌 100이 되어 버렸다.
직사각형의 동작으로 이해할 수 없는 일이 일어났다.</p>
<p>이 문제를 _The Square-Rectangle Problem_이라고 한다.
현실 세계의 통념에 따라 직사각형을 상속하여 정사각형을 만들었는데, 정사각형이 직사각형을 대체할 수 없게 되어 버렸다.</p>
<p>이 문제는 내가 모델링한 정사각형 클래스와 직사각형 클래스가 리스코프 치환 원칙을 위배했기 때문에 발생한다고 볼 수 있다.</p>
<h1 id="liskov-substitution-principle">Liskov Substitution Principle</h1>
<p>리스코프 치환 원칙. Liskov Substitution Principle을 줄여 LSP로 표현하기도 한다. 객체 지향 설계 원칙 SOLID 중 L을 담당하는 규칙이다.</p>
<p><code>리스코프 치환 원칙</code>이란, <code>하위 타입이 상위 타입으로 교체되어도 동일한 동작을 수행해야 한다</code>는 것을 의미한다.</p>
<p>정사각형 클래스는 직사각형 클래스의 서브 클래스이므로, 직사각형 클래스처럼 사용해도 문제가 없어야 한다. 그러나 위의 예시에서 <code>rec</code>의 width, height setter는 직사각형의 setter처럼 동작하지 않는다.</p>
<p>잘못 설계된 객체 관계라고 볼 수 있다.
어떻게 해결해야 할까?</p>
<h1 id="해결법">해결법</h1>
<p>정사각형과 직사각형을 상속을 통해 객체로 만들었을 때 호환되지 않는다면, 굳이 상속을 사용하지 않아도 된다.</p>
<p>공통 속성을 인터페이스로 분리하고, 정사각형과 직사각형을 별도의 클래스로 만들면 LSP에 위배되지 않는다.</p>
<pre><code class="language-ts">interface Shape {
    readonly area: number;
}

class Rectangle implements Shape {
    protected x: number;
    protected y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    get area() {
        return this.x * this.y
    }
    get width() {
        return this.x
    }
    set width(w: number) {
        this.x = w
    }
    get height() {
        return this.y
    }
    set height(h: number) {
        this.y = h
    }
}

class Square implements Shape {
    protected x: number;
    constructor(x: number) {
        this.x = x
    }
    get area() {
        return this.x ** 2
    }
    get width() {
        return this.x
    }
    set width(w: number) {
        this.x = w
    }
    get height() {
        return this.x
    }
    set height(h: number) {
        this.x = h
    }
}</code></pre>
<p>이 방식의 단점은 크게 2가지가 있다.</p>
<ol>
<li>현실 세계의 정사각형과 직사각형의 관계를 표현할 수 없다. 이제 Square와 Rectangle은 부모 자식 관계가 아니다.</li>
<li>코드 중복이 늘어난다.</li>
</ol>
<h1 id="결론">결론</h1>
<p>현실 속의 포함 관계를 객체로 완전히 동일하게 표현하는데 한계가 존재한다는 사실을 알게 되었다.</p>
<p>상속을 통해 객체를 표현해야 한다면, 개념적으로 하위 타입일 때뿐만 아니라 객체의 행동까지도 완전히 대체할 수 있을 때 가능하다는 점을 기억하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[타입스크립트의 공변성, 반공변성, 초과 속성 검사]]></title>
            <link>https://velog.io/@rea-d2dive/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EA%B3%B5%EB%B3%80%EC%84%B1-%EB%B0%98%EA%B3%B5%EB%B3%80%EC%84%B1-%EC%B4%88%EA%B3%BC-%EC%86%8D%EC%84%B1-%EA%B2%80%EC%82%AC</link>
            <guid>https://velog.io/@rea-d2dive/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EA%B3%B5%EB%B3%80%EC%84%B1-%EB%B0%98%EA%B3%B5%EB%B3%80%EC%84%B1-%EC%B4%88%EA%B3%BC-%EC%86%8D%EC%84%B1-%EA%B2%80%EC%82%AC</guid>
            <pubDate>Sat, 18 Jan 2025 15:27:43 GMT</pubDate>
            <description><![CDATA[<p>타입스크립트에서 타입 간의 호환성을 다루는 개념인 공변성(covariant)과 반공변성(contravariant)이 적용되는 경우를 정리하고자 한다.
여기에 더해, 불변성(invariant)과 초과 속성 검사(Excess Property Check)도 같이 정리하겠다.</p>
<h2 id="공변성covariant">공변성(covariant)</h2>
<p>타입 A와 A의 서브타입 B가 있다고 하자.
A가 더 일반적인, 포괄적인 타입이므로 다음과 같이 예시를 들 수 있겠다.</p>
<ul>
<li><code>A -&gt; string | number</code>, <code>B -&gt; string</code></li>
<li><code>A -&gt; {age: 1}</code>, <code>B -&gt; {age: 1, name: &#39;john&#39;}</code></li>
</ul>
<p>A를 사용하는 모든 곳에서 B를 사용할 수 있다면 <strong>공변적</strong>이라고 할 수 있다.
다시 말해, 공변적이라는 것은 일반적인 타입이 구체적인 타입으로 대체될 수 있다는 의미이다.</p>
<p>타입스크립트는 기본적으로 공변성을 가질 때 타입 호환이 된다. 예를 들어:</p>
<pre><code class="language-ts">class Animal {}
class Dog extends Animal {}

const animals: Animal[] = [new Animal()];
const dogs: Dog[] = [new Dog()];

const moreAnimals: Animal[] = dogs;</code></pre>
<p><code>Dog[]</code> 타입은 <code>Animal[]</code>의 서브 타입이고, 서브 타입은 수퍼 타입에 할당될 수 있다. 공변성을 따르기 때문이다.</p>
<p>서로 다른 두 함수 시그니처에서 반환 타입이 포함 관계에 있을 때, 함수 할당 시 공변성을 따르는 예시이다:</p>
<pre><code class="language-ts">type Foo = () =&gt; string
type Bar = () =&gt; string | number

const f: Foo = () =&gt; &#39;this is foo&#39;

const b: Bar = f</code></pre>
<p>위 예시를 직관적으로 이해해보자.
변수 <code>b</code>에 <code>Bar</code> 타입을 명시함으로써, <code>string</code>이나 <code>number</code>를 반환하는 함수여야 한다는 것을 나타냈고, 여기에 <code>string</code>을 반환하는 함수인 <code>f</code>를 할당했다.
<code>string</code>이나 <code>number</code>를 반환하는 함수여야 하는데, <code>string</code>을 반환하는 함수를 할당했으니 아무런 문제가 없다. 공변성을 따라도 괜찮다!</p>
<p>공변성은 타입이 Output으로서 사용되었을 때에만 적용된다.</p>
<h2 id="반공변성contravariant">반공변성(contravariant)</h2>
<p>반공변성은 공변성의 반대다.
구체적인 타입이 일반적인 타입으로 대체될 수 있다는 의미가 된다.</p>
<p>타입스크립트에서는 함수의 매개변수가 반공변성을 따른다.</p>
<pre><code class="language-ts">type SpecificHandler = (input: { a: string; b: string }) =&gt; void;
type GeneralHandler = (input: { a: string }) =&gt; void;

const handler: GeneralHandler = (input) =&gt; console.log(input.a);

const specificHandler: SpecificHandler = handler;</code></pre>
<p>매개변수가 더 일반적인 handler 함수를 구체적인 매개변수를 요구하는 변수에 할당했다.
구체적인 타입을 일반적인 타입에 할당하는 경우와 반대이므로 반공변성이라고 할 수 있다.</p>
<p>이 또한 직관적으로 이해해보자.
<code>GeneralHandler</code> 타입을 갖는 handler 함수는 매개변수로 <code>{ a: string }</code>만을 필요로 한다. a만 있다면, 다른 속성이 존재해도 아무 상관이 없다.
<code>SpecificHandler</code> 타입을 갖는 specificHandler 함수는 매개변수로 <code>{ a: string; b: string }</code>를 가질 것이다. 실제 값에 해당하는 handler 함수는 <code>{ a: string }</code>만 있으면 되기 때문에, <code>{ a: string; b: string }</code>를 전달받아도 문제될 것이 없다. <code>b</code>는 안 쓰면 되니까.</p>
<p>반대로, <code>GeneralHandler</code>를 <code>SpecificHandler</code>로 대체한다고 가정해보자.
실제로는 <code>a</code>속성과 <code>b</code>속성이 필요한 함수이지만, <code>GeneralHandler</code> 타입 정의에 따르면 매개변수로 <code>a</code> 속성만 넘기면 된다고 속이는 꼴이다.</p>
<pre><code class="language-ts">type SpecificHandler = (input: { a: string; b: string }) =&gt; void;
type GeneralHandler = (input: { a: string }) =&gt; void;

const handler: SpecificHandler = (input) =&gt; console.log(input.a, input.b);

const generalHandler: GeneralHandler = handler;</code></pre>
<p>따라서, 함수의 매개변수는 반공변성을 따라야 한다.</p>
<p>반공변성은 타입이 Input으로서 사용되었을 때에만 적용된다.</p>
<h2 id="불변성invariant">불변성(Invariant)</h2>
<p>공변성을 따르지도, 반공변성을 따르지도 않는 경우에 불변성을 따른다고 한다.
Typescript와 Java의 경우가 약간 다르다.</p>
<p>앞서 제시된 공변성을 따르는 타입스크립트 예시를 Java로 바꿔 생각해보자.</p>
<pre><code class="language-java">List&lt;Animal&gt; animals = new ArrayList&lt;Cat&gt;(); // Compile Error
animals.add(new Dog());</code></pre>
<p>결론부터 말하면, Java는 불변성을 따르게 한다.
공변성을 허용하게 되면, Cat 리스트에 Dog를 추가할 수 있게 되는데, Java는 이런 런타임 타입 오류도 막고자 하기 때문이다.</p>
<pre><code class="language-ts">class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

const animals: Animal[] = [new Animal()];
const dogs: Dog[] = [new Dog()];

const moreAnimals: Animal[] = dogs;

moreAnimals.push(new Cat()) // ok</code></pre>
<p>공변성을 따르는 타입스크립트는 다르다. 런타임 시에 dogs 배열에 Cat을 추가할 수 있게 되는 것까지 막지 않는다. (moreAnimals는 Dog 배열이 아닌 Animal 배열로 간주되므로 문제가 없다고 하는 것이 정확한 표현 같다.)</p>
<h2 id="초과-속성-검사excess-property-check">초과 속성 검사(Excess Property Check)</h2>
<p>객체 리터럴을 사용하여 새로운 객체를 생성하는 경우, 대상 타입에 명시되지 않은 속성이 추가로 존재하거나 생략된 경우에 타입 에러를 던진다.
객체 속성이 정확히 동일해야 할당이 허용되므로 불변성을 띄는 것처럼 보이지만, 이는 초과 속성 검사에 의한 것이다.</p>
<pre><code class="language-ts">interface Person {
  name: string;
  age: number;
}

// Error: Object literal may only specify known properties, and &#39;job&#39; does not exist in type &#39;Person&#39;
const person: Person = { name: &quot;Alice&quot;, age: 25, job: &quot;SW developer&quot; };</code></pre>
<p>위 예시에서는 Person의 서브 타입을 갖는 객체 리터럴이 <code>person</code>에 할당되는 경우이므로 공변성을 따르지만, 초과 속성 검사에 의해 허용되지 않고 있다.</p>
<p>초과 속성 검사는 객체 리터럴을 바로 할당하지 않으면 수행되지 않는다.</p>
<pre><code class="language-ts">interface Person {
  name: string;
  age: number;
}
type WorkingPerson = {
    job: string
} &amp; Person

const workingPerson: WorkingPerson = { name: &quot;Alice&quot;, age: 25, job: &#39;SW Developer&#39; };

const person: Person = workingPerson</code></pre>
<p>코드 실행 동작은 완전히 동일하다.</p>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li><a href="https://youtu.be/FdFBYUQCuHQ?feature=shared">https://youtu.be/FdFBYUQCuHQ?feature=shared</a></li>
<li><a href="https://inpa.tistory.com/entry/TS-%F0%9F%93%98-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B3%B5%EB%B3%80%EC%84%B1-%EB%B0%98%EA%B3%B5%EB%B3%80%EC%84%B1-%F0%9F%92%A1-%ED%95%B5%EC%8B%AC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0">https://inpa.tistory.com/entry/TS-%F0%9F%93%98-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B3%B5%EB%B3%80%EC%84%B1-%EB%B0%98%EA%B3%B5%EB%B3%80%EC%84%B1-%F0%9F%92%A1-%ED%95%B5%EC%8B%AC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</a></li>
<li><a href="https://ramincoding.tistory.com/entry/TypeScript-%EC%B4%88%EA%B3%BC-%EC%86%8D%EC%84%B1-%EA%B2%80%EC%82%ACExcess-Property-Checks">https://ramincoding.tistory.com/entry/TypeScript-%EC%B4%88%EA%B3%BC-%EC%86%8D%EC%84%B1-%EA%B2%80%EC%82%ACExcess-Property-Checks</a></li>
<li><a href="https://velog.io/@koreanthuglife/%EB%A9%80%EB%A6%AC-%EB%B3%B4%EA%B8%B0%EC%9C%84%ED%95%9C-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8">https://velog.io/@koreanthuglife/%EB%A9%80%EB%A6%AC-%EB%B3%B4%EA%B8%B0%EC%9C%84%ED%95%9C-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[mysql2 execute로 bulk insert할 때 주의사항]]></title>
            <link>https://velog.io/@rea-d2dive/mysql2-execute%EB%A1%9C-bulk-insert%ED%95%A0-%EB%95%8C-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD</link>
            <guid>https://velog.io/@rea-d2dive/mysql2-execute%EB%A1%9C-bulk-insert%ED%95%A0-%EB%95%8C-%EC%A3%BC%EC%9D%98%EC%82%AC%ED%95%AD</guid>
            <pubDate>Sun, 29 Dec 2024 05:08:11 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>여러 개의 카드 리스트를 한꺼번에 insert하고 싶었다. 
찾아보니 bulk insert라는 기능이 있어 mysql2에서 지원하는 방식대로 데이터를 한꺼번에 전달해주려고 했다.</p>
<pre><code class="language-js">const cardLists = [
    { id: 1, name: &#39;To Do&#39; },
    { id: 2, name: &#39;Doing&#39; },
    { id: 3, name: &#39;Done&#39; },
]
const insertCardListSQL = `INSERT INTO card_list (id, name) VALUES ?`
await DB.execute(insertCardListSQL, [cardLists.map(({ id, name }) =&gt; [id, name])])</code></pre>
<p>실행했더니 다음과 같은 에러가 났다.</p>
<pre><code>Error: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near &#39;?&#39; at line 1
{
  code: &#39;ER_PARSE_ERROR&#39;,
  errno: 1064,
  sql: &#39;INSERT INTO card_list (id, name) VALUES ?&#39;,
  sqlState: &#39;42000&#39;,
  sqlMessage: &quot;You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near &#39;?&#39; at line 1&quot;
}</code></pre><p>이상했다. mysql2의 format으로 raw SQL을 만들어 보면 문법 오류가 없는 SQL문이 생성되는데, execute를 호출하면 에러가 났다.</p>
<p>구글링해서 <a href="https://github.com/sidorares/node-mysql2/issues/1244">github issue</a>를 발견했다. prepared statements를 만들 때 placeholder에 전달될 데이터의 개수를 몰라서 안된다는 식의 답변이 적혀있다.</p>
<p>prepared statement가 무엇인지를 알아봐야 이해할 수 있겠다.</p>
<h2 id="prepared-statements">Prepared Statements</h2>
<p>Prepared Statements는 SQL문의 데이터 부분에 placeholder라고 부르는 <code>?</code>이 들어간 불완전한 statement이다. <code>?</code> 부분에 들어갈 데이터는 분리하여 보관, 전달할 수 있다.</p>
<p>Prepared Statements를 사용하면 얻을 수 있는 이점으로 크게 2가지가 있다. <code>SQL Injection 공격 방지</code>, <code>성능 향상</code>이다. 이를 이해하기 위해서는 DBMS가 SQL 쿼리를 처리하는 과정을 알아야 한다.</p>
<p>DBMS는 SQL 쿼리를 실행할 때 다음과 같은 단계를 거친다.</p>
<ol>
<li>Parsing and Normalization Phase</li>
<li>Compilation Phase</li>
<li>Query Optimization Plan</li>
<li>Cache</li>
<li>Execution</li>
</ol>
<p>Prepared Statements가 전달되면, 1단계에서 4단계까지 진행된다. placeholder에 들어갈 데이터가 오지 않았기 때문에, 컴파일된 쿼리 상태로 캐싱되어있다가 데이터가 전달되면 데이터만 조립하여 바로 실행시킬 수 있는 상태가 되는 것이다.</p>
<p>Prepared Statements가 동일하면 캐싱되어있는 컴파일된 쿼리를 가져다 사용할 수 있다. 데이터만 끼워 넣고 실행시키면 된다. 1~3단계를 거치지 않으므로 효율적이다. =&gt; <strong>성능 향상</strong></p>
<p>SQL문을 파싱하고 컴파일하는 과정은 1, 2단계에서 일어난다. 이미 컴파일된 prepared statements에 SQL Injection을 유도하는 문자열을 집어넣어도, 이미 구문 분석이 끝났기 때문에 공격자가 유도한 SQL은 실행될 여지가 전혀 없다. =&gt; <strong>SQL Injection 공격 방지</strong></p>
<h2 id="결론">결론</h2>
<p>mysql2의 query 함수에서 사용 가능한 bulk insert는 MySQL이나 SQL의 문법이 아니고, mysql2에서 사용자 편의를 위해 만든 기능이다.(<code>?</code> 하나만 넣고 3차원 배열 전달하는 것)</p>
<p>mysql2의 execute는 쿼리를 prepare하고, 데이터를 전달하고, 최종적으로 실행시키는 과정을 하나의 함수로 합친 것이다. 
애초에 <code>INSERT INTO card_list (id, name) VALUES ?</code>는 올바는 SQL 문법이 아니며, 배열로 전달될 데이터의 길이가 얼마가 될 지는 쿼리를 prepare할 때 체크하지 않기 때문에 하나뿐인 <code>?</code>를 미리 데이터 수에 맞게 추가할 수 없는 것 같다.</p>
<p>그래서 bulk insert 할 때는 execute 대신 query를 사용하던지, 데이터의 수를 미리 아는 상황이라면 SQL문에 데이터 수만큼 <code>?</code>를 넣어주어야 한다.</p>
<p>mysql2를 사용하면 query를 사용해도 SQL Injection에 대응하기 위해 데이터에 대해 escape를 수행하기 때문에, 성능 목적이 아니면 그냥 query를 사용해도 좋을 것 같다. 성능 목적 상 bulk insert에 prepared statements 사용을 고려한다면, 데이터의 수가 유동적일 땐 <code>?</code>의 개수도 변해야 하므로 캐싱이 무용지물이 된다는 점을 유념해야 할 것이다.</p>
<h1 id="참고-자료">참고 자료</h1>
<ul>
<li><a href="https://stackoverflow.com/a/34126564/19380793">https://stackoverflow.com/a/34126564/19380793</a></li>
<li><a href="https://github.com/sidorares/node-mysql2/issues/1244">https://github.com/sidorares/node-mysql2/issues/1244</a></li>
<li><a href="https://sidorares.github.io/node-mysql2/docs/documentation/prepared-statements">https://sidorares.github.io/node-mysql2/docs/documentation/prepared-statements</a></li>
<li><a href="https://www.w3schools.com/php/php_mysql_prepared_statements.asp">https://www.w3schools.com/php/php_mysql_prepared_statements.asp</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[#!의 역할]]></title>
            <link>https://velog.io/@rea-d2dive/%EC%9D%98-%EC%97%AD%ED%95%A0</link>
            <guid>https://velog.io/@rea-d2dive/%EC%9D%98-%EC%97%AD%ED%95%A0</guid>
            <pubDate>Sun, 29 Dec 2024 05:02:50 GMT</pubDate>
            <description><![CDATA[<p>Ubuntu에서 bash script를 만들 때 아무 생각 없이 작성하던 <code>#!/bin/bash</code>. 무슨 의미인지 급 궁금해져서 자료 조사해봤다.</p>
<h2 id="는-shebang이다">#!는 shebang이다</h2>
<p>스크립트의 맨 윗줄에 위치한 <code>#!~</code> <strong>shebang</strong>이라는 것으로, 스크립트를 실행할 때 사용할 인터프리터를 지정하는 역할을 수행한다. 즉, <code>#!/bin/bash</code>는 본 스크립트를 bash shell로 실행하라는 의미이며, bash의 주소는 <code>/bin/bash</code>임을 나타낸다.</p>
<p><code>chmod +x script_file</code>로 스크립트를 executable로 만들 수 있다. executable로 실행시킬 때는 shebang이 없으면 본 스크립트를 어떻게 실행시켜야 하는지 모르기 때문에 shebang이 필요하다. 스크립트를 실행파일로 실행시키지 않는다면 꼭 넣어주지 않아도 된다. 예를 들어, <code>bash script_file</code> 명령어로 스크립트를 실행시키는 경우, <code>script_file</code>을 bash로 실행시킨다는 의미이므로 shebang이 필요하지 않다.</p>
<h2 id="usrbinenv-node의-의미"><code>#!/usr/bin/env node</code>의 의미</h2>
<p>간혹 <code>#!/usr/bin/env node</code>와 같은 shebang이 붙는 경우가 있다. express-generator를 사용하여 express.js skeleton app을 생성시킬 때 만들어지는 <code>./bin/www</code>에도 위와 같은 shebang이 붙는다.</p>
<p><code>env</code>는 현재 시스템의 환경 변수를 확인하거나 설정할 수 있는 유닉스 계열의 명령어다. 예를 들어, <code>/usr/bin/env node</code>를 입력하면 현재 시스템의 환경변수에서 node 위치를 찾아서 실행시킨다. 
즉, shebang으로 <code>#!/usr/bin/env node</code>를 사용하면 현재 스크립트를 환경변수에 저장된 위치의 node로 실행시킬 수 있게 되는 것이다.</p>
<p>이는 스크립트를 실행시킬 환경을 모를 때 유용하다. <code>bash가 /bin/bash</code>에 저장되어있는 것을 안다면 <code>#!/bin/bash</code>를 사용해도 된다. 그러나 bash가 <code>/somewhere/over/the/rainbow/bash</code>에 위치해있다면? 스크립트를 만든 사람에게 미리 알려주지 않는 이상, 알아낼 방법이 없다. 이때 <code>#!/usr/bin/env bash</code>를 사용해주면 스크립트를 실행시키는 내 운영체제는 <code>/somewhere/over/the/rainbow/bash</code>를 찾아서 실행시켜줄 것이다.</p>
<p>정리하자면, <code>#!/usr/bin/env node</code>는 스크립트 파일의 이식성을 높여주는 shebang 사용 방법이라고 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP/2가 HTTP/1.1보다 성능이 좋은 이유]]></title>
            <link>https://velog.io/@rea-d2dive/HTTP2%EA%B0%80-HTTP1.1%EB%B3%B4%EB%8B%A4-%EC%84%B1%EB%8A%A5%EC%9D%B4-%EC%A2%8B%EC%9D%80-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@rea-d2dive/HTTP2%EA%B0%80-HTTP1.1%EB%B3%B4%EB%8B%A4-%EC%84%B1%EB%8A%A5%EC%9D%B4-%EC%A2%8B%EC%9D%80-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 29 Dec 2024 04:56:06 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@rea-d2dive/HTTP-%EC%9A%94%EC%B2%AD%EA%B3%BC-%EC%9D%91%EB%8B%B5%EC%9D%98-%EC%88%9C%EC%84%9C%EB%8A%94-%EB%B0%98%EB%93%9C%EC%8B%9C-%EB%8F%99%EC%9D%BC%ED%95%9C%EA%B0%80">HTTP 요청과 응답의 순서는 반드시 동일한가?</a> 게시글을 작성하면서 HTTP/2의 multiplexing에 대해 알아보았다. 이번 글에서는 Multiplexing을 포함하여 HTTP/2의 성능 향상을 가능케 한 특징에 대해 간단히 정리하겠다.</p>
<p>정리가 잘 된 글이 있어 참고했다.
<a href="https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-HTTP-20-%ED%86%B5%EC%8B%A0-%EA%B8%B0%EC%88%A0-%EC%9D%B4%EC%A0%9C%EB%8A%94-%ED%99%95%EC%8B%A4%ED%9E%88-%EC%9D%B4%ED%95%B4%ED%95%98%EC%9E%90">https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-HTTP-20-%ED%86%B5%EC%8B%A0-%EA%B8%B0%EC%88%A0-%EC%9D%B4%EC%A0%9C%EB%8A%94-%ED%99%95%EC%8B%A4%ED%9E%88-%EC%9D%B4%ED%95%B4%ED%95%98%EC%9E%90</a></p>
<h2 id="http-header-compression">HTTP Header Compression</h2>
<ul>
<li>HTTP/2에서 Header를 Binary Frame으로 만들면서 압축 가능해짐.<ul>
<li>HPACK 압축 방식을 사용</li>
</ul>
</li>
<li>중복된 header 속성을 생략하여 패킷 크기 절약<ul>
<li>요청 여러 개 보낼 때, host나 scheme, user-agent와 같은 정보가 중복 전송되기 때문에, 해당 정보를 table에 저장하고 header에는 테이블의 인덱스만 보내면 데이터 중복을 최소화할 수 있음.</li>
</ul>
</li>
</ul>
<h2 id="http-multiplexing">HTTP Multiplexing</h2>
<ul>
<li>HTTP/2에서는 각 Frame을 Stream ID로 구분할 수 있으므로, 하나의 TCP connection에서 여러 요청과 응답(stream)을 순서 상관 없이 병렬로 보낼 수 있기 때문에 전체적인 Response Time이 감소됨.</li>
</ul>
<h2 id="server-push">Server Push</h2>
<ul>
<li>HTML이 CSS나 JS 파일, 이미지 파일 등을 참조하고 있는 경우, 서버가 이를 파악하여 클라이언트가 요청하기 전에 HTML과 같이 보내버림. n회의 RTT 감소</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP 요청과 응답의 순서는 반드시 동일한가?]]></title>
            <link>https://velog.io/@rea-d2dive/HTTP-%EC%9A%94%EC%B2%AD%EA%B3%BC-%EC%9D%91%EB%8B%B5%EC%9D%98-%EC%88%9C%EC%84%9C%EB%8A%94-%EB%B0%98%EB%93%9C%EC%8B%9C-%EB%8F%99%EC%9D%BC%ED%95%9C%EA%B0%80</link>
            <guid>https://velog.io/@rea-d2dive/HTTP-%EC%9A%94%EC%B2%AD%EA%B3%BC-%EC%9D%91%EB%8B%B5%EC%9D%98-%EC%88%9C%EC%84%9C%EB%8A%94-%EB%B0%98%EB%93%9C%EC%8B%9C-%EB%8F%99%EC%9D%BC%ED%95%9C%EA%B0%80</guid>
            <pubDate>Sun, 29 Dec 2024 04:53:23 GMT</pubDate>
            <description><![CDATA[<p>네이버 부스트캠프에서 웹 서버 구현 미션을 진행하면서, HTTP 요청의 순서와 응답의 순서가 반드시 일치하도록 만들어야 하는지 의문이 들었다. 이를 위해 조사한 내용이 본문에 정리되어 있다.</p>
<p>다음 이미지는 설계한 웹 서버 시스템 구조도이다.
<img src="https://velog.velcdn.com/images/rea-d2dive/post/c2c9750f-44a1-4505-b56d-aa1df4475aa7/image.png" alt=""></p>
<p>HTTP Socket은 Event Emitter로 구현되어 있으며, HTTP 요청 객체를 이벤트로 받는다.</p>
<pre><code class="language-ts">export class Server {
    private _server: net.Server
    private router: Router

    constructor() {
        this._server = net.createServer()
        this._server.on(&#39;connection&#39;, (socket) =&gt; {
            this.serverConnectionHandler(socket)
        })
        this.router = new Router()
    }

    private serverConnectionHandler(socket: net.Socket) {
        const httpSocket = new HTTPSocket(socket, this.router)
        const httpRequestParser = new HTTPRequestParser()
        socket.on(&#39;data&#39;, this.socketDataHandler(httpSocket, httpRequestParser))
    }

    private socketDataHandler(httpSocket: HTTPSocket, httpRequestParser: HTTPRequestParser) {
        return (data: Buffer) =&gt; {
            const requests = httpRequestParser.getCompleteRequests(data)
            for (const req of requests) {
                httpSocket.emit(&#39;request&#39;, req)
            }
        }
    }
  // ...
}
</code></pre>
<p>동일한 HTTP 소켓으로 <code>요청1</code>과 <code>요청2</code>를 보냈는데, <code>응답1</code>이 <code>응답2</code>보다 늦게 생성되는 경우를 가정해보자. 예를 들어, <code>요청1</code>은 처리되기까지 5초가 걸리고 <code>요청2</code>는 1초가 걸리는 상황이다.</p>
<p><strong>이때, <code>응답2</code>를 <code>응답1</code>보다 먼저 소켓에 write해도 될까?</strong></p>
<p><code>응답2</code>를 먼저 보낼 수 있다면 전체적인 Response Time을 낮출 수 있겠지만, 브라우저는 동일한 소켓에서 온 응답의 순서가 요청과 달라지는 경우를 처리할 수 있을지 확인이 필요하다.</p>
<h2 id="자료-조사">자료 조사</h2>
<h3 id="http11-pipelining">HTTP/1.1 Pipelining</h3>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Connection_management_in_HTTP_1.x#http_pipelining">https://developer.mozilla.org/en-US/docs/Web/HTTP/Connection_management_in_HTTP_1.x#http_pipelining</a>
<img src="https://velog.velcdn.com/images/rea-d2dive/post/300a798f-d970-4ff0-a31f-19dc1928a956/image.png" alt=""></p>
<p>이전 요청의 응답을 기다리지 않고 다음 요청을 곧바로 보낼 수 있게 하는 기술이다. 다만, 이전 요청의 응답이 늦어지면 다음 요청도 늦어지는 Head Of Line Blocking 문제와 HTTP Pipelining을 제대로 처리하지 못하는 buggy proxy 문제 때문에, modern browser는 pipelining을 사용하지 않는다.</p>
<p>브라우저는 대신 동일한 도메인에 한해 TCP 소켓을 최대 6개(브라우저마다 다름) 열어 요청을 병렬로 보내는 방법으로 response time을 줄인다. 브라우저의 최대 소켓 수 제한을 우회하려면 Domain Sharding을 사용할 수 있다.</p>
<blockquote>
<p><strong>Domain Sharding에 대한 간단 설명</strong>
<code>example.com</code> 도메인에 대해서는 기본적으로 6개까지 병렬 요청이 가능함. <code>aaa.example.com</code>, <code>bbb.example.com</code>을 모두 <code>example.com</code>으로 프록시되도록 조치하면, 브라우저에서는 <code>aaa.example.com</code>으로 최대 6개, <code>bbb.example.com</code>으로 최대 6개를 보낼 수 있으니, 이론상 최대 12개의 병렬 요청을 보낼 수 있게 됨.</p>
</blockquote>
<p>위 조사 결과를 바탕으로, <strong>HTTP/1.1의 경우에 HTTP Pipelining 사용 여부와 상관 없이, 서버는 동일한 소켓에 도착한 요청 순서대로 응답을 보내야 한다는 사실을 알 수 있다.</strong></p>
<h3 id="http2-multiplexing">HTTP/2 Multiplexing</h3>
<p><img src="https://velog.velcdn.com/images/rea-d2dive/post/12ce99f7-967d-4195-a232-b9afa57cce53/image.png" alt=""></p>
<p>HTTP/2에서는 Frame, Message, Stream이라는 개념이 존재한다.</p>
<p>하나의 HTTP 요청, 응답이 각각 하나의 Message가 된다. 이건 HTTP/1.1과 동일하다.</p>
<p>Frame은 Header frame, Data frame을 구분하는 최소 단위가 된다. Frame이 header인지 body인지는 Frame 내부 헤더로 파악할 수 있다. Frame의 길이도 헤더에 나와있다.</p>
<p>Stream은 HTTP 요청과 요청에 대한 응답을 의미한다. 모든 Frame에는 Stream ID가 포함되어 있기 때문에 Frame의 스트림을 구분할 수 있다.</p>
<p><strong>즉, <code>응답2</code>를 <code>응답1</code>보다 먼저 보내도 수신 측에서 <code>응답2</code>의 stream ID로 <code>요청2</code>에 대한 응답임을 알 수 있다.</strong></p>
<h2 id="결론">결론</h2>
<p>HTTP/1.1을 사용하면 같은 소켓에 도착한 요청의 순서에 따라 응답을 보내야 한다.
HTTP/2를 사용하면 같은 소켓에 도착한 요청의 순서에 상관 없이 응답을 보낼 수 있다.</p>
<p>HTTP/2의 다른 특징은 <a href="https://velog.io/@rea-d2dive/HTTP2%EA%B0%80-HTTP1.1%EB%B3%B4%EB%8B%A4-%EC%84%B1%EB%8A%A5%EC%9D%B4-%EC%A2%8B%EC%9D%80-%EC%9D%B4%EC%9C%A0">여기</a>에 정리해두었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TCP는 가상 회선 패킷 교환 방식을 사용하는가?]]></title>
            <link>https://velog.io/@rea-d2dive/TCP%EB%8A%94-%EA%B0%80%EC%83%81-%ED%9A%8C%EC%84%A0-%ED%8C%A8%ED%82%B7-%EA%B5%90%ED%99%98-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@rea-d2dive/TCP%EB%8A%94-%EA%B0%80%EC%83%81-%ED%9A%8C%EC%84%A0-%ED%8C%A8%ED%82%B7-%EA%B5%90%ED%99%98-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80</guid>
            <pubDate>Sun, 29 Dec 2024 04:49:06 GMT</pubDate>
            <description><![CDATA[<p>TCP에 대해 공부하면서 인터넷 자료를 찾다가 TCP와 UDP에 대해 표 형식으로 비교해놓은 자료를 많이 보았다.
TCP와 UDP의 패킷 교환 방식 차이점으로 TCP는 가상 회선 방식이고, UDP는 데이터그램 방식이라고 나와있다.</p>
<p>Computer Networking A Top Down Approach 7판, 3.5절에는 TCP가 가상회선 네트워크가 아니라는 내용이 나온다. 대치되는 내용을 보고 의문이 들었다. <strong>TCP가 진짜 가상 회선 패킷 교환 방식일까?</strong></p>
<h2 id="자료-조사">자료 조사</h2>
<h3 id="패킷-교환-방식">패킷 교환 방식</h3>
<p>컴퓨터 네트워크와 통신의 방식 중 하나로 현재 가장 많은 사람들이 사용하는 통신 방식이다. 작은 블록의 패킷으로 데이터를 전송하며 데이터를 전송하는 동안만 네트워크 자원을 사용하도록 하는 방법을 말한다. 정보 전달의 단위인 패킷은 여러 통신 지점(Node)을 연결하는 데이터 연결 상의 모든 노드들 사이에 개별적으로 경로가 제어된다. 이 방식은 통신 기간 동안 독점적인 사용을 위해 두 통신 노드 사이를 연결하는 회선 교환 방식과는 달리 짤막한 데이터 트래픽에 적합하다. (<a href="https://ko.wikipedia.org/wiki/%ED%8C%A8%ED%82%B7_%EA%B5%90%ED%99%98">위키피디아</a>)</p>
<h3 id="가상-회선-패킷-교환-방식">가상 회선 패킷 교환 방식</h3>
<p>패킷 교환 방식을 사용하지만 여러 노드 간에 논리적인 회선을 마련하여 해당 회선으로만 패킷이 전달되도록 하는 방식으로, 물리적으로 회선을 점유하는 회선 교환 방식과는 차이가 있다.</p>
<p>패킷을 전달하기 전에 사전에 Call Setup이 이루어지면, 라우터가 가상 회선을 사용하는 패킷을 식별할 수 있고, 특정 경로로만 라우팅하도록 설정되기에, 패킷이 나뉘어 전달되지만 모두 동일한 경로로 전달된다. 이러한 특성 덕분에 패킷의 순서가 달라질 일이 없다.</p>
<h2 id="결론">결론</h2>
<p>가상 회선 패킷 교환 방식은 TCP와 유사한 특징을 지닌다. 패킷을 전송하기 전에 양 단말 간 연결을 수립해야 하고, 패킷의 순서가 달라지지 않음이 보장된다.</p>
<p>그러나, TCP 프로토콜은 오직 종단 시스템에서만 동작하고, 라우터나 브리지 등의 중간 네트워크 요소에는 아무런 영향을 끼치지 않는다. 라우터는 TCP에 대해 모른다는 뜻이다. 위의 가상 회선 패킷 교환 방식과는 차이가 있다.</p>
<p>TCP 세그먼트를 담는 IP 데이터그램은 반드시 동일한 경로로만 전송되진 않는다. 그렇기 때문에, TCP가 호스트 측에서 누락된 세그먼트를 감지하는 작업을 수행해주는 것이다.</p>
<p>따라서, TCP는 가상 회선 패킷 교환 방식이 아니라고 생각한다.</p>
<p>P.S. &lt;기술 면접 대비 CS 전공 핵심요약집&gt;이라는 책에 TCP가 가상 회선 패킷 교환 방식이라고 정리한 표가 나온다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Puppeteer Browsers API로 설치한 Chrome 실행 시 shared libraries: libnss3.so를 찾지 못하는 문제]]></title>
            <link>https://velog.io/@rea-d2dive/Puppeteer-Browsers-API%EB%A1%9C-%EC%84%A4%EC%B9%98%ED%95%9C-Chrome-%EC%8B%A4%ED%96%89-%EC%8B%9C-shared-libraries-libnss3.so%EB%A5%BC-%EC%B0%BE%EC%A7%80-%EB%AA%BB%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@rea-d2dive/Puppeteer-Browsers-API%EB%A1%9C-%EC%84%A4%EC%B9%98%ED%95%9C-Chrome-%EC%8B%A4%ED%96%89-%EC%8B%9C-shared-libraries-libnss3.so%EB%A5%BC-%EC%B0%BE%EC%A7%80-%EB%AA%BB%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Sun, 29 Dec 2024 04:44:08 GMT</pubDate>
            <description><![CDATA[<h1 id="이전-상황">이전 상황</h1>
<p>브라우저 자동화 도구 Puppeteer로 데이터 스크레이핑을 하고 있었다.
배포 환경은 AWS Lambda이고, 개발 환경은 내 로컬 PC(Ubuntu 24.04.1 LTS, node v22.12.0)다.</p>
<p>Lambda로 puppeteer를 실행시키려면 경량화된 chromium binary가 필요했다. 이에 <a href="https://github.com/Sparticuz/chromium">서버리스 플랫폼용 chromium</a>을 사용하고 있었다.
로컬 환경에서는 굳이 서버리스 플랫폼용 chromium을 사용할 필요가 없으므로, puppeteer가 설치해주는 기본 chrome을 사용하기로 했다.</p>
<p>위 조건을 만족시키기 위해 브라우저 실행 시 <code>puppeteer-core</code>를 사용하고, 브라우저는 따로 설치하도록 했다.</p>
<pre><code class="language-ts">const browser = await puppeteer.launch({
        args: process.env.NODE_ENV === &#39;development&#39; ? [&#39;--no-sandbox&#39;] : chromium.args,
        defaultViewport: chromium.defaultViewport,
        executablePath:
            process.env.NODE_ENV === &#39;development&#39;
                ? await getExecutablePathFromCache()
                : await chromium.executablePath(),
        headless: process.env.NODE_ENV === &#39;development&#39; ? false : chromium.headless,
})

async function getExecutablePathFromCache() {
    const chromiumVersion = process.env.CHROMIUM_VERSION_REAL

    if (!chromiumVersion) {
        throw new Error(&#39;CHROMIUM_VERSION_REAL is not set&#39;)
    }

    // 설치된 브라우저 목록 가져오기
    const installedBrowsers = await browsers.getInstalledBrowsers({
        cacheDir: &#39;.&#39;,
    })

    // 설치된 브라우저 목록에서 크롬 브라우저를 찾아서 실행 파일 경로를 가져오기
    let executablePath = installedBrowsers.find(
        (browser) =&gt; browser.browser === &#39;chrome&#39; &amp;&amp; browser.buildId === chromiumVersion
    )?.executablePath

    // 실행 파일 경로가 없으면 크롬 브라우저를 설치하기
    if (executablePath === undefined) {
        try {
            const chromium = await browsers.install({
                cacheDir: `${process.cwd()}`,
                browser: browsers.Browser.CHROME,
                buildId: chromiumVersion,
            })
            executablePath = chromium.executablePath
        } catch (error) {
            console.error(&#39;Browser Installation Failed&#39;)
            throw error
        }
    }

    return executablePath
}
</code></pre>
<p>NODE_ENV가 development인 경우, 현재 프로젝트의 root dir에 puppeteer로 chrome을 설치하도록 만들었다.</p>
<h1 id="문제">문제</h1>
<p>chrome binary를 설치하고 puppeteer를 실행시켰더니, 다음과 같은 에러가 나왔다.</p>
<pre><code>Error: Failed to launch the browser process!
chrome/linux-131.0.6778.204/chrome-linux64/chrome: error while loading shared libraries: libnss3.so: cannot open shared object file: No such file or directory


TROUBLESHOOTING: https://pptr.dev/troubleshooting

    at Interface.onClose (/home/hwan/restock-notifier/dist/index.js:77482:16)
    at Interface.emit (node:events:536:35)
    at Interface.close (node:internal/readline/interface:526:10)
    at Socket.onend (node:internal/readline/interface:252:10)
    at Socket.emit (node:events:536:35)
    at endReadableNT (node:internal/streams/readable:1698:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:90:21)

Node.js v22.12.0</code></pre><p>libnss3.so 라이브러리가 없다고 나온다. </p>
<h1 id="해결-과정">해결 과정</h1>
<h2 id="chrome-의존성-설치하기">Chrome 의존성 설치하기</h2>
<p>libnss3.so는 Ubuntu에 설치되는 공유 라이브러리다. 정적 라이브러리와는 달리, 라이브러리가 프로그램에 포함되지 않고, 런타임에 참조되어 사용된다고 한다.</p>
<p>Chrome을 실행하기 위해 libnss3 공유 라이브러리가 필요한 것이 아닌지 의심이 되었다. 공식 문서를 찾아보니, 다음과 같은 옵션을 발견할 수 있었다.
<img src="https://velog.velcdn.com/images/rea-d2dive/post/5989f78f-8c82-4caf-a0f3-57c76a44f1ea/image.png" alt=""></p>
<ul>
<li>docs 주소: <a href="https://pptr.dev/browsers-api/browsers.installoptions">https://pptr.dev/browsers-api/browsers.installoptions</a></li>
<li>puppeteer 버전: 23.11.1</li>
</ul>
<p>installOptions에 installDeps=true를 추가해주었다.</p>
<pre><code class="language-ts">const chromium = await browsers.install({
    cacheDir: `${process.cwd()}`,
    browser: browsers.Browser.CHROME,
    buildId: chromiumVersion,
    installDeps: true,
})</code></pre>
<h2 id="node-스크립트-root-권한으로-실행시키기">Node 스크립트 Root 권한으로 실행시키기</h2>
<p>installOptions에 installDeps=true를 추가하여 실행시켰더니 다음과 같은 에러가 나왔다.</p>
<pre><code>Error: Installing system dependencies requires root privileges
    at installDeps (/home/hwan/restock-notifier/dist/index.js:77735:11)
    at installUrl (/home/hwan/restock-notifier/dist/index.js:77785:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async install (/home/hwan/restock-notifier/dist/index.js:77715:18)
    at async getExecutablePathFromCache (/home/hwan/restock-notifier/dist/index.js:83916:25)
    at async Crawler.openTargetPage (/home/hwan/restock-notifier/dist/index.js:83950:64)
    at async Crawler.scrape (/home/hwan/restock-notifier/dist/index.js:83961:31)
    at async handler (/home/hwan/restock-notifier/dist/index.js:83989:3)

Node.js v22.12.0</code></pre><p>공식 문서를 보면, apt-get을 수행하기 위한 sudo 권한이 필요하다고 나와 있다.
그래서 node script를 sudo로 실행해봤는데, node를 찾지 못해서 <code>which node</code>로 node가 어디에 있는지 확인했고, <code>/home/hwan/.nvm/versions/node/v22.12.0/bin/node</code>에 있다는 것을 파악했다.</p>
<p>해당 node를 /usr/local/bin에 위치하도록 심볼릭 링크를 걸어주었다.
<code>sudo ln -s &quot;$NVM_DIR/versions/node/$(nvm version)/bin/node&quot; &quot;/usr/local/bin/node&quot;</code></p>
<p>이제 <code>sudo node run.js</code>를 입력하면 sudo 권한으로 node가 실행되고, installDeps=true 옵션에 따라 시스템 의존성을 설치할 수 있게 되었다.</p>
<h1 id="결과">결과</h1>
<p><code>puppeteer-core</code>와 별도 설치된 브라우저 바이너리를 같이 사용할 때, 브라우저 실행에 필요한 시스템 라이브러리를 자동으로 설치할 수 있게 되었다.</p>
<p>시스템 라이브러리를 자동으로 설치하도록 하기 위해 브라우저 설치 API를 사용할 때 installDeps 옵션을 추가했고, 이를 위해 nvm으로 node를 관리하는 상황에서 sudo 권한으로 node script를 실행할 수 있게 만들었음을 위 <code>해결 과정</code> 섹션에서 보였다.</p>
<p>시스템 라이브러리를 <code>sudo apt-get install ~~</code>와 같이 직접 설치해주는 방법도 가능했지만, 수동으로 설치해주어야 하는 의존성을 최대한 줄이고 싶어서 의존성 설치 과정을 자동화했다.</p>
]]></description>
        </item>
    </channel>
</rss>