<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>one_ik.log</title>
        <link>https://velog.io/</link>
        <description>초보 개발자의 블로그입니다</description>
        <lastBuildDate>Wed, 30 Oct 2024 03:32:58 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>one_ik.log</title>
            <url>https://velog.velcdn.com/images/one_ik/profile/204a2ca2-ef35-4dee-9ef1-b044a275bb96/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. one_ik.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/one_ik" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[인프라 아키텍처 소개]]></title>
            <link>https://velog.io/@one_ik/%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@one_ik/%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Wed, 30 Oct 2024 03:32:58 GMT</pubDate>
            <description><![CDATA[<h2 id="인프라-아키텍처-개요">인프라 아키텍처 개요</h2>
<p><img src="https://velog.velcdn.com/images/one_ik/post/6ddbc05a-9326-4682-82e0-cb0e8e614d6f/image.png" alt=""></p>
<p>ECS 배포 방식에 대해 설명하기 전에 프로젝트의 인프라 아키텍처를 살펴보겠습니다. 프로젝트는 React로 구현된 <strong>FrontEnd</strong>와 NestJS로 구현된 <strong>BackEnd</strong>로 구성되어 있으며, 두 애플리케이션 모두 <strong>AWS ECS</strong>를 통해 컨테이너화하여 배포하였습니다.</p>
<h2 id="주요-aws-서비스-구성">주요 AWS 서비스 구성</h2>
<ul>
<li>ECS: Frontend/Backend 컨테이너 실행 및 관리</li>
<li>ALB(Application Load Balancer): 트래픽 분산 및 라우팅</li>
<li>RDS(Relational Database Service): MySQl 데이터베이스 서버</li>
<li>CloudWatch: 로그 및 모니터링</li>
</ul>
<h2 id="alb를-통한-트래픽-관리">ALB를 통한 트래픽 관리</h2>
<p><strong>ALB</strong>(Application Load Balancer)는 들어오는 트래픽을 <strong>경로 기반</strong>으로 각각의 ECS서비스에 라우팅합니다:</p>
<ul>
<li>Frontend 요청(<code>/</code>): React 애플리케이션으로 라우팅</li>
<li>Backend 요청(<code>/api</code>): NestJS 서버로 라우팅</li>
</ul>
<h2 id="frontend를-배포할-때-vercel-대신-ecs를-선택했나">Frontend를 배포할 때 Vercel 대신 ECS를 선택했나?</h2>
<h3 id="1-인프라-관리의-일원화">1. 인프라 관리의 일원화</h3>
<p>서버가 분리되어 있으면 각각의 환경에맞는 설정과 모니터링 도구를 따로 구축해야 합니다. 하지만 <strong>ECS를 통해 통합 배포</strong>함으로써 <strong>CloudWatch</strong>로 모든 로그를 한 곳에서 <strong>모니터링</strong>할 수 있고, 장애 발생 시 문제를 더 빠르게 추적할 수 있습니다.</p>
<h3 id="2-확장성-고려">2. 확장성 고려</h3>
<p>트래픽이 증가하면 Frontend도 <strong>수평적 확장(Scale-Out)</strong>이 필요할 수 있습니다. ECS는 <strong>Auto Scaling</strong>을 통해 Frontend와 Backend 모두 트래픽에 따른 자동 확장이 가능하므로, 서비스의 안정성을 높일 수 있습니다.</p>
<h3 id="3-개발-및-운영-일관성">3. 개발 및 운영 일관성</h3>
<p>Frontend와 Backend의 배포 프로세스가 다르면 팀원들이 두 가지 배포 방식을 모두 학습해야 합니다. <strong>GitHub Actions</strong>와 <strong>ECS</strong>를 통한 통합 배포는 <strong>동일한 배포 프로세스</strong>를 가져갈 수 있어 팀의 생산성을 높일 수 있습니다.</p>
<h2 id="마무리">마무리</h2>
<p>간단하게 인프라 아키텍처에 대하여 소개하였습니다. 다음 글에서는 ECS 배포의 첫 단계인 <strong>Docker 이미지 생성 방법</strong>에 대해 알아보도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ECS(Elastic Container Service)란?]]></title>
            <link>https://velog.io/@one_ik/ECSElastic-Container-Service%EB%9E%80</link>
            <guid>https://velog.io/@one_ik/ECSElastic-Container-Service%EB%9E%80</guid>
            <pubDate>Fri, 25 Oct 2024 06:57:41 GMT</pubDate>
            <description><![CDATA[<h2 id="ecselastic-container-service란">ECS(Elastic Container Service)란?</h2>
<p><img src="https://velog.velcdn.com/images/one_ik/post/3504f7c0-42f7-4ee5-9e43-52c2e69a3b8c/image.png" alt=""></p>
<p>ECS는 <strong>컨테이너화된 애플리케이션을 쉽게 배포, 관리 및 확장</strong>할 수 있게 해주는 완전관리형 컨테이너 오케스트레이션 서비스입니다.</p>
<p>쉽게 말하자면, 개발자는 애플리케이션 코드를 Docker 이미지로 만들어서 ECS에 올리기만 하면 <strong>컨테이너 운영에 필요한 복잡한 작업들</strong>(컨테이너 생성, 종료, 복제, 로드 밸런싱, 장애 복구 등)을 ECS가 <strong>자동</strong>으로 처리해줍니다.</p>
<p>특히 Fargate를 사용하면 서버 관리도 신경 쓸 필요 없이, 필요한 만큼만 리소스를 사용하고 그만큼의 비용만 지불하면 됩니다.</p>
<h2 id="ecs를-선택한-이유">ECS를 선택한 이유</h2>
<p>처음에는 AWS Lightsail을 사용하여 서비스를 배포하려 했습니다. Lightsail은 간단한 웹 애플리케이션을 배포하기에는 적합하지만, 다음과 같은 제한사항들이 존재합니다:</p>
<ol>
<li><strong>인스턴스 수를 수동으로 관리</strong>해야 하고, 기본적인 자동 스케일링이 제공되지 않습니다.</li>
<li><strong>트래픽</strong>이 갑자기 <strong>증가</strong>할 경우 개발자가 직접 인스턴스를 추가해야 하기 때문에 <strong>즉각적인 대응이 어렵습니다</strong>.</li>
<li>서비스 <strong>장애 발생 시 수동으로 대응</strong>해야 하며, 자동 복구 시스템이 존재하지 않습니다.</li>
<li><strong>복잡한 배포 전략</strong>(블루/그린 배포, 롤링 업데이트 등)을 구현하기 어렵습니다.</li>
</ol>
<p>이러한 한계들이 존재하여 이를 극복하기 위해 ECS를 선택하게 되었습니다. <strong>ECS는 자동 스케일링, 장애 복구, 다양한 배포 전략을 기본적으로 지원하며, AWS의 다른 서비스들과도 긴밀하게 통합</strong>되어 있습니다.</p>
<h2 id="ecs-구성-요소">ECS 구성 요소</h2>
<p><img src="https://velog.velcdn.com/images/one_ik/post/00632638-62c1-485c-b5f0-8a65f2cd4b05/image.jpg" alt=""></p>
<h3 id="task-definition">Task Definition</h3>
<p>Task Definition은 <strong>컨테이너를 실행하기 위해 정의한 명세서</strong>입니다. 어떤 도커 이미지를 사용할지, CPU/메모리 할당량, 환경 변수, 네트워크 설정, 볼륨 마운트 등 컨테이너 실행에 필요한 설정을 정의합니다. 이를 기반으로 실제 동작하는 컨테이너 인스턴스가 바로 Task입니다.</p>
<h3 id="service">Service</h3>
<p>Service는 <strong>Task들의 수명주기를 관리</strong>합니다. 예를 들어, 특정 Task가 죽으면 <strong>자동으로 새로운 Task를 생성</strong>하여 지정된 개수를 유지합니다. 또한 <strong>ALB</strong>(Application Load Balancer)와 연동하여 트래픽을 분산시킬 수 있으며, <strong>CPU 사용량이나 메모리 사용량에 따른 자동 스케일링</strong>도 설정할 수 있습니다.</p>
<h3 id="cluster">Cluster</h3>
<p>Task와 Service가 실행되는 <strong>논리적 그룹</strong>입니다. 수많은 컨테이너들을 개별적으로 관리하는 것은 매우 복잡하고 비효율적이므로 이들을 하나의 단위로 묶어서 관리할 필요가 있습니다. ECS Cluster는 이러한 목적으로 생성된 컨테이너들의 하나의 그룹이며, 이를 통해 <strong>리소스를 관리, 모니터링, 보안 설정</strong> 등을 수행할 수 있습니다.</p>
<h2 id="마무리">마무리</h2>
<p>지금까지 ECS란 무엇이고 왜 ECS를 선택하게 되었는지, 그리고 ECS의 주요 구성 요소들에 대해 알아보았습니다. 다음 글에서는 프로젝트의 인프라 아키텍처를 어떻게 구성했는지 간단하게 소개해보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Axios] RefreshToken 만료 시 에러 처리 개선하기]]></title>
            <link>https://velog.io/@one_ik/Axios-RefreshToken-%EB%A7%8C%EB%A3%8C-%EC%8B%9C-%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one_ik/Axios-RefreshToken-%EB%A7%8C%EB%A3%8C-%EC%8B%9C-%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 30 Sep 2024 15:50:54 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p><a href="https://velog.io/@one_ik/Axios-%ED%86%A0%ED%81%B0-%EC%9E%AC%EB%B0%9C%EA%B8%89-%EC%A4%91%EB%B3%B5-%EC%9A%94%EC%B2%AD-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0">이전 글</a>에서 토큰 재발급 중복 요청 문제를 해결했지만, 새로운 문제가 발생했습니다. RefreshToken이 만료되었을 때 예상했던 것과 달리 에러 처리가 되지 않았습니다.</p>
<p>예상했던 동작은 아래와 같습니다:</p>
<ol>
<li>RefreshToken으로 새로운 AccessToken을 요청할 때 에러가 발생한다.</li>
<li>try/catch로 에러를 잡아낸다.</li>
<li>저장된 AccessToken을 삭제하고 사용자를 로그인 페이지로 보낸다.</li>
</ol>
<p>하지만 실제로는 RefreshToken 만료 시 try/catch에서 에러를 잡지 못하는 문제가 발생했습니다.</p>
<h2 id="원인-파악">원인 파악</h2>
<p>디버깅 결과, 문제는 <strong>Axios 인터셉터</strong>에 있었습니다. 
<img src="https://velog.velcdn.com/images/one_ik/post/ef617493-fb0a-4cbf-b105-9bf80c5ae196/image.jpg" alt=""></p>
<p>위 그림처럼<em>Axios 요청 중 발생한 에러를 인터셉터에서 가로채는데, 인터셉터에에 *</em>가로챈 에러를 적절히 처리하는 로직이 없었기 때문에<strong>, 의도했던대로 try/catch문에 도달하지 못하는 문제가 **발생</strong>했습니다.</p>
<h2 id="해결-방법">해결 방법</h2>
<p>인터셉터에서 RefreshToken 관련 에러를 다시 던지도록 수정했습니다.</p>
<pre><code class="language-ts">// axios.ts

  axiosInstance.interceptors.response.use(
    (response) =&gt; {
      return response;
    },
    async (error) =&gt; {
      const originalRequest = error.config;
      if (originalRequest.url === API_END_POINT.REFRESH_TOKEN) return Promise.reject(error);
    ...</code></pre>
<p>이 수정으로 RefresToken 만료 시 발생하는 에러를 제대로 잡아 처리할 수 있게 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Axios] 토큰 재발급 중복 요청 문제 해결]]></title>
            <link>https://velog.io/@one_ik/Axios-%ED%86%A0%ED%81%B0-%EC%9E%AC%EB%B0%9C%EA%B8%89-%EC%A4%91%EB%B3%B5-%EC%9A%94%EC%B2%AD-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@one_ik/Axios-%ED%86%A0%ED%81%B0-%EC%9E%AC%EB%B0%9C%EA%B8%89-%EC%A4%91%EB%B3%B5-%EC%9A%94%EC%B2%AD-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Fri, 27 Sep 2024 21:56:14 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>클라이언트는 Axios Interceptors를 이용하여 모든 요청을 가로채 AccessToken을 Header에 추가합니다. AccessToken이 만료된다면, Cookie에 저장된 RefreshToken을 이용하여 새로운 AccessToken을 발급 받습니다.</p>
<p><img src="https://velog.velcdn.com/images/one_ik/post/a97dbf84-d2ef-4f5e-be00-f6e581d226a2/image.jpg" alt=""></p>
<p>이러한 토큰 관리 시스템에서 메인 페이지 접속 시 다음과 같은 문제가 발생했습니다. 메인 페이지 로드 시, 유저 정보, 메인 퀘스트 정보, 서브 퀘스트 정보를 비동기로 요청합니다. AccessToken이 만료된 경우 각 요청마다 401에러가 발생합니다. 그 결과 RefreshToken을 이용한 <strong>새로운 AccessToken 재발급 요청</strong>이 3번 <strong>중복</strong>해서 <strong>발생</strong>하게 되었습니다.</p>
<blockquote>
<p>참고: Axios는 비동기로 요청을 보내고, 서버에서 먼저 처리된 순서대로 응답을 받습니다.</p>
</blockquote>
<h2 id="해결-방법-대기열-방식">해결 방법: 대기열 방식</h2>
<p>저는 대기열 방식을 통해 토큰 재발급 요청 문제를 해결했습니다. 이 방식은 다음과 같이 작동합니다.</p>
<ol>
<li><strong>가장 먼저 401에러 응답</strong>을 받은 경우에만 <strong>토큰 재발급을 요청</strong>합니다.</li>
<li><strong>이후의 401에러 콜백</strong>들은 토큰 재발급 요청을 보내지 않고 <strong>대기열에 등록</strong>합니다.</li>
<li>새 토큰을 받으면, 대기열에 등록된 모든 콜백에 새 토큰을 전달합니다.</li>
<li>새 토큰을 받은 각 요청은 갱신된 토큰으로 원래의 요청을 재시도합니다.</li>
</ol>
<pre><code class="language-ts">// 토큰 갱신중인지 확인하기 위한 변수
let isTokenRefreshing = false;
// 대기열
let refreshSubscribers: ((accessToken: string) =&gt; void)[] = [];

const onTokenRefreshed = (accessToken: string) =&gt; {
 refreshSubscribers.forEach((callback) =&gt; callback(accessToken));
 refreshSubscribers = [];
};

const addRefreshSubscriber = (callback: (accessToken: string) =&gt; void) =&gt; {
 refreshSubscribers.push(callback);
};

