<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>fever-max.log</title>
        <link>https://velog.io/</link>
        <description>선명한 삶을 살기 위하여</description>
        <lastBuildDate>Fri, 24 Apr 2026 02:23:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>fever-max.log</title>
            <url>https://velog.velcdn.com/images/fever-max/profile/c608eee7-bec6-46e1-8f2e-c9682f734cb5/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. fever-max.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/fever-max" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Docker에서 서브넷을 적용할 수 있을까?]]></title>
            <link>https://velog.io/@fever-max/Docker%EC%97%90%EC%84%9C-%EC%84%9C%EB%B8%8C%EB%84%B7%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@fever-max/Docker%EC%97%90%EC%84%9C-%EC%84%9C%EB%B8%8C%EB%84%B7%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Fri, 24 Apr 2026 02:23:37 GMT</pubDate>
            <description><![CDATA[<h2 id="배경">배경</h2>
<p>기존 서비스는 Windows 서버에서 JAR 파일을 직접 실행하는 방식으로 운영되고 있었다. 서버가 자주 불안정했고, Windows 환경 특성상 운영 자체가 불편한 점이 많았다. 결국 리뉴얼을 결정하면서 Linux + Docker 환경으로 전환하기로 했다.</p>
<p>전환을 준비하던 중 감사 일정이 잡혔다. 감사에서 가장 중요한 요구사항은 <strong>개인정보가 저장된 DB에 대한 접근 제한</strong>이었다.</p>
<p>요구사항을 정리하면:</p>
<ul>
<li>DB는 외부에서 직접 접속 불가</li>
<li>API 서버만 외부에 노출</li>
<li>나머지 서비스와 DB는 내부에서만 접근 가능</li>
<li>DB 접근은 반드시 서버 SSH 접속 후 컨테이너를 통해서만 가능</li>
</ul>
<hr>
<h2 id="서브넷subnet이란">서브넷(Subnet)이란?</h2>
<p>서브넷은 하나의 네트워크를 더 작은 단위로 나눈 것이다. 쉽게 말하면 <strong>같은 건물 안에서도 층별로 독립된 내선 전화망을 구성하는 것</strong>과 비슷하다. 같은 층(서브넷) 안에서는 자유롭게 통신할 수 있지만, 다른 층(서브넷)과는 별도의 경로를 통해야 한다.</p>
<p>예를 들어 <code>172.10.0.0/24</code> 라는 서브넷은:</p>
<ul>
<li><code>172.10.0.0</code> — 네트워크 주소</li>
<li><code>/24</code> — 앞 24비트가 네트워크 대역, 나머지 8비트가 호스트 대역</li>
<li>즉 <code>172.10.0.1</code> ~ <code>172.10.0.254</code> 범위의 IP를 사용할 수 있음</li>
</ul>
<hr>
<h2 id="docker-네트워크란">Docker 네트워크란?</h2>
<p>Docker는 컨테이너 간 통신을 위한 가상 네트워크를 직접 구성할 수 있다. 기본적으로 컨테이너를 실행하면 <code>bridge</code> 라는 기본 네트워크에 연결되는데, 이 경우 모든 컨테이너가 같은 네트워크에 있어 서로 통신이 가능하다.</p>
<p>커스텀 네트워크를 만들면:</p>
<ul>
<li>같은 네트워크 안의 컨테이너끼리만 통신 가능</li>
<li>컨테이너에 고정 IP 부여 가능</li>
<li>네트워크 단위로 격리 가능</li>
</ul>
<h3 id="서브넷을-docker에-적용할-수-있는-이유">서브넷을 Docker에 적용할 수 있는 이유</h3>
<p>전통적인 서버 환경에서 서브넷은 물리적인 네트워크 장비로 구성한다. 하지만 Docker는 소프트웨어적으로 가상 네트워크를 만들 수 있어서, 서브넷 개념을 컨테이너 레벨에서 그대로 구현할 수 있다.</p>
<p>즉 Docker 네트워크에 서브넷을 지정하면:</p>
<ul>
<li>같은 서브넷 안의 컨테이너끼리만 통신 가능</li>
<li>다른 서브넷의 컨테이너는 접근 불가</li>
<li>물리적인 네트워크 장비 없이 소프트웨어로 격리 가능</li>
</ul>
<p>이 특성을 활용해서 <strong>외부에 노출할 서비스(API)</strong> 와 <strong>내부에서만 동작할 서비스(Batch, DB 등)</strong> 를 분리했다.</p>
<hr>
<h2 id="적용">적용</h2>
<h3 id="네트워크-생성">네트워크 생성</h3>
<pre><code class="language-bash">docker network create \
  --driver bridge \
  --subnet 172.10.0.0/24 \
  --gateway 172.10.0.1 \
  app-network</code></pre>
<ul>
<li><code>--driver bridge</code> — 단일 호스트 내 컨테이너 간 통신</li>
<li><code>--subnet</code> — 네트워크 대역 지정</li>
<li><code>--gateway</code> — 게이트웨이 IP 지정</li>
</ul>
<h3 id="컨테이너-ip-고정">컨테이너 IP 고정</h3>
<p>각 컨테이너에 고정 IP를 부여했다. IP가 고정되어 있으면 컨테이너가 재시작되어도 IP가 유지되기 때문에 서비스 간 통신 설정이 흔들리지 않는다.</p>
<pre><code class="language-bash">docker run \
  --name app-api \
  --network app-network \
  --ip 172.10.0.11 \
  ...</code></pre>
<table>
<thead>
<tr>
<th>컨테이너</th>
<th>IP</th>
<th>외부 노출</th>
</tr>
</thead>
<tbody><tr>
<td>nginx</td>
<td>-</td>
<td>✅</td>
</tr>
<tr>
<td>app-api</td>
<td>172.10.0.11</td>
<td>❌ (nginx 통해서만)</td>
</tr>
<tr>
<td>app-a</td>
<td>172.10.0.12</td>
<td>❌</td>
</tr>
<tr>
<td>app-b</td>
<td>172.10.0.13</td>
<td>❌</td>
</tr>
<tr>
<td>app-mysql</td>
<td>172.10.0.10</td>
<td>❌</td>
</tr>
</tbody></table>
<h3 id="nginx를-통한-외부-노출">nginx를 통한 외부 노출</h3>
<p>기존 nginx 컨테이너가 있다면 app-network에 추가로 연결할 수 있다. nginx는 여러 네트워크에 동시에 연결될 수 있기 때문에 외부 네트워크와 app-network 사이의 리버스 프록시 역할을 할 수 있다.</p>
<pre><code class="language-bash">docker network connect app-network nginx</code></pre>
<pre><code>외부 요청 흐름
외부 → nginx → app-api (172.10.0.11)

내부에서만 접근 가능
app-a     (172.10.0.12)
app-b     (172.10.0.13)
app-mysql (172.10.0.10)</code></pre><h3 id="db-접근-제한">DB 접근 제한</h3>
<p>DB는 app-network 내부에서만 접근 가능하기 때문에 외부에서 직접 접속할 수 없다. 반드시 서버에 SSH로 접속한 후 컨테이너를 통해 접근해야 한다.</p>
<pre><code class="language-bash"># 서버 SSH 접속
ssh user@server-ip

# 컨테이너 진입 후 DB 접속
docker exec -it app-mysql bash
mysql -u root -p</code></pre>
<hr>
<h2 id="마무리">마무리</h2>
<p>처음에는 네트워크를 별도로 구성해야 한다는 게 번거롭게 느껴졌다. 그냥 포트를 열고 닫는 것만으로도 충분하지 않을까 싶었는데, 직접 구성해보니 생각보다 훨씬 명확하게 서비스를 격리할 수 있었다.</p>
<p>서브넷이라는 개념이 물리적인 네트워크 장비에서만 가능한 줄 알았는데, Docker에서 소프트웨어적으로 동일하게 구현할 수 있다는 게 인상적이었다.</p>
<p>다만 이 방식은 <strong>단일 호스트에서의 격리에 한정</strong>된다는 점을 염두에 두어야 한다. 서비스가 커지거나 멀티 호스트 환경이 필요해진다면 Docker 네트워크만으로는 한계가 있다. Kubernetes는 이런 네트워크 격리와 서비스 디스커버리를 훨씬 정교하게 다룰 수 있고, 실제로 이 부분이 Docker와 Kubernetes의 가장 큰 차이 중 하나라고 생각한다.</p>
<p>Docker 네트워크를 직접 구성해보면서 네트워크 격리의 개념을 이해할 수 있었고, 이게 Kubernetes에서는 어떻게 추상화되어 동작하는지 더 공부해보고 싶어졌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[실무에서 모니터링 스택 구축하기 (Prometheus + Grafana + Loki + Tempo)]]></title>
            <link>https://velog.io/@fever-max/%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%8A%A4%ED%83%9D-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-Prometheus-Grafana-Loki-Tempo</link>
            <guid>https://velog.io/@fever-max/%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%8A%A4%ED%83%9D-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-Prometheus-Grafana-Loki-Tempo</guid>
            <pubDate>Fri, 24 Apr 2026 02:00:08 GMT</pubDate>
            <description><![CDATA[<p>사내 서비스를 운영하면서 장애가 발생했을 때 원인을 파악하는 데 너무 많은 시간이 걸렸다. 로그를 일일이 뒤지고, 어느 시점에 문제가 생겼는지 추적하는 것 자체가 고역이었다. 그러던 중 모니터링 수업을 듣게 됐고, Prometheus, Grafana, Loki, Tempo 조합을 접하게 됐다.</p>
<p>각 도구의 역할을 간단히 정리하면:</p>
<table>
<thead>
<tr>
<th>도구</th>
<th>역할</th>
<th>질문</th>
</tr>
</thead>
<tbody><tr>
<td>Prometheus</td>
<td>메트릭 수집</td>
<td>얼마나? (CPU, 메모리, 응답시간)</td>
</tr>
<tr>
<td>Grafana</td>
<td>시각화 대시보드</td>
<td>한눈에 보기</td>
</tr>
<tr>
<td>Loki</td>
<td>로그 수집</td>
<td>무슨 일이?</td>
</tr>
<tr>
<td>Tempo</td>
<td>트레이스 수집</td>
<td>어디서 느려?</td>
</tr>
</tbody></table>
<p>이 4개가 함께 동작하면 장애 발생 시 <strong>언제, 어디서, 왜</strong> 를 빠르게 파악할 수 있다.</p>
<hr>
<h2 id="왜-elk가-아닌가">왜 ELK가 아닌가?</h2>
<p>로그 수집 하면 보통 ELK (Elasticsearch + Logstash + Kibana) 스택을 먼저 떠올린다. 실제로 많은 회사에서 사용하는 검증된 스택이기도 하다.</p>
<p>하지만 우리 인프라는 <strong>Traditional 3-Tier Architecture</strong> (Web - WAS - DB) 였다. Kubernetes 환경이 아니다 보니 ELK가 제공하는 강점들이 크게 필요하지 않았다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>ELK</th>
<th>Loki</th>
</tr>
</thead>
<tbody><tr>
<td>리소스</td>
<td>Heavy (Elasticsearch 메모리 소모 큼)</td>
<td>Lightweight</td>
</tr>
<tr>
<td>로그 저장 방식</td>
<td>로그 내용을 인덱싱</td>
<td>레이블만 인덱싱</td>
</tr>
<tr>
<td>Grafana 연동</td>
<td>별도 플러그인 필요</td>
<td>기본 지원</td>
</tr>
<tr>
<td>적합한 환경</td>
<td>Kubernetes, 대규모 분산 환경</td>
<td>소규모, 단순 인프라</td>
</tr>
<tr>
<td>학습 곡선</td>
<td>높음</td>
<td>낮음</td>
</tr>
</tbody></table>
<p>Grafana와 자연스럽게 연동되는 Loki, 메트릭 수집의 Prometheus, 트레이스의 Tempo까지 하나의 Grafana 대시보드에서 모두 확인할 수 있다는 점이 매력적이었다. ELK는 강력하지만 Traditional 3-Tier 소규모 서비스에서 Elasticsearch의 리소스 부담은 오버스펙에 가까웠다.</p>
<hr>
<h2 id="테스트-서버-구축">테스트 서버 구축</h2>
<p>먼저 테스트 서버에 4개 스택을 전부 올려봤다. docker-compose로 구성했고, Spring Boot 애플리케이션에 actuator와 OpenTelemetry agent를 붙여서 메트릭, 로그, 트레이스를 각각 수집했다.</p>
<pre><code class="language-yaml">services:
  prometheus:
    image: prom/prometheus:v3.5.1
    ports:
      - 9090:9090
  grafana:
    image: grafana/grafana:12.4.2
    ports:
      - 3000:3000
  loki:
    image: grafana/loki:3.4.2
    ports:
      - 3100:3100
  tempo:
    image: grafana/tempo:2.7.2
    ports:
      - 3200:3200
      - 4317:4317</code></pre>
<p>Grafana 대시보드에서 메트릭을 보고, 이상한 구간을 발견하면 Loki로 로그를 확인하고, Tempo로 트레이스를 추적하는 흐름이 생각보다 훨씬 강력했다. 테스트 서버에서는 모두 정상 동작했다.</p>
<hr>
<h2 id="spring-boot-연동">Spring Boot 연동</h2>
<p>모니터링 스택을 구성했다고 끝이 아니다. 애플리케이션에서 메트릭을 노출해야 Prometheus가 수집할 수 있다.</p>
<h3 id="의존성-추가">의존성 추가</h3>
<p><em>build.gradle</em></p>
<pre><code class="language-groovy">implementation &#39;org.springframework.boot:spring-boot-starter-actuator&#39;
implementation &#39;io.micrometer:micrometer-registry-prometheus&#39;</code></pre>
<p><em>pom.xml</em></p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
    &lt;groupId&gt;io.micrometer&lt;/groupId&gt;
    &lt;artifactId&gt;micrometer-registry-prometheus&lt;/artifactId&gt;
&lt;/dependency&gt;</code></pre>
<h3 id="applicationyml-설정">application.yml 설정</h3>
<pre><code class="language-yaml">management:
  endpoints:
    web:
      exposure:
        include: prometheus, health, info
  endpoint:
    prometheus:
      enabled: true
  metrics:
    tags:
      application: ${spring.application.name}</code></pre>
<h3 id="prometheusyml-scrape-설정">prometheus.yml scrape 설정</h3>
<p>Prometheus가 어떤 엔드포인트에서 메트릭을 가져올지 설정한다.</p>
<pre><code class="language-yaml">scrape_configs:
  - job_name: my-app
    metrics_path: /actuator/prometheus
    static_configs:
      - targets:
        - 192.168.0.1:8080
        labels:
          application: my-app</code></pre>
<hr>
<h2 id="production-서버-적용--내부망이라는-벽">Production 서버 적용 — 내부망이라는 벽</h2>
<p>문제는 Production 서버였다. Production 서버는 <strong>내부망</strong> 환경이었고, 외부와의 통신이 제한되어 있었다. VPN으로 접속하는 방식이었는데, Windows 서버 VPN 특성상 <strong>접속할 때마다 IP가 달라지는</strong> 문제가 있었다.</p>
<p>여기서 Prometheus와 Loki/Tempo의 근본적인 차이가 드러났다.</p>
<h3 id="pull-vs-push">Pull vs Push</h3>
<table>
<thead>
<tr>
<th>방식</th>
<th>도구</th>
<th>동작 방식</th>
</tr>
</thead>
<tbody><tr>
<td>Pull</td>
<td>Prometheus</td>
<td>모니터링 서버가 애플리케이션에서 메트릭을 <strong>가져옴</strong></td>
</tr>
<tr>
<td>Push</td>
<td>Loki, Tempo</td>
<td>애플리케이션이 수집 서버로 데이터를 <strong>보냄</strong></td>
</tr>
</tbody></table>
<p><strong>Prometheus</strong>는 Pull 방식이라 모니터링 서버에서 애플리케이션 엔드포인트로 주기적으로 요청을 보내서 메트릭을 가져온다. 즉 모니터링 서버가 애플리케이션 서버에 접근할 수 있으면 되기 때문에 내부망에서도 문제없이 동작했다.</p>
<p>반면 <strong>Loki와 Tempo</strong>는 Push 방식이라 애플리케이션이 직접 Loki/Tempo 서버로 데이터를 전송해야 한다. 내부망에서는 애플리케이션이 외부로 나갈 수가 없었고, 거기다 VPN IP가 매번 바뀌니 고정 엔드포인트 설정 자체가 불가능했다.</p>
<p>결국 Production 서버에는 <strong>Prometheus + Grafana + AlertManager</strong> 만 적용하기로 결정했다.</p>
<hr>
<h2 id="production-서버-구성--prometheus--grafana--alertmanager">Production 서버 구성 — Prometheus + Grafana + AlertManager</h2>
<h3 id="docker-compose-구성">docker-compose 구성</h3>
<pre><code class="language-yaml">services:
  prometheus:
    image: prom/prometheus:v3.5.1
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --web.enable-lifecycle
      - --storage.tsdb.retention.time=30d
      - --storage.tsdb.retention.size=10GB
      - --web.enable-remote-write-receiver
    restart: unless-stopped

  alertmanager:
    image: prom/alertmanager:v0.31.1
    command:
      - --config.file=/etc/alertmanager/alertmanager.yml
    restart: unless-stopped
    depends_on:
      - prometheus-msteams

  grafana:
    image: grafana/grafana:12.4.2
    restart: unless-stopped
    depends_on:
      - prometheus

  prometheus-msteams:
    image: quay.io/prometheusmsteams/prometheus-msteams:latest
    environment:
      - TEAMS_INCOMING_WEBHOOK_URL=https://your-teams-webhook-url
      - TEAMS_REQUEST_URI=alertmanager
    command:
      - -workflow-webhook
    restart: unless-stopped</code></pre>
<h3 id="alertmanager-teams-연동">AlertManager Teams 연동</h3>
<p>Teams webhook을 직접 AlertManager에 붙이는 건 지원이 안 돼서 <strong>prometheus-msteams</strong> 라는 중간 브릿지를 사용했다.</p>
<p><strong>흐름</strong></p>
<pre><code>Prometheus → AlertManager → prometheus-msteams → Teams</code></pre><p>처음에는 기존 Incoming Webhook 방식으로 연동하려 했는데, 2025년 이후 Teams Incoming Webhook 지원이 종료되면서 <strong>Power Automate Workflow Webhook</strong> 방식으로 변경해야 했다. Webhook URL을 Power Automate에서 생성하고 환경변수로 주입하면 된다.</p>
<p><em>alertmanager.yml</em></p>
<pre><code class="language-yaml">global:
  resolve_timeout: 5m
route:
  receiver: &#39;teams-notification&#39;
  group_by: [&#39;alertname&#39;, &#39;application&#39;]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 1h
receivers:
- name: &#39;teams-notification&#39;
  webhook_configs:
  - url: &#39;http://prometheus-msteams:2000/alertmanager&#39;
    send_resolved: true</code></pre>
<h3 id="알림-규칙-설정">알림 규칙 설정</h3>
<p>서비스 다운, CPU 과부하, JVM Heap 과부하 3가지 알림을 설정했다.</p>
<p><em>alert_rules.yml</em></p>
<pre><code class="language-yaml">groups:
  - name: 기본알람
    rules:
      - alert: ServiceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          description: &quot;[{{ $labels.application }}] 서비스 다운&quot;

      - alert: HighCpuUsage
        expr: process_cpu_usage * 100 &gt; 80
        for: 5m
        labels:
          severity: warning
        annotations:
          description: &quot;[{{ $labels.application }}] CPU {{ $value | printf \&quot;%.1f\&quot; }}% 초과&quot;

      - alert: HighHeapUsage
        expr: |
          (
            sum by(instance, application) (jvm_memory_used_bytes{area=&quot;heap&quot;})
            /
            sum by(instance, application) (jvm_memory_max_bytes{area=&quot;heap&quot;})
          ) * 100 &gt; 85
        for: 5m
        labels:
          severity: critical
        annotations:
          description: &quot;[{{ $labels.application }}] Heap {{ $value | printf \&quot;%.1f\&quot; }}% 초과&quot;</code></pre>
<hr>
<h2 id="test-vs-production-비교">Test vs Production 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Test 서버</th>
<th>Production 서버</th>
</tr>
</thead>
<tbody><tr>
<td>메트릭</td>
<td>Prometheus ✅</td>
<td>Prometheus ✅</td>
</tr>
<tr>
<td>로그</td>
<td>Loki ✅</td>
<td>❌ (내부망)</td>
</tr>
<tr>
<td>트레이스</td>
<td>Tempo ✅</td>
<td>❌ (내부망)</td>
</tr>
<tr>
<td>알림</td>
<td>AlertManager ✅</td>
<td>AlertManager ✅</td>
</tr>
<tr>
<td>시각화</td>
<td>Grafana ✅</td>
<td>Grafana ✅</td>
</tr>
</tbody></table>
<p>로그와 트레이스를 포기한 건 아쉬웠지만, Prometheus만으로도 CPU, 메모리, JVM Heap, 응답시간, 서비스 상태 등 핵심 메트릭은 충분히 수집할 수 있었다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>모니터링을 붙이고 나서 가장 크게 달라진 건 <strong>장애 대응 속도</strong>였다. 이전에는 VOC가 들어온 후에야 인지했다면, 이제는 알림으로 먼저 인지하고 Grafana 대시보드에서 어느 시점에 무슨 일이 있었는지 바로 확인할 수 있게 됐다.</p>
<p>완벽한 구성은 아니었지만, 제약된 환경에서도 할 수 있는 최선을 찾아서 적용한 경험이 됐다. 나중에 내부망 환경에서 Loki를 붙일 수 있는 방법을 더 찾아볼 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그 파일이 두 군데 남는 이유 (Logback과 catalina.out의 관계)]]></title>
            <link>https://velog.io/@fever-max/%EB%A1%9C%EA%B7%B8-%ED%8C%8C%EC%9D%BC%EC%9D%B4-%EB%91%90-%EA%B5%B0%EB%8D%B0-%EB%82%A8%EB%8A%94-%EC%9D%B4%EC%9C%A0-Logback%EA%B3%BC-catalina.out%EC%9D%98-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@fever-max/%EB%A1%9C%EA%B7%B8-%ED%8C%8C%EC%9D%BC%EC%9D%B4-%EB%91%90-%EA%B5%B0%EB%8D%B0-%EB%82%A8%EB%8A%94-%EC%9D%B4%EC%9C%A0-Logback%EA%B3%BC-catalina.out%EC%9D%98-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Fri, 24 Apr 2026 01:41:18 GMT</pubDate>
            <description><![CDATA[<p>평소에 로그를 볼 때 항상 이렇게 봤다.</p>
<pre><code class="language-bash">tail -f /logs/tomcat/catalina.out</code></pre>
<p>그러다 Logback 설정을 열어봤는데 파일이 여러 개였다.</p>
<pre><code>app.log
error.log
business.log</code></pre><p>&quot;어? 그럼 catalina.out이랑 app.log가 같은 내용 아니야?&quot;</p>
<p>이 의문에서 시작했다.</p>
<hr>
<h1 id="tomcat-로그-종류부터-정리해야-한다">Tomcat 로그 종류부터 정리해야 한다</h1>
<p>로그 파일이 여러 개 생기는데, 각자 역할이 다르다.</p>
<p><strong>AccessLogValve</strong>는 HTTP 요청 기록이다.</p>
<p>server.xml에서 설정한다.</p>
<pre><code class="language-xml"></code></pre>
<p>찍히는 로그는 이렇다.</p>
<pre><code>192.168.1.10 - - [15/Jan/2024:10:23:45 +0900] &quot;GET /api/orders HTTP/1.1&quot; 200 1234</code></pre><p>누가 뭘 요청했고 응답코드가 뭔지만 찍힌다.
앱 내부에서 뭔 일이 있었는지는 모른다.</p>
<p><strong>catalina.out</strong>은 Tomcat 표준출력이다.</p>
<p>setenv.sh에서 경로를 지정한다.</p>
<pre><code class="language-bash">CATALINA_OUT=&quot;/data/tomcat/logs/catalina.out&quot;</code></pre>
<p>JVM이 콘솔에 찍는 것들이 전부 여기 쌓인다.</p>
<p><strong>Logback</strong>은 앱 내부 로직 기록이다.</p>
<p>개발자가 log.info(), log.error()로 직접 찍은 것들이다.</p>
<hr>
<h1 id="그래서-두-군데-찍히는-게-맞나">그래서 두 군데 찍히는 게 맞나</h1>
<p>맞다.</p>
<p>원인은 ConsoleAppender 때문이다.</p>
<pre><code class="language-xml">

    /data/tomcat/logs/app.log
</code></pre>
<p>흐름이 이렇다.</p>
<pre><code>log.info(&quot;주문 생성: orderId=1234&quot;)
         │
         ├──► STDOUT(ConsoleAppender) → catalina.out  ← 여기도 찍힘
         ├──► DEFAULT(FileAppender)  → app.log        ← 여기도 찍힘
         └──► ERROR(LevelFilter)     → error.log      ← ERROR만</code></pre><p>ConsoleAppender가 콘솔로 출력하면 Tomcat이 그걸 catalina.out으로 받아버린다.</p>
<p>catalina.out이랑 app.log는 같은 내용이 중복으로 쌓이고 있는 거다.</p>
<hr>
<h1 id="그럼-왜-굳이-파일을-따로-빼나">그럼 왜 굳이 파일을 따로 빼나</h1>
<p>catalina.out은 Tomcat이 관리하는 파일이라 Logback이 직접 rotate를 못 한다.</p>
<p>DEFAULT appender로 따로 빼면 Logback이 직접 관리할 수 있다.</p>
<pre><code class="language-xml">
    /data/tomcat/logs/backup/app.%d{yyyy-MM-dd}.%i.gz
    100MB
    90
    2GB
</code></pre>
<p>100MB 넘으면 자동으로 압축해서 backup 폴더로 보내고,
90일치 보관하고, 전체 2GB 넘으면 오래된 것부터 지운다.</p>
<p>catalina.out은 이런 관리가 안 된다.
그래서 별도로 logrotate를 써야 한다.</p>
<hr>
<h1 id="catalinaout은-logrotate로-관리한다">catalina.out은 logrotate로 관리한다</h1>
<p>Ubuntu 기준으로 logrotate는 cron이 아니라 systemd timer로 돌아간다.</p>
<pre><code class="language-bash"># /lib/systemd/system/logrotate.timer
OnCalendar=daily</code></pre>
<p>흐름은 이렇다.</p>
<pre><code>systemd timer (매일)
    │
    └──► /etc/logrotate.conf
              │
              └──► include /etc/logrotate.d/
                        │
                        └──► tomcat
                                  │
                                  └──► catalina.out rotate 실행</code></pre><p>/etc/logrotate.d/ 폴더에 파일만 넣으면 자동으로 포함된다.</p>
<p>설정은 이렇다.</p>
<pre><code class="language-bash">/data/tomcat/logs/catalina.out {
    copytruncate   # Tomcat 재시작 없이 rotate
    daily          # 매일
    rotate 60      # 60일치 보관
    compress       # gzip 압축
    delaycompress  # 가장 최근 파일은 압축 안함
    missingok      # 파일 없어도 에러 안남
    dateext        # 파일명에 날짜 붙임
}</code></pre>
<p>여기서 핵심은 copytruncate다.</p>
<p>일반 rotate는 파일을 옮기고 새 파일을 만드는데,
Tomcat은 파일 경로를 고정으로 잡고 있어서 파일이 바뀌면 못 찾는다.</p>
<p>copytruncate는 파일을 복사한 다음 원본을 비워버리는 방식이라
Tomcat 재시작 없이 rotate가 가능하다.</p>
<hr>
<h1 id="마무리">마무리</h1>
<p>로그 구조를 정리하면 이렇다.</p>
<pre><code>HTTP 요청
    │
    ├──► AccessLogValve → localhost_access_log.txt
    │
    └──► Spring App
              │
              ├──► ConsoleAppender → catalina.out  (logrotate)
              ├──► DEFAULT         → app.log       (Logback 관리)
              └──► ERROR           → error.log     (ERROR만)</code></pre><p>결국 catalina.out이랑 app.log는 중복이 맞다.</p>
<p>근데 각자 관리 주체가 다르다.</p>
<p>catalina.out은 Tomcat이 들고 있어서 logrotate로 밖에 못 건드리고,
app.log는 Logback이 직접 들고 있어서 세밀하게 관리할 수 있다.</p>
<p>같은 내용이 두 군데 찍히는 게 버그가 아니라
<strong>관리 주체가 다른 두 채널이 동시에 받고 있는 구조</strong>였던 거다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB 커넥션 풀은 어떻게 동작하는가 (HikariCP 내부 구조)]]></title>
            <link>https://velog.io/@fever-max/DB-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B0%80-HikariCP-%EB%82%B4%EB%B6%80-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@fever-max/DB-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B0%80-HikariCP-%EB%82%B4%EB%B6%80-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Wed, 01 Apr 2026 09:26:19 GMT</pubDate>
            <description><![CDATA[<p>백엔드 개발하다 보면 한 번쯤 이런 상황 겪는다.</p>
<p>“DB도 정상이고 CPU도 널널한데, 왜 API만 느리지?”</p>
<p>이럴 때 의외로 많이 걸리는 게 커넥션 풀이다.
나도 처음엔 maxPoolSize만 늘리면 해결되는 줄 알았는데, 구조를 이해하고 나니까 문제 보는 시선이 완전히 달라졌다.</p>
<p>이 글은 설정값 얘기 말고, <strong>HikariCP가 실제로 어떻게 동작하는지</strong>를 흐름 기준으로 정리한다.</p>
<hr>
<h1 id="커넥션-풀은-단순-재사용이-아니다">커넥션 풀은 단순 재사용이 아니다</h1>
<p>커넥션 풀을 흔히 “미리 만들어두고 재사용”이라고 생각하기 쉬운데, 실제 역할은 그보다 더 중요하다.</p>
<p>커넥션 풀은 결국 <strong>동시에 DB에 붙을 수 있는 수를 제한하는 장치</strong>다.</p>
<p>요청이 100개 들어와도 커넥션이 10개면, 동시에 처리 가능한 건 10개뿐이다.
나머지는 무조건 기다린다.</p>
<p>그래서 커넥션 풀은 성능을 올리는 도구라기보다
<strong>시스템이 감당 가능한 처리량을 강제로 맞추는 장치</strong>에 가깝다.</p>
<h1 id="hikaricp-구조를-보면-왜-빠른지-이해된다">HikariCP 구조를 보면 왜 빠른지 이해된다</h1>
<p>HikariCP 내부 구조는 단순한 큐 기반이 아니다.</p>
<pre><code class="language-text">Application Thread
    ↓
HikariPool
    ↓
ConcurrentBag
    ↓
Connection</code></pre>
<p>여기서 핵심은 ConcurrentBag이다.</p>
<p>일반적인 풀처럼 큐에 넣고 빼는 방식이 아니라,
각 커넥션이 상태를 가지고 있고 스레드가 그 상태를 바꾸면서 가져가는 구조다.</p>
<p>이 방식의 목적은 하나다.</p>
<p>락을 최대한 피하고, 동시에 여러 스레드가 접근해도 병목이 생기지 않게 만드는 것.</p>
<h1 id="왜-굳이-이런-구조를-썼을까">왜 굳이 이런 구조를 썼을까</h1>
<p>일반적으로는 BlockingQueue를 써서 풀을 만든다.
구현도 쉽고 직관적이다.</p>
<p>근데 트래픽이 늘어나면 문제가 생긴다.</p>
<p>스레드가 많아질수록 lock 경쟁이 심해지고,
대기와 컨텍스트 스위칭이 늘어나면서 전체 성능이 떨어진다.</p>
<p>HikariCP는 이 문제를 피하려고 아래 3가지를 중심으로 설계했다.</p>
<ul>
<li>가능한 한 lock을 쓰지 않는다</li>
<li>상태 변경은 CAS 방식으로 처리한다</li>
<li>같은 스레드는 같은 커넥션을 다시 쓰도록 유도한다</li>
</ul>
<p>이 세 가지가 합쳐지면서, 요청이 많아져도 성능이 크게 떨어지지 않는다.</p>
<h1 id="커넥션을-가져오는-실제-흐름">커넥션을 가져오는 실제 흐름</h1>
<p>HikariCP에서 커넥션을 얻는 과정은 생각보다 단순하다.</p>
<pre><code class="language-text">1. ThreadLocal에서 먼저 확인
2. 있으면 그대로 사용
3. 없으면 ConcurrentBag에서 탐색
4. 사용 가능한 커넥션이 있으면 가져옴
5. 없으면 새로 생성 (가능한 경우)
6. 최대치면 대기</code></pre>
<p>여기서 중요한 포인트는 ThreadLocal이다.</p>
<p>같은 스레드는 이전에 사용했던 커넥션을 우선적으로 다시 사용하려고 한다.
이 경우 풀 전체를 탐색하지 않기 때문에 거의 비용이 발생하지 않는다.</p>
<p>결과적으로 반복 요청이 많은 환경에서는
커넥션을 가져오는 비용 자체가 거의 사라진다.</p>
<h1 id="성능을-결정하는-건-커넥션-수가-아니다">성능을 결정하는 건 커넥션 수가 아니다</h1>
<p>많이 하는 실수가 하나 있다.</p>
<p>“느리니까 maxPoolSize를 늘리자”</p>
<p>근데 대부분 이걸로 해결 안 된다.</p>
<p>진짜 중요한 건 커넥션 개수가 아니라
<strong>커넥션을 얼마나 오래 잡고 있느냐</strong>다.</p>
<p>예를 들어 커넥션이 10개 있고, 각 요청이 1초씩 잡고 있으면
초당 최대 10개밖에 처리 못 한다.</p>
<p>여기서 트랜잭션 안에 외부 API 호출이 들어가면 상황이 더 나빠진다.</p>
<p>DB 작업이 끝났는데도 커넥션을 계속 잡고 있기 때문이다.</p>
<p>이러면 풀을 아무리 늘려도 계속 밀린다.</p>
<h1 id="커넥션-풀이-터지는-순간">커넥션 풀이 터지는 순간</h1>
<p>모든 커넥션이 사용 중이면 이후 요청은 전부 대기한다.</p>
<pre><code class="language-text">모든 커넥션 사용 중
→ 요청 대기
→ 응답 지연
→ timeout</code></pre>
<p>이 상태가 되면 특징이 있다.</p>
<p>CPU는 정상이고, DB도 정상인데 API만 느리다.</p>
<p>이럴 때 커넥션 풀을 의심해야 한다.</p>
<h1 id="maxpoolsize는-많다고-좋은-게-아니다">maxPoolSize는 많다고 좋은 게 아니다</h1>
<p>maxPoolSize를 늘리면 무조건 좋아질 것 같지만, 실제로는 그렇지 않다.</p>
<p>DB도 동시에 처리할 수 있는 한계가 있고,
커넥션이 많아질수록 스레드 간 경쟁과 컨텍스트 스위칭이 늘어난다.</p>
<p>결국 일정 수준을 넘으면 오히려 성능이 떨어진다.</p>
<p>중요한 건 단순히 늘리는 게 아니라
<strong>DB가 감당 가능한 수준에 맞추는 것</strong>이다.</p>
<h1 id="마무리">마무리</h1>
<p>처음에는 커넥션 풀을 그냥 설정값으로만 봤는데,
구조를 이해하고 나니까 문제를 보는 기준이 완전히 달라졌다.</p>
<p>이제는 API가 느려지면 먼저 본다.</p>
<p>“지금 커넥션을 누가 오래 잡고 있지?”</p>
<p>이 질문이 나오기 시작하면,
단순 튜닝이 아니라 구조를 이해한 상태다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[세션은 어떻게 유지되는가 (JSESSIONID의 역할)]]></title>
            <link>https://velog.io/@fever-max/%EC%84%B8%EC%85%98%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9C%A0%EC%A7%80%EB%90%98%EB%8A%94%EA%B0%80-JSESSIONID%EC%9D%98-%EC%97%AD%ED%95%A0</link>
            <guid>https://velog.io/@fever-max/%EC%84%B8%EC%85%98%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9C%A0%EC%A7%80%EB%90%98%EB%8A%94%EA%B0%80-JSESSIONID%EC%9D%98-%EC%97%AD%ED%95%A0</guid>
            <pubDate>Thu, 19 Mar 2026 08:26:54 GMT</pubDate>
            <description><![CDATA[<p>웹 개발을 하다 보면 세션은 너무 당연하게 사용된다.<br>로그인을 유지하고, 사용자 상태를 기억하는 데 필수적인 요소다.</p>
<p>하지만 실제로 세션이 어떻게 동작하는지,<br>특히 멀티 WAS 환경에서 어떤 문제가 발생하는지 이해하고 있는 경우는 많지 않다.</p>
<p>이 글에서는 JSESSIONID를 중심으로<br>세션의 내부 동작과 구조를 조금 더 깊게 정리한다.</p>
<hr>
<h2 id="세션의-본질">세션의 본질</h2>
<p>세션은 단순히 말하면 다음과 같다.</p>
<ul>
<li>서버에 저장된 사용자 상태</li>
<li>요청 간 상태를 유지하기 위한 장치</li>
</ul>
<p>HTTP는 stateless(무상태)이기 때문에<br>요청마다 사용자를 식별할 방법이 필요하다.</p>
<p>이때 사용하는 것이 세션이다.</p>
<hr>
<h2 id="jsessionid의-역할">JSESSIONID의 역할</h2>
<p>세션은 서버에 저장되지만,<br>클라이언트는 자신의 세션을 어떻게 식별할까?</p>
<p>그 역할을 하는 것이 JSESSIONID다.</p>
<h3 id="전체-흐름">전체 흐름</h3>
<ol>
<li>클라이언트 최초 요청</li>
<li>서버에서 세션 생성</li>
<li>세션 ID 생성 (예: ABC123)</li>
<li>응답에 쿠키로 전달</li>
</ol>
<pre><code>Set-Cookie: JSESSIONID=ABC123</code></pre><ol start="5">
<li>이후 요청마다 자동 포함</li>
</ol>
<pre><code>Cookie: JSESSIONID=ABC123</code></pre><ol start="6">
<li>서버는 해당 ID로 세션 조회</li>
</ol>
<hr>
<h2 id="세션-저장-위치">세션 저장 위치</h2>
<p>세션은 일반적으로 다음 위치에 저장된다.</p>
<h3 id="1-was-메모리-기본">1. WAS 메모리 (기본)</h3>
<ul>
<li>Tomcat, Tomee 등</li>
<li>가장 빠름</li>
<li>서버마다 따로 존재</li>
</ul>
<h3 id="2-외부-저장소">2. 외부 저장소</h3>
<ul>
<li>Redis</li>
<li>DB</li>
</ul>
<p>멀티 서버 환경에서 사용</p>
<hr>
<h2 id="중요한-오해">중요한 오해</h2>
<p>많이 하는 착각이 있다.</p>
<blockquote>
<p>JSESSIONID에 사용자 정보가 들어있다?</p>
</blockquote>
<p>아니다.</p>
<ul>
<li>JSESSIONID = 단순한 키</li>
<li>실제 데이터 = 서버 내부</li>
</ul>
<p>즉, 클라이언트는 아무 정보도 가지고 있지 않다.</p>
<hr>
<h2 id="단일-was-환경">단일 WAS 환경</h2>
<pre><code>[Client] → [WAS1]</code></pre><ul>
<li>세션 생성</li>
<li>JSESSIONID로 계속 접근</li>
</ul>
<p>문제 없음</p>
<hr>
<h2 id="멀티-was-환경-문제">멀티 WAS 환경 문제</h2>
<pre><code>[Client] → [LB] → [WAS1 / WAS2]</code></pre><h3 id="시나리오">시나리오</h3>
<ol>
<li>로그인 → WAS1 → 세션 생성</li>
<li>다음 요청 → WAS2</li>
</ol>
<p>결과:</p>
<ul>
<li>세션 없음</li>
<li>로그인 풀림</li>
</ul>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="1-sticky-session">1. Sticky Session</h3>
<ul>
<li>LB가 특정 WAS로 고정</li>
</ul>
<p>장점:</p>
<ul>
<li>간단</li>
</ul>
<p>단점:</p>
<ul>
<li>확장성 낮음</li>
<li>장애 시 문제</li>
</ul>
<hr>
<h3 id="2-session-replication">2. Session Replication</h3>
<ul>
<li>WAS끼리 세션 공유</li>
</ul>
<p>단점:</p>
<ul>
<li>네트워크 비용 큼</li>
<li>성능 저하</li>
</ul>
<hr>
<h3 id="3-redis-기반-세션">3. Redis 기반 세션</h3>
<ul>
<li>외부 저장소 사용</li>
</ul>
<p>구조:</p>
<pre><code>Client → LB → WAS → Redis</code></pre><p>장점:</p>
<ul>
<li>확장성 좋음</li>
<li>안정적</li>
</ul>
<hr>
<h2 id="jsessionid--jvm-route">JSESSIONID + JVM Route</h2>
<p>클러스터 환경에서는 이런 형태도 사용한다.</p>
<pre><code>JSESSIONID=ABC123.node1</code></pre><p>여기서</p>
<ul>
<li>ABC123 → 세션 ID</li>
<li>node1 → 서버 식별자</li>
</ul>
<p>LB가 이 값을 보고 특정 서버로 라우팅</p>
<hr>
<h2 id="실무에서-자주-터지는-문제">실무에서 자주 터지는 문제</h2>
<h3 id="1-로그인-유지-안됨">1. 로그인 유지 안됨</h3>
<ul>
<li>멀티 WAS</li>
<li>세션 공유 없음</li>
</ul>
<hr>
<h3 id="2-특정-api에서만-세션-유실">2. 특정 API에서만 세션 유실</h3>
<ul>
<li>다른 서버로 라우팅됨</li>
</ul>
<hr>
<h3 id="3-외부-콜백-이후-세션-사라짐">3. 외부 콜백 이후 세션 사라짐</h3>
<ul>
<li>도메인 변경</li>
<li>쿠키 미전달</li>
</ul>
<hr>
<h2 id="쿠키와-보안">쿠키와 보안</h2>
<p>JSESSIONID는 쿠키이기 때문에<br>보안 설정이 중요하다.</p>
<ul>
<li>HttpOnly</li>
<li>Secure</li>
<li>SameSite</li>
</ul>
<p>설정에 따라</p>
<ul>
<li>XSS 방지</li>
<li>CSRF 대응</li>
</ul>
<p>가능</p>
<hr>
<h2 id="왜-이런-구조일까">왜 이런 구조일까?</h2>
<p>HTTP는 stateless이기 때문에</p>
<ul>
<li>서버가 상태를 기억하려면</li>
<li>클라이언트가 식별자를 보내야 한다</li>
</ul>
<p>그래서</p>
<ul>
<li>서버 → 세션 저장</li>
<li>클라이언트 → ID만 보관</li>
</ul>
<p>이 구조가 만들어졌다.</p>
<hr>
<h2 id="한-단계-더">한 단계 더</h2>
<p>세션 방식 외에도 다른 방식이 있다.</p>
<ul>
<li>JWT (Stateless)</li>
<li>Token 기반 인증</li>
</ul>
<p>세션은 상태 기반(stateful),<br>JWT는 무상태(stateless) 방식이다.</p>
<hr>
<h2 id="정리">정리</h2>
<ul>
<li>세션은 서버에 저장된다</li>
<li>JSESSIONID는 그 세션을 찾는 키다</li>
<li>단일 서버에서는 단순하다</li>
<li>멀티 서버에서는 전략이 필요하다</li>
</ul>
<hr>
<h2 id="한줄-요약">한줄 요약</h2>
<p>세션은 서버에 있고,<br>JSESSIONID는 그 세션을 찾기 위한 식별자다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[auto increment인데 duplicate key 나는 이유]]></title>
            <link>https://velog.io/@fever-max/auto-increment%EC%9D%B8%EB%8D%B0-duplicate-key-%EB%82%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@fever-max/auto-increment%EC%9D%B8%EB%8D%B0-duplicate-key-%EB%82%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Thu, 19 Mar 2026 08:21:11 GMT</pubDate>
            <description><![CDATA[<p>백엔드 개발을 하다 보면 한 번쯤 이런 에러를 보게 된다.</p>
<pre><code>ERROR: duplicate key value violates unique constraint
Key (id)=(4) already exists</code></pre><p>auto increment인데 왜 중복이 발생할까?</p>
<p>처음에는 DB가 자동으로 값을 잘 관리해줄 거라고 생각하기 쉽다.<br>하지만 실제로는 그렇지 않다.</p>
<p>결론부터 말하면<br><strong>시퀀스는 테이블 상태를 전혀 모른다.</strong></p>
<hr>
<h2 id="auto-increment의-정체">auto increment의 정체</h2>
<p>PostgreSQL에서 auto increment는 내부적으로 다음과 같이 동작한다.</p>
<pre><code class="language-sql">nextval(&#39;sequence_name&#39;)</code></pre>
<p>이 함수는 단순히 숫자를 하나씩 증가시키며 반환한다.</p>
<p>중요한 점은 다음이다.</p>
<ul>
<li>테이블을 조회하지 않는다</li>
<li>현재 PK 상태를 확인하지 않는다</li>
<li>단순히 숫자만 증가시킨다</li>
</ul>
<p>즉, auto increment는 “똑똑한 증가 기능”이 아니라<br><strong>독립적인 숫자 생성기</strong>에 가깝다.</p>
<hr>
<h2 id="duplicate-key가-발생하는-구조">duplicate key가 발생하는 구조</h2>
<p>다음과 같은 상황을 가정해보자.</p>
<h3 id="현재-상태">현재 상태</h3>
<p>테이블 데이터:
id = 1, 2, 3, 4, 5</p>
<p>시퀀스 상태:
nextval = 4</p>
<p>이 상태에서 insert를 수행하면</p>
<pre><code class="language-sql">INSERT INTO table (...) VALUES (...);</code></pre>
<p>실제로는 내부적으로 다음과 같이 처리된다.</p>
<pre><code class="language-sql">id = nextval(...)
→ 4</code></pre>
<p>이미 테이블에 존재하는 값이기 때문에<br>duplicate key 에러가 발생한다.</p>
<hr>
<h2 id="시퀀스가-꼬이는-이유">시퀀스가 꼬이는 이유</h2>
<p>이런 상황은 의외로 자주 발생한다.</p>
<h3 id="1-수동으로-pk를-넣은-경우">1. 수동으로 PK를 넣은 경우</h3>
<pre><code class="language-sql">INSERT INTO table(id, ...) VALUES (100, ...);</code></pre>
<p>테이블에는 100이 들어가지만<br>시퀀스는 이 사실을 모른다.</p>
<p>이후 시퀀스는 기존 값 기준으로 계속 증가하게 되고<br>결국 충돌이 발생한다.</p>
<hr>
<h3 id="2-dump--restore">2. dump / restore</h3>
<p>데이터를 백업했다가 복구할 때</p>
<ul>
<li>데이터는 정상적으로 들어오지만</li>
<li>시퀀스 값은 초기 상태로 남아있는 경우가 많다</li>
</ul>
<p>실무에서 가장 흔한 원인이다.</p>
<hr>
<h3 id="3-서버-간-데이터-이관">3. 서버 간 데이터 이관</h3>
<p>운영 환경의 데이터를 개발 환경으로 복사할 때</p>
<ul>
<li>테이블 데이터만 복사</li>
<li>시퀀스 값은 그대로</li>
</ul>
<p>이 경우에도 쉽게 문제가 발생한다.</p>
<hr>
<h3 id="4-truncate에-대한-오해">4. truncate에 대한 오해</h3>
<pre><code class="language-sql">TRUNCATE TABLE table;</code></pre>
<p>이 명령은 데이터를 삭제할 뿐<br>시퀀스 값은 초기화하지 않는다.</p>
<p>그래서 이후 insert 시 예상과 다른 값이 들어갈 수 있다.</p>
<hr>
<h3 id="5-트랜잭션-롤백">5. 트랜잭션 롤백</h3>
<pre><code class="language-sql">SELECT nextval(&#39;seq&#39;); -- 10
ROLLBACK;</code></pre>
<p>이 경우 많은 사람들이<br>값이 롤백될 것이라고 생각한다.</p>
<p>하지만 시퀀스는 트랜잭션과 무관하다.</p>
<ul>
<li>10은 이미 사용된 값</li>
<li>다시 되돌아가지 않는다</li>
</ul>
<p>그래서 중간 숫자가 비는 것은 정상적인 동작이다.</p>
<hr>
<h2 id="왜-이렇게-설계되어-있을까">왜 이렇게 설계되어 있을까</h2>
<p>시퀀스가 테이블과 독립적으로 동작하는 이유는 성능 때문이다.</p>
<p>만약 시퀀스가 테이블 상태를 매번 확인한다면</p>
<ul>
<li>락이 발생하고</li>
<li>동시성이 크게 떨어진다</li>
</ul>
<p>그래서 DB는 단순한 구조를 선택했다.</p>
<ul>
<li>숫자를 빠르게 발급</li>
<li>충돌 여부는 제약조건으로 처리</li>
</ul>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<p>문제가 발생했다면 시퀀스를 테이블에 맞춰주면 된다.</p>
<pre><code class="language-sql">SELECT setval(
  &#39;sequence_name&#39;,
  (SELECT MAX(id) FROM table)
);</code></pre>
<p>이렇게 하면 시퀀스가 현재 데이터 기준으로 재정렬된다.</p>
<hr>
<h2 id="실무에서의-대응-방법">실무에서의 대응 방법</h2>
<h3 id="1-데이터-이관-후-시퀀스-정렬">1. 데이터 이관 후 시퀀스 정렬</h3>
<p>데이터를 복사하거나 복구했다면<br>반드시 시퀀스를 맞춰야 한다.</p>
<h3 id="2-pk-수동-입력-지양">2. PK 수동 입력 지양</h3>
<p>특별한 이유가 없다면<br>auto increment에 맡기는 것이 안전하다.</p>
<h3 id="3-duplicate-key-발생-시-우선-확인">3. duplicate key 발생 시 우선 확인</h3>
<p>코드 문제로 보기 전에<br>시퀀스 상태를 먼저 확인하는 것이 빠르다.</p>
<hr>
<h2 id="정리">정리</h2>
<p>auto increment는 테이블을 기준으로 동작하는 기능이 아니다.</p>
<ul>
<li>테이블 상태를 모른다</li>
<li>트랜잭션과 무관하다</li>
<li>단순히 숫자만 증가시킨다</li>
</ul>
<p>이 구조를 이해하지 못하면<br>예상하지 못한 duplicate key 문제를 만나게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring @Transactional이 안 먹는 이유 (self-invocation 때문에 2시간 날린 썰)]]></title>
            <link>https://velog.io/@fever-max/Spring-Transactional%EC%9D%B4-%EC%95%88-%EB%A8%B9%EB%8A%94-%EC%9D%B4%EC%9C%A0-self-invocation-%EB%95%8C%EB%AC%B8%EC%97%90-2%EC%8B%9C%EA%B0%84-%EB%82%A0%EB%A6%B0-%EC%8D%B0</link>
            <guid>https://velog.io/@fever-max/Spring-Transactional%EC%9D%B4-%EC%95%88-%EB%A8%B9%EB%8A%94-%EC%9D%B4%EC%9C%A0-self-invocation-%EB%95%8C%EB%AC%B8%EC%97%90-2%EC%8B%9C%EA%B0%84-%EB%82%A0%EB%A6%B0-%EC%8D%B0</guid>
            <pubDate>Fri, 20 Feb 2026 02:39:34 GMT</pubDate>
            <description><![CDATA[<p>Spring에서 @Transactional을 붙였는데
분명 예외가 발생했는데도 롤백이 되지 않는 경험을 한 적이 있다.</p>
<p>처음에는 MyBatis 문제인가 싶었고,
DB 설정 문제인가 싶었고,
격하게 삽질을 했다.</p>
<p>결론은 단순했다.</p>
<blockquote>
<p>트랜잭션이 안 먹은 게 아니라, 프록시를 타지 않았다.</p>
</blockquote>
<h1 id="문제-상황">문제 상황</h1>
<p>다음과 같은 코드였다. (예시 코드)</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class OrderApplication {

    private final OrderService orderService;
    private final OrderResultPublisher publisher;

    public void orderGenerateWithPublish(OrderVo orderVo) {
        OrderMessage message = generateOrder(orderVo);
        publisher.send(message);
    }

    @Transactional
    public OrderMessage generateOrder(OrderVo orderVo) {

        if (orderService.existsByOrderNumber(orderVo.getOrderNumber())) {
            orderService.deleteByOrderNumber(orderVo.getOrderNumber());
        }

        Order order = orderService.save(orderVo.toEntity());

        // 강제 예외
        if (true) {
            throw new RuntimeException(&quot;예외 발생&quot;);
        }

        return OrderMessage.from(order);
    }
}</code></pre>
<p>의도는 단순했다.</p>
<ul>
<li>주문 저장</li>
<li>예외 발생 시 롤백</li>
<li>트랜잭션 보장</li>
</ul>
<p>하지만 실제로는
삭제도 되고 insert도 되고 예외도 터지는데 롤백이 되지 않았다.</p>
<h1 id="원인">원인</h1>
<p>문제는 이 한 줄이었다.</p>
<pre><code class="language-java">OrderMessage message = generateOrder(orderVo);</code></pre>
<p>같은 클래스 내부에서 this 로 메서드를 호출했다.
이게 왜 문제일까?</p>
<blockquote>
<p>Spring 트랜잭션은 프록시 기반이다.</p>
</blockquote>
<p>Spring의 <code>@Transactional</code>은 AOP 기반으로 동작한다.</p>
<p>우리가 주입받는 Bean은 실제 객체가 아니라
트랜잭션 처리를 감싸고 있는 프록시 객체다.</p>
<p>구조를 단순화하면 다음과 같다.</p>
<blockquote>
<p>[Proxy 객체] → [실제 Bean]</p>
</blockquote>
<p>메서드를 호출하면 프록시가 먼저 실행되고
여기서 트랜잭션을 시작한다.</p>
<p>하지만 같은 클래스 내부에서 다른 메서드를 호출하면, </p>
<pre><code class="language-java">generateOrder();</code></pre>
<ul>
<li>프록시를 거치지 않는다</li>
<li>그냥 자기 자신 메서드를 호출한다</li>
<li>트랜잭션 AOP가 실행되지 않는다</li>
</ul>
<blockquote>
<p>즉, 트랜잭션이 안 먹은 게 아니라, 애초에 트랜잭션 로직이 실행되지 않은 것이다.</p>
</blockquote>
<h2 id="트랜잭션이-동작하지-않는-구조">트랜잭션이 동작하지 않는 구조</h2>
<pre><code class="language-java">public void outer() {
    this.inner(); // 프록시 안 거침
}

@Transactional
public void inner() {
}</code></pre>
<p>이 구조에서는 <code>@Transactional</code>이 적용되지 않는다.</p>
<h2 id="정상-동작-구조">정상 동작 구조</h2>
<p>다른 Bean을 통해 호출해야 한다.</p>
<pre><code class="language-java">@Service
public class OrderTxService {

    @Transactional
    public void generate() {
        // 트랜잭션 보장
    }
}</code></pre>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class OrderApplication {

    private final OrderTxService orderTxService;

    public void outer() {
        orderTxService.generate(); // 프록시 경유
    }
}</code></pre>
<p>이렇게 하면 프록시를 거치기 때문에 트랜잭션이 정상 동작한다.</p>
<h2 id="추가로-많이-헷갈리는-포인트">추가로 많이 헷갈리는 포인트</h2>
<h3 id="1-private과-transactional">1. private과 @Transactional</h3>
<pre><code class="language-java">@Transactional
private void save() {}</code></pre>
<p>동작하지 않는다.
프록시가 가로챌 수 없기 때문이다.</p>
<h3 id="2-예외를-try-catch로-삼켜버린-경우">2. 예외를 try-catch로 삼켜버린 경우</h3>
<pre><code class="language-java">@Transactional
public void save() {
    try {
        throw new RuntimeException();
    } catch (Exception e) {
        // 아무것도 안 함
    }
}</code></pre>
<p>예외가 밖으로 던져지지 않으면 롤백되지 않는다.</p>
<h3 id="3-checked-exception">3. Checked Exception</h3>
<pre><code class="language-java">@Transactional(rollbackFor = Exception.class)</code></pre>
<p>Checked Exception은 기본적으로 롤백되지 않는다.
명시적으로 설정해야 한다.</p>
<h1 id="정리-및-마무리">정리 및 마무리</h1>
<p>Spring에서 <code>@Transactional</code>이 안 먹는 대표적인 이유는 다음과 같다.</p>
<ol>
<li>같은 클래스 내부 호출 (Self Invocation)</li>
<li>private 메서드</li>
<li>예외를 삼켜버림</li>
<li>Checked Exception</li>
</ol>
<p>그중 가장 많이 터지는 원인은 Self Invocation이다.</p>
<blockquote>
<p>@Transactional이 안 먹는 게 아니라 프록시를 타지 못하고 있었던 것이다.
트랜잭션 문제는 코드 한 줄의 문제가 아니라 호출 구조의 문제인 경우가 많다.
실무에서 한 번쯤은 반드시 겪게 되는 이슈다.</p>
</blockquote>
<p>같은 삽질을 반복하지 않기를.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트 리뉴얼에서 DTO 설계 기준을 다시 잡은 이유]]></title>
            <link>https://velog.io/@fever-max/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC%EC%97%90%EC%84%9C-DTO-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%A4%80%EC%9D%84-%EB%8B%A4%EC%8B%9C-%EC%9E%A1%EC%9D%80-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@fever-max/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC%EC%97%90%EC%84%9C-DTO-%EC%84%A4%EA%B3%84-%EA%B8%B0%EC%A4%80%EC%9D%84-%EB%8B%A4%EC%8B%9C-%EC%9E%A1%EC%9D%80-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 03 Feb 2026 06:30:57 GMT</pubDate>
            <description><![CDATA[<p>리뉴얼 프로젝트에 들어가기 전까지는
DTO를 그저 DB 결과를 담는 그릇 정도로 생각했다.
그런데 레거시 시스템을 유지한 채 구조를 바꾸는 대규모 리뉴얼을 겪으면서,
DTO가 생각보다 훨씬 많은 역할을 하고 있다는 걸 체감하게 됐다.</p>
<blockquote>
<p>대규모 리뉴얼 프로젝트를 겪으면서 DTO 설계 기준을 다시 세우게 된 이유를 정리한 기록이다.</p>
</blockquote>
<h3 id="1-resultmap-중심-설계의-한계">1. resultMap 중심 설계의 한계</h3>
<p>기존 구조에서는 resultMap이 거의 모든 걸 책임지고 있었다. (진짜 보기 힘든 개쓰레기 코드들의 향연...)</p>
<blockquote>
<p>문제1. 컬럼명 ↔ 필드명 매핑이 각기 다름
문제2. 타입이 뭔지 알아 볼 수가 없음
문제3. 중첩 객체 구성 (맵안에서 맵을 쓰는 기괴한...)</p>
</blockquote>
<p>처음엔 유연해 보였지만, 시간이 지나면서 문제가 드러났다.
특히 아래 3가지 문제가 심각했다.</p>
<blockquote>
<p>심각한 문제1. resultMap 하나 수정하면 여러 쿼리에 영향
심각한 문제2. DTO 구조를 한눈에 파악하기 어려움
심각한 문제3. 실제로 어떤 데이터가 내려오는지 추적이 힘듦 </p>
</blockquote>
<p>특히 3번 때문에 봤던 코드를 몇번이나 다시 봤는지 모른다.
이렇게 지독한 코드 다시보기를 체험하니 마음이 단호해졌다.</p>
<p>매핑 로직이 많아질수록 DTO는 ‘보이지 않는 객체’가 된다.
그래서 resultMap 의존도를 줄이고, DTO를 중심으로 쿼리와 매핑을 맞추는 방향으로 기준을 바꾸기로 다짐했다. (제발)</p>
<h3 id="2-dto는-조회용과-행위용을-섞으면-안-된다">2. DTO는 “조회용”과 “행위용”을 섞으면 안 된다.</h3>
<p>리뉴얼 초기에 가장 많이 봤던 패턴이 이거였다.</p>
<blockquote>
<p>조회 결과를 담는 DTO
저장/수정 요청을 받는 DTO
계산 결과까지 담기 시작한 DTO</p>
</blockquote>
<p>이게 한 클래스에 섞이기 시작하면 DTO는 금방 비대해진다.
왜냐하면 a테이블, b테이블에거 갖고온 모든 데이터들이 짬뽕처럼 섞인 김치짬뽕우웩탕이 탄생되기 때문...</p>
<p>사실 제일 큰 문제는 크기보다 역할이 모호해진다는 점이었다.
그래서 기준을 단순하게 잡았다.</p>
<blockquote>
<p>조회 DTO: 화면이나 API에 내려줄 데이터만
요청 DTO: 입력값 검증 중심
내부 계산용 데이터는 별도 객체로 분리</p>
</blockquote>
<p>DTO는 상태를 담는 객체이지, 비즈니스 판단을 하는 객체가 아니니까! 절대로 db를 그대로 때려박지 않으려고 노력했다.</p>
<h3 id="3-날짜와-금액-타입은-타협하지-않기로-했다">3. 날짜와 금액 타입은 타협하지 않기로 했다.</h3>
<p>레거시에서는 이런 타입들이 섞여 있었다.</p>
<blockquote>
<p>날짜: String, Date, Timestamp 혼용
금액: int, long, double 혼재</p>
</blockquote>
<p>리뉴얼 과정에서 이걸 그대로 가져가면,
나중에 버그가 나는 지점을 예측하기가 어려웠다.
특히 가격 계산이 중요한 프로젝트인데 숫자들이 아주 지멋대로라 굉장히 곤란...</p>
<p>그래서 DTO 기준을 아예 고정했다.</p>
<blockquote>
<p>날짜/시간: LocalDateTime
금액/비율(정수, 소수): Integer, BigDecimal</p>
</blockquote>
<p>변환 시간이 조금 들더라도 의미가 명확한 타입을 쓰는 게 유지보수에서는 훨씬 싸다는 결론이었다. 그게 조금 더 확실한 계산이 가능하니까!</p>
<h3 id="5-dto가-바뀌면-로직도-같이-흔들린다">5. DTO가 바뀌면 로직도 같이 흔들린다.</h3>
<p>리뉴얼을 하면서 가장 크게 느끼고 걱정했던 점이 이거였다.</p>
<p>DTO는 단순하지만 서비스 구조를 나타내는 설계의 일부분이다.
DTO 필드가 하나 추가될 때마다 로직이 늘어나고 검증이 생기고 테스트를 해야한다.</p>
<p>그래서 DTO를 추가할 때마다 이 필드가 정말 있어야 하는지 고민을 많이많이많이 했다. 
내 선택에 후회가 없기를. (물론 이렇게 하고 다음날에 후회해서 몇백줄 바꾸고 했음;)</p>
<h3 id="마무리">마무리</h3>
<p>리뉴얼 프로젝트를 하면서 깨달은 건 하나다.</p>
<p>DTO는 빨리 만들수록 좋지만,
아무 기준 없이 만들면 가장 오래 발목을 잡는다.</p>
<p>이후로는 DTO를 만들 때 항상 이 질문을 먼저 던진다.</p>
<blockquote>
<p>이 DTO의 역할은 무엇인가
이 데이터는 언제까지 유효한가
서비스 로직과 얼마나 강하게 결합되는가</p>
</blockquote>
<p>DTO 설계 기준을 정리한 것만으로도
리뉴얼 이후 코드의 흔들림은 확실히 줄어들었다.
특히 도메인을 바꾼다던가 이상한 짓은 절대 하지 않기로 다짐했다. </p>
<p>이상... 프로젝트 8개를 한번에 리뉴얼한 개발자의 일기... 끝. (문제는 아직도 테스트 중이다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[insert에서 객체를 재사용해도 될까?]]></title>
            <link>https://velog.io/@fever-max/insert%EC%97%90%EC%84%9C-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%9E%AC%EC%82%AC%EC%9A%A9%ED%95%B4%EB%8F%84-%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@fever-max/insert%EC%97%90%EC%84%9C-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%9E%AC%EC%82%AC%EC%9A%A9%ED%95%B4%EB%8F%84-%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Tue, 06 Jan 2026 02:58:17 GMT</pubDate>
            <description><![CDATA[<p>실무에서 insert 로직을 작성하다 보면 이런 고민을 하게 된다.</p>
<blockquote>
<p>“어차피 값 몇 개만 바꿔서 한 번 더 insert 하는 건데<br>객체를 새로 만들지 않고 재사용해도 되지 않을까?”</p>
</blockquote>
<p>결론부터 말하면 <strong>insert에서 객체 재사용은 피하는 게 맞다</strong>.  
특히 이력성·동의성 데이터라면 더더욱 그렇다.</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<p>아래는 하나의 요청에서 두 종류의 동의 데이터를 저장하는 상황을 단순화한 예시다.
먼저 기본 동의를 저장하고, 조건에 따라 추가 동의를 한 번 더 저장한다.</p>
<pre><code class="language-java">Agreement agre = new Agreement();

// 공통 값 세팅
agre.setOrderId(orderId);
agre.setUserType(&quot;MAIN&quot;);
agre.setCreatedAt(now);
// 기본 동의
agre.setAgreeItems(baseAgreeList);
baseAgre.setExtraYn(&quot;N&quot;);
mapper.insert(agre);

// 추가 동의
if (!extraAgreeList.isEmpty()) {
    agre.setAgreeItems(extraAgreeList);
    baseAgre.setExtraYn(&quot;Y&quot;);
    mapper.insert(agre);
}</code></pre>
<p>겉보기에는 문제가 없어 보이고, 실제로도 정상적으로 insert는 수행된다.
하지만 이 구조에는 몇 가지 근본적인 문제가 있다.</p>
<h2 id="문제점">문제점</h2>
<h3 id="1-insert인데-update처럼-보인다">1. insert인데 update처럼 보인다</h3>
<p>같은 객체를 수정해서 다시 insert 하면 코드를 읽는 입장에서는 자연스럽게 이런 의문이 든다.</p>
<blockquote>
<p>이 객체는 이미 DB에 들어간 상태 아닌가?
PK나 식별 값은 어디서 관리되는가?
update와 무엇이 다른가?</p>
</blockquote>
<p>의도가 코드에 드러나지 않아 이해 비용이 커진다.</p>
<h3 id="2-값-오염-가능성">2. 값 오염 가능성</h3>
<p>현재는 필드가 단순해서 괜찮아 보일 수 있다.
하지만 시간이 지나면서 다음과 같은 변화가 생긴다.</p>
<blockquote>
<p>컬럼 추가
insert 조건 분기 증가
기본값 세팅 로직 변경</p>
</blockquote>
<p>이 순간부터 이전 insert의 값이 다음 insert에 섞여 들어갈 위험이 생긴다.</p>
<h3 id="3-이력성-데이터에겐-최악">3. 이력성 데이터에겐 최악</h3>
<p>동의, 로그, 이력 테이블은 “언제, 무엇이, 왜 저장됐는지”가 중요하다.
객체 하나를 계속 수정해서 여러 row를 만드는 구조는
장애 분석과 디버깅 시 추적 난이도를 크게 높인다.</p>
<h2 id="권장-방식">권장 방식</h2>
<p>원칙은 단순하다.</p>
<blockquote>
<p>insert = 객체 1개 = row 1개</p>
</blockquote>
<p>공통 세팅은 메서드로 분리하고, 각 insert마다 새 객체를 생성한다.</p>
<pre><code class="language-java">private Agreement baseAgreement(String orderId, LocalDateTime now) {
    Agreement agre = new Agreement();
    agre.setOrderId(orderId);
    agre.setUserType(&quot;MAIN&quot;);
    agre.setCreatedAt(now);
    return agre;
}</code></pre>
<pre><code class="language-java">// 기본 동의
Agreement baseAgre = baseAgreement(orderId, now);
baseAgre.setAgreeItems(baseAgreeList);
baseAgre.setExtraYn(&quot;N&quot;);
mapper.insert(baseAgre);

// 추가 동의
if (!extraAgreeList.isEmpty()) {
    Agreement extraAgre = baseAgreement(orderId, now);
    extraAgre.setAgreeItems(extraAgreeList);
    extraAgre.setExtraYn(&quot;Y&quot;);
    mapper.insert(extraAgre);
}</code></pre>
<h2 id="정리">정리</h2>
<p>insert 로직에서 객체 재사용은 당장은 코드가 짧아 보일 수 있지만, 시간이 지날수록 유지보수 비용이 커진다. 특히 동의·로그·이력 데이터라면 객체를 아끼기보다 명확한 구조를 선택하는 게 맞다.</p>
<blockquote>
<p>insert는 항상 “이 객체는 하나의 row만 책임진다” 라는
원칙을 지키는 것이 가장 안전하다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tomcat] war 중복 배포 방지 (스케줄러 중복 실행 문제)]]></title>
            <link>https://velog.io/@fever-max/Tomcat%EC%97%90%EC%84%9C-war-%EC%A4%91%EB%B3%B5-%EB%B0%B0%ED%8F%AC-%EB%B0%A9%EC%A7%80-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC-%EC%A4%91%EB%B3%B5-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@fever-max/Tomcat%EC%97%90%EC%84%9C-war-%EC%A4%91%EB%B3%B5-%EB%B0%B0%ED%8F%AC-%EB%B0%A9%EC%A7%80-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC-%EC%A4%91%EB%B3%B5-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Wed, 06 Aug 2025 01:11:18 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>로컬에선 문제 없던 스케줄러가 톰캣에 올리면 중복 실행되는 문제 발생했다.
아래는 기존 server.xml 설정이다.</p>
<pre><code class="language-xml">&lt;Host name=&quot;localhost&quot; appBase=&quot;webapps&quot; unpackWARs=&quot;true&quot; autoDeploy=&quot;true&quot;&gt;  
&lt;Context docBase=&quot;test&quot; path=&quot;/&quot; reloadable=&quot;true&quot; /&gt;</code></pre>
<h2 id="원인">원인</h2>
<pre><code class="language-bash">/webapps/
├── test.war
├── test/          ← test.war가 자동으로 풀리면서 생성됨
├── ROOT/          ← 기본 context</code></pre>
<p>톰캣이 실행되면 webapps안에 test 폴더와 ROOT 폴더가 생기는데,
결과적으로 동일 WAR의 인스턴스가 중복 실행되어 @Scheduled 사용시 2번 실행된다.</p>
<h2 id="해결-방법">해결 방법</h2>
<h3 id="1-autodeploy-deployonstartup비활성화">1. <code>autoDeploy</code>, <code>deployOnStartup</code>비활성화</h3>
<pre><code class="language-xml">&lt;Host name=&quot;localhost&quot; appBase=&quot;webapps&quot; unpackWARs=&quot;true&quot; autoDeploy=&quot;false&quot; deployOnStartup=&quot;false&quot;&gt;  
&lt;Context docBase=&quot;test&quot; path=&quot;/&quot; reloadable=&quot;true&quot; /&gt;</code></pre>
<p>autoDeploy=&quot;false&quot;
→ WAR 파일이 있어도 자동 배포 안 함</p>
<p>deployOnStartup=&quot;false&quot;
→ 톰캣 기동 시 WAR 자동 전개 안 함</p>
<p><code>&lt;Context&gt;</code>에 명시된 docBase=&quot;test&quot;만 명확하게 실행되며 test.war가 자동으로 풀려서 test/ 폴더 생기는 것도 방지된다. </p>
<h3 id="2-appbase-자체를-별도로-설정">2. <code>appBase</code> 자체를 별도로 설정</h3>
<pre><code class="language-xml">&lt;Host name=&quot;localhost&quot; appBase=&quot;webapps/test&quot; unpackWARs=&quot;true&quot; autoDeploy=&quot;true&quot;&gt;  
&lt;Context docBase=&quot;test&quot; path=&quot;/&quot; reloadable=&quot;true&quot; /&gt;</code></pre>
<p>webapps/test라는 별도의 디렉토리를 톰캣 배포 루트(appBase)로 지정하여 해당 경로에 test.war만 존재하도록 관리한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Filebeat > Logstash > Elasticsearch> Kibana로그 수집 시스템 구축 가이드]]></title>
            <link>https://velog.io/@fever-max/Filebeat-Logstash-Elasticsearch-Kibana%EB%A1%9C%EA%B7%B8-%EC%88%98%EC%A7%91-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@fever-max/Filebeat-Logstash-Elasticsearch-Kibana%EB%A1%9C%EA%B7%B8-%EC%88%98%EC%A7%91-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Mon, 21 Jul 2025 05:05:54 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/fever-max/post/5f68a0fa-7407-48c3-a336-2193c5f2ee9b/image.png" alt=""></p>
<h2 id="📌-구성-요소">📌 구성 요소</h2>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할 설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Elasticsearch</strong></td>
<td>로그 데이터를 저장하고 검색할 수 있는 중앙 저장소</td>
</tr>
<tr>
<td><strong>Kibana</strong></td>
<td>Elasticsearch의 데이터를 시각화하는 웹 UI</td>
</tr>
<tr>
<td><strong>Logstash</strong></td>
<td>로그를 수신하고 가공한 후 Elasticsearch로 전달</td>
</tr>
<tr>
<td><strong>Filebeat</strong></td>
<td>서버의 로그 파일을 수집해 Logstash로 전달</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔁-가동-순서">🔁 가동 순서</h2>
<ol>
<li><strong>Elasticsearch</strong></li>
<li><strong>Kibana</strong></li>
<li><strong>Logstash</strong></li>
<li><strong>Filebeat</strong></li>
</ol>
<blockquote>
<p>⛔ 종료 시에는 반대 순서로: Filebeat → Logstash → Kibana → Elasticsearch</p>
</blockquote>
<hr>
<h2 id="elasticsearch">Elasticsearch</h2>
<h3 id="1-설치">1. 설치</h3>
<pre><code class="language-shell"># Elasticsearch 컨테이너 실행
docker run -d \
  --name elasticsearch \
  -p 9200:9200 \
  -e discovery.type=single-node \
  -e ELASTIC_PASSWORD=**** \
  -e xpack.security.enabled=true \
  -e xpack.security.authc.api_key.enabled=true \
  -e ES_JAVA_OPTS=&quot;-Xms512m -Xmx512m&quot; \
  -v esdata:/usr/share/elasticsearch/data \
  docker.elastic.co/elasticsearch/elasticsearch:9.0.3

# 접속 주소: http://localhost:9200
# 기본 관리자 계정: elastic
# 비밀번호: ELASTIC_PASSWORD 설정
# 실행 모드: 단일 노드 (discovery.type=single-node)
# Kibana, Logstash, Filebeat 모두 이 주소로 연결됨</code></pre>
<h2 id="kibana">Kibana</h2>
<h3 id="1-설치-1">1. 설치</h3>
<pre><code class="language-shell">docker run -d \
  --name kibana \
  -p 5601:5601 \
  -e ELASTICSEARCH_HOSTS=http://elasticsearch:9200 \
  -e ELASTICSEARCH_USERNAME=kibana_system \
  -e ELASTICSEARCH_PASSWORD=***** \
  -e xpack.security.encryptionKey=**** \
  -e xpack.encryptedSavedObjects.encryptionKey=**** \
  -e xpack.reporting.encryptionKey=**** \
  docker.elastic.co/kibana/kibana:9.0.3</code></pre>
<h2 id="logstash">Logstash</h2>
<h3 id="1-설치-2">1. 설치</h3>
<pre><code class="language-shell">docker run -d \
  --name logstash \
  -p 5044:5044 \
  -e xpack.monitoring.elasticsearch.username=elastic \
  -e xpack.monitoring.elasticsearch.password=**** \
  -e xpack.monitoring.elasticsearch.hosts=http://elasticsearch:9200 \
  docker.elastic.co/logstash/logstash:9.0.3</code></pre>
<h3 id="2-설정">2. 설정</h3>
<pre><code class="language-shell">docker exec -it logstash /bin/bash
cd /usr/share/logstash/pipeline
vi logstash.conf</code></pre>
<pre><code class="language-conf">input {
  beats {
    port =&gt; 5044
  }
}

filter {
  json {
    source =&gt; &quot;message&quot;
    remove_field =&gt; [&quot;message&quot;]
    tag_on_failure =&gt; [&quot;_jsonparsefailure&quot;]
  }
}

output {
  elasticsearch {
    hosts =&gt; [&quot;http://elasticsearch:9200&quot;]
    user =&gt; &quot;elastic&quot;
    password =&gt; &quot;****&quot;
    index =&gt; &quot;log-%{project}-%{+YYYY.MM.dd}&quot;
  }

  stdout {
    codec =&gt; rubydebug
  }
}</code></pre>
<h2 id="filebeat">Filebeat</h2>
<h3 id="1-설치-3">1. 설치</h3>
<pre><code class="language-shell">curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.18.3-amd64.deb
sudo dpkg -i filebeat-8.18.3-amd64.deb
cd /etc/filebeat
vi filebeat.yml</code></pre>
<h3 id="2-설정-1">2. 설정</h3>
<pre><code class="language-yml">filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /path/to/your/logs/app.log
    fields:
      project: your-project-name
    fields_under_root: true

output.logstash:
  hosts: [&quot;your-logstash-host:5044&quot;]</code></pre>
<pre><code class="language-shell">sudo chown root:root filebeat.yml
sudo chmod 644 filebeat.yml</code></pre>
<h3 id="3-실행">3. 실행</h3>
<pre><code class="language-shell"># 서비스 등록
sudo systemctl enable filebeat
# 시작
sudo systemctl start filebeat
# 시작
sudo systemctl stop filebeat
# 재시작 
sudo systemctl restart filebeat
# 로깅
sudo journalctl -u filebeat -f</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] Nginx와 Tomcat 연결]]></title>
            <link>https://velog.io/@fever-max/Docker-Nginx%EC%99%80-Tomcat-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@fever-max/Docker-Nginx%EC%99%80-Tomcat-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Thu, 06 Mar 2025 07:34:08 GMT</pubDate>
            <description><![CDATA[<h2 id="1-docker에-nginx-컨테이너-올리기">1. Docker에 Nginx 컨테이너 올리기</h2>
<ul>
<li><p>Nginx 올리기
<code>docker run --name nginx -d -p 80:80 -p 443:443 nginx</code></p>
</li>
<li><p>접속
<code>docker exec -it nginx /bin/bash</code></p>
</li>
<li><p>conf 설정 (include 설정 추가)
<code>vi /etc/nginx/nginx.conf</code>
<img src="https://velog.velcdn.com/images/fever-max/post/29eff7ea-a0b3-485a-95f3-bd38d23a0415/image.png" alt=""></p>
</li>
</ul>
<h2 id="2-docker에-tomcat-java-이미지-설치">2. Docker에 tomcat, java 이미지 설치</h2>
<ul>
<li>원하는 설정에 맞춰서 톰캣과 자바를 설치
<code>docker pull tomcat:10.1.30-jdk17-temurin-noble</code></li>
</ul>
<h2 id="3-docker에-tomcat-컨테이너-올리기">3. Docker에 tomcat 컨테이너 올리기</h2>
<ul>
<li>호스트 포트 및 컨테이너 포트 설정 후 올리기
<code>docker run -d -p 8080:8080 --name tomcat-container tomcat:10.1.30-jdk17-temurin-noble</code></li>
</ul>
<h2 id="4-nginx-conf-설정">4. Nginx conf 설정</h2>
<ul>
<li>includ에 설정한 위치에 설정 conf 생성</li>
<li>포워딩 주소 및 도메인 설정
<img src="https://velog.velcdn.com/images/fever-max/post/89af52d2-da0b-474d-9fdc-3b524a3a085a/image.png" alt=""></li>
</ul>
<p><code>proxy_set_header X-Real-IP $remote_addr;</code>
클라이언트의 실제 IP 주소를 백엔드 서버로 전달
이 설정을 생략하면 백엔드 서버에서 클라이언트의 실제 IP를 알 수 없고, 대신 프록시 서버의 IP 주소만 보이게 되어, 원본 클라이언트의 IP를 추적할 수 없음.</p>
<p><code>proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for</code>
클라이언트의 IP 주소를 X-Forwarded-For 헤더에 추가
이 헤더를 설정하지 않으면, 백엔드 서버가 요청의 출처 IP를 알 수 없게 되어, 클라이언트의 실제 IP를 파악할 수 없음</p>
<p><code>proxy_set_header X-Forwarded-Port $server_port;</code>
요청을 받은 서버의 포트 번호를 X-Forwarded-Port 헤더로 전달
이 설정을 하지 않으면, 백엔드 서버에서 요청이 들어온 포트를 알 수 없어서, 요청이 어떤 포트로 들어왔는지 확인할 수 없음.</p>
<p><code>proxy_set_header X-Forwarded-Proto $scheme;</code>
요청이 HTTPS인지 HTTP인지 구분할 수 있도록 X-Forwarded-Proto 헤더를 전달
이 헤더가 없으면 백엔드 서버는 요청이 HTTP로 전달되었는지 HTTPS로 전달되었는지 알 수 없어서 보안이나 리디렉션 관련 로직에서 문제가 발생할 수 있음</p>
<p><code>proxy_set_header X-Forwarded-Host $Host;</code>
클라이언트가 요청한 원래의 호스트 이름을 X-Forwarded-Host 헤더로 전달
이 헤더가 없으면, 백엔드 서버가 원래 요청된 호스트 이름을 알 수 없어, 동적 도메인 기반 로직이 제대로 작동하지 않을 수 있음 </p>
<p><code>proxy_set_header Host $http_host;</code>
클라이언트가 요청한 호스트를 Host 헤더로 전달하고 $http_host는 클라이언트의 요청에 포함된 호스트 정보를 나타냄
이 설정을 하지 않으면, 백엔드 서버가 요청에 대한 정확한 호스트를 알 수 없고, 이는 다중 도메인을 사용하는 시스템에서 중요한 정보를 잃게 될 수 있음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Linux] 클러스터 (Cluster) 개념 및 종류]]></title>
            <link>https://velog.io/@fever-max/Linux-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-Cluster-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EC%A2%85%EB%A5%98</link>
            <guid>https://velog.io/@fever-max/Linux-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-Cluster-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EC%A2%85%EB%A5%98</guid>
            <pubDate>Tue, 03 Sep 2024 14:17:37 GMT</pubDate>
            <description><![CDATA[<h2 id="💻-클러스터cluster">💻 클러스터(Cluster)</h2>
<p>여러 개의 독립적인 컴퓨터나 서버가 네트워크를 통해 연결되어 하나의 시스템처럼 작동하도록 구성된 시스템. 클러스터의 주요 목적은 성능 향상, 신뢰성 증대, 또는 두 가지 모두를 달성하는 것. 클러스터는 여러 형태가 있으며, 각 형태는 특정 용도와 요구 사항에 맞게 설계된다.</p>
<h2 id="📌-클러스터cluster-종류">📌 클러스터(Cluster) 종류</h2>
<table>
<thead>
<tr>
<th>클러스터 유형</th>
<th>목적</th>
<th>구성</th>
<th>주요 기술</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>고가용성 클러스터 (High-Availability Cluster)</td>
<td>시스템의 지속적인 가용성을 보장하고 장애 발생 시 자동 전환</td>
<td>여러 노드가 중복되어 있으며, 하나의 노드가 실패 시 다른 노드가 대체</td>
<td>페일오버(failover), 복제(redundancy)</td>
<td>웹 서버 클러스터, 데이터베이스 클러스터</td>
</tr>
<tr>
<td>베어울프 클러스터 (Beowulf Cluster)</td>
<td>고성능 컴퓨팅을 위한 클러스터, 연구 및 과학적 계산에 사용</td>
<td>일반 상용 하드웨어를 사용하여 네트워크로 연결된 저비용 클러스터</td>
<td>병렬 처리(parallel processing), 메시지 패싱(message passing)</td>
<td>연구 프로젝트, 데이터 분석</td>
</tr>
<tr>
<td>HPC 클러스터 (High-Performance Computing Cluster)</td>
<td>복잡한 계산 문제를 해결하기 위한 대규모 컴퓨팅 파워 제공</td>
<td>고성능 컴퓨팅 노드와 스토리지 시스템으로 구성, 고속 네트워크로 연결</td>
<td>병렬 컴퓨팅(parallel computing), 고속 네트워크(high-speed networking), 대용량 데이터 처리</td>
<td>기후 모델링, 유전자 분석</td>
</tr>
<tr>
<td>고계산용 클러스터 (High-Throughput Computing Cluster)</td>
<td>대량의 작업을 효율적으로 처리, 전체 작업량의 처리 속도 향상</td>
<td>많은 수의 컴퓨터 노드가 연결되어 있으며, 각 노드는 상대적으로 간단한 작업 처리</td>
<td>작업 스케줄링(job scheduling), 분산 처리(distributed processing)</td>
<td>데이터 처리, 시뮬레이션</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Linux] 시그널(signal) 종류]]></title>
            <link>https://velog.io/@fever-max/Linux-%EC%8B%9C%EA%B7%B8%EB%84%90signal-%EC%A2%85%EB%A5%98</link>
            <guid>https://velog.io/@fever-max/Linux-%EC%8B%9C%EA%B7%B8%EB%84%90signal-%EC%A2%85%EB%A5%98</guid>
            <pubDate>Tue, 03 Sep 2024 13:25:40 GMT</pubDate>
            <description><![CDATA[<h2 id="💻-시그널signal">💻 시그널(signal)</h2>
<p>프로세스 간의 통신 방법 중 하나로 프로세스에 특정 이벤트가 발생했음을 알리기 위해 사용된다. 예를 들어, 시그널을 사용하여 프로세스에 종료, 중지, 일시 정지와 같은 다양한 명령을 전달할 수 있다.</p>
<h2 id="📌-주요-시그널과-명령어">📌 주요 시그널과 명령어</h2>
<table>
<thead>
<tr>
<th>시그널</th>
<th>번호</th>
<th>설명</th>
<th>명령어 및 발생 상황</th>
</tr>
</thead>
<tbody><tr>
<td>SIGHUP</td>
<td>1</td>
<td>터미널이 연결 종료되었음을 프로세스에 알림</td>
<td><code>nohup</code>, 터미널 종료 시</td>
</tr>
<tr>
<td>SIGINT</td>
<td>2</td>
<td>인터럽트 신호, 실행 중인 프로세스를 중지 요청</td>
<td><code>Ctrl+C</code></td>
</tr>
<tr>
<td>SIGKILL</td>
<td>9</td>
<td>강제 종료 신호, 즉시 프로세스 종료</td>
<td><code>kill -9</code></td>
</tr>
<tr>
<td>SIGTERM</td>
<td>15</td>
<td>종료 신호, 정상적으로 프로세스 종료 요청</td>
<td><code>kill</code> (기본), <code>kill -15</code></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Linux] 프린트 명령어]]></title>
            <link>https://velog.io/@fever-max/Linux-%ED%94%84%EB%A6%B0%ED%8A%B8-%EB%AA%85%EB%A0%B9%EC%96%B4</link>
            <guid>https://velog.io/@fever-max/Linux-%ED%94%84%EB%A6%B0%ED%8A%B8-%EB%AA%85%EB%A0%B9%EC%96%B4</guid>
            <pubDate>Tue, 03 Sep 2024 13:21:54 GMT</pubDate>
            <description><![CDATA[<h2 id="💻-리눅스-프린터-명령어">💻 리눅스 프린터 명령어</h2>
<table>
<thead>
<tr>
<th>명령어</th>
<th>설명</th>
<th>사용 예시</th>
<th>주요 옵션</th>
</tr>
</thead>
<tbody><tr>
<td><code>lp</code></td>
<td>프린터에 문서를 인쇄함</td>
<td><code>lp document.txt</code></td>
<td><code>-d printer_name</code> : 프린터 지정</td>
</tr>
<tr>
<td><code>lpr</code></td>
<td>프린터에 문서를 제출함</td>
<td><code>lpr document.txt</code></td>
<td><code>-P printer_name</code> : 프린터 지정</td>
</tr>
<tr>
<td><code>lpstat</code></td>
<td>프린터 상태와 작업 큐를 확인함</td>
<td><code>lpstat -p</code> (프린터 상태 확인)</td>
<td><code>-p</code> : 프린터 상태, <code>-o</code> : 작업 큐 확인</td>
</tr>
<tr>
<td><code>cancel</code></td>
<td>프린터 작업을 취소함</td>
<td><code>cancel 123</code> (작업 ID로 취소)</td>
<td>-</td>
</tr>
<tr>
<td><code>lpadmin</code></td>
<td>프린터를 관리하고 설정을 변경함</td>
<td><code>lpadmin -p printer_name -E -v device_uri -m model</code> (프린터 추가)</td>
<td><code>-p</code> : 프린터 추가, <code>-x</code> : 프린터 삭제</td>
</tr>
<tr>
<td><code>lpq</code></td>
<td>프린터의 작업 큐를 확인함</td>
<td><code>lpq</code></td>
<td>-</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Linux] 패키지 관리 도구]]></title>
            <link>https://velog.io/@fever-max/Linux-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B4%80%EB%A6%AC-%EB%8F%84%EA%B5%AC</link>
            <guid>https://velog.io/@fever-max/Linux-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B4%80%EB%A6%AC-%EB%8F%84%EA%B5%AC</guid>
            <pubDate>Mon, 02 Sep 2024 14:52:38 GMT</pubDate>
            <description><![CDATA[<h2 id="💻-온라인-패키지-관리-기법">💻 온라인 패키지 관리 기법</h2>
<p>인터넷을 통해 원격 저장소(리포지토리)에서 패키지를 다운로드하고 설치하는 방법</p>
<ul>
<li>원격 저장소 사용: 패키지 관리 도구가 인터넷에 연결된 저장소에서 최신 패키지 정보를 가져와 패키지를 설치, 업데이트, 제거한다.</li>
<li>의존성 해결: 패키지를 설치할 때 필요한 다른 패키지(의존성)를 자동으로 찾아서 함께 설치한다.</li>
</ul>
<blockquote>
<p><strong>명령어 예시</strong>
레드햇 계열: yum install &lt;패키지&gt;, dnf install &lt;패키지&gt;
데비안 계열: apt-get install &lt;패키지&gt;, apt install &lt;패키지&gt;
SUSE 계열: zypper install &lt;패키지&gt;
Arch Linux: pacman -S &lt;패키지&gt;</p>
</blockquote>
<h2 id="💻-오프라인-패키지-관리-기법">💻 오프라인 패키지 관리 기법</h2>
<p>인터넷 연결 없이 로컬에 저장된 패키지 파일을 직접 설치, 제거, 관리하는 방법</p>
<ul>
<li>로컬 패키지 사용: 이미 다운로드된 패키지 파일을 로컬에서 직접 설치하거나 관리한다.</li>
<li>의존성 해결 필요: 오프라인으로 패키지를 설치할 때는 의존성을 수동으로 해결할 수도 있음</li>
</ul>
<blockquote>
<p><strong>명령어 예시</strong>
레드햇 계열 및 SUSE: rpm -ivh &lt;패키지.rpm&gt;
데비안 계열: dpkg -i &lt;패키지.deb&gt;
Arch Linux: pacman -U &lt;패키지.tar.xz&gt;</p>
</blockquote>
<h2 id="📌-온라인오프라인-패키지-관리기법-구분표">📌 온라인/오프라인 패키지 관리기법 구분표</h2>
<table>
<thead>
<tr>
<th><strong>배포판 계열</strong></th>
<th><strong>온라인 패키지 관리 도구</strong></th>
<th><strong>오프라인 패키지 관리 도구</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>레드햇 계열 (RHEL, CentOS, Fedora)</strong></td>
<td><strong>YUM (Yellowdog Updater, Modified)</strong><br> <code>yum install &lt;패키지&gt;</code> <br> <strong>DNF (Dandified YUM)</strong><br> <code>dnf install &lt;패키지&gt;</code></td>
<td><strong>RPM (Red Hat Package Manager)</strong><br> <code>rpm -ivh &lt;패키지.rpm&gt;</code></td>
</tr>
<tr>
<td><strong>데비안 계열 (Debian, Ubuntu, Linux Mint)</strong></td>
<td><strong>APT (Advanced Package Tool)</strong><br> <code>apt-get install &lt;패키지&gt;</code> <br> <code>apt install &lt;패키지&gt;</code> (새로운 명령어)</td>
<td><strong>DPKG (Debian Package)</strong><br> <code>dpkg -i &lt;패키지.deb&gt;</code></td>
</tr>
<tr>
<td><strong>SUSE 계열 (openSUSE, SLES)</strong></td>
<td><strong>Zypper</strong><br> <code>zypper install &lt;패키지&gt;</code></td>
<td><strong>RPM (Red Hat Package Manager)</strong><br> <code>rpm -ivh &lt;패키지.rpm&gt;</code></td>
</tr>
<tr>
<td><strong>기타 (Arch Linux 등)</strong></td>
<td><strong>Pacman (Arch Linux)</strong><br> <code>pacman -S &lt;패키지&gt;</code></td>
<td><strong>Pacman (Arch Linux)</strong><br> <code>pacman -U &lt;패키지.tar.xz&gt;</code></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Linux] LVM 개념 및 구성 요소]]></title>
            <link>https://velog.io/@fever-max/Linux-LVM-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EA%B5%AC%EC%84%B1-%EC%9A%94%EC%86%8C</link>
            <guid>https://velog.io/@fever-max/Linux-LVM-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EA%B5%AC%EC%84%B1-%EC%9A%94%EC%86%8C</guid>
            <pubDate>Mon, 02 Sep 2024 14:41:55 GMT</pubDate>
            <description><![CDATA[<h2 id="💻-lvm-logical-volume-manager">💻 LVM (Logical Volume Manager)</h2>
<p>리눅스에서 디스크 관리의 유연성을 제공하는 시스템으로 물리적 디스크(Physical Volume, PV)를 그룹으로 묶어 하나의 논리적 볼륨(Logical Volume, LV)을 만들 수 있게 한다. 이 논리적 볼륨은 파일 시스템이 사용하는 공간으로, 물리적인 디스크의 제약에서 벗어나 크기를 동적으로 조절하거나, 디스크를 추가할 수 있는 장점이 있다.</p>
<h2 id="❗️-lvm-주요-구성-요소">❗️ LVM 주요 구성 요소</h2>
<p><strong>Physical Volume (PV)</strong>
실제 물리적인 디스크 또는 디스크 파티션을 의미하며 LVM의 가장 기본 단위다.</p>
<p><strong>Volume Group (VG)</strong>
여러 개의 Physical Volume(PV)을 묶어 하나의 Volume Group(VG)을 만들고, 논리적 볼륨을 생성할 수 있는 공간을 제공한다.</p>
<p><strong>Logical Volume (LV)</strong>
Volume Group(VG)에서 생성된 논리적인 파티션으로 실제 파일 시스템을 생성하고 데이터를 저장할 수 있는 공간으로 사용된다.</p>
<h2 id="⚙️-lvm-사용-순서">⚙️ LVM 사용 순서</h2>
<ul>
<li>PV &gt; VG &gt; LV</li>
</ul>
<table>
<thead>
<tr>
<th><strong>단계</strong></th>
<th><strong>설명</strong></th>
<th><strong>명령어 예시</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>1. Physical Volume (PV) 생성</strong></td>
<td>물리 디스크를 LVM의 PV로 초기화</td>
<td><code>pvcreate /dev/sdX</code></td>
</tr>
<tr>
<td><strong>2. Volume Group (VG) 생성</strong></td>
<td>여러 PV를 묶어 VG를 생성</td>
<td><code>vgcreate my_vg /dev/sda1 /dev/sdb1</code></td>
</tr>
<tr>
<td><strong>3. Logical Volume (LV) 생성</strong></td>
<td>VG에서 논리적 볼륨을 생성</td>
<td><code>lvcreate -L 20G -n my_lv my_vg</code></td>
</tr>
<tr>
<td><strong>4. 파일 시스템 생성</strong></td>
<td>LV에 파일 시스템을 생성</td>
<td><code>mkfs.ext4 /dev/my_vg/my_lv</code></td>
</tr>
<tr>
<td><strong>5. LV 마운트 및 사용</strong></td>
<td>LV를 마운트하여 사용</td>
<td><code>mount /dev/my_vg/my_lv /mnt/data</code></td>
</tr>
<tr>
<td><strong>6. LV 크기 조정</strong></td>
<td>LV의 크기를 조정 (필요시)</td>
<td><strong>확장</strong>: <code>lvextend -L +10G /dev/my_vg/my_lv</code> <br> <strong>축소</strong>: <code>lvreduce -L -5G /dev/my_vg/my_lv</code></td>
</tr>
<tr>
<td><strong>7. 스냅샷 생성</strong></td>
<td>LV의 특정 시점 스냅샷을 생성 (선택사항)</td>
<td><code>lvcreate -s -L 5G -n snap_lv /dev/my_vg/my_lv</code></td>
</tr>
<tr>
<td><strong>8. LV 삭제</strong></td>
<td>LV를 삭제 (필요시)</td>
<td><code>lvremove /dev/my_vg/my_lv</code></td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Linux] RAID 0~6 특징 및 용량 계산 방법]]></title>
            <link>https://velog.io/@fever-max/Linux-RAID%EB%9E%80-RAID-06-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EC%9A%A9%EB%9F%89-%EA%B3%84%EC%82%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@fever-max/Linux-RAID%EB%9E%80-RAID-06-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EC%9A%A9%EB%9F%89-%EA%B3%84%EC%82%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 02 Sep 2024 14:34:43 GMT</pubDate>
            <description><![CDATA[<h2 id="⚙️-raid-redundant-array-of-independent-disks">⚙️ RAID (Redundant Array of Independent Disks)</h2>
<p>여러 개의 하드디스크를 결합하여 하나의 논리적인 디스크처럼 동작하게 하는 기술로 데이터의 성능, 안정성, 용량을 개선하기 위해 사용된다. RAID는 여러 수준(Level)으로 나뉘며, 각 수준마다 특성과 목적이 다르다.</p>
<table>
<thead>
<tr>
<th>RAID 레벨</th>
<th>특성</th>
<th>최소 디스크 수</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>RAID 0</strong></td>
<td>스트라이핑 (Striping)</td>
<td>2</td>
<td>높은 성능 (읽기 및 쓰기 속도 향상)</td>
<td>데이터 보호 없음, 한 디스크라도 손실되면 모든 데이터 손실</td>
</tr>
<tr>
<td><strong>RAID 1</strong></td>
<td>미러링 (Mirroring)</td>
<td>2</td>
<td>높은 데이터 보호 (디스크 하나 손실 시에도 데이터 유지)</td>
<td>저장 용량 감소 (전체 용량의 50% 사용 가능)</td>
</tr>
<tr>
<td><strong>RAID 2</strong></td>
<td>비트 수준 스트라이핑 (Bit-Level Striping)</td>
<td>3</td>
<td>에러 수정 가능 (Hamming 코드 사용)</td>
<td>구현이 복잡하고, 비효율적이며 거의 사용되지 않음</td>
</tr>
<tr>
<td><strong>RAID 3</strong></td>
<td>바이트 수준 스트라이핑 (Byte-Level Striping) + 패리티</td>
<td>3</td>
<td>높은 전송 속도, 단일 디스크 장애 복구 가능</td>
<td>구현 복잡, 다수의 디스크 필요</td>
</tr>
<tr>
<td><strong>RAID 4</strong></td>
<td>블록 수준 스트라이핑 (Block-Level Striping) + 패리티</td>
<td>3</td>
<td>고속 읽기 성능, 단일 디스크 장애 복구 가능</td>
<td>패리티 디스크 병목 현상 발생 가능</td>
</tr>
<tr>
<td><strong>RAID 5</strong></td>
<td>블록 수준 스트라이핑 + 분산 패리티 (Distributed Parity)</td>
<td>3</td>
<td>읽기 성능 우수, 디스크 하나 손실 시 데이터 복구 가능</td>
<td>쓰기 성능 저하 (패리티 계산), 구현 복잡</td>
</tr>
<tr>
<td><strong>RAID 6</strong></td>
<td>블록 수준 스트라이핑 + 이중 분산 패리티 (Double Distributed Parity)</td>
<td>4</td>
<td>두 개의 디스크 손실에도 데이터 복구 가능</td>
<td>쓰기 성능 저하, 구현 복잡</td>
</tr>
</tbody></table>
<h2 id="📌-raid-레벨별-계산-공식">📌 RAID 레벨별 계산 공식</h2>
<ol>
<li><p><strong>RAID 0</strong></p>
<ul>
<li><strong>가용 용량</strong> = N × S</li>
<li><strong>손실 용량</strong> = 0</li>
<li>성능과 용량 극대화가 목적, 가용 용량은 모든 디스크의 용량 합과 동일</li>
</ul>
</li>
<li><p><strong>RAID 1</strong></p>
<ul>
<li><strong>가용 용량</strong> = S</li>
<li><strong>손실 용량</strong> = (N - 1) × S</li>
<li>데이터 보호를 위해 모든 디스크에 동일한 데이터를 저장, 가용 용량은 단일 디스크의 용량과 동일</li>
</ul>
</li>
<li><p><strong>RAID 5</strong></p>
<ul>
<li><strong>가용 용량</strong> = (N - 1) × S</li>
<li><strong>손실 용량</strong> = S</li>
<li>패리티 정보가 각 디스크에 분산 저장, 전체 디스크 수에서 하나를 제외한 나머지 디스크의 용량 합이 가용 용량.</li>
</ul>
</li>
<li><p><strong>RAID 6</strong></p>
<ul>
<li><strong>가용 용량</strong> = (N - 2) × S</li>
<li><strong>손실 용량</strong> = 2 × S</li>
<li>두 개의 패리티 정보가 각 디스크에 분산 저장, 전체 디스크 수에서 두 개를 제외한 나머지 디스크의 용량 합이 가용 용량.</li>
</ul>
</li>
</ol>
<h2 id="💻-예시-문제-및-풀이">💻 예시 문제 및 풀이</h2>
<h4 id="예시-1-raid-0">예시 1: RAID 0</h4>
<ul>
<li><strong>문제</strong>: 4개의 디스크가 있으며, 각 디스크의 용량이 1TB인 경우, RAID 0의 가용 용량은?</li>
<li><strong>풀이</strong>:<ul>
<li>N = 4, S = 1TB</li>
<li><strong>가용 용량</strong> = N × S = 4 × 1TB = <strong>4TB</strong></li>
</ul>
</li>
</ul>
<h4 id="예시-2-raid-1">예시 2: RAID 1</h4>
<ul>
<li><strong>문제</strong>: 4개의 디스크가 있으며, 각 디스크의 용량이 2TB인 경우, RAID 1의 가용 용량은?</li>
<li><strong>풀이</strong>:<ul>
<li>N = 4, S = 2TB</li>
<li><strong>가용 용량</strong> = S = <strong>2TB</strong></li>
</ul>
</li>
</ul>
<h4 id="예시-3-raid-5">예시 3: RAID 5</h4>
<ul>
<li><strong>문제</strong>: 5개의 디스크가 있으며, 각 디스크의 용량이 1TB인 경우, RAID 5의 가용 용량은?</li>
<li><strong>풀이</strong>:<ul>
<li>N = 5, S = 1TB</li>
<li><strong>가용 용량</strong> = (N - 1) × S = (5 - 1) × 1TB = <strong>4TB</strong></li>
</ul>
</li>
</ul>
<h4 id="예시-4-raid-6">예시 4: RAID 6</h4>
<ul>
<li><strong>문제</strong>: 6개의 디스크가 있으며, 각 디스크의 용량이 1TB인 경우, RAID 6의 가용 용량은?</li>
<li><strong>풀이</strong>:<ul>
<li>N = 6, S = 1TB</li>
<li><strong>가용 용량</strong> = (N - 2) × S = (6 - 2) × 1TB = <strong>4TB</strong></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[99클럽 코테 스터디 42일차 TIL | Best Time to Buy and Sell Stock]]></title>
            <link>https://velog.io/@fever-max/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-42%EC%9D%BC%EC%B0%A8-TIL-Best-Time-to-Buy-and-Sell-Stock</link>
            <guid>https://velog.io/@fever-max/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-42%EC%9D%BC%EC%B0%A8-TIL-Best-Time-to-Buy-and-Sell-Stock</guid>
            <pubDate>Sun, 01 Sep 2024 11:09:34 GMT</pubDate>
            <description><![CDATA[<h2 id="🖥️-문제">🖥️ 문제</h2>
<p>You are given an array prices where prices[i] is the price of a given stock on the ith day.</p>
<p>You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.</p>
<p>Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.</p>
<p><strong>Example 1:</strong>
Input: prices = [7,1,5,3,6,4]
Output: 5
Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.
Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell.</p>
<p><strong>Example 2:</strong>
Input: prices = [7,6,4,3,1]
Output: 0
Explanation: In this case, no transactions are done and the max profit = 0.</p>
<h2 id="📝-풀이">📝 풀이</h2>
<pre><code class="language-java">class Solution {
    public int maxProfit(int[] prices) {
        int buy = prices[0];
        int profit = 0;
        for (int i = 1; i &lt; prices.length; i++) {
            buy = Math.min(buy, prices[i]);
            profit = Math.max(profit, prices[i] - buy);
        }
        return profit;
    }
}
</code></pre>
<p>문제는 주식을 한 번 사고, 나중에 팔아서 벌 수 있는 최대의 이익을 찾는 것이다. </p>
<p>먼저 <code>buy</code>를 첫 번째 날의 주식 가격인 prices[0]으로 초기화하고, 현재까지 주식을 살 수 있는 가장 저렴한 가격을 담는다. 그리고 <code>profit</code>를 0으로 초기화하여, 현재까지 계산된 최대 이익을 저장한다.</p>
<p>다음, 두 번째 날부터 시작하여 <code>(i = 1) 마지막 날</code>까지 반복문을 돌린다. 이후 buy를 업데이트하면서 현재의 buy 값과 i번째 날의 가격을 비교하고 더 작은 값을 선택한다. 또한 <code>Math.max</code>로 현재까지의 최대 이익과 i번째 날의 가격에서 현재 최저 구매 가격을 뺀 값(prices[i] - buy) 중 더 큰 값을 선택한다. </p>
<p>이렇게 반복문을 수행하면 매일 주식을 팔았을 때의 최대 이익을 쉽게 계산할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[99클럽 코테 스터디 41일차 TIL | N-th Tribonacci Number]]></title>
            <link>https://velog.io/@fever-max/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-41%EC%9D%BC%EC%B0%A8-TIL-N-th-Tribonacci-Number</link>
            <guid>https://velog.io/@fever-max/99%ED%81%B4%EB%9F%BD-%EC%BD%94%ED%85%8C-%EC%8A%A4%ED%84%B0%EB%94%94-41%EC%9D%BC%EC%B0%A8-TIL-N-th-Tribonacci-Number</guid>
            <pubDate>Sat, 31 Aug 2024 08:58:51 GMT</pubDate>
            <description><![CDATA[<h2 id="🖥️-문제">🖥️ 문제</h2>
<p><img src="https://velog.velcdn.com/images/fever-max/post/95f92e97-cb88-4d74-9c1f-73708e40c2ea/image.png" alt=""></p>
<h2 id="📝-풀이">📝 풀이</h2>
<pre><code class="language-java">class Solution {
    public int tribonacci(int n) {
         if (n == 0) {
            return 0;
        }

        if (n == 1 || n == 2) {
            return 1;
        }

        int t0 = 0, t1 = 1, t2 = 1;
        int tn = 0;

        for (int i = 3; i &lt;= n; i++) {
            tn = t0 + t1 + t2;
            t0 = t1;
            t1 = t2;
            t2 = tn;
        }

        return tn;
    }
}</code></pre>
<p>문제를 해결하기 위해, 동적 계획법(Dynamic Programming)을 사용해 이전 결과를 활용하여 다음 값을 계산하는 반복문 방법을 선택했다.</p>
<p>Tribonacci 수열의 시작점인 T0, T1, T2 값이 이미 정의되어 있으므로, 이들에 대한 기본 조건을 설정한다. 이후 기본 값을 제외한 나머지 Tribonacci 값은 이전 세 항의 합으로 이루어지므로, 반복문을 통해 이를 계산한다.</p>
]]></description>
        </item>
    </channel>
</rss>