<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>CodeMemoir</title>
    <link>https://what-and-how-to-code.tistory.com/</link>
    <description>모르는 것을 알아가고, 아는 것을 더 깊게 파고드는 공간</description>
    <language>ko</language>
    <pubDate>Thu, 7 May 2026 06:44:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>LoopThinker</managingEditor>
    <image>
      <title>CodeMemoir</title>
      <url>https://tistory1.daumcdn.net/tistory/5319349/attach/2f4d3154b7f64b1f93c028e6b019658e</url>
      <link>https://what-and-how-to-code.tistory.com</link>
    </image>
    <item>
      <title>Test</title>
      <link>https://what-and-how-to-code.tistory.com/290</link>
      <description>&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Python&quot; data-ke-language=&quot;Python&quot;&gt;&lt;code&gt;# ============================================================
# 설정
# ============================================================
DB_CONFIG = {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;host&quot;: &quot;localhost&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;port&quot;: 5432,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;database&quot;: &quot;your_database&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;user&quot;: &quot;your_user&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;password&quot;: &quot;your_password&quot;,
}
TABLE_NAME&amp;nbsp;&amp;nbsp;= &quot;score_data&quot;
ASSET_ID&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;= &quot;asset_001&quot;&amp;nbsp;&amp;nbsp; # 조회할 설비 ID
# ============================================================

import psycopg2
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import json, warnings
warnings.filterwarnings(&quot;ignore&quot;)

plt.rcParams['axes.unicode_minus'] = False
try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;plt.rcParams['font.family'] = 'NanumGothic'
except:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pass

# ------------------------------------------------------------------
# 1. 데이터 로드
# ------------------------------------------------------------------
conn&amp;nbsp;&amp;nbsp;= psycopg2.connect(**DB_CONFIG)
query = f&quot;&quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SELECT event_time, asset_id, causes_param
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FROM {TABLE_NAME}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;WHERE asset_id = %s
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;AND causes_param IS NOT NULL
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ORDER BY event_time
&quot;&quot;&quot;
df_raw = pd.read_sql(query, conn, params=(ASSET_ID,))
conn.close()

df_raw[&quot;event_time&quot;] = pd.to_datetime(df_raw[&quot;event_time&quot;])
print(f&quot;✅ 로드된 행 수: {len(df_raw)}&quot;)

# ------------------------------------------------------------------
# 2. JSON 파싱 → long-form 변환
#&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;구조: causes_param → unique_scen_id2param_prob
#&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;→ { scenario: { param: value, ... }, ... }
# ------------------------------------------------------------------
rows = []
for _, row in df_raw.iterrows():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;data = json.loads(row[&quot;causes_param&quot;])
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;scen_map = data.get(&quot;unique_scen_id2param_prob&quot;, {})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for scen, params in scen_map.items():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for param, value in params.items():
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if value is None or value == 0:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;rows.append({
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;event_time&quot;: row[&quot;event_time&quot;],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;scenario&quot;:&amp;nbsp;&amp;nbsp; scen,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;param&quot;:&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;param,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;value&quot;:&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;float(value),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;})
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;except Exception:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue

df = pd.DataFrame(rows)
print(f&quot;✅ 파싱된 행 수: {len(df)}&quot;)

scenarios = sorted(df[&quot;scenario&quot;].unique())
print(f&quot;  시나리오 ({len(scenarios)}개): {scenarios}&quot;)

# ------------------------------------------------------------------
# 3. 시각화 — 시나리오별 subplot, 파라미터별 라인
# ------------------------------------------------------------------
N_SCEN&amp;nbsp;&amp;nbsp;= len(scenarios)
N_COLS&amp;nbsp;&amp;nbsp;= 2
N_ROWS&amp;nbsp;&amp;nbsp;= (N_SCEN + 1) // N_COLS

cmap_base = plt.get_cmap(&quot;tab20&quot;)

fig, axes = plt.subplots(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;N_ROWS, N_COLS,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;figsize=(20, 5 * N_ROWS),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;constrained_layout=True
)
axes_flat = axes.flatten() if N_SCEN &amp;gt; 1 else [axes]