...

 axiosInstance.interceptors.response.use(
   (response) =&gt; {
     return response;
   },
   async (error) =&gt; {
     const originalRequest = error.config;

     if (error.response?.status === 401) {
       if (!isTokenRefreshing) {
       // 가장 먼저 들어온 401에러일 경우, true로 변경
         isTokenRefreshing = true;
         try {
           // 비동기로 토큰 재발급 요청
           const { accessToken } = await refreshToken();
           setToken(accessToken);
           originalRequest.headers.Authorization = `Bearer ${accessToken}`;

           // 갱신된 토큰을 콜백에 전달
           onTokenRefreshed(accessToken);
           isTokenRefreshing = false;
           return axiosInstance(originalRequest);
         } catch (error) {
           isTokenRefreshing = false;
           refreshSubscribers = [];
           removeToken();
           await logout();
           return Promise.reject(error);
         }
       }

     // 토큰 갱신중이라면, 콜백을 대기열에 추가
       const retryOriginalRequest = new Promise((resolve) =&gt; {
         addRefreshSubscriber((accessToken) =&gt; {
           originalRequest.headers.Authorization = `Bearer ${accessToken}`;
           resolve(axiosInstance(originalRequest));
         });
       });

       return retryOriginalRequest;
     }

     return Promise.reject(error);
   }
 );

 return axiosInstance;
};</code></pre>
<h2 id="참고자료">참고자료</h2>
<p><a href="https://gusrb3164.github.io/web/2022/08/07/refresh-with-axios-for-client/">axios interceptors와 refresh token을 활용한 jwt 토큰 관리</a>
<a href="https://small-stap.tistory.com/m/117">Axios Jwt RefreshToken 중복요청</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] No space left on device 에러 해결]]></title>
            <link>https://velog.io/@one_ik/Docker-No-space-left-on-device-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@one_ik/Docker-No-space-left-on-device-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 04 Sep 2024 10:59:00 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>MariaDB 컨테이너를 Docker COmpose로 실행하려 했으나, 아래와 같은 에러 메시지와 함께 실행되지 않았습니다.</p>
<pre><code>db-1  | 2024-08-28  9:01:35 0 [ERROR] mariadbd: Error writing file &#39;./ddl_recovery.log&#39; (Errcode: 28 &quot;No space left on device&quot;)</code></pre><h2 id="원인-파악">원인 파악</h2>
<h3 id="1-docker-vm에-할당된-디스크-사용량-확인">1. Docker VM에 할당된 디스크 사용량 확인</h3>
<p>우선 Docker VM에 할당된 디스크 공간이 어떻게 사용중인지 확인해보겠습니다.</p>
<pre><code>docker exec [컨테이너 이름] df -h</code></pre><p>확인해보니 <strong>Docker VM에 할당된 디스크 공간</strong>이 <strong>모두 사용 중</strong>인 것을 확인하였습니다.</p>
<pre><code>Filesystem      Size  Used Avail Use% Mounted on
overlay          59G   58G     0 100% /
tmpfs            64M     0   64M   0% /dev
shm              64M     0   64M   0% /dev/shm
/dev/vda1        59G   58G     0 100% /etc/hosts
tmpfs           2.0G     0  2.0G   0% /sys/firmware</code></pre><h3 id="2-docker-시스템-전체-디스크-사용량-확인">2. Docker 시스템 전체 디스크 사용량 확인</h3>
<p>디스크 공간이 부족한 이유를 알아보기 위하여 Docker 시스템 전체의 리소스 사용 현황을 확인해보겠습니다.</p>
<pre><code>docker system df</code></pre><p>확인 결과 Build Cache가 가장 많은 공간을 차지하고 있었습니다.</p>
<pre><code>TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          11        8         3.492GB   2.537GB (72%)
Containers      29        15        362B      180B (49%)
Local Volumes   1         1         186.8MB   0B (0%)
Build Cache     424       0         20.27GB   20.27GB</code></pre><p>Build Cache는 Docker 이전 빌드의 레이어를 재사용하여 빌드 시간을 단축시킵니다. 여태까지 다양한 프로젝트나 다른 버전의 이미지를 빌드할 때마다 누적된 캐시였습니다.</p>
<h2 id="해결">해결</h2>
<h3 id="build-cache-삭제">Build Cache 삭제</h3>
<p>Reclaimable을 보니, 현재 활성화된 빌드 캐시가 없다는 것을 확인하였고, 사용하지 않는 빌드 캐시를 정리하였습니다.</p>
<pre><code>docker builder prune</code></pre><p>모든 Build Cache가 삭제되었음을 확인할 수 있었고</p>
<pre><code>TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          11        8         3.492GB   2.537GB (72%)
Containers      29        15        362B      180B (49%)
Local Volumes   1         1         186.8MB   0B (0%)
Build Cache     16        0         0B        0B</code></pre><p>디스크 공간 재확인 결과 Docker VM의 사용할 수 있는 디스크 공간이 크게 증가하였음을 확인할 수 있었습니다.</p>
<pre><code>Filesystem      Size  Used Avail Use% Mounted on
overlay          59G  6.1G   50G  11% /
tmpfs            64M     0   64M   0% /dev
shm              64M     0   64M   0% /dev/shm
/dev/vda1        59G  6.1G   50G  11% /etc/hosts
tmpfs           2.0G     0  2.0G   0% /sys/firmware</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[TypeORM 엔티티 업데이트 시 발생한 id 타입 불일치 문제 해결]]></title>
            <link>https://velog.io/@one_ik/TypeORM-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-id-%ED%83%80%EC%9E%85-%EB%B6%88%EC%9D%BC%EC%B9%98-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@one_ik/TypeORM-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8-%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-id-%ED%83%80%EC%9E%85-%EB%B6%88%EC%9D%BC%EC%B9%98-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Mon, 29 Jul 2024 20:42:09 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>프로젝트 진행 중 메인 퀘스트 업데이트 시, 1:N 관계로 설정된 사이드 퀘스트도 함께 업데이트해야만 한다</p>
<pre><code class="language-sql">UPDATE `quest` SET `end_date` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE (`id` = ?) -- PARAMETERS: [&quot;2024-08-24T14:59:59.999Z&quot;,&quot;3&quot;]
UPDATE `side_quest` SET `quest_id` = ?, `content` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE (`id` = ?) -- PARAMETERS: [3,&quot;사이드 퀘스트 1 수정7&quot;,&quot;5&quot;]
UPDATE `side_quest` SET `quest_id` = ?, `content` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE (`id` = ?) -- PARAMETERS: [3,&quot;사이드 퀘스트 2 수정7&quot;,&quot;6&quot;]
UPDATE `side_quest` SET `quest_id` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE `id` = ? -- PARAMETERS: [null,&quot;5&quot;]
UPDATE `side_quest` SET `quest_id` = ?, `updated_at` = CURRENT_TIMESTAMP WHERE `id` = ? -- PARAMETERS: [null,&quot;6&quot;]</code></pre>
<p>그러나 위와 같은 SQL문을 통해 사이드 퀘스트에서 <code>quest_id</code>값이 <code>null</code>로 업데이트되는 문제가 발생했다</p>
<h2 id="원인-파악">원인 파악</h2>
<p>디버깅을 통해 원인을 파악한 결과, <strong><code>id</code>값의 타입을 <code>bigint</code>로 설정한 것이 문제</strong>였다. JavaScript에서 정확하게 표현할 수 있는 가장 큰 정수는 <code>2^53 - 1</code>이다</p>
<p>TypeORM 문서에 따르면,</p>
<blockquote>
<p>Note about bigint type: bigint column type, used in SQL databases, doesn&#39;t fit into the regular number type and maps property to a string instead.</p>
</blockquote>
<p>위처럼 <code>bigint</code> 타입을 <code>string</code>으로 매핑한다</p>
<p>그래서 데이터베이스 설정 파일에 <code>bigNumberStrings: false</code> 옵션을 추가하여 <code>bigint</code> 값을 <code>number</code>로 가져오도록 설정하였다.</p>
<p>문제는 TypeORM 라이브러리의 <code>EntityPersistExecutor</code> 클래스에서 발생했다</p>
<p><code>EntityPersistExecutor</code> 클래스는 전달받은 엔티티를 데이터베이스에 저장하거나 업데이트하는 역할을 한다</p>
<pre><code class="language-js">// EntityPersistExecutor.js
...

    await new SubjectDatabaseEntityLoader_1.SubjectDatabaseEntityLoader(queryRunner, subjects).load(this.mode);
    // console.timeEnd(&quot;loading...&quot;);
    // console.time(&quot;other subjects...&quot;);
    // build all related subjects and change maps
    if (this.mode === &quot;save&quot; ||
        this.mode === &quot;soft-remove&quot; ||
        this.mode === &quot;recover&quot;) {
        new OneToManySubjectBuilder_1.OneToManySubjectBuilder(subjects).build();
        new OneToOneInverseSideSubjectBuilder_1.OneToOneInverseSideSubjectBuilder(subjects).build();
        new ManyToManySubjectBuilder_1.ManyToManySubjectBuilder(subjects).build();
    }
</code></pre>
<p>그 중,<code>SubjectDatabaseEntityLoader.load()</code> 메서드를 통해 전달받은 <code>subjects</code>(퀘스트 엔티티와 사이드 퀘스트들)를 통해 데이터베이스에서 엔티티 데이터들을 로드해온다.</p>
<p>그러나, 여기서 퀘스트와 연관 설정된 사이드 퀘스트들을 로드해올 때, <code>bigNumberStrings</code>옵션이 적용되지 않아 사이드 퀘스트의 <code>id</code>값이 <code>string</code>타입으로 가져와졌다</p>
<p>그 후, <code>OneToManySubjectBuilder.build()</code>메서드에서 엔티티 간의 관계를 설정할 때, 업데이트를 위해 전달받은 엔티티와 데이터베이스에서 로드해온 엔티티 간의 관계를 비교한다</p>
<p>이 때 <code>id</code>값의 타입 불일치로 인해 비교 결과가 <code>false</code>가 되어 새로운 <code>subject</code>가 생성되었다</p>
<p>결과적으로 <strong>불필요한 <code>update</code>로직이 추가로 발생</strong>하게 되어, <strong><code>quest_id</code>가 <code>null</code>로 설정</strong>되는 문제가 발생했다</p>
<h2 id="해결-방법">해결 방법</h2>
<p>수정하고자 하는 퀘스트와 연관된 사이드 퀘스트들을 각각 별도로 업데이트하고, 하나의 트랜잭션으로 묶어줬다</p>
<pre><code class="language-ts">// quest.service.ts
  @Transactional()
  async updateMainQuest(
    userId: number,
    questId: number,
    request: UpdateMainQuestRequest
  ): Promise&lt;void&gt; {
    try {
      const { title, difficulty, startDate, endDate, hidden, sideQuests } = request;
      const quest = await this.findById(userId, questId);
      quest.updateMainQuest(title, difficulty, hidden, startDate, endDate);

    // 퀘스트와 연관된 사이드 퀘스트 수정 후, 저장하는 로직
      await this.sideQuestService.updateSideQuests(questId, sideQuests);
      await this.questRepository.save(quest);
    } catch (error) {
      throw new HttpException(&#39;퀘스트 업데이트에 실패하였습니다&#39;, HttpStatus.CONFLICT);
    }
  }
</code></pre>
<p>여기서 중요한 점은 퀘스트에 대한 데이터를 가져올 때, <code>relations</code>나 <code>eager</code>옵션을 통해 연관된 사이드 퀘스트들을 함께 가져온다면, <code>OneToManySubjectBuilder</code>에서 동일한 오류가 발생하므로, <strong>퀘스트의 데이터만 가져오도록 설정</strong>해야한다</p>
<pre><code class="language-ts">// quest.repository.ts
export class QuestRepository extends GenericTypeOrmRepository&lt;Quest&gt; implements IQuestRepository {

  ...

  async findById(userId: number, questId: number): Promise&lt;Quest&gt; {
    const findOptions: FindOneOptions = { where: { id: questId, userId } };
    return this.getRepository().findOne(findOptions);
  }
}</code></pre>
<h2 id="참고">참고</h2>
<p><a href="https://typeorm.io/">TypeORM 공식 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[문자열을 Date 객체로 변환하기]]></title>
            <link>https://velog.io/@one_ik/%EB%AC%B8%EC%9E%90%EC%97%B4%EC%9D%84-Date-%EA%B0%9D%EC%B2%B4%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one_ik/%EB%AC%B8%EC%9E%90%EC%97%B4%EC%9D%84-Date-%EA%B0%9D%EC%B2%B4%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 25 Jul 2024 07:38:13 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>클라이언트로부터 퀘스트 시작 날짜와 종료 날짜를 <code>2024-07-25</code>와 같은 문자열 형태로 받을 때, 이를 데이터베이스에 저장하기 위해 <code>Date</code> 객체로 변환해줄 필요가 있었다. </p>
<p>이 과정에서 <code>class-transformer</code>의 <code>@Transform</code> 데코레이터를 사용하여 데이터 변환을 시도했지만, 문제가 발생했다.</p>
<h2 id="초기-접근-방식">초기 접근 방식</h2>
<p>처음에는 다음과 같이 <code>@Transform</code> 데코레이터를 사용하여 날짜 변환을 시도했다. 
<code>startDate</code>와 <code>endDate</code>를 <code>property</code>에 담아서 넘겨주기 때문에, <code>value</code>를 추출해서 원하는 형태로 변환하도록 설정하였다.</p>
<pre><code class="language-ts">// create-quest.request.ts
export class CreateQuestRequest {

 ...

  @IsDateString()
  @IsNotEmpty()
  @Transform((property) =&gt; {
    return toUTCStartOfDay(property.value);
  })
  startDate: Date;

