<?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>👩🏻‍💻</description>
        <lastBuildDate>Fri, 06 Feb 2026 11:04:37 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/dev_0livia/profile/cb8af2bb-0926-4545-a9c0-4d7e9aebbb32/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/dev_0livia" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[CORS 에러]]></title>
            <link>https://velog.io/@dev_0livia/CORS-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@dev_0livia/CORS-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Fri, 06 Feb 2026 11:04:37 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/6e42c10a-8194-4631-8223-0d04b96fe213/image.png" alt=""></p>
<p>처음 백엔드 코드를 AWS 클러스터에 배포하고 S3에 배포한 프론트랑 연결하다가 마주친 CSP 에러!
반갑당</p>
<p>프론트 이미지 빌드할때 API_BASE_URL을 같이 빌드하니 BE로 요청은 가는데 CSP 에러 때문에 에러 발생.</p>
<hr>
<h2 id="s3-배포-환경에서-백엔드-api-호출-실패">S3 배포 환경에서 백엔드 API 호출 실패</h2>
<h2 id="요약">요약</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>문제</td>
<td>S3에 배포된 프론트엔드에서 백엔드 API 호출이 완전히 차단됨</td>
</tr>
<tr>
<td>근본 원인</td>
<td>Content Security Policy(CSP)에 백엔드 도메인이 허용되지 않음</td>
</tr>
<tr>
<td>상태</td>
<td>✅ 해결됨</td>
</tr>
<tr>
<td>심각도</td>
<td>High (서비스 불가)</td>
</tr>
</tbody></table>
<hr>
<h2 id="1-문제-정의">1. 문제 정의</h2>
<h3 id="11-증상">1.1 증상</h3>
<ul>
<li><strong>로컬 환경</strong>: 백엔드 API 호출 정상 작동 (<code>npm run dev</code>)</li>
<li><strong>S3 배포 환경</strong>: 브라우저 Network 탭에 API 요청 자체가 나타나지 않음</li>
<li><strong>증상 특징</strong>: CORS 에러도 표시되지 않고, 요청이 아예 발생하지 않음</li>
</ul>
<h3 id="12-재현-조건">1.2 재현 조건</h3>
<ol>
<li>프론트엔드를 S3에 배포</li>
<li>브라우저에서 S3 URL로 접속</li>
<li>백엔드 API를 호출하는 기능 실행</li>
<li>Network 탭 확인 → 요청 없음</li>
</ol>
<h3 id="13-환경-차이">1.3 환경 차이</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>로컬 개발 환경</th>
<th>S3 배포 환경</th>
</tr>
</thead>
<tbody><tr>
<td>프론트엔드 URL</td>
<td><code>localhost:4180</code></td>
<td><code>s3-bucket-url/AAA</code></td>
</tr>
<tr>
<td>백엔드 호출 방식</td>
<td>Vite 프록시 (<code>/api</code> → 백엔드)</td>
<td>직접 호출 (<code>https://api-AAA.com</code>)</td>
</tr>
<tr>
<td>CSP 적용</td>
<td>개발 모드 (느슨함)</td>
<td>빌드된 HTML의 엄격한 CSP</td>
</tr>
</tbody></table>
<h3 id="14-영향-범위">1.4 영향 범위</h3>
<ul>
<li><strong>영향받는 기능</strong>: 백엔드 API를 사용하는 모든 기능</li>
<li><strong>사용자 영향</strong>: S3 배포 환경에서 서비스 완전 불가</li>
<li><strong>데이터 영향</strong>: 없음 (요청이 발생하지 않음)</li>
</ul>
<hr>
<h2 id="2-타임라인">2. 타임라인</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>활동 내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>문제 보고</strong></td>
<td>&quot;로컬에서는 되는데 S3에서는 백엔드 호출이 안 됨&quot;</td>
</tr>
<tr>
<td><strong>초기 가설</strong></td>
<td>CORS 에러로 추정 → 하지만 Network 탭에 요청 자체가 없음 확인</td>
</tr>
<tr>
<td><strong>환경 분석</strong></td>
<td><code>.env.development</code>와 <code>.env.production</code> 확인 → 환경 변수명 불일치 발견</td>
</tr>
<tr>
<td><strong>빌드 검증</strong></td>
<td>빌드 명령어 확인: <code>VITE_API_BASE_URL=...</code> 환경 변수로 빌드 → API URL은 정상 주입됨</td>
</tr>
<tr>
<td><strong>dist 분석</strong></td>
<td><code>dist/assets/*.js</code> 파일에서 <code>https://api-AAA</code> 확인 → API URL 정상</td>
</tr>
<tr>
<td><strong>CSP 발견</strong></td>
<td><code>index.html</code>의 CSP 메타 태그 확인 → <code>connect-src</code>에 백엔드 도메인 누락 발견</td>
</tr>
<tr>
<td><strong>해결 적용</strong></td>
<td>CSP의 <code>connect-src</code>에 <code>https://*.AAA</code> 추가</td>
</tr>
<tr>
<td><strong>검증</strong></td>
<td>재빌드 및 재배포 필요 (사용자 측에서 확인 예정)</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-근본-원인-분석">3. 근본 원인 분석</h2>
<h3 id="31-5-whys-분석">3.1 5 Whys 분석</h3>
<table>
<thead>
<tr>
<th>#</th>
<th>질문</th>
<th>답변</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>왜 S3에서 백엔드 호출이 안 되는가?</td>
<td>브라우저가 요청을 차단함</td>
</tr>
<tr>
<td>2</td>
<td>왜 브라우저가 요청을 차단하는가?</td>
<td>Content Security Policy 위반</td>
</tr>
<tr>
<td>3</td>
<td>왜 CSP 위반이 발생하는가?</td>
<td><code>connect-src</code>에 백엔드 도메인이 없음</td>
</tr>
<tr>
<td>4</td>
<td>왜 백엔드 도메인이 없는가?</td>
<td>CSP 설정 시 백엔드 도메인을 고려하지 않음</td>
</tr>
<tr>
<td>5</td>
<td>왜 로컬에서는 작동했는가?</td>
<td>Vite 개발 서버의 프록시가 CSP를 우회함</td>
</tr>
</tbody></table>
<p>🎯 <strong>근본 원인</strong>:
<code>index.html</code>의 Content Security Policy에서 <code>connect-src</code> 지시어가 <code>&#39;self&#39; https://*.paddle.com</code>만 허용하도록 설정되어 있었고, 백엔드 API 도메인(<code>https://*.AAA</code>)이 포함되지 않아 브라우저가 요청을 원천 차단했음.</p>
<h3 id="32-기술적-세부-사항">3.2 기술적 세부 사항</h3>
<h4 id="csp-content-security-policy-동작-원리">CSP (Content Security Policy) 동작 원리</h4>
<pre><code class="language-html">&lt;meta http-equiv=&quot;Content-Security-Policy&quot;
  content=&quot;connect-src &#39;self&#39; https://*.paddle.com;&quot; /&gt;</code></pre>
<ul>
<li><code>connect-src</code>: XMLHttpRequest, Fetch API, WebSocket 등 네트워크 요청을 제어</li>
<li><code>&#39;self&#39;</code>: 현재 origin(S3 도메인)만 허용</li>
<li><code>https://*.paddle.com</code>: Paddle 결제 도메인만 허용</li>
<li><strong>문제</strong>: <code>api-dev.skuberplus.com</code>은 허용되지 않음</li>
</ul>
<h4 id="로컬-환경이-정상-작동한-이유">로컬 환경이 정상 작동한 이유</h4>
<pre><code class="language-typescript">// vite.config.ts
server: {
  proxy: {
    &#39;/api&#39;: {
      target: &#39;https://api-AAA&#39;,
      changeOrigin: true,
    },
  },
}</code></pre>
<ul>
<li>로컬에서는 Vite 프록시가 <code>/api</code> 요청을 백엔드로 전달</li>
<li>브라우저 입장에서는 같은 origin(<code>localhost:4180</code>)으로 요청</li>
<li>CSP의 <code>&#39;self&#39;</code>에 해당하여 허용됨</li>
</ul>
<h4 id="s3-배포-환경의-차이">S3 배포 환경의 차이</h4>
<pre><code class="language-typescript">// src/api/client.ts
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
// → &quot;https://api-dev.AAA&quot;

export const apiClient = axios.create({
  baseURL: API_BASE_URL, // 직접 호출
});</code></pre>
<ul>
<li>S3에는 프록시 서버가 없음</li>
<li>브라우저가 직접 `api-dev.AAA으로 요청</li>
<li>CSP에 해당 도메인이 없어 차단됨</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[macOS 앱 개발: WebView 임베딩 테스트를 
  위한 로컬 서버 준비]]></title>
            <link>https://velog.io/@dev_0livia/macOS-%EC%95%B1-%EA%B0%9C%EB%B0%9C-WebView-%EC%9E%84%EB%B2%A0%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%A1%9C%EC%BB%AC-%EC%84%9C%EB%B2%84-%EC%A4%80%EB%B9%84</link>
            <guid>https://velog.io/@dev_0livia/macOS-%EC%95%B1-%EA%B0%9C%EB%B0%9C-WebView-%EC%9E%84%EB%B2%A0%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%A1%9C%EC%BB%AC-%EC%84%9C%EB%B2%84-%EC%A4%80%EB%B9%84</guid>
            <pubDate>Wed, 26 Nov 2025 08:25:41 GMT</pubDate>
            <description><![CDATA[<p>macOS 앱에 WebView를 임베딩하기 전, Docker를 사용해 로컬에서 테스트 서버를 실행하는 방법을 정리하려고 함.</p>
<h2 id="1-docker-desktop-실행">1. Docker Desktop 실행</h2>
<p>Docker 컨테이너를 실행하기 위해서는 먼저 Docker daemon이 실행 중이어야 한다.</p>
<pre><code>open -a Docker</code></pre><p>Docker Desktop이 완전히 시작될 때까지 기다렸다가 실행.</p>
<h2 id="2-nginx-컨테이너-실행">2. Nginx 컨테이너 실행</h2>
<p>Docker daemon이 실행되면 nginx 컨테이너를 시작한다.</p>
<pre><code>docker run -d -p 8080:80 --name webview-test nginx</code></pre><h3 id="명령어-설명">명령어 설명:</h3>
<ul>
<li>-d: 백그라운드에서 실행 (detached mode)</li>
<li>-p 8080:80: 로컬 8080 포트를 컨테이너의 80 포트로 매핑</li>
<li>--name webview-test: 컨테이너에 이름 지정</li>
</ul>
<h3 id="처음-실행-시-출력">처음 실행 시 출력:</h3>
<pre><code> Unable to find image &#39;nginx:latest&#39;
  locally
  latest: Pulling from library/nginx
  88770be1d442: Download complete
  40b6fc5618c6: Download complete
  bb8ecb62799c: Download complete
  b89cf3ec7a3e: Download complete
  2254fb813b11: Download complete
  cc57e8335c98: Download complete
  cf9a807fe41d: Download complete
  Digest: sha256:553f64aecdc31b5bf944521731
  cd70e35da4faed96b2b7548a3d8e2598c52a42
  Status: Downloaded newer image for
  nginx:latest
  b7557f3935858c0c6ca277230b43eb636b26e4025
  e5432697efff0dbb51c44bd</code></pre><p>Docker Desktop 앱에서 실행 중인 nginx 컨테이너를 확인할 수 있다.</p>
<h2 id="3-실행-확인">3. 실행 확인</h2>
<h3 id="실행-중인-컨테이너-확인">실행 중인 컨테이너 확인</h3>
<pre><code>docker ps</code></pre><h3 id="curl로-테스트">curl로 테스트</h3>
<pre><code>curl http://localhost:8080</code></pre><h3 id="접근-가능한-주소">접근 가능한 주소</h3>
<p>다음 주소들로 nginx에 접근할 수 있다.</p>
<ul>
<li><a href="http://localhost:8080">http://localhost:8080</a></li>
<li><a href="http://127.0.0.1:8080">http://127.0.0.1:8080</a></li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/cfac66cd-d715-4752-9a67-edc2dcb3cea1/image.png" alt=""></p>
<h2 id="5-클라이언트-코드에서-사용">5. 클라이언트 코드에서 사용</h2>
<p>WebView를 임베딩할 macOS 앱의 코드에서 다음과 같이 사용할 수 있다.</p>
<pre><code>const nginxUrl = &#39;http://localhost:8080&#39;;</code></pre><h2 id="컨테이너-관리">컨테이너 관리</h2>
<h3 id="컨테이너-중지">컨테이너 중지</h3>
<pre><code>docker stop webview-test</code></pre><h3 id="컨테이너-재시작">컨테이너 재시작</h3>
<pre><code>docker start webview-test</code></pre><h3 id="컨테이너-삭제">컨테이너 삭제</h3>
<pre><code>docker rm webview-test</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Prometheus란?]]></title>
            <link>https://velog.io/@dev_0livia/Prometheus%EB%9E%80</link>
            <guid>https://velog.io/@dev_0livia/Prometheus%EB%9E%80</guid>
            <pubDate>Thu, 06 Nov 2025 07:52:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/ae4ac67c-e5c5-47f0-8491-c11a46398583/image.png" alt=""></p>
<h1 id="prometheus란">Prometheus란?</h1>
<p>Prometheus는 SoundCloud에서 만든 <span style="background:#f7d2c9"><strong>시스템 및 서비스의 상태를 모니터링</strong></span>하는 오픈소스 모니터링 시스템이다.
현재는 Kubernetes 다음으로 인기있는 CNCF 프로젝트라고 볼 수 있다.</p>
<p>Prometheus는 특히 <strong>메트릭 수집</strong>과 <strong>알람</strong>에 특화되어있다.</p>
<h2 id="prometheus-메트릭-수집-방법">Prometheus 메트릭 수집 방법</h2>
<p>메트릭 수집의 경우 HTTP 엔드 포인트를 통한 <code>pull</code> 방식으로 이루어지고 있다.</p>
<h3 id="pull-방식">Pull 방식</h3>
<ul>
<li>주기적으로 타겟 서버를 <span style="background:#f7d2c9"><strong>찾아가서</strong></span> 메트릭을 긁어옴 (scraping)<ul>
<li>ex) 매 15초마다 <code>/metrics</code> 엔드포인트 호출</li>
</ul>
</li>
</ul>
<h3 id="push-방식">Push 방식</h3>
<ul>
<li>각 서버/애플리케이션이 직접 모니터링 시스템으로 메트릭 전송</li>
<li>애플리케이션이 주기적으로 메트릭을 모니터링 시스템의 api로 전송</li>
</ul>
<h3 id="pull-vs-push-방식-비교">Pull vs Push 방식 비교</h3>
<table>
<thead>
<tr>
<th>Pull (Prometheus)</th>
<th>Push (기존 방식)</th>
</tr>
</thead>
<tbody><tr>
<td>앱이 모니터링 서버 주소 몰라도 됨</td>
<td>앱이 모니터링 서버 주소 알아야 함</td>
</tr>
<tr>
<td>네트워크 문제 시 바로 감지</td>
<td>데이터가 안 오면 이유를 모름</td>
</tr>
<tr>
<td>수집 주기를 중앙에서 관리</td>
<td>각 앱마다 따로 설정</td>
</tr>
<tr>
<td>방화벽 설정이 복잡할 수 있음</td>
<td>방화벽 설정 간단</td>
</tr>
</tbody></table>
<h3 id="prometheus-핵심-개념">Prometheus 핵심 개념</h3>
<h4 id="이렇게-수집한-데이터는-span-stylebackgroundf7d2c9시계열-데이터span로-저장한다">이렇게 수집한 데이터는 <span style="background:#f7d2c9"><strong>시계열 데이터</strong></span>로 저장한다.</h4>
<pre><code>시계열 데이터란 모든 데이터를 시간과 함께 저장하는 것이다.  
시간별로 데이터가 어떻게 변했는지 기록하는 것처럼 시간별로 데이터가 저장된다.
&quot;시간&quot; + &quot;벨류&quot;
대신 과거 데이터를 수정하지 않고 계속 쌓기만 한다. 따라서 시간 기반 계산에 최적화되어있다.</code></pre><h4 id="예시">예시</h4>
<p>웹브라우저로 <code>http://.../metrics</code> 접속하면 보이는 내용</p>
<pre><code class="language-javascript">  http_requests_total{method=&quot;GET&quot;, endpoint=&quot;/login&quot;} 1234
  http_requests_total{method=&quot;POST&quot;, endpoint=&quot;/signup&quot;} 567
  memory_usage_bytes 1073741824
  cpu_usage_percent 45.2</code></pre>
<ul>
<li>메트릭이름{라벨=&quot;값&quot;} 현재값</li>
<li>타임스탬프는 보이지 않음 (Prometheus가 수집 시점에 자동 기록)</li>
<li>브라우저로 /metrics 접속하면 현재 시점의 값만 보임 </li>
<li>Prometheus가 주기적으로 이 엔드포인트르 스크레핑해서 타임스탬프랑 같이 저장</li>
</ul>
<h2 id="prometheus-메트릭-타입">Prometheus 메트릭 타입</h2>
<ul>
<li><span style="background:#f7d2c9"><strong>Counter</strong></span><ul>
<li>누적값, 증가만 가능</li>
<li>예: <code>http_requests_total</code>, <code>error_count_total</code></li>
</ul>
</li>
<li><span style="background:#f7d2c9"><strong>Gauge</strong></span><ul>
<li>증가 / 감소 가능한 값</li>
<li>예: <code>cpu_usage_percent</code>, <code>memory_usage_bytes</code></li>
</ul>
</li>
<li><span style="background:#f7d2c9"><strong>Histogram</strong></span><ul>
<li>구간별 분포 측정</li>
<li>예: <code>http_request_duration_seconds</code></li>
</ul>
</li>
<li><span style="background:#f7d2c9"><strong>Summary</strong></span><ul>
<li>백분위수 계산 </li>
<li>예: <code>response_time_percentile</code></li>
</ul>
</li>
</ul>
<h2 id="prometheus-라벨">Prometheus 라벨</h2>
<p>라벨을 통해서 더 세밀하게 구분할 수 있다. key-value로 이루어져 있으며, 영문자로 시작해야한다.
여기서 <strong>__</strong>로 시작하는 이름은 prometheus에서 내부적으로 사용하는 예약어라 사용하면 안된다.</p>
<h3 id="라벨-활용-예시">라벨 활용 예시</h3>
<h4 id="좋은-라벨-설계">좋은 라벨 설계</h4>
<pre><code>  http_requests_total{
    method=&quot;POST&quot;,          # 값이 제한적 (GET, POST, PUT, DELETE)
    endpoint=&quot;/api/users&quot;,  # 엔드포인트 수는 한정적
    status=&quot;200&quot;,          # HTTP 상태 코드 (200, 404, 500 등)
    env=&quot;production&quot;       # 환경 (dev, staging, production)
  }</code></pre><h4 id="나쁜-라벨-설계">나쁜 라벨 설계</h4>
<pre><code>  http_requests_total{
    user_id=&quot;user_123456&quot;,     # 수백만 개의 고유값 -&gt; 메모리 폭발
    timestamp=&quot;2024-01-01&quot;      # 시간은 이미 자동 저장됨
  }</code></pre><p>주의사항: 라벨이 변경되면 변경된 라벨에 맞춰 새로운 시계열이 생성된다. 
라벨 값이 바뀌면 다른 시계열로 취급되어 문제를 발생시킬 수 있다.
이렇게되면 <span style="background:#f7d2c9"><strong>시계열 데이터 수가 매우 증가할 수 있기 때문에 메모리 성능에 문제</strong></span>가 생길 수 있어 정말 필요한 부분에만 라벨을 처리하는게 중요하다.</p>
<h2 id="저장된-데이터-찾기">저장된 데이터 찾기</h2>
<p>Prometheus는 데이터를 저장하기 위해서 <code>Chunk(청크)</code>와 <code>WAL(Write-Ahead-Log)</code>를 사용한다.</p>
<h3 id="chunk">Chunk</h3>
<p>청크는 <span style="background:#f7d2c9"><strong>여러개의 샘플을 하나의 덩어리로 묶은 데이터 구조</strong></span>이다.
만약 1시간 동안의 CPU 사용률 데이터의 경우 아래와 같다.</p>
<pre><code>  [10:00:00, 45%] [10:00:15, 47%] [10:00:30, 52%] ...</code></pre><p>하나의 청크로 압축되어 디스크에 저장된다.
이렇게 압축된 청크 덕분에 Prometheus는 많은 데이터를 효율적으로 저장하고 빠르게 조회할 수 있다.</p>
<h3 id="wal-write-ahead-log">WAL (Write-Ahead-Log)</h3>
<p>WAL은 데이터 안정성을 보장하는 로그 파일이다. 샘플이 수집되게되면 가장 먼저 WAL에 저장된다.</p>
<pre><code>수집 → WAL에 즉시 기록 → 메모리 저장 → 청크로 압축 → 디스크 저장</code></pre><p>WAL의 경우 만약 장애가 발생하게 되면 복구를 도와주는 중요한 역할을 하게 된다. 
만약, Prometheus가 비정상적으로 종료되었을 경우에, WAL 파일을 읽어서 데이터를 복구할 수 있게 된다.</p>
<ol>
<li><strong>메트릭 수집</strong>: Prometheus는 타겟으로부터 메트릭을 수집하고, 이를 시계열 데이터로 변환한다.</li>
<li><strong>WAL 기록</strong>: 수집된 샘플과 해당 시계열 정보는 먼저 WAL에 기록되어 장애 발생 시 복구 가능하게 한다.</li>
<li><strong>메모리 저장</strong>: 수집된 메트릭은 Prometheus의 in-memory TSDB 구조에 시계열(series)과 샘플(sample) 형태로 저장된다.</li>
<li><strong>청크 압축</strong>: 메모리 내의 샘플들은 chunk로 압축되어 저장된다.</li>
<li><strong>디스크 저장</strong>: 일정 시간이 경과하거나 조건이 충족되면 디스크에 block 형태로 flush 된다. 이후 해당 시점까지의 WAL segment는 삭제된다.</li>
<li><strong>Compaction</strong>: 디스크에 저장된 block들은 주기적으로 compaction 과정을 거치며, 이 과정에서 저장
공간을 최적화한다.</li>
</ol>
<h3 id="재시작시-복구-과정">재시작시 복구 과정</h3>
<p>만약 문제가 있어 Prometheus가 재시작이 된다면 다음과 같은 과정을 거치게 된다.</p>
<ol>
<li>WAL 파일을 읽어 메모리에만 존재하던 데이터를 복구한다</li>
<li>시계열과 샘플 정보를 다시 메모리에 로드한다</li>
<li>데이터 연속성을 유지한다.</li>
</ol>
<h4 id="데이터-저장-위치와-보존-기간-설정-예시">데이터 저장 위치와 보존 기간 설정 예시</h4>
<pre><code class="language-yaml"># prometheus.yml
  storage:
    tsdb:
      path: /var/lib/prometheus  # 데이터 저장 경로
      retention.time: 15d         # 15일간 보관
      retention.size: 10GB        # 최대 10GB까지 저장</code></pre>
<h2 id="promql-prometheus-query-language">PromQL (Prometheus Query Language)</h2>
<p>Prometheus는 자체 쿼리인 PromQL을 제공하며, 다양한 방식으로 메트릭을 집계하고 필터링한다.</p>
<h4 id="promql-예시">PromQL 예시</h4>
<pre><code class="language-sql">
  # 5분간 초당 요청 수 (QPS)
  rate(http_requests_total[5m])

  # 엔드포인트별 에러율
  sum(rate(http_requests_total{status=~&quot;5..&quot;}[5m])) by (endpoint)
  /
  sum(rate(http_requests_total[5m])) by (endpoint) * 100

  # 메모리 사용률 80% 넘는 서버 찾기
  (memory_usage_bytes / memory_total_bytes) &gt; 0.8

  # 특정 팀의 프로덕션 서버만 모니터링
  cpu_usage{team=&quot;backend&quot;, env=&quot;production&quot;}
</code></pre>
<h2 id="알람-설정">알람 설정</h2>
<p>Prometheus는 알람 기능도 제공하고 있기 때문에 임계값을 초과하면 알림을 발송해서 문제를 알고 대응할 수 있다.</p>
<h4 id="알람-설정-예시">알람 설정 예시</h4>
<pre><code class="language-yaml">
  groups:
    - name: example
      rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~&quot;5..&quot;}[5m]) &gt; 0.05
        for: 5m
        annotations:
          summary: &quot;{{ $labels.endpoint }} 에러율 5% 초과&quot;
          description: &quot;현재 에러율: {{ $value | humanizePercentage }}&quot;

      - alert: HighMemoryUsage
        expr: memory_usage_bytes / memory_total_bytes &gt; 0.9
        for: 10m
        annotations:
          summary: &quot;메모리 사용률 90% 초과&quot;</code></pre>
<h3 id="모니터링">모니터링</h3>
<p>PromQL의 쿼리 결과들은 보통 다른 모니터링 도구로 그래프와 대시보드 형태로 표현해 확인할 수 있다.
나의 경우 <code>Grafana</code>를 통해서 초기 대시보드 화면을 구현한적 있다.
직접 만든 화면은 회사 데이터가 있으므로 <code>Grafana</code>에서 가져온 것으로 대체한다.
<img src="https://velog.velcdn.com/images/dev_0livia/post/48a8153c-7652-4812-bc24-6e6c3ede0d42/image.png" alt=""></p>
<p>Prometheus는 데이터 수집과 저장에 특화되어있고, 시각화는 주로 Grafana를 연동해서 사용하는 추세이다.</p>
<ul>
<li><ol>
<li>Prometheus가 메트릭 수집/저장</li>
</ol>
</li>
<li><ol start="2">
<li>Grafana가 Prometheus를 데이터소스로 연결함</li>
</ol>
</li>
<li><ol start="3">
<li>대시보드에서 PromQL로 쿼리 작성</li>
</ol>
</li>
<li><ol start="4">
<li>실시간 그래프와 알림 설정 가능</li>
</ol>
</li>
</ul>
<p>실제 Grafana Dashboard Template이 있어서 원하는 템플릿을 직접 가져와서 사용해도 된다.
나의 경우 데이터가 보이지 않거나 다른 데이터를 보여줘야할때 Panel을 추가해서 직접 PromQL로 데이터를 보여줬다</p>
<p><span style="color:#555">* Grafana Dashboard Template: <a href="https://grafana.com/grafana/dashboards/">https://grafana.com/grafana/dashboards/</a> </span></p>
<hr>
<h3 id="prometheus의-한계">Prometheus의 한계</h3>
<p>Prometheus의 경우 분명 <u><strong>메트릭</strong></u>을 수집하는데 특화되어있다. 
그러나 실제 운영에 있어서 모니터링앱을 구성할때는 메트릭 데이터만으로는 모니터링 앱을 만드는데 한계가 있다.
로그나 트레이싱을 지원하지 않기 때문에 완벽한 모니터링 앱을 만들고자한다면,
<span style="background:#f7d2c9"><strong>로그나 트레이싱(예., <code>Jaeger</code>) 데이터를 수집하는 다른 것들이 필요</strong></span>하다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OpenTelemetry란 무엇인가]]></title>
            <link>https://velog.io/@dev_0livia/OpenTelemetry%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@dev_0livia/OpenTelemetry%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Wed, 05 Nov 2025 09:40:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/af4248cb-b183-4c01-af57-63b9ac13eabc/image.png" alt=""></p>
<p><strong>MSA(Microservices Architecture)</strong> 기반 서비스가 증가하면서 서비스 간 의존성과 상호작용이 복잡해지고 있다. </p>
<p>예를 들어 사용자가 주문 버튼을 누르면
<code>유저서비스 → 상품서비스 → 재고서비스 → 결제서비스 → 주문서비스 → 알림서비스...</code></p>
<p>이렇게 엮여있다보니, 주문이 안들어왔을 경우에, 어디서 막혔는지 찾는게 쉽지 않게 되어버렸다.</p>
<p>클라우드 환경에서도 마찬가지다.
클라우드 환경에서도 가끔 알 수 없는 문제가 발생한다.
예를 들어 <code>트래픽이 몰려 자동 스케일링으로 인해 잠깐 서비스에 문제가 발생</code> 한다던가, <code>네트워크가 갑자기 느려져 타임아웃이 발생</code>한다던가 말이다.</p>
<p>예전의 모니터링 툴에서는 CPU나 메모리만 보면 정상처럼 보이기 때문에 잡아내는게 어렵다.</p>
<p>이로 인해 Observability (서비스 관측성)이 중요해지고 있다.</p>
<p>단순히 <code>&quot;서버가 살아있나&quot;</code> 를 보기보다 <strong>내 요청이 어디로 가서 얼마나 걸리고 어디서 막히는지 <u>실시간으로 추적</u></strong>이 필요해졌기 때문이다.</p>
<p>오늘은 Observability를 위한 다양한 오픈 소스 중에 CNCF(Cloud Native Computing Foundation)의 프로젝트인 <span style="background:#cadbfb"><strong>OpenTelemetry</strong></span>에 대해서 공부해보고자 한다.</p>
<hr>
<h2 id="opentelemetry란-무엇일까">OpenTelemetry란 무엇일까?</h2>
<blockquote>
<p>OpenTelemetry는 줄여서 OTel이라 표기하기때문에 지금부터 OTel로 표현하도록 하겠다.</p>
</blockquote>
<p>OTel은 <code>Trace</code>, <code>Metrics</code>, <code>Logs</code> 이 3가지 핵심 관찰성 데이터를 생성하고 수집하고 관리하는 통합 표준이라고 볼 수 있다.</p>
<pre><code>Metrics: 시간에 따른 숫자 측정값(CPU 사용률, 응답 시간, 요청 수)
Log: 이벤트 기록(에러 메세지, 디버그 정보 등). 디버깅에 사용
Traces: 분산 시스템에서 요청의 전체 경로 추적. 호출 관계와 성능 병목 파악 가능</code></pre><p>다만, OTel 자체에서는 <span style="background:#cadbfb"><strong>데이터 저장이나 쿼리 기능을 제공하지 않는다.</strong></span>
대신 표준화된 방식으로 Telemetry 데이터를 생성하고 수집한 뒤, 다양한 백엔드 시스템으로 전송하는 역할만 담당하고 있다.</p>
<ul>
<li><p>상용 서비스의 경우: <code>Dataodg</code>, <code>New Relic</code>, <code>Groundcover</code>와 같은  SaaS 플랫폼</p>
</li>
<li><p>오픈소스 백엔드의 경우: <code>Grafana</code>, <code>Prometheus</code>, <code>Jaeger</code>등의 자체 호스팅 솔루션</p>
<img src="https://velog.velcdn.com/images/dev_0livia/post/14c6914e-1bd8-424e-b830-4aea0214a67e/image.png" />
Grafana 대시보드라면 많이들 들어봤을 것이다.
Grafana에서 OTel로 수집한 데이터를 활용해 실시간 모니터링 대시보드를 구축할 수 있다.


</li>
</ul>
<p>결국, OTel은  <span style="background:#cadbfb"><strong>어떻게 관찰성 데이터를 만들고 보낼 것인가</strong></span>에 대한 표준이지 <span style="background:#cadbfb"><strong>어디에 저장하고 어떻게 볼 것인가</strong></span>는 OTel의 역할이 아니라고 볼 수 있다.</p>
<hr>
<h2 id="opentelemetry-아키텍처-및-구성-요소">OpenTelemetry 아키텍처 및 구성 요소</h2>
<p>OTel은 개발 언어별로 SDK, 데이터 변환 수집, 변환 및 데이터 내보내기 등 여러 구성 요소로 구성된다.</p>
<pre><code>SDK (Software Development Kit)
개발자가 애플리케이션에 관찰성 기능을 쉽게 추가할 수 있도록 제공되는 라이브러리 세트

Java, Python, Go, JavaScript, PHP 등 주요 언어를 지원한다.

https://opentelemetry.io/docs/languages/</code></pre><img src="https://opentelemetry.io/img/otel-diagram.svg" />

<ul>
<li><p><strong>API</strong> :</p>
<ul>
<li>텔레메트리 데이터 생성을 위한 인터페이스</li>
<li>언어별로 제공</li>
</ul>
</li>
<li><p><strong>SDK</strong> : </p>
<ul>
<li>코드에서 자동으로 메트릭, 로그, 트레이스 생성</li>
<li>HTTP 요청, 데이터베이스 쿼리, 외부 API 호출 등을 자동 추적</li>
<li>수동으로 커스텀 매트릭 가능</li>
<li>예) Otel SDK 추가 후 설정 코드 추가 -&gt; Otel Collector가 데이터 수집</li>
</ul>
<ul>
<li><strong>Collector</strong> :<ul>
<li>텔레메트리 데이터 수신, 처리, 전송</li>
<li>다양한 백엔드로 데이터 라우팅 가능</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="opentelemetry-collector">OpenTelemetry Collector</h2>
<img src="https://opentelemetry.io/docs/collector/img/otel-collector.svg" />

<p>Otel Collector는 측정 데이터인 <code>Metrics</code>, <code>Traces</code>, <code>Logs</code>를 수집하고 처리해서 여러 백엔드로 동시에 전송 가능하다.
콜렉터가 없다면 각각의 백엔드 서비스마다 다른 설정을 해야하지만, Collector를 사용하면 각 백엔드 앱마다 설정을 따로 하지 않아도 된다.</p>
<pre><code>
  Collector 없이:
  앱1 → Prometheus
  앱2 → Jaeger
  앱3 → Datadog
  각각 다른 설정, 복잡함


  Collector 있으면:
  앱1,2,3 → OTel Collector → Prometheus
                          → Jaeger
                          → Datadog</code></pre><h3 id="opentelemetry-collector의-구성">OpenTelemetry Collector의 구성</h3>
<p>OTel Collector의 경우 <code>수집 (receive)</code>,  <code>처리 (process)</code>, <code>전송 (export)</code>로 구성된다.</p>
<ul>
<li><strong>Receiver(수집)</strong>: <ul>
<li>여러 애플리케이션에서 보내는 메트릭, 로그, 트레이스를 수신 받는다.</li>
<li>하나 이상의 수집기를 구성할 수 있다.</li>
</ul>
</li>
<li><strong>Processor(처리)</strong>:<ul>
<li>수신한 데이터를 백엔드로 보내기 전에 데이터를 필터링, 변환, 샘플링한다.</li>
<li>민감한 정보를 제거할 수 있고</li>
<li>데이터 포맷을 변환할 수 있다.</li>
</ul>
</li>
<li><strong>Exporter(전송)</strong>:<ul>
<li>여러 백엔드로 동시 전송이 가능하다.</li>
</ul>
</li>
</ul>
<p>이 OTel Collector의 경우 yaml로 설정할 수 있는데 아래의 예와 같다.</p>
<pre><code class="language-yaml"> # collector-config.yaml
  receivers:
    # OTLP 프로토콜로 데이터 수신
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318

    # Prometheus 메트릭 스크래핑
    prometheus:
      config:
        scrape_configs:
          - job_name: &#39;my-app&#39;
            static_configs:
              - targets: [&#39;localhost:8080&#39;]

  processors:
    # 샘플링 (10%만 저장)
    probabilistic_sampler:
      sampling_percentage: 10.0

    # 메모리 제한
    memory_limiter:
      limit_mib: 400
      spike_limit_mib: 100

    # 배치 처리
    batch:
      timeout: 1s
      send_batch_size: 1024

    # 리소스 속성 추가
    resource:
      attributes:
        - key: environment
          value: production
          action: upsert

  exporters:
    # Jaeger로 트레이스 전송
    jaeger:
      endpoint: jaeger-collector:14250
      tls:
        insecure: true

    # Prometheus로 메트릭 전송
    prometheus:
      endpoint: &quot;0.0.0.0:8889&quot;

    # Datadog으로 전송
    datadog:
      api:
        key: &quot;${DD_API_KEY}&quot;
        site: datadoghq.com

  service:
    pipelines:
      # 트레이스 파이프라인
      traces:
        receivers: [otlp]
        processors: [memory_limiter, probabilistic_sampler, batch]
        exporters: [jaeger, datadog]

      # 메트릭 파이프라인
      metrics:
        receivers: [otlp, prometheus]
        processors: [memory_limiter, resource, batch]
        exporters: [prometheus, datadog]

      # 로그 파이프라인
      logs:
        receivers: [otlp]
        processors: [memory_limiter, batch]
        exporters: [datadog]</code></pre>
<h3 id="모니터링-앱에서-opentelemetry를-사용하는-이유">모니터링 앱에서 OpenTelemetry를 사용하는 이유</h3>
<p>지금까지 글을 읽어보면 단번에 대답할 수 있을 것이다. </p>
<p>우선 각 백엔드별로 개별 설정 파일이 필요하다.
버전 업데이트도 따로따로할테고, 문제가 생기면 어느 에이전트 때문인지 알기 어렵다.
만약 Datadog을 쓰다가 New Replic으로 바꾸게 되어도 코드를 전체 다 다시 작성해야할 것이다.</p>
<p>그러나 Otel을 사용하면, Collector의 설정값만 바꾸면 되기 때문에 데이터 수집은 OTel에 맡기고, 데이터 분석과 시각회에 집중할 수 있어 더 나은 모니터링 앱을 만들 수 있을 것이다.</p>
<hr>
<p>출저: <a href="https://opentelemetry.io/docs/">https://opentelemetry.io/docs/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RDBMS vs NoSQL]]></title>
            <link>https://velog.io/@dev_0livia/RDBMS-vs-NoSQL</link>
            <guid>https://velog.io/@dev_0livia/RDBMS-vs-NoSQL</guid>
            <pubDate>Thu, 23 Oct 2025 10:07:13 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/94d8a239-86ef-4de7-aab0-84c634113ead/image.png" alt=""></p>
<p>프로젝트마다 다른 데이터베이스를 사용하는 이유가 궁금해졌다</p>
<p>현재 회사에서 3개의 프로젝트를 담당하고 있는데, 흥미롭게도 각 프로젝트가 서로 다른 데이터베이스를 사용하고 있다. 
하나는 PostgreSQL(RDBMS)을, 나머지 두 개는 MongoDB(NoSQL)를 사용 중이다.</p>
<p>솔직히 말하면, 지금까지는 각 데이터베이스를 &quot;왜&quot; 선택했는지 깊이 고민해본 적이 없다.</p>
<p>운영 중인 첫 번째 프로젝트는 투입 당시 이미 PostgreSQL이 세팅되어 있었고, 급하게 운영에 들어가야 했기에 기술 선택의 배경을 파악할 여유가 없었다. 
나머지 프로젝트들도 선임 개발자들의 결정을 따라갔을 뿐, 그 이유를 깊이 탐구하지 못했다.</p>
<p>그런데 최근 새로운 프로젝트를 시작하면서 또 다시 NoSQL을 사용하게 되자, 문득 의문이 들었다.</p>
<h3 id="span-stylebackgroundacd2bc왜-어떤-프로젝트는-rdbms를-쓰고-어떤-프로젝트는-nosql을-선택하는-걸까span"><span style="background:#acd2bc"><strong>&quot;왜 어떤 프로젝트는 RDBMS를 쓰고, 어떤 프로젝트는 NoSQL을 선택하는 걸까?&quot;</strong></span></h3>
<hr>
<h2 id="span-stylebackgroundacd2bc--데이터베이스를-집🏠으로-비유해보자-span"><span style="background:#acd2bc">  데이터베이스를 집🏠으로 비유해보자. </span></h2>
<h3 id="postgresql-rdbms--아파트-🏢">PostgreSQL (RDBMS) = 아파트 🏢</h3>
<p>아파트를 생각해보면 모든 집이 똑같은 구조로 되어있다.
101호도 방 3개에 화장실 1개, 102호도 방 3개에 화장실 1개.
규칙이 엄격해서 방을 마음대로 추가하거나 제거할 수 없다.
그리고 아파트 자체는 관리사무소가 있기 때문에 관리가 체계적이고 자유롭게 뭔가 관리할 수 없다. </p>
<h3 id="nosql-mongodb--단독-주택-🏠">NoSQL (MongoDB) = 단독 주택 🏠</h3>
<p>한 마을에 단독 주택들을 떠올려보자. 각 주택마다 구조가 저마다 다 다르다. 
어떤 집은 방이 2개고, 어떤 집은 방만 5개에다 지하실과 다락방이 존재할 수도 있다.
아파트에 비해 자유롭게 개조할 수 있고, 관리사무소가 통제하고 있지 않아서 각자 관리하고 있다.</p>
<hr>
<h2 id="span-stylebackgroundacd2bc-이번에는-기술적으로-바라보자span"><span style="background:#acd2bc"> 이번에는 기술적으로 바라보자.</span></h2>
<h3 id="rdbms-postgresql--구조화된-세계">RDBMS (PostgreSQL) = 구조화된 세계</h3>
<h4 id="schema-스키마">Schema 스키마</h4>
<p>RDBMS의 경우 <code>Schema (스키마)</code>라는 미리 정해진 설계도가 존재한다.
모든 데이터가 정해진 <code>테이블</code>과 <code>컬럼</code> 형식을 따라야 한다. 따라서 RDBMS의 경우, 모든 경우를 위한 컬럼을 미리 다 만들어야한다. 
그리고 여기에서는 <code>데이터 타입</code>, <code>길이</code>, <code>관계</code>가 엄격하게 정의되고 있다.</p>
<h4 id="acid-보장">ACID 보장</h4>
<ul>
<li><strong>Atomicity</strong> : 거래가 전부 성공하거나 전부 실패</li>
<li><strong>Consistency</strong> : 데이터 무결성 유지</li>
<li><strong>Isolation</strong> : 동시 작업 간 충돌 방지</li>
<li><strong>Durability</strong> : 한 번 저장된 데이터는 영구 보존</li>
</ul>
<p>따라서 RDBMS의 경우 <strong>정확한 거래 기록</strong>이 필수인 <strong>금융 시스템</strong>이나, <strong>재고 관리</strong> 시스템과 같이 복잡한 관계를 가진 데이터를 다뤄야할때 사용하면 좋다.</p>
<h3 id="nosql--유연한-세계">NoSQL = 유연한 세계</h3>
<h4 id="schema-less">Schema-less</h4>
<p>NoSQL의 경우 단독주택처럼 각 문서(document)가 다른 구조일 수 있다. 따라서 필드를 자유롭게 <u>추가/삭제</u>할 수 있기 때문에 빠른 개발과 변경이 잦을 경우 사용하기 유리하다.</p>
<h4 id="horizontal-scaling-수평적-확장">Horizontal Scaling 수평적 확장</h4>
<p>한 마을에 집들을 더 짓는 것처럼 서버를 늘려서 성능을 향상시킬 수 있고, 대용량 데이터를 처리하거나 분산 처리하기 용이하다.</p>
<p>따라서 NoSQL의 경우 다양한 콘텐츠가 있는 <strong>소셜 미디어</strong>나, 센서마다 다른 데이처인 <strong>IoT 데이터</strong>를 수집할때 좋으며, 실시간 분석이 필요한 빅데이터를 다룰때 사용하면 좋다.</p>
<hr>
<h2 id="span-stylebackgroundacd2bc-실제로-데이터가-어떻게-저장될까span"><span style="background:#acd2bc"> 실제로 데이터가 어떻게 저장될까?</span></h2>
<h3 id="postgresql의-경우">PostgreSQL의 경우</h3>
<pre><code> 학생 테이블 (무조건 이 칸을 다 채워야 함!)
  ┌─────────┬────────┬─────┬────────┬──────────┐
  │ 학생번호  │   이름  │  나이 │   반   │  전화번호  │
  ├─────────┼────────┼─────┼────────┼──────────┤
  │    1    │ 이올뱌   │  17 │  3-2  │ 010-1234  │
  │    2    │ 이슭    │  16 │  2-5  │ 010-5678  │
  │    3    │ 햅삐슭  │  17 │  3-1  │    NULL    │ &lt;- 전화번호 없으면 NULL
  └─────────┴───────┴─────┴───────┴────────────┘


  성적 테이블 (학생 테이블과 연결됨)
  ┌─────────┬─────────┬──────┬──────┐
  │  성적번호 │  학생번호 │  과목  │ 점수  │
  ├─────────┼─────────┼──────┼──────┤
  │    1    │    1    │ 수학  │  90  │
  │    2    │    1    │ 영어  │  85  │
  │    3    │    2    │ 수학  │  95  │
  └─────────┴─────────┴──────┴──────┘</code></pre><p>  특징:</p>
<ul>
<li>이올뱌의 성적을 보려면 학생번호 1을 찾아서 성적 테이블과 연결해야 한다.</li>
<li>새로운 정보(예: 혈액형)를 추가하려면 전체 테이블 구조를 바꿔야 한다. 
현재 학생 테이블에는 <code>학생 번호</code>, <code>이름</code>, <code>나이</code>, <code>반</code>, <code>전화번호</code>만 존재하기 때문이다.</li>
<li>모든 학생이 똑같은 정보를 가져야하고 만약 정보가 없다면 NULL이 들어와야 한다.</li>
</ul>
<h3 id="nosql의-경우">NoSQL의 경우</h3>
<pre><code class="language-sql">[
    {
      &quot;학생번호&quot;: 1,
      &quot;이름&quot;: &quot;이올뱌&quot;,
      &quot;나이&quot;: 17,
      &quot;반&quot;: &quot;3-2&quot;,
      &quot;전화번호&quot;: &quot;010-1234&quot;,
      &quot;성적&quot;: [  // 바로 여기에 성적 정보를 넣음!
        {&quot;과목&quot;: &quot;수학&quot;, &quot;점수&quot;: 90},
        {&quot;과목&quot;: &quot;영어&quot;, &quot;점수&quot;: 85}
      ],
      &quot;특별활동&quot;: [&quot;개발부&quot;, &quot;과학동아리&quot;]  // 이올뱌만 있는 정보
    },
    {
      &quot;학생번호&quot;: 2,
      &quot;이름&quot;: &quot;이슭&quot;,
      &quot;나이&quot;: 16,
      &quot;반&quot;: &quot;2-5&quot;,
      &quot;전화번호&quot;: &quot;010-5678&quot;,
      &quot;성적&quot;: [
        {&quot;과목&quot;: &quot;수학&quot;, &quot;점수&quot;: 95, &quot;담당선생님&quot;: &quot;김선생님&quot;}  // 이슭의 선생님 
  // 정보 추가
      ],
      &quot;알레르기&quot;: [&quot;먼지&quot;]  // 이슭만 있는 정보
    },
    {
      &quot;학생번호&quot;: 3,
      &quot;이름&quot;: &quot;햅삐슭&quot;,
      &quot;나이&quot;: 17,
      &quot;반&quot;: &quot;3-1&quot;
      // 전화번호 필드 자체가 없어도 OK
    }
  ]</code></pre>
<hr>
<h2 id="span-stylebackgroundacd2bc-성능-차이에서-비교해보자-span"><span style="background:#acd2bc"> 성능 차이에서 비교해보자 </span></h2>
<h4 id="rdbms">RDBMS</h4>
<p><span style="background:#acd2bc">정확하지만 느릴 수 있다. </span>
복잡한 관계 연산(<code>join</code>)때문에 느려질 수 있지만, 데이터 정확성은 더 높을 수 있다.</p>
<h4 id="nosql">NoSQL</h4>
<p><span style="background:#acd2bc">빠르지만 주의해야함.</span>
실제로 단순한 조회는 빠르게할 수 있었지만, 연결을 해서 보거나 좀더 깊은 데이터만 뽑아 보려고할 때 연산이 어려운 것을 느꼈다.</p>
<hr>
<h2 id="span-stylebackgroundacd2bc-실제-프로젝트와-비교해보자-span"><span style="background:#acd2bc"> 실제 프로젝트와 비교해보자. </span></h2>
<p>3개의 프로젝트는 성격이 아주 다르다.</p>
<h3 id="a-프로젝트--k8s-관리-플랫폼-mongodb-사용">A 프로젝트 : K8s 관리 플랫폼 MongoDB 사용</h3>
<p>쿠버네티스를 관리하고 있기때문에 쿠버네티스 리소스(<code>pod</code>, <code>service</code>, <code>deployment</code> ...)가 계속해서 변화할 수 있기 때문에 동적인 데이터 구조를 가지고 있다.
시스템 이벤트들이 각각 다른 형태의 메타데이터를 보유하고 있고, 클러스터가 커질수록 대량의 로그와 매트릭 데이터를 처리해야하기 때문에 MongoDB를 사용하게 되었다.</p>
<pre><code class="language-go">  @Schema({ versionKey: false })
  export class Event {
    @Prop() podName: string;
    @Prop() uid: string;
    @Prop() timestamp: string;
    @Prop() type: string;
    @Prop() reason: string;
    @Prop() message: string;
  }
</code></pre>
<h3 id="b-프로젝트--자동차-소프트웨어-관련-시스템">B 프로젝트 : 자동차 소프트웨어 관련 시스템</h3>
<p>차량 소프트웨어 배포의 경우 실패하면 안되기 때문에 엄격한 데이터가 일관적이여야한다.
또한 사용자 - 이벤트 - 권한 등등 서로 밀접하게 연결되어있어서 테이블마다 복잡하게 엮여져있다.</p>
<pre><code class="language-javascript">  @Entity(&#39;&#39;)
  export default class TABLE {
    @PrimaryColumn() versionId: string;
    @Column({ nullable: true }) eventId: string;
    @Column({ nullable: true }) versionName: string;
    @Column({ type: &#39;boolean&#39;, nullable: true }) approval: boolean;
    @Column({ type: &#39;json&#39;, nullable: true })
    part: { ecuNm: string; scomoNodeNm: string }[];
  }
</code></pre>
<p>그러나 B 프로젝트의 경우, <code>Kafka</code>로 거의 1초마다 대량의 로그 데이터를 수신 받고 있다. 그리고 로그에는 JSON 형태로도 저장하고 있기때문에 이런 용도라면 MongoDB가 더 적합할 것이라고 생각했다.</p>
<p>하지만, Kafka 메세지를 수신받기 전 이미 PostgreSQL 기반으로 시스템이 구축되어 있고, 로그 데이터보다 통합 관리의 목적으로 다른 테이블에 있는 데이터들이 더욱 중요하기 때문에 PostgreSQL을 사용하고 있다.
또한 각 테이블마다 매우 밀접하게 연결되어있어서 Kafka를 제외하고서는 PostgreSQL이 운영자 입장에서 더욱 편하게 사용할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Three.js] 인터랙티브 페이지 중간 보고]]></title>
            <link>https://velog.io/@dev_0livia/Three.js-%EC%9D%B8%ED%84%B0%EB%9E%99%ED%8B%B0%EB%B8%8C-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A4%91%EA%B0%84-%EB%B3%B4%EA%B3%A0</link>
            <guid>https://velog.io/@dev_0livia/Three.js-%EC%9D%B8%ED%84%B0%EB%9E%99%ED%8B%B0%EB%B8%8C-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A4%91%EA%B0%84-%EB%B3%B4%EA%B3%A0</guid>
            <pubDate>Fri, 17 Oct 2025 10:22:27 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Three.js 프로젝트를 통해 웹의 새로운 가능성을 탐구하고, 사용자에게 혁신적인 온라인 경험을 제공하고자 합니다.</p>
</blockquote>
<h2 id="span-stylebackground-colord8e0c11-프로젝트-개요span"><span style="background-color:#d8e0c1">1. 프로젝트 개요</span></h2>
<h3 id="11-프로젝트명">1.1 프로젝트명</h3>
<p>“3D 인터랙티브 웹 경험 (3D Interactive Web Experience)” </p>
<h3 id="12-프로젝트-중간-보고">1.2 프로젝트 중간 보고</h3>
<p><code>Three.js</code>를 사용하여 개발된 인터랙티브 3D 우주 시뮬레이션입니다. </p>
<p>메인 프로젝트 전 <code>three.js</code>를 학습하고자 만든 페이지 입니다.
지구와 달, 주변의 별들로 이루어진 간단한 태양계를 시뮬레이션화 했습니다.
지옥에서 온 고양이 캐릭터와 함께 다양한 카메라 컨트롤을 통해 사용자 상호작용 요소를 포함하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/cabb6914-990b-409e-9623-484763ec61c2/image.gif" alt=""></p>
<h3 id="13-기술-스택">1.3 기술 스택</h3>
<ul>
<li><strong>Frontend</strong>: HTML5, CSS3, JavaScript</li>
<li><strong>3D 렌더링</strong>: Three.js</li>
<li><strong>배포 관리:</strong> Netlify</li>
<li><strong>버전 관리</strong>: Git</li>
</ul>
<hr>
<h2 id="span-stylebackground-colord8e0c12-프로젝트-세부-내용span"><span style="background-color:#d8e0c1">2. 프로젝트 세부 내용</span></h2>
<h3 id="-21-프로젝트-구조"># 2.1 프로젝트 구조</h3>
<blockquote>
<p>현재 프로젝트 구조</p>
</blockquote>
<pre><code class="language-jsx">project_root/
│
├── dist/
├── node_modules/
│── src/
│   ├── texture/
│   │   ├── cat_eyes.png
│   │   └── fur.jpg
│   ├── cat.js
│   ├── index.html
│   ├── main.css
│   ├── main.js
│   ├── planet.js
│   └── spaceProject.js
├── babel.config.js
├── package-lock.json
├── package.json
├── README.md
└── webpack.config.js</code></pre>
<h3 id="현재-프로젝트-구조의-문제점">현재 프로젝트 구조의 문제점</h3>
<p><strong>[모듈화 부재]</strong></p>
<ul>
<li><code>spaceProject.js</code> 에  <strong>scene, camera, 객체 생성 등 너무 많은 다양한 기능들이 혼재</strong>되어 있음.</li>
<li><code>cat</code>, <code>planet</code>만 분리되어있고, 다른 주요 컴포넌트들은 개별 모듈로 분리되어있지 않다.</li>
</ul>
<p><strong>→ 가독성 및 유지보수성 떨어짐</strong></p>
<h3 id="22-프로젝트-구조-개선-방안">#2.2 프로젝트 구조 개선 방안</h3>
<blockquote>
<p>컴포넌트 기반 구조화</p>
</blockquote>
<pre><code class="language-jsx">project_root/
│
├── dist/
├── node_modules/
└── src/
    ├── components/
    │   ├── Planet.js
    │   ├── Star.js
    │   └── Cat.js
    ├── scenes/
    │   ├── MainScene.js
    ├── controllers/
    │   └── OrbitController.js
    ├── assets/
    │   ├── textures/
    │   └── models/
    ├── styles/
    │   └── main.css
    ├── index.html
    └── main.js</code></pre>
<ul>
<li><strong><code>components</code></strong>: 각 3D 객체를 개별 컴포넌트로 분리하여 재사용성과 유지보수성을 높일 수 있다.</li>
<li><strong><code>scenes</code></strong>: 여러 씬을 관리할 수 있게 되어 추후 로딩 화면, 메인 화면 등을 별도로 구현할 수 있다.</li>
<li><strong><code>controllers</code></strong>: 입력 처리와 관련된 로직을 한 곳에서 관리할 수 있다. 현재는 카메라 관련 컨트롤러만 넣어 놓은 상태다.</li>
<li><strong><code>assets</code></strong> : 모든 외부 리소스를 관리한다.</li>
<li><strong><code>styles</code></strong>: CSS 파일을 별도 폴더에서 관리한다</li>
</ul>
<hr>
<h2 id="span-stylebackground-colord8e0c13-주요-기능span"><span style="background-color:#d8e0c1">3. 주요 기능</span></h2>
<img src="https://velog.velcdn.com/images/dev_0livia/post/ed63f932-8564-4d37-8f1e-cb6aaf6d3c5a/image.png" />

<pre><code>Geometry &amp; Material만 가지고 만든 
“지옥에서 온 고양이..”</code></pre><h3 id="1-우주-시뮬레이션">1. 우주 시뮬레이션</h3>
<ul>
<li>지구와 달의 사실적인 3D 모델</li>
<li>bump 매핑과 반사 하이라이트를 통해 텍스쳐 표면</li>
<li>지구와 달의 대기 효과</li>
<li>지구 주위를 도는 달의 rotation</li>
</ul>
<h3 id="2-우주-환경-효과">2. 우주 환경 효과</h3>
<ul>
<li>움직이는 별</li>
<li>은하수같은 별 배경 효과</li>
</ul>
<h3 id="3-조명">3. 조명</h3>
<ul>
<li>사실감을 위한 그림자 생성</li>
<li>다양한 조명 포함</li>
</ul>
<h3 id="2-상호작용">2. 상호작용</h3>
<ul>
<li>줌인/아웃 등 카메라 효과</li>
<li>scene 내에서 드래그 가능한 고양이 캐릭터</li>
<li>카메라 및 회전 등 설정을 조정할 수 있는 GUI 컨트롤</li>
<li>전체 화면 rotation</li>
</ul>
<h2 id="span-stylebackground-colord8e0c14-기술적-시도span"><span style="background-color:#d8e0c1">4. 기술적 시도</span></h2>
<blockquote>
<p>핵심 컴포넌트로 createPlanet  를 통해 지구 및 달을 생성한다.</p>
</blockquote>
<h3 id="행성-표면-만들기">[행성 표면 만들기]</h3>
<pre><code class="language-jsx">const surfaceGeometry = new THREE.SphereGeometry(surface.size, 32, 32);
const surfaceMaterial = new THREE.MeshPhongMaterial({
  map: new THREE.TextureLoader().load(surface.textures.map),
  bumpMap: new THREE.TextureLoader().load(surface.textures.bumpMap),
  bumpScale: surface.material.bumpScale,
  specularMap: new THREE.TextureLoader().load(surface.textures.specularMap),
  specular: surface.material.specular,
  shininess: surface.material.shininess,
});
const planetSurface = new THREE.Mesh(surfaceGeometry, surfaceMaterial);
planetGroup.add(planetSurface);</code></pre>
<ul>
<li><code>SphereGeometry</code>로 구 모양을  만든 뒤 . <code>surface.size</code>로 크기를 정한다.</li>
<li><code>MeshPhongMaterial</code>로 행성 표면의 재질을 만든다.<ul>
<li><code>map</code>: 행성 표면의 기본 이미지</li>
<li><code>bumpMap</code>: 표면에 울퉁불퉁한 느낌을 주는 이미지</li>
<li><code>specularMap</code>: 빛이 반사되는 부분</li>
</ul>
</li>
<li><code>Mesh</code>로 구 모양과 재질을 합쳐 행성 표면을 만든다</li>
<li>만든 표면을 <code>planetGroup</code>에 추가한다.</li>
</ul>
<h3 id="행성-표면-만들기-1">[행성 표면 만들기]</h3>
<pre><code class="language-jsx">const surfaceGeometry = new THREE.SphereGeometry(surface.size, 32, 32);
const surfaceMaterial = new THREE.MeshPhongMaterial({
  map: new THREE.TextureLoader().load(surface.textures.map),
  bumpMap: new THREE.TextureLoader().load(surface.textures.bumpMap),
  bumpScale: surface.material.bumpScale,
  specularMap: new THREE.TextureLoader().load(surface.textures.specularMap),
  specular: surface.material.specular,
  shininess: surface.material.shininess,
});
const planetSurface = new THREE.Mesh(surfaceGeometry, surfaceMaterial);
planetGroup.add(planetSurface);</code></pre>
<ul>
<li><code>SphereGeometry</code>로 구 모양을  만든 뒤 . <code>surface.size</code>로 크기를 정한다.</li>
<li><code>MeshPhongMaterial</code>로 행성 표면의 재질을 만든다.<ul>
<li><code>map</code>: 행성 표면의 기본 이미지</li>
<li><code>bumpMap</code>: 표면에 울퉁불퉁한 느낌을 주는 이미지</li>
<li><code>specularMap</code>: 빛이 반사되는 부분</li>
</ul>
</li>
<li><code>Mesh</code>로 구 모양과 재질을 합쳐 행성 표면을 만든다</li>
<li>만든 표면을 <code>planetGroup</code>에 추가한다.</li>
</ul>
<h3 id="⭐️-지구와-달의-대기효과-만들기">[⭐️ 지구와 달의 대기효과 만들기]</h3>
<blockquote>
<p><code>Shader</code> 사용</p>
</blockquote>
<p><code>Shader</code>를 사용하기 전에는 단순한 Mesh만을 이용한 구를 사용해 대기를 표현했다.
이 방식을 사용하니 <strong>실제 대기와 같은 자연스러운 발광 효과를 만들기 어려웠고, 그라데이션 효과를 구현하는 것도 한계</strong>가 있었다.</p>
<p><code>Shader</code>의 경우 <strong>픽셀 단위로 색상과 투명도를 정밀하게 제어</strong>할 수 있고, 카메라 위치에 따라서 <strong>대기의 모습을 동적으로 변경</strong>할 수 있었다.
또한, 수학적인 계산을 통해 더 사실적인 발광 효과를 만들 수 있어서 <code>Shader</code>를 사용하게 되었다.</p>
<pre><code class="language-jsx">const atmosphereMaterial = new THREE.ShaderMaterial({
  vertexShader: `
    ...
  `,
  fragmentShader: `
    uniform vec3 glowColor;
    uniform float coefficient;
    uniform float power;
    void main() {
      float intensity = pow(coefficient - dot(vNormal, vec3(0, 0, 1.0)), power);
      gl_FragColor = vec4(glowColor, 1.0) * intensity;
    }
  `,
  uniforms: {
   ...
  },
  ...
});</code></pre>
<p><strong>[코드 설명]</strong> </p>
<blockquote>
<p><code>GLSL</code>과 <code>Shader</code> 사용</p>
</blockquote>
<p><code>Shader</code>은 픽셀 단위로 색상을 정의하기 때문에 <code>GLSL</code>를 사용했다.
<code>shader</code>에게 명령을 전달하기 위해서는 <code>shader</code> 언어(<strong>GLSL</strong>, HLSL 등)으로 작성된 작성된 코드가 필요하기 때문이다.</p>
<p>GLSL은 Three.js의 기본 material로는 구현하기 어려운 복잡한 시각 효과를 만들 수 있고, vec3, dot() 와 같은 함수를 사용해서 그래픽에 필요한 함수를 제공하기 때문이다.
사실 GLSL은 C 기반 언어라서 C/C++를 사용할 수 있다면 쉽게 작성할 수 있겠지만, 나는 C언어에 대한 지식이 부족하여 공부 후 더 정리하도록 하겠다.</p>
<pre><code class="language-jsx">uniform vec3 glowColor;
uniform float coefficient;
uniform float power;</code></pre>
<ol>
<li><p><strong><code>uniform vec3 glowColor;</code></strong></p>
<p> <code>uniform</code>은 셰이더 프로그램 전체에서 동일한 값을 가지는 변수를 선언할 때 사용한다. <code>vec3</code>는  RGB 색상을 나타낸다. <code>glowColor</code>는 발광 효과의 색상을 지정하는 변수로, 색상 코드를 사용해 대기에 빛나는 부분을 설정한다.</p>
</li>
<li><p><strong>`uniform float coefficient;</strong>
float<code>는 소수점이 있는 숫자 타입, 그리고</code>coefficient`으로, 위에 설정한 발광 효과의 강도를 조절하는 것이다. </p>
</li>
<li><p><strong>`uniform float power;</strong>
power`는 발광 효과가 얼마나 강한지 약한지를 제어해주는 값.</p>
</li>
</ol>
<p>[계산식]</p>
<pre><code class="language-jsx">void main() {
  float intensity = pow(coefficient - dot(vNormal, vec3(0, 0, 1.0)), power);
  gl_FragColor = vec4(glowColor, 1.0) * intensity;
}</code></pre>
<ol>
<li><strong>intensity</strong>: 각 픽셀의 발광 강도</li>
</ol>
<p><strong><code>float intensity = pow(coefficient - dot(vNormal, vec3(0, 0, 1.0)), power);</code></strong>
    - <strong>pow(… , power)</strong>
        - pow는 지곱 함수로, 사용자가 에서 설정한 발광 효과 강도에서 dot(..) 값을 뺀뒤, 발광 효과의 강도를 조절한다, 이를 통해 가장자리에서 중심으로 갈때 더 자연스럽게 발광 효과가 줄어들 수 있도록 설정할 수 있다.
        만약, pow를 사용하지 않더라면, 직선 형태로 일정하게 연한 값으로 줄어들기 때문에 자연스럽지 못하다.</p>
<ul>
<li><strong>coefficient - dot(vNormal, vec3(0, 0, 1.0))</strong><img src="https://velog.velcdn.com/images/dev_0livia/post/5519abfc-2ee3-46c3-a46b-c7a8a63e3a69/image.png" style="width:350px;"/></li>
<li>vNormal은 지구나 달 표면의 각 점에서의 방향을 말한다.
vNormal과 카메라 방향이 일치하면 (dot  = 1) ⇒  행성 중심 ( 구 가운데 ) 
vNormal과 카메라 방향이 거의 수직이라면 (dot = 0) ⇒ 행성 가장자리 (구 보면 옆에 동그라미 라인)</li>
<li>vec3(0, 0, 1.0)은 카메라 방향을 나타내며 정면을 가르키고 있다.</li>
<li>ceofficient - dot()
ceofficient를 1이라고 가정한다면, 행성의 중심: 1 - 1 = 0 (어두움)  | 행성 가장자리: 1 - 0 = 1 (밝음)</li>
<li>위의 값에서 <code>coefficient</code><strong>를 빼는 이유: 
행성의 가장자리가 밝고, 점점 어두워지는 효과를 주기 위해서</strong>임.  그냥 값을 뒤집어서 우리가 원하는 밝다가 점점 어두워지는 효과를 만들기 위함.<img src="https://velog.velcdn.com/images/dev_0livia/post/879163ed-ccec-4fb5-b586-17a348e85e4f/image.png" style="width:350px;" />

</li>
</ul>
<h2 id="span-stylebackground-colord8e0c1shader-사용-전-후span"><span style="background-color:#d8e0c1"><code>Shader</code> 사용 전 후</span></h2>
<blockquote>
<p>Shader 사용 전/후 비교를 위해  임시적으로 발광하는 부분의 크기를 변경해서 비교했다.</p>
</blockquote>
<div style="align:left">
전) 
<img src="https://velog.velcdn.com/images/dev_0livia/post/8969fbf3-9f87-4f9b-8cbe-9159f636aca3/image.png" style="width: 350px;" />
<span>

<ul>
<li><p>그라데이션 효과가 없다.</p>
</li>
<li><p>부자연스럽고 지구위에 막이 덮힌 느낌이라 지구의 색이 선명하지 못함</p>
</span>
</div>
후)
<img src="https://velog.velcdn.com/images/dev_0livia/post/b7402c88-d15b-42b7-ad1c-30e6bf5027c9/image.png" style="width: 350px" />
</li>
<li><p>그라데이션 효과</p>
</li>
<li><p>지구 위에 막이 덮힌 느낌이 없어 지구의 색이 선명함.</p>
</li>
<li><p>질감 효과를 준 것에 영향을 끼치지 않아 더욱 더 사실감 있게 표현 가능</p>
</li>
</ul>
<h2 id="span-stylebackground-colord8e0c15-어려웠던-점span"><span style="background-color:#d8e0c1">5. 어려웠던 점</span></h2>
<p>해당 시뮬레이션을 만들면서 가장 힘들었던점은 에러 로깅.
기존 React나 Next.js 의 경우 콘솔에 에러 내용이 나왔는데 three.js는 어떠한 로그도 찍혀있지 않은 경우가 꽤나 있었다.</p>
<p>scene에 등록을 하지 않는다던가, 컨트롤끼리 충돌이 났다던가, 혹은 뭔가 설정이 잘 못 되어있어서 그냥 흰 바탕만 나오는 경우가 있어서 뭐가 문제인지 알아내기가 조금 힘들었다.</p>
<p>이에 관련해서 에러 로깅이라던지 다른 방법이 있는지 알아보고난 후 적용시켜 본 프로젝트를 시작하면 좋을 것 같다.</p>
<hr>
<p>컨트롤 파넬은 ui 수정이 가능한지 봐야겠다. 뭔가 안이쁨. 내스타일이 아님. 이쁜게조아ㅏㅏㅏㅏ<del>~</del> 🧚‍♀️✨
별도 너무 정신없이 너무 많다. 사용자가 보고있다보면 어지러울 수 있을 것 같아서 좀 더 자연스러운 별을 만들어보도록 해야겠음,.,., </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Three.js] 인터랙티브 홈페이지 프로젝트 계획]]></title>
            <link>https://velog.io/@dev_0livia/Three.js-%EC%9D%B8%ED%84%B0%EB%9E%99%ED%8B%B0%EB%B8%8C-%ED%99%88%ED%8E%98%EC%9D%B4%EC%A7%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B3%84%ED%9A%8D</link>
            <guid>https://velog.io/@dev_0livia/Three.js-%EC%9D%B8%ED%84%B0%EB%9E%99%ED%8B%B0%EB%B8%8C-%ED%99%88%ED%8E%98%EC%9D%B4%EC%A7%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B3%84%ED%9A%8D</guid>
            <pubDate>Fri, 17 Oct 2025 09:33:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Three.js 프로젝트를 통해 웹의 새로운 가능성을 탐구하고, 사용자에게 혁신적인 온라인 경험을 제공하고자 합니다.</p>
</blockquote>
<h2 id="span-stylebackground-colord8e0c11-프로젝트-개요span"><span style="background-color:#d8e0c1">1. 프로젝트 개요</span></h2>
<h3 id="11-프로젝트명">1.1 프로젝트명</h3>
<p>“3D 인터랙티브 웹 경험 (3D Interactive Web Experience)” </p>
<h3 id="12프로젝트-목적">1.2프로젝트 목적</h3>
<p><strong><code>Three.js</code></strong>를 활용하여 사용자에게 독특하고 몰입감 있는 3D 웹 경험을 제공하는 인터랙티브 홈페이지 목업 개발</p>
<h3 id="13-프로젝트-기대-효과">1.3 프로젝트 기대 효과</h3>
<ul>
<li>기업 또는 개인의 온라인 존재감 강화</li>
<li>사용자 참여도 및 체류 시간 증가</li>
<li>기술 혁신을 통한 브랜드 이미지 제고</li>
</ul>
<h2 id="span-stylebackground-colord8e0c12-프로젝트-세부-내용span"><span style="background-color:#d8e0c1">2. 프로젝트 세부 내용</span></h2>
<h3 id="21-주요-기능">2.1 주요 기능</h3>
<ol>
<li><strong>3D 인터랙티브 내비게이션</strong><ul>
<li>3D 공간에서 마우스/터치로 이동 가능
메뉴 시스템</li>
</ul>
</li>
<li><strong>동적 3D 배경</strong><ul>
<li>사용자 상호작용에 반응하는 애니메이션 배경</li>
</ul>
</li>
<li><strong>3D 제품/서비스 쇼케이스</strong><ul>
<li>회전, 확대/축소가 가능한 3D 모델 전시</li>
</ul>
</li>
<li><strong>인터랙티브 데이터 시각화</strong><ul>
<li>3D 그래프, 차트를 통한 정보 표현</li>
</ul>
</li>
<li><strong>가상 투어</strong><ul>
<li>3D 공간을 탐험할 수 있는 가상 환경</li>
</ul>
</li>
</ol>
<h3 id="22-기술-스택">2.2 기술 스택</h3>
<ul>
<li><strong>Frontend</strong>: HTML5, CSS3, JavaScript</li>
<li><strong>3D 렌더링</strong>: Three.js</li>
<li><strong>반응형 디자인</strong>: CSS Grid, Flexbox</li>
<li><strong>버전 관리</strong>: Git</li>
</ul>
<h3 id="23-개발-단계">2.3 개발 단계</h3>
<ol>
<li><strong>기획 및 디자인 (2주</strong>)<ul>
<li>와이어프레임 및 3D 모델 concept 설계</li>
</ul>
</li>
<li><strong>3D 모델링 및 텍스처 작업 (3주)</strong><ul>
<li>필요한 3D 에셋 제작</li>
</ul>
</li>
<li><strong>Three.js 기반 개발 (4주)</strong><ul>
<li>3D 렌더링 및 인터랙션 구현</li>
</ul>
</li>
<li><strong>UI/UX 개선 및 최적화 (2주)</strong><ul>
<li>사용자 경험 향상 및 성능 최적화</li>
</ul>
</li>
<li><strong>테스트 및 디버깅 (1주)</strong><ul>
<li>크로스 브라우저 테스트 및 버그 수정</li>
</ul>
</li>
</ol>
<h2 id="span-stylebackground-colord8e0c13-리소스span"><span style="background-color:#d8e0c1">3. 리소스</span></h2>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/022caa5a-9e02-410c-8943-b6fb097f2025/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영 서버 CPU 사용률 최적화 - PostgreSQL 인덱스 추가로 해결하기]]></title>
            <link>https://velog.io/@dev_0livia/%EC%9A%B4%EC%98%81-%EC%84%9C%EB%B2%84-CPU-%EC%82%AC%EC%9A%A9%EB%A5%A0-%EC%B5%9C%EC%A0%81%ED%99%94-PostgreSQL-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%B6%94%EA%B0%80%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_0livia/%EC%9A%B4%EC%98%81-%EC%84%9C%EB%B2%84-CPU-%EC%82%AC%EC%9A%A9%EB%A5%A0-%EC%B5%9C%EC%A0%81%ED%99%94-PostgreSQL-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%B6%94%EA%B0%80%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 27 Aug 2025 00:54:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/8ed343a9-3270-495c-aa0a-aadafd9ba8ff/image.png" alt=""></p>
<h1 id="문제-상황">문제 상황</h1>
<p>운영 중인 서비스에서 데이터 센터로부터 CPU 사용량이 MAX를 기록했다는 알림 메일을 받았다. 처음에는 특정 시간대에만 발생하는 문제라면 크론잡(cron job) 관련 이슈일 가능성을 생각했지만, 실제로는 불특정 시간에 지속적으로 발생하는 문제였다.</p>
<h1 id="문제-원인-분석">문제 원인 분석</h1>
<p>CPU 사용률이 급증하는 원인을 파악하기 위해 데이터베이스 성능부터 점검해보기로 했다. PostgreSQL에서 테이블별 스캔 통계를 확인할 수 있는 쿼리를 실행했다.</p>
<pre><code class="language-sql">SELECT schemaname, relname, seq_scan, seq_tup_read, idx_scan
FROM pg_stat_user_tables
ORDER BY seq_tup_read DESC;</code></pre>
<p>이 쿼리를 통해 각 테이블의 아래 목록을 확인할 수 있었다: </p>
<ul>
<li>seq_scan: Sequential Scan(전체 테이블 스캔) 횟수</li>
<li>seq_tup_read: Sequential Scan으로 읽은 행의 총 개수</li>
<li>idx_scan: Index Scan 횟수</li>
</ul>
<h1 id="문제-발견">문제 발견</h1>
<p>통계 결과를 분석한 결과, 가장 많이 스캔되는 상위 4개 테이블에서 인덱스가 설정되지 않은 컬럼들이 빈번하게 조회되고 있었다. 이로 인해 매번 전체 테이블을 스캔(Full Table Scan)하면서 CPU 사용률이 급증한 것으로 판단되었다.</p>
<h1 id="해결-방법">해결 방법</h1>
<p>운영 중인 서비스이므로 서비스 중단 없이 인덱스를 추가해야 했다. PostgreSQL의 CONCURRENTLY 옵션을 사용하여 인덱스를 생성했다.</p>
<pre><code class="language-sql">CREATE INDEX CONCURRENTLY index_name ON &quot;table_name&quot; (&quot;column_name&quot;);</code></pre>
<hr>
<h3 id="concurrently-옵션을-사용하는-이유">CONCURRENTLY 옵션을 사용하는 이유</h3>
<h4 id="✅-concurrently-사용-시">✅ CONCURRENTLY 사용 시</h4>
<ul>
<li>인덱스 생성 중에도 테이블에 대한 읽기/쓰기 작업이 정상적으로 가능</li>
<li>서비스 중단 없이 인덱스 생성 가능</li>
<li>사용자 경험에 영향 없음</li>
</ul>
<h4 id="❌-concurrently-미사용-시">❌ CONCURRENTLY 미사용 시</h4>
<ul>
<li>인덱스 생성 동안 해당 테이블이 완전히 락(lock) 됨</li>
<li>모든 INSERT, UPDATE, DELETE 작업이 블록됨</li>
<li>테이블 크기에 비례하여 락 시간 증가 (수 분 ~ 수십 분)</li>
<li>서비스 장애 발생 위험 </li>
</ul>
<hr>
<h1 id="성능-개선-결과">성능 개선 결과</h1>
<p>인덱스 추가 후 쿼리 실행 시간을 테스트해보았다.</p>
<pre><code class="language-sql">EXPLAIN (ANALYZE, BUFFERS)
SELECT &quot;table&quot;.&quot;column&quot;
FROM &quot;table&quot;
WHERE &quot;table&quot;.&quot;column&quot; = &#39;test_value&#39;
LIMIT 1;</code></pre>
<p>실행 계획 분석 결과, <strong>쿼리 실행 시간이 대폭 단축</strong>되었다.</p>
<h1 id="결론">결론</h1>
<ul>
<li>운영 서버의 성능 이슈 발생 시 데이터베이스 스캔 통계 확인이 효과적</li>
<li>자주 조회되는 컬럼에는 반드시 적절한 인덱스 설정 필요</li>
<li>운영 중인 서비스에서는 CONCURRENTLY 옵션으로 무중단 인덱스 생성 권장</li>
<li>성능 최적화 후 반드시 실행 계획 분석을 통한 검증 수행</li>
</ul>
<p>간단한 인덱스 추가만으로도 시스템 성능을 크게 개선할 수 있었던 사례였다.
자세한건 운영팀의 모니터링 결과를 좀 보긴 해야한다..! </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[HackerRank] Diagonal Difference]]></title>
            <link>https://velog.io/@dev_0livia/HackerRank-Diagonal-Difference</link>
            <guid>https://velog.io/@dev_0livia/HackerRank-Diagonal-Difference</guid>
            <pubDate>Mon, 11 Aug 2025 04:07:53 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-설명">문제 설명</h2>
<p>정사각형 형태의 2차원 배열(행렬)이 주어졌을 때, 왼쪽 대각선 (primary diagonal) 과 오른쪽 대각선 (secondary diagonal)의 합의 절댓값 차이를 구하시오.</p>
<h3 id="입력-예시">입력 예시</h3>
<pre><code>3
11 2 4
4 5 6
10 8 -12</code></pre><p>11 + 5 + -12 = 4
4 + 5 + 10 = 19
답 : 4 - 19 = 15</p>
<hr>
<h3 id="풀이-과정">풀이 과정</h3>
<p>처음에 어떻게 풀어야하는지 이해가 가지 않아서 차근차근 하나씩 손으로 적어가면서 이해했다.
여기서 가장 중요했던 개념은 <code>array[i][i]</code></p>
<pre><code>array[i][i]란?
해당 열의 인텍스를 찾는 것이다.</code></pre><p>예를 들어 [1, 3, 5]가 있다고 하면, array[0][1]은 3이 된다.
0번째열에 1번째 인덱스의 값을 찾는 것이기 때문이다.</p>
<pre><code>11 2 4
4 5 6
10 8 -12</code></pre><p>의 경우에는 다음과 같이 손으로 먼저 적었다. 
11, 5, -12를 먼저 추출해야했으니,</p>
<ul>
<li>arr[0][0] = 11</li>
<li>arr[1][1] = 5</li>
<li>arr[2][2] = -12</li>
</ul>
<p>그 다음으로 4, 5, 10 추출</p>
<ul>
<li>arr[0][2] = 4</li>
<li>arr[1][1] = 5</li>
<li>arr[2][0] = 10</li>
</ul>
<p>여기서 가만히보면 이런 생각이 든다. 왼쪽에서 오른쪽으로 내려가는 대각선의 경우 인덱스와 열이 같다.
arr[i][i]
하지만 오른쪽에서 왼쪽으로 내려가는 대각선은 다음과 같은 느낌이다.
지금 현재 배열의 길이가 3개다.
그럼 arr[i][배열의 길이 - i -1]을 한 값과 같다고 볼 수 있다. 
그래서 다음과 같이 코드를 짰다.</p>
<pre><code class="language-jsp"> right += arr[i][(len-i-1)];</code></pre>
<p>여기서 절대값을 구해야하는데, 위의 값으로는 -값이 붙는다.
그래서 <code>Math.abs()</code>를 사용해서 절대값을 구했다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node Affinity와 Pod Affinity]]></title>
            <link>https://velog.io/@dev_0livia/Node-Affinity%EC%99%80-Pod-Affinity</link>
            <guid>https://velog.io/@dev_0livia/Node-Affinity%EC%99%80-Pod-Affinity</guid>
            <pubDate>Tue, 08 Jul 2025 01:58:23 GMT</pubDate>
            <description><![CDATA[<h2 id="🧱-배경">🧱 배경</h2>
<p>Kubernetes 환경에서 vector는 DaemonSet으로 배포되어 모든 노드에 하나씩 상주하며, 로그 및 메트릭 수집을 수행한다. 반면, kube-state-metrics는 클러스터의 상태 정보를 제공하는 단일 Pod로 배포된다.
현재 구성에서는 각 노드의 vector 인스턴스가 동일한 kube-state-metrics 데이터를 모두 수집하고 있어, 중복된 메트릭 전송 및 저장 문제가 발생하고 있다.
이를 해결하기 위해 <code>Node Affinity</code>와 <code>Pod Affinitiy</code>가 언급되어 해당 기능에 대해 공부하고자 한다.</p>
<h2 id="⚠️-문제-사항">⚠️ 문제 사항</h2>
<p>vector DaemonSet → 모든 노드에 존재
kube-state-metrics → 1개의 노드에만 존재</p>
<p>모든 vector가 동일한 kube-state-metrics 엔드포인트를 scrape
→ <strong>데이터 중복 수집, 스토리지 낭비, 리소스 과소비 발생</strong></p>
<hr>
<h2 id="🔍-pod-affinity란">🔍 Pod Affinity란?</h2>
<p>Pod Affinity는 Kubernetes 스케줄링 기능 중 하나로,
특정 조건을 만족하는 다른 Pod가 존재하는 노드에만 새로운 Pod을 배포하도록 지정할 수 있다.</p>
<h3 id="✳️-주요-용도">✳️ 주요 용도</h3>
<p>같은 노드에 앱과 사이드카 또는 로깅 에이전트 등을 공존시키고자 할 때,
데이터 로컬리티가 중요한 경우 (예: 캐시 활용, 동일 디스크 접근)</p>
<pre><code class="language-yaml">podAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchExpressions:
          - key: app
            operator: In
            values:
              - kube-state-metrics
      topologyKey: &quot;kubernetes.io/hostname&quot;</code></pre>
<p>→ 위 설정은 kube-state-metrics Pod이 있는 노드에만 새로운 Pod을 배치하게 함</p>
<h2 id="⚙️-node-affinity란">⚙️ Node Affinity란?</h2>
<p>Node Affinity는 노드에 정의된 label을 기준으로, 특정 Pod이 배포될 노드를 제어하는 스케줄링 기능.
DaemonSet과 같이 모든 노드에 배포되는 리소스에 대해서도 적용 가능하며, 특정 노드에만 제한적으로 배포되도록 구성할 수 있다.</p>
<pre><code class="language-yaml">nodeAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    nodeSelectorTerms:
      - matchExpressions:
          - key: ksm-node
            operator: In
            values:
              - &quot;true&quot;</code></pre>
<p>→ ksm-node=true label이 설정된 노드에만 해당 Pod이 배포됨</p>
<hr>
<p>하지만 해당 기능은 부적합할 수 있다고 판단.</p>
<h2 id="✅-pod-affinity-방식의-한계">✅ Pod Affinity 방식의 한계</h2>
<h3 id="1-daemonset과-pod-affinity는-근본적으로-충돌">1. DaemonSet과 Pod Affinity는 근본적으로 충돌</h3>
<p>Pod Affinity는 <strong>“다른 Pod가 있는 노드에 배치된다”</strong></p>
<p>만약, <code>kube-state-metrics</code>가 있는 노드에만 vector를 배치하려고 할 경우:</p>
<pre><code class="language-yaml">podAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchLabels:
          app: kube-state-metrics
      topologyKey: &quot;kubernetes.io/hostname&quot;</code></pre>
<p>그러나 DaemonSet은 모든 노드에 무조건 배포된다.</p>
<p>-&gt; kube-state-metrics가 없는 노드에도 배포되려 하기 때문에, affinity 조건과 충돌하게 된다. 이렇게되면 나머지 노드에는 affinity 조건이 맞지 않아 배포되지 않음 → vector의 다른 기능 (예: 로그 수집)도 사라짐</p>
<h4 id="🔎-즉-pod-affinity는-deployment에는-유용하지만-daemonset에는-적절하지-않다">🔎 즉, Pod Affinity는 Deployment에는 유용하지만, DaemonSet에는 적절하지 않다.</h4>
<h3 id="2-pod-affinity는-스케줄링-시점-기준이며-동적-환경에-취약">2. Pod Affinity는 스케줄링 시점 기준이며, 동적 환경에 취약</h3>
<p>Pod Affinity는 <strong>&quot;이 노드에 kube-state-metrics가 있어야만 배포됨&quot;</strong>이라는 조건을 요구한다.
하지만 현실에서는 kube-state-metrics Pod이 재시작하거나 다른 노드로 옮겨질 수 있다.
이럴 경우 vector Pod이 사라지거나 생성되지 않을 수 있다. → 불안정한 배포</p>
<h3 id="3-⚠️-daemonset--pod-affinity의-제약사항">3. ⚠️ DaemonSet + Pod Affinity의 제약사항</h3>
<h4 id="daemonset의-특성">DaemonSet의 특성:</h4>
<ul>
<li>모든 노드에 1개씩: DaemonSet은 기본적으로 모든 노드에 배포</li>
<li>Affinity 무시: Pod Affinity는 DaemonSet에서 제한적으로 동작</li>
<li>NodeSelector 우선: DaemonSet은 nodeSelector, tolerations만 주로 사용</li>
</ul>
<h4 id="kube-state-metrics의-특성">kube-state-metrics의 특성:</h4>
<ul>
<li>단일 파드: 일반적으로 Deployment로 1개 파드만 실행</li>
<li>유동적 배치: Kubernetes가 적절한 노드에 스케줄링</li>
<li>재시작 시 이동: 파드 재시작/재스케줄링 시 다른 노드로 이동 가능</li>
</ul>
<h3 id="🤔-pod-affinity-방식의-문제점">🤔 Pod Affinity 방식의 문제점</h3>
<h3 id="1-kube-state-metrics-이동-시-문제">1. kube-state-metrics 이동 시 문제:</h3>
<pre><code>1. kube-state-metrics가 worker001에 있음
2. worker001의 Vector만 수집 중
3. kube-state-metrics가 worker002로 이동
4. worker001 Vector는 계속 시도하지만 실패
5. worker002 Vector는 아직 수집 안함
→ 데이터 수집 중단!</code></pre><h3 id="2-동적-감지의-복잡성">2. 동적 감지의 복잡성:</h3>
<ul>
<li>실시간 감지 어려움: initContainer는 파드 시작 시에만 실행</li>
<li>지속적 모니터링 필요: kube-state-metrics 이동을 실시간 감지 어려움</li>
<li>복잡한 로직: Kubernetes API 지속적 호출 필요</li>
</ul>
<h3 id="3-성능-오버헤드">3. 성능 오버헤드:</h3>
<ul>
<li>API 호출 부담: 모든 Vector가 지속적으로 kube-state-metrics 위치 확인
⚡ 지연 시간: 동적 감지로 인한 수집 지연</li>
</ul>
<hr>
<h1 id="pod-affinity-vs-라벨-기반-비교">Pod Affinity vs 라벨 기반 비교</h1>
<table>
<thead>
<tr>
<th>방식</th>
<th>장점</th>
<th>단점</th>
<th>추천도</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Pod Affinity</strong></td>
<td>🔄 다른 Pod 위치에 따라 자동 추적 가능</td>
<td>❌ 스케줄링 복잡성, 지연, DaemonSet과 비호환</td>
<td>⭐⭐</td>
</tr>
<tr>
<td><strong>라벨 기반</strong></td>
<td>✅ 단순하고 안정적, 제어 용이</td>
<td>⚠️ 라벨 수동 관리 필요</td>
<td>⭐⭐⭐⭐⭐</td>
</tr>
<tr>
<td><strong>고정 배치</strong></td>
<td>✅ 예측 가능한 수집 노드 제어</td>
<td>⚠️ kube-state-metrics HA 구성 어려움</td>
<td>⭐⭐⭐⭐</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[MongoDB 백업]]></title>
            <link>https://velog.io/@dev_0livia/MongoDB-%EB%B0%B1%EC%97%85</link>
            <guid>https://velog.io/@dev_0livia/MongoDB-%EB%B0%B1%EC%97%85</guid>
            <pubDate>Fri, 20 Jun 2025 04:33:23 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/c5f33bb5-679b-44c7-bbbd-65367a39cad3/image.png" alt=""></p>
<p>meme이랑 비슷한 상황,,
클라우드에 올린 클러스터들이 많아서 비용문제로 인해 클러스터 삭제가 필요했다.
이에따라 DB 데이터를 백업해야하는 상황.</p>
<p>현재 MongoDB를 사용하고 있으므로, MongoDB 데이터를 안전하게 백업하는 방법에 대해서 정리하겠다.</p>
<hr>
<h2 id="1-파드-상태-확인">1. 파드 상태 확인</h2>
<blockquote>
<p>실행 중인 MongoDB 파드를 확인해야한다.</p>
</blockquote>
<pre><code class="language-bash">kubectl get pods -A</code></pre>
<h2 id="2-파드에-접속해-데이터-확인">2. 파드에 접속해 데이터 확인</h2>
<blockquote>
<p>MongoDB 파드에 접속해서 실제로 데이터가 있는지 확인한다.</p>
</blockquote>
<pre><code class="language-bash"># 파드 접속
kubectl exec -it [mongoDB파드 이름] -n [네임스페이스] -- bash

# MongoDB 접속 및 데이터 확인
mongo
&gt; show dbs
&gt; use [db이름]
&gt; show collections
&gt; db.resources.count()
&gt; exit
&gt; exit</code></pre>
<h2 id="3-mongodump로-데이터-백업">3. mongodump로 데이터 백업</h2>
<blockquote>
<p>MongoDB의 mongodump 명령어를 사용해 모든 데이터를 BSON 형식으로 백업</p>
</blockquote>
<pre><code class="language-bash">kubectl exec -it [파드이름] -n [네임스페이스] -- mongodump --out /tmp/backup</code></pre>
<h3 id="bson-형식으로-백업하는-이유">BSON 형식으로 백업하는 이유</h3>
<ul>
<li><p>데이터 타입 무손실</p>
<ul>
<li>ObjectId, Date, NumberLong, Binary, Decimal128 등 모든 MongoDB 타입 보존</li>
<li>인덱스 정보도 함께 백업됨</li>
</ul>
</li>
<li><p>빠른 복원 속도</p>
<ul>
<li>바이너리 형식이라 복원이 매우 빠름</li>
<li>대용량 데이터에서 특히 효과적</li>
</ul>
</li>
</ul>
<h3 id="json-형식의-한계">JSON 형식의 한계</h3>
<ul>
<li>ObjectId가 문자열로 변환되어 원본과 달라짐</li>
<li>Date 타입이 ISO 문자열로 변환</li>
<li>복원 시 타입 변환 문제 발생 가능</li>
<li>인덱스 정보 손실</li>
</ul>
<h2 id="4-백업-파일-확인">4. 백업 파일 확인</h2>
<pre><code class="language-bash"># 백업 디렉토리 확인
kubectl exec -it [파드이름] -n [네임스페이스] -- ls -la /tmp/backup/

# 백업 크기 확인
kubectl exec -it [파드이름] -n [네임스페이스] -- du -sh /tmp/backup/</code></pre>
<h2 id="5-백업-파일-압축">5. 백업 파일 압축</h2>
<blockquote>
<p>네트워크 전송을 위해 백업 파일 압축 필요</p>
</blockquote>
<pre><code class="language-bash"># 파드 내에서 압축
kubectl exec -it [파드이름] -n [네임스페이스] -- tar -czf /tmp/mongodb-backup.tar.gz -C /tmp backup

# 압축 파일 크기 확인
kubectl exec -it [파드이름] -n [네임스페이스] -- ls -lh /tmp/mongodb-backup.tar.gz</code></pre>
<h2 id="6-로컬로-백업-파일-복사">6. 로컬로 백업 파일 복사</h2>
<blockquote>
<p>kubectl cp 명령어를 사용해 압축된 백업 파일을 로컬로 복사합니다.</p>
</blockquote>
<pre><code class="language-bash"># 로컬로 복사 (현재 디렉토리에 날짜별로 저장)
kubectl cp [네임스페이스]/[파드이름]:/tmp/mongodb-backup.tar.gz ./mongodb-backup-$(date +%Y%m%d-%H%M%S).tar.gz</code></pre>
<h2 id="7-로컬에서-압축-해제-및-내용-확인">7. 로컬에서 압축 해제 및 내용 확인</h2>
<pre><code class="language-bash"># 복사된 파일 확인
ls -lh mongodb-backup-*.tar.gz

# 압축 해제
tar -xzf mongodb-backup-*.tar.gz

# 백업 내용 확인
ls -la backup/</code></pre>
<h3 id="백업된-파일-구조">백업된 파일 구조</h3>
<pre><code>backup/
├── admin/
│   ├── system.version.bson
│   └── system.version.metadata.json
└── [db이름]/
    ├── accounts.bson
    ├── accounts.metadata.json
    ├── resources.bson
    ├── resources.metadata.json
    ├── events.bson
    ├── events.metadata.json
    └── ... (기타 컬렉션들)</code></pre><p>각 MongoDB 컬렉션마다 두 개의 파일이 생성된다.</p>
<ul>
<li><code>.bson</code> 파일: 실제 데이터 (바이너리 형식)</li>
<li><code>.metadata.json</code> 파일: 인덱스 및 컬렉션 설정 정보</li>
</ul>
<h2 id="8-데이터-복원-방법-참고">8. 데이터 복원 방법 (참고)</h2>
<blockquote>
<p>나중에 새로운 MongoDB 인스턴스에서 데이터를 복원할 때를 대비해 미리 작성</p>
</blockquote>
<pre><code class="language-bash"># 백업 파일을 새 파드로 복사
kubectl cp ./backup [네임스페이스]/[새로운 파드 이름]:/tmp/restore-backup

# 데이터 복원
kubectl exec -it [새로운 파드 이름] -n [네임스페이스] -- mongorestore /tmp/restore-backup</code></pre>
<h3 id="정리">정리</h3>
<ul>
<li>mongodump: MongoDB 데이터를 BSON 형식으로 완전 백업</li>
<li>압축 전송: 네트워크 효율성을 위한 tar.gz 압축</li>
<li>kubectl cp: 쿠버네티스 파드와 로컬 간 파일 전송</li>
<li>완전한 복원: BSON + metadata로 인덱스까지 완벽 복원 가능</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Linux / eBPF / Cilium ...]]></title>
            <link>https://velog.io/@dev_0livia/Linux-vs-eBPF</link>
            <guid>https://velog.io/@dev_0livia/Linux-vs-eBPF</guid>
            <pubDate>Thu, 12 Jun 2025 06:11:40 GMT</pubDate>
            <description><![CDATA[<h1 id="linux-시스템-아키텍처">Linux 시스템 아키텍처</h1>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/ee3448c4-7911-48d8-8bb0-312a4e37e342/image.png" alt=""></p>
<p>Linux의 커널은 컴퓨터를 부팅하면 하드웨어 메모리에 올라가서 끌때까지 메모리 위에서 동작한다.
따라서 응용 프로그램을 실행하면, 커널이 메모리에 충분한 공간이 있는지 없는지 모니터링하는 역할을 하게 된다. </p>
<h2 id="기존-linux-모니터링의-한계">기존 Linux 모니터링의 한계</h2>
<h3 id="제한적인-관찰-능력">제한적인 관찰 능력</h3>
<ul>
<li>정해진 형태의 정보만 제공: 커널이 미리 준비해둔 데이터만 확인 가능</li>
<li>낮은 실시간성: 주로 1초 단위로 업데이트되는 평균값만 제공</li>
<li>표면적인 정보: 시스템 내부에서 일어나는 세부적인 동작을 파악하기 어려움</li>
</ul>
<h3 id="커널-확장의-위험성">커널 확장의 위험성</h3>
<p>만약 커널 내부에서 일어나는 일들을 사용자가 원하는 방식으로 관찰하거나, 수정하기 위해서는 <strong>커널 모듈(kernel module)</strong> 을 작성해야했는데 이 방식은 위험하고 복잡한 작업이었다. 
이 작업은 마치 집 구조를 바꾸려고 할때, 벽을 허무는 것과 같은 작업이다. 만일하나 실수를 하게되면 집 전체가 무너질 수 있는 위험도가 컸다.</p>
<h2 id="ebpf-안전한-커널-확장-기술">eBPF: 안전한 커널 확장 기술</h2>
<p>반면 eBPF는 Linux 커널의 한계를 확장시켜 사용할 수 있다.
기존의 커널을 사용하려면, 커널 모듈(<code>kernel module</code>)을 작성해야했다. 
하지만, eBPF는 샌드박스 환경에서 안전하게 커널 레벨 코드를 실행할 수 있게 해준다.
또한, 커널이 기본적으로 제공하는 모니터링은 제한적이었으나, eBPF로는 네트워크 패킷 하나, 시스템 콜 호출, 파일 접근 등 실시간으로 세밀하게 관찰하고 분석할 수 있다.</p>
<h2 id="ebpf의-핵심-장점">eBPF의 핵심 장점</h2>
<h3 id="1-안전한-실행-환경">1. 안전한 실행 환경</h3>
<ul>
<li>샌드박스 보호: 격리된 환경에서 코드 실행</li>
<li>사전 검증: 코드 로딩 전 안전성 검사</li>
<li>메모리 보호: 허용된 메모리 영역만 접근 가능</li>
<li>무한루프 방지: 실행 시간 제한으로 시스템 보호</li>
</ul>
<h3 id="2-동적-기능-추가">2. 동적 기능 추가</h3>
<p>시스템 재부팅 없이 커널에 새로운 기능을 즉시 추가할 수 있다.</p>
<h4 id="예시-웹-서버-보안">예시: 웹 서버 보안</h4>
<blockquote>
<p>상황: 웹 서버에 특정 IP의 요청만 차단하고 싶은 경우</p>
</blockquote>
<ul>
<li>기존 방식: 커널의 동작 방식을 바꾸거나 확장이 필요하기 때문에 수정 후 재부팅이 필요했고, 재부팅을 하지 않는다면 시스템 크래시가 발생할 위험이 있었다.</li>
<li>eBPF 방식: 안전하면서 즉시 적용할 수 있게 되었고, 재부팅이 불필요해서 안전하게 새로운 기능을 추가할 수 있다</li>
</ul>
<p>이는 수천 대의 서버를 관리하는 환경에서 특히 중요하다. 서비스 중단 없이 보안 정책이나 모니터링 규칙을 실시간으로 적용할 수 있기 때문이다.</p>
<h2 id="모니터링-방식의-혁신적-변화">모니터링 방식의 혁신적 변화</h2>
<h3 id="linux">Linux</h3>
<p>Linux 에서는 &quot;이미 만들어진 음식을 받아먹는 것&quot;으로 볼 수 있었다.
1초 단위로 업데이트되는 평균값들을 커널이 미리 준비해둔 정보만 볼 수 있었고, 정해진 형식으로만 데이터를 제공받을 수 있었다.</p>
<pre><code class="language-bash">netstat -an | grep :80
# 결과: 현재 80포트 연결 상태의 스냅샷만 제공</code></pre>
<h3 id="ebpf">eBPF</h3>
<p>eBPF에서는 &quot;주방에 들어가 요리하는 과정을 직접 보는 것&quot;으로 볼 수 있다.
커널 내부에서 일어나는 모든 일들을 &quot;나노초 단위로 실시간으로 정밀하게 측정하여 관찰할 수 있다.</p>
<pre><code class="language-bash">bpftrace -e &#39;kprobe:tcp_connect { 
    printf(&quot;%s PID:%d -&gt; %s:%d (지연시간: %dus)\n&quot;, 
           comm, pid, ntop(arg1), arg2, elapsed/1000) 
}&#39;</code></pre>
<p>연결 시도 순간마다 실시간 추적이 가능하고, 어떤 프로그램이 어디에 얼마나 빨리 연결했는지 상세 정보를 확인 할 수 있다. 또한, 데이터 커스터마이징이 가능한 장점이 있다.</p>
<h2 id="클라우드-환경에서-ebpf의-중요성">클라우드 환경에서 eBPF의 중요성</h2>
<p>클라우드 네이티브 환경, 특히 Kubernetes와 같은 환경에서는 수천 개의 컨테이너가 복잡한 네트워크 환경에서 작동한다. 기존의 모니터링 도구로는 상세한 문제 추적이 어려워 eBPF 기반 도구가 점점 중요해지고 있다.</p>
<ul>
<li><strong>실시간 성능 모니터링</strong>: 컨테이너의 성능을 초정밀 실시간으로 측정 및 관리</li>
<li><strong>네트워크 트래픽 분석</strong>: 상세한 패킷 수준의 실시간 트래픽 추적</li>
<li><strong>보안 위협 탐지</strong>: 이상 행동과 잠재적인 보안 위협을 신속히 식별</li>
<li><strong>서비스 메쉬 관찰성 향상</strong>: 컨테이너 간 통신 흐름과 장애를 실시간으로 명확히 추적하여 빠른 문제 해결 지원</li>
</ul>
<hr>
<h1 id="서비스-메시와-ebpf-기반-도구들">서비스 메시와 eBPF 기반 도구들</h1>
<h2 id="서비스-메시service-mesh">서비스 메시(Service Mesh)</h2>
<p>여러 서비스들이 서로 데이터를 주고 받을 때, 각 지점마다 전담 프록시를 두고, 중앙에서 모든 과정을 관리하는 시스템이라고 볼 수 있다.</p>
<h3 id="왜-서비스-메시가-필요한가">왜 서비스 메시가 필요한가?</h3>
<h4 id="전통적인-방식-모놀리식">전통적인 방식 (모놀리식):</h4>
<pre><code>하나의 큰 애플리케이션
[사용자] → [하나의 큰 애플리케이션] → [데이터베이스]

- 쇼핑몰 전체가 하나의 프로그램
- 주문, 결제, 배송이 모두 한 덩어리 </code></pre><h4 id="마이크로서비스-방식">마이크로서비스 방식</h4>
<pre><code>[사용자] → [API Gateway] 
                ↓
[주문서비스] ↔ [결제서비스] ↔ [재고서비스] ↔ [배송서비스]
    ↓           ↓           ↓           ↓
[주문DB]    [결제DB]    [재고DB]    [배송DB]
</code></pre><p>마이크로서비스 방식의 경우, 서비스들 간의 통신이 복잡해졌다.
어떤 서비스가 어떤 서비스와 얼마나 많이 통신하는지 파악이 어려워, <code>네트워크 오류</code>, <code>보안</code>, <code>트래픽 제어</code> 등을 각 서비스마다 따로 관리하는 문제가 있었다.</p>
<h4 id="서비스메시의-역할">서비스메시의 역할</h4>
<p>서비스메시는 이런 복잡한 통신을 <strong>투명하게</strong> 관리해주는 도구다.</p>
<p>서비스 메시가 없었을 때는 개발자가 직접 통신해야했다.</p>
<pre><code>[주문서비스] --직접통신--&gt; [결제서비스]
(개발자가 직접 재시도, 암호화, 모니터링 코드 작성)</code></pre><p>그러나 서비스 메시가 있을 경우, 중앙에서 모든 통신들을 관리하기 </p>
<pre><code>서비스 메시 있을 때:
[주문서비스] → [프록시] → [프록시] → [결제서비스]
                ↑                ↑
            자동으로 재시도,     자동으로 암호화,
            로드밸런싱,          모니터링
            트래픽 분석</code></pre><hr>
<h2 id="메트릭metric">메트릭(Metric)</h2>
<blockquote>
<p>메트릭 = 측정 가능한 숫자 데이터</p>
</blockquote>
<pre><code>서버 상태:
- CPU 사용률: 75%
- 메모리 사용량: 8GB / 16GB
- 디스크 사용률: 60%
- 네트워크 트래픽: 100Mbps</code></pre><h3 id="메트릭-수집이-왜-중요한가">메트릭 수집이 왜 중요한가?</h3>
<p>메트릭 없이 서버 관리한다면 다음과 같은 상황 발생</p>
<pre><code>상황: 쇼핑몰 웹사이트가 갑자기 느려짐

개발자: &quot;어? 왜 느리지?&quot;
방법: 서버에 직접 접속해서 하나씩 확인
- htop 실행해서 CPU 확인
- free 명령어로 메모리 확인  
- df 명령어로 디스크 확인
- 로그 파일 하나씩 뒤져보기

문제: 이미 문제가 생긴 후에야 알 수 있음</code></pre><p>메트릭을 수집하면 문제를 즉시 발견하고 원인을 파악하기 용이하다.</p>
<pre><code>상황: 쇼핑몰 웹사이트가 갑자기 느려짐

모니터링 대시보드에서 즉시 확인:
- CPU: 95% (빨간불) ← 문제 발견!
- 메모리: 90% (주황불)
- 응답시간: 5초 (빨간불)
- 에러율: 10% (빨간불)

결과: 문제를 즉시 발견하고 원인도 바로 파악</code></pre><h2 id="ebpf로-메트릭-수집-방법">eBPF로 메트릭 수집 방법</h2>
<h3 id="기존-방식-vs-ebpf-방식">기존 방식 vs eBPF 방식</h3>
<h4 id="기존-방식">기존 방식</h4>
<blockquote>
<p>외부에서 관찰</p>
</blockquote>
<pre><code>모니터링 프로그램이 1초마다:
cat /proc/cpuinfo     # CPU 정보 읽기
cat /proc/meminfo     # 메모리 정보 읽기
netstat -an          # 네트워크 연결 정보 읽기</code></pre><p>문제점:</p>
<ul>
<li>1초 단위로만 확인 가능</li>
<li>평균값만 알 수 있음</li>
<li>세세한 내부 동작은 모름</li>
</ul>
<h4 id="ebpf-방식">eBPF 방식</h4>
<blockquote>
<p>내부에서 실시간으로 관찰</p>
</blockquote>
<pre><code>커널 내부에 eBPF 프로그램을 심어두고:
- 함수가 호출될 때마다 측정
- 네트워크 패킷이 올 때마다 기록
- 파일이 읽힐 때마다 시간 측정</code></pre><p>결과:</p>
<ul>
<li>나노초 단위 정밀 측정</li>
<li>실시간 데이터</li>
<li>세세한 내부 동작까지 파악</li>
</ul>
<hr>
<h2 id="ebpf-메트릭-수집-장점">eBPF 메트릭 수집 장점</h2>
<h3 id="1-정확성">1. 정확성</h3>
<pre><code>기존: &quot;평균적으로 응답시간이 2초&quot;
eBPF: &quot;95%는 100ms, 4%는 500ms, 1%는 10초&quot;</code></pre><p>→ 1%의 매우 느린 요청이 문제라는 걸 정확히 파악할 수 있다. </p>
<h3 id="2-실시간성">2. 실시간성</h3>
<pre><code>기존: 1분 후에 &quot;아, 1분 전에 문제가 있었음&quot;
eBPF: &quot;지금 이 순간 문제 발생!!!&quot;</code></pre><h3 id="3-세밀함">3. 세밀함</h3>
<pre><code>기존: &quot;네트워크가 느림&quot;
eBPF: &quot;TCP 연결 설정은 빠른데, 데이터 전송에서 지연이 생김. 
      특히 192.168.1.100 서버와 통신할 때만 느림&quot;   </code></pre><h2 id="정리">정리</h2>
<p>메트릭 수집 = 컴퓨터의 상태를 숫자로 측정해서 기록하는 것
eBPF로 메트릭 수집 = 커널 내부에서 실시간으로 정확하게 측정하여 더 빠르고 정확한 문제 발견할 수 있고, 세밀한 성능 분석과 자동화된 대응을 가능하게 한다.</p>
<hr>
<h1 id="cilium-hubble-prometheus">Cilium, Hubble, Prometheus</h1>
<blockquote>
<p>eBPF가 내장됨. (즉, Cilium과 Hubble을 설치하면 자동으로 eBPF가 사용됨)</p>
</blockquote>
<img src="https://velog.velcdn.com/images/dev_0livia/post/71146ccb-0211-404c-a8d1-d81ca1a26b91/image.png" width="300px" height="200px" />


<p>직접 eBPF로 개발을 한다는 것은 &#39;자동차 엔진을 직접 만드는 것&#39;과 같다.
C언어로 직접 eBPF 프로그램을 작성해야하고, 커널에 직접 로드해야한다.
데이터 수집 로직도 구현해야하기 때문에 매우 복잡하고 어렵다.</p>
<p>그러나, Cilium과 Hubble을 사용하면 이미 eBPF가 내장되어있어 &#39;완성된 자동차&#39;를 구매하는 것 과같다.
Cilium과 Hubble은 설치만하면 바로 사용 가능하기 때문에 복잡한 개발 과정이 불필요하다.</p>
<h2 id="cilium">Cilium</h2>
<blockquote>
<p>네트워킹 + 보안</p>
</blockquote>
<img src="https://velog.velcdn.com/images/dev_0livia/post/abec44ff-3d6e-479f-9dd2-348789599757/image.png" width="350px"/>

<p>Cilium은 eBPF기반 쿠버네티스 네트워킹 솔루션이다.</p>
<h3 id="cilium-설치-시">Cilium 설치 시</h3>
<pre><code class="language-bash"># Cilium 설치
helm install cilium cilium/cilium --namespace kube-system</code></pre>
<h3 id="cilium-설치시-커널에-자동으로-로드되는-ebpf-프로그램들">Cilium 설치시 커널에 자동으로 로드되는 eBPF 프로그램들:</h3>
<blockquote>
<p>Cilium이 설치되면 자동으로 eBPF 프로그램들이 커널에 로드되고, 아래와 같은 기능들을 사용할 수 있게 된다. </p>
</blockquote>
<ul>
<li>네트워크 패킷 필터링 </li>
<li>TCP/UDP 연결 추적 </li>
<li>HTTP 요청/응답 분석 </li>
<li>보안 정책 적용 </li>
<li>메트릭 수집                      </li>
<li>로드밸런싱</li>
</ul>
<pre><code class="language-bash"># 설치만
kubectl apply -f cilium.yaml</code></pre>
<pre><code class="language-yaml"># cilium-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cilium-config
data:
  # eBPF로 네트워크 정책 적용
  enable-policy: &quot;true&quot;
  # eBPF로 로드밸런싱
  enable-l7-proxy: &quot;true&quot;
  # 실시간 모니터링 활성화
  enable-hubble: &quot;true&quot;</code></pre>
<p>위의 Yaml을 등록하면, Cilium은 알아서 수십 개의 eBPF 프로그램을 커널에 로드하게 된다.
그렇게되면 실시간으로 네트워크 모니터링을 시작하게 되고, 메트릭 수집을 자동으로 시작한다. </p>
<h3 id="cilium이-하는-일">Cilium이 하는 일:</h3>
<ul>
<li>Pod 간 네트워크 통신 관리</li>
<li>보안 정책 적용 (어떤 Pod끼리 통신 가능한지)</li>
<li>로드밸런싱</li>
<li>네트워크 메트릭 수집</li>
</ul>
<h2 id="hubble">Hubble</h2>
<blockquote>
<p>관찰! 위의 Cilium을 관찰하는 도구임.</p>
</blockquote>
<img src="https://velog.velcdn.com/images/dev_0livia/post/e2efa6b3-6211-4089-ac7d-f879c7d94c6d/image.png" width="450px"/>


<pre><code class="language-bash"># Hubble로 실시간 네트워크 트래픽 관찰
hubble observe

# 출력 예시:
# Oct 20 12:34:56.789: frontend-pod -&gt; backend-pod:8080 (HTTP GET /api/users)
# Oct 20 12:34:56.792: backend-pod -&gt; db-pod:3306 (TCP)
# Oct 20 12:34:56.845: db-pod -&gt; backend-pod:3306 (TCP)
# Oct 20 12:34:56.850: backend-pod -&gt; frontend-pod:8080 (HTTP 200 OK)</code></pre>
<p>-&gt; 로그 보니까 Skuber에서 log에 보이는 로그들과 비슷한 것 같음. (확인 필요)</p>
<h3 id="네트워크-정책-적용">네트워크 정책 적용</h3>
<pre><code class="language-yaml"># network-policy.yaml
apiVersion: &quot;cilium.io/v2&quot;
kind: CiliumNetworkPolicy
metadata:
  name: &quot;allow-frontend-to-backend&quot;
spec:
  endpointSelector:
    matchLabels:
      app: frontend
  egress:
  - toEndpoints:
    - matchLabels:
        app: backend
    toPorts:
    - ports:
      - port: &quot;8080&quot;
        protocol: TCP

# 적용하면 eBPF가 자동으로 패킷 필터링 시작
kubectl apply -f network-policy.yaml</code></pre>
<h3 id="hubble이-수집하는-메트릭">Hubble이 수집하는 메트릭</h3>
<pre><code class="language-bash"># 서비스별 트래픽 통계
hubble metrics list
# - dns_queries_total
# - drop_count_total  
# - tcp_flags_total
# - http_requests_total</code></pre>
<h3 id="백그라운드에서-일어나는-일">백그라운드에서 일어나는 일</h3>
<ol>
<li>kubectl apply 명령 실행</li>
<li>Cilium이 정책을 읽음</li>
<li>eBPF 프로그램을 자동으로 생성/업데이트</li>
<li>커널에서 즉시 패킷 필터링 시작</li>
<li>정책 위반 패킷은 자동으로 차단</li>
</ol>
<h2 id="prometheus">Prometheus</h2>
<blockquote>
<p>메트릭 저장소로 메트릭을 수집하고 저장하는 데이터베이스.</p>
</blockquote>
<pre><code class="language-yaml"># prometheus-config.yaml
global:
  scrape_interval: 15s

scrape_configs:
  # Hubble에서 메트릭 수집
  - job_name: &#39;hubble&#39;
    static_configs:
      - targets: [&#39;hubble:9090&#39;]

  # 내 Go 애플리케이션에서 메트릭 수집  
  - job_name: &#39;my-go-app&#39;
    static_configs:
      - targets: [&#39;my-app:8080&#39;]
</code></pre>
<hr>
<h2 id="go-예시">Go 예시</h2>
<pre><code class="language-go">package main

import (
    &quot;encoding/json&quot;
    &quot;fmt&quot;
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;time&quot;

    &quot;github.com/gorilla/mux&quot;
    &quot;github.com/prometheus/client_golang/prometheus&quot;
    &quot;github.com/prometheus/client_golang/prometheus/promhttp&quot;
)

// 상품 구조체
type Product struct {
    ID    int    `json:&quot;id&quot;`
    Name  string `json:&quot;name&quot;`
    Price int    `json:&quot;price&quot;`
}

// Prometheus 메트릭 정의
var (
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: &quot;http_requests_total&quot;,
            Help: &quot;총 HTTP 요청 수&quot;,
        },
        []string{&quot;method&quot;, &quot;endpoint&quot;, &quot;status_code&quot;},
    )

    httpDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: &quot;http_duration_seconds&quot;,
            Help: &quot;HTTP 요청 처리 시간&quot;,
            Buckets: prometheus.DefBuckets,
        },
        []string{&quot;method&quot;, &quot;endpoint&quot;},
    )

    databaseConnections = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Name: &quot;database_connections_active&quot;,
            Help: &quot;활성 데이터베이스 연결 수&quot;,
        },
    )
)</code></pre>
<p>수정중..,</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next] build 에러]]></title>
            <link>https://velog.io/@dev_0livia/next-build-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@dev_0livia/next-build-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Wed, 21 May 2025 07:52:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/f652ee7e-8ee0-47b4-a2aa-d82ce9a3de9c/image.png" alt=""></p>
<p><code>next.js</code> 프로젝트를 서버에 배포하고 빌드하는 과정에서 두 가지 주요 오류가 발생했다🤦‍♀️.</p>
<ul>
<li><span style="background-color: #d0f4de">빌드 시 권한 문제</span></li>
<li><span style="background-color: #d0f4de">서버 실행 시 포트 충돌 문제</span></li>
</ul>
<p>이 글에서는 위의 문제의 원인과 해결 과정을 공유하고자 한다.</p>
<h1 id="span-stylecolor-red오류-1-빌드-시-권한-문제-eacces-permission-denied"><span style="color: red">오류 1: 빌드 시 권한 문제 (EACCES: permission denied)</h1>
<p><code>next build</code> 명령을 실행했을 때 다음과 같은 오류가 발생했다.</span></p>
<pre><code>&gt; Build error occurred
[Error: EACCES: permission denied, unlink &#39;/.../.next/server/middleware-manifest.json&#39;] {
  errno: -13,
  code: &#39;EACCES&#39;,
  syscall: &#39;unlink&#39;,
  path: &#39;/.../.next/server/middleware-manifest.json&#39;
}</code></pre><p>도대체 <strong>middleware-manifest</strong>가 뭔데 여기 접근을 못하는걸까?</p>
<h3 id="middleware-manifestjson"><code>middleware-manifest.json</code></h3>
<p>middleware-manifest.json 파일은 <span style="background-color: #d0f4de">Next.js 빌드 시 생성되는 내부 메타데이터 파일</span> 중 하나다. 
Middleware, Edge Functions 및 관련 라우팅 정보를 Next.js 런타임에서 사용하기 위해 이 매니페스트 파일에 포함된다. 
일반적으로 next build 시 <strong>자동으로 생성</strong>되며, <code>.next/server/</code> 디렉토리 내부에 위치한다.</p>
<p>여기서 에러메세지를 보면 빌드 프로세스 중에 <span style="background-color: #d0f4de">이 파일에 접근할 수 없어서 에러가 발생</span>했다는 뜻이다. </p>
<ul>
<li><strong>EACCES: permission denied</strong> - 파일 시스템 권한 거부</li>
<li><strong>syscall: &#39;unlink&#39;</strong> - 파일을 삭제하려고 시도했으나 실패</li>
<li><strong>/.../.next/server/middleware-manifest.json</strong> - 문제가 발생한 파일 경로</li>
</ul>
<p>즉, <span style="background-color: #d0f4de"><strong><code>Next.js</code>가 빌드 과정에서 middleware-manifest.json 파일을 삭제하거나 수정하려고 했지만, 해당 파일에 대한 충분한 권한이 없어서 build를 실패하는 것</strong></span>이다.</p>
<h2 id="해결-시도">해결 시도</h2>
<h3 id="방법-1-next-디렉토리-삭제-후-다시-빌드">방법 1 <code>.next</code> 디렉토리 삭제 후 다시 빌드</h3>
<p>가장 먼저 시도해 볼 수 있는 방법은 기존 빌드 결과물인 <strong><code>.next</code> 디렉토리를 삭제하고 다시 빌드하는 것</strong>이다.</p>
<pre><code class="language-bash">sudo rm -rf /경로/.next
npm run build</code></pre>
<p>하지만, 이 방법으로 해결되지 않았고, 동일한 오류가 계속 발생했다.</p>
<h3 id="방법-2-권한-변경">방법 2. <code>권한</code> 변경</h3>
<p>오류 메세지처럼 파일/디렉토리 권한 문제일 가능성이 높으므로, <strong><code>.next</code> 디렉토리에 대한 권한을 변경</strong>했다.</p>
<pre><code class="language-bash">sudo chmod -R 777 /경로/.next
npm run build</code></pre>
<p>chmod -R 777은 모든 사용자에게 모든 권한을 부여하므로 <span style="background-color: #d0f4de"><u>보안상 권장되지 않는다</u></span>. 
임시적인 해결책이나 테스트 환경에서만 사용하는 것이 좋으며, 실제 운영 환경에서는 빌드를 실행하는 사용자에게 필요한 최소한의 권한만 부여하거나 파일 소유권을 올바르게 설정하는 것이 바람직하다. 
(예: sudo chown -R $(whoami) /프로젝트경로/.next)</p>
<h2 id="span-stylecolor-red오류-2-포트-충돌-문제-eaddrinuse-address-already-in-usespan"><span style="color: red">오류 2: 포트 충돌 문제 (EADDRINUSE: address already in use)</span></h2>
<p>권한 문제를 해결한 후 npm run build가 성공하고 서버를 시작하려고 할 때 다음과 같은 포트 충돌 오류가 발생했다.</p>
<pre><code>Browserslist: caniuse-lite is outdated. Please run:
0|ccap-admin-stg  |   npx browserslist@latest --update-db
0|ccap-admin-stg  |   Why you should do it regularly: https://github.com/browserslist/browserslist#browsers-data-updating
0|ccap-admin-stg  | Error: listen EADDRINUSE: address already in use :::8090
0|ccap-admin-stg  |     at Server.setupListenHandle [as _listen2] (node:net:1463:16)
0|ccap-admin-stg  |     at listenInCluster (node:net:1511:12)
0|ccap-admin-stg  |     at Server.listen (node:net:1599:7)
0|ccap-admin-stg  |     at /경로/server.stg.js:25:6 {
0|ccap-admin-stg  |   code: &#39;EADDRINUSE&#39;,
0|ccap-admin-stg  |   errno: -98,
0|ccap-admin-stg  |   syscall: &#39;listen&#39;,
0|ccap-admin-stg  |   address: &#39;::&#39;,
0|ccap-admin-stg  |   port: 8090
0|ccap-admin-stg  | }
0|ccap-admin-stg  | error - uncaughtException: Error: listen EADDRINUSE: address already in use :::8090</code></pre><p>EADDRINUSE 오류는 <strong>Error: Address Already in Use</strong>의 약자로, <span style="background-color: #d0f4de">애플리케이션이 사용하려고 하는 네트워크 포트(이 경우 8090)를 이미 다른 프로세스가 사용하고 있음을 의미</span>한다.</p>
<p>해당 프로젝트의 경우, 8090 포트는 현재 Next.js 애플리케이션(FE)이 사용하도록 설정된 포트다. 
뭔가 이전에 <u><span style="background-color: #d0f4de">비정상적으로 종료</span></u>되었거나 <u><span style="background-color: #d0f4de">다른 방식으로 실행된 동일 애플리케이션의 인스턴스가 포트를 계속 점유</span></u>하고 있을 가능성이 높다.</p>
<h2 id="해결-과정">해결 과정</h2>
<h3 id="1-이미-실행-중인-포트가-있는지-확인">1. 이미 실행 중인 포트가 있는지 확인</h3>
<p>먼저, 어떤 프로세스가 8090 포트를 사용하고 있는지, 그리고 PM2로 관리되는 관련 프로세스가 있는지 확인했다.</p>
<pre><code class="language-bash">ss -tulpn | grep 8090
pm2 list</code></pre>
<pre><code>ss -tulpn | grep 8090
tcp   LISTEN 0      511                *:8090             *:*
[서버 계정]$ pm2 list
[PM2] Spawning PM2 daemon with pm2_home=/경로/.pm2
[PM2] PM2 Successfully daemonized
┌────┬───────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name      │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
└────┴───────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘</code></pre><p>ss 명령어 결과, 8090 포트가 LISTEN 상태인 것을 확인했다. 
하지만 pm2 list 결과에는 해당 포트를 사용하는 것으로 예상되는 PM2 프로세스가 없었다. 
이는 <span style="background-color: #d0f4de">이전 애플리케이션이 PM2를 통하지 않고 실행되었거나, PM2 프로세스가 비정상 종료되어 포트만 점유된 상태일 수 있다고 판단</span>했다.</p>
<h3 id="2-포트-종료-및-재실행">2. 포트 종료 및 재실행</h3>
<pre><code class="language-bash">sudo kill -9 $(sudo fuser -n tcp 8090 2&gt;/dev/null)

pm2 start ecosystem.stg.config &amp;&amp; pm2 logs 0</code></pre>
<p>원인 불명의 프로세스가 8090 포트를 점유하고 있으므로, 해당 프로세스를 찾아 강제로 종료했다. fuser 명령어를 사용하면 특정 포트를 사용하는 프로세스의 PID를 찾아 종료할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebSocket connection failed errors]]></title>
            <link>https://velog.io/@dev_0livia/WebSocket-connection-failed-errors</link>
            <guid>https://velog.io/@dev_0livia/WebSocket-connection-failed-errors</guid>
            <pubDate>Wed, 02 Apr 2025 07:33:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/aa5ec470-4d27-452e-b6a0-a7ff99de061c/image.png" alt=""></p>
<p>또 만났다. NGINX
Nginx는 도대체 뭐때문에 날 이렇게 괴롭히는 것인지?</p>
<h2 id="문제-상황">문제 상황</h2>
<p>현재 프로젝트 서버 마이그레이션중으로 to-be 서버에 Next.js 프로젝트를 배포했다.
화면은 정상적으로 나오지만, 브라우저 콘솔에는 Websocket오류가 계속 발생했다. 🤦‍♀️ 그만 좀 떠라 제발..</p>
<p>회사 url이 있기 때문에 사진 첨부는 어렵지만, 대략 아래와 같은 메세지가 반복해서 나왔다.
<span style="background-color: rgb(205, 145, 158); color: red">🚨 WebSocket connection to &#39;wss:// [domain]: 8080/_next/webpack-hmr&#39; failed</span></p>
<p>사실 화면도 잘 뜨고 프로젝트도 잘 돌아가니까, 굳이 해야하나 싶었다.
그치만 콘솔창 들어갈때마다 보이는 에러가 보기 싫어서 결국 구글링 및 삽질해서 해결했다.</p>
<p>해결은 역시나 <code>nginx.conf</code> 😊</p>
<hr>
<h2 id="프로젝트-환경">프로젝트 환경</h2>
<p>우선 해당 프로젝트 스펙을 간단하게 정리해본다.</p>
<ul>
<li><strong>Next.js</strong>: v12</li>
<li><strong>Nginx</strong>: 프록시 서버로 사용</li>
<li><strong>배포 도구</strong>: PM2</li>
<li><strong>특이사항</strong>: <u>Websocket을 실제로 사용하지 않는</u> 프로젝트, Load Balancer를 통한 SSL 사용</li>
</ul>
<hr>
<h2 id="문제-해결-과정">문제 해결 과정</h2>
<h3 id="시도-1--nextjs-설정-수정---해결-❌">시도 1:  Next.js 설정 수정 - 해결 ❌</h3>
<p>WebSocket을 사용하지 않는 프로젝트다.
<u><strong>next.js v12는 서버 커넥션 유지를 위해 WebSocket을 활용하기 시작</strong></u>했다.
이에 따라 <strong>Next.js 버전 11에서 12로 업그레이드한 사용자 중에 해당 에러가 빈번히 발생</strong>하는 문제가 많음을 발견했다.
여기서 나는 <span style="background-color: #e4c75b"><strong>Next.js에서 웹소켓 연결을 시도하는 것이 문제라고 판단해 <code>next.config.js</code> 를 수정</strong></span>하고자 했다. 
몇몇 사용자는 v11로 내리니 에러가 사라졌다고 하나, *<em>프로젝트는 처음부터 v12버전을 사용하고 있었기때문에 11로 버전을 내릴 생각은 없었다. *</em></p>
<p>그래서 <code>next.js next.config.js</code>에서 <strong>HMR(Hot Module Replacement)</strong> 관련 설정을 <strong>비활성</strong>하려고 시도했다.</p>
<p>실제 운영중인 프로젝트이기때문에 소스코드를 그대로 적을 수 없어 추가한 설정만 남겨놓겠다.</p>
<pre><code class="language-javascript">module.exports = withTM({
  publicRuntimeConfig,
  trailingSlash: true,
  reactStrictMode: false,
  experimental: {
    esmExternals: false,
    jsconfigPaths: true
  },
  webpack: (config) =&gt; {
    config.resolve.alias = {
      ...config.resolve.alias,
     ...
    }

    config.optimization = {
      ...config.optimization,
      runtimeChunk: false,
      splitChunks: false
    }

    return config;
  },

  webSocketTransport: false,
  async rewrites() {
    return rewrites
  }
})</code></pre>
<p>결과 <u><strong>실패</strong></u>. 😢
계속 같은 에러가 발생했고,  WebSocket 연결 시도가 계속되어졌다.</p>
<hr>
<h3 id="시도-2--nextjs-설정에-header-설정-추가---해결-❌">시도 2:  Next.js 설정에 header 설정 추가 - 해결 ❌</h3>
<p>WebSocket 연결 실패가 <span style="background-color: #e4c75b"><strong>CORS(Cross-Origin Resource Sharing)</strong> 정책 위반</span>일 수 있다고 판단했다. 
브라우저 보안 정책으로 인한 차단일 수 있어서 CORS header 추가를 시도했다.</p>
<pre><code class="language-javascript">    async headers() {
    return [
      {
        source: &#39;/_next/webpack-hmr&#39;,
        headers: [
          {
            key: &#39;Access-Control-Allow-Origin&#39;,
            value: &#39;*&#39;
          },
          {
            key: &#39;Access-Control-Allow-Methods&#39;,
            value: &#39;GET, POST, PUT, DELETE, OPTIONS&#39;
          },
          {
            key: &#39;Access-Control-Allow-Headers&#39;,
            value: &#39;X-Requested-With, Content-Type, Upgrade, Connection&#39;
          },
          {
            key: &#39;Connection&#39;,
            value: &#39;Upgrade&#39;
          },
          {
            key: &#39;Upgrade&#39;,
            value: &#39;websocket&#39;
          }
        ]
      }
    ]
  }</code></pre>
<p>결과 역시 <u><strong>실패</strong></u> 😢😢
CORS Header를 여러개 추가하고 삭제도 해보고 해봤지만, 같은 에러가 발생했다.
이에따라 단순한 CORS 문제가 아님을 깨달았다.</p>
<hr>
<h3 id="시도-3--ecosystemconfig파일-수정---해결-❌">시도 3:  ecosystem.config파일 수정 - 해결 ❌</h3>
<p><code>next.config.js</code> 설정만으로는 해결이 안되어 <span style="background-color: #e4c75b"><u><strong>프로세스 실행 환경(pm2)</strong></u>에서 WebSocket을 비활성</span>하려고 시도했다.
환경변수를 통해서 <code>Node.js</code>레벨에서 WebSocket 연결을 막을 수 있을 것이라 예상했다.</p>
<pre><code class="language-javascript">module.exports = {
  apps: [
    {
      name: &quot;&quot;,
        ...
      env: {
        ...,
        NEXT_HAS_CLIENT_WEBSOCKETS: &quot;false&quot;,
        DISABLE_WEBSOCKET: &quot;true&quot;
      },
    }
  ]
}</code></pre>
<p>결과로는 <u><strong>실패</strong></u>했다. 😢😢😢
환경변수 설정 적용이 안되었고, 여전히 같은 에러가 발생했다.</p>
<p>사실 이 이외에도 다양한 설정들을 주고 적용해봤지만 결과는 여전히 실패였다..</p>
<hr>
<h3 id="시도-4--nginx-수정----해결--✅">시도 4:  Nginx 수정 -  해결  ✅</h3>
<p>에러 메세지가 <code>wss://</code>로 시작하는 것을 보고 혹시 <u><strong>SSL/TLS</strong></u> 관련 문제이지 않을까? 라고 추측했다.
기존 프로젝트의 경우, SSL은 nginx에서 처리했다. 하지만 <u><strong>To-be 서버에서는 LB(Load Balancer)에서 SSL을 처리하도록 변경</strong></u>되었다.
이로 인해 WebSocket의 프로토콜 업그레이드가 제대로 되지 않는걸까? 라는 추측에서 시작되었다.
또한, <span style="background-color: #e4c75b"><u><strong>nginx 프록시 서버로 작동하면서 WebSocket 연결 요청을 제대로 처리하지 못하는 걸수도 있다고 추측</strong></u></span>했다.</p>
<p>(실제로는 여러 시도가 실패하다 보니 &#39;이쯤 되면 nginx 문제겠지...&#39;하는 마음 99.99999% .... 👉👈)</p>
<hr>
<h3 id="시도-4-1---nginx-설정-수정----해결--➿">시도 4-1 :  nginx 설정 수정 -  해결  ➿</h3>
<p>현재 <span style="background-color: #e4c75b"><u><strong>프로젝트는 8081 포트로 접속</strong></u></span>한다. 그래서 당연히 nginx.conf에서8081 포트에 WebSocket 설정을 수정해줬다.</p>
<pre><code class="language-javascript">server {
    listen 8081;  
    server_name name~;

    location / {
        proxy_pass http://localhost:8090;
    }
    location /_next/webpack-hmr {
        proxy_pass http://localhost:8090;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection &quot;upgrade&quot;;
    }
}</code></pre>
<p>결과로는 <u><strong>실패</strong></u>했다. 😢😢😢😢
계속 같은 에러가 발생했다. nginx 도 문제가 아닌걸까... 하다가 다시 에러 메세지를 확인했다.</p>
<p><span style="background-color: #ef9dae">🚨 WebSocket connection to &#39;wss:// [domain]: 8080/_next/webpack-hmr&#39; failed</span></p>
<hr>
<h3 id="span-stylebackground-color-c9f6ed시도-4-2---nginx-설정-수정----해결--✅u"><span style="background-color: #c9f6ed">시도 4-2 :  nginx 설정 수정 -  해결  ✅</u></h3>
<p>에러메세지를 자세히보니 <span style="color: red"><u>8080</u></span> 포트가 언급되어있다.
즉, <u><strong>클라이언트가 8080포트로 WebSocket 연결을 시도</strong></u>하고 있다고 판단해서 위의 설정을 8080포트에도 설정해줬다.</p>
<pre><code class="language-javascript">server {
    listen 8080;  
        ....

    location / {
        proxy_pass http://localhost:8090;
    }

    location /_next/webpack-hmr {
        proxy_pass http://localhost:8090;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection &quot;upgrade&quot;;
    }
}</code></pre>
<p>결과 <u><strong>성공</strong></u> 🎉🎉
<img src="https://velog.velcdn.com/images/dev_0livia/post/0c8be117-a6bf-4c0c-b2ef-c155ef664cc9/image.png" alt=""></p>
<p>더 이상 해당 에러가 콘솔에서 보이지 않았다.</p>
<p><strong><code>next.js</code>는 서버 상태 유지를 위해 WebSocket을 사용</strong>하는데, <span style="background-color: #e4c75b"><u><code>nginx</code>에 관련 설정이 없어서 연결이 계속 끊어</u></span>지고 있었다.
또한, <span style="background-color: #e4c75b">클라이언트는 8080 포트로 접속을 시도하는데, <u><strong>나는 8081 포트만 설정해둔 것</strong></u>이 문제</span>였다.</p>
<p>따라서 클라이언트가 접속하는 8080 포트에 WebSocket 설정을 추가하여 해결할 수 있었다.</p>
<hr>
<p>여기까지 느낀 결과: <strong>에러 메세지를 잘 읽자. 😊</strong>
<strong>라고 했으나 내가 8080 포트로 들어가서 그랬던 것.... ^^</strong></p>
<hr>
<p>참고:
<a href="https://github.com/vercel/next.js/issues/30491">https://github.com/vercel/next.js/issues/30491</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[pm2-logrotate]PM2 로그 관리를 통한 서버 메모리 이슈 해결]]></title>
            <link>https://velog.io/@dev_0livia/pm2-logrotatePM2-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%84%9C%EB%B2%84-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev_0livia/pm2-logrotatePM2-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%84%9C%EB%B2%84-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 02 Apr 2025 07:18:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/28c7da07-b8bd-4412-b7f0-d6cfbbb7ff46/image.png" alt=""></p>
<h1 id="배경">배경</h1>
<p>현재 운영 중인 프로젝트는 <code>Kafka Consumer</code>로 구성되어 있어, 다른 회사(Producer)로 부터 주기적으로 메세지를 전달받고 있다.</p>
<p>꽤나 방대한 양의 메세지를 받는 중이다🧚‍♀️✨
현재 서버 마이그레이션 작업이 진행 중이라 모니터링하는 과정에서 서버의 메모리 이슈가 발생했다.
꽤나 중요한 문제였기때문에 그동안 삽질하면서 해결했던 과정에 대해서 정리하려고 한다.</p>
<p>.pm2 로그 관리를 통한 서버 메모리 이슈 해결 내용이 필요한 분들은 가장 하단으로 이동해주길 바란다.</p>
<h3 id="기타-환경">기타 환경</h3>
<p>node.js 애플리케이션의 상용 프로세스를 관리하는 도구로 현 프로젝트는 pm2를 사용하고 있다.
pm2를 이용해서 애플리케이션이 종료되지 않도록 유지시켜주며, 다운타임 없이 재시작할 수 있게 된다.</p>
<hr>
<h2 id="🚨-문제-상황-1-git-pull--or-build이슈">🚨 문제 상황 1: <code>git pull</code>  or <code>build</code>이슈</h2>
<h3 id="💡-현상">💡 현상</h3>
<p>테스트 중 소스 코드 변경이 필요해서 WAS 서버에서 <code>git pull</code>을 시도했으나, 서버 용량 부족으로 인해 <code>git pull</code>이 되지 않았다.</p>
<p><code>git pull</code>로 당겨서 업데이트 된 소스코드를 받았지만, <code>build</code>가 안되는 문제 또한 발생했다.</p>
<pre><code class="language-bash">$ npm run build
npm ERR! code ENOSPC
npm ERR! syscall write
npm ERR! errno -28
npm ERR! nospc ENOSPC: no space left on device, write
npm ERR! nospc There appears to be insufficient space on your system to finish.
npm ERR! nospc Clear up some disk space and try again.

npm ERR! A complete log of this run can be found in:
npm ERR!    .../.log</code></pre>
<h3 id="🛠️-조치">🛠️ 조치</h3>
<p>급하게 소스 코드를 업데이트해서 빌드해야했으므로 임시 방편으로 임시 파일들을 지우는 방법으로 조치하고 작업을 수행했다.</p>
<p>급하게 처리한 것이라 정리가 안되었다.
순서가 바뀔 수 있을텐데 그래도 필요한 분이 있다면 참고가 되었으면 하는 마음에 사용했던 방법들을 아래에 정리해놨다.</p>
<pre><code class="language-bash">rm -rf .git/objects/pack/*.pack
rm -rf .git/objects/pack/*.idx

rm -f .git/objects/pack/._pack-*
rm -rf .git
git init
git remote add origin https://github.com/was.git
git pull
git remote show origin

git branch
git branch --set-upstream-to=origin/main master
git pull
git checkout -b main origin/main</code></pre>
<pre><code class="language-bash">// 문제 발생

$ git branch --set-upstream-to=origin/main master
fatal: branch &#39;master&#39; does not exist

$ git pull origin main
From https://github.com/was
 * branch            main       -&gt; FETCH_HEAD
error: Untracked working tree file &#39;.dockerignore&#39; would be overwritten by merge.</code></pre>
<pre><code class="language-bash">git clean -f .dockerignore
git pull origin main
git status
git diff origin/main
git branch -avv 
git remote -v 
git branch -m master main 
git branch --set-upstream-to=origin/main main 
git ls-remote origin
git remote show origin
git fetch origin --prune 
git reset HEAD .
git checkout .
git fetch --all
git reset --hard origin/main</code></pre>
<p>이후에는 git pull과 build를 잘해서 새로운 소스코드를 서버에 잘 반영했다.</p>
<h2 id="🚨-문제-상황-2-kafka-메세지-수신-이슈">🚨 문제 상황 2: <code>Kafka</code> 메세지 수신 이슈</h2>
<h3 id="💡-현상-1">💡 현상</h3>
<p>Kafka Broker IP를 변경 후 TO-BE Kafka 트랜잭선이 보이지 않는 문제가 발생했다.
WAS log를 모니터링하면 <code>JS stacktrace</code> 에러가 발생한 뒤에 더 이상 Kafka 메세지를 받아오지 못했다.</p>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/6e1989d5-4866-4125-9ce7-41e53d82e665/image.png" alt=""></p>
<p><span style="color: red">FATAL ERROR</span>: Reached heap limit Allocation <span style="color: red">failed</span>  - JavaScript heap <span style="color: orange">out of memory</span></p>
<p>해당 문제는 <span style="color: red"><code>JavaScript heap out of memory</code></span>로 heap 메모리가 부족해서 발생한 것이다.</p>
<h3 id="🛠️-1-차-조치-시도-cron-작업-주기-조정">🛠️ 1 차 조치 시도: <code>Cron</code> 작업 주기 조정</h3>
<p><code>Cron</code>을 통해서 작업해야했는데 테스트 때문에 Cron 주기를 너무 빈번하게 했다.
cron 작업은 새로운 프로세스를 생성하고 메모리를 할당받기 때문에 너무 빈번한 실행은 결국 메모리 부족을 발생시킬 수 있다.
이에 따라 cron 주기를 한달에 한 번으로 늘렸다.</p>
<p>해결되는 듯했으나, 일시적이였고 곧 메모리 할당 문제로 동일한 현상이 재발했다.</p>
<hr>
<h3 id="🛠️-2-차-조치-시도-로그-최적화">🛠️ 2 차 조치 시도: 로그 최적화</h3>
<p>Kafka 메세지가 들어올 때마다 로그가 너무 빈번하게 기록되는 문제를 발견했다.
로그가 과도하게 쌓일 경우, 결국 디스크 공간이 부족할 수 있기 때문에, Kafka 메세지에 대해 한 번만 로그를 기록하도록 수정했다.</p>
<p>물론 어느정도 해결은 되었으나, 동일한 메모리 문제가 발생했다.</p>
<hr>
<h3 id="🛠️-3-차-조치-시도-메모리-추가-할당">🛠️ 3 차 조치 시도: 메모리 추가 할당</h3>
<p>다시 에러를 확인했는데, Heap 메모리가 너무 부족한 것이 계속 눈에 밟혔다.
현재 전체 용량이 어느정도인지 확인했다.</p>
<pre><code class="language-bash">$ node -e &#39;console.log(v8.getHeapStatistics().heap_size_limit/(1024*1024))&#39;

// 2096</code></pre>
<p>2096이니까 약 2배전도 용량을 늘려 메모리 할당을 증가시켰다.</p>
<p>용량을 늘리는 것은 간단하다. ecosystem.config에서 하당의 코드를 그대로 추가했다.</p>
<pre><code class="language-bash">module.exports = {
    apps: [
        {
            name: &#39;nest&#39;,
            script: &#39;./dist/main.js&#39;,
            node_args:  &quot;--max-old-space-size=4096&quot;,,
            ...
        }
    ]
}</code></pre>
<p>한 동안 에러 없이 Kafka 메세지가 제대로 들어오는 것을 확인했다.</p>
<hr>
<h3 id="🛠️-4-차-조치-시도--db-쿼리-최적화">🛠️ 4 차 조치 시도:  DB 쿼리 최적화</h3>
<p>계속 되는 용량 문제때문에 분석하던 중, Kafka 메세지 수신 시 비정상적으로 많은 DB쿼리가 실행되는 것을 발견했다.
문제가 된 쿼리들은 아래와 같았다.</p>
<pre><code>1. 레거시 코드에 남아있는 미사용 쿼리들
2. 불 필요한 중복 조회 쿼리들 
3. 최적화 되지 않은 쿼리문 </code></pre><p>위의 쿼리들을 제거하자 Kafka 메세지가 정상적으로 수신되었다. 
하지만, 이것 역시  임시적인 해결책에 불과했다.</p>
<hr>
<h3 id="🛠️-5-차-조치-시도--근본-원인-발견">🛠️ 5 차 조치 시도:  근본 원인 발견</h3>
<p>이틀 후 또 다시 메세지가 중단되어 디스크 사용량을 확인해봤다.</p>
<pre><code class="language-bash">$ df -h</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/5455dbe8-4c2b-4f95-915a-3c496d57e691/image.png" alt=""></p>
<p>확인 결과 <code>.pm2</code> 디렉토리의 <span style="background-color: gold">로그 파일이 과도하게 누적</span>되어 있었다.</p>
<p>장기간 운영하던 프로젝트이기 때문에, 급격히 늘어난 Kafka 메세지 양으로 인해 로그가 지속적으로 축척되었다.</p>
<p>따라서 로그 파일을 백업한 후 삭제하자, 지금까지도 Kafka 메세지가 안정적으로 수신되는 것을 확인했다.</p>
<p>또 문제가 발생하면 &#39;6차 시도&#39;에 관해 글을 업데이트하겠지만, 현재까지는 로그 파일이 위의 모든 에러들의 근본적인 원인임을 파악하고, 
로그 파일을 관리하는 것을 아래에 작성할 것이다.</p>
<hr>
<h3 id="pm2-로그-파일">pm2 로그 파일</h3>
<p>기본적으로 pm2는 로그를 <code>/home/user/.pm2</code>에 log 폴더가 존재한다.</p>
<pre><code class="language-bash">// .pm2/logs

$ ls
nest-error.log  nest-out.log</code></pre>
<p>특별한 설정을 해두지 않았기 때문에, <code>nest-error.log</code>와 <code>nest-out.log</code>에 로그가 쌓여있는 것을 확인할 수 있다.</p>
<p>이미 로그를 한 번 삭제했기 때문에, 현재 많은 데이터가 쌓여있지 않지만, 분명 며칠이 지나면 또 가득 찰 것으로 예상된다.</p>
<pre><code class="language-bash">[user@ip logs]$ ls
nest-error.log  nest-out.log

[user@ip logs]$ ls -alh
total 5.4G
drwxr-xr-x. 2 user user 4.0K Dec 18 13:37 .
drwxr-xr-x. 5 user user 4.0K Dec 19 11:16 ..
-rw-r--r--. 1 user user 4.7G Dec 20 06:55 nest-error.log
-rw-r--r--. 1 user user 643M Dec 20 06:55 nest-out.log</code></pre>
<h3 id="pm2-logrotate-모듈">PM2 logrotate 모듈</h3>
<p>앞서 지금까지 상황을 미루어보아 위의 로그 파일들은 현 프로젝트가 계속 운영될수록 점점 더 쌓여서 또 서버 용량을 가득 차게 만들 것이다.</p>
<p>그럼 분명 디스크 용량이 부족하게되면서 <span style="color: red"><code>git</code>, <code>build</code>, <code>kafka 메세지 수신</code></span>등 많은 문제를 일으킬 수 있다.</p>
<p>이를 위해 pm2 로그 파일을 관리하는 <span style="color: red"><code>pm2 logrotate</code></span>모듈을 사용해보기로 했다.</p>
<h4 id="pm2-logrotate-모듈이란">PM2 logrotate 모듈이란?</h4>
<p>pm2를 실행할때마다 로그 경로에 저장되는 로그들은 직접 삭제해주지 않는다면, 계속 쌓이게 된다. 이 모듈을 설치하게 되면 로그 자동 삭제, 파일 리사이즈 등등 다양한 로그 관리 기능을 이용할 수 있게 된다.</p>
<h4 id="pm2-logrotate-설치-방법">PM2 logrotate 설치 방법</h4>
<p>pm2 logrotate 파일은 아래 명령어를 통해 설치할 수 있다.</p>
<pre><code class="language-bash">pm2 install pm2-logrotate</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/18f5ae23-6934-4fcd-83de-350bdeb70071/image.png" alt=""></p>
<h4 id="pm2-logrotate--옵션">PM2 logrotate  옵션</h4>
<pre><code class="language-bash">pm2 conf</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/b89545fd-8e2d-4b9f-83e0-7e18c4f964e8/image.png" alt=""></p>
<p><code>pm2 conf</code>를 통해 logrotate에 기본적으로 설정된 옵션을 확인할 수 있다.</p>
<ul>
<li>max_size: 각각의 로그파일의 최대 크기. 해당 용량이 넘어가면 다음 파일을 생성하게 된다.(기본값 10M)</li>
<li>retain: 로그 저장 갯수 (기본값 30개)</li>
<li>compress: 생성된 로그를 gzip으로 압축 유무 (기본값 false)</li>
<li>dataFormat: 로그 파일 뒤에 날짜 포맷 (기본값 YYYY-MM-DD_HH-mm-ss)</li>
<li>workerInterval: 로그 파일 사이즈를 확인하는 1초마다의 횟수. (기본값 초당 30회)</li>
<li>rotateInterval: cronjob (기본값 0 0 * * -&gt; 매일 정각)</li>
<li>rotateModule: 새로운 로그 파일 생성 여부 (기본값 true)</li>
</ul>
<p>기본 설정을 보면, 로그가 10MB 넘길때마다 새로운 로그 파일이 생성되며, 30개 이상 생성되지 않도록 한다. =&gt; 300MB 까지만 로그가 기록된다.</p>
<p>현재 꽤나 용량이 크기 때문에 설정을 아래와 같이 바꿨다.</p>
<pre><code class="language-bash">pm2 set pm2-logrotate:rotateInterval &quot;0 0 * * *&quot;
pm2 set pm2-logrotate:compress true
pm2 set pm2-logrotate:retain 30
pm2 set pm2-logrotate:dateFormat YYYY-MM-DD
pm2 set pm2-logrotate:workerInterval 30
pm2 set pm2-logrotate:max_size 2000M</code></pre>
<p>잘 설정되었는지 확인하려면  <code>pm2 conf</code>로 확인할 수 있다.
<img src="https://velog.velcdn.com/images/dev_0livia/post/67a6d77a-1075-478f-83dc-88c352eda03b/image.png" alt=""></p>
<p>이렇게 되면 자동으로 <code>./pm2/logs</code>폴더 안에 <code>nest-out.log</code>, <code>nest-error.log</code>파일들 뒤에 날짜가 붙은채로 압축된 파일이 매일 하나씩 생성될 것이다.</p>
<p>(현재 테스트 중)</p>
<p>이렇게되면 가독성이 없다고 판단해서 폴더를 따로 만들어서 거기다가 압축파일을 옮기고 싶었는데, 하루종일 삽질한 결과 <code>pm2-logrotate</code>에는 그런 기능은 없는듯하다..</p>
<hr>
<h2 id="🤯-삽질의-기록">🤯 삽질의 기록</h2>
<p>궁금한 사람들을 위한 내 삽질 기록..</p>
<p>나는 댕멍청하게 config파일에 <code>error_file</code>과 <code>out_file</code>을 설정하면 될것이라고 판단했다.</p>
<pre><code class="language-bash">module.exports = {
  apps: [
    {
      name: &#39;nest&#39;,
      script: &#39;./dist/main.js&#39;,
      cwd: &#39;/home/user/was&#39;,
      watch: true,
      log_date_format: &#39;YYYY-MM-DD&#39;,
      error_file: &#39;/home/user/was/.pm2/logs/logError/nest-error.log&#39;,
      out_file: &#39;/home/user/was/.pm2/logs/logOut/nest-out.log&#39;,
      env: {
              ...
</code></pre>
<p>이렇게 설정해두고서 로그를 찍어봤는데, <code>pm2 logs nest 0</code>을 하면 실시간으로 로그를 확인할 수 있는데,
<code>.pm2/logs/</code>폴더에서는 로그가 저장되지 않았다.</p>
<p>이미 <code>.pm2/logs</code>폴더 안에 <code>nest-out.log</code>와 <code>nest-error.log</code>가 존재하기 때문에 해당 파일을 찾지 못해서 발생하는 문제인 듯 하다.</p>
<p>그래서 해당 설정을 지우니까 다시 log가 잘 저장되었다.</p>
<p>(왜 글이 사라졌냐... 왜....)</p>
<p><del>지금 정리하면서 든 생각은 이미 있는 nest-out.log와 nest-error.log의 파일 위치를 각각 logOut과 logError 폴더로 이동시키면 되지 않을까..? 라는 생각 ??</del>
<del>출근해서 테스트 해보고 다시 수정해보겠다.</del> <span style="background-color: #eba487">=&gt;cron과 script파일로 수정할 예정.</span></p>
<hr>
<h3 id="💡cron--script-설정">💡cron &amp; script 설정</h3>
<p>테스트해본 결과 <span style="color: red"><code>pm2-logrotate-out___2024.12.22.log.gz</code></span> 이런 식으로 pm2-logrotate 로그파일이 계속 생성되었다.
이렇게 될 경우, 또 로그파일이 쌓이면서 디스크 용량이 더 커질 것으로 생각되었다.
따라서 해당 로그파일은 이틀에 한 번씩 삭제하는 걸로 수정하기로 했다.
또한, <span style="color: red"><code>nest-error</code></span>와 <span style="color: red"><code>nest-out</code></span> 파일이 압축되면서 <u>디렉토리 구조가 너무 지저분하게 관리되는 문제</u>가 있었다.
한 두개의 압축파일은 괜찮겠지만, 한 달동안 둬야할 경우에는 분명 파일 찾아보는 것이 힘들 것이라 판단했다.
이 경우에도 script로 관리하는 것으로 해결했다.</p>
<h3 id="🛠️-script-설정---pm2-logrotate-out___loggz-파일-삭제">🛠️ script 설정 - <code>pm2-logrotate-out___log.gz</code> 파일 삭제</h3>
<p>우선, script 파일들을 저장할 <code>script</code>폴더를 만들었다.</p>
<pre><code class="language-bash">cd .pm2/logs
mkdir -p scripts</code></pre>
<p>이후 pm2-logrotate와 관련된 로그를 지우는 스크립트를 작성했다.</p>
<p><code>error</code>는 당분간 모니터링을 위해서 지우지 않는 것으로 했다.</p>
<pre><code class="language-bash">vi clean_logrotate_log.sh
#!/bin/bash

LOG_PATH=&quot;/home/user/.pm2/logs&quot;

find &quot;$LOG_PATH&quot; -type f -name &quot;pm2-logrotate-out__*.log.gz&quot; -mtime +2 -exec rm -f {} \;</code></pre>
<p><code>esc</code> -&gt; <code>:wq!</code>로 저장.</p>
<p>만약 스크립트가 잘 작동되는지 확인하려면, <code>./clean_logrotate_log.sh</code>를 입력해서 해당 로그파일들이 삭제되는지 확인하면 된다.</p>
<p>삭제가 잘 된다면, 스크립트가 실행될 수 있도록 권한을 부여한다.</p>
<pre><code class="language-bash">chmod +x /home/user/.pm2/logs/scripts/clean_logrotate_logs.sh</code></pre>
<h4 id="🛠️-script-설정---nest-out--nest-error-파일--이동">🛠️ script 설정 - <code>nest-out</code> &amp; <code>nest-error</code> 파일  이동</h4>
<blockquote>
<p><code>.pm2/logs/script</code>폴더 안에 스크립트 파일 생성</p>
</blockquote>
<p>우선, logs 폴더 안에 errorLog와 outLog 폴더를 생성한다.</p>
<pre><code class="language-bash">mkdir -p errorLog
mkdir -p outLog</code></pre>
<p><code>./pm2/logs/script</code>폴더로 이동해서 <code>move_nest_logs.sh</code>스크립트를 생성한다.</p>
<pre><code class="language-bash">vi move_nest_logs.sh
#!/bin/bash

SOURCE_DIR=&quot;/home/user/.pm2/logs&quot;
ERR_DEST=&quot;$SOURCE_DIR/errorLog&quot;
OUT_DEST=&quot;$SOURCE_DIR/outLog&quot;

find &quot;$SOURCE_DIR&quot; -type f -name &quot;nest-error__*.log.gz&quot; -exec mv {} &quot;$ERR_DEST&quot; \;

find &quot;$SOURCE_DIR&quot; -type f -name &quot;nest-out__*.log.gz&quot; -exec mv {} &quot;$OUT_DEST&quot; \;</code></pre>
<h4 id="🛠️-cron-설정">🛠️ cron 설정</h4>
<p>매일 자정 위에서 만든 스크립트가 실행되도록 cron을 설정한다.</p>
<p>대신 로그가 찍히는 것을 막기 위해서 <code>/dev/null 2&gt;&amp;1</code> 를 설정해줬다.</p>
<pre><code class="language-bash">crontab -e
0 0 * * * /home/user/.pm2/logs/scripts/clean_logoutrotate_logs.sh &gt; /dev/null 2&gt;&amp;1 &amp;&amp; /home/user/.pm2/logs/scripts/move_nest_logs.sh &gt; /dev/null 2&gt;&amp;1</code></pre>
<hr>
<h4 id="🚨-pm2-install-pm2-logroate-설치-에러">🚨 pm2 install pm2-logroate 설치 에러</h4>
<blockquote>
<p>npm 네트워크 문제로 설치가 안될 경우</p>
</blockquote>
<pre><code class="language-bash">[user@user ~]$ pm2 install pm2-logrotate
[PM2][Module] Installing NPM pm2-logrotate module
[PM2][Module] Calling [NPM] to install pm2-logrotate ...
npm ERR! code ECONNRESET
npm ERR! syscall read
npm ERR! errno ECONNRESET
npm ERR! network request to https://registry.npmjs.org/pm2-logrotate failed, reason: read ECONNRESET
npm ERR! network This is a problem related to network connectivity.
npm ERR! network In most cases you are behind a proxy or have bad network settings.
npm ERR! network
npm ERR! network If you are behind a proxy, please make sure that the
npm ERR! network &#39;proxy&#39; config is set properly.  See: &#39;npm help config&#39;

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/user/.npm/_logs/2024-12-23T07_14_47_792Z-debug-0.log
[PM2][ERROR] Installation failed via NPM, module has been restored to prev version</code></pre>
<p>자꾸 pm2-logrotate를 하는 과정에서 에러가 발생했다.
그래서 직접 수동으로 <code>pm2-logrotate</code>소스코드를 직접 다운받기로 했다.</p>
<pre><code class="language-bash">git clone https://github.com/pm2-hive/pm2-logrotate.git</code></pre>
<p>이 경우, npm 으로 설치할때 글로벌로 설치해야한다.</p>
<pre><code class="language-bash"># sudo로 글로벌 설치
sudo npm install -g

# 또는 현재 위치에서 pm2에 직접 설치
cd pm2-logrotate
pm2 install .</code></pre>
<p>나의 경우, <code>pm2-logrotate</code> 폴더에서 직접 pm2에 설치했다.</p>
<p>설치 후 <code>pm2 conf</code>를 했을 경우, default로 설정되어있는 것들이 설치가 안되어있어서 다시 설치를 해줬다.</p>
<pre><code class="language-bash">pm2 set pm2-logrotate:rotateInterval &quot;0 0 * * *&quot;
pm2 set pm2-logrotate:compress true
pm2 set pm2-logrotate:retain 30
pm2 set pm2-logrotate:dateFormat YYYY-MM-DD
pm2 set pm2-logrotate:workerInterval 30
pm2 set pm2-logrotate:max_size 2000M
pm2 set pm2-logrotate:rotateModule true</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/81eeac0b-40e5-4ab1-a9ad-53fab59f4581/image.png" alt=""></p>
<p><span style="color: red">안됨. 망할 방화벽..,</span>
<span style="color: red">안되어서... 쉘 스크립트 또 만들었음..</span></p>
<pre><code class="language-bash">#!/bin/bash

LOG_DIR=&quot;/home/user/.pm2/logs&quot;
RETAIN_DAYS=30
DATE_FMT=$(date &#39;+%Y-%m-%d&#39;)


TARGET_FILES=(&quot;name-out.log&quot; &quot;name-error.log&quot;)

for logfile in &quot;${TARGET_FILES[@]}&quot;
do
  FULLPATH=&quot;$LOG_DIR/$logfile&quot;

  if [ -f &quot;$FULLPATH&quot; ]; then
    base_name=&quot;${logfile%.log}&quot;
    new_name=&quot;${base_name}__${DATE_FMT}.log&quot;

    mv &quot;$FULLPATH&quot; &quot;$LOG_DIR/$new_name&quot;

    gzip -f &quot;$LOG_DIR/$new_name&quot;

  fi
done

pm2 reloadLogs

find &quot;$LOG_DIR&quot; -type f -name &quot;*.gz&quot; -mtime +$RETAIN_DAYS -exec rm -f {} \;</code></pre>
<p>이렇게 만들고 이 파일도 crontab에 추가해주면 된다.</p>
<hr>
<h2 id="🧚♀️-결론">🧚‍♀️ 결론:</h2>
<p>pm2-logrotate를 사용하니까 또 pm2.log가 쌓여가서..
결국은 모든 서버에 scrtips, cron으로 셋팅하고 log-rotate를 제거했다 ^^..,</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Nginx에서 SSL 인증서 갱신해보자!!!!!]]></title>
            <link>https://velog.io/@dev_0livia/centOS%EC%97%90%EC%84%9C-nginx-SSL-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@dev_0livia/centOS%EC%97%90%EC%84%9C-nginx-SSL-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Mon, 02 Dec 2024 11:07:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dev_0livia/post/b5968ffe-75ed-4ffe-81b6-dd429d484174/image.png" alt="">
똥멍청이인 나를 위해 <code>nginx</code> <strong>ssl 변경</strong>하는 과정을 다시 기록하고자 한다...🤦‍♀️</p>
<p>현회사에서 <strong>WEB서버는 <code>nginx</code>를 통해 ssl를 설정</strong>하고 있다.
정확하게 기억은 안나지만, 대략 6-8개월 전에 한 번 ssl을 변경시켰었던 적이 있었다. 
처음이자 마지막이였는데, 또 다시 업데이트를 하게 되었다. 하지만 기억이 나지 않...🤯
분명 SSL은 업데이트를 시켰는데, 재실행하지 않아서 인증서가 만료되는 똥멍청이같은 짓을 하게 되었다.</p>
<p>기록해두고 담번엔 실수 안해야징 쿄쿄</p>
<hr>
<h1 id="nginx-ssl-변경-방법">Nginx SSL 변경 방법</h1>
<h2 id="ssl이-위치한-폴더로-이동">ssl이 위치한 폴더로 이동</h2>
<p>나의 경우 mobaxterm에서 WEB 서버를 nginx 계정으로 로그인하여 들어갔다.
나의 경우 <code>/webserver/nginx/ssl</code> 폴더에 ssl이 위치해있다.
여기다 업데이트할 파일을 옮겨준다.</p>
<h2 id="ssl-파일명이-변경되었을-경우">ssl 파일명이 변경되었을 경우,</h2>
<p>이번 경우에는 ssl 파일명이 변경되지 않아 <code>nginx.conf</code> 파일을 변경하지 않아도 되었지만, 
만약, <strong>파일명이 변경되었을 경우 <code>nginx.conf</code>파일에 들어가서 ssl 이 설정된 곳을 변경</strong>시켜줘야 한다.</p>
<p>나의 경우, <code>/webserver/nginx/httpd/conf</code>에 <code>nginx.conf</code> 파일이 존재한다.</p>
<p>vi 혹은 nano를 통해서 nginx.conf파일을 수정한다.</p>
<pre><code class="language-bash">vi nginx.conf

...
server {
 ssl_certificate      /ssl파일이 있는 경로/certification 파일 이름;
 ssl_certificate_key  /ssl파일이 있는 경로/key 파일 이름;
}

...

:wq!</code></pre>
<p>해당 ssl이 해당하는 위치를 확인하고 작성하는 것이 좋다.</p>
<h2 id="⭐️-제일-중요-nginx-재시작-⭐️">⭐️ 제일 중요!! nginx 재시작!! ⭐️</h2>
<p>내가 재시작하는 것을 잊어서... 이 사단이 났다.. 하.. 왜.. 나ㅡㄴ...</p>
<p>현 회사의 경우,  nginx는 /sbin/nginx 에서 실행 및 정지를 하고 있다.</p>
<h3 id="sbin으로-들어가기">/sbin으로 들어가기</h3>
<pre><code class="language-bin">/webserver/nginx/httpd/sbin</code></pre>
<h3 id="nginx를-정지시키기">nginx를 정지시키기</h3>
<pre><code class="language-bin">./nginx -s stop</code></pre>
<h3 id="nginx가-멈춘지-확인">nginx가 멈춘지 확인</h3>
<pre><code class="language-bash">ps aux | grep nginx</code></pre>
<p>여기서 </p>
<pre><code class="language-bash">root      0000  0.0  0.0  0000   000 ?        Ss   11:11   0:00 nginx: master process ./nginx
nginx     0000  0.0  0.0  0000  0000 ?        S    11:11   0:00 nginx: worker process</code></pre>
<p>이게 안보이면 멈춘것.</p>
<h4 id="nginx-재시작">nginx 재시작</h4>
<pre><code class="language-bin">./nginx</code></pre>
<h4 id="nginx가-재시작한지-확인">nginx가 재시작한지 확인</h4>
<pre><code class="language-bash">ps aux | grep nginx</code></pre>
<p>여기서 </p>
<pre><code class="language-bash">root      0000  0.0  0.0  0000   000 ?        Ss   11:11   0:00 nginx: master process ./nginx
nginx     0000  0.0  0.0  0000  0000 ?        S    11:11   0:00 nginx: worker process</code></pre>
<p>이게 보이면 재시작한 것.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Cron으로 Task Scheduling]]></title>
            <link>https://velog.io/@dev_0livia/Cron%EC%9C%BC%EB%A1%9C-Task-Scheduling</link>
            <guid>https://velog.io/@dev_0livia/Cron%EC%9C%BC%EB%A1%9C-Task-Scheduling</guid>
            <pubDate>Mon, 25 Nov 2024 08:48:19 GMT</pubDate>
            <description><![CDATA[<p>현재 회사에서 맡고 있는 프로젝트에서 꽤나 자주 사용하는 <code>Cron</code>에 대해서 정리해보고자 한다.
예전에 Kubernetes를 살짝 찍먹할때 <code>Cronjob</code>에 대해서 아주 살짝 공부한 적이 있기 때문에 그렇게 낯설지 않 았던 <code>Cron</code>...</p>
<pre><code>쿠버네티스에서 Cronjob
- 클러스터 레벨에서 동작하는 스케줄링 리소스
- 주기적으로 pod 생성해 작업 수행</code></pre><p><img src="https://velog.velcdn.com/images/dev_0livia/post/a97b8da6-8159-4534-b1cf-4a2f156765a7/image.png" alt=""></p>
<p>또 다시 마주치고 싶지 않아 k8s...🤦‍♀️</p>
<p>위에서 살짝 언급했듯, <strong>쿠버네티스에서 <code>Cronjob</code></strong>은 <strong>클러스터 레벨에서 작동</strong>을 한다. 
반면, <strong>Nest.js의 <code>Cron</code></strong>은 <strong>애플리케이션 레벨에서 동작</strong>하며, <strong>애플리케이션이 실행중인 동안 계속 동작</strong>하게 된다.</p>
<p>어쨌거나 저쨌거나 내가 이번 프로젝트에서 Cron을 사용한 이유는 여러가지가 있겠지만 크게 두 가지 기능을 위해서다.</p>
<blockquote>
</blockquote>
<p><strong>1. 사용자 비밀번호 만료일 관리:</strong> 비밀번호 만료일(expire date)를 확인하고, 만료된 계정을 자동으로 disabled 처리하는 작업
<strong>2. 로그 데이터 정리:</strong> 2022년부터 쌓여있는 방대한 로그 기록 중 최근 1달치만 보관하고 나머지는 자동 삭제하는 작업</p>
<p>이렇게 <strong>반복적이고 정기적으로 실행</strong>해야하는 작업들은 <code>Cron</code>을 통해서 자동화할 수 있다.
특히, <strong>Nest.js</strong>에서는 <strong>애플리케이션 코드 내에서 이런 작업들을 직접 스케줄링</strong>할 수 있어서 <strong>별도의 인프라 설정 없이도 편리하게 사용</strong>할 수 있다.</p>
<p>백엔드를 하게된지 0.5개월된 신입 개발자의 머리에는 <code>Cron</code>말고 떠오르는게 없는데, 더 좋은 방법이 있다면 언제든 댓글로 알려주십쇼.. 😮‍💨</p>
<hr>
<h2 id="nestjs에서-cron-사용하기">Nest.js에서 Cron 사용하기</h2>
<p>Nest.js에서 <strong>Cron</strong>을 사용하기 위해서는 우선 관련 모듈을 설치해야 한다.</p>
<h3 id="npm을-사용하는-경우">npm을 사용하는 경우:</h3>
<pre><code class="language-bash">npm install --save @nestjs/schedule
npm install --save-dev @types/cron</code></pre>
<h3 id="yarn을-사용하는-경우">yarn을 사용하는 경우:</h3>
<pre><code class="language-bash">yarn add @nestjs/schedule
yarn add --dev @types/cron</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TDD]]></title>
            <link>https://velog.io/@dev_0livia/TDD</link>
            <guid>https://velog.io/@dev_0livia/TDD</guid>
            <pubDate>Mon, 21 Oct 2024 08:59:03 GMT</pubDate>
            <description><![CDATA[<h1 id="시작하며">시작하며</h1>
<p>현 회사에서 기존에 개발되어 운영 중인 서비스를 담당하게 되었다.
처음엔 단순 유지보수만 하면 될 줄 알았지만 시간이 지날수록 기존 코드를 수정하고 새로운 기능을 추가해야 하는 일이 잦아졌다.</p>
<p>이런 과정에서 가장 큰 고민은 코드 수정이나 기능 추가 후 기존 기능들이 정상 작동 하는것이냐였다.
물론 문제 없이 잘 돌아가면 좋겠지만, 현실은 그렇지 않았다. 
수정 후 직접 확인했음에도 예상치 못한 곳에서 에러가 튀어나오곤 해서 결국 모든 기능을 일일이 확인해야만 했다.</p>
<p>시간이 충분하다면 전부 꼼꼼히 확인하는 게 최선이겠지만, 현실적으로 매번 그러기는 힘들다. </p>
<p>이런 고민을 하다 문득 취준생 시절 기술 면접에서 자주 들었던 질문이 떠올랐다. 
&quot;테스트 코드 작성해보셨어요?&quot;
아이러니하게도 취업 후엔 테스트 코드를 작성할 기회가 줄어들어 그 중요성을 잊고 있었다. 
이번 경험을 계기로 테스트 코드의 필요성을 다시 한번 느끼고, 제대로 공부해보기로 마음먹었다.</p>
<h1 id="tdd란">TDD란</h1>
<p>TDD(TEST-DRIVEN DEVELOPMENT)는 동작하는 코드를 작성하기 이전에, 테스트를 먼저 작성하고, 그 테스트를 통과하는 코드를 작성한다. 통과한다면 그 코드를 리팩토링한다.</p>
<img src="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDRgJ0%2FbtrZo7L2KvJ%2FREWhExmRdD1yGxbZYcmob1%2Fimg.webp"/>

<p>위의 사진처럼 TDD는 3가지 단계를 한 사이클로 돈다.</p>
<ol>
<li><span style="color: red"><strong>Write a failing test</strong></span> : 실패하는 작은 테스트 작성. 컴파일러조차 되지 않을 수 있지만 일단 작성한다.</li>
<li><span style="color: green"><strong>Make the test pass</strong></span> : 테스트를 통과하도록 최소한의 코딩을 하는 것</li>
<li><span style="color: blue"><strong>Refactor</strong></span> : 리팩토링. 중복된 코드를 제거하는 등 코드를 리팩토링한다.</li>
</ol>
<h3 id="왜-실패하는-작은-테스트를-작성해야하는-것일까">왜 실패하는 작은 테스트를 작성해야하는 것일까?</h3>
<p>내 생각에는 테스트 코드를 써보고 실행해서 잘못되었을 경우, <strong>더 개발이 진행되기 전에 개발 초기 단계에서 잘못된 코드/분석 및 설계라는 것을 깨달을 수 있기 때문이라 생각</strong>한다.</p>
<p>테스트 코드를 작성하다보면, 코드의 구조에 대해서 깊게 고민할 수 있고, 새로운 방향을 깨달을 수 있다.
더더욱 복잡한 로직일수록 체계적으로 해결해 더 옳은 방향으로 개발 할 수 있도록 깨달을 수 있기 때문이다. </p>
<p>초기 프로젝트에 대해서 분석하고 설계하는 것은 생각보다 굉장히 어려운 일이라고 생각한다.
프로젝트의 미래와 방향 등은 모두 <strong>&quot;추측&quot;</strong>에 기반되어 시작된다.</p>
<p>이런 추측에 기반해 오랜 기간 설계하고 구현하다보면 해당 설계가 맞으면 좋겠지만, 선택한 설계 방향이 적절하지 않음을 깨달을 수 있다.</p>
<p>이미 상당 시간을 분석과 설계 그리고 어느정도의 개발 단계까지 투자했기 때문에, 다시 원점으로 되돌리게 된다면 데드라인을 맞추지 못할 가능성이 크다.</p>
<h3 id="반드시-tdd를-사용해야할까">반드시 TDD를 사용해야할까?</h3>
<p>반드시 필요하다 말하기도 어렵다.
이 글을 쓰기 전까지 나는 오히려 &#39;TDD는 비효율적이다.&#39; 라고 생각하는 개발자 중 한 명이었다.</p>
<p>예를들어, 내가 맡았던 프로젝트들의 경우 개발 기한이 생각보다 굉장히 짧을 때가 많았다.
개인적으로 나는 이해 관계자들과 함께 업무를 진행해야하는 환경 속에서 가장 중요한 것은 무엇보다도 <strong>&quot;기한 지키기&quot;</strong> 였다.</p>
<p>따라서 기능을 하나 추가할때마다, 테스트 코드를 미리 작성해야하기 때문에 오히려 개발 속도를 늦출 수 있기 때문에 단기적인 생산성을 저하시킬 수 있다고 생각한다.
따라서 대부분의 프로젝트는 <strong>&quot;분석 -&gt; 설계 -&gt; 구현 -&gt; 테스트 -&gt; 오픈&quot;</strong> 방향으로 개발을 했다.</p>
<p>어느것이 옳고 틀린지 아직은 명확하게 알 수 없으나, 개인적인 생각으론 <strong><u>각 프로젝트마다 환경과 요구사항이 다 다르기 때문에, 상황에 맞는 방법론을 선택하여 테스트</strong></u>하는 것이 좋다고 생각한다.</p>
<h1 id="간단한-예제를-통해서-테스트해보자-with-reactjs">간단한 예제를 통해서 테스트해보자. (with React.js)</h1>
]]></description>
        </item>
        <item>
            <title><![CDATA[Webpack이란?]]></title>
            <link>https://velog.io/@dev_0livia/Webpack%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@dev_0livia/Webpack%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Tue, 01 Oct 2024 14:33:50 GMT</pubDate>
            <description><![CDATA[<h1 id="webpack이란">Webpack이란?</h1>
<p>Bundling = JavaScript파일들, 이미지 css들을 하나의 모듈로 보고 배포용으로 병합하고 포장하는 작업을 함.
이 작업을 수행하는 툴들을 번들러라고 하는데, 번들러 중에 Webpack이 현재 시점에서 가장 인기가 많음.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[02] Three.js - camera]]></title>
            <link>https://velog.io/@dev_0livia/02-Three.js-camera</link>
            <guid>https://velog.io/@dev_0livia/02-Three.js-camera</guid>
            <pubDate>Mon, 23 Sep 2024 11:26:35 GMT</pubDate>
            <description><![CDATA[<h1 id="camera">Camera</h1>
<p><img src="https://velog.velcdn.com/images/dev_0livia/post/b1af575f-c157-455c-8c77-e8baa80795be/image.png" alt=""></p>
<h2 id="perspectivecamera">PerspectiveCamera</h2>
<blockquote>
<p>사람이 보는듯한 시야각이 있는 카메라 모드.</p>
</blockquote>
<h3 id="perspectivecamerafov-number-aspect-number-near-number-far-number">PerspectiveCamera(fov: Number, aspect: Number, near: Number, far: Number)</h3>
<ul>
<li><strong>fov (시야각)</strong>: 사람이 보는 시야각(field of view)
우리 눈으로 보는 것처럼, 카메라가 얼마나 넓게 볼 수 있는지를 결정한다.
<img src="https://velog.velcdn.com/images/dev_0livia/post/a1c44993-1757-4031-8906-8110b3fa8300/image.png" alt=""></li>
</ul>
<ul>
<li><strong>aspect (화면의 높이 및 너비)</strong>: 가로 세로 화면 비율. </li>
<li><strong>near (가까운 거리 제한)</strong>: 카메라가 볼 수 있는 가장 가까운 거리.
이 거리보다 더 가까이 있는 물체는 화면에 보이지 않는다. </li>
<li><strong>far (먼 거리 제한)</strong>: 카메라가 볼 수 있는 가장 먼 거리.
이 거리보다 더 멀리 있는 물체는 화면에 보이지 않는다.</li>
</ul>
<h4 id="span-stylebackground-colorc3e0a3여기서-물체는-near과-far-사이에-있고-fov-안에-들어와야지만-보인다-span"><span style="background-color:#c3e0a3">*<em>여기서 물체는 near과 far 사이에 있고 fov 안에 들어와야지만 보인다. *</em></span></h4>
<h3 id="설정">설정</h3>
<pre><code class="language-jsp">const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);

scene.add(camera)</code></pre>
<h3 id="카메라-위치의-중요성">카메라 위치의 중요성</h3>
<p>3D 공간에서 카메라는 우리의 <span style="color:#7ea454"><strong>눈 역할</strong></span>을 한다.
따라서 이렇게 설정한 카메라는 반드시 <span style="color:#7ea454"><strong>위치를 (position)설정</strong></span>해야 한다.
만약 카메라의 위치를 설정하지 않으면, 기본적으로 (0, 0, 0) 위치에 놓이게 되는데,
이 위치는 3D 공간의 중심점으로,  <span style="color:#7ea454"><strong>대부분의 물체들이 배치되는 곳이라 자칫하면 물체와 겹쳐지기 때문</strong></span>이다.
이렇게 되면 물체를 제대로 볼 수 없고, 화면에 아무것도 보이지 않을 수 있다.
따라서 <span style="background-color:#c3e0a3"><strong>카메라는 물체보다 조금 더 앞에 떨어진 곳에 위치</strong></span>해야 한다.</p>
<h4 id="일반적인-카메라-위치">일반적인 카메라 위치</h4>
<pre><code class="language-jsp">const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);

camera.position.z = 6; 혹은
camera.position.set(0, 1, 5)
scene.add(camera)</code></pre>
<p>일반적으로 카메라는 물체보다 약간 뒤쪽에 위치한다. 이는 z 값을 양수로 설정하여 구현할 수 있다.
또한 조금 위쪽에서 내려다보는 각도로 설정하는 것이 일반적이다. 이는 y 값을 약간 높게 설정하여 구현할 수 있다.
예를 들어, camera.position.set(0, 1, 5)와 같이 설정하면 물체보다 약간 위, 그리고 뒤쪽에 카메라가 위치하게 되는 것이다.</p>
<hr>
<h2 id="orthographiccamera">OrthographicCamera</h2>
<blockquote>
<p>물체의 크기, 카메라의 거리와는 상관 없이 일정하게 보이는 것. </p>
</blockquote>
<p>OrthographicCamera(left: Number, right: Number, top: Number, bottom: Number, near: Number, far: Number)</p>
]]></description>
        </item>
    </channel>
</rss>