for idx, scen in enumerate(scenarios):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;= axes_flat[idx]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sub&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; = df[df[&quot;scenario&quot;] == scen]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;params&amp;nbsp;&amp;nbsp;= sorted(sub[&quot;param&quot;].unique())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;n_param = len(params)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;colors&amp;nbsp;&amp;nbsp;= {p: cmap_base(i % 20) for i, p in enumerate(params)}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for param, grp in sub.groupby(&quot;param&quot;):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;grp = grp.sort_values(&quot;event_time&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.plot(grp[&quot;event_time&quot;], grp[&quot;value&quot;],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;marker=&quot;o&quot;, markersize=2, linewidth=1.1,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;label=param, color=colors[param])

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.set_title(f&quot;  {scen}&amp;nbsp;&amp;nbsp;({n_param} params)&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; fontsize=10, fontweight=&quot;bold&quot;, pad=6)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.set_xlabel(&quot;Time&quot;, fontsize=8)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.set_ylabel(&quot;Value&quot;, fontsize=8)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.xaxis.set_major_formatter(mdates.DateFormatter(&quot;%m-%d\n%H:%M&quot;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.xaxis.set_major_locator(mdates.AutoDateLocator())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.tick_params(axis=&quot;x&quot;, labelsize=7)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.tick_params(axis=&quot;y&quot;, labelsize=7)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.grid(True, linestyle=&quot;--&quot;, alpha=0.35)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 범례: 파라미터 수에 따라 폰트 조절
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;legend_fs = 6 if n_param &amp;gt; 20 else 7
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.legend(fontsize=legend_fs, loc=&quot;upper left&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bbox_to_anchor=(1.01, 1), borderaxespad=0,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ncol=1 + n_param // 25)

# 남는 subplot 숨기기
for idx in range(N_SCEN, len(axes_flat)):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;axes_flat[idx].set_visible(False)

fig.suptitle(f&quot;Asset: {ASSET_ID}&amp;nbsp;&amp;nbsp;·&amp;nbsp;&amp;nbsp;Scenario × Parameter&amp;nbsp;&amp;nbsp;Time-Series&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; fontsize=14, fontweight=&quot;bold&quot;)

plt.savefig(&quot;scenario_viz.png&quot;, dpi=150, bbox_inches=&quot;tight&quot;)
plt.show()
print(&quot;✅ 저장 완료 → scenario_viz.png&quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Python&quot; data-ke-language=&quot;Python&quot;&gt;&lt;code&gt;# ------------------------------------------------------------------
# 3. 시각화 — 시나리오별 Figure, 파라미터별 subplot
# ------------------------------------------------------------------
N_COLS = 3&amp;nbsp;&amp;nbsp; # 한 줄에 파라미터 몇 개씩 배치할지

cmap_base = plt.get_cmap(&quot;tab20&quot;)

for scen in scenarios:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sub&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;= df[df[&quot;scenario&quot;] == scen]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;params = sorted(sub[&quot;param&quot;].unique())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;n_param = len(params)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;colors&amp;nbsp;&amp;nbsp;= {p: cmap_base(i % 20) for i, p in enumerate(params)}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;N_ROWS = (n_param + N_COLS - 1) // N_COLS
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fig, axes = plt.subplots(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;N_ROWS, N_COLS,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;figsize=(18, 3.5 * N_ROWS),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;constrained_layout=True
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;axes_flat = axes.flatten() if n_param &amp;gt; 1 else [axes]

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for idx, param in enumerate(params):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax&amp;nbsp;&amp;nbsp;= axes_flat[idx]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;grp = sub[sub[&quot;param&quot;] == param].sort_values(&quot;event_time&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.plot(grp[&quot;event_time&quot;], grp[&quot;value&quot;],
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;marker=&quot;o&quot;, markersize=2.5, linewidth=1.2,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;color=colors[param])

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.set_title(param, fontsize=9, fontweight=&quot;bold&quot;, pad=5)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.set_xlabel(&quot;Time&quot;, fontsize=7.5)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.set_ylabel(&quot;Value&quot;, fontsize=7.5)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.xaxis.set_major_formatter(mdates.DateFormatter(&quot;%m-%d\n%H:%M&quot;))
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.xaxis.set_major_locator(mdates.AutoDateLocator())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.tick_params(axis=&quot;x&quot;, labelsize=6.5)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.tick_params(axis=&quot;y&quot;, labelsize=7)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ax.grid(True, linestyle=&quot;--&quot;, alpha=0.35)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;# 남는 subplot 숨기기
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for idx in range(n_param, len(axes_flat)):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;axes_flat[idx].set_visible(False)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fig.suptitle(f&quot;Asset: {ASSET_ID}&amp;nbsp;&amp;nbsp;·&amp;nbsp;&amp;nbsp;Scenario: {scen}&amp;nbsp;&amp;nbsp;({n_param} params)&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; fontsize=13, fontweight=&quot;bold&quot;)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fname = f&quot;scenario_{scen}.png&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;plt.savefig(fname, dpi=150, bbox_inches=&quot;tight&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;plt.show()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;print(f&quot;✅ 저장 완료 → {fname}&quot;)
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/290</guid>
      <comments>https://what-and-how-to-code.tistory.com/290#entry290comment</comments>
      <pubDate>Wed, 6 May 2026 15:21:50 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 이미지 CVE 대응 정리 CVE-2025-69720, CVE-2025-32434 (python:3.9-slim, PyTorch)</title>
      <link>https://what-and-how-to-code.tistory.com/286</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 이미지 스캔 과정에서 여러 CVE가 발견되었고, 이를 해결하는 과정에서 다음과 같은 질문이 발생했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;base image 문제인지, 라이브러리 문제인지 어떻게 구분할까?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apt-get install -y --no-install-recommends&lt;/code&gt;는 왜 쓰는 걸까?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CVE-2025-32434&lt;/code&gt;는 왜 발생했고 어떻게 해결해야 할까?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requirements.txt&lt;/code&gt;에서는 무엇을 바꿔야 할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 이번 문제는 &lt;b&gt;OS 패키지와 Python 패키지 취약점을 구분하고 각각 다른 방식으로 해결하는 과정&lt;/b&gt;이 핵심이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제의 시작: CVE 탐지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 중인 base image는 아래와 같았습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;python:3.9.12-slim&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 스캔 결과 여러 취약점이 발견되었고, 처음에는 base image 문제로 판단했지만 분석 결과 &lt;b&gt;취약점 종류에 따라 해결 방법이 완전히 다르다&lt;/b&gt;는 것을 알게 되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 취약점 유형 구분 (핵심 포인트)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 해결하기 위해 가장 먼저 정리한 기준은 다음과 같습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;해결 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OS 패키지 취약점&lt;/td&gt;
&lt;td&gt;apt (update, upgrade, purge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python 패키지 취약점&lt;/td&gt;
&lt;td&gt;pip (requirements 수정)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 구분하지 않으면 계속 엉뚱한 방향으로 대응하게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. OS 패키지 관련 대응&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에 발견된 취약점 중 일부는 Debian 패키지 기반이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 접근 방식은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;RUN apt-get update \
    &amp;amp;&amp;amp; apt-get upgrade -y \
    &amp;amp;&amp;amp; apt-get install -y --no-install-recommends 패키지 \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 질문이 하나 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q. &lt;code&gt;--no-install-recommends&lt;/code&gt;는 왜 쓰는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 필수 패키지만 설치하고 불필요한 패키지를 제거하기 위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지는 다음과 같이 나뉩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Depends: 필수&lt;/li&gt;
&lt;li&gt;Recommends: 권장 (기본 설치됨)&lt;/li&gt;
&lt;li&gt;Suggests: 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션을 사용하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 용량 감소&lt;/li&gt;
&lt;li&gt;공격 표면 감소 (CVE 감소)&lt;/li&gt;
&lt;li&gt;빌드 속도 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Docker에서는 사실상 필수 옵션입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 핵심 문제: CVE-2025-32434&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 계속 추적하다 보니, 이번 이슈의 핵심은 OS가 아니라 &lt;b&gt;PyTorch&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;code&gt;requirements.txt&lt;/code&gt; 일부는 아래와 같았습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;--find-links https://download.pytorch.org/whl/torch_stable.html
torch==2.0.0+cpu
torchvision==0.15.1+cpu
torchaudio==2.0.1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 질문이 생겼습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q. 왜 이게 문제인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. &lt;code&gt;torch==2.0.0&lt;/code&gt;은 취약 버전이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CVE-2025-32434&lt;/code&gt;는 PyTorch에서 발생하는 취약점이며,&lt;br /&gt;&lt;b&gt;2.5.1 이하 버전에서 영향&lt;/b&gt;, &lt;b&gt;2.6.0에서 패치&lt;/b&gt;되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 해결 방법: PyTorch 업그레이드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 정의 이후 해결 방법은 명확했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;torch==2.0.0+cpu
torchvision==0.15.1+cpu
torchaudio==2.0.1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;--index-url https://download.pytorch.org/whl/cpu
torch==2.6.0
torchvision==0.21.0
torchaudio==2.6.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 단순히 torch만 올리는 것이 아니라&lt;br /&gt;&lt;b&gt;버전 호환 세트를 맞추는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Q. &lt;code&gt;--index-url&lt;/code&gt;은 꼭 필요할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분도 중요한 고민 포인트였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Docker 환경에서는 유지하는 것이 안전&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PyTorch는 일반 PyPI보다 전용 wheel 저장소 사용이 안정적&lt;/li&gt;
&lt;li&gt;CPU/GPU 환경에 따라 wheel이 다름&lt;/li&gt;
&lt;li&gt;잘못된 wheel 설치 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래 구조 유지가 가장 안정적입니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;--index-url https://download.pytorch.org/whl/cpu&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 추가로 발견한 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;requirements.txt&lt;/code&gt;를 정리하는 과정에서 아래 문제가 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;PyYAML==6.0
PyYAML&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;PyYAML==6.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 선언은 제거하는 것이 안전합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 최종 requirements.txt&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 정리된 파일은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;--index-url https://download.pytorch.org/whl/cpu
torch==2.6.0
torchvision==0.21.0
torchaudio==2.6.0
fastapi==0.95.0
pandas==2.2.2
uvicorn==0.21.1
pytest==7.1.1
tqdm==4.64.0
requests==2.27.1
psycopg2-binary==2.9.5
scikit-learn==1.3.0
numpy==1.26.4
PyYAML==6.0
lmdb==1.4.1
kafka-python==2.0.2
python-dotenv
boto3==1.40.40
shap==0.49.1
SQLAlchemy==2.0.45&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 결과 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 캐시 때문에 반드시 캐시 없이 재빌드해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;docker build --no-cache -t your-image .&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치된 버전 확인:&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;python -c &quot;import torch, torchvision, torchaudio; print(torch.__version__, torchvision.__version__, torchaudio.__version__)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 출력:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2.6.0 0.21.0 2.6.0&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 결과 및 출력&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PyTorch 취약점 제거&lt;/li&gt;
&lt;li&gt;CVE-2025-32434 해결&lt;/li&gt;
&lt;li&gt;불필요 패키지 정리&lt;/li&gt;
&lt;li&gt;requirements 구조 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 응용 및 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) CVE는 무조건 base image 문제가 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Python 패키지일 가능성도 항상 확인&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) Docker는 최소 설치가 핵심&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;code&gt;--no-install-recommends&lt;/code&gt; 적극 활용&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) ML 라이브러리는 버전 호환이 중요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; torch / torchvision / torchaudio 세트로 관리&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) requirements.txt 정리는 필수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 중복, 불필요 패키지 제거&lt;/p&gt;</description>
      <category>개발 (Development)/Docker</category>
      <category>backend</category>
      <category>CVE</category>
      <category>DevOps</category>
      <category>docker</category>
      <category>dockerfile</category>
      <category>ML</category>
      <category>python</category>
      <category>pytorch</category>
      <category>Requirements</category>
      <category>보안취약점</category>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/286</guid>
      <comments>https://what-and-how-to-code.tistory.com/286#entry286comment</comments>
      <pubDate>Fri, 3 Apr 2026 23:14:36 +0900</pubDate>
    </item>
    <item>
      <title>[Java] DTO에서 값이 설정되지 않았을 때 필드를 제외하는 방법</title>
      <link>https://what-and-how-to-code.tistory.com/285</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 API 응답을 만들던 중 DTO에 &lt;code&gt;value&lt;/code&gt;라는 필드가 있었는데,&lt;br /&gt;이 값을 설정하지 않았을 때도 JSON 응답에 &lt;code&gt;&quot;value&quot;: null&lt;/code&gt; 형태로 포함되는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 동작은 다음과 같았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값이 설정되지 않은 경우 &amp;rarr; JSON에서 필드 자체가 없어야 함&lt;/li&gt;
&lt;li&gt;값이 설정된 경우 &amp;rarr; 정상적으로 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 불필요한 null 필드를 응답에서 제거하고 싶었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면 Jackson의 &lt;code&gt;@JsonInclude&lt;/code&gt; 옵션을 사용하면 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO 클래스에 아래와 같이 설정하면 된다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ExampleDto {

    private String value;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 값을 설정하지 않은 경우&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;ExampleDto dto = new ExampleDto();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 JSON&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;code&gt;value&lt;/code&gt; 필드 자체가 사라진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 값을 설정한 경우&lt;/h3&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;dto.setValue(&quot;abc&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 JSON&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;value&quot;: &quot;abc&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 정상적으로 포함된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 따라 null 뿐 아니라 빈 값도 제외하고 싶을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NON_EMPTY 옵션&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@JsonInclude(JsonInclude.Include.NON_EMPTY)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션은 다음을 모두 제외한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;null&lt;/li&gt;
&lt;li&gt;&quot;&quot;&lt;/li&gt;
&lt;li&gt;빈 리스트&lt;/li&gt;
&lt;li&gt;빈 Map&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특정 필드에만 적용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO 전체가 아니라 특정 필드에만 적용할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public class ExampleDto {

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String value;

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전역 설정 (Spring Boot)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 DTO에 적용하고 싶다면 application.yml에서 설정 가능하다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;spring:
  jackson:
    default-property-inclusion: non_null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 모든 응답에서 null 필드가 자동으로 제외된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DTO에서 값이 설정되지 않은 필드를 숨기려면 &lt;code&gt;@JsonInclude&lt;/code&gt; 사용&lt;/li&gt;
&lt;li&gt;가장 일반적인 옵션은 &lt;code&gt;NON_NULL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;빈 값까지 제거하려면 &lt;code&gt;NON_EMPTY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;전역 적용도 가능&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발 (Development)/Java</category>
      <category>API</category>
      <category>backend</category>
      <category>DTO</category>
      <category>jackson</category>
      <category>java</category>
      <category>JSON</category>
      <category>Spring</category>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/285</guid>
      <comments>https://what-and-how-to-code.tistory.com/285#entry285comment</comments>
      <pubDate>Fri, 3 Apr 2026 23:04:31 +0900</pubDate>
    </item>
    <item>
      <title>[Java/MyBatis] ${} 사용 시 SQL Injection을 피하는 안전한 동적 컬럼 처리 방법</title>
      <link>https://what-and-how-to-code.tistory.com/281</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis에서 동적 컬럼과 테이블명을 처리할 때 &lt;code&gt;${}&lt;/code&gt;를 잘못 사용하면 SQL Injection 취약점이 발생할 수 있습니다. 특히 &lt;code&gt;array_agg()&lt;/code&gt;와 &lt;code&gt;&amp;lt;foreach&amp;gt;&lt;/code&gt;를 활용해 컬럼을 동적으로 구성하는 경우, 보안과 구조를 동시에 고려해야 합니다. 이 글에서는 실제 구현 과정에서 겪은 문제를 정리하고, 안전하게 해결하는 방법을 단계별로 설명하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제 정의: 동적 컬럼 + array_agg + foreach&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 요구사항이 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;event_time은 하나의 배열로 반환&lt;/li&gt;
&lt;li&gt;여러 파라미터 컬럼도 각각 배열로 반환&lt;/li&gt;
&lt;li&gt;파라미터 목록은 동적으로 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 쿼리 형태는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;select
    array_agg(event_time order by event_time) as event_time,
    array_agg(column_a order by event_time) as column_a,
    array_agg(column_b order by event_time) as column_b
from some_table&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 MyBatis &lt;code&gt;&amp;lt;foreach&amp;gt;&lt;/code&gt;로 구현하면 다음과 같이 작성하게 됩니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;&amp;lt;foreach item=&quot;item&quot; collection=&quot;parameters&quot; separator=&quot;,&quot;&amp;gt;
    array_agg(${item} order by event_time) as &quot;${item}&quot;
&amp;lt;/foreach&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 문제가 발생합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 핵심 원인: #{ } 와 ${ } 의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MyBatis에는 두 가지 문법이 존재합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;#{ } &amp;rarr; PreparedStatement 바인딩 (값 전용, 안전)&lt;/li&gt;
&lt;li&gt;${ } &amp;rarr; 문자열 그대로 치환 (SQL 구조 변경 가능, 위험)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼명이나 테이블명은 SQL 구조이므로 &lt;code&gt;#{}&lt;/code&gt;를 사용할 수 없고 &lt;code&gt;${}&lt;/code&gt;를 써야 합니다.&lt;br /&gt;문제는 사용자가 &lt;code&gt;${}&lt;/code&gt;로 들어가는 값을 조작할 수 있다면, SQL Injection이 발생한다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 다음과 같이 조작한다면:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;column_a; drop table users; --&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 그대로 SQL에 삽입됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 결론은 명확합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자 입력이 직접 &lt;code&gt;${}&lt;/code&gt;에 들어가면 안 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 안전한 해결 방법 1: 화이트리스트 매핑 (정석 방법)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 안전한 방법은 사용자 입력을 서버에서 허용된 컬럼명으로 매핑하는 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 서버에서 허용 컬럼 정의&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;private static final Map&amp;lt;String, String&amp;gt; ALLOWED_COLUMNS = Map.of(
    &quot;speed&quot;, &quot;\&quot;column_speed\&quot;&quot;,
    &quot;status&quot;, &quot;\&quot;column_status\&quot;&quot;
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 사용자 입력을 안전 컬럼으로 변환&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; safeColumns = userInputList.stream()
    .map(ALLOWED_COLUMNS::get)
    .filter(Objects::nonNull)
    .toList();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 사용자가 어떤 값을 보내더라도, 서버에 정의된 컬럼만 통과합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. MyBatis에서는 안전 값만 사용&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;&amp;lt;foreach item=&quot;item&quot; collection=&quot;safeColumns&quot; separator=&quot;,&quot;&amp;gt;
    , array_agg(${item} order by event_time) as ${itemAlias}
&amp;lt;/foreach&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 &lt;code&gt;${item}&lt;/code&gt;이 이미 서버에서 검증된 값이라는 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 안전한 해결 방법 2: 테이블명 동적 처리 시 주의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블명도 &lt;code&gt;${}&lt;/code&gt;를 사용해야 하므로 매우 위험합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 예:&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;from ${tableName}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안전한 예:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;int index = Integer.parseInt(userInput);
if (index &amp;lt; 0 || index &amp;gt; 9999) {
    throw new IllegalArgumentException();
}
String tableName = String.format(&quot;trace_table_%04d&quot;, index);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 숫자 검증 + 포맷 강제 방식으로 서버에서 생성해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 더 안전한 구조: 동적 컬럼 자체를 제거하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안을 최우선으로 한다면, 구조를 바꾸는 것도 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가로 배열 구조 대신 세로 구조로 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;select event_time, param_name, param_value
from trace_table
where ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 애플리케이션에서 pivot 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;code&gt;${}&lt;/code&gt; 자체를 제거할 수 있어 가장 안전합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 최종 정리된 안전 구조&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자 입력은 절대 직접 &lt;code&gt;${}&lt;/code&gt;에 넣지 않는다.&lt;/li&gt;
&lt;li&gt;서버에서 화이트리스트로 매핑한다.&lt;/li&gt;
&lt;li&gt;alias도 서버 생성 값으로 제한한다.&lt;/li&gt;
&lt;li&gt;테이블명은 숫자 검증 후 포맷으로 생성한다.&lt;/li&gt;
&lt;li&gt;가능하면 쿼리 구조를 단순화한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구성하면 다음을 모두 만족합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동적 파라미터 배열 반환&lt;/li&gt;
&lt;li&gt;array_agg 기반 시계열 구조 유지&lt;/li&gt;
&lt;li&gt;SQL Injection 차단&lt;/li&gt;
&lt;li&gt;유지보수 가능 구조 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 배운 점 &amp;middot; 시사점 &amp;middot; 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 단순히 동적 SQL을 작성하는 것과, 안전하게 작성하는 것은 전혀 다른 문제라는 점을 다시 확인했습니다. &lt;code&gt;${}&lt;/code&gt;는 편리하지만 동시에 가장 위험한 도구입니다. 특히 컬럼명과 테이블명을 동적으로 구성하는 경우, 반드시 서버 레벨에서 통제해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시사점은 명확합니다.&lt;br /&gt;동적 SQL이 필요한 경우, &amp;ldquo;입력을 어떻게 막을 것인가&amp;rdquo;를 먼저 설계해야 합니다. 기능 구현보다 보안 설계가 우선입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, MyBatis에서 &lt;code&gt;${}&lt;/code&gt;는 사용해도 되지만 &amp;ldquo;사용자가 조작할 수 없는 값&amp;rdquo;일 때만 허용해야 합니다. 그 외의 경우에는 구조를 재설계하는 것이 장기적으로 더 안전합니다.&lt;/p&gt;</description>
      <category>개발 (Development)/Java</category>
      <category>array_agg</category>
      <category>Java백엔드</category>
      <category>MyBatis</category>
      <category>PostgreSQL</category>
      <category>sqlinjection</category>
      <category>동적쿼리</category>
      <category>백엔드보안</category>
      <category>서버보안</category>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/281</guid>
      <comments>https://what-and-how-to-code.tistory.com/281#entry281comment</comments>
      <pubDate>Sat, 28 Feb 2026 01:12:44 +0900</pubDate>
    </item>
    <item>
      <title>[Python] FastAPI 백그라운드 학습에서 메모리 사용량 급증을 줄이는 리팩터링 정리</title>
      <link>https://what-and-how-to-code.tistory.com/276</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI의 백그라운드 작업에서 대용량 학습 로직을 실행하면, 요청이 누적될수록 프로세스 메모리가 지속 증가하는 현상이 발생할 수 있습니다. 본 글에서는 실제 코드 흐름에서 메모리 사용량이 커지는 원인을 짚고, DataFrame/NumPy 처리 및 finally 정리 전략을 통해 메모리 부담을 줄이는 방법을 정리합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 상황에서 메모리 사용량 급증 또는 누수처럼 보이는 현상이 발생할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 요청 처리 프로세스 내부에서 장시간 학습/전처리 로직을 실행한다.&lt;/li&gt;
&lt;li&gt;시나리오(또는 그룹) 루프마다 대형 DataFrame을 &lt;code&gt;copy()&lt;/code&gt;하여 반복 생성한다.&lt;/li&gt;
&lt;li&gt;NumPy 배열 변환 과정에서 &lt;code&gt;astype()&lt;/code&gt; 등으로 대형 배열 복사가 여러 번 일어난다.&lt;/li&gt;
&lt;li&gt;동일한 크기의 마스크(&lt;code&gt;np.isnan&lt;/code&gt;)를 반복 계산하여 임시 객체가 계속 생성된다.&lt;/li&gt;
&lt;li&gt;결과 딕셔너리에 큰 모델/설명기(직렬화 결과 등)를 중복 저장하여 참조가 오래 유지된다.&lt;/li&gt;
&lt;li&gt;예외 발생 시 정리(cleanup)가 보장되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &amp;ldquo;루프마다 새로 만들 필요가 있는 것은 데이터 전체 복사본이 아니라, 시나리오별로 달라지는 라벨/행 인덱스/선택된 피처 및 최종 입력(X, y)만&amp;rdquo;이라는 점입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드/방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) &lt;code&gt;data_copy = data&lt;/code&gt;는 원본에 영향을 준다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 복사가 아니라 참조입니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;data_view = base_table&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;data_view&lt;/code&gt;에서 컬럼을 추가/수정하거나 &lt;code&gt;.loc[...] = ...&lt;/code&gt; 같은 in-place 변경을 하면 &lt;code&gt;base_table&lt;/code&gt;도 함께 변경됩니다. 원본을 불변으로 유지하려면 &amp;ldquo;데이터 전체 복사&amp;rdquo; 대신 &amp;ldquo;라벨/마스크만 분리&amp;rdquo;하는 접근이 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 시나리오 루프에서 DataFrame 전체 복사 대신 라벨 배열 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 패턴(메모리 부담 큼):&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시나리오마다 대형 테이블을 &lt;code&gt;copy()&lt;/code&gt;로 복제&lt;/li&gt;
&lt;li&gt;라벨 컬럼을 추가하고 구간별로 &lt;code&gt;.loc&lt;/code&gt;로 수정&lt;/li&gt;
&lt;li&gt;마지막에 numpy 변환 및 &lt;code&gt;astype&lt;/code&gt;로 재복사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 패턴(권장):&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원본 테이블은 불변(복사하지 않음)&lt;/li&gt;
&lt;li&gt;시나리오별로 &lt;code&gt;label_array&lt;/code&gt;(NumPy)를 생성/갱신&lt;/li&gt;
&lt;li&gt;시나리오별로 필요한 행/열만 선택해 X를 한 번만 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시(개념 코드):&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import numpy as np

def train_by_group(base_table, time_index, group_ids):
    n = len(base_table)

    for group_id in group_ids:
        # 시나리오별 라벨만 생성(가벼움)
        label_array = np.full(n, 2, dtype=np.int8)

        # 구간 규칙에 따라 label_array만 업데이트
        # label_array[mask_a] = 0
        # label_array[mask_b] = 1

        # 시나리오별로 사용할 컬럼/행 인덱스 계산
        feature_cols = compute_feature_columns(base_table, group_id)
        row_idx = compute_valid_rows(base_table, feature_cols)

        # 최종 입력 X, y만 생성(필요한 부분만 복사)
        X = base_table.iloc[row_idx, feature_cols].to_numpy(dtype=np.float32, copy=True)
        y = label_array[row_idx]

        run_model_training(X, y)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시나리오 개수만큼 대형 DataFrame 복사본을 만들지 않는다.&lt;/li&gt;
&lt;li&gt;원본 데이터 오염 위험이 없다.&lt;/li&gt;
&lt;li&gt;메모리 피크가 &amp;ldquo;필요한 X 생성 시점&amp;rdquo;으로 제한된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 전처리 함수에서 deepcopy, 반복 astype, 반복 isnan 제거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전처리/피처 선택 함수에서 흔히 메모리를 크게 쓰는 패턴은 다음입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;deepcopy(input_array)&lt;/code&gt;로 입력 전체를 복사&lt;/li&gt;
&lt;li&gt;&lt;code&gt;astype(float32/float64)&lt;/code&gt;를 중간에 여러 번 수행&lt;/li&gt;
&lt;li&gt;&lt;code&gt;np.isnan(data)&lt;/code&gt;를 여러 번 호출하여 같은 크기의 boolean 배열을 반복 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;np.unique(..., return_counts=True)&lt;/code&gt;로 불필요한 count 배열까지 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 목표는 &amp;ldquo;float 변환은 1회, isnan 마스크는 1회, unique는 필요한 컬럼에만&amp;rdquo;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 동작을 유지하면서 메모리 생성을 줄인 최적화 예시입니다. (변수/함수명은 일반화했습니다.)&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import numpy as np

def select_features_optimized(
    input_array: np.ndarray,
    categorical_threshold: int = 15,
    nan_ratio_threshold: float = 0.3,
):
    &quot;&quot;&quot;
    반환:
      nan_ratio_cols, categorical_cols, valid_row_index, drop_cols, use_cols

    핵심 최적화:
    - 입력 deepcopy 제거
    - float 변환 1회
    - np.isnan 마스크 1회 생성 후 재사용
    - unique는 후보 컬럼에만 수행, return_counts 제거
    &quot;&quot;&quot;

    if input_array is None:
        return [], [], np.array([], dtype=int), set(), []

    arr = np.asarray(input_array)
    if arr.size == 0 or arr.shape[0] &amp;lt; 1:
        return [], [], np.array([], dtype=int), set(), []

    n_rows, n_cols = arr.shape

    # 1) 문자열/객체 컬럼 탐지(필요 최소)
    text_cols = []
    if arr.dtype == object or arr.dtype.kind in (&quot;U&quot;, &quot;S&quot;):
        for c in range(n_cols):
            col = arr[:, c]
            try:
                _ = col.astype(np.float32, copy=False)
            except Exception:
                text_cols.append(c)

    # 2) float 배열 1회 생성
    if text_cols:
        text_set = set(text_cols)
        arr_f = np.empty((n_rows, n_cols), dtype=np.float32)
        for c in range(n_cols):
            if c in text_set:
                arr_f[:, c] = np.nan
            else:
                arr_f[:, c] = arr[:, c].astype(np.float32, copy=False)
    else:
        arr_f = arr.astype(np.float32, copy=False)

    # 3) isnan 마스크 1회 생성
    nan_mask = np.isnan(arr_f)

    # 4) 컬럼별 NaN 포함/비율 계산
    nan_included = nan_mask.any(axis=0)
    nan_ratio = nan_mask.mean(axis=0)  # axis=0이면 분모는 행 수가 되어야 함

    nan_ratio_cols = np.flatnonzero(nan_ratio &amp;gt; nan_ratio_threshold).tolist()

    # 5) 범주형 컬럼 탐색(삭제될 컬럼은 제외)
    drop_by_nan = set(nan_ratio_cols)
    categorical_cols = []

    for c in range(n_cols):
        if c in drop_by_nan:
            continue
        if c in set(text_cols):
            continue

        col = arr_f[:, c]
        col = col[~np.isnan(col)]
        if col.size == 0:
            continue

        if np.unique(col).size &amp;lt; categorical_threshold:
            categorical_cols.append(c)

    categorical_cols.extend(text_cols)
    categorical_cols = sorted(set(categorical_cols))

    # 6) 삭제/사용 컬럼
    drop_cols = set(nan_ratio_cols) | set(categorical_cols)
    use_cols = [c for c in range(n_cols) if c not in drop_cols]

    # 7) 사용할 컬럼 기준으로 NaN 없는 행 인덱스 계산
    if use_cols:
        row_has_nan = nan_mask[:, use_cols].any(axis=1)
        valid_row_index = np.flatnonzero(~row_has_nan)
    else:
        valid_row_index = np.array([], dtype=int)

    return nan_ratio_cols, categorical_cols, valid_row_index, drop_cols, use_cols&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식으로 바꾸면 다음이 개선됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 전체 deepcopy 제거로 메모리 피크 감소&lt;/li&gt;
&lt;li&gt;&lt;code&gt;np.isnan&lt;/code&gt; 중복 호출 제거로 임시 배열 생성 감소&lt;/li&gt;
&lt;li&gt;&lt;code&gt;astype&lt;/code&gt; 다중 호출 제거로 대형 복사 횟수 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) &lt;code&gt;finally&lt;/code&gt;에서 try 블록의 객체 정리 가능 여부와 안전한 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;finally&lt;/code&gt;에서 &lt;code&gt;try&lt;/code&gt; 블록 안에서 생성된 객체를 &lt;code&gt;del&lt;/code&gt;로 제거하는 것은 가능합니다. 다만 예외가 중간에 발생하면 변수가 아직 생성되지 않았을 수 있으므로, 안전한 패턴을 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권장 패턴(사전 초기화):&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import gc

def run_job(...):
    big_table = None
    work_array = None
    model_blob = None

    try:
        big_table = load_table(...)
        work_array = make_array(big_table)
        model_blob = train_model(work_array)
        return {&quot;status&quot;: &quot;ok&quot;}
    finally:
        # 생성 여부와 관계없이 안전
        del big_table
        del work_array
        del model_blob
        gc.collect()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의 사항:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서 주입된 커넥션/프로듀서(예: 메시지 브로커 클라이언트)는 &lt;code&gt;finally&lt;/code&gt;에서 무조건 &lt;code&gt;close()&lt;/code&gt;하면 다음 요청에서 재사용 불가 문제가 생길 수 있습니다.&lt;/li&gt;
&lt;li&gt;전송 완료 보장은 &lt;code&gt;flush()&lt;/code&gt;를 적절한 위치(루프 밖 1회)에서 수행하는 방식이 더 안전합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과/출력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 변경을 적용하면 다음 효과를 기대할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시나리오 수가 늘어도 DataFrame 전체 복사본 생성이 없어져 메모리 피크가 크게 감소한다.&lt;/li&gt;
&lt;li&gt;같은 크기의 마스크/배열을 반복 생성하지 않아 GC 부담이 줄어든다.&lt;/li&gt;
&lt;li&gt;큰 결과물(모델/설명기)을 딕셔너리에 중복 저장하지 않도록 하면 참조 유지로 인한 메모리 고착이 감소한다.&lt;/li&gt;
&lt;li&gt;예외가 발생해도 &lt;code&gt;finally&lt;/code&gt;에서 정리가 보장되어 &amp;ldquo;누수처럼 보이는 현상&amp;rdquo;을 완화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;응용/팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 큰 결과물은 반환/로그/메시지에 중복 저장하지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 모델 직렬화 결과(문자열/바이트/딕셔너리)를 다음과 같이 여러 곳에 동시에 담으면 메모리가 오래 유지됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태 반환 객체&lt;/li&gt;
&lt;li&gt;내부 누적 결과 딕셔너리&lt;/li&gt;
&lt;li&gt;메시지 전송 payload&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권장 방식:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저장소(오브젝트 스토리지/DB)에 저장 후 키만 반환/전송&lt;/li&gt;
&lt;li&gt;혹은 반환에는 길이/해시/버전 등 메타 정보만 담기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 전송 클라이언트의 flush는 루프 밖에서 1회만&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시나리오마다 &lt;code&gt;flush()&lt;/code&gt;를 호출하면 불필요한 대기와 내부 버퍼 관리 부담이 커질 수 있습니다. 가능하면 루프 끝에서 1회 호출로 합칩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 데이터 타입을 &lt;code&gt;float64&lt;/code&gt;로 올리는 순간 메모리는 즉시 커진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 &lt;code&gt;float32&lt;/code&gt;로 충분한지 먼저 검토합니다. 특히 입력 행/열이 큰 경우 효과가 큽니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 근본적으로는 학습 작업을 API 프로세스 밖으로 분리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API는 &amp;ldquo;요청 접수 및 작업 큐 발행&amp;rdquo;만, 학습은 별도 워커 프로세스에서 수행하는 구조가 가장 안정적입니다. 이는 단순한 코드 최적화보다 장애 가능성을 근본적으로 낮춥니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 정리의 요지는 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시나리오별로 데이터가 달라진다고 해서 매번 전체 DataFrame을 복사할 필요는 없습니다. 원본을 불변으로 두고, 시나리오별로 달라지는 라벨/인덱스/X,y만 생성하는 방식이 메모리 효율이 가장 좋습니다.&lt;/li&gt;
&lt;li&gt;전처리 함수에서는 deepcopy와 반복적인 &lt;code&gt;astype&lt;/code&gt;, 반복적인 &lt;code&gt;np.isnan&lt;/code&gt; 계산을 제거하면 메모리 피크와 임시 객체 생성을 크게 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;finally&lt;/code&gt;에서는 &lt;code&gt;try&lt;/code&gt; 내 객체 정리가 가능하며, 사전 초기화 패턴으로 예외 시에도 안전하게 정리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;최종적으로는 장시간 학습 로직은 API 프로세스에서 분리하는 것이 가장 안정적인 방향입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배운 점과 시사점은 &amp;ldquo;복사는 안전하지만 비싸며, 불변 원본 + 필요한 부분만 생성하는 설계가 대규모 데이터 처리에서 가장 중요하다&amp;rdquo;는 것입니다. 또한 예외 처리와 정리 로직이 보장되지 않으면 메모리 문제는 재현이 어렵고 누수처럼 보이기 쉬워, 구조적 분리와 cleanup 보장이 함께 필요합니다.&lt;/p&gt;</description>
      <category>개발 (Development)/Python</category>
      <category>FastAPI</category>
      <category>NumPy</category>
      <category>pandas</category>
      <category>python</category>
      <category>리팩터링</category>
      <category>메모리최적화</category>
      <category>백그라운드작업</category>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/276</guid>
      <comments>https://what-and-how-to-code.tistory.com/276#entry276comment</comments>
      <pubDate>Tue, 10 Feb 2026 22:36:41 +0900</pubDate>
    </item>
    <item>
      <title>[Java/Spring] Cloud OpenFeign 타임아웃 설정이 적용되지 않았던 이유와 해결 방법</title>
      <link>https://what-and-how-to-code.tistory.com/269</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud OpenFeign을 사용해 &lt;b&gt;모델 학습 API&lt;/b&gt;를 호출하던 중,&lt;br /&gt;로컬 환경에서 &lt;code&gt;SocketTimeoutException&lt;/code&gt;이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;application.yml&lt;/code&gt;에서 Feign 타임아웃을 충분히 늘렸음에도 불구하고&lt;br /&gt;설정이 전혀 적용되지 않는 문제가 있었고,&lt;br /&gt;결과적으로 &lt;b&gt;설정 자체가 로딩되지 않은 구조적인 원인&lt;/b&gt;이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면,&lt;br /&gt;&lt;b&gt;FeignClient에 지정한 configuration 클래스에 &lt;code&gt;@Configuration&lt;/code&gt; 어노테이션이 없어&lt;br /&gt;Feign 설정이 전혀 적용되지 않았던 것이 원인&lt;/b&gt;이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. FeignClient 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FeignClient는 아래와 같이 별도의 configuration 클래스를 지정해서 사용 중이었다.&lt;/p&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;@FeignClient(
    value = &quot;example-client&quot;,
    url = &quot;${external.api.url:http://external-api:8080}&quot;,
    path = &quot;/api&quot;,
    configuration = {
        AuthTokenConfiguration.class,
        FeignCommonConfiguration.class
    }
)
public interface ExternalApiClient extends ExternalApi {

    @GetMapping(path = &quot;/models/train&quot;)
    Object requestModelTrain(...);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 학습 API는 처리 시간이 길어&lt;br /&gt;기본 Feign readTimeout을 초과하면서 타임아웃이 발생했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 발생한 에러&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 발생한 예외는 다음과 같은 형태였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;feign.RetryableException: timeout executing GET ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;cause: &lt;code&gt;java.net.SocketTimeoutException: timeout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;status: &lt;code&gt;-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;responseBody: &lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 서버 오류가 아니라,&lt;br /&gt;&lt;b&gt;응답을 받기 전에 클라이언트의 readTimeout이 먼저 만료되었음을 의미&lt;/b&gt;한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시도했던 해결 방법과 한계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml에서 타임아웃 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 아래와 같이 &lt;code&gt;application.yml&lt;/code&gt;에 Feign 타임아웃을 설정했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  cloud:
    openfeign:
      client:
        config:
          example-client:
            connectTimeout: 60000
            readTimeout: 900000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 설정을 적용해도 타임아웃은 계속 발생했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;근본 원인 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FeignClient의 configuration 속성 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@FeignClient(configuration = ...)&lt;/code&gt; 를 사용하는 경우,&lt;br /&gt;해당 FeignClient는 &lt;b&gt;전용 Spring Context&lt;/b&gt;를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 중요한 포인트는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;configuration으로 지정한 클래스는 &lt;b&gt;Spring 설정 클래스&lt;/b&gt;여야 한다&lt;/li&gt;
&lt;li&gt;즉, &lt;code&gt;@Configuration&lt;/code&gt; 또는 최소한 &lt;code&gt;@Component&lt;/code&gt;가 필요하다&lt;/li&gt;
&lt;li&gt;그렇지 않으면 내부의 &lt;code&gt;@Bean&lt;/code&gt; 정의는 전혀 로딩되지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;FeignCommonConfiguration&lt;/code&gt;, &lt;code&gt;AuthTokenConfiguration&lt;/code&gt; 클래스에&lt;br /&gt;&lt;b&gt;&lt;code&gt;@Configuration&lt;/code&gt; 어노테이션이 없었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FeignClient는 configuration 클래스를 참조하고 있었지만&lt;/li&gt;
&lt;li&gt;Spring은 해당 클래스를 설정 클래스로 인식하지 못했고&lt;/li&gt;
&lt;li&gt;타임아웃을 포함한 모든 Feign 커스텀 설정이 무시되고 있었다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Configuration 클래스에 @Configuration 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 핵심적인 수정이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class FeignCommonConfiguration {

    @Bean
    public Request.Options feignRequestOptions() {
        return new Request.Options(
            60_000,   // connectTimeout (60초)
            900_000   // readTimeout (15분)
        );
    }

    // Encoder / Decoder / ErrorDecoder 등 기존 설정
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AuthTokenConfiguration&lt;/code&gt; 역시 동일하게 &lt;code&gt;@Configuration&lt;/code&gt;을 추가했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 적용 확인 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 시작 시 &lt;code&gt;feignRequestOptions()&lt;/code&gt; 메서드에 브레이크포인트를 걸어&lt;/li&gt;
&lt;li&gt;실제로 Bean이 생성되는지 확인&lt;/li&gt;
&lt;li&gt;이후 모델 학습 API 호출 시 타임아웃이 정상적으로 늘어났는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리 및 시사점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@FeignClient(configuration = ...)&lt;/code&gt;를 사용할 경우&lt;br /&gt;&lt;b&gt;해당 configuration 클래스는 반드시 Spring 설정 클래스로 등록되어야 한다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;application.yml&lt;/code&gt; 설정이 적용되지 않을 때는&lt;br /&gt;단순한 설정 오타보다 &lt;b&gt;Feign 전용 컨텍스트 구조&lt;/b&gt;를 의심해야 한다&lt;/li&gt;
&lt;li&gt;Feign 타임아웃의 최종 기준은 &lt;code&gt;Request.Options&lt;/code&gt; Bean이다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무적인 권장 사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통 Feign 설정과 장시간 수행 API용 설정은 분리하는 것이 안전하다&lt;/li&gt;
&lt;li&gt;오래 걸리는 작업(예: 모델 학습)은&lt;br /&gt;가능하면 동기 호출이 아닌 &lt;b&gt;비동기(jobId 반환) 구조&lt;/b&gt;로 설계하는 것이 바람직하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제는 타임아웃 값의 크기 문제가 아니라,&lt;br /&gt;&lt;b&gt;Feign configuration 클래스가 Spring 설정으로 등록되지 않았던 구조적인 문제&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Configuration&lt;/code&gt; 한 줄이 빠져 있었을 뿐인데&lt;br /&gt;모든 Feign 설정이 무시되고 있었고,&lt;br /&gt;이를 통해 Feign 설정 구조를 다시 한 번 점검하게 되었다.&lt;/p&gt;</description>
      <category>개발 (Development)/Java</category>
      <category>Configuration</category>
      <category>feignClient</category>
      <category>OpenFeign</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>Timeout</category>
      <category>백엔드</category>
      <category>트러블슈팅</category>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/269</guid>
      <comments>https://what-and-how-to-code.tistory.com/269#entry269comment</comments>
      <pubDate>Mon, 9 Feb 2026 22:35:37 +0900</pubDate>
    </item>
    <item>
      <title>[Java/Spring] WebClient / HttpClient를 Config로 분리해 Bean으로 재사용하는 방법</title>
      <link>https://what-and-how-to-code.tistory.com/268</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 서비스 메소드 내부에서 HttpClient와 WebClient를 매번 생성하면 객체 생성 비용이 반복되고, 커넥션 재사용과 설정 관리가 어려워집니다.&lt;br /&gt;@Configuration에서 한 번만 생성해 Bean으로 등록하고 주입받아 사용하면 성능과 유지보수성이 함께 개선됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 메소드 내부에서 다음 패턴으로 작성하는 경우가 많습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메소드 호출마다 HttpClient.create()를 수행합니다.&lt;/li&gt;
&lt;li&gt;이어서 WebClient.builder()로 WebClient를 생성합니다.&lt;/li&gt;
&lt;li&gt;post() 호출 후 subscribe()로 비동기 실행하고 즉시 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 동작은 하지만 다음과 같은 문제가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청마다 객체가 생성되어 GC 부담이 증가합니다.&lt;/li&gt;
&lt;li&gt;커넥션 풀이 재사용되지 않아 네트워크 효율이 떨어질 수 있습니다.&lt;/li&gt;
&lt;li&gt;타임아웃, 헤더, 코덱 설정이 메소드마다 중복되기 쉽습니다.&lt;/li&gt;
&lt;li&gt;운영 중 설정 변경이 어렵습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방향은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HttpClient를 Config에서 Bean으로 등록합니다.&lt;/li&gt;
&lt;li&gt;해당 HttpClient를 사용하는 WebClient도 Config에서 Bean으로 등록합니다.&lt;/li&gt;
&lt;li&gt;Service에서는 Bean을 주입받아 호출만 담당합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) application.yml로 baseUrl을 외부화합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경별로 Python 서버 주소가 달라질 수 있으므로 설정을 분리합니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;python:
  base-url: http://localhost:8000&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) HttpClient와 WebClient를 Config에서 Bean으로 등록합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예시는 connect/read/write/response 타임아웃을 설정하고 WebClient가 이를 사용하도록 구성한 예시입니다.&lt;br /&gt;트리거성 API 기준으로 초 단위 타임아웃을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Configuration
public class WebClientConfig {

    @Bean
    public HttpClient reactorHttpClient() {
        int connectTimeoutMillis = 10_000;
        int readWriteTimeoutSec  = 30;

        return HttpClient.create()
                .responseTimeout(Duration.ofSeconds(30))
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis)
                .doOnConnected(conn -&amp;gt; conn
                        .addHandlerLast(new ReadTimeoutHandler(readWriteTimeoutSec, TimeUnit.SECONDS))
                        .addHandlerLast(new WriteTimeoutHandler(readWriteTimeoutSec, TimeUnit.SECONDS))
                );
    }

    @Bean
    public WebClient pythonWebClient(
            HttpClient reactorHttpClient,
            @Value(&quot;${python.base-url}&quot;) String baseUrl
    ) {
        return WebClient.builder()
                .baseUrl(baseUrl)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .clientConnector(new ReactorClientHttpConnector(reactorHttpClient))
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) Service에서는 WebClient를 주입받아 사용합니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 메소드에서는 더 이상 WebClient.builder()를 호출하지 않고, 주입받은 Bean으로 요청만 수행합니다.&lt;br /&gt;요청 바디는 문자열 JSON이 아니라 객체를 그대로 전달합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ModelService {

    private final WebClient pythonWebClient;

    public Map&amp;lt;String, Object&amp;gt; trainSupervisedModel(
            String uri,
            Map&amp;lt;String, Object&amp;gt; sendData
    ) {

        pythonWebClient.post()
                .uri(uri)
                .bodyValue(sendData)
                .retrieve()
                .toBodilessEntity()
                .subscribe();

        Map&amp;lt;String, Object&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
        result.put(&quot;status&quot;, &quot;processing&quot;);
        result.put(&quot;message&quot;, &quot;Training started in background.&quot;);
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;운영 팁&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;connect timeout을 과도하게 길게 설정하지 않는 편이 안전합니다.&lt;/li&gt;
&lt;li&gt;장시간 학습 작업은 동기 응답보다 202 Accepted + jobId 구조가 운영에 유리합니다.&lt;/li&gt;
&lt;li&gt;네트워크 설정은 호출 코드와 분리해 관리하는 것이 유지보수에 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebClient와 HttpClient를 Config에서 Bean으로 분리해 재사용하면 커넥션 관리와 설정 일관성이 확보됩니다.&lt;br /&gt;서비스 코드는 요청 로직에만 집중할 수 있고, 운영 중 튜닝과 장애 대응이 훨씬 수월해집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&amp;middot;시사점&amp;middot;정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 설정은 한 곳에서 관리하는 것이 가장 중요합니다.&lt;/li&gt;
&lt;li&gt;타임아웃을 늘리는 것보다 비동기 설계가 더 안전합니다.&lt;/li&gt;
&lt;li&gt;Bean 재사용 구조는 성능과 운영 안정성을 동시에 개선합니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발 (Development)/Java</category>
      <category>httpclient</category>
      <category>java</category>
      <category>ReactorNetty</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>Timeout</category>
      <category>WebClient</category>
      <category>WebFlux</category>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/268</guid>
      <comments>https://what-and-how-to-code.tistory.com/268#entry268comment</comments>
      <pubDate>Mon, 9 Feb 2026 22:24:20 +0900</pubDate>
    </item>
    <item>
      <title>[PostgreSQL] pg_cron을 활용한 중복 데이터 정기 정리 방법</title>
      <link>https://what-and-how-to-code.tistory.com/267</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 기본적으로 데이터베이스 내부 Job 스케줄러 기능을 제공하지 않습니다.&lt;br /&gt;그러나 pg_cron 확장을 활용하면 데이터베이스 내부에서 정기적인 SQL 작업을 안정적으로 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 문서에서는 PostgreSQL 환경에서&lt;br /&gt;중복 데이터를 제거하는 SQL 쿼리를 설계하는 방법과,&lt;br /&gt;이를 pg_cron 스케줄러에 등록하여 정기적으로 실행하는 절차를 단계적으로 설명합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 중복 데이터 정리의 필요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경의 데이터베이스에서는 동일한 기준을 가진 데이터가 반복적으로 저장되는 현상이 발생합니다.&lt;br /&gt;이러한 중복 데이터는 다음과 같은 문제를 유발합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 크기 증가로 인한 조회 성능 저하가 발생합니다.&lt;/li&gt;
&lt;li&gt;통계 및 집계 결과의 정확성이 저하됩니다.&lt;/li&gt;
&lt;li&gt;스토리지 자원이 비효율적으로 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 중복 데이터에 대한 명확한 기준을 정의하고,&lt;br /&gt;이를 주기적으로 정리하는 관리 체계가 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 중복 데이터 판단 기준 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 문서에서는 다음과 같은 기준을 예시로 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대상 테이블은 public.my_table 입니다.&lt;/li&gt;
&lt;li&gt;중복 판단 기준 컬럼은 user_id, event_date 입니다.&lt;/li&gt;
&lt;li&gt;유지 기준은 created_at 컬럼 기준으로 가장 최근 데이터 1건입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 동일한 (user_id, event_date) 조합을 가지는 레코드 중&lt;br /&gt;최신 데이터 1건을 제외한 나머지를 중복 데이터로 정의합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 중복 제거 SQL 쿼리 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL에서 제공하는 ROW_NUMBER 윈도우 함수를 활용하면&lt;br /&gt;중복 데이터의 순서를 명확하게 구분할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;DELETE FROM public.my_table
WHERE ctid IN (
    SELECT ctid
    FROM (
        SELECT
            ctid,
            ROW_NUMBER() OVER (
                PARTITION BY user_id, event_date
                ORDER BY created_at DESC
            ) AS rn
        FROM public.my_table
    ) t
    WHERE t.rn &amp;gt; 1
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쿼리 설계 원리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PARTITION BY user_id, event_date&lt;br /&gt;동일 기준의 데이터를 그룹화합니다.&lt;/li&gt;
&lt;li&gt;ORDER BY created_at DESC&lt;br /&gt;최신 데이터를 우선 순위로 정렬합니다.&lt;/li&gt;
&lt;li&gt;rn &amp;gt; 1&lt;br /&gt;중복 데이터로 판단되는 레코드를 식별합니다.&lt;/li&gt;
&lt;li&gt;ctid&lt;br /&gt;PostgreSQL 내부 레코드 식별자로 DELETE 연산의 안정성을 확보합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 스케줄 등록 전 검증 절차&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정기 실행 작업으로 등록하기 전에는&lt;br /&gt;삭제 대상 데이터가 정확한지 반드시 검증해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 삭제 대상 사전 조회&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM (
    SELECT
        *,
        ROW_NUMBER() OVER (
            PARTITION BY user_id, event_date
            ORDER BY created_at DESC
        ) AS rn
    FROM public.my_table
) t
WHERE t.rn &amp;gt; 1
LIMIT 100;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 조회 결과를 통해&lt;br /&gt;삭제 대상 데이터가 정의된 중복 기준과 일치하는지 확인합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 트랜잭션 기반 DELETE 검증&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;BEGIN;

DELETE FROM public.my_table
WHERE ctid IN (
    SELECT ctid
    FROM (
        SELECT
            ctid,
            ROW_NUMBER() OVER (
                PARTITION BY user_id, event_date
                ORDER BY created_at DESC
            ) AS rn
        FROM public.my_table
    ) t
    WHERE t.rn &amp;gt; 1
);

ROLLBACK;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 단계에서는 ROLLBACK을 수행하여&lt;br /&gt;실제 데이터 변경 없이 삭제 로직의 정확성을 검증합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. pg_cron 스케줄러 등록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증이 완료된 SQL 쿼리는 pg_cron Job으로 등록합니다.&lt;br /&gt;아래 예시는 매일 새벽 3시에 중복 데이터 정리 작업을 수행하도록 설정한 사례입니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT cron.schedule(
    'daily_dedup_my_table',
    '0 3 * * *',
    $$
    DELETE FROM public.my_table
    WHERE ctid IN (
        SELECT ctid
        FROM (
            SELECT
                ctid,
                ROW_NUMBER() OVER (
                    PARTITION BY user_id, event_date
                    ORDER BY created_at DESC
                ) AS rn
            FROM public.my_table
        ) t
        WHERE t.rn &amp;gt; 1
    );
    $$
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 등록된 Job 관리 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 Job 목록 조회&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;SELECT jobid, jobname, schedule, active
FROM cron.job;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 Job 비활성화&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;SELECT cron.alter_job(jobid := 1, active := FALSE);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 Job 삭제&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT cron.unschedule(1);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 문서에서는 PostgreSQL 환경에서 pg_cron 확장을 활용하여&lt;br /&gt;중복 데이터를 정기적으로 제거하는 방법을 설명했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 방식은 다음과 같은 효과를 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 데이터 누적을 방지합니다.&lt;/li&gt;
&lt;li&gt;데이터 정합성을 지속적으로 유지합니다.&lt;/li&gt;
&lt;li&gt;조회 및 분석 성능을 개선합니다.&lt;/li&gt;
&lt;li&gt;운영 자동화 수준을 향상시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROW_NUMBER와 ctid를 활용한 중복 제거 방식은&lt;br /&gt;PostgreSQL 특성을 고려한 안정적인 설계로 평가할 수 있습니다.&lt;/p&gt;</description>
      <category>개발 (Development)/PostgreSQL</category>
      <category>pg_cron</category>
      <category>PostgreSQL</category>
      <category>sql</category>
      <category>데이터베이스운영</category>
      <category>자동화</category>
      <category>정기작업</category>
      <category>중복제거</category>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/267</guid>
      <comments>https://what-and-how-to-code.tistory.com/267#entry267comment</comments>
      <pubDate>Mon, 9 Feb 2026 22:13:25 +0900</pubDate>
    </item>
    <item>
      <title>Docker 빌드 시 adoptopenjdk/openjdk11:jre HIGH 취약점 해결 방법</title>
      <link>https://what-and-how-to-code.tistory.com/258</link>
      <description>&lt;h2&gt;요약&lt;/h2&gt;
&lt;p&gt;Docker 빌드 과정에서 adoptopenjdk/openjdk11:jre 베이스 이미지를 사용할 경우 다수의 HIGH 등급 CVE가 탐지되는 문제는, 이미지 내부 설정 문제가 아니라 베이스 이미지 자체가 더 이상 유지보수되지 않기 때문이다. 이 문제는 공식 후속 이미지인 eclipse-temurin으로 교체하는 방식으로 해결할 수 있다.&lt;/p&gt;
&lt;h2&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;컨테이너 보안 스캐너(trivy, grype 등)를 통해 Docker 이미지를 검사하던 중, 아래와 같은 CVE 목록이 지속적으로 탐지되었다.&lt;br&gt;    •    CVE-2025-65018 (HIGH)&lt;br&gt;    •    CVE-2025-64720 (HIGH)&lt;br&gt;    •    CVE-2025-9230 (HIGH)&lt;br&gt;    •    CVE-2023-4039 (MEDIUM)&lt;br&gt;    •    기타 다수&lt;/p&gt;
&lt;p&gt;공통점은 모두 &lt;strong&gt;베이스 이미지가 adoptopenjdk/openjdk11:jre&lt;/strong&gt;인 경우 발생한다는 점이었다.&lt;/p&gt;
&lt;h2&gt;원인 분석&lt;/h2&gt;
&lt;p&gt;adoptopenjdk/openjdk Docker 이미지는 다음과 같은 상태이다.&lt;br&gt;    •    AdoptOpenJDK 프로젝트 자체는 종료됨&lt;br&gt;    •    Docker Hub 이미지 업데이트는 2021년 이후 중단&lt;br&gt;    •    OS 패키지 및 JRE 보안 패치가 더 이상 반영되지 않음&lt;/p&gt;
&lt;p&gt;따라서 Dockerfile 내부에서 apt-get upgrade 등을 수행하더라도, 근본적으로 CVE를 제거할 수 없는 구조이다.&lt;/p&gt;
&lt;h2&gt;해결 방법 개요&lt;/h2&gt;
&lt;p&gt;해결의 핵심은 유지보수 중인 공식 후속 OpenJDK 이미지로 베이스 이미지를 교체하는 것이다.&lt;/p&gt;
&lt;p&gt;권장 대체 이미지&lt;/p&gt;
&lt;p&gt;eclipse-temurin:11-jre-jammy&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;•    AdoptOpenJDK의 공식 후속 프로젝트
•    Ubuntu 22.04 LTS(jammy) 기반
•    JRE 및 OS 보안 패치 지속 반영
•    기업 환경에서도 널리 사용 중&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Dockerfile 수정 예시&lt;/h2&gt;
&lt;p&gt;기존 Dockerfile&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Dockerfile&quot;&gt;FROM adoptopenjdk/openjdk11:jre&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;변경 후 Dockerfile&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Dockerfile&quot;&gt;FROM eclipse-temurin:11-jre-jammy

RUN apt-get update \
 &amp;amp;&amp;amp; apt-get -y upgrade \
 &amp;amp;&amp;amp; apt-get -y autoremove \
 &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY build/libs/app.jar /app/app.jar
ENTRYPOINT [&amp;quot;java&amp;quot;,&amp;quot;-jar&amp;quot;,&amp;quot;/app/app.jar&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 변경만으로도 대부분의 HIGH 등급 CVE가 제거되거나 크게 감소한다.&lt;/p&gt;
&lt;h2&gt;이미지 Pull 방법&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;docker pull eclipse-temurin:11-jre-jammy&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;빌드 시 항상 최신 이미지를 사용하려면 다음 옵션을 권장한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docker build --pull --no-cache -t my-app .&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;추가 고려 사항&lt;/h2&gt;
&lt;p&gt;JRE vs JDK&lt;br&gt;    •    런타임만 필요: &lt;em&gt;-jre-&lt;/em&gt;&lt;br&gt;    •    빌드, 디버깅 필요: &lt;em&gt;-jdk-&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;보안 스캔 결과가 남는 경우&lt;br&gt;    •    OS 패키지 기반 CVE → apt-get upgrade로 해결 가능&lt;br&gt;    •    JRE 기반 CVE → Temurin의 더 최신 11.0.x 태그로 업그레이드 필요&lt;/p&gt;
&lt;h2&gt;결론 및 정리&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;•    adoptopenjdk/openjdk11:jre의 HIGH 취약점은 이미지 내부 문제가 아니라 유지보수 종료가 원인
•    컨테이너 보안 관점에서 해당 이미지는 더 이상 사용하기 어렵다
•    공식 후속 이미지인 eclipse-temurin:11-jre-jammy로 교체하는 것이 가장 현실적이고 안전한 해결책이다
•    베이스 이미지 선택은 애플리케이션 코드만큼이나 중요한 보안 요소임을 다시 한번 확인했다&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발 (Development)/Docker</category>
      <category>adoptopenjdk</category>
      <category>CVE</category>
      <category>DevOps</category>
      <category>docker</category>
      <category>EclipseTemurin</category>
      <category>JAVA11</category>
      <category>openjdk</category>
      <category>보안취약점</category>
      <category>컨테이너보안</category>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/258</guid>
      <comments>https://what-and-how-to-code.tistory.com/258#entry258comment</comments>
      <pubDate>Sun, 25 Jan 2026 23:18:03 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Spring Boot + Logback에서 Datadog trace_id, span_id 로그에 출력하기</title>
      <link>https://what-and-how-to-code.tistory.com/250</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;본 문서는 Spring Boot 서비스에서 Datadog APM을 사용 중인 환경을 전제로,&lt;br /&gt;로그(Log)와 트레이스(Trace)를 연계하기 위해 trace_id 및 span_id를 로그에 출력하는 방법을 정리한 자료입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Logback을 사용하는 서비스에서 Datadog trace 정보가 로그에 포함되지 않는 원인과, 이를 해결하기 위한 설정 절차를 중심으로 설명드리고자 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 배경 및 문제 인식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Datadog APM을 통해 요청 단위 트레이스는 정상적으로 수집되고 있으나, 로그를 확인하는 과정에서 다음과 같은 한계가 확인되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그만으로는 특정 요청 또는 트레이스와의 연계가 어렵습니다.&lt;/li&gt;
&lt;li&gt;Datadog Logs 화면에서 로그와 Trace 간 상호 이동이 불가능합니다.&lt;/li&gt;
&lt;li&gt;장애 발생 시 로그 기반 원인 분석의 효율이 저하됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 문제의 근본 원인은 로그에 Datadog의 &lt;code&gt;trace_id&lt;/code&gt; 및 &lt;code&gt;span_id&lt;/code&gt;가 포함되지 않았기 때문으로 판단됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 해결 방안 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot와 Logback 환경에서 Datadog 로그 상관관계를 구성하기 위해서는 다음 두 가지 설정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, Datadog Java Agent를 통해 trace 정보를 MDC(Mapped Diagnostic Context)에 주입해야 합니다.&lt;br /&gt;둘째, Logback 로그 패턴에서 MDC에 저장된 값을 출력하도록 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Datadog Java Agent가 정상적으로 설정될 경우, 다음 항목들이 MDC에 자동으로 주입됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;dd.trace_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dd.span_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dd.service&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dd.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dd.version&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Datadog 로그 인젝션 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 로그 인젝션 기능의 필요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Datadog Java Agent는 로그 인젝션(Log Injection) 기능을 통해 SLF4J 및 Logback의 MDC에 trace 관련 정보를 자동으로 추가합니다.&lt;br /&gt;해당 기능이 활성화되지 않을 경우, Logback 설정을 변경하더라도 trace 정보는 로그에 출력되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 환경 변수 설정&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;DD_LOGS_INJECTION=true
DD_SERVICE=my-springboot-service
DD_ENV=prod
DD_VERSION=1.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DD_LOGS_INJECTION&lt;/code&gt; 설정은 로그 MDC에 trace_id 및 span_id를 주입하기 위한 필수 항목입니다.&lt;br /&gt;&lt;code&gt;DD_SERVICE&lt;/code&gt;, &lt;code&gt;DD_ENV&lt;/code&gt;, &lt;code&gt;DD_VERSION&lt;/code&gt;은 Datadog 상에서 로그, 트레이스, 메트릭을 정확히 매칭하기 위한 식별 정보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 설정은 Datadog Java Agent가 &lt;code&gt;-javaagent&lt;/code&gt; 옵션 또는 &lt;code&gt;JAVA_TOOL_OPTIONS&lt;/code&gt;를 통해 이미 애플리케이션에 적용되어 있다는 전제 하에 유효합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Logback 로그 패턴 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계는 Logback 설정 파일에서 MDC 값을 로그 패턴에 포함하는 작업입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 logback-spring.xml 설정 예시&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;
    &amp;lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;pattern&amp;gt;
                %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} -
                trace_id=%X{dd.trace_id:-0}
                span_id=%X{dd.span_id:-0}
                service=%X{dd.service:-}
                env=%X{dd.env:-}
                version=%X{dd.version:-}
                - %msg%n
            &amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;root level=&quot;INFO&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;CONSOLE&quot;/&amp;gt;
    &amp;lt;/root&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 패턴 구성 설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;%X{dd.trace_id}&lt;/code&gt;, &lt;code&gt;%X{dd.span_id}&lt;/code&gt;는 MDC에 저장된 Datadog trace 정보를 로그에 출력합니다.&lt;br /&gt;기본값을 의미하는 &lt;code&gt;:-0&lt;/code&gt;, &lt;code&gt;:-&lt;/code&gt; 옵션은 MDC 값이 존재하지 않을 경우에도 로그 포맷이 유지되도록 하기 위한 설정입니다.&lt;br /&gt;&lt;code&gt;dd.service&lt;/code&gt;, &lt;code&gt;dd.env&lt;/code&gt;, &lt;code&gt;dd.version&lt;/code&gt; 정보는 Datadog 내 서비스 단위 식별 및 분석 정확도를 높이는 데 활용됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 적용 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 적용 이후 로그는 다음과 같은 형식으로 출력됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2025-01-01 12:00:00.123 INFO  [http-nio-8080-exec-1] c.example.DemoController -
trace_id=1234567890123456789 span_id=9876543210987654321
service=my-springboot-service env=prod version=1.0.0
- request started&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Datadog Logs 화면에서는 해당 로그를 기준으로 관련 트레이스로 즉시 이동할 수 있으며, 요청 단위의 로그와 트레이스를 함께 분석할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 주요 점검 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;trace_id가 출력되지 않는 경우에는 &lt;code&gt;DD_LOGS_INJECTION&lt;/code&gt; 설정 누락 여부, Datadog Java Agent 적용 여부, APM 트레이싱 활성화 상태를 우선적으로 확인할 필요가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 로그에서만 trace_id가 출력되지 않는 경우에는 &lt;code&gt;@Async&lt;/code&gt;, 스케줄러, 별도 ThreadPool 등 비동기 환경에서 생성된 로그일 가능성이 있습니다. 이 경우 MDC 및 Trace Context 전파가 누락되었을 수 있으며, 추가적인 설정이 필요할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 결론 및 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot와 Logback 환경에서 Datadog trace_id 및 span_id를 로그에 포함하는 작업은 구현 난이도는 높지 않으나, 사전 설정이 정확히 이루어지지 않을 경우 정상적으로 동작하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 설정을 통해 로그와 트레이스를 하나의 흐름으로 분석할 수 있으며, 장애 원인 파악과 요청 단위 분석의 효율을 크게 향상시킬 수 있습니다.&lt;br /&gt;운영 환경에서의 가시성과 대응 속도 또한 함께 개선될 것으로 기대됩니다.&lt;/p&gt;</description>
      <category>개발 (Development)/Java</category>
      <category>APM</category>
      <category>datadog</category>
      <category>java</category>
      <category>Logback</category>
      <category>logging</category>
      <category>observability</category>
      <category>span</category>
      <category>springboot</category>
      <category>Trace</category>
      <author>LoopThinker</author>
      <guid isPermaLink="true">https://what-and-how-to-code.tistory.com/250</guid>
      <comments>https://what-and-how-to-code.tistory.com/250#entry250comment</comments>
      <pubDate>Thu, 25 Dec 2025 23:49:40 +0900</pubDate>
    </item>
  </channel>
</rss>