  @IsDateString()
  @IsNotEmpty()
  @Transform((property) =&gt; {
    return toUTCEndOfDay(property.value);
  })
  endDate: Date;
}
</code></pre>
<h2 id="검증-에러-발생">검증 에러 발생</h2>
<p>위와 같은 방식은 다음과 같은 검증 오류를 발생시켰다.</p>
<pre><code class="language-json">{
  &quot;statusCode&quot;: 400,
  &quot;timestamp&quot;: &quot;2024-07-24T14:08:12.825Z&quot;,
  &quot;path&quot;: &quot;/quests&quot;,
  &quot;method&quot;: &quot;POST&quot;,
  &quot;message&quot;: &quot;Bad Request Exception&quot;,
  &quot;details&quot;: [
    &quot;startDate: startDate must be a valid ISO 8601 date string&quot;,
    &quot;endDate: endDate must be a valid ISO 8601 date string&quot;
  ]
}</code></pre>
<p>상세 메세지만 봤을 때는 전달받은 <code>value</code>가 <code>ISO 8601</code> 형태가 아니기 때문에, 발생한 문제로 생각했지만, 디버깅 과정에서 <code>class-transformer</code>와 <code>class-validator</code>의 <strong>동작 순서가 예상과 달랐음을 발견</strong>했다.</p>
<p><code>@IsDateString()</code> 데코레이터를 통해 <code>value</code>를 검증 후, <code>@Transform</code> 데코레이터를 이용하여 변환될 것으로 예상했는데, 먼저 <code>@Transform</code> 데코레이터를 통해 <code>Date</code> 객체로 변환되었다. </p>
<p>그 후 <code>@IsDateString()</code> 데코레이터를 통해 검증하기 때문에, 오류가 발생한 것이다.</p>
<h2 id="class-transformer와-class-validator의-동작-순서">class-transformer와 class-validator의 동작 순서</h2>
<p>위 문제의 핵심은 NestJS에서 <code>class-transformer</code>와 <code>class-validator</code>의 동작 순서에 있다.</p>
<p><img src="https://velog.velcdn.com/images/one_ik/post/9af79090-f7f3-4969-9d2a-402a746e4800/image.jpg" alt=""></p>
<p>클라이언트로부터 HTTP 요청이 서버로 전송되고, <strong><code>class-transformer</code>를 이용해 요청의 JSON 형태인 body 값을 지정된 클래스의 인스턴스로 변환</strong>한다.</p>
<p>그 후, <code>class-validator</code>를 이용하여 생성된 인스턴스에 대해 유효성 검사를 수행하게 된다.</p>
<p>따라서, <code>class-transformer</code>를 이용하여 인스턴스로 변환하는 과정에서 <code>@Transform</code> 데코레이터가 먼저 동작하게 되는 것이다</p>
<h2 id="해결책-커스텀-데코레이터-생성">해결책: 커스텀 데코레이터 생성</h2>
<p>이 문제를 해결하기 위해 커스텀 데코레이터 <code>@TransformDateToUTC</code>를 만들었다.
이 데코레이터는 전달받은 <code>option</code>에 따라 <code>value</code>를 원하는 형태로 변환하는 데코레이터다.</p>
<pre><code class="language-ts">export type TransformDateOptions = {
  option: &#39;start&#39; | &#39;end&#39;;
};



export function TransformDateToUTC({ option }: TransformDateOptions): PropertyDecorator {
    return Transform(({ value }) =&gt; {
        const regex = /^\d{4}-\d{2}-\d{2}$/;
        if (typeof value !== &#39;string&#39; &amp;&amp; !regex.test(value) &amp;&amp; !dayjs(value).isValid()) {
        throw new BadRequestException(`Please provide only date like &#39;YYYY-MM-DD&#39;`);
        }
        return option === &#39;start&#39; ? toUTCStartOfDay(value) : toUTCEndOfDay(value);
});
}

ts
// create-qeust.request.ts

export class CreateQuestRequest {

 ...

    @TransformDateToUTC({ option: &#39;start&#39; })
    @IsNotEmpty()
    @IsDate()
    startDate: Date;

    @IsNotEmpty()
    @TransformDateToUTC({ option: &#39;end&#39; })
    @IsNotEmpty()
    @IsDate()
    endDate: Date;
}</code></pre>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://jojoldu.tistory.com/610">NestJS에서 응답/요청 객체 직렬화 (Serialization) 하기</a>
<a href="https://velog.io/@dramatic/NestJS-class-validator%EC%99%80-transformer%EA%B0%80-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0%EC%99%80-%EB%8B%A4%EB%A5%B8%EC%A0%90">NestJS class-validator와 transformer가 데코레이터와 다른점</a>
<a href="https://seungtaek-overflow.tistory.com/13">[TS] class-validator의 활용과 검증 옵션</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[가상 머신과 도커]]></title>
            <link>https://velog.io/@one_ik/docker</link>
            <guid>https://velog.io/@one_ik/docker</guid>
            <pubDate>Sun, 21 Jul 2024 18:58:36 GMT</pubDate>
            <description><![CDATA[<h2 id="가상-머신virtual-machine">가상 머신(Virtual Machine)</h2>
<p><img src="https://velog.velcdn.com/images/one_ik/post/afb1754e-5937-42a0-b70d-7746d444619f/image.jpg" alt=""></p>
<p>가상 머신이란, 컴퓨터 안에서 소프트웨어로 만들어진 가상의 컴퓨터이다. 이런 가상의 컴퓨터가 필요한 이유는 뭘까?</p>
<p>예를 들어, 호스트 컴퓨터의 운영 체제(Host OS)는 Linux 기반인데, Windows에서만 동작하는 게임이 존재한다고 해보자. 이 게임을 실행하기 위해 매번 Linux를 삭제하고 Windows를 설치한다면, 많은 시간과 노력이 낭비될 것이다.</p>
<p>이런 문제를 해결하기 위해 탄생한 것이 가상 머신이다. 가상 머신을 활용하면, <strong>하나의 물리적 컴퓨터에서 여러 운영 체제를 동시에 운영할 수 있게 된다.</strong> </p>
<p>따라서 Linux Host OS 위에서 Windows 가상 머신을 실행하여, 각 OS 환경에서만 동작하는 프로그램들을 하나의 컴퓨터에서 편리하게 사용할 수 있다.</p>
<blockquote>
<p><strong>하이퍼바이저(Hypervisor)</strong>
 가상 머신(VM)을 생성하고 관리하는 소프트웨어다. 물리적 하드웨어를 가상화하여 각 가상 머신에 가상 하드웨어를 제공한다.</p>
</blockquote>
<h3 id="가상-머신의-문제점">가상 머신의 문제점</h3>
<p>가상 머신 기술은 위처럼 많은 이점을 제공하지만, 동시에 큰 단점도 가지고 있다.</p>
<p>물리적 하드웨어에 접근하기 위해 추가적인 소프트웨어 계층이 필요하다. 각 가상 머신은 Guest OS를 가지며, 하이퍼바이저를 통해 Host OS와 통신하여 하드웨어 자원을 활용하기 때문에, 성능 저하가 발생한다.</p>
<p>또한, 각 가상 머신(VM)은 독립적인 운영체제를 실행하므로, 전체적인 메모리 사용량이 증가한다.</p>
<h2 id="도커docker">도커(Docker)</h2>
<p><img src="https://velog.velcdn.com/images/one_ik/post/78d64771-481f-4533-b712-a91d701a0509/image.jpg" alt=""></p>
<p>도커(Docker)는 컨테이너 기반 가상화 플랫폼으로, 가상 머신의 한계를 극복하고 애플리케이션 배포와 실행을 획기적으로 간소화했다. </p>
<p>기존 가상 머신과 달리, 도커 컨테이너는 완전한 운영체제를 포함하지 않는다. 대신, 애플리케이션 실행에 필요한 최소한의 구성 요소인 바이너리, 라이브러리, 설정 파일만을 하나의 패키지로 묶어 제공한다.</p>
<p>도커의 핵심인 도커 엔진은 이러한 컨테이너들이 <strong>Host OS의 커널을 공유</strong>하여 물리적 하드웨어와 효율적으로 통신할 수 있게 한다. 이 커널 공유 방식은 메모리 사용량을 크게 줄이고, 전체적인 시스템 활용을 최적화한다. </p>
<p>또한, 컨테이너는 운영체제를 부팅할 필요가 없어 가상 머신에 비해 훨씬 빠르게 시작된다.</p>
<h2 id="참고">참고</h2>
<p><a href="https://www.youtube.com/watch?v=zh0OMXg2Kog&amp;t=1019s">20분 만에 전공자처럼 도커, 가상화 이해하기!</a>
<a href="https://www.docker.com/resources/what-container/">도커 공식 홈페이지</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJS에서 AOP를 활용하여 트랜잭션 관리 개선하기]]></title>
            <link>https://velog.io/@one_ik/NestJS-Transaction-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one_ik/NestJS-Transaction-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 07 Jul 2024 13:57:17 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-너무-복잡한-코드">문제: 너무 복잡한 코드</h2>
<p>기존에 TypeORM 라이브러리에서 트랜잭션을 적용하던 방식은 아래와 같다</p>
<pre><code class="language-ts">  async createUser(createUserDto: CreateUserDto) {
    const { email, password, nickname } = createUserDto;

    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {

    // 비즈니스 로직

      await queryRunner.commitTransaction();
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();
    }
  }</code></pre>
<p>위 코드처럼 기존의 트랜잭션 사용 방식은 해당 메서드의 크기가 커질 수 있었고, try-catch 절의 비즈니스 로직을 한 눈에 파악하기 어려웠다.</p>
<p>그래서 AOP를 이용하여 트랜잭션과 관련된 로직을 구현하기로 결정하였다.</p>
<blockquote>
<p><strong>Aspect Oriented Programming(관점 지향 프로그래밍)이란?</strong></p>
</blockquote>
<p>AOP는 애플리케이션의 여러 부분에서 반복되는 <strong>횡단 관심사(cross-cutting concerns)를 분리</strong>하여 모듈화하는 프로그래밍 패러다임이다. </p>
<blockquote>
</blockquote>
<p>NestJS에서는 이를 통해 코드의 재사용성과 관리성을 높일 수 있다.</p>
<h2 id="transcationinterceptor-사용하기">TranscationInterceptor 사용하기</h2>
<p>NestJS의 인터셉터를 이용하면, 트랜잭션이라는 공통 로직을 중앙에서 관리할 수 있게 된다.</p>
<h3 id="동작-과정">동작 과정</h3>
<ol>
<li>인터셉터를 통해 요청을 가로챈 후, QueryRunner를 생성하여 새로운 트랜잭션을 시작한다.</li>
<li>QueryRunnerManager를 요청 객체에 추가한다.</li>
<li>TransactionManager 데코레이터를 통해 요청 객체에 존재하는 QueryRunnerManager를 추출하여, 서비스 메서드에 전달한다.</li>
<li>모든 작업이 완료되면, 인터셉터에서 응답을 가로채서 트랜잭션을 커밋하고, QueryRunner를 해제한다.</li>
<li>에러가 발생하면, 트랜잭션을 롤백하고 QueryRunner를 해제한다.</li>
</ol>
<h4 id="transactioninterceptor-구현">TransactionInterceptor 구현</h4>
<pre><code class="language-ts">@Injectable()
export class TransactionInterceptor implements NestInterceptor {
  constructor(private readonly dataSource: DataSource) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise&lt;Observable&lt;any&gt;&gt; {
    const req = context.switchToHttp().getRequest();
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    req.queryRunnerManager = queryRunner.manager;

    return next.handle().pipe(
      catchError(async (err) =&gt; {
        await queryRunner.rollbackTransaction();
        await queryRunner.release();
        if (err instanceof HttpException) {
          throw new HttpException(err.message, err.getStatus());
        } else {
          throw new InternalServerErrorException(err.message);
        }
      }),
      tap(async () =&gt; {
        await queryRunner.commitTransaction();
        await queryRunner.release();
      })
    );
  }
}</code></pre>
<h4 id="transactionmanager-데코레이터">TransactionManager 데코레이터</h4>
<pre><code class="language-ts">// transaction-manager.decorator.ts
export const TransactionManager = createParamDecorator((data: unknown, ctx: ExecutionContext) =&gt; {
    const req = ctx.switchToHttp().getRequest();
    return req.queryRunnerManager;
});</code></pre>
<h4 id="컨트롤러에서-사용">컨트롤러에서 사용</h4>
<pre><code class="language-ts">
// users.controller.ts
@Post(&#39;signup&#39;)
@UsePipes(ValidationPipe)
@HttpCode(HttpStatus.CREATED)
async signUp(
  @Body() createUserDto: CreateUserDto,
  @TransactionManager() queryRunnerManager: EntityManager,
) {
  await this.usersService.signUp(createUserDto, queryRunnerManager);
  return { message: &#39;success&#39; };
}</code></pre>
<h3 id="그러나-oauth-구현-시-문제-발생">그러나, OAuth 구현 시 문제 발생</h3>
<p><img src="https://velog.velcdn.com/images/one_ik/post/f057a2b4-f969-4b5d-a699-d24db8ec3a4d/image.png" alt=""></p>
<p>OAuth를 구현하기 위해 Passport 라이브러리를 이용했는데, 그러다보니 가드에서 해당 OAuth 전략에 맞는 유저를 생성하는 작업을 담당하였다.</p>
<p>그러나, NestJS의 실행 순서를 확인해보면, 인터셉터보다 가드가 먼저 실행되기 때문에, 트랜잭션이 적용되지 않는 문제가 발생했다.</p>
<h2 id="transactional-decorator-사용하기">Transactional Decorator 사용하기</h2>
<p>인터셉터에서의 문제를 해결하기 위해 접근 방식을 변경하였다. 인터셉터 대신 미들웨어를 사용하고, <a href="https://www.npmjs.com/package/cls-hooked">cls-hooked</a> 라이브러리를 사용하였다.</p>
<p>cls-hooked 라이브러리는 비동기 작업 간에 트랜잭션 컨텍스트를 유지할 수 있게 도와주는 라이브러리다.</p>
<h3 id="동작-방식">동작 방식</h3>
<ol>
<li>요청이 들어오면, TransactionMiddleware에서 namespace를 만든 후, EntityManager를 등록한다.</li>
<li>서비스 레이어에서 해당 메서드에 Transactional 데코레이터를 사용하여 트랜잭션을 적용한다.</li>
<li>Custom Repository에서 namespace에서 EntityManager를 꺼내 사용할 수 있도록 TransactionManager를 적용한다.</li>
</ol>
<h4 id="transactionmiddleware에서-namespace에-entitymanager-등록">TransactionMiddleware에서 namespace에 EntityManager 등록</h4>
<p>클라이언트의 요청은 NestJS의 미들웨어를 가장 먼저 거친다. 따라서 namespace를 미들웨어에서 생성한 후, EntityManager를 등록한다. </p>
<pre><code class="language-ts">@Injectable()
export class TransactionMiddleware implements NestMiddleware {
  constructor(private readonly entityManager: EntityManager) {}
  use(_req: Request, _res: Response, next: NextFunction) {
    const namespace = getNamespace(TRANSACTION) ?? createNamespace(TRANSACTION);

    return namespace.runAndReturn(async () =&gt; {
      Promise.resolve()
        .then(() =&gt; this.setEntityManager())
        .then(next);
    });
  }
  private setEntityManager() {
    const namespace = getNamespace(TRANSACTION) as Namespace;
    namespace.set(ENTITY_MANAGER, this.entityManager);
  }
}</code></pre>
<h4 id="transactional-데코레이터">Transactional 데코레이터</h4>
<p>미들웨어에서 등록한 EntityManager을 가져오고, 트랜잭션을 실행한다. 그 후, 데코레이터를 적용한 서비스 레이어의 메서드를 감싸도록하여 트랜잭션이 적용되도록 한다.</p>
<pre><code class="language-ts">export function Transactional() {
  return function (_target: any, _propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const originMethod = descriptor.value;

    async function transactionWrapped(...args: unknown[]) {
      const namespace = getNamespace(TRANSACTION);
      if (!namespace || !namespace.active) {
        throw new InternalServerErrorException(`${TRANSACTION} is not active`);
      }

      const entityManager = namespace.get(ENTITY_MANAGER) as EntityManager;
      if (!entityManager) {
        throw new InternalServerErrorException(
          `Could not find EntityManager in ${TRANSACTION} namespace`
        );
      }

      return await entityManager.transaction(async (transactionalEntityManager: EntityManager) =&gt; {
        namespace.set(ENTITY_MANAGER, transactionalEntityManager);
        return await originMethod.apply(this, args);
      });
    }

    descriptor.value = transactionWrapped;
  };
}</code></pre>
<h4 id="transactionmanger">TransactionManger</h4>
<p>namespace가 존재하고 active 상태라면, EntityManager를 반환하는 헬퍼다. 이 헬퍼를 이용해서 레포지토리는 트랜잭션 컨텍스트에 접근하여 EntityManager를 가져온다.</p>
<pre><code class="language-ts">@Injectable()
export class TransactionManager {
  public getEntityManager(): EntityManager {
    const namespace = getNamespace(TRANSACTION);
    if (!namespace || !namespace.active)
      throw new InternalServerErrorException(`${TRANSACTION} is not active`);
    return namespace.get(ENTITY_MANAGER);
  }
}

// generic-transaction.repository.ts
export abstract class GenericTypeOrmRepository&lt;T extends RootEntity&gt;
  implements IGenericRepository&lt;T&gt;
{
  constructor(
    @Inject(TransactionManager) private readonly transactionManager: TransactionManager
  ) {}

  ...

}</code></pre>
<h2 id="reference">Reference</h2>
<p><a href="https://hou27.tistory.com/entry/NestJS-Transaction-Interceptor-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0">NestJS - Transaction Interceptor 적용하기</a>
<a href="https://www.youtube.com/watch?v=AHSHjCVUsu8&amp;t=8592s">NestJS Meet up</a>
<a href="https://velog.io/@dev_leewoooo/NestJs-Transaction-Decorator-%EB%A7%8C%EB%93%A4%EA%B8%B0#namespace%EB%A5%BC-%EC%83%9D%EC%84%B1-%ED%9B%84-entitymanager%EB%A5%BC-%EC%8B%AC%EC%96%B4%EC%A3%BC%EB%8A%94-middleware">NestJs Transaction Decorator 만들기</a>
<a href="https://velog.io/@onejaejae/NestJS-Transaction-Decorator-%EA%B5%AC%ED%98%84%EA%B8%B0-with-TypeORM">Handling Transactions (feat. NestJS, TypeORM)</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TypeORM 마이그레이션]]></title>
            <link>https://velog.io/@one_ik/TypeORM-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-migration%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one_ik/TypeORM-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-migration%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 18 Jun 2024 14:22:12 GMT</pubDate>
            <description><![CDATA[<h2 id="마이그레이션이-필요한-이유">마이그레이션이 필요한 이유</h2>
<p>기존 프로젝트에서는 <strong>synchronize: true</strong> 설정을 통해서 데이터베이스 스키마를 설정하였다.</p>
<p>문제는 위와 같은 설정을 한다면 애플리케이션이 시작될 때 TypeORM이 엔티티를 검사하고, 현재 데이터베이스 스키마와 비교한 후, 변경된 부분이 있다면 <strong>자동으로 수정</strong>한다는 것이다.</p>
<p>개발 환경에서는 즉각적인 피드백을 받을 수 있기 때문에, 유용하게 사용할 수 있지만 프로덕션 환경에서는 의도치 않은 데이터 손실이 발생할 수 있기 때문에, 위와 같은 설정은 지양해야만 한다.</p>
<h2 id="마이그레이션의-장점">마이그레이션의 장점</h2>
<h3 id="안정성">안정성</h3>
<p>프로젝트는 혼자서 작업하는게 아니다. 여러 개발자가 동시에 작업할 때, 마이그레이션을 사용한다면 데이터베이스 관리자를 통해 모든 변경 사항을 사전에 검토하고 승인할 수 있어 안정성을 높일 수 있다. </p>
<h3 id="버전-관리">버전 관리</h3>
<p>누가 어떤 변경을 했는지 쉽게 파악할 수 있고, 각 마이그레이션 파일에는 변경된 날짜와 시간이 포함되어 있기 때문에, 이를 통해 변경 사항이 언제 적용되었는지도 확인할 수 있다.</p>
<p>또한 변경 사항을 적용한 후 문제가 발생했을 때, 개발자가 쉽게 이전 상태로 되돌릴 수 있다.</p>
<h2 id="마이그레이션-사용하기">마이그레이션 사용하기</h2>
<h3 id="datasourceoptions-설정하기">dataSourceOptions 설정하기</h3>
<pre><code class="language-ts">// datasource.config.ts
import { DataSource, DataSourceOptions } from &#39;typeorm&#39;;
import { SnakeNamingStrategy } from &#39;typeorm-naming-strategies&#39;;
import * as dotenv from &#39;dotenv&#39;;
import * as path from &#39;path&#39;;

dotenv.config({
  path: process.env.NODE_ENV === &#39;production&#39; ? &#39;.production.env&#39; : &#39;.development.env&#39;,
});

const entityPath = path.join(__dirname, &#39;../entities/*/*.entity.{js,ts}&#39;);
const migrationPath = path.join(__dirname, &#39;../migrations/*.{js,ts}&#39;);

export const dataSourceOptions: DataSourceOptions = {
  type: &#39;mysql&#39;,
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT),
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
  entities: [entityPath],
  synchronize: false,
  migrationsRun: false,
  namingStrategy: new SnakeNamingStrategy(),
  migrationsTableName: &#39;migrations&#39;,
  migrations: [migrationPath],
  logging: process.env.NODE_ENV === &#39;production&#39; ? [&#39;error&#39;] : true,
};

export const MysqlDataSource = new DataSource(dataSourceOptions);</code></pre>
<p>기존 설정에서는 서버가 실행될 때, 데이터베이스와 연동할 수 있었기 때문에, 동적으로 환경 변수를 설정하기 위해서 <strong>ConfigService를 사용</strong>하였다.</p>
<p>그러나, migration을 실행하기 위해서는 서버가 실행되기 전에 typeorm-cli를 통해 적용해야만 했다.</p>
<p>따라서, <strong>dotenv를 이용</strong>하여 NODE_ENV에 설정되어 있는 값에 따라 환경 변수를 가져와 사용할 수 있도록 하였다.</p>
<h3 id="datasourceoptions를-통해-데이터베이스와-연동하기">dataSourceOptions를 통해 데이터베이스와 연동하기</h3>
<pre><code class="language-ts">// typeorm.module.ts
@Module({
  imports: [TypeOrmModule.forRoot(dataSourceOptions)]
  ...
})

export class TypeOrmModule {}</code></pre>
<p>기존에 데이터베이스와 연동하기 위해 작성해놓은 코드가 존재했지만, 데이터베이스의 설정을 일관적으로 유지하기 위해 작성해둔 dataSourceOptions를 가져와 적용하였다.</p>
<h3 id="typeorm-마이그레이션을-위한-cli-설정">TypeORM 마이그레이션을 위한 CLI 설정</h3>
<pre><code class="language-pakage.json">&quot;scripts&quot;: {

    ...

    &quot;typeorm&quot;: &quot;node -r ts-node/register ./node_modules/typeorm/cli.js&quot;,
    &quot;typeorm:dev&quot;: &quot;cross-env NODE_ENV=development node -r ts-node/register ./node_modules/typeorm/cli.js -d src/configs/datasource.config.ts&quot;,
    &quot;typeorm:prod&quot;: &quot;cross-env NODE_ENV=production node -r ts-node/register ./node_modules/typeorm/cli.js -d src/configs/datasource.config.ts&quot;
},</code></pre>
<p>TypeScript로 작성된 파일은 Node.js 환경에서 직접 실행할 수 없다. 따라서, <strong>ts-node</strong>를 사용하여 <strong>TypeScript로 작성된 파일을 직접 실행</strong>할 수 있도록 하였다.</p>
<p>또한 TypeORM CLI를 이용해서 데이터베이스 연결 설정 파일을 통해 마이그레이션할 수 있도록 스크립트를 작성하였다.</p>
<h3 id="마이그레이션-파일-생성-명령어">마이그레이션 파일 생성 명령어</h3>
<pre><code class="language-json">// 명령어
npm run typeorm migration:create ./src/{migrations-dir}/{filename}

// 명령어 예시
npm run typeorm migration:create ./src/migrations/CreateUserTable</code></pre>
<h3 id="마이그레이션-파일-작성">마이그레이션 파일 작성</h3>
<pre><code class="language-ts">import { MigrationInterface, QueryRunner } from &#39;typeorm&#39;;

export class CreateUserTable1718647700144 implements MigrationInterface {
  name = &#39;CreateUserTable1718647700144&#39;;

  public async up(queryRunner: QueryRunner): Promise&lt;void&gt; {
    await queryRunner.query(`
      CREATE TABLE IF NOT EXISTS user (
        id            INT            NOT NULL AUTO_INCREMENT PRIMARY KEY                COMMENT &#39;PK&#39;,
        email         VARCHAR(50)    NOT NULL UNIQUE                                     COMMENT &#39;유저 이메일&#39;,
        password      VARCHAR(100)   NULL                                               COMMENT &#39;유저 비밀번호&#39;,
        provider      VARCHAR(50)    NULL                                               COMMENT &#39;OAuth 제공자&#39;,
        provider_id   VARCHAR(100)   NULL                                               COMMENT &#39;OAuth 제공자 id&#39;,
        created_at    TIMESTAMP(6)   NOT NULL DEFAULT CURRENT_TIMESTAMP(6)              COMMENT &#39;생성 시간&#39;,
        updated_at    TIMESTAMP(6)   NOT NULL DEFAULT CURRENT_TIMESTAMP(6) 
                                    ON UPDATE CURRENT_TIMESTAMP(6)                      COMMENT &#39;수정 시간&#39;
      ) ENGINE=InnoDB;
    `);

    await queryRunner.query(`ALTER TABLE user COMMENT = &#39;유저의 중요 정보를 관리하는 테이블&#39;;`);
  }

  public async down(queryRunner: QueryRunner): Promise&lt;void&gt; {
    await queryRunner.query(`DROP TABLE IF EXISTS user;`);
  }
}</code></pre>
<p>생성된 마이그레이션 파일에 위처럼 SQL문을 작성해주면 된다.</p>
<p><strong>up() 메서드</strong>를 통해 테이블과 필요한 주석을 <strong>생성</strong>하고, <strong>down() 메서드</strong>를 통해 생성된 테이블을 <strong>삭제</strong>한다.</p>
<h3 id="마이그레이션-실행-및-롤백">마이그레이션 실행 및 롤백</h3>
<pre><code>// 실행 명령어
npm typeorm:prod migration:run

// 롤백 명령어
npm typeorm:prod migration:revert</code></pre><p>마이그레이션 실행 명령어를 입력하면,
<img src="https://velog.velcdn.com/images/one_ik/post/44d4b4f8-6bad-4356-973c-80ca7c3d3bca/image.png" alt="">
migrations 테이블이 생성되고,
<img src="https://velog.velcdn.com/images/one_ik/post/62468b48-f9a2-49d4-8c6b-f2f370c2494a/image.png" alt="">
TypeORM이 현재 데이터베이스 상태와 마이그레이션 파일을 비교하여, 아직 실행되지 않은 마이그레이션을 순서대로 실행한다. 이를 통해 데이터베이스 스키마를 최신 사앹로 업데이트한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 최종 프로젝트 6~7주차 회고]]></title>
            <link>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-67%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-67%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 10 Jun 2024 23:47:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>앞으로 최종 프로젝트를 진행하면서 겪었던 일들을 기록하고자 한다</p>
</blockquote>
<h2 id="트랜잭션-관리-개선">트랜잭션 관리 개선</h2>
<h3 id="적용-배경">적용 배경</h3>
<p>기존의 트랜잭션 방식은 트랜잭션이 필요한 메서드마다 QueryRunner를 이용하여 하나의 데이터베이스의 연결을 생성하고 컨트롤하였다. 그러다보니, 트랜잭션이 필요한 메서드마다 QueryRunner를 이용해야만했고, 이러한 방식은 굉장히 복잡해보이고 가독성이 떨어졌다. 그래서 트랜잭션을 간편하게 적용할 수 있도록 수정하게 되었다.</p>
<h3 id="적용-방법1-transactioninterceptor-이용하기">적용 방법1: TransactionInterceptor 이용하기</h3>
<p>interceptor를 이용하여 트랜잭션이 필요한 라우터 핸들러에 도달하면 QueryRunner를 생성하여 주입하는 방식으로 작성하였다.</p>
<p>이와 같은 방식은 라우터 핸들러를 통해서만 QueryRunner를 전달할 수 있지만, Nest.js가 Interceptor를 사용하는 방식을 권장하며 구현이 비교적 쉬워 손쉽게 적용할 수 있었다.</p>
<h3 id="문제-발생">문제 발생</h3>
<p>그러나 Interceptor를 이용하는 방식에서는 서비스 계층에서 트랜잭션을 적용하기 위해서는 라우터 핸들러를 거쳐야만 하는 문제가 발생했다.</p>
<p>특히 Google OAuth에서는 GoogleAuthGuard가 GoogleStrategy를 사용해 인증 절차를 처리한다. Nest.js의 LifeCycle를 보면, Interceptor보다 Guard가 먼저 동작하도록 설계되어 있다.</p>
<p>따라서 인증이 통과되면, Google에서 전달받은 사용자 정보를 바탕으로 유저를 생성하게 되는데, Interceptor보다 앞서서 동작하므로 트랜잭션을 적용할 수 없다는 문제가 발생하였다.</p>
<h3 id="적용-방법2-transactionmiddleware-이용하기">적용 방법2: TransactionMiddleware 이용하기</h3>
<p>그래서 이러한 문제를 해결하기 위해 cls-hooked 라이브러리를 이용한 TransactionMiddleware를 적용하였다. </p>
<p>Node.js는 단일 스레드를 사용하기 때문에, 비동기 작업 간 상태를 전달하기 어렵다.(사용자 정보, 트랜잭션 등) cls-hooked는 이러한 비동기 작업 체인에서 context를 유지하고 상태들을 쉽게 공유할 수 있게 도와준다.</p>
<p>cls-hooked를 이용하여 <code>TRANSACION</code>이라는 namespace를 만들어서 EntityManager를 설정하고, <code>@Transactional()</code>이라는 데코레이터를 만들어서 서비스 계층에서 해당 메서드에 손쉽게 트랜잭션을 적용할 수 있었다. </p>
<h2 id="google-oauth">Google OAuth</h2>
<h3 id="적용-배경-1">적용 배경</h3>
<p>소셜 로그인은 신뢰할 수 있는 보안 시스템을 가지고 있다. 따라서 사용자가 이미 신뢰하고 있는 서비스를 통해 로그인을 함으로써, 애플리케이션에 대한 신뢰성을 높이고 싶었다. </p>
<p>또한 사용자가 회원가입 절차를 거치지 않고도 빠르게 서비스에 접근할 수 있어 사용자 경험을 향상시키고자 하였다.</p>
<h3 id="적용-방법-passport-라이브러리-사용하기">적용 방법: Passport 라이브러리 사용하기</h3>
<p>Passport라는 라이브러리를 사용하였는데, Google OAuth만 적용하는게 아니라 naver, kakao, github 등 여러가직 OAuth를 적용해야하기 때문에, 확장성이 뛰어난 Passport 라이브러리를 사용하기로 했다. Passport는 모듈화된 전략 패턴을 사용하여 각 OAuth 제공자별로 간편하게 설정할 수 있다.</p>
<p>이를 통해 사용자는 간편하게 Google 계정으로 로그인할 수 있게 되었다. 또한 추가적으로 Naver, Kakao, GitHub 등 다른 OAuth도 구현해볼 생각이다.</p>
<h2 id="마무리">마무리</h2>
<p>Google OAuth 구현은 생각보다 어려웠다. 처음에는 쉽게 생각했지만, Passport의 동작 방식을 이해하는 데 어려움을 격었고, 트랜잭션이라는 복병이 숨어있던 터라 구현하는데 시간이 많이 걸린 것 같다.</p>
<p>그래도 이러한 작업을 마무리하게 되어서 뿌듯하다. 하지만 아직도 해야할 일이 산더미같다. 기존의 CI/CD 방식이 맘에들지 않아 젠킨스를 활용한 방식으로 수정해볼 계획이고, ECS도 도입해볼 생각이다.</p>
<p>처음에는 해결할 수 있을까 고민했던 작업들을 하나씩 해결하면서 점점 성장하고 있는 것 같다. 조급해지지 말고, 지금처럼만 해보자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 최종 프로젝트 5주차 회고]]></title>
            <link>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-5%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-5%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 25 May 2024 08:19:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>앞으로 최종 프로젝트를 진행하면서 겪었던 일들을 기록하고자 한다</p>
</blockquote>
<h1 id="cd-에러-발생">CD 에러 발생</h1>
<h2 id="변경되지-않은-main-branch의-소스코드가-이미지화-되는-문제-발생">변경되지 않은 main branch의 소스코드가 이미지화 되는 문제 발생</h2>
<pre><code class="language-shell">Containers:
  ingame-backend:
    Container ID:   docker://c6ff2a21abc076645c8b4b4354db5cb7ecc2d520894068200ec5d6d20ac966e1
    Image:          ...ecr.ap-northeast-2.amazonaws.com/ingame-be:64db9d62bb446cb76ee7f602a18522b5a495b606
</code></pre>
<p>위의 코드는 생성된 Kubernetes pod의 정보인데, <code>image</code>를 보면 정상적으로 ECR의 이미지가 적용된 것을 확인할 수 있다.</p>
<p>즉, ECR의 도커 이미지를 제대로 가져와서 배포했다는 소리다. 그러나 변경된 main 브랜치의 소스코드가 아닌, 이전 코드로 만들어진 image가 배포되는게 문제였다.</p>
<p>확인해보니, CI 작업이 끝나고 바로 바로 CD 작업이 진행되도록 설정한 부분이 문제였다.</p>
<h2 id="해결">해결</h2>
<pre><code>// 수정 전
on:
  workflow_run:
    workflows: [&#39;Backend CI&#39;]
    types:
      - completed

// 수정 후
on:
  push:
    branches: [main]
    paths:
      - &#39;server/**&#39;</code></pre><p><code>main</code> 브랜치에 코드가 push될 때 트리거가 발생하도록 수정하였다. 그 결과, 정상적으로 최신 소스코드가 반영된 이미지를 배포할 수 있었다.</p>
<h2 id="고민할-부분">고민할 부분</h2>
<p><img src="https://velog.velcdn.com/images/one_ik/post/754166c3-322e-4aee-91ee-5577dd3dd2e2/image.png" alt=""></p>
<p>위 그림은 정상적으로 CI/CD 작동하는데 걸린 시간이다.</p>
<p><img src="https://velog.velcdn.com/images/one_ik/post/8de09b6b-3cd9-4662-9559-fbb4774190a5/image.png" alt=""></p>
<p>그러나, 평소 3분밖에 걸리지 않던 것이 Github Actions Runner의 네트워크 연결 장애로 인해 배포 작업이 50분씩 걸리는 문제가 발생했다.</p>
<p>임시 방편으로 Self-hosted Runner를 사용하여 내 컴퓨터의 자원을 사용하도록 구성하였다.</p>
<p>하지만, 이런 문제가 다시 발생할 가능성이 있기 때문에, 다른 배포 도구인 Jenkins를 사용하여 CI/CD를 구축해볼 계획이다. Jenkins는 참고 자료가 많고, 다양한 시각화 플러그인이 존재하기 때문에, 그런 플러그인도 사용해보면 좋을 것 같다.</p>
<h1 id="cloudwatch를-통한-로깅-데이터-모니터링">CloudWatch를 통한 로깅 데이터 모니터링</h1>
<p><a href="https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0">이전 글</a>에서 요청, 응답과 에러를 로깅하기 위해 Winston 라이브러리를 사용했지만, 배포 환경에서 모니터링하는 부분이 부족했다. 나는 이를 보완하기 위해 CloudWatch를 사용하여 모니터링을 설정하였다.</p>
<pre><code class="language-ts">    const cloudWatchConfig = {
      logGroupName: &#39;InGame-Logs&#39;,
      logStreamName: &#39;ingame-logger&#39;,
      awsRegion: this.configService.get&lt;string&gt;(&#39;AWS_REGION&#39;),
      awsAccessKeyId: this.configService.get&lt;string&gt;(&#39;AWS_ACCESS_KEY_ID&#39;),
      awsSecretKey: this.configService.get&lt;string&gt;(&#39;AWS_SECRET_ACCESS_KEY&#39;),
      jsonMessage: true,
    };

...
    this.logger = winston.createLogger({
      ...
      if (isProduction) {
        this.logger.add(new winstonCloudWatch(cloudWatchConfig));
      }
    }</code></pre>
<p>나는 <code>winston-cloudwatch</code> 라이브러리를 활용하기로 하였고, 위와 같이 CloudWatch에 대한 환경을 설정한 후, production 환경일 때만 CloudWatch에서 모니터링할 수 있도록 설정하였다.</p>
<p>그러나, 문제가 발생했는데 서버 배포는 성공하였으나, 아무리 서버로 요청을 보내봐도 어떤 데이터도 로깅되지 않았다.</p>
<h2 id="node_envproduction-존재하지-않음">NODE_ENV=production 존재하지 않음</h2>
<pre><code class="language-shell">kubectl exec -it ingame-be-798df9b775-rdzzw -n ingame -- printenv | grep NODE_ENV</code></pre>
<p>위와 같은 명령어로 pod에서 직접 NODE_ENV 환경 변수가 존재하는지 확인해보니, 어떠한 데이터도 나오지 않았다.</p>
<h2 id="해결-1">해결</h2>
<p>이 문제를 해결하기 위해 백엔드 서버에서 필요한 환경 변수를 구성하는 configmap에 <code>NODE_ENV: production</code> 으로 추가 설정하였다. </p>
<p><img src="https://velog.velcdn.com/images/one_ik/post/fee06e1a-16ff-4ed9-b3ed-ace4be27afeb/image.png" alt=""></p>
<p>환경 변수를 추가 설정해주니, 위처럼 정상적으로 로깅 데이터를 모니터링할 수 있게 되었다.</p>
<h1 id="refreshtoken-저장하기">RefreshToken 저장하기</h1>
<h2 id="사용-배경">사용 배경</h2>
<p>기존에는 AccessToken만을 사용하여 인증 절차를 거쳤다. 그러다보니 보안적인 측면에서, AccessToken만 사용하는 것은 위험하다고 판단했고, 따라서 RefreshToken을 통해 AccessToken을 갱신하도록 설정했다.</p>
<p>이때, RefreshToken이 유출되었거나 클라이언트가 로그아웃할 때 서버에서 해당 RefreshToken을 무효화할 수 있어야 한다. </p>
<p>그래서 cookie에 담아서 클라이언트에게 보내는 것 뿐만 아니라 서버에서도 저장소에 RefreshToken을 저장해야 한다고 생각했다.</p>
<h2 id="저장-방식">저장 방식</h2>
<p><img src="https://velog.velcdn.com/images/one_ik/post/63e83703-cc42-4c7f-a8f5-57d20fbe0d5a/image.png" alt=""></p>
<p>저장 방식으로는 2가지가 존재했는데, RDS에 저장하거나 Redis에 저장하는 방식이었다. 그 중 Redis는 메모리 기반 데이터 저장소로 매우 빠른 읽기 및 쓰기 속도를 제공한다. RefreshToken 검증은 매우 빈번하게 이루어질 수 있고, 또한 TTL(Time To Live)을 설정할 수 있어, RefreshToken의 유효 기간을 자동으로 관리하는 데 매우 편리하다고 생각했다.</p>
<p>또한 프로그래머스에서 AWS ElastiCache Redis를 제공해주기 때문에, RefreshToken을 저장하기 위해 Redis를 부담없이 사용할 수 있었다.</p>
<h1 id="마무리">마무리</h1>
<p>프로젝트의 정규 기간이 이번주로 마무리되었다. S3를 통해 이미지를 업로드하거나 OAuth를 구현하는 것 같은 추가 작업을 시간 내에 마무리하지 못한 것이 아쉽지만, 우리 팀은 기간이 지나도 추가적으로 고도화 작업을 계속 진행하기로 하였다.</p>
<p>한달이라는 기간이 정말짧게 느껴졌다. 다른 조들의 발표를 보면서 아직도 많이 부족하다는 것을 느꼈고, 더 열심히 노력해야겠다고 다짐하는 계기가 되었다.</p>
<p>목표했던 프로젝트 완성까지 한 발자국 남은 것 같다. 여태까지 잘 해왔던 것처럼 꾸준히 하다 보면 언젠간 해뜰날이 올 것이다. </p>
<p>좀 더 힘내보자! 화이팅!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스]최종 프로젝트 3~4주차 회고]]></title>
            <link>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 16 May 2024 01:34:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>앞으로 최종 프로젝트를 진행하면서 겪었던 일들을 기록하고자 한다</p>
</blockquote>
<p>백엔드 개발자가 나를 포함하여 두 명이다 보니, 생각보다 담당해야 할 업무가 많았다. 3~4주차에 다양한 기술을 사용하면서, 각 기술을 선택한 이유와 이 과정에서 마주친 문제들과 팀원과의 문제들에 대해서도 얘기해보고자 한다.</p>
<h1 id="winston-로거-사용">Winston 로거 사용</h1>
<h2 id="배경">배경</h2>
<p>개발을 진행하다 보니, 예외처리한 에러에 대해서는 어디에서 에러가 발생했는지 위치를 파악할 수 있지만, 예상치 못한 에러의 발생지는 찾기 어려웠다.</p>
<p>그래서 모든 요청을 받았을 때, 어떤 응답을 보내줬고, 에러가 발생했다면 모든 에러를 기록하는 로깅할 필요성을 느끼게 되었다.</p>
<p>NestJS의 기본 로거도 유용하지만, 다양한 로깅 레벨이 존재하고 커스텀 설정을 제공하여 더 세밀한 로깅을 가능하게 하는 Winston 로거를 선택했다.</p>
<h2 id="적용-방법">적용 방법</h2>
<p>공식 문서는 각 컨트롤러에 Winson 인스턴스를 주입하는 방식을 제안하지만, 이는 컨트롤러에서 담당하는 역할이 과도하게 많아진다고 판단했고, 따라서 중앙에서 모든 요청과 응답, 에러를 가로채서 로깅하면 좋을 것 같다고 판단했다.</p>
<h3 id="1-인터셉터-사용">1. 인터셉터 사용</h3>
<p>처음으로 고민했던 방식이다. NestJS에서는 인터셉터를 통해 메소드 실행 전이나 후에 로깅하는 작업을 추가할 수 있다. 그러나 이러한 로깅 작업은 요청이 컨트롤러의 라우터 핸들러에 도달하기 전에는 작동하지 않기 때문에, 잘못된 요청이나 악의적인 공격에 대해서 로깅할 수 없다라고 판단했다.</p>
<h3 id="2-미들웨어와-예외-필터-사용">2. 미들웨어와 예외 필터 사용</h3>
<p>다음으로 고민한 것은 미들웨어와 예외 필터를 사용하는 것이었다.</p>
<p><img src="https://velog.velcdn.com/images/one_ik/post/c8a29283-75b5-4ede-9c07-0ddc212b0eb1/image.png" alt=""></p>
<p>위 그림은 NestJS의 LifeCycle인데, Middleware를 사용한다면, 모든 요청과 응답에 대해서 로깅할 수 있고, 예외 필터를 통해 에러를 로깅할 수 있다고 판단하여 미들웨어와 예외 필터를 사용하기로 결정했다.</p>
<h2 id="고려할-부분">고려할 부분</h2>
<p>이제 위와 같이 Winston 로거를 사용해서 모든 요청, 응답 그리고 에러를 로깅할 수 있게 되었는데, 이 로그들을 수동으로 검토하는 것은 가독성도 떨어지고, 매우 비효율적이라고 생각한다.</p>
<p>결국, 위와 같은 로그들을 모니터링하기 위한 도구가 필요할텐데, 어떤 도구를 사용해야할지 고민해봐야 할 것 같다.</p>
<h1 id="swagger-적용">Swagger 적용</h1>
<h2 id="배경-1">배경</h2>
<p>지금까지 API를 테스트하기 위해 Postman을 사용하며, 요청과 응답의 구조를 작성해놓은 API문서에서 직접 확인해야 했다.</p>
<p>그러나 위 과정은 번거롭고 시간이 많이 소요되는 작업이었다.</p>
<p>그래서 Swagger라는 도구를 도입하게 되었는데, Swagger는 Rest API를 편리하게 문서화 해줌으로써, 사용자가 요청 값과 응답 값의 예시를 확인해 볼 수 있고, 편리하게 API를 호출해보고 테스트 할 수 있게 해주는 도구다.</p>
<p><img src="https://velog.velcdn.com/images/one_ik/post/0053b9f1-3326-462e-b719-98559ad9469e/image.png" alt=""></p>
<h2 id="효과">효과</h2>
<p>Swagger를 적용함으로써, 프론트엔드 개발자와의 협업을 크게 개선했다. API의 요청과 응답 형식을 실시간으로 확인하고, 직접 테스트 할 수 있는 환경을 제공함으로써 개발 효율성을 높일 수 있었다.</p>
<h1 id="cicd-구축">CI/CD 구축</h1>
<p><img src="https://velog.velcdn.com/images/one_ik/post/dc70a484-d719-4fea-b990-f39e46a5bb79/image.png" alt=""></p>
<h2 id="배경-2">배경</h2>
<p>초기에는 EC2에 Nginx 웹 서버를 설치하고 Minikube를 설치한 후, 컨테이너 안에 Docker 이미지를 수동으로 배포하였다.</p>
<p>그러나, 현재 우리의 프로젝트가 성장함에 따라, 변경 사항을 신속하게 적용하는 것이 점점 어려워졌다.</p>
<p>그래서 배포를 진행함에 있어서 발생할 수 있는 버그 및 코드 오류를 예방하는 동시에 수정된 production 코드를 수동으로 배포했하던 과정을 자동화 함으로써, 사용자 피드백을 더 자주 효과적으로 통합할 수 있게 하는 CI/CD 파이프라인을 구축하게 되었다.</p>
<p>CI/CD 파이프라인을 구축하기 위한 툴은 여러가지가 존재하지만, 우리는 YAML 파일을 통해 커뮤니티에서 제공하는 액션들을 사용하여 손쉽게 CI/CD 파이프라인을 구축할 수 있는 Github Actions를 사용하기로 했다.</p>
<h2 id="동작-방식">동작 방식</h2>
<h3 id="1-변경된-코드를-main-브랜치에-pr-보내기">1. 변경된 코드를 main 브랜치에 PR 보내기</h3>
<p>우리 팀은 브랜치 전략을 <a href="https://techblog.woowahan.com/2553/">Git-Flow</a>전략을 사용하기로 했다. </p>
<p>그래서 변경된 코드를 production이 배포되어 있는 AWS EC2에 적용하기 위해서 main 브랜치에 PR을 보냈을 때만 트리거가 발동하도록 설정하였다.</p>
<h3 id="2-ci테스트와-빌드">2. CI(테스트와 빌드)</h3>
<p>테스트와 빌드해보기 전에, node_module 폴더를 캐시하는 작업을 추가하였다.</p>
<p>node_module 폴더를 캐시함으로써, 반복적인 의존성 설치 시간을 줄이고, 빌드 속도가 향상되도록 설정하였다.</p>
<p>그 후, Jest를 사용하여 테스트를 진행하고, 테스트를 통과하면 Build 과정에서 오류가 발생하지는 않는지 확인해본다.</p>
<h3 id="3-ecr에-docker-이미지-push">3. ECR에 docker 이미지 Push</h3>
<p>위의 CI 과정을 통과했다면, Dockerfile을 통해 docker 이미지를 만든 후, ECR(Elastic Container Registry)에 Push 되도록 설정하였다.</p>
<p>ECR에 Push하기 위해서는 자격 증명 과정이 필요하기 때문에, accessKeyId와 secretAccessKey를 통해 로그인 과정을 거친 후, docker 이미지가 Push 되도록 설정하였다.</p>
<h3 id="4-minikube-cluster에-원격으로-접속하기">4. Minikube Cluster에 원격으로 접속하기</h3>
<p>Github Actions는 최근에 나온 CI/CD 툴이다. 그래서 생각보다 참고 자료가 적은 편이었는데, 우리는 EC2에 Minikube Cluster를 구성하여 그 안에 배포를 진행해야 했는데, 참고할만한 자료가 없는 상황이었다.</p>
<p>그러나 우리는 Github Actions가 쿠버네티스 환경을 조작할 수 있도록 kubectl이 설치되어 있다는 것을 알게 되었고, 원격으로 EC2의 Minikube Cluster에 접속할 수 있도록 환경 설정을 구성하였다.</p>
<h3 id="5-deployment-동적으로-생성">5. Deployment 동적으로 생성</h3>
<p>쿠버네티스는 Deployment를 사용하여 컨테이너화된 애플리케이션을 관리하고 자동으로 업데이트하는데, Deployment를 활용하여 컨테이너의 환경을 구성하기 위해서는 보안적인 문제가 발생할 수 있는 환경 변수들을 입력해줘야 했다.</p>
<p>그래서 Github Secrets를 활용하여 필요한 환경 변수들을 등록해줬고, 그 환경 변수들을 활용하여 동적으로 Deployment YAML 파일을 생성해주기 위해 envsubst를 사용하였다.</p>
<h3 id="6-ecr에서-docker-이미지를-가져와-배포하기">6. ECR에서 docker 이미지를 가져와 배포하기</h3>
<p>동적으로 생성된 Deployment에서 Docker 이미지를 ECR에서 가져와 컨테이너에게 Push해주기 위해서는 ECR에서 해당 docker 이미지를 Pull해올 필요가 있었는데, 이미지를 Pull해오기 위해서는 인증 토큰이 필요했다.</p>
<p>인증 토큰은 12시간마다 갱신해야하기 때문에, 배포를 진행할때마다 기존에 인증 토큰이 있다면 갱신되도록 설정하였다.</p>
<p>그 후, 이미지를 가져와 쿠버네티스의 컨테이너에 배포를 성공적으로 할 수 있게 되었다.</p>
<h2 id="개선-사항">개선 사항</h2>
<p>이번 프로젝트에서는 우리에게 주어진 리소스가 EC2, ECR 뿐이었기 때문에, 위처럼 구성하였지만, Minikube는 개발 및 테스트 용도로 많이 사용한다고 한다. 쿠버네티스 클러스터의 전체 기능을 간소화된 형태로 제공하기 때문에, 프로덕션 환경에는 적합하지 않을 수 있다.</p>
<p>따라서 다음에는 EKS(Elastic Kubernetes Service)를 사용하여 배포를 진행해 보고 싶고, 또한 Github Actions이 아닌 다양한 플러그인이 존재하는 Jenkins를 사용하여 CI/CD를 구축해보고 싶다.</p>
<h1 id="팀원과의-소통-문제">팀원과의 소통 문제</h1>
<p>우리팀은 메인 퀘스트를 수정하기 위해서는 메인 퀘스트와 퀘스트에 종속된 사이드 퀘스트를 함께 수정되도록 기획하였다. </p>
<p>예상치 못한 오류로 인해 일부만 수정되는 상황을 예방하고자 메인 퀘스트와 종속되어 있는 사이드 퀘스트를 트랜잭션을 활용하여 하나의 작업 단위로 묶기로 하였는데, 개발하시는 팀원 분이 메인 퀘스트 수정과 사이드 퀘스트 수정을 분리하여 개발하는 상황이 발생하였다.</p>
<p>위와 같은 변동 사항이 존재할 때, 회의 시간에 말씀해주셨다면 위와 같은 문제가 발생하지 않았겠지만, 위의 문제에 있어서는 나 뿐만 아니라 다른 팀원들에게도 문제가 있었다.</p>
<p>개발 시간이 부족하다는 이유로 팀원분이 올린 PR을 제대로 리뷰하지 못했던 것도 문제였기 때문이다.</p>
<p>그래서 이번 일을 계기로 위와 같은 문제가 발생할 수 있음을 인지하게 되었고, 좀 더 PR을 꼼꼼히 체크하고 리뷰를 진행해야겠다고 다짐하게 되었다.</p>
<h1 id="마무리">마무리</h1>
<p>이번 주도 쉽지 않았다. 할 일은 산더미 같이 쌓여 있고, 이를 해결하기 위해 최대한 빠르게 작업을 진행했다.</p>
<p>그러다보니 팀원과의 소통 문제가 발생했지만, 오히려 좋은 교훈이 되었다.</p>
<p>팀원과의 충분히 소통하지 않았을 때 발생할 수 있는 문제들을 명확히 파악할 수 있었고, 코드 리뷰를 소홀히 할 경우 문제가 더 악화될 수 있다는 것을 깨닫게 되었다.</p>
<p>이 경험을 바탕으로 비슷한 실수를 반복하지 않기 위해 더욱 노력해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스]최종 프로젝트 2주차]]></title>
            <link>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 05 May 2024 17:12:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>앞으로 최종 프로젝트를 진행하면서 겪었던 일들을 기록하고자 한다</p>
</blockquote>
<h2 id="기능-구현-시작">기능 구현 시작</h2>
<p>드디어 업무를 분담하여 기능 구현을 시작하게 되었다. </p>
<p>우리팀의 백엔드 개발자는 총 3명이었는데, 그 중 한명이 취직을 하게되어 이제 2명이 백엔드 서버를 개발하게 되었다. </p>
<p>팀 인원이 줄어든 만큼, 각자 맡은 바 임무가 늘어나긴 했지만, 더 열심히 노력해서 프로젝트를 성공적으로 완수해보려 한다.</p>
<h3 id="회원가입-구조">회원가입 구조</h3>
<p><img src="https://velog.velcdn.com/images/one_ik/post/1234f1df-3cce-4f7d-966a-7678b678fecb/image.png" alt=""></p>
<p>이메일, 닉네임에 유니크 설정을 적용하여 중복 검증 과정을 추가하였다. 또한 우리의 프로젝트는 users 테이블, userInfo 테이블, profilePhoto 테이블이 존재한다. 각각 1:1 관계로 설정되어 있는데, 사용자의 정보를 수정하기 위해 이메일과 비밀번호를 가져오는 것은 불필요하다라고 생각했고, 그래서 따로 사용자의 정보만 저장할 수 있는 테이블을 만들게 되었다.</p>
<p>그렇다보니 회원가입 시 각 테이블에 데이터를 생성해야 하는데, 생성 도중 예상치 못한 에러가 발생한다면, users 테이블에 데이터가 생성되었지만 userInfo 테이블에는 데이터가 생성되지 않는 상황이 발생할 수 있었다.</p>
<p>이러한 경우를 대비해 예상치 못한 에러가 발생한다면 모든 데이터가 롤백되도록 해야했고, 이렇게 하나의 작업 단위로 묶기 위해 트랜잭션을 적용하게 되었다.</p>
<h3 id="로그인-구조">로그인 구조</h3>
<p><img src="https://velog.velcdn.com/images/one_ik/post/a3e3a8b1-b9c6-4e01-882c-8d51af5471ce/image.png" alt=""></p>
<p>로그인 시 사용자의 정보를 서버에서 관리하는 세션 방식 대신, 사용자의 정보를 담은 JWT 토큰을 쿠키에 담아 클라이언트에게 보내도록 하였다. 세션 방식에서는 서버 측에서 세션 정보를 유지하기 위해 메모리나 데이터베이스를 사용해야 하기 때문이다.</p>
<p>하지만 쿠키에 JWT 토큰을 담아 보내는 경우, 쿠키가 탈취되면 만료될 때까지 사용될 수 있기 때문에 보안적으로 단점이 존재할 수 있는데, 이러한 문제를 어떻게 해결할 수 있을지 고민해봐야할 것 같다.</p>
<h3 id="로그인-인증">로그인 인증</h3>
<p><img src="https://velog.velcdn.com/images/one_ik/post/6cbf2c17-bd35-4bb7-903d-2440fb46ae4d/image.png" alt=""></p>
<p>로그인에 성공했을 때, 쿠키에 JWT 토큰을 담아 클라이언트에게 전달한다. 로그인이 필요한 기능에서는 받은 토큰을 다시 서버로 전달해 검증하고, 검증에 성공하면 JWT 토큰 안에 담긴 사용자 정보를 서버에서 전달받아 사용하도록 설계하였다.</p>
<p>NestJS에는 가드라는 것이 있는데, 이는 주로 사용자를 인증하거나 특정 권한을 요구하는 경우에 사용한다. 가든느 데코레이터를 사용하여 쉽게 구현할 수 있었다.</p>
<h3 id="랭킹-조회-구조">랭킹 조회 구조</h3>
<p><img src="https://velog.velcdn.com/images/one_ik/post/6b50849a-e1f2-43c7-93e8-9aecedfeb7d4/image.png" alt=""></p>
<p>랭킹 조회 기능에서는 사용자의 포인트를 기준으로 순위를 매기게 된다. 랭킹을 계산할 때 동점자를 고려해야 한다. 동점자가 여러 명인 경우, 동점자들은 같은 순위를 갖게 되고 다음 순위의 사용자는 그만큼 순위가 뒤로 밀리게 된다. 예를 들어, 포인트 점수가 <code>10/9/9/9/8</code>인 경우 순위는 <code>1/2/2/2/5</code>가 되는 것이다. 순위를 매긴 후, 사용자 정보에 순위 정보를 추가하여 클라이언트에게 보내줬다.</p>
<p>또한 고려해볼 것이 있는데, 현재 테스트에서는 이용자 수가 적기 때문에,<code>67ms</code>라는 시간dl 소요되어 데이터를 가져올 수 있었지만, 만약 사용자의 수가 10만명, 100만명이라면 사용자의 랭킹을 조회하기 위해서는 상당히 많은 시간이 소요될 것이다. </p>
<p>위와 같은 문제를 해결하기 위해 임의의 데이터를 삽입하여 테스트한 후, 최적화시킬 수 있는 방법을 확인해볼 생각이다.</p>
<h2 id="팀원과의-소통-문제">팀원과의 소통 문제</h2>
<p>기능을 구현하던 중 생각지도 못했던 문제가 발생했다. 데이터베이스 스키마에 제약 조건을 설정하는 부분이었는데, 나는 TypeORM의 synchronize라는 기능을 사용하면 자동으로 데이터베이스와 동기화된다는 것을 알고 있었기 때문에, 각 엔티티에 제약조건을 설정하였다. </p>
<p>하지만, 팀원은 MYSQLWorkbench와 같은 도구를 이용하여 미리 제약 조건을 설정한 후 작업을 진행하였다. 그 이유는 백엔드 코드에서 발생할 수 있는 실수나 오류를 사전에 방지할 수 있다라고 생각하셨던 것 같다.</p>
<p>이처럼 서로 소통이 부족한 채 작업을 진행하고 있었던 것이다. 따로 작업을 진행하게 되니 불필요하게 시간이 낭비되었고, 다시 작업해야하는 수고로움이 생겼다. </p>
<p>결국, 대화를 나눠본 결과 synchronize 기능을 사용하는 대신 TypeORM의 마이그레이션 기능을 사용하여 제약조건을 설정하게 되었지만, 다음부터는 이런 공통적으로 처리해야 할 부분에 대해서는 충분한 소통 후 작업을 진행해야 함을 깨닫게 되었다.</p>
<h2 id="마무리">마무리</h2>
<p>이번주도 쉽지만은 않았던 것 같다. 크고 작은 문제들도 있었는데, 갑작스럽게 팀원 한 분이 빠지기도 했고, 소통 부족으로 인한 문제도 있었다.</p>
<p>하지만, 기능 개발하는 것은 즐거웠고, 문제점을 개선해 나가는 과정 자체도 재미있었다.</p>
<p>다음주에 1차 스프린트가 종료되는데, 결과물이 어떻게 나올지 기대가 되고, 또한 제대로 결과물이 나오지 않더라도 그 문제들을 해결하는 과정속에서 많은 것을 배울 수 있을 것 같아 기대된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스]최종 프로젝트 1주차]]></title>
            <link>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@one_ik/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91-1%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 28 Apr 2024 14:06:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>앞으로 최종 프로젝트를 진행하면서 겪었던 일들을 기록하고자 한다</p>
</blockquote>
<h2 id="주제-선정---ingame">주제 선정 - InGame</h2>
<p>&quot;<strong>만약 인생이 게임처럼 내 성장을 수치로 확인할 수 있다면 어떨까?</strong>&quot; 라는 생각을 오랫동안 해왔다. 이러한 주제에 대해서 팀원들에게 제안했을 때, 팀원 모두 흥미를 느꼈고, 긍정적인 반응을 보여주었다. 그래서 위와 같은 주제로 함께 프로젝트를 진행하게 되었다.</p>
<h2 id="데이터베이스-설계">데이터베이스 설계</h2>
<p>프로젝트의 데이터베이스 설계를 진행하게되었다. 퀘스트를 어떻게 생성하고 관리할지에 대한 의견이 갈리게되었는데, 1안은 하나의 큰 테이블에서 메인 퀘스트, 서브 퀘스트, 하위 퀘스트를 모두 관리하는 것이었다. 이렇게 하면 subsidiary라는 별도의 테이블을 통해 메인 퀘스트와 관련된 하위 퀘스트를 연결할 수 있었다.</p>
<p>그러나 내 생각으로는 하위 퀘스트는 메인 퀘스트에 종속되어 있는 퀘스트인데, 굳이 <strong>하나의 퀘스트 테이블에서 생성할 필요가 있을까?</strong>였다</p>
<p>위와 같은 상황들이 구두로만 전달되는 상황이다보니, 내가 생각한 부분을 정확히 설명하지 못해 아쉬웠다.</p>
<p><img src="https://velog.velcdn.com/images/one_ik/post/fe660472-8bdc-46e7-a7e1-efe1365a8bb3/image.png" alt=""></p>
<p>그래서 두 번째 회의에서 위 그림처럼 이미지화하여 각각의 테이블 설계에 대한 장단점을 설명드렸고, 메인 퀘스트에 종속된 하위 퀘스트를 별도의 테이블로 관리하는 구조로 결정하게 되었다.</p>
<p>위의 경험을 토대로 앞으로 다른 사람에게 설명하기 위해서는 말보다는 이미지로 설명드리는게 훨씬 이해하기 쉽다는 것을 깨닫게 되었다. 그래서 앞으로 다이어그램에 대해서도 학습해볼 생각이다.</p>
<h2 id="nestjs-선택-및-환경설정">NestJS 선택 및 환경설정</h2>
<p>우리 팀은 이번 프로젝트에서 백엔드 서버를 구축하기 위해 NestJS 프레임워크를 선택했다. 이전에 사용해봤던 Express 프레임워크는 애플리케이션을 자유롭게 설계할 수 있는 장점이 있지만, 체계적인 구조가 부족하다고 판단했기 떄문에, 우리 프로젝트에서는 NestJS 프레임워크가 더 적합하다고 생각했다.</p>
<p>그 후, NestJS 프레임워크의 백엔드 서버를 구축하기 위해서 초기 환경 설정을 진행했는데, 이전 프로젝트에서 경험해을 토대로, 초반에 개발 환경과 배포 환경을 분리해서 환경설정을 구축하는게 편하다는 것을 깨달았다.</p>
<h3 id="동적으로-환경설정">동적으로 환경설정</h3>
<pre><code class="language-ts">import { Module } from &#39;@nestjs/common&#39;;
import { ConfigModule, ConfigService } from &#39;@nestjs/config&#39;;
import { TypeOrmModule } from &#39;@nestjs/typeorm&#39;;
import { typeORMConfig } from &#39;./config/typeorm.config&#39;;

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      cache: true,
      envFilePath: process.env.NODE_ENV === &#39;production&#39; ? &#39;.production.env&#39; : &#39;.development.env&#39;,
    })
  ],
})
export class AppModule {}</code></pre>
<p>위 코드처럼 <code>NODE_ENV</code> 값을 <code>development</code>와 <code>production</code>으로 설정하여, 환경에 맞는 변수 파일을 자동으로 불러오도록 구성했다.</p>
<p>이로인해 앞으로는 개발 환경과 배포 환경에 필요한 환경 변수 파일을 별도로 작성해두기만 하면, 개발 환경과 배포 환경에 따라 소스 코드를 수정할 필요가 없을 것 같다.</p>
<h2 id="마무리">마무리</h2>
<p>말로만 다른 사람들을 설득하는 과정이 쉽지 않다는 것을 다시 한번 깨닫게되었다. 의견이 다른 상황에서 어떻게하면 상대방이 기분 상하지 않게, 그리고 설득력 있게 말할 수 있을지 고민하게 되는 시간이었다.</p>
<p>그래도 이번 경험을 통해 말만으로 설득하는 것보다 이미지와 같은 시각적 도구를 함께 사용하는 것이 훨씬 더 설득력을 높일 수 있다는 것을 배우게되었다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TypeScript-Express]MySQL2 Connection Pool 사용하기]]></title>
            <link>https://velog.io/@one_ik/TypeScript-ExpressMySQL2-Connection-Pool-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one_ik/TypeScript-ExpressMySQL2-Connection-Pool-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 25 Mar 2024 14:20:44 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>이전에는 JavaScript를 이용하여 Express 기반의 백엔드 서버를 개발하였다. 이번 프로젝트에서는 TypeScript를 도입하여 서버를 만들어보려한다. 그 중 데이터베이스와의 연결을 최적화하기 위해 커넥션 풀(Connection Pool)을 사용한 경험에 대해 공유하려 한다.</p>
<h2 id="커넥션-풀connection-pool이란">커넥션 풀(Connection Pool)이란?</h2>
<p><img src="https://velog.velcdn.com/images/one_ik/post/d3d65012-3175-4314-97ae-07e5029caec1/image.png" alt=""></p>
<p>사용자의 요청마다 데이터베이스와의 연결을 새로 생성하고, 작업 완료 후 연결을 해제하는 것은 비효율적이라고 생각했다. 이러한 문제를 해결하기 위해 커넥션 풀(Connection Pool)이라는 기능을 사용했다.</p>
<p>커넥션 풀이란, 데이터베이스와의 연결을 미리 여러 개 생성해두고, 필요할 때마다 이를 재사용하는 방식을 말한다. 즉, 요청이 들어올 때마다 연결을 생성하는 대신, 미리 준비된 연결을 가져다 사용하고 작업이 끝나면 다시 Pool에 반환하는 것이다.</p>
<p>이러한 커넥션 풀을 이용하여 이전의 연결을 생성하는 과정을 생략할 수 있게 되었다.</p>
<h2 id="커넥션-풀-사용하기">커넥션 풀 사용하기</h2>
<pre><code class="language-ts">// mariadb.ts

import { Pool, createPool } from &quot;mysql2/promise&quot;;
import { MARIADB_DATABASE, MARIADB_HOST, MARIADB_PASSWORD, MARIADB_USER } from &quot;../settings&quot;;

const pool: Pool = createPool({
  host: MARIADB_HOST,
  user: MARIADB_USER,
  password: MARIADB_PASSWORD,
  database: MARIADB_DATABASE,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
});

export default pool;
</code></pre>
<p><code>createPool</code> 메서드를 사용하여 커넥션 풀을 생성한 하였고, 외부에서 사용할 수 있도록 export 해주었다.</p>
<p>위처럼 생성한 커넥션을 가져다 사용하기 위해서는 <code>getConnection()</code>메서드를 이용해야했다.</p>
<p>모든 데이터베이스와 연결하는 곳에서 매번 <code>getConnection</code>메서드를 사용해야한다면, 이것 또한 중복된 코드를 생성하고, 비효율적이라고 생각했다. 하나의 <code>Middleware</code>에서 커넥션을 빌려주고 반환한다면 더 효과적이라 생각했다.</p>
<h2 id="poolconnectionmiddleware">poolConnectionMiddleware</h2>
<pre><code class="language-ts">// poolConnection.middleware.ts

import { Request, Response, NextFunction } from &quot;express&quot;;
import pool from &quot;../utils/mariadb&quot;;

export const poolConnection = async (req: Request, res: Response, next: NextFunction) =&gt; {
  try {
    const connection = await pool.getConnection();
    req.poolConnection = connection;

    res.on(&quot;finish&quot;, () =&gt; {
      if (req.poolConnection) req.poolConnection.release();
    });

    next();
  } catch (error) {
    next(error);
  }
};</code></pre>
<p>애플리케이션의 모든 데이터베이스 요청에서 커넥션 풀을 사용하기 위해, 커넥션을 관리하는 미들웨어를 구현했다.이 미들웨어에서는 풀에서 커넥션을 가져와 Request 객체에 담아줬다.</p>
<p>그 후, HTTP의 모든 요청이 끝나면 커넥션을 반납하도록 설정하였다.</p>
<h2 id="타입-확장">타입 확장</h2>
<pre><code class="language-ts">// express.d.ts
import { PoolConnection } from &quot;mysql2/promise&quot;;

declare global {
  namespace Express {
    interface Request {
      poolConnection?: PoolConnection;
    }
  }
}</code></pre>
<p>그러나 위의 방식에도 문제점이 있었는데, TypeScript를 도입함으로써, Request 객체에 특정 요소를 추가하기 위해서는 타입을 확장할 필요가 있었다.</p>
<p>기존의 Express namespace 아래에 존재하던 Request 인터페이스에 poolConnection이 존재할 수 있도록 추가함으로써, 커넥션 풀을 관리하는 미들웨어에서 Request 객체에 connection을 할당할 수 있게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024.03.21 TIL]]></title>
            <link>https://velog.io/@one_ik/2024.03.21-TIL-wti1dbfi</link>
            <guid>https://velog.io/@one_ik/2024.03.21-TIL-wti1dbfi</guid>
            <pubDate>Thu, 21 Mar 2024 07:21:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 문서는 데브코스 교육 과정 중 학습한 주요 주제들에 대해 이해를 돕고 복습용도로 사용하기 위해 질문과 답변 형식으로 정리되었습니다.</p>
</blockquote>
<h2 id="question">Question</h2>
<h3 id="아키텍쳐">아키텍쳐</h3>
<ul>
<li>Q-1) 아키텍쳐란 무엇인가?</li>
<li>Q-2) 아키텍쳐가 중요한 이유는?</li>
</ul>
<h3 id="쿠버네티스의-서비스">쿠버네티스의 서비스</h3>
<ul>
<li>Q-1) 서비스(Service)란 무엇인가?</li>
<li>Q-2) 서비스의 종류 어떤 것들이 존재하고, 사용 예시에 대해 설명해주시오</li>
</ul>
<h3 id="동적-수평-오토스케일링">동적 수평 오토스케일링</h3>
<ul>
<li>Q-1) 동적 수평 오토스케일링(Horizontal Pod Autoscaler)이란 무엇인가?</li>
<li>Q-2) 동작 원리에 대해 설명해주시오</li>
</ul>
<h3 id="볼륨volumes">볼륨(Volumes)</h3>
<ul>
<li>Q-1) 볼륨이란?</li>
<li>Q-2) PV란?</li>
<li>Q-3) PVC란?</li>
</ul>
<h2 id="answer">Answer</h2>
<h3 id="아키텍쳐-1">아키텍쳐</h3>
<ul>
<li>A-1) 지식을 공유하고 바꾸기 어려운 중요한 어떤 것. 설계 과정 중에 수행되는 다양한 의사 결정의 결과물</li>
<li>A-2) 잘짜여진 아키텍쳐로 만들어진 소프트웨어는 내부 퀄리티가 좋기 때문에, 새로운 기능을 더 빠르게 추가할 수 있다.</li>
</ul>
<h3 id="쿠버네티스의-서비스-1">쿠버네티스의 서비스</h3>
<ul>
<li>A-1) 클러스터 내부에서 동작하는 애플리케이션을 클러스터 외부로 노출시키는 방법을 말한다</li>
<li>A-2) ClusterIP, NodePort, LoadBalancer, Ingress가 존재한다. ClusterIP는 클러스터 내부 IP에서만 접근할 수 있게 한다. 클러스터 내부에서 다른 포드나 서비스와 통신할 때 사용된다. 따라서 테스트, 디버깅 등의 목적으로 제한하여 이용한다.
NodePort는 모든 워커 노드의 특정 포트를 할당하고, 이 포트를 통해 클러스터 외부에서 포드로 접근할 수 있게 한다. NodePort는 ClusterIP 위에 구축되며, 클러스터 내부, 외부 모두에서 접근 가능하다. 개발 또는 테스트 환경에서 클러스터 욉웨서 애플리케이션에 접근할 필요가 있을 때 사용된다.
LoadBalancer는 클러스터 외부에 존재하는 클라우드의 로드 밸런서를 사용하여 서비스에 대한 외부 접근을 가능하게 한다. NodePort와 ClusterIP 설정 위에 추가로 로드 밸런서를 구성한다. 프로덕션 환경에서 고가용성을 요구하는 웹 애플리케이션에 사용된다
Ingress는 단일 IP 주소를 통해 클러스터 내의 여러 서비스에 접근할 수 있도록 해주는 도구다.</li>
</ul>
<h3 id="동적-수평-오토스케일링-1">동적 수평 오토스케일링</h3>
<ul>
<li>A-1) 애플리케이션의 부하량에 따라 포드의 수를 자동으로 조절하는 쿠버네티스의 기능이다.</li>
<li>A-2) 메트릭 서버(Metrics Server)로부터 포드와 노드의 리소스 사용 데이터를 수집하고, 이 데이터를 API를 통해 다른 컴포넌트에 제공되고, 동적 수평 오토스케일링이 설정되어 있는 컨트롤러에서 이 데이터를 전달받아 포드의 수를 자동으로 조절한다</li>
</ul>
<h3 id="볼륨volumes-1">볼륨(Volumes)</h3>
<ul>
<li>A-1) 포드가 실행되는 동안 데이터를 저장하는 일종의 디렉토리. 컨테이너 내의 데이터는 재시작될 때 사라지는데, 이를 방지하기 위해 볼륨을 사용한다. 볼륨은 컨테이너와는 독립적으로 존재하여 데이터를 유지하며, 여러 컨테이너가 동시에 볼륨을 공유할 수 있다.</li>
<li>A-2) PV(Persistent Volume)는 클러스터 내에 존재하는 스토리지를 추상화한 것으로, 사용자는 스토리지의 실제 구현 세부 사항을 몰라도 데이터를 저장할 수 있다.</li>
<li>A-3) PVC(Persistent Volume Claim)는 PV에 저장된 데이터에 접근하고자 할 때 사용하는 요청이다. 사용자는 PVC를 통해 필요한 스토리지의 크기와 접근 모드를 요청할 수 있다. 쿠버네티스 시스템은 이 요청을 만족시킬 수 있는 PV를 자동으로 찾아 바인딩하고, 이 과정을 통해 사용자는 애플리케이션에 필요한 스토리지를 동적으로 할당받을 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024.03.20 TIL]]></title>
            <link>https://velog.io/@one_ik/2024.03.20-TIL</link>
            <guid>https://velog.io/@one_ik/2024.03.20-TIL</guid>
            <pubDate>Wed, 20 Mar 2024 12:48:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 문서는 데브코스 교육 과정 중 학습한 주요 주제들에 대해 이해를 돕고 복습용도로 사용하기 위해 질문과 답변 형식으로 정리되었습니다.</p>
</blockquote>
<h2 id="question">Question</h2>
<h3 id="개발-방법론">개발 방법론</h3>
<ul>
<li>Q-1) 폭포수 방법론이란 무엇이고 장단점은?</li>
<li>Q-2) 애자일 방법론이란 무엇이고 장단점은?</li>
<li>Q-3) 폭포수 방법론과 애자일 방법론을 각각 적용하면 좋을 시나리오는 어떤 것들이 있나?</li>
</ul>
<h3 id="서비스-모델-아키텍처">서비스 모델 아키텍처</h3>
<ul>
<li>Q-1) Reverse Proxy란 무엇이고 특징은?</li>
<li>Q-2) 아래 그림의 서비스 모델에서 사용자가 로그인을 하기 위해 데이터를 입력했을 때, 데이터 전달 흐름이 어떻게 되는지 설명하시오
<img src="https://velog.velcdn.com/images/one_ik/post/cbdc8562-ca3e-4e09-948a-39dd107afe8e/image.jpg" alt=""></li>
</ul>
<h3 id="인프라-환경">인프라 환경</h3>
<ul>
<li>Q-1) AWS S3란 무엇이고 어떠한 용도로 사용되나?</li>
<li>Q-2) AWS ECR이란?</li>
</ul>
<h2 id="answer">Answer</h2>
<h3 id="개발-방법론-1">개발 방법론</h3>
<ul>
<li><p>A-1) 폭포수 방법론이란, 순차적인 과정을 따르는 전통적인 소프트웨어 개발 방법론이다. 요구 사항 정의, 설계, 구현, 검증(테스트), 유지보수 등의 단계를 한 번에 하나씩 차례대로 진행하며, 각 단계가 완료되면 다음 단계로 넘어간다. 각 단계가 명확하고 문서화가 잘 되어 있지만, 유연성이 부족하고 고객 피드백에 제한이 있다</p>
</li>
<li><p>A-2) 애자일 방법론이란, 소프트웨어 개발 과정을 짧은 주기의 스프린트로 나누고, 빠르고 반복적인 개발 주기를 통해 소프트웨어를 개선해 나간다. 프로젝트 요구 사항의 변경에 유연하게 대응할 수 있고, 고객 중심이며 식속하게 초기 버전을 출시하고 지속적으로 개선해 나갈 수 있다.</p>
</li>
<li><p>A-3) 폭포수 방법론은 대규모의 복잡한 시스템이나 규제가 많은 환경(금융)에서 적용하면 좋고, 애자일 방법론은 변경 가능성이 높은 프로젝트나 신속한 제품 출시가 필요한 스타트업에서 적용하면 좋을 것이다.</p>
</li>
</ul>
<h3 id="서비스-모델-아키텍처-1">서비스 모델 아키텍처</h3>
<ul>
<li><p>A-1) Reverse Proxy란, 클라이언트와 서버의 중간에서 요청과 응답을 전달해주는 매개체 역할을 하는 서버다. 요청을 분산시켜 각 서버에 균등하게 부하를 분산시키는 로드 밸런싱 역할을 하고, 서버의 실제 IP 주소를 숨길 수 있게 해줌으로써, 외부 공격으로부터 서버를 보호하는 데 도움을 준다</p>
</li>
<li><p>A-2) 클라이언트(브라우저)가 로그인 페이지를 요청 &gt; 프록시 서버를 거쳐서 FE 서버로 요청 &gt; 요청받은 FE 서버는 로그인 페이지에 대한 소스를 응답 &gt; 프록시 서버를 거쳐 클라이언트에게 응답 전달 &gt; 클라이언트가 로그인 페이지에서 데이터를 입력 &gt; 프록시 서버를 통해 BE 서버의 api 호출 &gt; 백엔드 서버에서 요청 처리 &gt; 결과를 프록시 서버를 통해 클라이언트에게 전달</p>
</li>
</ul>
<h3 id="인프라-환경-1">인프라 환경</h3>
<ul>
<li><p>A-1) S3(Simple Storage Service)는 객체 스토리지 서비스다. 데이터를 파일 형태로 저장하고, 각 파일은 데이터와 메타데이터를 포함한다. S3는 인터넷을 통해 언제 어디서나 접근 가능한 저장공간이며, 사진, 비디오, 로그 파일 등 거의 모든 유형의 데이터를 저장하고 관리할 수 있다.
보통 웹사이트 호스팅, 데이터 백업 및 복구, 빅 데이터 분석 등 다양한 용도로 사용된다</p>
</li>
<li><p>A-2) ECR(Elastic Container Registry)는 Docker 컨테이너 이미지를 저장, 관리, 배포하기 위한 AWS 서비스다. Docker는 애플리케이션을 컨테이너라는 격리된 환경에서 실행할 수 있게 해주는 기술이다. ECR은 이러한 컨테이너 이미지들을 안전하게 저장하고 관리할 수 있는 곳을 제공한다</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Express TypeScript로 환경 설정]]></title>
            <link>https://velog.io/@one_ik/Express-TypeScript%EB%A1%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@one_ik/Express-TypeScript%EB%A1%9C-%ED%99%98%EA%B2%BD-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Sun, 17 Mar 2024 14:24:17 GMT</pubDate>
            <description><![CDATA[<h2 id="typescript-시작">TypeScript 시작</h2>
<h3 id="1-packagejson-파일-생성">1. package.json 파일 생성</h3>
<pre><code class="language-zsh![](https://velog.velcdn.com/images/one_ik/post/8b049ad1-f811-4a4c-9c5c-1da6a0f8c3cc/image.jpg)">
npm init -y</code></pre>
<ul>
<li>Node.js 프로젝트를 초기화하는 데 사용</li>
<li>package.json 파일을 자동으로 생성한다</li>
<li>이 파일은 프로젝트의 설정과 의존성 관리에 중요한 역할을 한다</li>
<li><code>-y flag</code>를 사용하여 사용자의 추가적인 입력 없이 기본값을 사용하여 <code>pakage.json</code> 파일을 생성한다</li>
</ul>
<h3 id="2-typescript-설치-및-설정">2. TypeScript 설치 및 설정</h3>
<h4 id="2-1-설치">2-1. 설치</h4>
<pre><code class="language-zsh">npm install --save-dev typescript ts-node @types/node @types/express</code></pre>
<ul>
<li><strong>ts-node</strong>: TypeScript 파일을 직접 실행할 수 있도록 해주는 도구. 별도의 컴파일 과정 없이 타입스크립트 코드를 실행할 수 있게 해준다.</li>
<li><strong>@types/node</strong>: Node.js의 내장 API에 대한 타입스크립트 타입 정의를 제공한다.</li>
<li><strong>@types/express</strong>: Express.js 라이브러리의 타입스크립트 타입 정의를 제공한다.</li>
</ul>
<h4 id="2-2-설정">2-2. 설정</h4>
<pre><code class="language-zsh">tsc --init</code></pre>
<ul>
<li><code>tsc --init</code>을 실행해서 이 프로젝트를 타입스크립트 프로젝트로 초기화한다.</li>
</ul>
<h3 id="2-3-tsconfigjson-설정">2-3 tsconfig.json 설정</h3>
<pre><code class="language-js">{
    &quot;compilerOptions&quot;: {
        &quot;target&quot;: &quot;es2016&quot;, // 컴파일된 JavaScript의 ECMAScript 버전
        &quot;module&quot;: &quot;commonjs&quot;, // 모듈 시스템
        &quot;moduleResolution&quot;: &quot;node&quot;,
        &quot;outDir&quot;: &quot;./dist&quot;, // 컴파일된 JavaScript 파일이 위치할 디렉토리
        &quot;rootDir&quot;: &quot;./src&quot;, // TypeScript 파일이 위치한 디렉토리
        &quot;strict&quot;: true,
        &quot;esModuleInterop&quot;: true,
        &quot;skipLibCheck&quot;: true,
        &quot;forceConsistentCasingInFileNames&quot;: true    
    },
    &quot;include&quot;: [&quot;src/**/*&quot;], // 컴파일할 파일들
    &quot;exclude&quot;: [&quot;node_modules&quot;] // 컴파일에서 제외할 파일들
}</code></pre>
<h3 id="3-nodemon-설치-및-script-설정">3. nodemon 설치 및 script 설정</h3>
<h4 id="3-1-nodemon-설치">3-1. nodemon 설치</h4>
<pre><code class="language-zsh">npm i --save-dev nodemon</code></pre>
<ul>
<li><code>nodemon</code>은 파일이 변경될 때마다 자동으로 애플리케이션을 재시작해주는 도구다.<h4 id="3-2-script-설정">3-2. script 설정</h4>
</li>
</ul>
<pre><code class="language-js">
&quot;scripts&quot;: {
    &quot;start&quot;: &quot;node dist/app.js&quot;,
    &quot;build&quot;: &quot;tsc&quot;,
    &quot;dev&quot;: &quot;nodemon --exec ts-node src/app.js&quot;
}</code></pre>
<ul>
<li><code>ts-node</code>는 실행 시마다 타입스크립트 코드를 자바스크립트로 변환한다. 이 과정에서 추가 오버헤드가 발생하기 때문에, 프로덕션 환경에서는 권장되지 않는다.</li>
<li>따라서, 프로덕션 환경과 개발 환경을 분리해서 실행할 수 있도록 설정한다.</li>
</ul>
<h3 id="4-개발-환경과-프로덕션-환경에서의-실행">4. 개발 환경과 프로덕션 환경에서의 실행</h3>
<h4 id="4-1-개발-환경에서-실행">4-1. 개발 환경에서 실행</h4>
<pre><code class="language-zsh">npm run dev</code></pre>
<h4 id="4-2-프로덕션-환경에서-실행">4-2. 프로덕션 환경에서 실행</h4>
<pre><code class="language-zsh">npm run build // TypeScript 코드를 JavaScript 코드로 변환

npm start // 컴파일된 JavaScript 코드 실행</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[2024.03.13 TIL]]></title>
            <link>https://velog.io/@one_ik/2024.03.13-TIL</link>
            <guid>https://velog.io/@one_ik/2024.03.13-TIL</guid>
            <pubDate>Wed, 13 Mar 2024 09:39:56 GMT</pubDate>
            <description><![CDATA[<h2 id="question">Question</h2>
<h3 id="도커-컨테이너docker-container">도커 컨테이너(Docker Container)</h3>
<ul>
<li>Q-1) 외부에서 컨테이너에 접속할 때, 접속되지 않는 이유는?</li>
<li>Q-2) 외부에서 컨테이너에 접속하기 위해서 어떻게 해야하나?</li>
</ul>
<h3 id="도커-바인드-마운트bind-mount와-볼륨volume">도커 바인드 마운트(Bind Mount)와 볼륨(Volume)</h3>
<ul>
<li>Q-3) 바인드 마운트란 무엇인가?</li>
<li>Q-4) 볼륨이란 무엇인가?</li>
<li>Q-5) 바인드 마운트와 볼륨의 차이는 무엇인가?</li>
</ul>
<h3 id="쿠버네티스kubernetes">쿠버네티스(Kubernetes)</h3>
<ul>
<li>Q-6) 쿠버네티스란 무엇인가?</li>
<li>Q-7) 쿠버네티스 클러스터의 구조에 대해 설명하시오</li>
<li>Q-8) Pod의 생명주기에 대해서 설명하시오</li>
<li>Q-9) 디플로이먼트란 무엇인가?</li>
<li>Q-10) 외부에서 파드가 실행하는 애플리케이션에 접근하도록 하려면 어떻게 해야하나?</li>
</ul>
<h2 id="answer">Answer</h2>
<h3 id="도커-컨테이너docker-container-1">도커 컨테이너(Docker Container)</h3>
<ul>
<li>A-1) 도커의 컨테이너는 호스트 시스템과 연결되어 있지만, 내부 네트워크로 연결되어 있다. 그래서 외부에서는 접근하기 위해서는 호스트 시스템의 인터페이스를 통해서 접근해야만 한다</li>
<li>A-2) 호스트 시스템과 컨테이너의 포트를 맵핑해야한다. 포트란, 특정 IP주소 내에서 실행되는 프로그램을 식별하기 위해 사용된다. 쉽게 말하면, 컨테이너로 접근할 수 있는 길 안내를 해준다는 것이다</li>
</ul>
<h3 id="도커-바인드-마운트bind-mount와-볼륨volume-1">도커 바인드 마운트(Bind Mount)와 볼륨(Volume)</h3>
<ul>
<li>A-3) 바인드 마운트는 호스트 시스템의 특정 디렉터리를 마운트 상태로 만들어, 컨테이너가 해당 디렉터리의 데이터를 사용 가능한 상태로 만든다</li>
<li>A-4) 볼륨은 도커의 호스트의 파일 시스템에 저장되고, 도커가 직접 관리하며, 볼륨의 데이터를 컨테이너 간에 공유할 수 있다</li>
<li>A-5) 볼륨은 도커가 직접 관리해주지만, 바인드 마운트는 사용자가 직접 관리해야 하고, 볼륨은 보다 더 안전하고, 컨테이너 간 볼륨 데이터를 안전하게 공유할 수 있다. 그리고 볼륨은 백업 및 이동시키기 쉽다</li>
</ul>
<h3 id="쿠버네티스kubernetes-1">쿠버네티스(Kubernetes)</h3>
<ul>
<li>A-6) 쿠버네티스란, 컨테이너화된 애플리케이션의 배포, 확장 및 관리를 자동화하기 위한 오픈소스 시스템이다</li>
<li>A-7) 쿠버네티스의 클러스터는 control plane과 work node로 구성되어 있다. Control Plane은 work node를 관리하고, 컨테이너들의 그룹인 파드의 관리와 파드를 work node에  분배하는 역할을 한다. work node는 컨테이너들이 돌아가는 일종의 컴퓨터다</li>
<li>A-8) kubectl을 통해 API 서버에 파드 생성 요청 &gt; 컨트롤러 매니저가 파드를 생성하고, 이러한 상태를 API에 전달(아직 워커 노드에 적용할지 결정X) &gt; 스케쥴러는 포드가 생성되었다는 정보 인지 후, 어떤 워커 노드에 적용할지 결정 후, 파드 실행 요청 &gt; 워커 노드의 kubelet이 CRI에 요청하여 파드가 워커 노드에 만들어지고 사용가능한 상태가 됨</li>
<li>A-9) 디플로이먼트(Deployment)는 파드와 레플리카셋에 대한 선언적 업데이트를 제공한다. 선언적이라는 용어는 사용자가 원하는 최종 상태를 선언하고, 쿠버네티스 시스템이 현재 상태를 그 최종 상태로 만들기 위해 작업을 수행한다는 의미다. 디플로이먼트는 파드의 생성, 스케일링 및 업데이트를 관리한다</li>
<li>A-10) 서비스 오브젝트를 이용한다. 서비스 오브젝트에는 NodePort, LoadBalancer, ExternalName 등이 존재하는데, NodePort 같은 경우는 특정 파드에서 실행하는 컨테이너의 특정 포트를 노출해서 이 특정 포트를 통해 컨테이너에 접근하도록 한다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>