<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>StraWeb</title>
    <link>https://www.stragos.xyz</link>
    <description>개발, 리뷰, 투자 등 관심 있는 것들을 편하게 기록하는 블로그입니다.</description>
    <language>ko</language>
    <managingEditor>Stragos</managingEditor>
    <lastBuildDate>Thu, 21 May 2026 06:11:03 GMT</lastBuildDate>
    <atom:link href="https://www.stragos.xyz/rss.xml" rel="self" type="application/rss+xml"/>

    <item>
      <title><![CDATA[DeFi 용어집 2026 — 풀·LP·청산·MEV 한 번에 정리]]></title>
      <link>https://www.stragos.xyz/posts/defi-glossary-2026</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/defi-glossary-2026</guid>
      <pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[TVL, AMM, 슬리피지, 임퍼머넌트 로스, 청산, 펀딩비, 파밍, 플래시 론… DeFi 한 번이라도 써본 사람이 마주치는 용어들을 2026년 기준으로 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>DeFi는 한 번 발 들이면 단어 폭격이 시작된다.</p>
<p>유니스왑에서 스왑 한 번 하려고 하니 슬리피지·MEV가 나오고, AAVE에서 스테이블코인 빌리려니 LTV·Health Factor·청산가가 따라온다.</p>
<p>이게 또 별 게 아니다 — 다 <strong>같은 개념을 다른 동네에서 다른 단어로 부르고 있을 뿐</strong>이다.</p>
<p>이 글은 <a href="/posts/blockchain-glossary-2026">블록체인 용어집</a>에 다 못 담은 <strong>DeFi 전문 용어</strong>만 따로 묶은 거다. 한 번 정리해두면, 새 프로토콜 보다가 나오는 단어 90%는 여기서 찾아진다.</p>
<hr>
<h2 id="1-defi란--일단-큰-그림"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-defi란--일단-큰-그림">#</a>1. DeFi란 — 일단 큰 그림</h2>
<h3 id="defi-decentralized-finance-탈중앙-금융"><a class="anchor" aria-hidden="true" tabindex="-1" href="#defi-decentralized-finance-탈중앙-금융">#</a>DeFi (Decentralized Finance, 탈중앙 금융)</h3>
<p>은행·증권사가 하는 일을 <strong>스마트 컨트랙트가 대신 하는</strong> 금융 시스템이다.</p>
<ul>
<li>은행 → <strong>대출 프로토콜</strong> (Aave, Compound, Morpho)</li>
<li>증권사 → <strong>DEX</strong> (Uniswap, Curve, Aerodrome)</li>
<li>펀드 → <strong>수익 파밍 프로토콜</strong> (Yearn, Pendle)</li>
<li>보험사 → <strong>온체인 보험</strong> (Nexus Mutual)</li>
</ul>
<p>핵심은 "<strong>중간 관리자 없음</strong>".</p>
<p>스마트 컨트랙트가 24시간 자동으로 돌고, 누구나 같은 조건으로 쓸 수 있다. KYC도 없고 영업시간도 없다.</p>
<h3 id="tvl-total-value-locked"><a class="anchor" aria-hidden="true" tabindex="-1" href="#tvl-total-value-locked">#</a>TVL (Total Value Locked)</h3>
<p>해당 프로토콜에 <strong>묶여 있는 총 자산.</strong> DeFi 인기도의 핵심 지표.</p>
<p><strong>"Aave TVL 200억 달러"</strong> = "Aave에 사용자들이 200억 달러 어치 자산을 맡겨두고 있다"는 뜻. <a href="https://defillama.com">DeFiLlama</a>가 표준 집계 사이트.</p>
<blockquote>
<p><strong>TVL이 크면 무조건 안전한가?</strong></p>
<p>아니다. TVL은 "<strong>신뢰가 쌓인 정도</strong>"의 지표지, 안전 보증이 아니다. 2022년 Anchor(Terra)의 TVL은 폭락 직전 200억 달러였다.</p>
<p><strong>TVL이 크면 더 큰 표적이 되기도 한다.</strong> 해커한테는 한 방에 큰 돈이 보이는 곳이 매력적인 사냥터.</p>
</blockquote>
<hr>
<h2 id="2-dex와-거래--자동으로-돌아가는-시장"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-dex와-거래--자동으로-돌아가는-시장">#</a>2. DEX와 거래 — 자동으로 돌아가는 시장</h2>
<h3 id="dex-decentralized-exchange"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dex-decentralized-exchange">#</a>DEX (Decentralized Exchange)</h3>
<p>회사 없이 <strong>스마트 컨트랙트가 매매를 처리하는 거래소.</strong> 유니스왑, Curve, Aerodrome, Raydium 등.</p>
<p>업비트·바이낸스(CEX) 대비 차이:</p>
<table>
<thead>
<tr>
<th></th>
<th>CEX</th>
<th>DEX</th>
</tr>
</thead>
<tbody>
<tr>
<td>운영</td>
<td>회사가 함</td>
<td>스마트 컨트랙트</td>
</tr>
<tr>
<td>KYC</td>
<td>필요</td>
<td>불필요</td>
</tr>
<tr>
<td>자산 보관</td>
<td>거래소가 보관</td>
<td>본인 지갑</td>
</tr>
<tr>
<td>다운타임</td>
<td>가끔 멈춤</td>
<td>거의 안 멈춤</td>
</tr>
<tr>
<td>유동성</td>
<td>호가창</td>
<td>풀 (대부분)</td>
</tr>
</tbody>
</table>
<h3 id="amm-automated-market-maker-자동-시장-조성자"><a class="anchor" aria-hidden="true" tabindex="-1" href="#amm-automated-market-maker-자동-시장-조성자">#</a>AMM (Automated Market Maker, 자동 시장 조성자)</h3>
<p>DEX의 매매 알고리즘. <strong>수식 하나로 자동 가격 결정.</strong></p>
<p>기본 형태: <code>x * y = k</code>. ETH 1,000개와 USDC 200만 개가 있는 풀이라면, 둘의 곱(<code>k = 2,000,000,000</code>)이 항상 일정하게 유지되도록 환율이 자동 조정된다.</p>
<p><img src="/images/crypto/defi-amm-curve.svg" alt="AMM 곡선과 가격 결정" loading="lazy" decoding="async"></p>
<h3 id="clob-central-limit-order-book"><a class="anchor" aria-hidden="true" tabindex="-1" href="#clob-central-limit-order-book">#</a>CLOB (Central Limit Order Book)</h3>
<p><strong>호가창 기반 매매 방식.</strong> 주식 거래소가 쓰는 그 방식이다.</p>
<p>기존 DEX는 거의 다 AMM이었는데, 2024–2026년에 CLOB DEX가 다시 떠오르고 있다. <strong>dYdX, Hyperliquid, <a href="/posts/dango-exchange-airdrop-guide">Dango</a></strong> 같은 곳들. 슬리피지가 작고 MEV가 어려워서 트레이더 친화적이다.</p>
<h3 id="lp-liquidity-provider-유동성-공급자"><a class="anchor" aria-hidden="true" tabindex="-1" href="#lp-liquidity-provider-유동성-공급자">#</a>LP (Liquidity Provider, 유동성 공급자)</h3>
<p>풀에 <strong>자기 자산을 넣어주는 사람.</strong></p>
<p>ETH/USDC 풀에 ETH 1개 + USDC 2,000달러를 같이 넣어두면 → 다른 사람들이 그 풀에서 거래할 때마다 발생하는 <strong>수수료의 일부를 지분만큼 받는다</strong>. 보통 거래액의 0.05–1%가 풀에 쌓이고 LP들에게 분배된다.</p>
<blockquote>
<p><strong>LP는 왜 자기 돈을 풀에 넣어줄까?</strong></p>
<p><strong>수수료 수익 + 보상 토큰</strong> 때문이다.</p>
<p>ETH/USDC 같은 거래량 큰 풀은 연 5–30% 수익이 난다. 거기에 프로젝트가 자기 토큰까지 보상으로 추가로 주면(=<strong>유동성 마이닝</strong>) 수익률이 더 올라간다.</p>
<p>단, 공짜는 아니다. 다음에 나오는 임퍼머넌트 로스 때문.</p>
</blockquote>
<h3 id="슬리피지-slippage"><a class="anchor" aria-hidden="true" tabindex="-1" href="#슬리피지-slippage">#</a>슬리피지 (Slippage)</h3>
<p><strong>주문 시 가격 ≠ 체결 시 가격.</strong></p>
<p>풀에서 큰 거래를 하면 그 거래 자체가 풀의 비율을 바꿔서 가격이 움직인다. <strong>풀이 작을수록, 거래액이 클수록 슬리피지가 크다.</strong></p>
<p>DEX에서 거래할 때 "슬리피지 한도 0.5%"를 설정하는 게 그 때문 — 그보다 더 차이 나면 거래 자체가 취소된다.</p>
<h3 id="mev-maximal-extractable-value"><a class="anchor" aria-hidden="true" tabindex="-1" href="#mev-maximal-extractable-value">#</a>MEV (Maximal Extractable Value)</h3>
<p><strong>블록 생성자(또는 봇)가 거래 순서를 조작해서 뜯어가는 이익.</strong></p>
<p>대표적인 패턴 두 가지:</p>
<ul>
<li><strong>샌드위치 공격</strong> — 누가 큰 매수 주문 보내면, 봇이 그 앞에 자기 매수 + 뒤에 자기 매도를 끼워서 차익. 피해자는 더 비싸게 사게 됨.</li>
<li><strong>프론트러닝</strong> — 누가 좋은 거래 보내면 가스비 더 내고 먼저 끼어들어서 그 자리 차지.</li>
</ul>
<p>이걸 막으려고 <strong>MEV 보호 RPC</strong> (Flashbots Protect, MEV Blocker), <strong>인텐트 기반 DEX</strong> (CowSwap, UniswapX) 같은 게 등장했다.</p>
<h3 id="어그리게이터-aggregator"><a class="anchor" aria-hidden="true" tabindex="-1" href="#어그리게이터-aggregator">#</a>어그리게이터 (Aggregator)</h3>
<p>여러 DEX를 동시에 보고 <strong>가장 좋은 가격 경로</strong>를 찾아주는 서비스.</p>
<ul>
<li><strong>1inch, ParaSwap, Odos</strong> — 메이저 어그리게이터</li>
<li><strong>CowSwap</strong> — 인텐트 기반, MEV 방어 자동</li>
<li><strong>Jupiter</strong> — 솔라나의 표준 어그리게이터</li>
</ul>
<p>직접 유니스왑 가지 말고 어그리게이터 쓰는 게 보통 더 싸다.</p>
<hr>
<h2 id="3-유동성-공급--lp의-세계"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-유동성-공급--lp의-세계">#</a>3. 유동성 공급 — LP의 세계</h2>
<h3 id="풀-liquidity-pool"><a class="anchor" aria-hidden="true" tabindex="-1" href="#풀-liquidity-pool">#</a>풀 (Liquidity Pool)</h3>
<p>LP들이 자산을 넣는 <strong>공동 저금통.</strong> 거래자들이 이 풀에 대고 매매하고, 발생한 수수료를 LP들이 나눠 가진다.</p>
<p>대표적 풀:</p>
<ul>
<li><strong>ETH/USDC</strong> — 가장 거래량 많은 풀</li>
<li><strong>stablecoin pool</strong> (USDC/USDT/DAI) — 변동성 적고 수수료 안정</li>
<li><strong>상관관계 풀</strong> (stETH/ETH) — Curve 특화</li>
</ul>
<h3 id="임퍼머넌트-로스-impermanent-loss-il"><a class="anchor" aria-hidden="true" tabindex="-1" href="#임퍼머넌트-로스-impermanent-loss-il">#</a>임퍼머넌트 로스 (Impermanent Loss, IL)</h3>
<p><strong>LP의 가장 큰 리스크.</strong></p>
<p>풀에 넣은 두 토큰의 <strong>가격 비율이 변하면</strong>, 그냥 들고만 있었을 때보다 <strong>금액이 적어지는 현상</strong>이다.</p>
<p><img src="/images/crypto/defi-impermanent-loss.svg" alt="임퍼머넌트 로스 시각화" loading="lazy" decoding="async"></p>
<p>예시:</p>
<ul>
<li>ETH/USDC 풀에 ETH 1개 + USDC 2,000달러 넣음 (총 4,000달러어치)</li>
<li>ETH 가격이 2,000 → 4,000달러로 2배 뜀</li>
<li>풀 안에서는 자동 매매로 ETH가 일부 USDC로 바뀜 → ETH 약 0.71개 + USDC 약 2,830달러 (총 약 5,660달러)</li>
<li>그냥 들고 있었으면? ETH 1개 + USDC 2,000달러 = 6,000달러</li>
<li><strong>차액 ≈ -340달러 = IL</strong></li>
</ul>
<p>이 차액이 LP 수수료 수익보다 크면 손해다.</p>
<p>변동성 큰 페어일수록 IL이 커서, <strong>변동성 큰 알트코인 페어 LP는 손해 보는 경우가 흔하다.</strong></p>
<h3 id="집중-유동성-concentrated-liquidity"><a class="anchor" aria-hidden="true" tabindex="-1" href="#집중-유동성-concentrated-liquidity">#</a>집중 유동성 (Concentrated Liquidity)</h3>
<p>Uniswap v3에서 도입. <strong>특정 가격 구간에만 유동성을 몰아서</strong> 자본 효율을 높이는 방식.</p>
<ul>
<li>ETH가 $2,000–$3,000 구간에서만 거래된다고 보면, 그 구간에만 자기 유동성을 배치</li>
<li>그 구간 안에서 거래되면 수수료를 더 많이 받음</li>
<li>가격이 그 구간을 벗어나면 → <strong>한쪽 토큰만 남고 수수료 못 받음</strong></li>
</ul>
<p>수익률이 더 높은 대신 <strong>관리가 필요하다</strong>. 가격이 구간 벗어나면 다시 조정해줘야 하니까.</p>
<h3 id="jit-유동성-just-in-time"><a class="anchor" aria-hidden="true" tabindex="-1" href="#jit-유동성-just-in-time">#</a>JIT 유동성 (Just-In-Time)</h3>
<p>큰 거래가 들어올 때 <strong>그 직전에 유동성을 넣었다가 거래 끝나면 바로 빼는</strong> 봇 전략.</p>
<p>일반 LP의 수수료를 빼앗아 가는 효과가 있어서 비판도 많다.</p>
<hr>
<h2 id="4-대출청산--담보-잡고-빌리는-구조"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-대출청산--담보-잡고-빌리는-구조">#</a>4. 대출·청산 — 담보 잡고 빌리는 구조</h2>
<h3 id="대출-프로토콜-lending-protocol"><a class="anchor" aria-hidden="true" tabindex="-1" href="#대출-프로토콜-lending-protocol">#</a>대출 프로토콜 (Lending Protocol)</h3>
<p><strong>자산을 예치하면 이자를 받고, 담보 잡고 빌리면 이자를 낸다.</strong></p>
<ul>
<li><strong>Aave, Compound, Morpho, Spark</strong> — 메이저 대출 프로토콜</li>
<li>예치 금리 (Supply APR) &#x3C; 대출 금리 (Borrow APR)</li>
<li>차익이 프로토콜 수익</li>
</ul>
<h3 id="ltv-loan-to-value-담보-비율"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ltv-loan-to-value-담보-비율">#</a>LTV (Loan-To-Value, 담보 비율)</h3>
<p><strong>담보 가치 대비 빌릴 수 있는 비율.</strong></p>
<p>ETH 1개(=$3,000) 담보로 LTV 75%까지 자산을 빌릴 수 있다면 → 최대 $2,250 USDC 대출 가능.</p>
<h3 id="health-factor-건강-지수"><a class="anchor" aria-hidden="true" tabindex="-1" href="#health-factor-건강-지수">#</a>Health Factor (건강 지수)</h3>
<p><strong>현재 포지션이 청산까지 얼마나 여유 있는지</strong> 한 숫자로 보여주는 지표.</p>
<table>
<thead>
<tr>
<th>지수</th>
<th>상태</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>HF > 1.5</strong></td>
<td>안전</td>
</tr>
<tr>
<td><strong>HF 1.0–1.5</strong></td>
<td>주의</td>
</tr>
<tr>
<td><strong>HF = 1</strong></td>
<td>청산 임박</td>
</tr>
<tr>
<td><strong>HF &#x3C; 1</strong></td>
<td>청산 발동</td>
</tr>
</tbody>
</table>
<p>ETH가 떨어지면 담보 가치가 줄어서 HF도 떨어진다. <strong>HF가 1.2 아래면 위험 신호.</strong></p>
<h3 id="청산-liquidation"><a class="anchor" aria-hidden="true" tabindex="-1" href="#청산-liquidation">#</a>청산 (Liquidation)</h3>
<p>담보 가치가 임계치 아래로 떨어지면, <strong>누군가가 부채를 대신 갚아주고 담보를 할인된 가격으로 가져간다.</strong></p>
<ul>
<li>보통 담보의 5–10% 페널티 발생 → 청산자가 이걸 가져감</li>
<li>청산은 봇이 거의 다 함. 0.1초 단위로 모니터링하다가 즉시 실행</li>
<li>청산 봇끼리도 경쟁이라 가스비 전쟁 발생</li>
</ul>
<p><img src="/images/crypto/defi-liquidation.svg" alt="대출과 청산 메커니즘" loading="lazy" decoding="async"></p>
<h3 id="플래시-론-flash-loan"><a class="anchor" aria-hidden="true" tabindex="-1" href="#플래시-론-flash-loan">#</a>플래시 론 (Flash Loan)</h3>
<p><strong>한 트랜잭션 안에서 빌리고 갚으면 이자 없이 무한히 빌릴 수 있는 대출.</strong></p>
<p>DeFi의 가장 독특한 기능 중 하나다. 트랜잭션 종료 전에 갚지 못하면 그 트랜잭션 자체가 무효화되는 구조라, <strong>담보 없이도</strong> 가능하다.</p>
<p>활용:</p>
<ul>
<li><strong>차익거래(arbitrage)</strong> — DEX 간 가격 차이 이용</li>
<li><strong>포지션 자동 청산·리파이낸싱</strong></li>
<li><strong>(나쁜 쪽)</strong> 해킹·익스플로잇 — 큰 자본 동원이 필요한 공격에 쓰임</li>
</ul>
<hr>
<h2 id="5-파생상품--영원히-안-끝나는-선물"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-파생상품--영원히-안-끝나는-선물">#</a>5. 파생상품 — 영원히 안 끝나는 선물</h2>
<h3 id="perps-perpetual-futures-영구-선물"><a class="anchor" aria-hidden="true" tabindex="-1" href="#perps-perpetual-futures-영구-선물">#</a>Perps (Perpetual Futures, 영구 선물)</h3>
<p><strong>만기 없는 선물 계약.</strong> DeFi 파생의 표준이다.</p>
<p>만기일이 따로 없고, 포지션은 청산되거나 직접 닫을 때까지 유지된다.</p>
<ul>
<li><strong>Hyperliquid, dYdX, GMX, <a href="/posts/dango-exchange-airdrop-guide">Dango</a></strong> — 메이저 Perps DEX</li>
<li>레버리지 보통 5–50배 가능</li>
<li>24/7 거래</li>
</ul>
<h3 id="펀딩비-funding-rate"><a class="anchor" aria-hidden="true" tabindex="-1" href="#펀딩비-funding-rate">#</a>펀딩비 (Funding Rate)</h3>
<p>영구 선물의 핵심 메커니즘. <strong>롱과 숏의 균형을 맞추기 위해 한쪽이 다른 쪽에 주는 수수료</strong>다.</p>
<ul>
<li><strong>펀딩비 양수 (+)</strong> — 롱 우세 → 롱이 숏에게 지급 (보통 8시간마다)</li>
<li><strong>펀딩비 음수 (-)</strong> — 숏 우세 → 숏이 롱에게 지급</li>
</ul>
<p>펀딩비 자체가 <strong>시장 심리 지표</strong>이기도 하다. 펀딩비가 비정상적으로 높으면 → 롱 과열 → 단기 조정 신호.</p>
<h3 id="청산가-liquidation-price"><a class="anchor" aria-hidden="true" tabindex="-1" href="#청산가-liquidation-price">#</a>청산가 (Liquidation Price)</h3>
<p>레버리지 포지션이 자동으로 닫히는 가격.</p>
<ul>
<li><strong>10배 레버리지 롱</strong> — ETH 가격이 약 10% 떨어지면 청산</li>
<li><strong>30배 롱</strong> — 약 3% 만에 청산</li>
</ul>
<p><strong>레버리지가 크면 작은 변동에도 박살난다(rekt).</strong></p>
<h3 id="oi-open-interest"><a class="anchor" aria-hidden="true" tabindex="-1" href="#oi-open-interest">#</a>OI (Open Interest)</h3>
<p><strong>아직 청산되지 않고 살아있는 선물 계약 총량.</strong></p>
<p>OI가 높을수록 시장이 과열된 상태일 가능성이 크다. <strong>OI 급상승 + 펀딩비 양수 폭증</strong> = 롱 과열 → 청산 폭포 직전 신호.</p>
<hr>
<h2 id="6-수익-농사--이자가-이자를-낳는-구조"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-수익-농사--이자가-이자를-낳는-구조">#</a>6. 수익 농사 — 이자가 이자를 낳는 구조</h2>
<h3 id="일드-파밍-yield-farming"><a class="anchor" aria-hidden="true" tabindex="-1" href="#일드-파밍-yield-farming">#</a>일드 파밍 (Yield Farming)</h3>
<p><strong>여러 DeFi에 자산을 굴려서 수익을 얻는 행위 전반.</strong></p>
<p>기본 패턴:</p>
<ol>
<li>풀에 LP로 예치 → LP 토큰 받음</li>
<li>그 LP 토큰을 또 다른 프로토콜에 스테이킹 → 추가 보상 토큰</li>
<li>보상 토큰 매각 또는 재투자</li>
</ol>
<p>수익률이 높을수록 <strong>리스크와 복잡도도 같이 올라간다.</strong></p>
<h3 id="apr-vs-apy"><a class="anchor" aria-hidden="true" tabindex="-1" href="#apr-vs-apy">#</a>APR vs APY</h3>
<ul>
<li><strong>APR (Annual Percentage Rate, 연이율 — 단리)</strong> — 1년 보유했을 때 받는 비율 (재투자 X)</li>
<li><strong>APY (Annual Percentage Yield, 연수익률 — 복리)</strong> — 자동 재투자 가정 시 비율</li>
</ul>
<p>APR 50%여도, 매일 자동 복리하면 APY 64.8%로 표시된다.</p>
<p><strong>두 단어를 헷갈리면 수익률을 과장 또는 과소평가하게 된다.</strong></p>
<h3 id="오토컴파운드-auto-compound"><a class="anchor" aria-hidden="true" tabindex="-1" href="#오토컴파운드-auto-compound">#</a>오토컴파운드 (Auto-compound)</h3>
<p><strong>받은 보상을 자동으로 재투자</strong>해주는 기능. Yearn, Beefy 같은 볼트(Vault)가 대표적.</p>
<p>매일 보상 받아서 다시 풀에 넣는 작업을 사람이 직접 하면 가스비만 나간다 → 컨트랙트가 묶어서 한 번에 처리.</p>
<h3 id="보상-토큰-reward-token"><a class="anchor" aria-hidden="true" tabindex="-1" href="#보상-토큰-reward-token">#</a>보상 토큰 (Reward Token)</h3>
<p>LP 수수료 외에 <strong>프로토콜이 자기 토큰을 인센티브로 추가로 주는 것</strong>. <strong>유동성 마이닝(Liquidity Mining)</strong> 의 보상.</p>
<p>다만 보상 토큰 가격이 떨어지면 표시 APR도 같이 떨어진다. <strong>고APR = 위험 신호</strong> 인 경우가 많다.</p>
<h3 id="베이스-apr-vs-보상-apr"><a class="anchor" aria-hidden="true" tabindex="-1" href="#베이스-apr-vs-보상-apr">#</a>베이스 APR vs 보상 APR</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>의미</th>
<th>안정성</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Base APR</strong></td>
<td>거래 수수료에서 오는 수익</td>
<td>⭐⭐⭐</td>
</tr>
<tr>
<td><strong>Reward APR</strong></td>
<td>보상 토큰 인센티브</td>
<td>⭐ (토큰 가격 따라 변동 큼)</td>
</tr>
</tbody>
</table>
<p><strong>합산 APR이 같다면 Base 비중이 높은 풀이 안정적.</strong></p>
<hr>
<h2 id="7-스테이블코인-메커니즘--같은-1달러도-다-다른-구조"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7-스테이블코인-메커니즘--같은-1달러도-다-다른-구조">#</a>7. 스테이블코인 메커니즘 — 같은 1달러도 다 다른 구조</h2>
<p><a href="/posts/blockchain-glossary-2026">블록체인 용어집</a>에서 짧게 다뤘지만, 여기선 좀 더 자세히.</p>
<h3 id="담보형-collateral-backed"><a class="anchor" aria-hidden="true" tabindex="-1" href="#담보형-collateral-backed">#</a>담보형 (Collateral-backed)</h3>
<p>진짜 달러나 국채를 보유하면서 토큰 발행. <strong>USDT, USDC, PYUSD</strong>.</p>
<ul>
<li>가장 안정적, 다만 발행자가 회사라서 <strong>검열·동결 리스크</strong> 있음</li>
<li>미국 국채 보유 → 이자 수익은 발행사가 다 가져감</li>
</ul>
<h3 id="cdp형-collateralized-debt-position"><a class="anchor" aria-hidden="true" tabindex="-1" href="#cdp형-collateralized-debt-position">#</a>CDP형 (Collateralized Debt Position)</h3>
<p><strong>암호화폐를 담보로 잡고 스테이블코인을 빌리는</strong> 방식. <strong>DAI, LUSD, GHO</strong>.</p>
<ul>
<li>보통 150% 이상 오버 담보 (ETH $300어치로 $200 빌림)</li>
<li>담보 가치 떨어지면 청산</li>
<li>회사 없이 컨트랙트만으로 운영 가능</li>
</ul>
<h3 id="알고리즘-algorithmic"><a class="anchor" aria-hidden="true" tabindex="-1" href="#알고리즘-algorithmic">#</a>알고리즘 (Algorithmic)</h3>
<p>수학 공식만으로 1달러 유지하려는 방식. <strong>2022년 UST가 폭락하면서 대부분 사라짐.</strong></p>
<p>지금까지 살아남은 알고리즘 스테이블 = <strong>거의 없음</strong>. 새로운 모델이 나와도 시장이 신중함.</p>
<h3 id="rwa형"><a class="anchor" aria-hidden="true" tabindex="-1" href="#rwa형">#</a>RWA형</h3>
<p><strong>미국 국채 같은 실물 자산을 토큰화</strong>한 뒤 그걸 담보로 발행. <strong>USDe, USDS, sDAI</strong> 등이 RWA 비중을 늘리고 있다.</p>
<ul>
<li>보유 시 <strong>이자 수익</strong>까지 받을 수 있음 (sUSDe, sUSDS)</li>
<li>사실상 "배당 주는 디지털 달러"</li>
<li><a href="/posts/apyx-protocol-airdrop-guide">Apyx</a> 처럼 우선주 배당 기반의 변형도 등장</li>
</ul>
<h3 id="디페깅-depeg"><a class="anchor" aria-hidden="true" tabindex="-1" href="#디페깅-depeg">#</a>디페깅 (Depeg)</h3>
<p>스테이블코인이 <strong>1달러에서 벗어난 상태.</strong></p>
<ul>
<li>USDC가 2023년 SVB 사태 때 일시적으로 $0.87까지 빠짐 → 며칠 후 회복</li>
<li>UST는 2022년 디페깅 후 회복 못 함</li>
</ul>
<p>스테이블코인이 1달러에서 멀어지기 시작하면 <strong>빠르게 다른 안정적인 코인으로 갈아타는 게 안전</strong>.</p>
<hr>
<h2 id="8-브릿지랩핑--체인-사이를-건너는-자산"><a class="anchor" aria-hidden="true" tabindex="-1" href="#8-브릿지랩핑--체인-사이를-건너는-자산">#</a>8. 브릿지·랩핑 — 체인 사이를 건너는 자산</h2>
<h3 id="브릿지-bridge"><a class="anchor" aria-hidden="true" tabindex="-1" href="#브릿지-bridge">#</a>브릿지 (Bridge)</h3>
<p>다른 블록체인 간 자산을 옮기는 통로. <strong>LayerZero, Wormhole, Across, Stargate</strong> 등.</p>
<ul>
<li>보통 원본 체인에서 자산을 잠그고, 목적지 체인에서 같은 양의 토큰을 발행</li>
<li><strong>브릿지 해킹은 코인 시장 최대 사고 빈도 1위</strong> — 가급적 공식 브릿지 사용</li>
</ul>
<h3 id="wrapped-token-랩핑-토큰"><a class="anchor" aria-hidden="true" tabindex="-1" href="#wrapped-token-랩핑-토큰">#</a>Wrapped Token (랩핑 토큰)</h3>
<p><strong>다른 체인에서 쓰기 위해 한 번 감싼 토큰.</strong></p>
<p>대표 예시:</p>
<ul>
<li><strong>wBTC</strong> — 이더리움 위에서 쓸 수 있는 비트코인 (1 wBTC = 1 BTC 백업)</li>
<li><strong>wETH</strong> — ERC-20 표준에 맞춘 ETH (DeFi에서는 wETH로 다뤄짐)</li>
</ul>
<h3 id="native-vs-wrapped"><a class="anchor" aria-hidden="true" tabindex="-1" href="#native-vs-wrapped">#</a>Native vs Wrapped</h3>
<table>
<thead>
<tr>
<th>종류</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Native USDC on Base</strong></td>
<td>Circle이 Base 체인에서 직접 발행</td>
</tr>
<tr>
<td><strong>Bridged USDC.e on Base</strong></td>
<td>이더리움 USDC를 브릿지로 옮긴 것</td>
</tr>
</tbody>
</table>
<p>같은 "USDC" 라벨이라도 <strong>둘은 다른 토큰</strong>이다. 가격 차이 발생 가능, 변환 시 수수료·시간 추가. <strong>Native가 항상 더 안전.</strong></p>
<hr>
<h2 id="9-위험구조-용어--알아둬야-안-당한다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#9-위험구조-용어--알아둬야-안-당한다">#</a>9. 위험·구조 용어 — 알아둬야 안 당한다</h2>
<h3 id="스마트-컨트랙트-리스크"><a class="anchor" aria-hidden="true" tabindex="-1" href="#스마트-컨트랙트-리스크">#</a>스마트 컨트랙트 리스크</h3>
<p><strong>코드의 버그·취약점 때문에 자산이 빠져나갈 수 있는 위험.</strong></p>
<ul>
<li>감사(audit) 받았다고 100% 안전한 건 아님</li>
<li>메이저 프로토콜(Aave, Uniswap)도 한 번씩 사고 났음</li>
<li>신규 프로토콜은 무조건 자금 일부만 넣고 테스트</li>
</ul>
<h3 id="오라클-리스크"><a class="anchor" aria-hidden="true" tabindex="-1" href="#오라클-리스크">#</a>오라클 리스크</h3>
<p>DeFi에서 오라클이 잘못된 가격을 주면 → <strong>부당 청산 발생</strong>.</p>
<p>대형 사고 사례:</p>
<ul>
<li><strong>2022 Mango Markets</strong> — 오라클 가격 조작으로 $1.1억 탈취</li>
<li><strong>2023 Hundred Finance</strong> — 오라클 조작 + 대출 익스플로잇</li>
</ul>
<h3 id="composability-구성-가능성"><a class="anchor" aria-hidden="true" tabindex="-1" href="#composability-구성-가능성">#</a>Composability (구성 가능성)</h3>
<p>DeFi의 핵심 강점. <strong>레고처럼 다른 프로토콜과 자유롭게 연결 가능</strong>.</p>
<p>stETH (Lido) → wstETH로 감싼 후 → Aave 담보 → USDC 빌림 → Curve LP → ... 이런 식으로 무한 조합 가능.</p>
<p>장점이자 위험. <strong>한 곳이 무너지면 연쇄적으로 영향</strong>을 받는다 (composability risk).</p>
<h3 id="rehypothecation-재담보"><a class="anchor" aria-hidden="true" tabindex="-1" href="#rehypothecation-재담보">#</a>Rehypothecation (재담보)</h3>
<p><strong>담보로 받은 자산을 다시 다른 곳에 담보로 사용</strong>하는 것.</p>
<p>DeFi 자체는 잘 안 쓰지만, 리스테이킹·LRT 영역에서 비슷한 구조가 형성되고 있다 → 한 자산이 여러 군데 동시에 담보로 잡혀 있을 수 있음.</p>
<hr>
<h2 id="10-신규-트렌드--20242026-키워드"><a class="anchor" aria-hidden="true" tabindex="-1" href="#10-신규-트렌드--20242026-키워드">#</a>10. 신규 트렌드 — 2024–2026 키워드</h2>
<p>DeFi에서 최근 빠르게 자리 잡은 영역들. 새 프로토콜·트윗에서 자주 마주치는 단어들이다.</p>
<h3 id="리스테이킹-restaking"><a class="anchor" aria-hidden="true" tabindex="-1" href="#리스테이킹-restaking">#</a>리스테이킹 (Restaking)</h3>
<p>이미 스테이킹된 자산을 <strong>다른 프로토콜의 보안에도 다시 활용</strong>하는 구조.</p>
<ul>
<li>ETH를 Lido에 스테이킹 → stETH 발행</li>
<li>그 stETH를 <strong>EigenLayer</strong>에 다시 예치 → 다른 AVS(Actively Validated Service)의 보안에도 동원</li>
<li>그 대가로 추가 보상 + 포인트</li>
</ul>
<p>기존 스테이킹과 다르게 <strong>하나의 자산이 여러 곳에서 동시에 보안 자본으로 쓰임</strong> → 자본 효율 ↑, 다만 <strong>연쇄 슬래싱 리스크</strong>도 같이 ↑.</p>
<h3 id="lrt-liquid-restaking-token"><a class="anchor" aria-hidden="true" tabindex="-1" href="#lrt-liquid-restaking-token">#</a>LRT (Liquid Restaking Token)</h3>
<p>리스테이킹된 자산을 <strong>유동성 토큰으로 받는 방식.</strong> Lido(LST) 모델의 리스테이킹 버전.</p>
<ul>
<li><strong>EtherFi (eETH)</strong>, <strong>Renzo (ezETH)</strong>, <strong>Kelp (rsETH)</strong>, <strong>Puffer (pufETH)</strong> 등이 메이저</li>
<li>LRT를 들고 있으면 ETH 스테이킹 보상 + EigenLayer AVS 수수료 + 포인트(나중에 토큰 에어드랍 가능성)를 한 번에</li>
</ul>
<p>2024–2026년 DeFi 신규 TVL의 상당 부분이 이 영역에서 발생했다.</p>
<h3 id="vetoken--vote-escrow"><a class="anchor" aria-hidden="true" tabindex="-1" href="#vetoken--vote-escrow">#</a>veToken / Vote Escrow</h3>
<p>토큰을 <strong>장기 락업하는 대가로 거버넌스 권한과 수익을 받는</strong> 구조. Curve가 시작한 모델.</p>
<ul>
<li><strong>veCRV</strong> — CRV를 최대 4년까지 락업하면 ve(vote escrow)CRV 받음</li>
<li>락업 기간이 길수록 더 많은 ve 토큰</li>
<li>ve 보유자는 어느 풀에 인센티브 줄지 투표 → 프로젝트들이 표를 사기 위해 뇌물 경쟁 = "<strong>Curve war</strong>"</li>
<li><strong>Convex, Aerodrome, Velodrome</strong> 등이 이 모델을 차용</li>
</ul>
<p>장기 정렬 + 거버넌스 + 수익을 한 번에 묶는 메커니즘. 다만 토큰을 오래 묶어야 해서 유동성이 떨어진다.</p>
<h3 id="account-abstraction-erc-4337-aa"><a class="anchor" aria-hidden="true" tabindex="-1" href="#account-abstraction-erc-4337-aa">#</a>Account Abstraction (ERC-4337, AA)</h3>
<p><strong>스마트 컨트랙트 지갑으로 EOA(일반 지갑)의 한계를 넘어서는</strong> 표준.</p>
<p>기존 EOA의 제약:</p>
<ul>
<li>가스비를 ETH로만 결제 가능</li>
<li>시드구문 잃으면 끝</li>
<li>트랜잭션 1개씩 직접 서명</li>
</ul>
<p>AA로 가능해지는 것:</p>
<ul>
<li><strong>가스리스 (gasless)</strong> — 프로토콜·dApp이 가스를 대신 내줌</li>
<li><strong>소셜 로그인</strong> — 시드구문 대신 이메일·구글 계정으로 지갑 접근</li>
<li><strong>번들링</strong> — 여러 트랜잭션을 한 번에 묶어 실행</li>
<li><strong>세션 키</strong> — 한정된 권한 키로 자동 거래 (게임·트레이딩 봇)</li>
</ul>
<p><strong>Argent, Safe, Biconomy, Privy</strong> 등이 활용 중. 점진적으로 EOA를 대체하는 방향.</p>
<hr>
<h2 id="11-자주-보이는-약어-한-줄-정리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#11-자주-보이는-약어-한-줄-정리">#</a>11. 자주 보이는 약어 한 줄 정리</h2>
<table>
<thead>
<tr>
<th>약어</th>
<th>풀이</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>TVL</strong></td>
<td>Total Value Locked</td>
</tr>
<tr>
<td><strong>LP</strong></td>
<td>Liquidity Provider</td>
</tr>
<tr>
<td><strong>IL</strong></td>
<td>Impermanent Loss</td>
</tr>
<tr>
<td><strong>AMM</strong></td>
<td>Automated Market Maker</td>
</tr>
<tr>
<td><strong>CLOB</strong></td>
<td>Central Limit Order Book</td>
</tr>
<tr>
<td><strong>DEX / CEX</strong></td>
<td>Decentralized / Centralized Exchange</td>
</tr>
<tr>
<td><strong>MEV</strong></td>
<td>Maximal Extractable Value</td>
</tr>
<tr>
<td><strong>APR / APY</strong></td>
<td>Annual Percentage Rate / Yield</td>
</tr>
<tr>
<td><strong>LTV</strong></td>
<td>Loan-To-Value</td>
</tr>
<tr>
<td><strong>HF</strong></td>
<td>Health Factor</td>
</tr>
<tr>
<td><strong>OI</strong></td>
<td>Open Interest</td>
</tr>
<tr>
<td><strong>CDP</strong></td>
<td>Collateralized Debt Position</td>
</tr>
<tr>
<td><strong>RWA</strong></td>
<td>Real World Assets</td>
</tr>
<tr>
<td><strong>LST / LRT</strong></td>
<td>Liquid Staking / Liquid Restaking Token</td>
</tr>
<tr>
<td><strong>JIT</strong></td>
<td>Just-In-Time (liquidity)</td>
</tr>
<tr>
<td><strong>AA</strong></td>
<td>Account Abstraction (ERC-4337)</td>
</tr>
<tr>
<td><strong>AVS</strong></td>
<td>Actively Validated Service (EigenLayer)</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마무리">#</a>마무리</h2>
<p>DeFi 용어는 결국 <strong>"기존 금융을 그대로 옮긴 것"</strong> 이라고 보면 80%는 이해된다.</p>
<ul>
<li>대출 프로토콜 = 은행</li>
<li>DEX = 증권사</li>
<li>청산 = 마진콜</li>
<li>펀딩비 = 선물 코스트</li>
<li>TVL = 예금 잔고</li>
</ul>
<p>다른 점은 <strong>24시간 자동, 검열 안 됨, 누구나 같은 조건</strong> 정도.</p>
<p>처음엔 단어 폭격 같아도 한 사이클 돌고 나면 다 같은 얘기를 다른 단어로 부르고 있다는 게 보인다. 모르는 단어 마주칠 때마다 이 글에 한 번씩 다시 와서 보면 충분하다.</p>
<p>같이 보면 좋은 글:</p>
<ul>
<li><a href="/posts/blockchain-glossary-2026">블록체인 용어집 2026</a> — 기초 기술 용어</li>
<li><a href="/posts/crypto-slang-dictionary-2026">코인판 슬랭·약어 사전</a> — 트위터에서 보는 단어들</li>
<li><a href="/posts/crypto-stablecoin-gas-dex-guide">스테이블코인·가스비·DEX</a> — DeFi 입문 가이드</li>
<li><a href="/posts/apyx-protocol-airdrop-guide">Apyx 프로토콜 분석</a> — 새로운 스테이블코인 모델</li>
<li><a href="/posts/dango-exchange-airdrop-guide">Dango Exchange 분석</a> — CLOB DEX 사례</li>
</ul>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[DeFi]]></category>
      <category><![CDATA[용어집]]></category>
      <category><![CDATA[AMM]]></category>
      <category><![CDATA[LP]]></category>
      <category><![CDATA[TVL]]></category>
      <category><![CDATA[슬리피지]]></category>
      <category><![CDATA[MEV]]></category>
      <category><![CDATA[임퍼머넌트로스]]></category>
      <category><![CDATA[청산]]></category>
      <category><![CDATA[펀딩비]]></category>
      <category><![CDATA[파밍]]></category>
      <category><![CDATA[플래시론]]></category>
      <category><![CDATA[2026]]></category>
    </item>

    <item>
      <title><![CDATA[코인판 슬랭·약어 사전 2026 — GM부터 NGMI까지]]></title>
      <link>https://www.stragos.xyz/posts/crypto-slang-dictionary-2026</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/crypto-slang-dictionary-2026</guid>
      <pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[HODL, FOMO, GM, WAGMI, ape in, rekt, copium, exit liquidity… 코인 트위터·텔레그램 보다 보면 매일 마주치는 약어와 슬랭을 2026년 기준으로 한 번에 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>코인 트위터를 처음 들어가면 분명 한국어 타임라인인데도 외국어 같다.</p>
<p>"GM ser, WAGMI 🙏" "이번 알트 ape in 했더니 rekt..." "DYOR 하셈" "이거 진짜 알파임" — 단어 절반은 영어 약어이고, 그것도 사전 찾아도 안 나오는 것들이다. 뭘 검색해야 할지조차 막막하다.</p>
<p>근데 막상 알고 나면 별것 없다. 거의 다 <strong>감정·태도·정체성을 짧게 줄여 부르는 말</strong>이라서, 한 번 정리해두면 그 후로는 "아 또 그 얘기" 하고 넘어가게 된다.</p>
<p>이 글은 <strong>2026년 4월 기준</strong>으로 코인 트위터·텔레그램·디스코드에서 자주 마주치는 슬랭·약어를 카테고리별로 모아둔 거다. 기술 용어는 <a href="/posts/blockchain-glossary-2026">블록체인 용어집</a>, <a href="/posts/defi-glossary-2026">DeFi 용어집</a>에 따로 있다.</p>
<hr>
<h2 id="1-인사호칭--코인-트위터의-기본-어휘"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-인사호칭--코인-트위터의-기본-어휘">#</a>1. 인사·호칭 — 코인 트위터의 기본 어휘</h2>
<h3 id="gm--gn"><a class="anchor" aria-hidden="true" tabindex="-1" href="#gm--gn">#</a>GM / GN</h3>
<p><strong>Good Morning / Good Night.</strong></p>
<p>코인 트위터에서 가장 자주 보는 인사다. 아침에 일어나서 "GM 🌞" 한 줄 올리는 게 일종의 의식이고, 자기 전엔 "GN" 하고 닫는다.</p>
<p>근데 단순한 인사가 아니다. <strong>"오늘도 시장에서 살아남자"</strong> 라는 일종의 결속 표현. 시장이 박살나서 다들 조용해진 다음 날 "GM" 한 줄 올라오면 그게 또 위로가 된다.</p>
<h3 id="ser"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ser">#</a>ser</h3>
<p>"sir"의 의도적 오타. 친근한 말투다. <strong>"hey ser, 이거 알파임"</strong> 같은 식.</p>
<h3 id="anon"><a class="anchor" aria-hidden="true" tabindex="-1" href="#anon">#</a>anon</h3>
<p><strong>Anonymous.</strong> 익명. 코인판은 본명·얼굴 안 까고 활동하는 사람이 정상이라, 서로를 "anon"이라고 부르는 게 흔하다.</p>
<blockquote>
<p><strong>"NGMI anon"</strong> = "당신 못 살아남을 듯".</p>
</blockquote>
<h3 id="fren"><a class="anchor" aria-hidden="true" tabindex="-1" href="#fren">#</a>fren</h3>
<p>"friend"의 코인판식 표기. 친한 사람·동지 호칭.</p>
<p><strong>ser, fren, anon</strong> — 이 셋이 코인 트위터의 3대 호칭이다.</p>
<h3 id="og"><a class="anchor" aria-hidden="true" tabindex="-1" href="#og">#</a>OG</h3>
<p><strong>Original Gangster.</strong> "옛날부터 있던 사람."</p>
<p>2017년 ICO 붐 이전부터 활동한 사람들을 OG라고 부른다. 이 단어 붙으면 자연스럽게 "그쪽이 더 많이 안다"는 가산점이 붙는다.</p>
<h3 id="normie"><a class="anchor" aria-hidden="true" tabindex="-1" href="#normie">#</a>normie</h3>
<p><strong>코인 안 하는 일반인.</strong> 특별히 비하 의도는 없고 "코인판 밖 사람" 정도.</p>
<p>가족·직장 동료 얘기할 때 "내 normie 친구는 아직 비트코인이 뭔지도 모름" 식으로 쓴다.</p>
<h3 id="jeet"><a class="anchor" aria-hidden="true" tabindex="-1" href="#jeet">#</a>jeet</h3>
<p><strong>팔고 나간 사람.</strong> "사람들이 다 jeet했다(다 던졌다)" 식으로 쓴다.</p>
<p>대부분 부정적 뉘앙스. <strong>"이번엔 jeet하지 마라"</strong> = "이번엔 팔지 마라".</p>
<h3 id="kol"><a class="anchor" aria-hidden="true" tabindex="-1" href="#kol">#</a>KOL</h3>
<p><strong>Key Opinion Leader.</strong> 영향력 있는 인플루언서.</p>
<p>보통 트위터 팔로워 수만~수십만 명 단위. 프로젝트들이 KOL한테 토큰을 미리 뿌려서 홍보를 부탁하는 경우가 많다 → 그래서 <strong>KOL 트윗은 일정 부분 광고</strong>로 봐야 한다.</p>
<hr>
<h2 id="2-가격-심리--시장의-감정-사전"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-가격-심리--시장의-감정-사전">#</a>2. 가격 심리 — 시장의 감정 사전</h2>
<p>코인 가격이 오르내릴 때마다 사람들의 심리가 사이클을 그린다.</p>
<p>그 심리를 한 단어로 박제한 게 이 동네의 진짜 슬랭이다.</p>
<p><img src="/images/crypto/slang-emotion-cycle.svg" alt="가격 사이클과 감정 슬랭" loading="lazy" decoding="async"></p>
<h3 id="fomo"><a class="anchor" aria-hidden="true" tabindex="-1" href="#fomo">#</a>FOMO</h3>
<p><strong>Fear Of Missing Out.</strong> 놓칠까 봐 무서워서 사는 심리.</p>
<p>남들이 다 돈 벌고 있는데 나만 안 사는 것 같아서 결국 고점에서 산다. <strong>"FOMO 매수"</strong> 라는 표현이 따로 있을 정도로 흔하다.</p>
<p>99%의 경우, FOMO 매수의 결과는 <strong>물리는 것</strong>이다.</p>
<h3 id="fud"><a class="anchor" aria-hidden="true" tabindex="-1" href="#fud">#</a>FUD</h3>
<p><strong>Fear, Uncertainty, Doubt.</strong> 가격을 떨어뜨리려고 의도적으로 퍼뜨리는 부정적 정보(또는 그냥 부정적 의견).</p>
<p>"이거 다음 주에 큰 락업 풀린대" 같은 글이 올라오면 "FUD 작렬한다"고 표현한다. 진짜 위험인지 그냥 흔드는 건지 구분하는 게 트레이더의 실력.</p>
<h3 id="dyor"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dyor">#</a>DYOR</h3>
<p><strong>Do Your Own Research.</strong> "내 말 믿지 말고 직접 알아봐."</p>
<p>추천하면서 끝에 항상 붙는다. 사실상 <strong>책임 회피용 멘트</strong>다. "이거 알파임, <strong>DYOR</strong>" 라고 하면 손해 봐도 본인 책임이라는 뜻.</p>
<h3 id="copium"><a class="anchor" aria-hidden="true" tabindex="-1" href="#copium">#</a>copium</h3>
<p><strong>Cope + Opium.</strong> 손실을 직시 못 하고 자기 위로에 빠진 상태.</p>
<p>"이번엔 진짜 분명 다시 오를 거야"를 무한 반복하는 사람의 상태. 보통 이미 -70%인 경우가 많다. <strong>"copium 흡입 중"</strong> 식으로 쓴다.</p>
<h3 id="hopium"><a class="anchor" aria-hidden="true" tabindex="-1" href="#hopium">#</a>hopium</h3>
<p><strong>Hope + Opium.</strong> copium보다는 살짝 긍정적인 버전. 근거 없는 희망.</p>
<p>"이번 업데이트 끝나면 분명 100배 갈 거야 🙏" — 이게 hopium.</p>
<h3 id="diamond-hands--paper-hands"><a class="anchor" aria-hidden="true" tabindex="-1" href="#diamond-hands--paper-hands">#</a>diamond hands / paper hands</h3>
<ul>
<li><strong>diamond hands (💎🙌)</strong> — 손이 다이아몬드라서 코인을 절대 안 내려놓는 사람. <strong>"DH로 들고 간다"</strong> = "절대 안 판다."</li>
<li><strong>paper hands (📄🙌)</strong> — 종이손. 살짝만 떨어져도 패닉 셀하는 사람. 주로 자기를 놀리거나 남을 비하할 때 쓴다.</li>
</ul>
<h3 id="bagholder"><a class="anchor" aria-hidden="true" tabindex="-1" href="#bagholder">#</a>bagholder</h3>
<p><strong>가방 든 사람.</strong> 고점에서 사서 못 팔고 들고 있는 사람.</p>
<p><strong>"나 LUNA bagholder임 ㅠ"</strong> 처럼 자기 처지를 스스로 농담거리 삼는 표현.</p>
<hr>
<h2 id="3-가격-움직임--차트-위-단어들"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-가격-움직임--차트-위-단어들">#</a>3. 가격 움직임 — 차트 위 단어들</h2>
<h3 id="moon--mooning"><a class="anchor" aria-hidden="true" tabindex="-1" href="#moon--mooning">#</a>moon / mooning</h3>
<p><strong>달까지 간다 = 가격이 폭등한다.</strong></p>
<p>코인판 1세대 슬랭. 차트 그림에 🚀 붙는 이유.</p>
<h3 id="pump--dump"><a class="anchor" aria-hidden="true" tabindex="-1" href="#pump--dump">#</a>pump / dump</h3>
<ul>
<li><strong>pump</strong> — 가격 펌핑(급등)</li>
<li><strong>dump</strong> — 매도세로 급락</li>
<li><strong>pump and dump</strong> — 펌핑하고 던진다 = 인위적으로 올리고 빠지는 사기 패턴</li>
</ul>
<h3 id="rekt"><a class="anchor" aria-hidden="true" tabindex="-1" href="#rekt">#</a>rekt</h3>
<p><strong>wrecked</strong>의 의도적 표기. 박살남.</p>
<p>레버리지 청산당했거나 큰 손실 본 상태. <strong>"오늘 rekt 됐다"</strong> = "오늘 폭망했다".</p>
<h3 id="ath--atl"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ath--atl">#</a>ATH / ATL</h3>
<ul>
<li><strong>ATH</strong> — All-Time High. 사상 최고가. 가격이 ATH 찍으면 보통 "ATH 갱신!" 하고 트윗 도배됨.</li>
<li><strong>ATL</strong> — All-Time Low. 사상 최저가.</li>
</ul>
<h3 id="send-it--lfg"><a class="anchor" aria-hidden="true" tabindex="-1" href="#send-it--lfg">#</a>send it / LFG</h3>
<ul>
<li><strong>send it</strong> — 가즈아. "이거 보내자(가격이 위로 가자)" 같은 즉흥적 외침.</li>
<li><strong>LFG</strong> — <strong>Let's Fucking Go</strong>. send it보다 더 강한 외침. 발표·런칭 직전에 자주 쓴다.</li>
</ul>
<h3 id="bullish--bearish"><a class="anchor" aria-hidden="true" tabindex="-1" href="#bullish--bearish">#</a>bullish / bearish</h3>
<table>
<thead>
<tr>
<th>단어</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>bullish 🐂</strong></td>
<td>상승 전망</td>
</tr>
<tr>
<td><strong>bearish 🐻</strong></td>
<td>하락 전망</td>
</tr>
</tbody>
</table>
<p>가격뿐 아니라 의견 표현으로도 쓴다. <strong>"이 팀에 bullish하다"</strong> = "이 팀 좋게 본다".</p>
<hr>
<h2 id="4-매매-행동--들어가고-나오는-표현들"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-매매-행동--들어가고-나오는-표현들">#</a>4. 매매 행동 — 들어가고 나오는 표현들</h2>
<h3 id="ape-in"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ape-in">#</a>ape in</h3>
<p><strong>원숭이처럼 들어간다.</strong> 분석 같은 거 하지 말고 그냥 매수.</p>
<p>"리서치 했지" 같은 거창한 표현 대신 <strong>"ape했다"</strong> 라고 하면 그게 더 솔직하다. 코인판은 "ape하는 게 정상"이라며 스스로를 놀리는 분위기가 깔려 있다.</p>
<h3 id="hodl"><a class="anchor" aria-hidden="true" tabindex="-1" href="#hodl">#</a>HODL</h3>
<p><strong>"Hold on for Dear Life"의 줄임이라는 설</strong>이 있는데, 사실은 2013년 비트코인토크 게시글의 <strong>HOLD 오타</strong>가 밈이 된 거다.</p>
<p>작성자가 술 취해서 "I AM HODLING"이라고 적었고, 그게 그대로 굳어졌다. 의미는 명확 — <strong>팔지 말고 들고 있어라</strong>.</p>
<h3 id="dca"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dca">#</a>DCA</h3>
<p><strong>Dollar Cost Averaging.</strong> 분할 매수.</p>
<p>한 번에 사지 말고 시간 분산해서 사는 전략. <strong>"매주 BTC 10만 원씩 DCA 중"</strong> 처럼 쓴다. 가장 보수적이고 안정적인 매수법.</p>
<h3 id="tp--sl"><a class="anchor" aria-hidden="true" tabindex="-1" href="#tp--sl">#</a>TP / SL</h3>
<ul>
<li><strong>TP</strong> — Take Profit. 익절 가격</li>
<li><strong>SL</strong> — Stop Loss. 손절 가격</li>
</ul>
<p>선물·레버리지 거래에서 진입할 때 같이 설정한다.</p>
<h3 id="shill"><a class="anchor" aria-hidden="true" tabindex="-1" href="#shill">#</a>shill</h3>
<p><strong>광고·홍보.</strong> 좀 부정적 뉘앙스.</p>
<p><strong>"이 사람 토큰 shill 중"</strong> = "자기 보유 토큰 홍보 중". KOL이 돈 받고 트윗하면 그것도 shill.</p>
<h3 id="top-tick--bottom-tick"><a class="anchor" aria-hidden="true" tabindex="-1" href="#top-tick--bottom-tick">#</a>top tick / bottom tick</h3>
<ul>
<li><strong>top tick</strong> — 정확히 고점에 산 것</li>
<li><strong>bottom tick</strong> — 정확히 저점에 판 것</li>
</ul>
<p>둘 다 자기 처지를 비웃을 때 쓰는 표현. <strong>"또 top tick 했다"</strong> = "또 고점에 물렸다".</p>
<h3 id="alpha"><a class="anchor" aria-hidden="true" tabindex="-1" href="#alpha">#</a>alpha</h3>
<p><strong>남들이 아직 모르는 정보·기회.</strong></p>
<p><strong>"이거 alpha임"</strong> = "이거 아직 사람들이 모르는 좋은 종목·기회". 트위터에서 알파를 발견·공유하는 게 코인판 핵심 활동 중 하나다.</p>
<ul>
<li><strong>alpha leak</strong> — 알파 정보가 새어 나간 상태</li>
<li><strong>alpha group</strong> — 사적 채팅방·디스코드에서 알파 공유</li>
<li>단, "alpha임"이라고 외치는 트윗 90%는 광고(shill)인 경우가 많다</li>
</ul>
<h3 id="nfa-not-financial-advice"><a class="anchor" aria-hidden="true" tabindex="-1" href="#nfa-not-financial-advice">#</a>NFA (Not Financial Advice)</h3>
<p><strong>"투자 권유 아님"</strong> — DYOR과 짝꿍, 책임 회피용 멘트.</p>
<p><strong>"이거 100배 갈 듯, NFA"</strong> = "100배 가겠지만 손해 봐도 본인 책임이라는 안전장치." 사실상 트위터 글 끝마다 붙어다닌다.</p>
<hr>
<h2 id="5-운명예언--한-줄로-결판내는-단어들"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-운명예언--한-줄로-결판내는-단어들">#</a>5. 운명·예언 — 한 줄로 결판내는 단어들</h2>
<h3 id="wagmi--ngmi--gmi"><a class="anchor" aria-hidden="true" tabindex="-1" href="#wagmi--ngmi--gmi">#</a>WAGMI / NGMI / GMI</h3>
<table>
<thead>
<tr>
<th>약어</th>
<th>풀이</th>
<th>뉘앙스</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>WAGMI</strong></td>
<td>We're All Gonna Make It</td>
<td>우리 다 같이 성공할 거다. 결속·낙관.</td>
</tr>
<tr>
<td><strong>NGMI</strong></td>
<td>Not Gonna Make It</td>
<td>못 살아남는다. 남 비웃거나 자기를 놀릴 때.</td>
</tr>
<tr>
<td><strong>GMI</strong></td>
<td>Gonna Make It</td>
<td>(특정 사람·프로젝트는) 성공할 듯. 칭찬.</td>
</tr>
</tbody>
</table>
<p><strong>"이런 거 묻는 사람은 NGMI"</strong> = "이런 기본도 모르는 사람은 못 살아남음" (조롱).</p>
<p>반대로 <strong>"이 프로젝트 GMI"</strong> = "이건 살아남을 듯".</p>
<h3 id="probably-nothing"><a class="anchor" aria-hidden="true" tabindex="-1" href="#probably-nothing">#</a>probably nothing</h3>
<p>직역하면 "별 거 아닐 듯". 근데 실제로는 <strong>반어법</strong>이다.</p>
<p><strong>"이거 별 거 아닐 듯, probably nothing"</strong> = "이거 사실 엄청난 알파임" 의 의미. 코인 트위터의 시그니처 농담 중 하나.</p>
<h3 id="few-understand"><a class="anchor" aria-hidden="true" tabindex="-1" href="#few-understand">#</a>few understand</h3>
<p>"few(소수)만 이해한다"는 뜻.</p>
<p>자기가 들고 있는 종목·내러티브에 자신감 표현할 때 쓴다. <strong>"few understand the technology"</strong> — 약간 거만한 톤.</p>
<h3 id="wen"><a class="anchor" aria-hidden="true" tabindex="-1" href="#wen">#</a>wen</h3>
<p><strong>"when"의 의도적 오타.</strong> "언제?"라는 질문을 짧게 던지는 슬랭.</p>
<ul>
<li><strong>wen moon?</strong> — "언제 가격 오름?"</li>
<li><strong>wen lambo?</strong> — "언제 부자 됨?"</li>
<li><strong>wen TGE?</strong> — "언제 토큰 발행?"</li>
</ul>
<p>답변도 비슷한 톤: <strong>"soon™"</strong> = "곧 (이라고 1년째 말하는 중)".</p>
<h3 id="lambo"><a class="anchor" aria-hidden="true" tabindex="-1" href="#lambo">#</a>lambo</h3>
<p><strong>Lamborghini.</strong> 코인으로 부자가 되면 산다는 코인판의 농담.</p>
<p>"wen lambo?"가 클래식 슬랭. <strong>부의 시각적 상징</strong>으로 굳어졌다. 실제로 람보를 사는 사람은 극소수지만 단어는 끈질기게 살아남았다.</p>
<hr>
<h2 id="6-정체성캐릭터--코인판-사람-분류표"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-정체성캐릭터--코인판-사람-분류표">#</a>6. 정체성·캐릭터 — 코인판 사람 분류표</h2>
<h3 id="degen"><a class="anchor" aria-hidden="true" tabindex="-1" href="#degen">#</a>degen</h3>
<p><strong>Degenerate.</strong> "도박꾼". 위험 무시하고 막 들어가는 트레이더.</p>
<p><strong>자기를 놀리는 용도로 더 자주 쓰인다.</strong> "이번 주에 degen play 하나 했다" = "이번 주에 도박 한 판 했다". 코인판에선 이게 칭찬 비슷하게 쓰이기도 함.</p>
<h3 id="whale--shrimp--plebs"><a class="anchor" aria-hidden="true" tabindex="-1" href="#whale--shrimp--plebs">#</a>whale / shrimp / plebs</h3>
<table>
<thead>
<tr>
<th>호칭</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>whale 🐋</strong></td>
<td>한 번에 수십~수백억 단위로 움직이는 큰 손</td>
</tr>
<tr>
<td><strong>shrimp 🦐</strong></td>
<td>잔챙이. 1 BTC도 못 가진 사람 (보통 자기 비하)</td>
</tr>
<tr>
<td><strong>plebs</strong></td>
<td>평민. shrimp랑 비슷, 일반인 트레이더</td>
</tr>
</tbody>
</table>
<p><strong>"whale 움직임 추적해야 한다"</strong> = "고래가 어디 사는지 보자" — 이래서 <a href="https://t.me/lookonchainchannel">Lookonchain</a>, Whale Alert 같은 채널이 있는 거다.</p>
<h3 id="dev--anon-dev"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dev--anon-dev">#</a>dev / anon dev</h3>
<ul>
<li><strong>dev</strong> — 개발자. 프로젝트 만든 사람.</li>
<li><strong>anon dev</strong> — 익명 개발자. 본명·신원 안 까고 운영하는 사람. <strong>러그풀 위험이 더 크다</strong>고 본다.</li>
</ul>
<h3 id="chad--virgin--gigachad"><a class="anchor" aria-hidden="true" tabindex="-1" href="#chad--virgin--gigachad">#</a>chad / virgin / gigachad</h3>
<p>밈 캐릭터에서 온 표현.</p>
<ul>
<li><strong>chad</strong> — 자신감 있고 옳은 선택을 하는 사람. <strong>"BTC만 들고 있던 chad"</strong>.</li>
<li><strong>virgin</strong> — 그 반대. 잡코인 들고 흔들리는 사람.</li>
<li><strong>gigachad</strong> — chad의 강조형. <strong>"BTC만 4년 버틴 gigachad"</strong> — 알트 던지고 메이저만 끝까지 들고 간 사람을 칭송.</li>
</ul>
<h3 id="larp"><a class="anchor" aria-hidden="true" tabindex="-1" href="#larp">#</a>larp</h3>
<p><strong>Live Action Role Playing.</strong> 자기 능력·정체성을 과장하는 행위.</p>
<ul>
<li><strong>"이 KOL larp 같다"</strong> = "자칭 트레이더인데 실력 의심됨"</li>
<li>코인판은 익명이라 larp가 흔하다 → DM으로 거짓 자기소개로 사기치는 경우도 많음</li>
</ul>
<h3 id="maxi"><a class="anchor" aria-hidden="true" tabindex="-1" href="#maxi">#</a>maxi</h3>
<p><strong>Maximalist.</strong> 한 코인만 절대적으로 신봉하는 사람.</p>
<ul>
<li><strong>bitcoin maxi</strong> — "비트코인만 진짜 코인", 알트코인 다 부정.</li>
<li><strong>eth maxi</strong> — 이더리움 신봉자.</li>
</ul>
<p>대부분의 maxi는 다른 진영을 강하게 비판한다 → 트위터 싸움의 단골 소스.</p>
<h3 id="based"><a class="anchor" aria-hidden="true" tabindex="-1" href="#based">#</a>based</h3>
<p><strong>"맞는 말", "동의함."</strong></p>
<p>원래는 "based on truth"에서 출발한 인터넷 슬랭인데, 코인판에선 <strong>누가 정확한 의견·강한 입장을 냈을 때 응원·동의 표현</strong>으로 쓴다.</p>
<ul>
<li><strong>"ETH는 끝났다"</strong> → <strong>"based"</strong> (강한 동의)</li>
<li>반대말은 <strong>cringe</strong> (어색·동의 안 됨)</li>
</ul>
<hr>
<h2 id="7-토큰-분류--자주-보이는-카테고리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7-토큰-분류--자주-보이는-카테고리">#</a>7. 토큰 분류 — 자주 보이는 카테고리</h2>
<h3 id="shitcoin"><a class="anchor" aria-hidden="true" tabindex="-1" href="#shitcoin">#</a>shitcoin</h3>
<p><strong>가치·기술 거의 없는 토큰.</strong> 대부분의 알트코인을 비하해서 부르는 말이지만, 코인판에선 <strong>자기 처지를 놀리듯이</strong> 더 자주 쓴다.</p>
<p><strong>"이번 cycle은 shitcoin으로 100배 노린다"</strong> = "잡코인으로 큰 수익 노린다."</p>
<h3 id="memecoin"><a class="anchor" aria-hidden="true" tabindex="-1" href="#memecoin">#</a>memecoin</h3>
<p><strong>밈·문화 기반 토큰.</strong> 기술적 기능보다 커뮤니티·재미가 핵심.</p>
<p>대표 사례: 도지(DOGE), 시바이누(SHIB), 페페(PEPE), BONK, WIF, POPCAT 등. <strong>2024 cycle은 memecoin이 주류 narrative였다.</strong></p>
<p>shitcoin과 다른 점은 <strong>목적성이 명확하다는 것</strong> — "기술 없음"이 그 자체로 컨셉.</p>
<hr>
<h2 id="8-사기실패--알아둬야-안-당한다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#8-사기실패--알아둬야-안-당한다">#</a>8. 사기·실패 — 알아둬야 안 당한다</h2>
<h3 id="rug--rug-pull"><a class="anchor" aria-hidden="true" tabindex="-1" href="#rug--rug-pull">#</a>rug / rug pull</h3>
<p><strong>개발자가 유동성 들고 도망가는 것.</strong> 신생 토큰 사기 패턴 1위.</p>
<p><strong>"rug 당했다"</strong> = "내 토큰이 0이 됐다". 이 단어 나오는 토큰은 보통 살리기 어렵다 = 손실 확정.</p>
<h3 id="exit-liquidity"><a class="anchor" aria-hidden="true" tabindex="-1" href="#exit-liquidity">#</a>exit liquidity</h3>
<p><strong>탈출용 유동성.</strong> 좀 더 무서운 표현이다.</p>
<p>큰 손이 자기 물량 털기 위해 <strong>개미들을 끌어들여서 자기 매도 상대로 삼는 것</strong>을 가리킨다.</p>
<blockquote>
<p><strong>"우린 exit liquidity일 뿐"</strong>
= "우린 큰 손이 빠져나갈 때 받아주는 호구일 뿐."</p>
</blockquote>
<h3 id="honeypot"><a class="anchor" aria-hidden="true" tabindex="-1" href="#honeypot">#</a>honeypot</h3>
<p><strong>살 수는 있는데 못 파는</strong> 컨트랙트. 매수 후 매도 함수가 막혀 있어서 토큰이 갇힌다.</p>
<p>신생 밈코인에서 자주 발견된다. <a href="https://honeypot.is">honeypot.is</a> 같은 도구로 사전 점검 가능.</p>
<h3 id="ponzi"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ponzi">#</a>ponzi</h3>
<p>폰지 사기. 새로 들어온 사람 돈으로 먼저 들어온 사람한테 이자 주는 구조.</p>
<p><strong>"이건 ponzi다"</strong> 라는 비판이 자주 나오는데, 사실 코인판 상당수가 폰지에 가깝긴 하다 (그래서 "Ponzinomics"라며 스스로 인정하는 농담도 있음).</p>
<hr>
<h2 id="9-자주-보이는-트레이딩-약어"><a class="anchor" aria-hidden="true" tabindex="-1" href="#9-자주-보이는-트레이딩-약어">#</a>9. 자주 보이는 트레이딩 약어</h2>
<p>기술적 약어인데 슬랭처럼 쓰여서 같이 정리.</p>
<table>
<thead>
<tr>
<th>약어</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>MC</strong></td>
<td>Market Cap. 시가총액</td>
</tr>
<tr>
<td><strong>FDV</strong></td>
<td>Fully Diluted Valuation. 모든 토큰 풀린 시총</td>
</tr>
<tr>
<td><strong>OI</strong></td>
<td>Open Interest. 미결제 선물 계약. 시장 과열 지표</td>
</tr>
<tr>
<td><strong>TVL</strong></td>
<td>Total Value Locked. DeFi 인기도</td>
</tr>
<tr>
<td><strong>TGE</strong></td>
<td>Token Generation Event. 토큰 발행일</td>
</tr>
<tr>
<td><strong>IDO / ICO</strong></td>
<td>Initial DEX/Coin Offering. 토큰 첫 판매</td>
</tr>
<tr>
<td><strong>CEX / DEX</strong></td>
<td>중앙화 / 탈중앙 거래소</td>
</tr>
<tr>
<td><strong>APR / APY</strong></td>
<td>연 수익률 (단리 / 복리)</td>
</tr>
<tr>
<td><strong>ROI</strong></td>
<td>Return on Investment. 수익률</td>
</tr>
<tr>
<td><strong>PnL</strong></td>
<td>Profit and Loss. 손익</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="10-한국-코인판-슬랭"><a class="anchor" aria-hidden="true" tabindex="-1" href="#10-한국-코인판-슬랭">#</a>10. 한국 코인판 슬랭</h2>
<p>영어만 슬랭이 있는 게 아니다. 한국 커뮤니티에도 자기들만의 단어가 있다.</p>
<table>
<thead>
<tr>
<th>단어</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>가즈아</strong></td>
<td>"GO"의 한국식. 외쳐대는 응원.</td>
</tr>
<tr>
<td><strong>존버</strong></td>
<td>존나 버틴다. HODL의 한국 버전.</td>
</tr>
<tr>
<td><strong>떡상 / 떡락</strong></td>
<td>폭등 / 폭락</td>
</tr>
<tr>
<td><strong>익절 / 손절</strong></td>
<td>Take Profit / Stop Loss의 한국 표현</td>
</tr>
<tr>
<td><strong>물타기</strong></td>
<td>떨어진 코인을 추가 매수해서 평단가 낮추기</td>
</tr>
<tr>
<td><strong>풀매수 / 풀매도</strong></td>
<td>가진 돈 다 사기 / 다 팔기</td>
</tr>
<tr>
<td><strong>시드</strong></td>
<td>투자 원금. <strong>"시드 5천 박았다."</strong></td>
</tr>
<tr>
<td><strong>코린이</strong></td>
<td>코인 + 어린이. 코인 초보</td>
</tr>
<tr>
<td><strong>잡주 / 잡코인</strong></td>
<td>시총 작고 위험한 알트코인</td>
</tr>
<tr>
<td><strong>양봉 / 음봉</strong></td>
<td>캔들이 위로 / 아래로</td>
</tr>
<tr>
<td><strong>불장 / 베어장</strong></td>
<td>bull market / bear market의 한국 변형</td>
</tr>
<tr>
<td><strong>익절각 / 손절각</strong></td>
<td>익절·손절 타이밍</td>
</tr>
<tr>
<td><strong>존버는 승리한다</strong></td>
<td>"버티면 이긴다" — 절반은 사실, 절반은 위안</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="11-슬랭으로-이어지는-짧은-트윗-해석-연습"><a class="anchor" aria-hidden="true" tabindex="-1" href="#11-슬랭으로-이어지는-짧은-트윗-해석-연습">#</a>11. 슬랭으로 이어지는 짧은 트윗 해석 연습</h2>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="text" data-theme="github-dark"><code data-language="text" data-theme="github-dark" style="display: grid;"><span data-line=""><span>GM frens 🌞 어제 ape in한 알트 -40% rekt...</span></span>
<span data-line=""><span>근데 다른 anon이 "이건 진짜 알파다 DYOR" 한 거 하나는 +120% 갔다.</span></span>
<span data-line=""><span>WAGMI ser, 다음 주 토큰 unlock 풀린대 — FUD 살짝 깔리겠지만 결국 GMI일 듯 🙏</span></span>
<span data-line=""><span>이번에도 paper hands는 NGMI, diamond hands는 끝까지 간다.</span></span>
<span data-line=""><span>LFG 🚀</span></span></code></pre></figure>
<p>해석:</p>
<blockquote>
<p><strong>굿모닝 친구들 🌞 어제 분석 안 하고 매수한 알트가 -40% 폭망함.</strong></p>
<p><strong>근데 다른 익명이 "이건 진짜 미공개 정보다, 직접 알아봐라" 한 거 하나는 +120% 갔다.</strong></p>
<p><strong>우린 다 같이 살아남을 거다, 다음 주 토큰 락업 풀린다는데 부정적 정보가 깔리겠지만 결국 살아남을 듯 🙏</strong></p>
<p><strong>이번에도 패닉 셀하는 사람은 못 살아남고, 끝까지 들고 가는 사람이 이긴다.</strong></p>
<p><strong>가즈아 🚀</strong></p>
</blockquote>
<p>읽을 수 있게 되면 코인 트위터의 절반은 이런 톤이라는 게 보일 거다.</p>
<hr>
<h2 id="마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마무리">#</a>마무리</h2>
<p>슬랭은 트렌드처럼 변한다. 2017년에 메인이던 단어들 중 절반은 이미 안 쓰인다.</p>
<p><strong>2026년 4월 기준</strong>으로 위 단어들이 메이저인데, 1년 뒤에는 또 새로운 게 자리 잡고 있을 거다. 다 외울 필요는 없다. 코인 트위터·텔레그램에서 모르는 단어 마주칠 때마다 이 글에 와서 찾아보면 된다.</p>
<p>한 번 익숙해지면 그 다음엔 안 보고도 분위기가 잡힌다. 단어를 안다고 돈을 버는 건 아니지만, 적어도 남들이 무슨 얘기 하는지는 알아듣고 시작할 수 있다.</p>
<p>같이 보면 좋은 글:</p>
<ul>
<li><a href="/posts/blockchain-glossary-2026">블록체인 용어집 2026</a> — 기술 용어 정리</li>
<li><a href="/posts/defi-glossary-2026">DeFi 용어집 2026</a> — DeFi 핵심 어휘</li>
<li><a href="/posts/crypto-airdrop-beginner-guide">에어드랍 입문</a> — 무료 토큰 받는 법</li>
<li><a href="/posts/crypto-beginner-mistakes">코인 처음 할 때 실수들</a> — 초보자가 흔히 하는 실수</li>
</ul>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[코인슬랭]]></category>
      <category><![CDATA[약어]]></category>
      <category><![CDATA[HODL]]></category>
      <category><![CDATA[FOMO]]></category>
      <category><![CDATA[GM]]></category>
      <category><![CDATA[WAGMI]]></category>
      <category><![CDATA[NGMI]]></category>
      <category><![CDATA[DYOR]]></category>
      <category><![CDATA[ape]]></category>
      <category><![CDATA[degen]]></category>
      <category><![CDATA[코인트위터]]></category>
      <category><![CDATA[2026]]></category>
      <category><![CDATA[초보자]]></category>
    </item>

    <item>
      <title><![CDATA[STS4 5분 셋팅 — 첫 출근 첫날 바로 코딩 시작하기]]></title>
      <link>https://www.stragos.xyz/posts/sts4-quick-setup</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/sts4-quick-setup</guid>
      <pubDate>Tue, 28 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[SI·금융·공기업 신입이 첫 출근하자마자 STS4를 5분 만에 셋팅해서 바로 코딩 시작하는 방법. JDK 매핑·UTF-8 인코딩 4군데·Lombok·Boot Dashboard까지 2026년 최신 기준 가이드입니다.]]></description>
      <content:encoded><![CDATA[<p>첫 출근날, 사수가 USB 던지면서 "이거 깔고 점심 먹고 와" 하고 사라지는 그 상황. 회사가 IntelliJ 안 사주고 STS만 쓰라고 할 때, <strong>5분 안에</strong> 코딩 시작 가능한 상태까지 만드는 방법을 정리합니다.</p>
<p>검색하면 STS3 시절(2018~2020) 글이 대부분이라 화면이 다르고 메뉴 위치도 안 맞습니다. 이 글은 <strong>STS 4.21 + JDK 17</strong> 기준입니다.</p>
<p><img src="/images/dev/sts-quick-setup.svg" alt="STS4 5분 셋팅" loading="lazy" decoding="async"></p>
<hr>
<h2 id="누구한테-필요한-글인가요"><a class="anchor" aria-hidden="true" tabindex="-1" href="#누구한테-필요한-글인가요">#</a>누구한테 필요한 글인가요?</h2>
<ul>
<li>SI·금융·공기업 신입으로 첫 출근한 분</li>
<li>회사가 IntelliJ 라이선스 안 사주고 <strong>STS만 강요</strong>하는 환경</li>
<li>이미 STS 깔아봤는데 한글이 <code>???</code>로 깨져서 빡친 분</li>
<li>사수가 "그냥 알아서 해" 라고 하고 사라진 분</li>
</ul>
<p>순서대로만 따라가면 막히지 않습니다.</p>
<hr>
<h2 id="1단계-sts4-다운로드-1분"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1단계-sts4-다운로드-1분">#</a>1단계. STS4 다운로드 (1분)</h2>
<p><a href="https://spring.io/tools">spring.io/tools</a> 접속 → 자기 OS 골라서 다운로드.</p>
<ul>
<li><strong>Windows</strong>: <code>.zip</code> (win32.x86_64)</li>
<li><strong>macOS</strong>: <code>.dmg</code></li>
<li><strong>Linux</strong>: <code>.tar.gz</code></li>
</ul>
<p><strong>중요</strong>: STS는 Eclipse 기반이라 <strong>설치 마법사가 없습니다</strong>. 그냥 압축 파일이에요.</p>
<blockquote>
<p><strong>왜 설치 프로그램이 없나요?</strong></p>
<p>Eclipse 계열은 "압축 풀면 그게 설치 끝"이라는 철학을 따릅니다. 레지스트리에 뭐 박지도 않고, 시스템 PATH에도 안 건드립니다. 회사 노트북에서 관리자 권한 없이도 동작한다는 게 큰 장점이에요.</p>
</blockquote>
<hr>
<h2 id="2단계-압축-풀고-워크스페이스-만들기-30초"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2단계-압축-풀고-워크스페이스-만들기-30초">#</a>2단계. 압축 풀고 워크스페이스 만들기 (30초)</h2>
<p>압축은 <strong>C 드라이브 루트</strong> 또는 <strong>사용자 폴더 하위</strong>에 풀어주세요. <code>Program Files</code> 같은 권한 필요한 경로는 피합니다 (나중에 Lombok 설치 때 권한 에러 납니다).</p>
<pre><code>C:\sts-4.21.0.RELEASE\
└── SpringToolSuite4.exe   ← 더블클릭
</code></pre>
<p>처음 실행하면 <strong>Workspace 경로</strong>를 물어봅니다. 기본값 그대로 <code>Use this as the default and do not ask again</code> 체크하고 Launch.</p>
<hr>
<h2 id="3단계-jdk-17-매핑-1분"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3단계-jdk-17-매핑-1분">#</a>3단계. JDK 17 매핑 (1분)</h2>
<p><code>Window > Preferences > Java > Installed JREs</code></p>
<p><img src="/images/dev/sts-jre-mapping.svg" alt="Installed JREs 화면" loading="lazy" decoding="async"></p>
<ol>
<li><strong>Add...</strong> → Standard VM 선택</li>
<li>JRE home에 JDK 17 설치 폴더 지정 (<code>C:\Program Files\Java\jdk-17</code> 등)</li>
<li>Finish</li>
<li>추가된 JDK 17 옆 <strong>체크박스</strong> 클릭 → Default 지정</li>
<li>Apply and Close</li>
</ol>
<blockquote>
<p><strong>왜 명시적으로 매핑하나요?</strong></p>
<p>STS는 처음 켜질 때 시스템 PATH에 잡힌 JRE를 자동으로 찾습니다. 그런데 회사 노트북에는 <strong>JDK 8과 17이 같이 깔려있는 경우</strong>가 많아요. 자동 인식이 JDK 8을 잡으면 Spring Boot 3.x 프로젝트가 통째로 안 뜹니다. 그래서 명시적으로 17을 default로 박아둡니다.</p>
</blockquote>
<p>JDK가 아예 없다면 <a href="https://adoptium.net">Eclipse Temurin</a> 에서 무료로 받으세요. Oracle JDK는 회사용 라이선스 이슈가 있어서 Temurin이 안전합니다.</p>
<hr>
<h2 id="4단계-utf-8-인코딩-4군데-박기--핵심-1분-30초"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4단계-utf-8-인코딩-4군데-박기--핵심-1분-30초">#</a>4단계. UTF-8 인코딩 4군데 박기 (★ 핵심, 1분 30초)</h2>
<p><strong>이 글에서 제일 중요한 부분입니다.</strong> 한글이 <code>???</code>로 깨져서 회의록 PR에서 사수에게 욕먹는 그 사건, 여기서 막아둡니다.</p>
<p><img src="/images/dev/sts-encoding-map.svg" alt="UTF-8 박는 4군데" loading="lazy" decoding="async"></p>
<h3 id="4-1-workspace-전역-인코딩"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-1-workspace-전역-인코딩">#</a>4-1. Workspace 전역 인코딩</h3>
<p><code>Window > Preferences > General > Workspace</code></p>
<p>→ 하단 <strong>Text file encoding</strong> → Other 선택 → <strong>UTF-8</strong></p>
<h3 id="4-2-web-파일들-css--html--jsp"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-2-web-파일들-css--html--jsp">#</a>4-2. Web 파일들 (CSS · HTML · JSP)</h3>
<p><code>Window > Preferences > Web</code> 아래 세 항목 모두:</p>
<ul>
<li><strong>CSS Files</strong> → Encoding: ISO 10646/Unicode(UTF-8)</li>
<li><strong>HTML Files</strong> → Encoding: ISO 10646/Unicode(UTF-8)</li>
<li><strong>JSP Files</strong> → Encoding: ISO 10646/Unicode(UTF-8)</li>
</ul>
<p>JSP 안 쓴다고 빼지 마세요. 회사 레거시 프로젝트에서 갑자기 튀어나옵니다.</p>
<h3 id="4-3-content-types"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-3-content-types">#</a>4-3. Content Types</h3>
<p><code>Window > Preferences > General > Content Types</code></p>
<ul>
<li><strong>Text > Java Source File</strong> 선택 → Default encoding: <code>UTF-8</code> 입력 → Update</li>
<li><strong>Text > CSS · HTML · JSP</strong> 도 동일하게 처리</li>
</ul>
<h3 id="4-4-프로젝트-단위-재설정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-4-프로젝트-단위-재설정">#</a>4-4. 프로젝트 단위 재설정</h3>
<p>전역 설정을 해도 <strong>이미 만든 프로젝트는 자동 적용 안 됩니다.</strong> 기존 프로젝트가 있다면:</p>
<p><code>프로젝트 우클릭 > Properties > Resource > Text file encoding > UTF-8</code></p>
<blockquote>
<p><strong>왜 4군데 다 박나요?</strong></p>
<p>Eclipse 계열의 인코딩 설정은 <strong>상속 구조</strong>입니다. Workspace → Content Type → 파일 타입별 설정 → 프로젝트 설정 순으로 덮어쓰기 됩니다. 한 군데라도 빠지면 그 영역만 시스템 기본값(Windows = MS949)으로 떨어져서, 거기서만 한글이 깨집니다. 그래서 한꺼번에 다 박아두는 거예요.</p>
<p>한 번 깨진 한글은 <strong>복구 불가능</strong>합니다. 셋팅 먼저, 코딩은 그 다음입니다.</p>
</blockquote>
<hr>
<h2 id="5단계-lombok-설치-1분"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5단계-lombok-설치-1분">#</a>5단계. Lombok 설치 (1분)</h2>
<p>Spring Boot 프로젝트에서 <code>@Getter</code>, <code>@Setter</code>, <code>@RequiredArgsConstructor</code> 같은 어노테이션을 쓰려면 IDE에 Lombok이 인식되어야 합니다.</p>
<p><img src="/images/dev/sts-lombok-installer.svg" alt="Lombok Installer" loading="lazy" decoding="async"></p>
<ol>
<li><a href="https://projectlombok.org">projectlombok.org</a> 접속 → <strong>Download</strong> 클릭 → 최신 jar 다운</li>
<li>다운받은 폴더로 이동 → <code>lombok-1.18.x.jar</code> <strong>더블클릭</strong>
<ul>
<li>안 열리면 PowerShell에서 <code>java -jar lombok-1.18.x.jar</code></li>
</ul>
</li>
<li>인스톨러가 시스템 IDE를 자동 검색합니다 → STS4 경로 자동 감지</li>
<li>STS 옆 체크박스 ✓ → <strong>Install / Update</strong> 클릭</li>
<li>STS 재시작 (중요!)</li>
</ol>
<blockquote>
<p><strong>왜 별도 설치인가요? Maven 의존성으로 충분하지 않나요?</strong></p>
<p>Lombok은 <strong>컴파일 시점에 코드를 자동 생성</strong>하는 라이브러리입니다. Maven에 의존성을 추가하면 빌드는 되지만, IDE는 별도로 설정해줘야 빨간 줄이 안 그어집니다. jar 실행은 <code>SpringToolSuite4.ini</code> 파일에 <code>-javaagent:lombok.jar</code> 라인을 자동으로 추가해줘요. 직접 ini 파일 건드리는 것보다 인스톨러가 안전합니다.</p>
</blockquote>
<p>설치 후 STS 재시작 안 하면 적용 안 됩니다. 꼭 재시작.</p>
<hr>
<h2 id="6단계-boot-dashboard-켜기-15초"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6단계-boot-dashboard-켜기-15초">#</a>6단계. Boot Dashboard 켜기 (15초)</h2>
<p><code>Window > Show View > Other > Spring > Boot Dashboard</code></p>
<p><img src="/images/dev/sts-boot-dashboard.svg" alt="Boot Dashboard" loading="lazy" decoding="async"></p>
<p>이게 <strong>STS의 진짜 핵심 기능</strong>입니다. Spring Boot 앱을 클릭 한 번으로 Run / Debug / Stop / Restart 가능. IntelliJ의 Run 버튼이랑 같은 역할이에요.</p>
<p>여기서 앱을 Run하면 자동으로 활성 프로파일, 포트, PID까지 한눈에 보입니다. 디버그 모드 토글도 바로 가능.</p>
<hr>
<h2 id="7단계-자주-쓰는-단축키-5개"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7단계-자주-쓰는-단축키-5개">#</a>7단계. 자주 쓰는 단축키 5개</h2>
<table>
<thead>
<tr>
<th>단축키</th>
<th>기능</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Ctrl + Shift + R</code></td>
<td>파일 빠른 검색 (Resource)</td>
</tr>
<tr>
<td><code>Ctrl + Shift + T</code></td>
<td>클래스(타입) 빠른 검색</td>
</tr>
<tr>
<td><code>Ctrl + .</code></td>
<td>다음 에러로 이동</td>
</tr>
<tr>
<td><code>Ctrl + 1</code></td>
<td>Quick Fix (자동 import, 자동 변수 선언 등)</td>
</tr>
<tr>
<td><code>Ctrl + Shift + O</code></td>
<td>import 정리</td>
</tr>
</tbody>
</table>
<p><strong><code>Ctrl + 1</code></strong> 하나만 익혀도 코딩 속도가 두 배가 됩니다. 빨간 줄 그어진 곳에서 누르면 거의 다 해결돼요.</p>
<hr>
<h2 id="8단계-첫-프로젝트-만들고-검증-1분"><a class="anchor" aria-hidden="true" tabindex="-1" href="#8단계-첫-프로젝트-만들고-검증-1분">#</a>8단계. 첫 프로젝트 만들고 검증 (1분)</h2>
<p><code>File > New > Spring Starter Project</code></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody>
<tr>
<td>Type</td>
<td>Maven</td>
</tr>
<tr>
<td>Java Version</td>
<td><strong>17</strong></td>
</tr>
<tr>
<td>Packaging</td>
<td>Jar</td>
</tr>
<tr>
<td>Language</td>
<td>Java</td>
</tr>
</tbody>
</table>
<p>Next → <strong>Dependencies</strong> 에서 <code>Spring Web</code>, <code>Lombok</code> 두 개 체크 → Finish.</p>
<p>Boot Dashboard에서 프로젝트 선택 → <strong>▶ Run</strong> → 콘솔에 <code>Started Application in X.X seconds</code> 뜨면 성공.</p>
<p>브라우저에서 <code>http://localhost:8080</code> 접속해서 White Label Error Page 뜨면 셋팅 완료입니다. (404 같지만 Spring Boot가 정상 동작 중이라는 뜻)</p>
<hr>
<h2 id="자주-막히는-5가지"><a class="anchor" aria-hidden="true" tabindex="-1" href="#자주-막히는-5가지">#</a>자주 막히는 5가지</h2>
<h3 id="jre-not-found-에러"><a class="anchor" aria-hidden="true" tabindex="-1" href="#jre-not-found-에러">#</a>"JRE not found" 에러</h3>
<p>→ 3단계 누락. Installed JREs에 JDK 17 등록 안 됨. 다시 가서 등록 + Default 체크.</p>
<h3 id="한글이-로-깨짐"><a class="anchor" aria-hidden="true" tabindex="-1" href="#한글이-로-깨짐">#</a>한글이 <code>???</code>로 깨짐</h3>
<p>→ 4단계 4군데 중 한 군데 누락. 특히 <strong>Content Types를 빼먹는 경우</strong>가 가장 흔합니다. 프로젝트 단위 설정도 따로 해야 한다는 점 잊지 마세요.</p>
<h3 id="lombok-getter-가-안-먹힘"><a class="anchor" aria-hidden="true" tabindex="-1" href="#lombok-getter-가-안-먹힘">#</a>Lombok <code>@Getter</code> 가 안 먹힘</h3>
<p>→ 5단계 후 STS 재시작 안 한 경우. 또는 <code>SpringToolSuite4.ini</code>에 <code>-javaagent</code> 라인이 안 들어간 경우. ini 파일 직접 열어서 확인:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="ini" data-theme="github-dark"><code data-language="ini" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">-javaagent:lombok.jar</span></span></code></pre></figure>
<h3 id="maven-빌드-시-한글이-깨짐"><a class="anchor" aria-hidden="true" tabindex="-1" href="#maven-빌드-시-한글이-깨짐">#</a>Maven 빌드 시 한글이 깨짐</h3>
<p>→ IDE 인코딩이랑 별개로 Maven은 자체 설정을 봅니다. <code>pom.xml</code>에 추가:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="xml" data-theme="github-dark"><code data-language="xml" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">properties</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">project.build.sourceEncoding</span><span style="color:#E1E4E8">>UTF-8&#x3C;/</span><span style="color:#85E89D">project.build.sourceEncoding</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">java.version</span><span style="color:#E1E4E8">>17&#x3C;/</span><span style="color:#85E89D">java.version</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#85E89D">properties</span><span style="color:#E1E4E8">></span></span></code></pre></figure>
<h3 id="boot-dashboard에-프로젝트가-안-보임"><a class="anchor" aria-hidden="true" tabindex="-1" href="#boot-dashboard에-프로젝트가-안-보임">#</a>Boot Dashboard에 프로젝트가 안 보임</h3>
<p>→ Spring Boot 프로젝트로 인식 안 됨. <code>pom.xml</code>에 <code>spring-boot-starter-parent</code> 또는 <code>spring-boot-starter</code> 의존성이 있는지 확인하세요. Maven Update (<code>Alt + F5</code>)도 해보세요.</p>
<hr>
<h2 id="이제-진짜-시작입니다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#이제-진짜-시작입니다">#</a>이제 진짜 시작입니다</h2>
<p>5분 안에 끝났을 거예요. 이제 진짜 코드를 짤 시간입니다.</p>
<p>다음 단계로 풀스택 셋팅(React 연동, Oracle DB, MyBatis)이 궁금하다면 <a href="/posts/springboot-react-oracle-mybatis-setup-1">Spring Boot + React + Oracle + MyBatis 셋팅 시리즈</a>에서 이어서 보시면 됩니다.</p>
<p>회사에서 사수가 "왜 IntelliJ 안 쓰냐" 라고 묻거든, "라이선스 사주시면 쓰겠습니다" 라고 답하시면 됩니다. 그 전까진 STS4도 충분히 잘 굴러갑니다.</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[STS]]></category>
      <category><![CDATA[SpringToolSuite]]></category>
      <category><![CDATA[SpringBoot]]></category>
      <category><![CDATA[IDE]]></category>
      <category><![CDATA[셋팅]]></category>
      <category><![CDATA[초보자]]></category>
      <category><![CDATA[SI]]></category>
      <category><![CDATA[공기업]]></category>
      <category><![CDATA[신입]]></category>
      <category><![CDATA[JDK17]]></category>
      <category><![CDATA[Lombok]]></category>
    </item>

    <item>
      <title><![CDATA[블록체인 용어집 2026 — 코인판에서 자주 마주치는 단어들 정리]]></title>
      <link>https://www.stragos.xyz/posts/blockchain-glossary-2026</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/blockchain-glossary-2026</guid>
      <pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[DAO, 유틸리티/거버넌스 토큰, 리스테이킹, 모듈러, 인텐트, AI 에이전트… 코인 시장에서 "다들 아는 듯이 쓰는데 나만 모르는" 용어들을 2026년 기준으로 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>코인판에 잠깐만 한눈을 팔아도, 텔레그램·트위터 타임라인에 외계어가 늘어 있다.</p>
<p>"리스테이킹"이 뭔지 몰라서 검색하면 "LRT"가 또 튀어나오고, "모듈러"라더니 "DA 레이어"가 따라오고, 갑자기 AI 에이전트 토큰이 시총 상위에 박혀 있다. 분명 한국어로 적혀 있는데도 머리에 안 들어온다.</p>
<p>그래서 정리했다. <strong>2026년 4월 시점</strong>에서, 코인판에서 한 번쯤은 마주치는 용어들. 사전식 정의가 아니라 "왜 이 단어가 만들어졌고, 왜 지금 사람들이 얘기하는지" 정도까지.</p>
<p>처음부터 다 외울 필요는 없다. 모르는 단어 마주쳤을 때 한 번씩 와서 보면 충분하다.</p>
<hr>
<h2 id="1-기초--블록체인은-결국-뭔가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-기초--블록체인은-결국-뭔가">#</a>1. 기초 — 블록체인은 결국 뭔가</h2>
<h3 id="블록체인-blockchain"><a class="anchor" aria-hidden="true" tabindex="-1" href="#블록체인-blockchain">#</a>블록체인 (Blockchain)</h3>
<p>거래 기록을 <strong>블록</strong>이라는 단위로 묶고, 그 블록을 시간순으로 <strong>체인처럼 연결한 장부</strong>다. 누가 누구한테 얼마를 보냈는지가 영원히 남고, 한 번 기록되면 못 바꾼다.</p>
<p>은행 장부랑 다른 점은 딱 하나, <strong>관리자가 없다</strong>는 거다. 전 세계 컴퓨터 수만 대(노드)가 같은 장부를 들고 있고, 새로운 거래가 일어나면 다 같이 검증해서 다음 블록에 넣는다.</p>
<p><img src="/images/crypto/glossary-block-chain.svg" alt="블록체인 구조" loading="lazy" decoding="async"></p>
<h3 id="노드-node"><a class="anchor" aria-hidden="true" tabindex="-1" href="#노드-node">#</a>노드 (Node)</h3>
<p>블록체인 네트워크에 참여하고 있는 <strong>컴퓨터 한 대 한 대</strong>를 노드라고 부른다. 거래를 검증하고, 장부 사본을 갖고 있다. 비트코인은 전 세계에 약 2만 개, 이더리움은 약 1만 개의 노드가 돌고 있다.</p>
<blockquote>
<p><strong>노드는 왜 자기 전기·서버 비용 써가며 참여할까?</strong></p>
<p>한마디로 <strong>돈이 되니까.</strong> PoW 체인(비트코인)에서 노드는 새 블록을 만들면 <strong>블록 보상(현재 BTC 3.125개) + 거래 수수료</strong>를 받는다. PoS 체인에서는 스테이킹된 코인의 <strong>연 3–7% 이자</strong>가 들어온다.</p>
<p>그냥 거래 내역만 보관하는 <strong>풀 노드(Full Node)</strong> 도 있다. 이건 보상 없이 자발적으로 운영되는데, "내가 직접 검증한 장부를 신뢰할 수 있다"는 이념적 동기 + 자기 비즈니스(거래소·지갑) 인프라용으로 돌리는 경우가 대부분.</p>
</blockquote>
<h3 id="합의-메커니즘-consensus"><a class="anchor" aria-hidden="true" tabindex="-1" href="#합의-메커니즘-consensus">#</a>합의 메커니즘 (Consensus)</h3>
<p>수많은 노드가 "이 거래가 진짜야" 하고 동의하는 방식. 두 가지가 대표적이다.</p>
<ul>
<li><strong>PoW (작업 증명, Proof of Work)</strong> — 컴퓨터로 어려운 수학 문제를 먼저 푸는 사람이 다음 블록을 만든다 → 비트코인. 전기를 많이 쓴다. 이 일을 하는 사람을 <strong>마이너(Miner, 채굴자)</strong> 라고 부른다.</li>
<li><strong>PoS (지분 증명, Proof of Stake)</strong> — 토큰을 많이 맡긴(스테이킹) 사람이 검증할 권리를 얻는다 → 이더리움(2022년 9월부터). 전기 소비가 99% 감소. 이 일을 하는 사람을 <strong>밸리데이터(Validator, 검증자)</strong> 라고 부른다.</li>
</ul>
<blockquote>
<p><strong>왜 이런 복잡한 방식이 필요한가?</strong></p>
<p>"관리자 없이 모르는 사람들끼리 합의하기" 위해서다. 비싼 비용(전기·자금)을 들여야 블록을 만들 자격이 생기게 만들어두면, <strong>부정행위는 그 비용을 잃는 손해로 이어진다</strong>. PoW는 전기를, PoS는 락업한 자금을 잃는다(슬래싱). 그래서 정직하게 검증하는 게 더 이득.</p>
</blockquote>
<hr>
<h2 id="2-지갑키계정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-지갑키계정">#</a>2. 지갑·키·계정</h2>
<h3 id="개인키-private-key--시드-구문-seed-phrase"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개인키-private-key--시드-구문-seed-phrase">#</a>개인키 (Private Key) / 시드 구문 (Seed Phrase)</h3>
<p>지갑의 <strong>진짜 비밀번호</strong>다. 64자리 16진수 문자열인데, 사람이 외울 수 없으니 12개나 24개 영어 단어로 변환해서 보여주는 게 시드 구문이다.</p>
<p>이걸 가진 사람이 곧 그 지갑의 주인이다. 거래소 계정처럼 "비밀번호 분실하면 본인 확인하고 복구" 같은 게 없다. 종이에 적어서 보관하라는 게 그래서 그런 거다.</p>
<h3 id="eoa-vs-스마트-계정-account-abstraction"><a class="anchor" aria-hidden="true" tabindex="-1" href="#eoa-vs-스마트-계정-account-abstraction">#</a>EOA vs 스마트 계정 (Account Abstraction)</h3>
<p>지금까지 일반 지갑(메타마스크 등)은 <strong>EOA(Externally Owned Account)</strong>, 즉 개인키 1개로 관리되는 단순한 계정이었다.</p>
<p>**계정 추상화(AA, ERC-4337)**는 지갑을 <strong>스마트 컨트랙트로 만드는 표준</strong>이다. 2026년 시점에서 이미 메이저 L2 지갑(Base, Optimism)이 적극 채택 중이다. 뭐가 달라지냐면:</p>
<ul>
<li><strong>가스비를 ETH 말고 USDC로</strong> 낼 수 있다</li>
<li><strong>소셜 복구</strong> 가능 (지인 3명이 동의하면 지갑 복구)</li>
<li><strong>패스키 / 지문</strong>으로 로그인 (시드 구문 없이도 됨)</li>
<li><strong>여러 거래를 한 번에</strong> 묶어 처리</li>
</ul>
<p>지갑이 앱처럼 동작하는 시대로 넘어가는 중이다.</p>
<hr>
<h2 id="3-l1l2--코인이-사는-동네"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-l1l2--코인이-사는-동네">#</a>3. L1·L2 — 코인이 사는 동네</h2>
<h3 id="l1-layer-1"><a class="anchor" aria-hidden="true" tabindex="-1" href="#l1-layer-1">#</a>L1 (Layer 1)</h3>
<p>자기만의 블록체인을 갖고 있는 <strong>메인 네트워크</strong>. 비트코인, 이더리움, 솔라나, 아발란체, 수이(Sui) 같은 게 다 L1이다.</p>
<h3 id="l2-layer-2"><a class="anchor" aria-hidden="true" tabindex="-1" href="#l2-layer-2">#</a>L2 (Layer 2)</h3>
<p>L1이 너무 느리고 비싸서, 그 위에 얹어서 <strong>거래를 빠르고 싸게 처리하는 보조 체인</strong>이다. 결과만 가끔 L1에 정산해서 기록한다.</p>
<p>이더리움 L2가 2026년 기준 가장 활발하다. <strong>Base</strong>(코인베이스), <strong>Arbitrum</strong>, <strong>Optimism</strong>, <strong>zkSync</strong>, <strong>Linea</strong> 같은 게 대표적. 가스비가 L1의 1/100–1/1000 수준이라 실질적으로 거의 모든 DeFi가 L2로 옮겨갔다.</p>
<p><img src="/images/crypto/glossary-l1-l2.svg" alt="L1과 L2 관계" loading="lazy" decoding="async"></p>
<h3 id="롤업-rollup"><a class="anchor" aria-hidden="true" tabindex="-1" href="#롤업-rollup">#</a>롤업 (Rollup)</h3>
<p>L2 중에서도 <strong>거래를 모아서(roll up) 압축한 뒤 L1에 한 방에 기록</strong>하는 방식. 두 종류가 있다.</p>
<ul>
<li><strong>옵티미스틱 롤업</strong> (Arbitrum, Optimism, Base): 일단 맞다고 가정하고 처리, 이상 있으면 7일 내 이의 제기</li>
<li><strong>ZK 롤업</strong> (zkSync, Linea, StarkNet): 수학적 증명으로 거래 정합성을 검증</li>
</ul>
<p>2025–2026년 트렌드는 <strong>ZK 우세</strong> 쪽으로 기울고 있다. 인출 시간이 짧고(7일 → 거의 즉시) 수학적으로 더 견고해서.</p>
<h3 id="모듈러-블록체인-modular-blockchain"><a class="anchor" aria-hidden="true" tabindex="-1" href="#모듈러-블록체인-modular-blockchain">#</a>모듈러 블록체인 (Modular Blockchain)</h3>
<p>기존 블록체인은 <strong>합의 + 실행 + 데이터 저장 + 결제</strong>를 한 체인이 다 했다(모놀리식). 모듈러는 이걸 <strong>층층이 분리</strong>한다.</p>
<ul>
<li><strong>실행</strong> → L2 (거래 처리)</li>
<li><strong>합의·결제</strong> → 이더리움</li>
<li><strong>데이터 가용성(DA)</strong> → Celestia, EigenDA 같은 전용 레이어</li>
</ul>
<p>각 층이 자기 역할만 잘하면 되니까 전체적으로 더 빠르고 싸진다. 2024–2026 코인 내러티브에서 가장 큰 축 중 하나.</p>
<p><img src="/images/crypto/glossary-modular.svg" alt="모놀리식 vs 모듈러" loading="lazy" decoding="async"></p>
<h3 id="앱체인-app-chain"><a class="anchor" aria-hidden="true" tabindex="-1" href="#앱체인-app-chain">#</a>앱체인 (App-chain)</h3>
<p><strong>한 애플리케이션 전용으로 통째로 만든 블록체인.</strong> "다른 앱이랑 공유하는 체인이 답답하니까, 우리 게임/DEX 전용 체인을 만들자"는 컨셉. 그 체인 위에선 그 앱이 가스비 정책·수수료까지 다 정한다.</p>
<ul>
<li><strong>dYdX v4</strong> — 자체 Cosmos 앱체인으로 이전</li>
<li><strong>Avalanche Subnet</strong> — 게임·기업 전용 체인 양산</li>
<li><strong>Polygon CDK / OP Stack</strong> — L2 앱체인 키트</li>
</ul>
<h3 id="브리지-bridge"><a class="anchor" aria-hidden="true" tabindex="-1" href="#브리지-bridge">#</a>브리지 (Bridge)</h3>
<p><strong>다른 블록체인 간 자산을 옮겨주는 통로.</strong> 이더리움의 ETH를 솔라나에서 쓰려면 브리지를 거쳐야 한다.</p>
<ul>
<li><strong>LayerZero, Wormhole, Across</strong> — 메이저 크로스체인 브리지</li>
<li><strong>Native bridge</strong> — L2 공식 브리지 (Base, Arbitrum 등)</li>
</ul>
<blockquote>
<p><strong>브리지는 코인 시장에서 가장 큰 해킹 표적이다.</strong> 2022–2023년 사이 Ronin(6억 달러), Wormhole(3.2억 달러) 등 굵직한 사고가 이어졌다. 가급적 <strong>공식 브리지</strong>, 액수가 크면 <strong>여러 번 나눠서</strong> 옮기는 게 안전.</p>
</blockquote>
<h3 id="오라클-oracle"><a class="anchor" aria-hidden="true" tabindex="-1" href="#오라클-oracle">#</a>오라클 (Oracle)</h3>
<p>블록체인은 외부 정보(주가·환율·날씨)를 스스로 못 본다. <strong>외부 데이터를 온체인으로 가져다주는 인프라</strong>가 오라클이다.</p>
<ul>
<li><strong>Chainlink</strong> — 가격 피드의 표준</li>
<li><strong>Pyth</strong> — 솔라나 기반, 고속 가격 피드</li>
<li><strong>RedStone</strong> — LST/LRT 가격에 강함</li>
</ul>
<p>DeFi에서 청산·결제·파생상품 가격이 다 오라클 데이터에 의존한다. <strong>오라클이 잘못된 가격을 주면 → 부당 청산 발생</strong> → 실제로 종종 사고가 난다.</p>
<hr>
<h2 id="4-토큰의-종류--다-같은-코인이-아니다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-토큰의-종류--다-같은-코인이-아니다">#</a>4. 토큰의 종류 — 다 같은 코인이 아니다</h2>
<p>코인 시장에서 "토큰"이라고 다 같은 게 아니다. 어떤 역할을 하느냐에 따라 분류가 다른데, 이걸 알아야 토큰을 살 때 <strong>"이게 왜 가치가 있는가"</strong> 를 가늠할 수 있다.</p>
<h3 id="유틸리티-토큰-utility-token"><a class="anchor" aria-hidden="true" tabindex="-1" href="#유틸리티-토큰-utility-token">#</a>유틸리티 토큰 (Utility Token)</h3>
<p>특정 서비스를 <strong>사용하기 위한 입장권</strong> 역할의 토큰이다. 그 토큰을 갖고 있어야 그 네트워크의 기능을 쓸 수 있다.</p>
<ul>
<li><strong>FIL</strong> (Filecoin) — 분산 스토리지를 빌릴 때 결제 수단</li>
<li><strong>LINK</strong> (Chainlink) — 오라클 데이터 요청할 때 지불</li>
<li><strong>RNDR</strong> (Render) — GPU 자원 빌릴 때 결제</li>
<li><strong>ETH</strong> — 사실 가장 큰 유틸리티 토큰. 이더리움 위에서 뭘 하든 가스비로 필요</li>
</ul>
<p>가치의 근거가 <strong>"이 네트워크를 쓰려는 수요"</strong> 에 직결된다.</p>
<h3 id="거버넌스-토큰-governance-token"><a class="anchor" aria-hidden="true" tabindex="-1" href="#거버넌스-토큰-governance-token">#</a>거버넌스 토큰 (Governance Token)</h3>
<p><strong>프로토콜의 의사결정에 참여할 권리</strong>를 주는 토큰이다. 1토큰 = 1표 형태로, 안건을 제안하고 투표할 수 있다.</p>
<ul>
<li><strong>UNI</strong> (Uniswap) — 거버넌스 토큰의 시초</li>
<li><strong>AAVE</strong> (Aave) — 대출 프로토콜 운영 의결</li>
<li><strong>COMP</strong> (Compound)</li>
<li><strong>ARB</strong> (Arbitrum) — L2 자체 운영 결정</li>
<li><strong>CRV</strong> (Curve) — 대표적인 veToken 모델 (락업 기간 길수록 의결권 ↑)</li>
</ul>
<p>거버넌스 토큰은 "회사 주식"과 비슷한 면이 있지만, 실제로 의결권 외에 <strong>수익 분배가 약하다</strong>는 게 비판점이다. 그래서 최근엔 수수료 일부를 토큰 홀더에게 돌려주는 모델(Aave, MakerDAO)이 늘고 있다.</p>
<h3 id="페이먼트-토큰-payment-token"><a class="anchor" aria-hidden="true" tabindex="-1" href="#페이먼트-토큰-payment-token">#</a>페이먼트 토큰 (Payment Token)</h3>
<p>순수하게 <strong>결제 수단</strong>으로 쓰이는 토큰. 비트코인이 원조이고, 스테이블코인이 사실상 후계자다.</p>
<ul>
<li><strong>BTC</strong> — 디지털 금 + 결제</li>
<li><strong>USDT, USDC</strong> — 디지털 달러</li>
<li><strong>XRP</strong> — 국가 간 송금 인프라</li>
</ul>
<h3 id="시큐리티-토큰-security-token"><a class="anchor" aria-hidden="true" tabindex="-1" href="#시큐리티-토큰-security-token">#</a>시큐리티 토큰 (Security Token)</h3>
<p><strong>증권형 토큰</strong> — 회사 지분, 채권, 부동산 권리 같은 <strong>법적 자산을 토큰화</strong>한 것. 미국 SEC 같은 금융 당국 규제를 받는다. 일반 거래소엔 거의 안 올라온다(법적으로 못 올림).</p>
<p>RWA 트렌드와 직결되는 개념. BlackRock의 BUIDL이 사실상 시큐리티 토큰 형태.</p>
<h3 id="밈-토큰-meme-token"><a class="anchor" aria-hidden="true" tabindex="-1" href="#밈-토큰-meme-token">#</a>밈 토큰 (Meme Token)</h3>
<p><strong>커뮤니티와 농담, 내러티브로만 가치가 형성</strong>되는 토큰. 펀더멘털이 거의 없거나 의도적으로 만들지 않는다.</p>
<ul>
<li><strong>DOGE, SHIB</strong> — 1세대</li>
<li><strong>PEPE, WIF, BONK</strong> — 2세대</li>
<li><strong>GOAT, FARTCOIN</strong> — AI 시대 밈</li>
</ul>
<p>가격이 단기간에 100배 가는 일이 흔한 만큼, 99% 죽는 일도 흔하다. 짧은 사이클로 돈다.</p>
<h3 id="nft-non-fungible-token"><a class="anchor" aria-hidden="true" tabindex="-1" href="#nft-non-fungible-token">#</a>NFT (Non-Fungible Token)</h3>
<p><strong>대체 불가능한</strong> 토큰. 1 ETH = 1 ETH지만, NFT는 각각이 고유하다(그림, 게임 아이템, 멤버십 등).</p>
<p>ERC-721이 표준. 2021년 PFP(프로필 사진) 광풍 이후 시들했다가, 2024–2026년에는 <strong>티켓·멤버십·게임 아이템</strong> 같은 실용적 용도로 자리 잡고 있다.</p>
<hr>
<h2 id="5-defi-핵심"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-defi-핵심">#</a>5. DeFi 핵심</h2>
<h3 id="dex--amm--lp"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dex--amm--lp">#</a>DEX / AMM / LP</h3>
<ul>
<li><strong>DEX(탈중앙 거래소)</strong>: 회사가 운영하는 게 아니라 스마트 컨트랙트가 자동으로 매매 처리. 유니스왑, 라이덴(Raydium), 이더(Aerodrome) 등.</li>
<li><strong>AMM(자동 시장 조성자)</strong>: DEX의 핵심 알고리즘. <code>x * y = k</code> 같은 수식으로 가격을 자동 결정.</li>
<li><strong>LP(Liquidity Provider, 유동성 공급자)</strong>: 풀에 자기 자산을 넣어주는 사람. 다른 사람이 거래할 때 발생하는 수수료를 받아 간다.</li>
</ul>
<blockquote>
<p><strong>LP는 왜 자기 돈을 풀에 넣어줄까?</strong></p>
<p><strong>거래 수수료의 일부를 지분만큼 가져가기 때문.</strong> 보통 거래액의 0.05–1%가 풀에 쌓이고, 이게 LP들에게 분배된다. 거래량 많은 풀(USDC/ETH 같은)은 연 5–30% 수익이 나기도 한다.</p>
<p>다만 공짜는 아니다. 두 토큰의 가격 비율이 변하면 손실(아래 IL) 위험이 따라온다.</p>
</blockquote>
<h3 id="비영구손실-il-impermanent-loss"><a class="anchor" aria-hidden="true" tabindex="-1" href="#비영구손실-il-impermanent-loss">#</a>비영구손실 (IL, Impermanent Loss)</h3>
<p><strong>LP의 가장 큰 리스크.</strong> 풀에 넣은 두 토큰의 가격 비율이 변하면, 그냥 들고만 있었을 때보다 <strong>금액이 적어지는 현상</strong>이다.</p>
<p>예) ETH/USDC 풀에 절반씩 넣어뒀는데 ETH가 2배 뛰면 → 풀 안에서는 ETH가 자동으로 팔려나간다 → 결국 그냥 ETH 들고 있던 거보다 덜 받게 됨.</p>
<p>수수료 수익이 IL보다 크면 이득이지만, 변동성 큰 알트코인 페어는 <strong>수수료 받아도 손해</strong>인 경우가 흔하다. "Impermanent(일시적)" 손실이라고는 하지만, 풀에서 빼면 그 손실은 <strong>확정</strong>된다.</p>
<h3 id="스테이킹-staking"><a class="anchor" aria-hidden="true" tabindex="-1" href="#스테이킹-staking">#</a>스테이킹 (Staking)</h3>
<p>PoS 체인에서 <strong>자기 코인을 락업</strong>해서 검증자 활동에 동참하고, 그 대가로 <strong>이자(연 3–7% 수준)</strong> 를 받는 구조. 이더리움이 대표적.</p>
<blockquote>
<p><strong>검증자는 왜 코인을 락업하나?</strong></p>
<p>단순히 이자 때문이지만, 그게 다는 아니다. 락업된 코인은 <strong>보증금</strong> 역할을 한다. 부정행위(이중 서명, 다운타임)를 하면 락업한 코인의 일부가 <strong>슬래싱(몰수)</strong> 된다. 즉, "내 돈 걸고 정직하게 검증하겠다"는 약속이 곧 PoS의 보안 모델.</p>
</blockquote>
<h3 id="리퀴드-스테이킹-lst-liquid-staking-token"><a class="anchor" aria-hidden="true" tabindex="-1" href="#리퀴드-스테이킹-lst-liquid-staking-token">#</a>리퀴드 스테이킹 (LST, Liquid Staking Token)</h3>
<p>스테이킹하면 코인이 묶여서 못 쓰는 게 단점이었다. 그래서 <strong>락업한 만큼 거래 가능한 토큰을 대신 발행</strong>해주는 게 리퀴드 스테이킹이다.</p>
<p>ETH를 Lido에 맡기면 <strong>stETH</strong>를 받고, 그 stETH는 자유롭게 다른 DeFi에서 쓸 수 있다. mETH(Mantle), rsETH(Rocket Pool) 등도 같은 개념.</p>
<h3 id="리스테이킹--lrt"><a class="anchor" aria-hidden="true" tabindex="-1" href="#리스테이킹--lrt">#</a>리스테이킹 / LRT</h3>
<p>2024년부터 폭발적으로 커진 영역이다. 한 줄 요약: <strong>이미 스테이킹된 ETH를 한 번 더 빌려줘서 추가 보상을 받는 것.</strong></p>
<p>원래 스테이킹된 ETH는 이더리움 보안에만 쓰였다. 그런데 <strong>EigenLayer</strong>가 이런 아이디어를 냈다 — "이 보안을 다른 프로토콜(롤업, 오라클 등)에 빌려주자. 그 프로토콜이 보안 빌리는 대가로 토큰을 더 줄 거다."</p>
<p>리스테이킹된 자산을 거래 가능한 토큰으로 또 만든 게 <strong>LRT (Liquid Restaking Token)</strong>: ezETH(Renzo), weETH(Ether.fi), rsETH(Kelp) 등.</p>
<p>이자가 이중·삼중으로 쌓이는 구조라 매력적이지만, 리스크도 그만큼 쌓인다는 점은 기억해야 한다.</p>
<p><img src="/images/crypto/glossary-staking.svg" alt="스테이킹 → 리퀴드 스테이킹 → 리스테이킹" loading="lazy" decoding="async"></p>
<h3 id="인텐트-intent-기반-아키텍처"><a class="anchor" aria-hidden="true" tabindex="-1" href="#인텐트-intent-기반-아키텍처">#</a>인텐트 (Intent) 기반 아키텍처</h3>
<p>기존 DEX는 사용자가 <strong>"USDC → ETH로 바꾸겠다"</strong> 라고 직접 명령했다. 인텐트는 사용자가 <strong>의도</strong>만 표현하고, <strong>솔버(Solver)</strong> 라는 외부 주체가 가장 좋은 경로를 찾아서 실행하는 방식이다.</p>
<blockquote>
<p>사용자: "0.5% 슬리피지 안에서 USDC를 ETH로 바꾸고 싶어"
솔버들: 경쟁해서 가장 유리한 경로를 제안 → 가장 좋은 게 채택됨</p>
</blockquote>
<p>CowSwap, UniswapX, 1inch Fusion이 대표적. 2026년 들어 거의 모든 신규 DEX가 인텐트 모델을 채택하고 있다.</p>
<hr>
<h2 id="6-dao와-거버넌스"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-dao와-거버넌스">#</a>6. DAO와 거버넌스</h2>
<h3 id="dao-decentralized-autonomous-organization"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dao-decentralized-autonomous-organization">#</a>DAO (Decentralized Autonomous Organization)</h3>
<p><strong>탈중앙 자율 조직.</strong> 주식회사 같은 전통 조직이 CEO·이사회로 운영된다면, DAO는 <strong>토큰 홀더들의 투표로 운영</strong>되는 조직이다.</p>
<p>운영 방식이 회사보다 훨씬 단순하다.</p>
<ol>
<li>누가 안건(<strong>프로포절</strong>)을 올린다 — "수수료를 0.3%에서 0.25%로 낮추자"</li>
<li>토큰 홀더들이 정해진 기간 안에 투표한다</li>
<li><strong>쿼럼(Quorum, 의결 정족수)</strong> 을 넘기고 찬성이 많으면 통과</li>
<li>통과되면 <strong>스마트 컨트랙트가 자동으로 집행</strong>한다 (사람이 손댈 필요 없음)</li>
</ol>
<p>대표적인 DAO들:</p>
<ul>
<li><strong>Uniswap DAO</strong> — 수수료 정책, 신규 체인 배포 의결</li>
<li><strong>Arbitrum DAO</strong> — L2 트레저리(약 35억 달러 규모) 운영</li>
<li><strong>MakerDAO</strong> (현 Sky) — 스테이블코인 DAI/USDS 운영</li>
<li><strong>Nouns DAO</strong> — 매일 NFT 1개 경매 → 수익으로 펀딩</li>
</ul>
<h3 id="거버넌스에서-자주-보는-단어"><a class="anchor" aria-hidden="true" tabindex="-1" href="#거버넌스에서-자주-보는-단어">#</a>거버넌스에서 자주 보는 단어</h3>
<table>
<thead>
<tr>
<th>용어</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>트레저리(Treasury)</strong></td>
<td>DAO가 보유한 자금 금고. 보통 멀티시그·타임락으로 보호.</td>
</tr>
<tr>
<td><strong>프로포절(Proposal)</strong></td>
<td>안건. 통상 포럼 토론 → 정식 투표 단계로 진행</td>
</tr>
<tr>
<td><strong>쿼럼(Quorum)</strong></td>
<td>의결 정족수. 이 비율 미만 참여면 부결</td>
</tr>
<tr>
<td><strong>타임락(Timelock)</strong></td>
<td>가결돼도 즉시 실행 안 됨. 보통 24–72시간 지연 (긴급 대응 시간)</td>
</tr>
<tr>
<td><strong>델리게이션(Delegation)</strong></td>
<td>내 투표권을 다른 사람한테 위임. 투표하기 귀찮을 때</td>
</tr>
<tr>
<td><strong>veToken</strong></td>
<td>Vote-escrowed 토큰. <strong>락업 기간이 길수록 의결권 ↑</strong> (Curve 모델)</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>DAO 투표에 왜 시간 쓰며 참여하나?</strong></p>
<p>단순히 이상주의가 아니다. <strong>프로토콜이 잘 굴러가야 → 토큰 가치가 유지·상승</strong> → 곧 자기 자산 가치다. 큰 토큰을 들고 있는 펀드·기관은 <strong>풀타임 거버넌스 담당자</strong>를 두고 적극 참여한다. 어떤 DAO는 투표 참여 자체에 보상을 주기도 한다.</p>
</blockquote>
<h3 id="dao의-한계도-알아두자"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dao의-한계도-알아두자">#</a>DAO의 한계도 알아두자</h3>
<p>이상적으로는 모든 토큰 홀더가 평등하지만, 실제로는 <strong>고래(큰 손) 몇 명이 결정권을 좌우</strong>하는 경우가 많다. 투표 참여율이 5% 미만인 DAO도 흔하다. "탈중앙"이라는 명분과 <strong>현실의 운영은 따로 노는 경우</strong>가 많다는 점도 기억해두자.</p>
<hr>
<h2 id="7-시장에서-자주-보는-지표"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7-시장에서-자주-보는-지표">#</a>7. 시장에서 자주 보는 지표</h2>
<table>
<thead>
<tr>
<th>용어</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>TVL</strong></td>
<td>Total Value Locked. 그 프로토콜에 묶인 자산 총액. DeFi 인기도 지표.</td>
</tr>
<tr>
<td><strong>MC / Market Cap</strong></td>
<td>유통 중인 토큰 × 현재 가격</td>
</tr>
<tr>
<td><strong>FDV</strong></td>
<td>Fully Diluted Valuation. 모든 토큰(미배포 포함) × 현재 가격</td>
</tr>
<tr>
<td><strong>MC/FDV 비율</strong></td>
<td>0.3 미만이면 <strong>"미발행 물량 70% 이상이 풀릴 예정"</strong> = 덤핑 리스크</td>
</tr>
<tr>
<td><strong>OI</strong></td>
<td>Open Interest. 청산되지 않은 미결제 선물 계약 총량. 시장 과열 지표.</td>
</tr>
<tr>
<td><strong>Funding Rate</strong></td>
<td>영구 선물에서 롱/숏 균형 맞추려 양쪽이 주고받는 비율. 양수면 롱 우세.</td>
</tr>
<tr>
<td><strong>MEV</strong></td>
<td>Maximal Extractable Value. 블록 생성자가 거래 순서를 조작해서 뜯어가는 이익. 샌드위치 공격이 대표 사례.</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="8-2026년-메가-내러티브"><a class="anchor" aria-hidden="true" tabindex="-1" href="#8-2026년-메가-내러티브">#</a>8. 2026년 메가 내러티브</h2>
<h3 id="rwa-real-world-assets-실물-자산-토큰화"><a class="anchor" aria-hidden="true" tabindex="-1" href="#rwa-real-world-assets-실물-자산-토큰화">#</a>RWA (Real World Assets, 실물 자산 토큰화)</h3>
<p>미국 국채, 부동산, 회사 채권 같은 <strong>현실 자산을 토큰으로 만들어 온체인에서 거래</strong>하는 분야. 2024년에 BlackRock의 <strong>BUIDL 펀드</strong>(이더리움 기반 미국 국채 펀드)가 출시되면서 본격화됐고, 2026년 시점에 온체인 RWA 규모가 100억 달러를 넘었다.</p>
<p>스테이블코인이 "디지털 달러"였다면, RWA는 "디지털 채권·부동산"이다.</p>
<h3 id="depin-decentralized-physical-infrastructure-network"><a class="anchor" aria-hidden="true" tabindex="-1" href="#depin-decentralized-physical-infrastructure-network">#</a>DePIN (Decentralized Physical Infrastructure Network)</h3>
<p><strong>개인이 자기 자원(GPU, 스토리지, 와이파이, 차량 데이터)을 제공하고 토큰으로 보상받는</strong> 네트워크. 클라우드를 AWS 한 회사가 아니라 수많은 사람들이 분산 제공하는 구조다.</p>
<ul>
<li><strong>Helium</strong> — 와이파이/5G 핫스팟 공유</li>
<li><strong>Filecoin</strong> — 분산 스토리지</li>
<li><strong>Render / Akash</strong> — GPU 자원 임대 (AI 학습용)</li>
<li><strong>Hivemapper</strong> — 운전자가 도로 매핑하면 토큰</li>
</ul>
<p>AI 붐과 맞물려 GPU 계열 DePIN이 특히 주목받고 있다.</p>
<h3 id="ai-에이전트-토큰"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ai-에이전트-토큰">#</a>AI 에이전트 토큰</h3>
<p>2024–2025년에 새롭게 등장한 카테고리. <strong>자율 AI가 자기 명의로 트위터 운영하고, 자기 지갑으로 거래도 하고, 자기 토큰도 발행</strong>한다. 인간이 일일이 컨트롤하지 않는다.</p>
<ul>
<li><strong>Truth Terminal / GOAT</strong> — 첫 "밈코인을 만든 AI"로 화제</li>
<li><strong>ai16z</strong> — AI 에이전트 펀드 컨셉</li>
<li><strong>Virtuals Protocol</strong> (Base 위) — 누구나 AI 에이전트 발행 가능한 플랫폼</li>
</ul>
<p>기술적으로는 LLM + 지갑 + 자율 의사결정 루프가 합쳐진 형태. 2026년에는 이 카테고리만 시총 수십억 달러 규모가 됐다.</p>
<h3 id="비트코인-생태계-부활"><a class="anchor" aria-hidden="true" tabindex="-1" href="#비트코인-생태계-부활">#</a>비트코인 생태계 부활</h3>
<p>오랫동안 "BTC는 그냥 디지털 금" 정도였는데, 2023년부터 폭발했다.</p>
<ul>
<li><strong>Ordinals</strong> (2023): 비트코인에 NFT 새기기</li>
<li><strong>BRC-20</strong> (2023): 비트코인 위에 토큰 발행 표준</li>
<li><strong>Runes</strong> (2024): BRC-20보다 효율적인 새 토큰 표준</li>
<li><strong>Bitcoin L2</strong>: Stacks, Babylon, B² Network — 비트코인을 담보로 DeFi</li>
<li><strong>BTCFi</strong>: 비트코인을 그냥 들고만 있지 말고 스테이킹·대출 등에 활용</li>
</ul>
<h3 id="펌프펀-pumpfun--밈코인-슈퍼사이클"><a class="anchor" aria-hidden="true" tabindex="-1" href="#펌프펀-pumpfun--밈코인-슈퍼사이클">#</a>펌프펀 (pump.fun) / 밈코인 슈퍼사이클</h3>
<p>솔라나에 등장한 <strong>누구나 5초 만에 코인을 발행할 수 있는 플랫폼</strong>. 24시간에 수만 개의 밈코인이 만들어지고 99%는 사라진다. 2024–2026 밈코인 슈퍼사이클의 진앙지.</p>
<p>밈코인은 펀더멘털이 없다. 순수하게 <strong>커뮤니티 + 내러티브 + 타이밍</strong>의 게임이라 단기 변동성이 극단적이다. 99%는 본전도 못 회복한다는 점은 기억해두자.</p>
<hr>
<h2 id="9-알아두면-좋은-보안-용어"><a class="anchor" aria-hidden="true" tabindex="-1" href="#9-알아두면-좋은-보안-용어">#</a>9. 알아두면 좋은 보안 용어</h2>
<ul>
<li><strong>러그풀(Rug Pull)</strong> — 개발자가 유동성 들고 도망가는 것. 신생 토큰의 가장 흔한 사기 패턴.</li>
<li><strong>익스플로잇(Exploit)</strong> — 스마트 컨트랙트 취약점을 이용한 해킹.</li>
<li><strong>샌드위치 공격</strong> — 누가 큰 거래 보낼 때, 그 앞뒤로 자기 거래를 끼워서 차익을 뜯는 MEV의 한 종류.</li>
<li><strong>멀티시그(Multi-sig)</strong> — 여러 개인키가 동의해야 거래되는 지갑. 팀·DAO 자금 관리 표준.</li>
<li><strong>MPC 지갑</strong> — 키를 여러 조각으로 나눠 분산 보관. 한 조각만 유출돼도 안전. 거래소·기관용.</li>
<li><strong>슬래싱(Slashing)</strong> — PoS 검증자가 부정행위 시 스테이킹된 자산이 일부 몰수되는 것. 리스테이킹할수록 슬래싱 누적 리스크.</li>
</ul>
<hr>
<h2 id="10-토큰-발행배포-관련-용어"><a class="anchor" aria-hidden="true" tabindex="-1" href="#10-토큰-발행배포-관련-용어">#</a>10. 토큰 발행·배포 관련 용어</h2>
<p>신규 프로젝트 글에서 자주 보이는 단어들.</p>
<ul>
<li><strong>TGE (Token Generation Event)</strong> — 토큰 최초 발행 시점. 이때부터 거래 시작.</li>
<li><strong>베스팅 (Vesting)</strong> — 토큰 락업. 팀·투자자가 받은 토큰을 일정 기간 못 팔게 묶어둠.</li>
<li><strong>클리프 (Cliff)</strong> — 베스팅 시작 전 <strong>완전 잠금 기간</strong>. "12개월 클리프 + 24개월 선형 베스팅" = 1년간 0원, 그 후 2년에 걸쳐 풀림.</li>
<li><strong>언락(Unlock)</strong> — 베스팅 풀리는 시점. 보통 가격 하락 압력 → "<strong>클리프 풀리는 날</strong>"은 매도 주의 시점.</li>
<li><strong>에어드롭(Airdrop)</strong> — 무료 토큰 배포. 보통 초기 사용자·테스터에게.</li>
<li><strong>화이트리스트(Whitelist, WL)</strong> — 사전 등록자. 우선 매수권·에어드롭 자격.</li>
<li><strong>시빌(Sybil)</strong> — 한 사람이 지갑 100개 만들어서 에어드롭 어뷰징하는 행위. 프로젝트는 시빌 필터링으로 잡아낸다.</li>
</ul>
<blockquote>
<p><strong>프로젝트는 왜 토큰을 공짜로 뿌리나?</strong></p>
<p>마케팅비라고 보면 된다.</p>
<p><strong>(1) 사용자 확보</strong> — 에어드롭 노린 유저들이 미리 써보고 데이터 쌓음.</p>
<p><strong>(2) 거버넌스 분산화</strong> — 토큰이 몇 사람에게 몰리면 "탈중앙"이라는 명분이 깨짐.</p>
<p><strong>(3) 입소문</strong> — 에어드롭으로 큰 돈 받은 사용자가 자연스럽게 홍보.</p>
<p>결국 "공짜로 주는 게 광고비보다 싸다"는 계산.</p>
</blockquote>
<ul>
<li><strong>토크노믹스(Tokenomics)</strong> — 그 토큰의 발행량·분배·인플레이션 정책 전체를 통칭.</li>
<li><strong>메인넷(Mainnet) / 테스트넷(Testnet)</strong> — 실제 네트워크 vs 시험 네트워크. 테스트넷 토큰은 가치 없음.</li>
<li><strong>포셋(Faucet)</strong> — 테스트넷 토큰 무료로 받는 곳.</li>
</ul>
<hr>
<h2 id="11-시장에서-자주-쓰는-슬랭"><a class="anchor" aria-hidden="true" tabindex="-1" href="#11-시장에서-자주-쓰는-슬랭">#</a>11. 시장에서 자주 쓰는 슬랭</h2>
<p>코인 트위터·디스코드·텔레그램 보다 보면 한국어 안에 영어 약어가 섞여 나온다.</p>
<table>
<thead>
<tr>
<th>약어</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>HODL</strong></td>
<td>"Hold on for Dear Life" — 팔지 말고 들고 있어. 원래 오타였는데 밈이 됨.</td>
</tr>
<tr>
<td><strong>FOMO</strong></td>
<td>Fear Of Missing Out — 놓칠까 봐 무서워서 사는 심리. 99% 고점에 물림.</td>
</tr>
<tr>
<td><strong>FUD</strong></td>
<td>Fear, Uncertainty, Doubt — 가격 떨어뜨리려고 퍼뜨리는 부정적 정보.</td>
</tr>
<tr>
<td><strong>DYOR</strong></td>
<td>Do Your Own Research — "내 말 믿지 말고 직접 알아봐." 책임 회피용 만능 멘트.</td>
</tr>
<tr>
<td><strong>ATH / ATL</strong></td>
<td>All-Time High / Low — 사상 최고가 / 최저가</td>
</tr>
<tr>
<td><strong>고래(Whale)</strong></td>
<td>한 번에 수십–수백억 단위로 움직이는 큰 손</td>
</tr>
<tr>
<td><strong>에이프 인(Ape in)</strong></td>
<td>분석 없이 바로 매수. "원숭이처럼 들어간다"</td>
</tr>
<tr>
<td><strong>디제너(Degen)</strong></td>
<td>Degenerate. 위험 무시하고 도박처럼 노는 트레이더. 보통 자기를 놀리듯 씀.</td>
</tr>
<tr>
<td><strong>샤딩(Sharding)</strong></td>
<td>(기술 용어) 블록체인을 여러 조각으로 나눠 처리량 증가</td>
</tr>
<tr>
<td><strong>WAGMI / NGMI</strong></td>
<td>We're All Gonna Make It / Not Gonna Make It</td>
</tr>
<tr>
<td><strong>GM / GN</strong></td>
<td>Good Morning / Good Night. 코인 트위터의 인사</td>
</tr>
<tr>
<td><strong>알파(Alpha)</strong></td>
<td>남들 모르는 정보. "알파 공유한다" = 미공개 정보 알려준다</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마무리">#</a>마무리</h2>
<p>용어집은 한 번 읽고 끝낼 게 아니다.</p>
<p>트위터 글 읽다가, 디스코드에서 토론 보다가, 새로운 프로젝트 글 읽다가 <strong>모르는 단어가 튀어나올 때마다 다시 와서 찾아보면 된다.</strong> 그러다 보면 자기도 모르게 익숙해진다.</p>
<p>코인 시장은 1년만 안 봐도 새 단어가 산더미처럼 쌓인다. 2026년 4월 기준 위 용어들이 메인이지만, 6개월 뒤엔 또 새로운 게 메이저로 올라와 있을 거다. 그게 이 시장 속도다.</p>
<p><strong>모르는 단어가 나오면 단어부터 알아두자.</strong> 단어를 안다고 돈을 버는 건 아니지만, 적어도 남들이 무슨 얘기 하는지는 알아듣고 시작할 수 있다.</p>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[블록체인]]></category>
      <category><![CDATA[용어집]]></category>
      <category><![CDATA[DAO]]></category>
      <category><![CDATA[거버넌스토큰]]></category>
      <category><![CDATA[유틸리티토큰]]></category>
      <category><![CDATA[리스테이킹]]></category>
      <category><![CDATA[모듈러]]></category>
      <category><![CDATA[인텐트]]></category>
      <category><![CDATA[AI에이전트]]></category>
      <category><![CDATA[RWA]]></category>
      <category><![CDATA[DePIN]]></category>
      <category><![CDATA[비트코인L2]]></category>
      <category><![CDATA[토크노믹스]]></category>
      <category><![CDATA[2026]]></category>
      <category><![CDATA[초보자]]></category>
    </item>

    <item>
      <title><![CDATA[Dango Exchange 완전 분석 — Layer 1 CLOB DEX와 DNG 에어드랍 전략]]></title>
      <link>https://www.stragos.xyz/posts/dango-exchange-airdrop-guide</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/dango-exchange-airdrop-guide</guid>
      <pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[자체 Layer 1 위에서 온체인 CLOB을 돌리는 Dango Exchange. Perps 거래량과 볼트 예치로 포인트를 쌓고 DNG 에어드랍을 노리는 전략을 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>DeFi에서 DEX를 쓰다 보면 한 번쯤 짜증나는 순간이 온다.</p>
<p>내가 넣은 주문 가격이랑 실제 체결 가격이 다르다. <abbr title="주문 제출 시점과 체결 시점 사이에 발생하는 가격 차이. 유동성이 낮거나 주문 규모가 클수록 심해짐">슬리피지</abbr>다.</p>
<p>또는 뭔가 스왑을 하려는데 트랜잭션이 갑자기 실패하면서 가스비만 날아간다.</p>
<p>더 황당한 건 내 트랜잭션이 블록에 올라가기 직전에 봇이 먼저 끼어들어 가격을 올려놓고 이득을 챙기는 <abbr title="Maximal Extractable Value. 블록 생성자나 봇이 트랜잭션 순서를 조작해서 이익을 빼가는 것. 일반 사용자는 불리한 가격에 체결되는 피해를 입음">MEV</abbr>다.</p>
<p>기존 <abbr title="Automated Market Maker. 유동성 풀에 자산을 예치하고 수학 공식으로 가격을 결정하는 방식. Uniswap, Curve가 대표적">AMM</abbr> DEX 구조에서 이 문제들은 구조적이다.</p>
<p>근본적으로 고치기 어렵다.</p>
<p>Dango를 처음 봤을 때 "또 DEX냐" 했는데, 들여다보니 접근 자체가 달랐다. AMM을 개선한 게 아니라 <strong>아예 버리고</strong> <abbr title="Central Limit Order Book. 매수/매도 주문을 가격순으로 정렬해 매칭하는 방식. 주식거래소가 이 방식을 씀">CLOB</abbr>으로 갔다. 그것도 온체인에서, 자체 Layer 1을 만들어서.</p>
<h2 id="dango가-뭔지부터"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dango가-뭔지부터">#</a>Dango가 뭔지부터</h2>
<p>한 줄 요약: <strong>자체 Layer 1 위에서 완전한 온체인 CLOB을 돌리는 DeFi 슈퍼앱이다.</strong></p>
<p>기존 DEX와 비교해보면 차이가 선명하다.</p>
<table>
<thead>
<tr>
<th></th>
<th>기존 AMM DEX</th>
<th>Dango</th>
</tr>
</thead>
<tbody>
<tr>
<td>가격 결정</td>
<td>수학 공식 (x*y=k)</td>
<td>매수/매도 주문 매칭 (CLOB)</td>
</tr>
<tr>
<td>지정가 주문</td>
<td>불가 또는 불완전</td>
<td>완전 지원</td>
</tr>
<tr>
<td>슬리피지</td>
<td>구조적으로 발생</td>
<td>최소화</td>
</tr>
<tr>
<td>MEV</td>
<td>취약</td>
<td>주기적 배치 경매로 저항</td>
</tr>
<tr>
<td>인프라</td>
<td>다른 체인 위에 올라탐</td>
<td>자체 Layer 1</td>
</tr>
</tbody>
</table>
<p>CEX에서 당연하게 쓰던 기능들 — 지정가 주문, 정확한 체결 가격, 깊은 호가창 — 을 DEX에서 구현하겠다는 거다.</p>
<h2 id="메인넷-일정--지금-어디까지-왔나"><a class="anchor" aria-hidden="true" tabindex="-1" href="#메인넷-일정--지금-어디까지-왔나">#</a>메인넷 일정 — 지금 어디까지 왔나</h2>
<p>Dango는 2025년부터 단계적으로 진행해왔다.</p>
<ul>
<li><strong>2025년 Q1</strong>: Testnet 1 공개. 온체인 CLOB 스팟 거래(ETH/BTC/SOL), 패스키 계정 시스템 테스트</li>
<li><strong>2025년 중반</strong>: Testnet 1.5 → 알림 시스템, 사람이 읽기 좋은 유저네임, 블록 탐색기 추가</li>
<li><strong>2025년 Q3~Q4</strong>: Testnet 2 → 볼트, 대출, 크로스체인 브릿지(Hyperlane), 고급 주문 타입 추가</li>
<li><strong>2025년 5월</strong>: Galxe Starboard 참여. 테스트넷 OAT 배포 시작</li>
<li><strong>2026년 1월</strong>: <strong>메인넷 알파 공개</strong>. ETH/USDC 스팟 거래 시작. 실험적 운영 단계</li>
<li><strong>2026년 3월</strong>: <strong>Perps(퍼페추얼) 출시</strong>. 온체인 CLOB 방식의 퍼프 거래 지원</li>
<li><strong>2026년 3월 31일</strong>: 포인트 파밍 캠페인 시작 발표</li>
<li><strong>2026년 4월 13일</strong>: 포인트/배지 파밍 공식 시작 예정 → <strong>당일 퍼프 익스플로잇 발생</strong>, 체인 일시 중단</li>
<li><strong>현재</strong>: 익스플로잇 해결, 재개 완료 (불안정 구간)</li>
</ul>
<p>포인트 캠페인 시작일에 하필 익스플로잇이 터진 거라 타이밍이 좀 묘하다. 이 부분은 뒤에서 따로 다룬다.</p>
<h2 id="grug-실행-환경--이게-왜-중요한가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#grug-실행-환경--이게-왜-중요한가">#</a>Grug 실행 환경 — 이게 왜 중요한가</h2>
<p>Dango는 <strong>Grug</strong>라는 자체 실행 환경 위에 만들어졌다. EVM(이더리움 가상머신)과는 다른 접근이다.</p>
<p>체인 스펙을 보면 생각보다 퍼포먼스가 빠르다.</p>
<ul>
<li>배치 경매 주기: <strong>0.2~0.5초</strong> (주문 매칭이 이 주기로 일어남. 블록타임과는 별개)</li>
<li>합의 방식: <strong>CometBFT</strong>(구 Tendermint) 기반, 약 20개 <abbr title="검증자가 직접 신원을 밝히고 권한을 부여받는 방식. PoS처럼 토큰 스테이킹이 아닌 신뢰 기반 운영">PoA 검증자</abbr></li>
<li>즉시 완결성 (파이널리티)</li>
<li>검증자 선발: Left Curve Foundation(LCF)이 비용·평판·운영 품질 기준으로 직접 선정</li>
</ul>
<p>Grug가 가능하게 하는 핵심 기능들이다.</p>
<p><strong>마진 계정 (Margin Account)</strong></p>
<p>사용자는 하나의 계정에 자산을 넣고, 그걸 담보로 여러 활동을 동시에 할 수 있다. 스팟 거래, <abbr title="영구 선물. 만기가 없는 선물 계약. 레버리지로 자산 가격에 베팅할 수 있음">퍼페추얼</abbr> 거래, 대출, 수익 파밍을 <strong>하나의 포지션</strong>에서 관리한다.</p>
<p>기존 DeFi처럼 "이 프로토콜엔 ETH 넣고, 저 프로토콜엔 USDC 넣고, 또 저기엔..." 이런 자본 분산이 없다. 스팟에서 쓰던 담보를 그대로 퍼프에서도 쓸 수 있다.</p>
<p><strong>지갑리스 계정 시스템</strong></p>
<p>이게 좀 신기하다. Dango에 가입할 때 시드 프레이즈를 따로 발급받지 않는다.</p>
<p>대신 트위터 계정 만들듯이 <strong>유저네임을 고른다</strong>. 예: <code>@larry</code>. 이 유저네임이 온체인 신원이 되고, 주소 대신 유저네임으로 송금·거래 내역 조회가 가능하다.</p>
<p>인증은 <abbr title="FIDO2 표준 기반. 기기의 보안 영역(Secure Enclave)에 암호키를 저장하고 생체인증으로 서명하는 방식. 개인키가 기기 밖으로 절대 나오지 않음">Passkey</abbr>로 처리된다. Touch ID, Face ID, Windows Hello 등 기기 생체인증을 그대로 쓴다. iCloud·Google 계정으로 자동 동기화되어 기기를 바꿔도 그대로 쓸 수 있다.</p>
<p>MetaMask 같은 브라우저 확장 지갑이 필요 없다. DeFi를 처음 접하는 사람도 진입 장벽이 크게 낮아진다.</p>
<p><strong>서브계정</strong></p>
<p>마스터 계정 아래 여러 개의 서브계정을 만들 수 있다. 전략별 리스크 분리, API·봇 자동화, 팀 트레이딩 시 권한 분리 등에 활용한다.</p>
<p><strong>주기적 배치 경매 (Periodic Batch Auction)</strong></p>
<p>주문을 실시간으로 즉시 처리하는 대신, 짧은 주기로 모아서 한 번에 처리한다.</p>
<p>봇이 트랜잭션 순서를 조작할 틈이 없다. MEV 저항성이 구조적으로 생긴다.</p>
<p><strong>가스비를 USDC로 납부</strong></p>
<p>트랜잭션 수수료를 DNG 토큰이 아닌 <strong>USDC로 낸다</strong>. 가스비를 내려고 따로 토큰을 준비할 필요가 없다.</p>
<p>이 수수료는 공개 시장에서 DNG를 매입해 소각(burn)하는 데 쓰인다. DNG의 공급이 줄어드는 디플레이션 구조다.</p>
<p><strong>크로스체인 브릿지</strong></p>
<p>체인 간 자산 이동은 <strong>Hyperlane</strong> 프로토콜로 처리한다. Grug 실행 환경에 직접 통합되어 있다.</p>
<h2 id="팀과-자금--믿을-수-있는-프로젝트인가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#팀과-자금--믿을-수-있는-프로젝트인가">#</a>팀과 자금 — 믿을 수 있는 프로젝트인가</h2>
<p>새 DeFi 프로젝트를 볼 때마다 "러그풀 아니야?" 먼저 확인하는 습관이 생겼다. Dango에서 확인한 것들이다.</p>
<p><strong>개발사</strong></p>
<p><strong>Left Curve Software</strong> 가 개발을 맡고 있다. 소규모 기술 팀이다.</p>
<p>창업자는 <strong>larry0x</strong> — Cosmos 생태계 베테랑으로, Mars Protocol 개발에도 참여했던 인물이다. 익명이지만 DeFi 개발자로서의 트랙 레코드가 있다.</p>
<p>프로토콜 관리 및 검증자 선발은 <strong>Left Curve Foundation(LCF)</strong> 이 담당한다.</p>
<p><strong>자금 조달</strong></p>
<ul>
<li><strong>2024년 11월</strong> 시드 라운드: <strong>360만 달러</strong></li>
<li>리드 투자자: <strong>Hack VC</strong>, <strong>Lemniscap</strong></li>
<li>참여: Delphi Labs, Cherry Crypto, Interop, Public Works</li>
</ul>
<p>Hack VC는 Sui, Aptos, Solana 생태계 투자로 알려진 곳이다.</p>
<p>Delphi Labs는 DeFi 분석으로 업계에서 신뢰받는 리서치+투자사다.</p>
<p>자금 규모는 크지 않지만 투자자 라인업이 검증의 역할을 한다.</p>
<p><strong>보안 감사</strong></p>
<p>출시 전까지 총 5건의 외부 감사를 완료했다.</p>
<table>
<thead>
<tr>
<th>감사 기관</th>
<th>일자</th>
<th>범위</th>
</tr>
</thead>
<tbody>
<tr>
<td>Sherlock</td>
<td>2025-09</td>
<td>Exchange 전체</td>
</tr>
<tr>
<td>Zellic</td>
<td>2025-04</td>
<td>Hyperlane 브릿지 통합</td>
</tr>
<tr>
<td>Zellic</td>
<td>2025-04</td>
<td>계정·인증 시스템</td>
</tr>
<tr>
<td>Zellic</td>
<td>2024-10</td>
<td>JMT(Jellyfish Merkle Tree) 구현</td>
</tr>
<tr>
<td>Informal Systems</td>
<td>2024 Q4</td>
<td>JMT 형식 명세(Quint)</td>
</tr>
</tbody>
</table>
<p>Sherlock 콘테스트는 외부 보안 연구자들이 버그를 찾는 방식이라 단순 감사보다 범위가 넓다. 보안에 상당히 투자한 편이다.</p>
<h2 id="4월-13일-익스플로잇--포인트-캠페인-당일에-생긴-일"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4월-13일-익스플로잇--포인트-캠페인-당일에-생긴-일">#</a>4월 13일 익스플로잇 — 포인트 캠페인 당일에 생긴 일</h2>
<p>포인트 파밍 전략을 보기 전에 이걸 먼저 짚고 넘어가야 한다.</p>
<p><strong>2026년 4월 13일</strong>, 포인트/배지 파밍이 공식 시작되는 그날, 퍼프 컨트랙트에서 익스플로잇이 발생했다.</p>
<p><strong>어떤 버그였나</strong></p>
<p>퍼프 보험 펀드에는 누구나 자금을 기부(donate)할 수 있는 함수가 있었다. 문제는 이 함수가 <strong>기부 금액이 양수인지 검증하지 않았다</strong>.</p>
<p>공격자는 음수 금액을 넣어서 돈이 들어가는 게 아니라 나오게 만들었다. 결과적으로 <strong>$1.9M USDC</strong>가 퍼프 컨트랙트에서 빠져나갔다.</p>
<p><strong>어떻게 됐나</strong></p>
<ul>
<li>공격자는 <strong>$410K를 이더리움으로 브릿지</strong>했고, 나머지 <strong>$149만은 Dango 체인 내에 잔류</strong>했다</li>
<li>Dango 팀이 체인을 즉시 중단하고 SEAL-911 보안 팀을 개입시켰다</li>
<li>Circle, 주요 거래소에 해당 지갑 주소를 통보했다</li>
<li>공격자에게 버그 바운티를 제안했고, 공격자는 <strong>전액 반환 후 버그 바운티를 수령</strong>했다</li>
</ul>
<p>결론: 유저 자산 피해 없음. 자금 전액 회수.</p>
<p>단, 이 사건으로 <strong>포인트 프로그램은 일시 중단</strong>됐다. 취약한 기부 로직은 제거됐고, 추가 보안 검토 후 재개 예정이다. 퍼프 컨트랙트 자체의 주문 매칭, PnL 정산, 청산 로직은 이번 버그와 무관했다는 게 팀 입장이다.</p>
<h2 id="dng-토큰--아직-미발행-이게-기회"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dng-토큰--아직-미발행-이게-기회">#</a>DNG 토큰 — 아직 미발행, 이게 기회</h2>
<p>DNG는 아직 공개 시장에 상장되지 않은 미발행 토큰이다. TGE는 <strong>2026년 Q4(4분기)</strong> 로 예정되어 있다.</p>
<p><strong>총 공급량: 미정 (업데이트됨)</strong></p>
<p>초기에는 30,000개로 발표됐었다. Yearn Finance 런칭 당시의 공급량(30,000개)을 오마주한 제안이었는데, 파운더 larry0x가 직접 "커뮤니티 대부분이 레퍼런스를 이해하지 못해 혼란이 생겼다"며 이 계획을 철회하고 보다 일반적인 공급량으로 출시하겠다고 밝혔다.</p>
<p>구체적인 공급량과 배분 내역은 TGE 전 공식 발표를 통해 확인해야 한다.</p>
<p><strong>DNG 토큰의 역할 (변경 없음)</strong></p>
<p><strong>DNG 토큰의 역할</strong></p>
<ul>
<li>가스비 결제용이 아님 (가스는 USDC로 냄)</li>
<li>플랫폼 수수료 전액을 DNG 매입·소각에 사용</li>
<li>거래량이 늘수록 소각이 늘어나는 디플레이션 구조</li>
</ul>
<p>포인트 → DNG 전환 자체는 공식 방향이지만, <strong>포인트 1점당 DNG 몇 개</strong>인지는 시즌 종료 전까지 확정되지 않는다.</p>
<p>참여자가 많아질수록 같은 포인트로 받는 DNG가 줄어드는 구조다.</p>
<h2 id="에어드랍-전략--포인트-파밍-완전-분석"><a class="anchor" aria-hidden="true" tabindex="-1" href="#에어드랍-전략--포인트-파밍-완전-분석">#</a>에어드랍 전략 — 포인트 파밍 완전 분석</h2>
<p>익스플로잇 이후 재개된 상태다. 다만 아직 살짝 불안정한 구간이라 모두가 참여하는 분위기는 아니다. 할 사람은 하고 있고, 안 하는 사람은 좀 더 지켜보는 중이다.</p>
<p>재개된 지금, 매주 100만 포인트가 배분되는 구조로 돌아왔다.</p>
<p>배분 구조는 이렇다.</p>
<p><img src="/images/crypto/dango-points-dist.svg" alt="주당 포인트 배분 구조" loading="lazy" decoding="async"></p>
<p>이 100만 포인트를 해당 주에 활동한 모든 참여자가 나눠 갖는다.</p>
<p><strong>내 점유율이 높을수록 더 많이 받는다.</strong> 절대량이 아니라 상대 비율이다.</p>
<h3 id="oat-부스트--테스트넷-참여자-우대"><a class="anchor" aria-hidden="true" tabindex="-1" href="#oat-부스트--테스트넷-참여자-우대">#</a>OAT 부스트 — 테스트넷 참여자 우대</h3>
<p><strong>Galxe OAT (On-Chain Achievement Token)</strong> 를 보유하면 포인트에 배수가 붙는다.</p>
<blockquote>
<p>OAT 1개당 +100% 부스트, 최대 4개까지 = 최대 +400% (4배)</p>
</blockquote>
<table>
<thead>
<tr>
<th>보유 OAT 수</th>
<th>추가 부스트</th>
</tr>
</thead>
<tbody>
<tr>
<td>1개</td>
<td>+1배</td>
</tr>
<tr>
<td>2개</td>
<td>+2배</td>
</tr>
<tr>
<td>3개</td>
<td>+3배</td>
</tr>
<tr>
<td>4개 이상</td>
<td><strong>+4배</strong> (최대)</td>
</tr>
</tbody>
</table>
<p>OAT는 테스트넷 기간에 Galxe 퀘스트를 통해 배포됐다. Testnet Trader OAT, Last Hurrah OAT 등 여러 종류가 있었고, <strong>현재는 모든 Galxe 캠페인이 종료된 상태</strong>다. 신규로 OAT를 받는 건 불가능하다.</p>
<p>OAT 부스트는 이미 OAT를 보유한 사람에게만 적용된다. 테스트넷부터 참여했다면 그게 지금 실질적인 경쟁 우위다.</p>
<p>단, <strong>OAT 부스트는 포인트 캠페인 시작 기준 4주 한정</strong>이다. 이미 보유 중이라면 재개 직후 EVM 지갑을 Dango 계정에 연동해서 부스트를 활성화해야 한다.</p>
<h3 id="전략-1-perps-거래--포인트-파밍의-메인-트랙-75만주"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-1-perps-거래--포인트-파밍의-메인-트랙-75만주">#</a>전략 1. Perps 거래 — 포인트 파밍의 메인 트랙 (75만/주)</h3>
<p>전체 포인트의 75%가 Perps 트레이더에게 간다. 포인트를 가장 많이 쌓으려면 이걸 해야 한다.</p>
<p>Dango 메인넷에서 ETH, BTC, SOL 등 자산의 퍼페추얼 선물을 거래한다.</p>
<p>레버리지를 써서 롱/숏 포지션을 잡는 방식이다.</p>
<p><strong>포인트 배분 원리</strong></p>
<p>75만 포인트를 해당 주에 거래한 모든 Perps 트레이더가 <strong>거래량 비중대로</strong> 나눠 갖는다.</p>
<p>내 거래량 / 전체 거래량 = 내 포인트 점유율.</p>
<p><strong>거래 수수료 구조 (14일 롤링 거래량 기준)</strong></p>
<table>
<thead>
<tr>
<th>거래량 구간</th>
<th>Maker 수수료</th>
<th>Taker 수수료</th>
</tr>
</thead>
<tbody>
<tr>
<td>$0 ~ $100k</td>
<td>0.010%</td>
<td>0.038%</td>
</tr>
<tr>
<td>$100k ~ $1M</td>
<td>0.008%</td>
<td>0.032%</td>
</tr>
<tr>
<td>$1M ~ $10M</td>
<td>0.006%</td>
<td>0.026%</td>
</tr>
<tr>
<td>$10M ~ $50M</td>
<td>0.004%</td>
<td>0.020%</td>
</tr>
<tr>
<td>$50M ~ $200M</td>
<td>0.002%</td>
<td>0.016%</td>
</tr>
<tr>
<td>$200M+</td>
<td>0.000%</td>
<td>0.014%</td>
</tr>
</tbody>
</table>
<p>거래량이 쌓일수록 수수료가 낮아지는 구조다. Maker 주문(지정가)이 항상 Taker보다 저렴하다.</p>
<p><strong>OAT 4개 보유 시 효과</strong></p>
<p>같은 거래량으로 OAT 없는 사람 대비 <strong>4배</strong> 포인트를 받는다.</p>
<p>경쟁자가 1만 달러를 거래할 때 나는 같은 금액으로 4만 달러어치 포인트를 챙기는 셈이다.</p>
<p><strong>주의할 점</strong></p>
<p>Perps는 레버리지 상품이다. 방향이 틀리면 손실이 난다.</p>
<p>포인트 파밍 목적으로 무작정 포지션을 키웠다가 청산당하면 포인트는 의미가 없다.</p>
<p>작은 레버리지로, 또는 롱/숏을 동시에 열어서 <abbr title="가격 방향에 무관하게 손익이 발생하지 않는 포지션 구성. 롱과 숏을 같은 크기로 동시에 보유하는 방식">델타 중립</abbr> 포지션을 유지하는 방법을 쓰는 파머들이 있다. 다만 이 경우 <abbr title="Funding Rate. 퍼페추얼 선물에서 롱과 숏 포지션 간의 균형을 맞추기 위해 주기적으로 지급하거나 수취하는 비용">펀딩비</abbr> 비용이 발생한다.</p>
<h3 id="전략-2-볼트-예치--안정형-포인트-파밍-25만주"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-2-볼트-예치--안정형-포인트-파밍-25만주">#</a>전략 2. 볼트 예치 — 안정형 포인트 파밍 (25만/주)</h3>
<p>Dango 볼트에 자산을 예치하면 볼트 예치자 풀(주당 25만 포인트)에서 내 점유율만큼 받는다.</p>
<p><strong>왜 볼트인가</strong></p>
<ul>
<li>직접 거래 없이 포인트를 받을 수 있다</li>
<li>볼트는 패시브 <abbr title="마켓 메이킹. 매수/매도 양쪽 주문을 상시 제시해서 시장 유동성을 공급하는 역할. 스프레드 수익을 받음">마켓 메이킹</abbr> 역할을 한다</li>
<li>CLOB의 유동성을 높이는 데 기여하고 그 보상을 받는 구조</li>
</ul>
<p>전체 포인트의 25%만 배분된다. 같은 자금을 넣어도 Perps 거래자 대비 포인트 잠재량이 낮다.</p>
<p>단, 청산 리스크가 없고 단순히 예치만 하면 되기 때문에 거래를 잘 모르는 사람에겐 현실적인 선택이다.</p>
<h3 id="전략-3-루트박스--nft-획득-경로"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-3-루트박스--nft-획득-경로">#</a>전략 3. 루트박스 — NFT 획득 경로</h3>
<p>포인트와 별개로, 거래량을 쌓으면 <strong>루트박스</strong>를 받는다.</p>
<table>
<thead>
<tr>
<th>박스 등급</th>
<th>필요 거래량</th>
</tr>
</thead>
<tbody>
<tr>
<td>🥉 브론즈</td>
<td>$25,000마다 1개</td>
</tr>
<tr>
<td>🥈 실버</td>
<td>$100,000마다 1개</td>
</tr>
<tr>
<td>🥇 골드</td>
<td>$250,000마다 1개</td>
</tr>
<tr>
<td>💎 크리스탈</td>
<td>$500,000마다 1개</td>
</tr>
</tbody>
</table>
<p>루트박스를 열면 <strong>NFT</strong> 1개가 나온다. 희귀도는 Common부터 Mythic까지 랜덤이다.</p>
<p>각 NFT에는 <strong>숨겨진 포인트 가치</strong>가 있고, 포인트 프로그램이 종료될 때 공개된다. 희귀도가 높을수록 더 많은 포인트로 환산될 가능성이 높다.</p>
<p>볼트 예치만으로는 루트박스가 쌓이지 않는다. 거래량이 있어야 한다.</p>
<h3 id="전략-4-리그-시스템--랭킹-경쟁"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-4-리그-시스템--랭킹-경쟁">#</a>전략 4. 리그 시스템 — 랭킹 경쟁</h3>
<p>포인트 보유량을 기준으로 매주 순위가 갱신되며, 7개 리그로 분류된다.</p>
<table>
<thead>
<tr>
<th>리그</th>
<th>구간</th>
</tr>
</thead>
<tbody>
<tr>
<td>🪵 Wood</td>
<td>하위 30%</td>
</tr>
<tr>
<td>⚙️ Iron</td>
<td>30~55% 구간</td>
</tr>
<tr>
<td>🥇 Gold</td>
<td>55~73% 구간</td>
</tr>
<tr>
<td>🔷 Platinum</td>
<td>73~85% 구간</td>
</tr>
<tr>
<td>💎 Diamond</td>
<td>85~93% 구간</td>
</tr>
<tr>
<td>🏆 Master</td>
<td>93~98% 구간</td>
</tr>
<tr>
<td>👑 Grandmaster</td>
<td>상위 2%</td>
</tr>
</tbody>
</table>
<p>공식 문서 기준으로 <strong>리그 자체에는 별도 보상이 없다</strong> — "순전히 재미용"이라고 명시되어 있다. 포인트 순위가 에어드랍 배분에 직접 영향을 주는 건 아니다.</p>
<h3 id="전략-5-추천인referral--패시브-포인트-수익"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-5-추천인referral--패시브-포인트-수익">#</a>전략 5. 추천인(Referral) — 패시브 포인트 수익</h3>
<p><strong>2계층 추천인 구조</strong>가 있다.</p>
<ul>
<li><strong>직접 추천</strong>: 내가 초대한 사람이 포인트 5점을 벌면 나도 <strong>1점</strong> 추가 획득 (20%)</li>
<li><strong>간접 추천</strong>: 내 추천인의 추천인이 포인트 20점을 벌면 나도 <strong>1점</strong> 추가 획득 (5%)</li>
</ul>
<p>직접 거래 없이 네트워크 효과로 포인트가 쌓인다. 지인 중 Perps를 활발히 거래하는 사람에게 추천 링크를 전달하면 꾸준한 패시브 포인트 수입이 생긴다.</p>
<h3 id="전략-6-oat-연동--이미-있다면-지금-바로"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-6-oat-연동--이미-있다면-지금-바로">#</a>전략 6. OAT 연동 — 이미 있다면 지금 바로</h3>
<p>Galxe OAT 캠페인은 이미 종료됐다. 지금 새로 받을 수 없다.</p>
<p>테스트넷 때 Dango Galxe 퀘스트에 참여했다면 지갑에 OAT가 남아있을 가능성이 있다. 확인 방법:</p>
<ul>
<li>Galxe 앱에서 내 지갑 연결 후 보유 OAT 확인</li>
<li>Dango 계정 설정에서 해당 EVM 지갑 연동 → 자동으로 부스트 적용</li>
</ul>
<p>OAT 부스트 유효기간(4주)이 지나기 전에 연동하지 않으면 그냥 날아간다. 보유 중이라면 재개 직후 바로 확인해야 한다.</p>
<h3 id="전략-7-포트폴리오-구성--현실적-접근"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-7-포트폴리오-구성--현실적-접근">#</a>전략 7. 포트폴리오 구성 — 현실적 접근</h3>
<p>자금 규모별 현실적인 전략을 정리하면 이렇다.</p>
<p><strong>소액 참여자 ($100~$500)</strong></p>
<p>Perps 거래 위주로 가되, 레버리지를 낮게 유지한다. 볼트에 일부 예치.</p>
<p><strong>중간 참여자 ($500~$5,000)</strong></p>
<p>Perps 거래 + 볼트 예치 병행. OAT가 있으면 연동부터.</p>
<p>OAT 4개 × Perps 거래량 = 같은 자금으로 4배 포인트. 볼트엔 일부 예치해서 안전망 확보.</p>
<p><strong>적극적 참여자 ($5,000+)</strong></p>
<p>OAT 풀 부스트 + Perps 거래량 최대화. 루트박스 골드/크리스탈 등급 목표. 델타 중립 전략 고려 (펀딩비 비용 계산 필요).</p>
<h2 id="락드랍--포인트-파밍과는-별개의-dng-획득-경로"><a class="anchor" aria-hidden="true" tabindex="-1" href="#락드랍--포인트-파밍과는-별개의-dng-획득-경로">#</a>락드랍 — 포인트 파밍과는 별개의 DNG 획득 경로</h2>
<p>포인트 파밍 외에 DNG를 받을 수 있는 또 다른 공식 경로가 있다. <strong>락드랍(Lockdrop)</strong> 이다.</p>
<p>TGE 시점에 Dango의 세 가지 풀 중 하나에 자산을 잠그면 DNG를 받는 구조다.</p>
<p><strong>락업 대상 풀</strong></p>
<ul>
<li><abbr title="Automated Market Maker. 유동성 풀 방식의 거래 지원">AMM</abbr> 풀</li>
<li>마진 풀</li>
<li>퍼프 상대방 풀 (Perp Counterparty Pool)</li>
</ul>
<p><strong>핵심: 락업 기간이 길수록 비선형적으로 더 많이 받는다</strong></p>
<p>단순히 2배 기간 = 2배 토큰이 아니다.</p>
<p>락업 기간이 길수록 받는 DNG 양이 <strong>가속적으로 늘어난다</strong>. 짧게 잠그면 조금, 길게 잠그면 훨씬 많이. 장기 참여자를 우대하는 구조다.</p>
<p>포인트 파밍은 지금 당장 시작할 수 있고, 매주 거래량과 예치를 통해 꾸준히 포인트를 쌓는 방식이다.</p>
<p>락드랍은 TGE 시점에 한 번에 자산을 잠그는 별도 이벤트다. 둘 다 DNG를 받는 경로지만, 락드랍은 아직 시작되지 않았다.</p>
<p>TGE 날짜와 락드랍 세부 조건이 공식 발표되면 그때 본격적으로 전략을 짜야 한다. 지금은 포인트 파밍을 하면서 락드랍 공지를 놓치지 않는 게 맞다.</p>
<h2 id="리스크--이것도-반드시-읽어야-한다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#리스크--이것도-반드시-읽어야-한다">#</a>리스크 — 이것도 반드시 읽어야 한다</h2>
<h3 id="1-메인넷-알파--실제-익스플로잇-경험"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-메인넷-알파--실제-익스플로잇-경험">#</a>1. 메인넷 알파 + 실제 익스플로잇 경험</h3>
<p>지금 Dango는 <strong>알파</strong> 상태다. 그리고 이번에 실제로 익스플로잇이 터졌다.</p>
<p>다행히 전액 회수됐고 유저 피해는 없었다. 지금은 재개된 상태지만 아직 완전히 안정됐다고 보기는 어렵다. 알파 체인이라는 전제를 잊지 말고, 잃어도 괜찮은 금액으로 참여하는 게 맞다.</p>
<h3 id="2-perps-거래-청산-리스크"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-perps-거래-청산-리스크">#</a>2. Perps 거래 청산 리스크</h3>
<p>포인트 파밍 목적으로 Perps 포지션을 키우다가 시장이 반대로 움직이면 청산된다.</p>
<p>포인트는 쌓였을지 몰라도 원금이 사라진다. DNG 에어드랍 가치가 원금 손실을 보상할 것이란 보장은 없다.</p>
<h3 id="3-dng-토큰-가치-불확실성"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-dng-토큰-가치-불확실성">#</a>3. DNG 토큰 가치 불확실성</h3>
<p>현재 DNG는 시장에서 가격이 형성되지 않았다. 에어드랍을 받아도 실제 토큰 가치가 기대에 못 미칠 수 있다.</p>
<p>TGE 이후 초기 물량이 시장에 쏟아지면 가격이 급락하는 경우가 허다하다.</p>
<h3 id="4-포인트-전환-비율-불확실성"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-포인트-전환-비율-불확실성">#</a>4. 포인트 전환 비율 불확실성</h3>
<p>포인트를 쌓기 위해 실제 자산을 쓰고 있다 (거래 수수료, 펀딩비 등).</p>
<p>시즌 종료 시점에 총 포인트 합산에 따라 전환 비율이 결정된다. 참여자가 많아질수록 내 포인트 가치가 희석된다.</p>
<h3 id="5-재개-후-불안정-구간"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-재개-후-불안정-구간">#</a>5. 재개 후 불안정 구간</h3>
<p>지금은 재개됐지만 여전히 불안정한 구간이다.</p>
<p>할 사람은 하고 있고, 지켜보는 사람도 있다. 어느 쪽이 맞다고 할 수 없다. 본인이 감수할 수 있는 금액 안에서 판단하면 된다.</p>
<h3 id="6-규제-리스크"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-규제-리스크">#</a>6. 규제 리스크</h3>
<p>레버리지 DEX는 규제 기관이 관심을 가질 여지가 있다. 특히 특정 국가에서의 접근이 제한될 수 있다.</p>
<h2 id="참여-방법--단계별-정리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#참여-방법--단계별-정리">#</a>참여 방법 — 단계별 정리</h2>
<ol>
<li><strong>dango.exchange</strong> 접속</li>
<li>계정 생성 (유저네임 선택, Passkey 또는 지갑 연결)</li>
<li>USDC 또는 ETH를 브릿지해서 입금 (이더리움, Base, Arbitrum, Polygon 지원) — <strong>최소 $10 이상 입금해야 계정이 활성화된다</strong></li>
<li>테스트넷 OAT 보유 여부 확인 (Galxe 앱에서 내 지갑 확인)</li>
<li>OAT가 있으면 Dango 계정에 EVM 지갑 연동 → 부스트 활성화 (4주 한정)</li>
<li>Perps 거래 시작 (낮은 레버리지부터, 현재 재개 후 불안정 구간임을 감안)</li>
<li>볼트 예치 (일부 자금)</li>
<li>주간 거래량 목표 설정해서 루트박스 쌓기</li>
</ol>
<p>정리하면 이렇다.</p>
<p>AMM의 구조적 한계를 Layer 1 + 온체인 CLOB으로 풀려는 시도 자체는 DeFi에서 오랫동안 이야기된 방향이었다. 거기에 지갑리스 계정, 크로스콜래터럴 마진, MEV 저항성을 하나의 앱에 묶었다.</p>
<p>Hack VC + Delphi Ventures가 투자한 프로젝트고, 메인넷이 돌고 있다.</p>
<p>익스플로잇이 터졌다. 전액 회수됐다. 팀 대응은 나쁘지 않았다.</p>
<p>지금은 재개됐고, 살짝 불안정한 상태에서 할 사람들은 이미 하고 있다. 초기라 경쟁이 적은 구간이기도 하다.</p>
<p>불안하면 더 지켜봐도 된다. 들어가기로 했다면 OAT 연동부터 확인하고, 낮은 레버리지로 시작하는 게 맞다.</p>
<p><em>본 글은 투자 권유가 아닙니다. 모든 투자 결정과 그 책임은 본인에게 있습니다.</em></p>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[Dango]]></category>
      <category><![CDATA[DNG]]></category>
      <category><![CDATA[에어드랍]]></category>
      <category><![CDATA[DEX]]></category>
      <category><![CDATA[CLOB]]></category>
      <category><![CDATA[Layer1]]></category>
      <category><![CDATA[Perps]]></category>
      <category><![CDATA[Galxe]]></category>
      <category><![CDATA[OAT]]></category>
      <category><![CDATA[포인트파밍]]></category>
    </item>

    <item>
      <title><![CDATA[Apyx 프로토콜 완전 분석 — 배당금 스테이블코인과 에어드랍 전략]]></title>
      <link>https://www.stragos.xyz/posts/apyx-protocol-airdrop-guide</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/apyx-protocol-airdrop-guide</guid>
      <pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[선호주 배당금으로 수익을 내는 스테이블코인 프로토콜 Apyx. APYX 토큰 에어드랍을 노리는 Pips 포인트 시스템 전략을 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>솔직히 처음에 Apyx를 봤을 때 "또 <abbr title="달러에 가격이 고정된 암호화폐. 1개 = 1달러 유지를 목표로 함">스테이블코인</abbr>이네" 했다.</p>
<p>USDT, USDC, DAI, FRAX, LUSD... 스테이블코인 세상에 이미 스테이블코인이 넘친다. 이름도 비슷비슷해서 나중에는 구분이 안 된다. 그냥 "달러인 척하는 코인들" 정도로만 기억하게 되는 구간이 온다.</p>
<p>근데 Apyx는 구조가 달랐다. 들여다보다가 "이거 진짜 처음 보는 방식인데?" 했다.</p>
<hr>
<h2 id="기존-스테이블코인들이-하는-짓"><a class="anchor" aria-hidden="true" tabindex="-1" href="#기존-스테이블코인들이-하는-짓">#</a>기존 스테이블코인들이 하는 짓</h2>
<ul>
<li><strong>USDT/USDC</strong>: 달러 예치하고 영수증 발행. 당신 돈으로 이자 챙기고 당신한테는 0% 줌. 이게 무려 수십억 달러 규모로 굴러감.</li>
<li><strong>DAI</strong>: <abbr title="이더리움 등 다른 자산을 담보로 맡기고 달러를 빌리는 방식">ETH 담보로 달러 빌려줌</abbr>. <abbr title="담보가 빌린 금액보다 더 많아야 하는 구조. 100달러 빌리려면 150달러치 담보 필요">오버콜래터럴</abbr>이라 자본 효율이 살짝 아쉬움.</li>
<li><strong>UST</strong>: <abbr title="수학 공식으로 달러 가치를 유지하는 방식. 담보 없이 알고리즘만으로 운영">알고리즘으로 달러 유지</abbr>한다고 했다가 루나와 함께 역사의 뒤안길로... (명복을 빕니다)</li>
</ul>
<p>Apyx는 이런 구조들이랑 다르다. <strong>상장된 <abbr title="보통주보다 배당금이 먼저 지급되는 주식. 의결권은 없지만 안정적인 배당 수익이 특징">우선주</abbr>의 배당금</strong>으로 돌아간다.</p>
<hr>
<h2 id="apyx가-뭔지부터"><a class="anchor" aria-hidden="true" tabindex="-1" href="#apyx가-뭔지부터">#</a>Apyx가 뭔지부터</h2>
<h3 id="은행-예금-이야기로-시작해보자"><a class="anchor" aria-hidden="true" tabindex="-1" href="#은행-예금-이야기로-시작해보자">#</a>은행 예금 이야기로 시작해보자</h3>
<p>당신이 은행에 100만원을 예치하면 어떻게 되는지 아는가. 은행은 그 돈을 굴려서 수익을 낸다. 기업에 대출해주고, 채권을 사고, 온갖 방법으로 이자를 번다. 그리고 당신한테는 연 3-4% 준다. 나머지는 은행이 챙긴다.</p>
<p>USDT, USDC 같은 기존 스테이블코인도 똑같이 한다. 당신 달러를 받아서 미국채 등에 투자하고 수익을 낸다. 테더(Tether)는 2023년에 이렇게 <strong>72억 달러</strong> 순이익을 냈다. 직원 100명짜리 회사가. 그 돈 다 어디서 났을까 — 당신 USDT다.</p>
<p>Apyx의 아이디어는 이거다: <strong>그 수익을 사용자에게 돌려준다면?</strong></p>
<hr>
<h3 id="apyx가-실제로-하는-것"><a class="anchor" aria-hidden="true" tabindex="-1" href="#apyx가-실제로-하는-것">#</a>Apyx가 실제로 하는 것</h3>
<p>한 줄 요약: <strong>배당금 기반 스테이블코인(DBS)</strong> <abbr title="Dividend-Backed Stablecoin. 우선주 배당금을 담보로 하는 스테이블코인">(Dividend-Backed Stablecoin)</abbr> 프로토콜이다.</p>
<p>구체적으로 어떻게 돌아가는지 단계별로 보자.</p>
<p><strong>1단계: 자금 수집</strong></p>
<p>사용자가 달러(USDC 등)를 넣으면 Apyx가 그걸 모은다.</p>
<p><strong>2단계: 우선주 매입</strong></p>
<p>모인 달러로 <strong>DAT 기업</strong> <abbr title="Digital Asset Treasury. 비트코인 등 디지털 자산을 대량 보유한 상장 기업">(Digital Asset Treasury)</abbr>의 <strong>우선주</strong> <abbr title="보통주보다 배당금이 먼저 지급되는 주식. 의결권은 없지만 안정적인 배당 수익이 특징">(Preferred Stock)</abbr>를 산다. 구체적으로는:</p>
<ul>
<li><strong>STRC</strong> — Strategy Inc.(구 MicroStrategy)의 우선주. 연 11.50% 배당 (가변금리, 매월 조정).</li>
<li><strong>SATA</strong> — Strive Inc.의 우선주. 연 13.00% 배당 (가변금리, 매월 조정).</li>
</ul>
<p>잠깐, Strategy Inc.가 뭐냐고? 마이클 세일러가 만든 회사인데, 지금까지 비트코인을 <strong>78만 개 이상</strong> 사들인 곳이다 (2026년 4월 기준 약 780,897 BTC). 비트코인 최대 기업 보유자. 이 회사가 자금 조달을 위해 우선주를 발행하고, 우선주 보유자에게 매달 배당금을 준다.</p>
<p><strong>3단계: 배당금 수취</strong></p>
<p>이 우선주에서 매달 배당금이 들어온다. 비트코인 가격이 오르든 내리든, 코인 시장이 죽어있든 간에 <strong>배당금은 계약상 의무</strong>다. 예측 가능한 현금 흐름이 매달 프로토콜로 들어온다는 뜻이다.</p>
<p><strong>4단계: 배당금을 사용자에게 분배</strong></p>
<p>여기서 핵심이 나온다. 이 배당금을 프로토콜이 독식하지 않고 <strong>apyUSD 보유자에게 돌려준다</strong>.</p>
<pre><code>사용자 달러 → Apyx → STRC/SATA 우선주 매입 → 매달 배당금 수취 → apyUSD 보유자에게 분배
</code></pre>
<hr>
<h3 id="왜-하필-우선주냐"><a class="anchor" aria-hidden="true" tabindex="-1" href="#왜-하필-우선주냐">#</a>왜 하필 우선주냐</h3>
<p>"그냥 미국채 사면 되지 않나"라고 생각할 수 있다. 미국채는 안전하지만 수익률이 연 4-5% 수준이다. STRC, SATA 우선주는 <strong>연 11-12%</strong> 수준이다. 수익률이 두 배 이상 높다.</p>
<p>단, 높은 수익률엔 이유가 있다. 미국채와 달리 우선주는 기업 신용을 바탕으로 한다. 기업이 망하면 배당도 없다. 그래서 리스크가 더 높고, 그만큼 수익률도 높다. (이 부분은 리스크 섹션에서 더 자세히 다룬다.)</p>
<hr>
<h3 id="현실적으로-얼마나-버냐"><a class="anchor" aria-hidden="true" tabindex="-1" href="#현실적으로-얼마나-버냐">#</a>현실적으로 얼마나 버냐</h3>
<p>지금 apyUSD의 기본 수익률은 약 <strong>연 11-12% APY</strong>다. 이걸 DeFi 전략과 결합하면 실질 수익률이 더 올라간다.</p>
<p>비교해보면:</p>
<ul>
<li>은행 예금: 연 3-4%</li>
<li>USDC(Circle): 이자 없음 (수익은 Circle이 가짐)</li>
<li>Aave에 USDC 예치: 연 5-6%</li>
<li>apyUSD 기본 보유: 연 11-12%</li>
</ul>
<p>물론 이게 공짜는 아니다. 리스크가 있고, 환매에 30일이 걸리고, DeFi 특유의 복잡함이 있다. 하지만 수익률 자체는 DeFi에서도 꽤 경쟁력 있는 숫자다.</p>
<hr>
<h2 id="두-개의-토큰"><a class="anchor" aria-hidden="true" tabindex="-1" href="#두-개의-토큰">#</a>두 개의 토큰</h2>
<p>Apyx는 토큰을 두 가지로 쪼갰다. 처음에 "왜 복잡하게 두 개야" 싶었는데 나름 이유가 있다.</p>
<p><strong>apxUSD — 수익 없는 합성 달러</strong></p>
<p>쉽게 말해 "DeFi에서 굴리는 용 달러"다. <abbr title="탈중앙화 거래소. 유동성을 공급하면 거래 수수료를 나눠받음">Curve</abbr>, <abbr title="탈중앙화 대출 프로토콜. 자산을 담보로 맡기고 다른 자산을 빌릴 수 있음">Morpho</abbr> 같은 프로토콜에 예치하거나 담보로 쓸 수 있다. 수익은 없지만 그 대신 유동성이 있다. "일단 달러가 필요해"일 때 쓰는 버전.</p>
<p><strong>apyUSD — 수익이 붙는 스테이블코인</strong></p>
<p>apxUSD를 잠그면 받을 수 있다. 우선주 배당금이 여기로 흘러들어온다. <abbr title="수익형 토큰 표준. 토큰 개수는 유지하면서 환율이 올라가는 방식으로 수익을 쌓음">ERC-4626</abbr> 구조라 토큰 개수는 그대로인데 시간이 지나면서 환율이 오르는 방식으로 수익이 쌓인다. <abbr title="보유량이 자동으로 늘어나는 방식. 세금 계산이 복잡해지고 가격이 희석되는 느낌이라 호불호 갈림">리베이스</abbr> 없음. (리베이스 싫어하는 사람들 환호)</p>
<p>단점 하나: <abbr title="출금 요청 후 실제로 받을 수 있을 때까지 기다려야 하는 대기 기간">환매 쿨다운</abbr>이 약 30일이다. "환매"라는 말이 생소하면 그냥 <strong>출금</strong>이라고 생각하면 된다. apyUSD를 달러로 바꿔서 빼고 싶다 → 요청하고 → 30일 기다림 → 그때서야 달러가 내 지갑으로 온다. 쿨다운 중엔 수익도 안 붙는다. "장기 홀더 우대, 단기 투기꾼 비우대" 정책이라고 보면 된다.</p>
<hr>
<h2 id="apyx-거버넌스-토큰--아직-안-나왔는데-이게-핵심"><a class="anchor" aria-hidden="true" tabindex="-1" href="#apyx-거버넌스-토큰--아직-안-나왔는데-이게-핵심">#</a>APYX 거버넌스 토큰 — 아직 안 나왔는데 이게 핵심</h2>
<p>아직 출시 전인데 <abbr title="토큰의 총 공급량, 분배 방식, 인플레이션 등을 설계한 경제 구조">토크노믹스</abbr>가 꽤 마음에 들었다.</p>
<ul>
<li>총 공급량: <strong>1억 개 고정</strong>, 추가 발행 없음</li>
<li><strong>VC <abbr title="벤처캐피탈. 프로젝트 초기에 투자하고 토큰을 할인가로 받아두는 기관. 락업 풀리면 대량 매도하는 경우가 많아 악명 높음">(Venture Capital)</abbr> 할당 없음</strong> — 이게 진짜 중요하다. VC 물량 <abbr title="특정 기간 동안 토큰을 팔 수 없도록 묶어두는 제도">락업</abbr> 풀리면 매도 폭탄 맞는 거 한 번이라도 겪어본 사람은 알 거다.</li>
</ul>
<p><abbr title="토큰을 프로토콜에 맡겨두는 것. 보상을 받는 대신 일정 기간 인출이 제한됨">스테이킹</abbr>하면 월간 준비금 성장의 <strong>50%를 배당</strong>받는다. apxUSD 또는 APYX로 수령 선택 가능. 나머지 50%는 준비금으로 쌓여서 프로토콜 담보 비율을 강화한다. 선순환 구조다.</p>
<hr>
<h2 id="이-프로젝트-믿어도-되나--팀과-현황"><a class="anchor" aria-hidden="true" tabindex="-1" href="#이-프로젝트-믿어도-되나--팀과-현황">#</a>이 프로젝트 믿어도 되나 — 팀과 현황</h2>
<p>새로운 DeFi 프로젝트가 나올 때마다 "이거 러그풀 아니야?"가 먼저 나오는 건 당연하다. 한 번이라도 당해본 사람은 안다. 그래서 확인한 것들을 정리한다.</p>
<h3 id="팀-배경"><a class="anchor" aria-hidden="true" tabindex="-1" href="#팀-배경">#</a>팀 배경</h3>
<p>Apyx를 만든 팀은 <abbr title="나스닥에 상장된 Solana 기반 DAT 기업. 솔라나를 대량 보유·축적하는 전략을 쓰는 최초의 비트코인 외 DAT">DeFi Development Corp. (Nasdaq: DFDV)</abbr>를 만든 팀이다. 나스닥 상장사다. 익명 개발팀이 아니라는 얘기다. 공개 시장에서 책임을 지는 구조다.</p>
<h3 id="자금-조달"><a class="anchor" aria-hidden="true" tabindex="-1" href="#자금-조달">#</a>자금 조달</h3>
<ul>
<li>2026년 1월 시드 라운드: 밸류에이션 <strong>7,000만 달러</strong></li>
<li>이후 전략적 라운드: 밸류에이션 <strong>3억 달러</strong></li>
<li>총 조달액: <strong>300만 달러</strong> (밸류에이션 대비 적은 이유는 VC를 일절 배제했기 때문)</li>
</ul>
<p>VC가 없다는 게 단순한 마케팅 문구가 아니다. 실제로 모든 투자자가 전략적 투자자(Strategic)로만 구성됐다. 락업 풀리면 덤핑하는 VC 물량이 없다.</p>
<h3 id="현재-규모"><a class="anchor" aria-hidden="true" tabindex="-1" href="#현재-규모">#</a>현재 규모</h3>
<ul>
<li>apxUSD 총 발행량: <strong>4,300만 달러+</strong></li>
<li>apyUSD 잠금 규모: <strong>2,100만 달러+</strong></li>
<li>런칭 시점이 2026년 2월임을 감안하면 두 달 만에 이 수치다.</li>
</ul>
<h3 id="기관-파트너십"><a class="anchor" aria-hidden="true" tabindex="-1" href="#기관-파트너십">#</a>기관 파트너십</h3>
<p><strong>BitGo</strong>가 apxUSD의 수탁(custody) 지원을 공식 추가했다. BitGo는 전 세계 수십억 달러 규모의 디지털 자산을 수탁하는 업계 최대 적격 수탁 기관 중 하나다. BitGo 인프라에 올라가는 자산은 보안, 컴플라이언스, 운영 적합성 심사를 통과해야 한다. apxUSD가 그 심사를 통과했다는 뜻이다.</p>
<p><strong>Kraken &#x26; xStocks</strong>와도 파트너십을 맺고 토큰화 주식(tokenized equity) 확장을 추진 중이다.</p>
<h3 id="투명성"><a class="anchor" aria-hidden="true" tabindex="-1" href="#투명성">#</a>투명성</h3>
<ul>
<li>담보 구성 <strong>매일 NAV 공개</strong></li>
<li>제3자 감사 기관의 <strong>월간 증명 리포트</strong> 발행</li>
<li>프로토콜 담보 비율 실시간 대시보드 제공</li>
</ul>
<p>"믿어라"가 아니라 "직접 확인할 수 있게 열어뒀다"는 구조다.</p>
<hr>
<h2 id="에어드랍-전략--apyx-pips"><a class="anchor" aria-hidden="true" tabindex="-1" href="#에어드랍-전략--apyx-pips">#</a>에어드랍 전략 — Apyx Pips</h2>
<p>지금 시즌 1 포인트 프로그램이 돌고 있다. 이름은 <strong>Pips</strong>.</p>
<p><strong>운영 조건</strong>: 12주 또는 apxUSD 공급량이 10억 달러에 도달할 때까지. 둘 중 먼저 오는 것 기준.</p>
<p>포인트는 APYX 토큰으로 전환된다. <strong>공식 확정 사항</strong>이다. Apyx 팀은 총 공급량 1억 개 중 <strong>5%인 500만 APYX</strong>를 시즌 1 참여자에게 에어드랍한다고 명시했다. 추측이 아니라 오피셜이다.</p>
<p>포인트 산식은 이렇다.</p>
<blockquote>
<p><strong>Pips = USD 금액 × 배수 × 일수</strong></p>
</blockquote>
<p>$1,000 넣고 20x 전략 쓰면 하루에 20,000 Pips. 12주(84일)면 총 1,680,000 Pips다.</p>
<h3 id="활동별-배수-전체-테이블"><a class="anchor" aria-hidden="true" tabindex="-1" href="#활동별-배수-전체-테이블">#</a>활동별 배수 전체 테이블</h3>
<table>
<thead>
<tr>
<th>활동</th>
<th>프로토콜</th>
<th>배수</th>
</tr>
</thead>
<tbody>
<tr>
<td><abbr title="Pendle의 수익 토큰. 만기까지 발생할 수익을 미리 사는 레버리지 포지션. 만기 후 가치 0으로 수렴">Hold YT-apxUSD</abbr></td>
<td>Pendle</td>
<td><strong>32x</strong></td>
</tr>
<tr>
<td><abbr title="Pendle의 apxUSD 유동성 풀에 자금 공급. 거래 수수료 + 고배율 포인트 동시 수령">LP in apxUSD Pendle market</abbr></td>
<td>Pendle</td>
<td><strong>24x</strong></td>
</tr>
<tr>
<td><abbr title="apxUSD를 Apyx에 직접 잠금. 14일 해제 기간 있지만 즉시 요청 가능">Commit apxUSD</abbr></td>
<td>Apyx</td>
<td><strong>20x</strong></td>
</tr>
<tr>
<td>Hold YT-apyUSD</td>
<td>Pendle</td>
<td><strong>13x</strong></td>
</tr>
<tr>
<td><abbr title="Curve의 apxUSD/USDC 풀에 유동성 공급 후 커밋까지 해야 12x 적용">Curve LP apxUSD/USDC + commit</abbr></td>
<td>Curve</td>
<td><strong>12x</strong></td>
</tr>
<tr>
<td>LP in apyUSD Pendle market</td>
<td>Pendle</td>
<td><strong>11x</strong></td>
</tr>
<tr>
<td>Hold apxUSD</td>
<td>Apyx</td>
<td><strong>10x</strong></td>
</tr>
<tr>
<td>Curve LP apyUSD/apxUSD + commit</td>
<td>Curve</td>
<td><strong>6x</strong></td>
</tr>
<tr>
<td><abbr title="Morpho에서 apxUSD를 담보로 apyUSD를 빌리는 것">Borrow apxUSD (Morpho)</abbr></td>
<td>Morpho</td>
<td><strong>5x</strong></td>
</tr>
<tr>
<td>Deposit Morpho apxUSD Vault</td>
<td>Morpho</td>
<td><strong>5x</strong></td>
</tr>
<tr>
<td>Hold apyUSD</td>
<td>Apyx</td>
<td><strong>1.2x</strong></td>
</tr>
<tr>
<td>Hold apyUSD</td>
<td>Apyx</td>
<td><strong>1x</strong></td>
</tr>
</tbody>
</table>
<p>레퍼럴: 추천인 포인트의 <strong>+5%</strong> 추가 (상한: 내 포인트의 100%)</p>
<hr>
<h2 id="전략별-상세-분석--내-상황에-맞는-걸-골라라"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략별-상세-분석--내-상황에-맞는-걸-골라라">#</a>전략별 상세 분석 — 내 상황에 맞는 걸 골라라</h2>
<h3 id="전략-1-yt-apxusd-보유-32x--공격형"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-1-yt-apxusd-보유-32x--공격형">#</a>전략 1. YT-apxUSD 보유 (32x) — 공격형</h3>
<p><abbr title="Pendle Finance의 수익 토큰. 만기까지 발생할 수익 권리를 사는 것으로, 원금보다 훨씬 작은 금액으로 큰 수익 노출을 얻는 레버리지 구조">YT(Yield Token)</abbr>는 Pendle에서 살 수 있다. 개념이 처음에 낯선데, 이렇게 이해하면 쉽다.</p>
<blockquote>
<p>"만기까지 apxUSD가 만들어낼 수익을 통째로 미리 사는 것"</p>
</blockquote>
<p>예를 들어 apxUSD가 연 10% 수익을 내고 만기가 6개월 남았다면, 그 6개월치 수익(약 5%)을 지금 할인된 가격에 살 수 있다. 실제로는 100달러짜리 수익 권리를 3-4달러에 사는 식이다.</p>
<p><strong>왜 32x냐면</strong> — 3달러로 100달러짜리 수익을 샀으니까 레버리지 효과가 극대화된다. 포인트도 마찬가지로 실제 투입 금액 대비 포인트가 폭발적으로 쌓인다.</p>
<p><strong>Pips 계산 주의사항</strong></p>
<p>YT는 액면가의 3-4%에 거래된다. $500을 써서 YT를 사면 <strong>노셔널(notional) 기준으로는 $12,000-$16,000짜리 포지션</strong>이 생기는 구조다. Pips가 실제 지출액 기준으로 붙는지, 노셔널 기준으로 붙는지에 따라 숫자가 완전히 달라진다. 공식 문서에 명확히 나와있지 않으므로, <strong>실제 Pips 예상치는 앱에서 직접 포지션을 시뮬레이션해보고 확인하는 것을 권장한다.</strong></p>
<p>배수(32x) 자체는 다른 전략 대비 압도적으로 높다는 것은 변함없다.</p>
<p><strong>단점</strong>: 만기가 다가올수록 YT 가격이 0에 수렴한다. 만기 전에 팔거나 포인트 전환 타이밍을 잡아야 한다. "묻어두고 잊는" 전략에는 어울리지 않는다. 타이밍을 신경 쓸 수 있는 사람한테 맞는 전략이다.</p>
<hr>
<h3 id="전략-2-lp-in-apxusd-pendle-market-24x--공격형--수수료-수익"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-2-lp-in-apxusd-pendle-market-24x--공격형--수수료-수익">#</a>전략 2. LP in apxUSD Pendle market (24x) — 공격형 + 수수료 수익</h3>
<p>Pendle의 apxUSD 풀에 유동성을 공급하는 것이다. YT를 사는 것과 달리 <abbr title="유동성 공급자. 풀에 자금을 넣고 거래 수수료를 나눠받는 역할">LP</abbr>는 포지션이 상대적으로 안정적이다.</p>
<p><strong>왜 좋냐면</strong></p>
<p>포인트 24x는 두 번째로 높으면서, Pendle 거래 수수료까지 추가로 받는다. 포인트 + 수수료 이중 수령이다.</p>
<p><strong>시뮬레이션</strong></p>
<p>$1,000 넣고 84일 → 24x → 2,016,000 Pips. 같은 금액으로 Commit(20x)했을 때보다 400,000 Pips 더 쌓인다.</p>
<p><strong>단점</strong>: <abbr title="비영구적 손실. 유동성 공급 중 두 토큰의 가격 비율이 바뀌면 단순 보유 대비 손해가 생길 수 있음">비영구적 손실(IL)</abbr> 리스크가 있다. apxUSD는 스테이블코인이라 IL이 크지 않지만 아예 없진 않다. 또 Pendle 풀은 만기가 있어서 만기 이후에는 포지션을 재설정해야 한다.</p>
<hr>
<h3 id="전략-3-commit-apxusd-20x--안정형-고효율"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-3-commit-apxusd-20x--안정형-고효율">#</a>전략 3. Commit apxUSD (20x) — 안정형 고효율</h3>
<p>가장 직관적인 전략이다. apxUSD를 Apyx에 직접 커밋하면 된다. 복잡한 개념 없이 "넣으면 포인트"다.</p>
<p><strong>시뮬레이션</strong></p>
<p>$1,000 커밋 → 20x → 하루 20,000 Pips → 84일 후 1,680,000 Pips</p>
<p><strong>주의할 점</strong>: 14일 해제 기간이 있다. 지금 당장 빼려고 해도 14일은 기다려야 한다는 뜻. 급하게 출금할 일 없는 자금을 넣어야 한다.</p>
<hr>
<h3 id="전략-4-curve-lp-apxusdusdc--commit-12x--균형형"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-4-curve-lp-apxusdusdc--commit-12x--균형형">#</a>전략 4. Curve LP apxUSD/USDC + commit (12x) — 균형형</h3>
<p><abbr title="Curve Finance의 stableswap 풀. 스테이블코인끼리 교환할 때 슬리피지를 최소화하도록 설계된 AMM">Curve</abbr> apxUSD/USDC 풀에 유동성을 공급하고, 거기서 받은 LP 토큰을 다시 커밋해야 12x가 적용된다. 두 단계 필요.</p>
<p><strong>왜 괜찮냐면</strong></p>
<ul>
<li>Curve 수수료 수익</li>
<li>Curve 리워드 (있는 경우)</li>
<li>Apyx Pips 12x</li>
</ul>
<p>세 가지를 동시에 받는다. 배수가 낮아 보여도 실제 총 수익률은 상위 전략과 경쟁할 수 있다.</p>
<p><strong>스테이블코인끼리 풀이라 IL 리스크가 거의 없다.</strong> apxUSD와 USDC 둘 다 달러 페그 자산이니까. "가장 리스크 낮게 괜찮은 포인트 받고 싶다"면 여기가 답이다.</p>
<hr>
<h3 id="전략-5-morpho-borrow-5x--레버리지-전략의-시작점"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-5-morpho-borrow-5x--레버리지-전략의-시작점">#</a>전략 5. Morpho Borrow (5x) — 레버리지 전략의 시작점</h3>
<p>Morpho에서 apxUSD를 담보로 apyUSD를 빌린다. 배수는 5x로 낮아 보이지만, 이걸 기반으로 레버리지를 만들 수 있다.</p>
<p><strong>레버리지 루프 예시</strong></p>
<ol>
<li>$1,000 apxUSD → Morpho에 담보로 넣음</li>
<li>apyUSD 빌림 (예: $700)</li>
<li>빌린 apyUSD → apxUSD로 교환</li>
<li>교환한 apxUSD → 다시 Morpho에 담보</li>
<li>반복</li>
</ol>
<p>이렇게 하면 실제 자금은 $1,000이지만 포지션은 훨씬 커진다. 당연히 청산 리스크도 같이 커진다. <strong>레버리지 경험이 있는 사람에게만 권한다.</strong></p>
<hr>
<h3 id="전략-6-포트폴리오-분산--가장-현실적인-접근"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전략-6-포트폴리오-분산--가장-현실적인-접근">#</a>전략 6. 포트폴리오 분산 — 가장 현실적인 접근</h3>
<p>솔직히 하나에 몰빵하는 것보다 나눠 담는 게 심리적으로도, 리스크 관리로도 낫다.</p>
<p>예시: $3,000이 있다면</p>
<table>
<thead>
<tr>
<th>포지션</th>
<th>금액</th>
<th>배수</th>
<th>일별 Pips</th>
</tr>
</thead>
<tbody>
<tr>
<td>YT-apxUSD</td>
<td>$500</td>
<td>32x</td>
<td>앱에서 확인</td>
</tr>
<tr>
<td>Pendle LP apxUSD</td>
<td>$1,000</td>
<td>24x</td>
<td>24,000</td>
</tr>
<tr>
<td>Commit apxUSD</td>
<td>$1,000</td>
<td>20x</td>
<td>20,000</td>
</tr>
<tr>
<td>Curve LP + commit</td>
<td>$500</td>
<td>12x</td>
<td>6,000</td>
</tr>
<tr>
<td><strong>합계</strong></td>
<td><strong>$3,000</strong></td>
<td></td>
<td><strong>50,000+ Pips/일</strong></td>
</tr>
</tbody>
</table>
<p>YT 포지션은 노셔널 기준 계산 여부에 따라 Pips가 크게 달라지므로 앱에서 직접 확인 필요. LP/Commit/Curve 기준으로만 합산해도 84일 후 <strong>4,200,000 Pips</strong>. 단순 보유(10x)만 했을 때의 2,520,000 Pips 대비 1.7배 이상이다.</p>
<hr>
<h2 id="레퍼럴--알아두면-쓸모-있는-구조"><a class="anchor" aria-hidden="true" tabindex="-1" href="#레퍼럴--알아두면-쓸모-있는-구조">#</a>레퍼럴 — 알아두면 쓸모 있는 구조</h2>
<p>레퍼럴 구조는 간단하다. 누군가 내 링크로 가입해서 포인트를 쌓으면 그 5%를 내가 추가로 받는다. 자본을 더 쓰지 않고 포인트를 늘릴 수 있는 방법이다.</p>
<p>상한이 있다. 추천인 포인트 합산이 내 자체 포인트의 100%를 넘을 수 없다. 그래서 레퍼럴만으로 포인트 폭탄을 만들기는 어렵고, 본인이 열심히 할수록 레퍼럴 한도도 같이 올라가는 구조다.</p>
<p><strong>셀프 레퍼럴도 된다.</strong> 지갑을 두 개 쓰면 가능하다. 지갑 A로 레퍼럴 링크를 만들고, 지갑 B로 그 링크를 통해 가입하면 지갑 A가 B 포인트의 5%를 추가로 받는다. 소소하지만 알아두면 나쁠 건 없다.</p>
<p>이 글의 레퍼럴 링크: <a href="https://app.apyx.fi/join/iPG09WN">https://app.apyx.fi/join/iPG09WN</a></p>
<p>솔직히 말하면 이 링크로 들어오면 나도 포인트 5% 추가로 받는다. 부담 없이 본인 판단대로 쓰면 된다.</p>
<hr>
<h2 id="리스크--이것도-반드시-읽어야-한다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#리스크--이것도-반드시-읽어야-한다">#</a>리스크 — 이것도 반드시 읽어야 한다</h2>
<p>좋은 것만 쓰면 광고글이다. Apyx에는 진짜 리스크가 있다. 구체적으로 짚는다.</p>
<h3 id="1-우선주-신용-위험--가장-현실적인-리스크"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-우선주-신용-위험--가장-현실적인-리스크">#</a>1. 우선주 신용 위험 — 가장 현실적인 리스크</h3>
<p>Apyx의 수익 원천은 STRC, SATA의 배당금이다. 이 배당금은 기업이 주는 것이다. 만약 해당 기업이 재무 상황 악화로 배당을 삭감하거나 중단하면 어떻게 될까.</p>
<p>apyUSD 수익률이 직격탄을 맞는다. 투자자들이 이탈하면 apxUSD 수요가 줄고, 프로토콜 전체가 흔들릴 수 있다. 이건 "DeFi 리스크"가 아니라 <strong>기업 신용 리스크</strong>다. DeFi에서 처음 마주치는 종류의 리스크다.</p>
<p><strong>실제로 얼마나 현실적인가</strong>: STRC는 Strategy Inc.(구 MicroStrategy)의 우선주다. 마이클 세일러가 비트코인을 대량 보유한 회사. 비트코인이 폭락하면 이 회사 주가가 흔들리고, 극단적으로는 우선주 배당도 위태로울 수 있다.</p>
<h3 id="2-dat-사망-나선--극단적이지만-이해해야-할-시나리오"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-dat-사망-나선--극단적이지만-이해해야-할-시나리오">#</a>2. DAT 사망 나선 — 극단적이지만 이해해야 할 시나리오</h3>
<p>비트코인이 50% 이상 급락하는 상황을 가정해보자.</p>
<ol>
<li>DAT 기업(Strategy Inc. 등) 주가 급락</li>
<li>이 기업의 우선주(STRC, SATA) 가격도 하락</li>
<li>Apyx의 담보 가치 감소</li>
<li>apxUSD 오버콜래터럴 비율이 위험 구간으로 진입</li>
<li>프로토콜이 담보를 청산하기 시작</li>
<li>청산 물량이 시장에 나오면서 추가 하락 압력</li>
</ol>
<p>루나-테라의 알고리즘 사망 나선과 구조는 다르지만, 비슷한 피드백 루프가 생길 수 있다는 걸 알고 있어야 한다. 개발팀도 이 리스크를 문서에 명시하고 있다.</p>
<h3 id="3-apxusd-페그-리스크--눈에-잘-안-보이는-위험"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-apxusd-페그-리스크--눈에-잘-안-보이는-위험">#</a>3. apxUSD 페그 리스크 — 눈에 잘 안 보이는 위험</h3>
<p>apxUSD는 $1에 완전 고정된 게 아니다. 시장에서 수요/공급에 따라 $0.99-$1.01 사이를 오간다. 평소엔 무시할 수 있는 수준이지만, 시장이 패닉 상태에 들어가면 페그가 더 크게 벗어날 수 있다.</p>
<p><strong>실제 시나리오</strong>: 갑자기 대형 홀더가 대량 환매를 요청한다. 30일 쿨다운이 있어서 즉시 청산은 못 하지만, 시장에서 apxUSD를 팔기 시작한다. apxUSD 가격이 $0.97까지 떨어진다. 이때 들고 있으면 3% 손실.</p>
<p>소액이면 무시할 수 있지만 큰돈이면 이야기가 다르다.</p>
<h3 id="4-환매-쿨다운--급할-때-진짜-문제가-된다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-환매-쿨다운--급할-때-진짜-문제가-된다">#</a>4. 환매 쿨다운 — 급할 때 진짜 문제가 된다</h3>
<p>"환매(換買)"라는 단어가 낯설 수 있다. 펀드나 금융 상품에서 쓰는 용어인데, 쉽게 말하면 <strong>"내 돈 돌려줘" 요청</strong>이다. apyUSD를 달러로 교환해서 출금하고 싶을 때 하는 절차다.</p>
<p>문제는 이게 즉시 처리가 안 된다는 거다.</p>
<p><strong>실제 절차를 따라가보자</strong></p>
<ol>
<li>나: "apyUSD 1,000개 환매 요청합니다" → 클릭</li>
<li>시스템: "접수했습니다. 약 30일 후에 달러로 받을 수 있어요"</li>
<li>그 30일 동안: 내 apyUSD는 잠김 상태, 수익도 안 붙음</li>
<li>30일 후: 달러 1,000달러 + 알파가 지갑으로 도착</li>
</ol>
<p>은행 예금과 비교하면, 은행 정기예금을 중도해지하면 이자를 덜 주는 것처럼 여기서는 <strong>30일 대기 + 그 기간 수익 없음</strong>이라는 패널티가 있다. 단, 원금 자체를 깎지는 않는다.</p>
<p><strong>왜 이런 구조냐</strong></p>
<p>Apyx 입장에서 이유가 있다. 프로토콜은 당신 달러로 STRC, SATA 우선주를 매입해서 보유 중이다. 누군가 갑자기 대규모 출금을 요청하면 그 우선주를 팔아서 달러를 마련해야 한다. 우선주는 주식 시장에서 매도해야 하고, 시간이 걸린다. 패닉 매도가 나오면 담보 가치가 훼손된다.</p>
<p>30일 쿨다운은 이걸 방지하는 완충 장치다. 바꿔 말하면 뱅크런(bank run, 대규모 동시 인출 사태)을 구조적으로 막는 설계다.</p>
<p><strong>왜 리스크냐</strong></p>
<p>시장이 갑자기 무너지는 상황을 상상해보자. 비트코인이 하루에 30% 폭락한다. "지금 당장 달러로 빼야 해"라고 생각해도 클릭 한 번으로는 안 된다. 30일을 기다려야 한다. 그 30일 동안 다른 기회를 잡을 수도 없고, 시장이 더 나빠질 수도 있다.</p>
<p>게다가 <strong>한 사람당 미결제 요청은 하나만</strong> 허용된다. 1,000달러 환매 요청을 넣은 상태에서 추가로 500달러를 더 빼고 싶어도, 처음 요청이 완료될 때까지는 새 요청을 넣을 수 없다.</p>
<p>apyUSD에 자금을 넣을 때는 "이 돈은 최소 30일은 없는 돈"이라고 생각하고 들어가야 한다. 급한 자금은 절대 넣지 말 것.</p>
<h3 id="5-스마트-컨트랙트-리스크--코드는-항상-틀릴-수-있다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-스마트-컨트랙트-리스크--코드는-항상-틀릴-수-있다">#</a>5. 스마트 컨트랙트 리스크 — 코드는 항상 틀릴 수 있다</h3>
<p>Quantstamp, Certora, Zellic 세 곳에서 감사를 받았다. 업계에서 인정받는 곳들이다. 하지만 감사를 받은 프로토콜도 해킹당한다. 2022년 Nomad Bridge 해킹($190M), 2023년 Euler Finance 해킹($197M)도 감사를 받은 프로토콜이었다.</p>
<p>"감사 완료 = 안전"이 아니라 "감사 완료 = 그나마 검증됨" 정도로 이해해야 한다.</p>
<h3 id="6-규제-리스크--스테이블코인은-규제-타깃-1순위"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-규제-리스크--스테이블코인은-규제-타깃-1순위">#</a>6. 규제 리스크 — 스테이블코인은 규제 타깃 1순위</h3>
<p>스테이블코인은 전통 금융 시스템에 직접적인 영향을 미친다. 미국 SEC나 각국 규제 기관이 스테이블코인 규제를 강화하면 apxUSD/apyUSD 운영이 제약을 받을 수 있다. 현재는 미국, EU, EEA 거주자 접근이 불가하다는 것 자체가 규제를 이미 의식하고 있다는 신호다.</p>
<h3 id="7-apyx-토큰-전환-불확실성--에어드랍-사냥의-근본-전제"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7-apyx-토큰-전환-불확실성--에어드랍-사냥의-근본-전제">#</a>7. APYX 토큰 전환 불확실성 — 에어드랍 사냥의 근본 전제</h3>
<p>지금 포인트(Pips)를 쌓는 이유는 나중에 APYX 토큰으로 전환될 것이라는 기대다. 에어드랍 자체는 공식 확정됐다 — 총 공급량의 5%(500만 APYX)를 시즌 1 참여자에게 배분하는 것은 명시된 사항이다. 하지만 <strong>Pips 1개당 APYX가 몇 개 배분되는지</strong>(전환 비율)는 시즌 종료 시점의 총 Pips 합산에 따라 결정되므로 현재는 확정되지 않았다. 참여자가 많아질수록 같은 Pips로 받는 APYX 양이 줄어든다.</p>
<p>에어드랍 자체는 공식 확정됐지만, <strong>전환 비율</strong>(Pips 1개당 APYX 몇 개)은 시즌 종료 시점의 총 Pips 합산에 따라 결정된다. 참여자가 많아질수록 같은 Pips로 받는 APYX 양은 줄어든다. 포인트를 쌓기 위해 실제 자산을 잠그는 것이니까, 전환 비율이 기대보다 낮게 나올 가능성은 있다.</p>
<hr>
<h2 id="참여-방법"><a class="anchor" aria-hidden="true" tabindex="-1" href="#참여-방법">#</a>참여 방법</h2>
<ol>
<li><a href="https://app.apyx.fi/join/iPG09WN">Apyx 앱 접속</a> — 레퍼럴 링크 (들어오면 나도 포인트 5% 추가로 받음, 이건 솔직하게 말하는 게 맞다)</li>
<li>지갑 연결 (이더리움 또는 베이스 네트워크)</li>
<li>apxUSD <abbr title="스테이블코인을 새로 발행하는 것. 담보를 맡기고 apxUSD를 만들어냄">민팅</abbr> 또는 DEX에서 구매</li>
<li>위 전략 중 본인 성향에 맞는 것 선택</li>
<li>12주 버티기 (진짜로 이게 제일 어렵다)</li>
</ol>
<hr>
<p>정리하면 이렇다.</p>
<p>VC 오버행 없는 고정 공급 토큰, 실제 현금 흐름 기반 수익, 시즌 1 포인트 진행 중. 세 가지가 동시에 맞아떨어지는 경우가 흔하지 않다. 나는 들어가 있다.</p>
<p>단, 리스크 섹션에서 설명한 것들을 이해하고 들어가는 것과 모르고 들어가는 건 완전히 다른 얘기다. 특히 DAT 사망 나선과 쿨다운 리스크는 본인 자금 규모에 따라 치명적일 수 있다.</p>
<p>포인트 → 토큰 전환 공식 발표 나오면 그때는 이미 늦다. 발표 나오면 다들 몰려들고, 배수 한도부터 차기 시작한다.</p>
<p>관심 있으면 지금이다.</p>
<hr>
<p><em>본 글은 투자 권유가 아닙니다. 모든 투자 결정과 그 책임은 본인에게 있습니다.</em></p>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[Apyx]]></category>
      <category><![CDATA[에어드랍]]></category>
      <category><![CDATA[스테이블코인]]></category>
      <category><![CDATA[DeFi]]></category>
      <category><![CDATA[apxUSD]]></category>
      <category><![CDATA[apyUSD]]></category>
      <category><![CDATA[Pendle]]></category>
      <category><![CDATA[포인트]]></category>
    </item>

    <item>
      <title><![CDATA[Java 실력 차이를 만드는 코딩 습관 — Enum, Records, 동시성까지]]></title>
      <link>https://www.stragos.xyz/posts/java-coding-habits</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/java-coding-habits</guid>
      <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[Enum에 동작 넣기, Records로 DTO 정리, try-with-resources, Generics 설계, 단위 테스트, AtomicInteger까지. 코드 리뷰에서 한 단계 올라서게 해주는 Java 실전 습관 10가지.]]></description>
      <content:encoded><![CDATA[<p>어느 시점이 되면 내 코드가 "돌아가는 코드"에서 멈춰있다는 걸 느낄 때가 있다.</p>
<p>기능은 완성했고, 테스트도 통과했는데 — 코드 리뷰에서 조용히 질문이 달린다. "이 분기문이 상태 추가될 때마다 여기 찾아서 고쳐야 하지 않나요?", "이 DTO 파일 왜 이렇게 긴 거예요?" 같은 것들. 틀린 코드는 아닌데 뭔가 아쉽다는 느낌. 그 "아쉬움"이 뭔지 한동안 잘 몰랐다.</p>
<p>그때부터 필요해지는 것들이 있다. 입문서에는 잘 안 나오는데 실무에선 당연하게 쓰이는 패턴들. 아래는 그중에서 내가 직접 써보고 "이게 맞다" 싶었던 것들이다.</p>
<hr>
<h2 id="enum-은-상수-모음이-아니다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#enum-은-상수-모음이-아니다">#</a>Enum 은 상수 모음이 아니다</h2>
<p>처음에 Enum을 이렇게 배웠다. 상수 묶음. 타입 안전한 값.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> enum</span><span style="color:#B392F0"> OrderStatus</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#79B8FF">    PENDING</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">APPROVED</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">REJECTED</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">SHIPPED</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">DELIVERED</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>여기까진 다들 안다. 문제는 그 다음이다. 보통 이 Enum을 쓰는 코드가 이렇게 된다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">if</span><span style="color:#E1E4E8"> (order.</span><span style="color:#B392F0">getStatus</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">==</span><span style="color:#E1E4E8"> OrderStatus.APPROVED) {</span></span>
<span data-line=""><span style="color:#B392F0">    sendApprovalEmail</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">} </span><span style="color:#F97583">else</span><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (order.</span><span style="color:#B392F0">getStatus</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">==</span><span style="color:#E1E4E8"> OrderStatus.REJECTED) {</span></span>
<span data-line=""><span style="color:#B392F0">    sendRejectionEmail</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">} </span><span style="color:#F97583">else</span><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (order.</span><span style="color:#B392F0">getStatus</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">==</span><span style="color:#E1E4E8"> OrderStatus.SHIPPED) {</span></span>
<span data-line=""><span style="color:#B392F0">    sendShippingNotification</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>상태가 하나 추가될 때마다 이 분기문을 찾아서 고쳐야 한다. 서비스 어딘가에 숨어있으면 진짜 못 찾는다. 놓치면 그냥 버그다.</p>
<p>근데 Enum이 메서드를 가질 수 있다는 걸 알고 있었나? 처음 이걸 알았을 때 좀 충격이었다. 클래스처럼 각 값마다 동작을 정의할 수 있다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> enum</span><span style="color:#B392F0"> OrderStatus</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#79B8FF">    PENDING</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">        @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">        public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> notify</span><span style="color:#E1E4E8">(Order </span><span style="color:#FFAB70">order</span><span style="color:#E1E4E8">, NotificationService </span><span style="color:#FFAB70">svc</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">            svc.</span><span style="color:#B392F0">sendPendingNotice</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">        }</span></span>
<span data-line=""><span style="color:#E1E4E8">    },</span></span>
<span data-line=""><span style="color:#79B8FF">    APPROVED</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">        @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">        public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> notify</span><span style="color:#E1E4E8">(Order </span><span style="color:#FFAB70">order</span><span style="color:#E1E4E8">, NotificationService </span><span style="color:#FFAB70">svc</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">            svc.</span><span style="color:#B392F0">sendApprovalEmail</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">        }</span></span>
<span data-line=""><span style="color:#E1E4E8">    },</span></span>
<span data-line=""><span style="color:#79B8FF">    REJECTED</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">        @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">        public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> notify</span><span style="color:#E1E4E8">(Order </span><span style="color:#FFAB70">order</span><span style="color:#E1E4E8">, NotificationService </span><span style="color:#FFAB70">svc</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">            svc.</span><span style="color:#B392F0">sendRejectionEmail</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">        }</span></span>
<span data-line=""><span style="color:#E1E4E8">    },</span></span>
<span data-line=""><span style="color:#79B8FF">    SHIPPED</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">        @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">        public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> notify</span><span style="color:#E1E4E8">(Order </span><span style="color:#FFAB70">order</span><span style="color:#E1E4E8">, NotificationService </span><span style="color:#FFAB70">svc</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">            svc.</span><span style="color:#B392F0">sendShippingNotification</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">        }</span></span>
<span data-line=""><span style="color:#E1E4E8">    };</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> abstract</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> notify</span><span style="color:#E1E4E8">(Order </span><span style="color:#FFAB70">order</span><span style="color:#E1E4E8">, NotificationService </span><span style="color:#FFAB70">svc</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>abstract</code> 가 붙어있으니 각 상태값이 반드시 <code>notify()</code> 를 구현해야 한다. 빠트리면 컴파일 에러가 난다. "실수하면 컴파일러가 잡아준다" — 이게 핵심이다.</p>
<p>호출하는 쪽은 이렇게 된다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">order.</span><span style="color:#B392F0">getStatus</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">notify</span><span style="color:#E1E4E8">(order, notificationService);</span></span></code></pre></figure>
<p>if-else가 한 줄로 줄었다. 새 상태가 생기면 Enum에만 추가하면 된다. 더 이상 분기문 찾아다니지 않아도 된다는 게 처음엔 조금 해방감 같은 느낌이었다.</p>
<p>필드랑 생성자도 넣을 수 있다. 관련된 값들을 묶어두는 데 쓴다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> enum</span><span style="color:#B392F0"> HttpStatus</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#79B8FF">    OK</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">200</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"OK"</span><span style="color:#E1E4E8">),</span></span>
<span data-line=""><span style="color:#79B8FF">    NOT_FOUND</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">404</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"Not Found"</span><span style="color:#E1E4E8">),</span></span>
<span data-line=""><span style="color:#79B8FF">    INTERNAL_SERVER_ERROR</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">500</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"Internal Server Error"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#F97583"> int</span><span style="color:#E1E4E8"> code;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> String message;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#B392F0">    HttpStatus</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">int</span><span style="color:#FFAB70"> code</span><span style="color:#E1E4E8">, String </span><span style="color:#FFAB70">message</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.code </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> code;</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.message </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> message;</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> int</span><span style="color:#B392F0"> getCode</span><span style="color:#E1E4E8">()       { </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> code; }</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">getMessage</span><span style="color:#E1E4E8">() { </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> message; }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>HttpStatus.NOT_FOUND.getCode()</code> 하면 404가 나온다. 숫자를 상수로 따로 관리하다가 어디서 쓰는지 모르게 되는 것보다 훨씬 낫다.</p>
<hr>
<h2 id="record-쓰면-dto-파일-절반으로-줄어든다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#record-쓰면-dto-파일-절반으로-줄어든다">#</a>Record 쓰면 DTO 파일 절반으로 줄어든다</h2>
<p>솔직히 Java로 DTO 만들 때마다 조금 짜증났다. 필드 3개짜리 클래스인데 파일이 40줄이 넘는다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> UserDto</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> String name;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> String email;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#F97583"> int</span><span style="color:#E1E4E8"> age;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#B392F0"> UserDto</span><span style="color:#E1E4E8">(String </span><span style="color:#FFAB70">name</span><span style="color:#E1E4E8">, String </span><span style="color:#FFAB70">email</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">int</span><span style="color:#FFAB70"> age</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.name </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> name;</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.email </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> email;</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.age </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> age;</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">getName</span><span style="color:#E1E4E8">()  { </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> name; }</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">getEmail</span><span style="color:#E1E4E8">() { </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> email; }</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> int</span><span style="color:#B392F0"> getAge</span><span style="color:#E1E4E8">()      { </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> age; }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> boolean</span><span style="color:#B392F0"> equals</span><span style="color:#E1E4E8">(Object </span><span style="color:#FFAB70">o</span><span style="color:#E1E4E8">) { ... }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> int</span><span style="color:#B392F0"> hashCode</span><span style="color:#E1E4E8">() { ... }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">toString</span><span style="color:#E1E4E8">() { ... }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>생성자 만들고, getter 만들고, equals/hashCode/toString 만들고... 실제 데이터는 3개인데 코드 길이는 왜 이렇게 긴 거야. 이걸 Java 개발자들이 오래 불만으로 가져왔는데 Java 16에서 <code>record</code> 가 정식으로 들어왔다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> record</span><span style="color:#B392F0"> UserDto</span><span style="color:#E1E4E8">(String name, String email, </span><span style="color:#F97583">int</span><span style="color:#E1E4E8"> age) {}</span></span></code></pre></figure>
<p>이게 전부다. 생성자, getter, <code>equals()</code>, <code>hashCode()</code>, <code>toString()</code> 전부 컴파일러가 자동으로 만들어준다. 처음 봤을 때 "이게 뭐야" 싶었는데, 한 번 쓰고 나서 이전 방식으로 돌아가기 싫어졌다.</p>
<p>한 가지 다른 점이 있다. getter가 <code>getName()</code> 이 아니라 <code>name()</code> 형태다. 헷갈리는 부분이니 기억해두면 좋다.</p>
<p>유효성 검사가 필요하면 이렇게 추가할 수 있다. <code>public UserDto { ... }</code> 형태가 record 전용 생성자 블록인데, 여기에 검사 로직을 넣으면 된다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> record</span><span style="color:#B392F0"> UserDto</span><span style="color:#E1E4E8">(String name, String email, </span><span style="color:#F97583">int</span><span style="color:#E1E4E8"> age) {</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#B392F0"> UserDto</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">        if</span><span style="color:#E1E4E8"> (name </span><span style="color:#F97583">==</span><span style="color:#79B8FF"> null</span><span style="color:#F97583"> ||</span><span style="color:#E1E4E8"> name.</span><span style="color:#B392F0">isBlank</span><span style="color:#E1E4E8">()) {</span></span>
<span data-line=""><span style="color:#F97583">            throw</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> IllegalArgumentException</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"이름은 필수입니다."</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">        }</span></span>
<span data-line=""><span style="color:#F97583">        if</span><span style="color:#E1E4E8"> (age </span><span style="color:#F97583">&#x3C;</span><span style="color:#79B8FF"> 0</span><span style="color:#F97583"> ||</span><span style="color:#E1E4E8"> age </span><span style="color:#F97583">></span><span style="color:#79B8FF"> 150</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">            throw</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> IllegalArgumentException</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"나이가 유효하지 않습니다: "</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> age);</span></span>
<span data-line=""><span style="color:#E1E4E8">        }</span></span>
<span data-line=""><span style="color:#E1E4E8">        name </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> name.</span><span style="color:#B392F0">trim</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>API 응답 객체, 커맨드 객체처럼 "데이터를 담아서 전달하는" 용도에 딱 맞는다. Record는 불변이라서 한번 만들면 필드 값을 바꿀 수 없다. 그래서 안심하고 여기저기 넘길 수 있다.</p>
<p>JPA Entity에는 쓰지 않는 게 좋다. JPA는 기본 생성자가 필요하고 내부적으로 필드를 바꾸는 경우가 있는데, Record의 불변성과 충돌한다. 처음에 Entity를 record로 만들려다가 삽질한 기억이 있다.</p>
<hr>
<h2 id="자원-닫는-코드-더-이상-직접-쓰지-않아도-된다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#자원-닫는-코드-더-이상-직접-쓰지-않아도-된다">#</a>자원 닫는 코드, 더 이상 직접 쓰지 않아도 된다</h2>
<p>파일을 열거나, DB 커넥션을 맺거나, 소켓을 열면 — 반드시 다 쓰고 나서 닫아야 한다. 안 닫으면 메모리 누수가 생기거나 커넥션 풀이 고갈된다.</p>
<p>예전엔 <code>finally</code> 블록에서 직접 닫았다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">BufferedReader reader </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#F97583">try</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">    reader </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> BufferedReader</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">new</span><span style="color:#B392F0"> FileReader</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"data.txt"</span><span style="color:#E1E4E8">));</span></span>
<span data-line=""><span style="color:#E1E4E8">    String line;</span></span>
<span data-line=""><span style="color:#F97583">    while</span><span style="color:#E1E4E8"> ((line </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> reader.</span><span style="color:#B392F0">readLine</span><span style="color:#E1E4E8">()) </span><span style="color:#F97583">!=</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#B392F0">        process</span><span style="color:#E1E4E8">(line);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">} </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (IOException </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    log.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"파일 읽기 실패"</span><span style="color:#E1E4E8">, e);</span></span>
<span data-line=""><span style="color:#E1E4E8">} </span><span style="color:#F97583">finally</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (reader </span><span style="color:#F97583">!=</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        try</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">            reader.</span><span style="color:#B392F0">close</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// 여기서도 예외가 날 수 있다</span></span>
<span data-line=""><span style="color:#E1E4E8">        } </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (IOException </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">            log.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"파일 닫기 실패"</span><span style="color:#E1E4E8">, e);</span></span>
<span data-line=""><span style="color:#E1E4E8">        }</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>finally</code> 안에서 또 try-catch를 써야 한다. 처음 이걸 봤을 때 "이게 맞나?" 싶었다. 실수할 포인트가 너무 많고, 피로하다.</p>
<p>Java 7에서 <code>try-with-resources</code> 가 나왔다. <code>try (...)</code> 안에 자원을 선언하면, 블록이 끝날 때 자동으로 <code>close()</code> 가 호출된다. 정상 종료든, 예외가 터지든 상관없이.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">try</span><span style="color:#E1E4E8"> (BufferedReader reader </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> BufferedReader</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">new</span><span style="color:#B392F0"> FileReader</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"data.txt"</span><span style="color:#E1E4E8">))) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    String line;</span></span>
<span data-line=""><span style="color:#F97583">    while</span><span style="color:#E1E4E8"> ((line </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> reader.</span><span style="color:#B392F0">readLine</span><span style="color:#E1E4E8">()) </span><span style="color:#F97583">!=</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#B392F0">        process</span><span style="color:#E1E4E8">(line);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">} </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (IOException </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    log.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"파일 읽기 실패"</span><span style="color:#E1E4E8">, e);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>훨씬 낫다. 자원이 여러 개면 세미콜론으로 구분하면 된다. 마지막에 선언한 것부터 역순으로 닫힌다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">try</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">    Connection conn </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> dataSource.</span><span style="color:#B392F0">getConnection</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    PreparedStatement stmt </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> conn.</span><span style="color:#B392F0">prepareStatement</span><span style="color:#E1E4E8">(sql);</span></span>
<span data-line=""><span style="color:#E1E4E8">    ResultSet rs </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> stmt.</span><span style="color:#B392F0">executeQuery</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">    while</span><span style="color:#E1E4E8"> (rs.</span><span style="color:#B392F0">next</span><span style="color:#E1E4E8">()) {</span></span>
<span data-line=""><span style="color:#6A737D">        // 처리</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>직접 만든 클래스도 이 기능을 쓸 수 있다. <code>AutoCloseable</code> 인터페이스를 구현하고 <code>close()</code> 메서드를 정의하면 된다. "닫을 수 있는 자원"이라고 JVM에게 알려주는 셈이다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> HttpClient</span><span style="color:#F97583"> implements</span><span style="color:#B392F0"> AutoCloseable</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> CloseableHttpClient client;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#B392F0"> HttpClient</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.client </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> HttpClients.</span><span style="color:#B392F0">createDefault</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(String </span><span style="color:#FFAB70">url</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">throws</span><span style="color:#E1E4E8"> IOException {</span></span>
<span data-line=""><span style="color:#6A737D">        // HTTP 요청 처리</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> close</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">throws</span><span style="color:#E1E4E8"> Exception {</span></span>
<span data-line=""><span style="color:#E1E4E8">        client.</span><span style="color:#B392F0">close</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// try 블록이 끝나면 이게 자동으로 호출됨</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">try</span><span style="color:#E1E4E8"> (HttpClient client </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> HttpClient</span><span style="color:#E1E4E8">()) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    String response </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> client.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"https://api.example.com/data"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#B392F0">    process</span><span style="color:#E1E4E8">(response);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<hr>
<h2 id="generic-클래스-한-번쯤은-직접-만들어봐야-한다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#generic-클래스-한-번쯤은-직접-만들어봐야-한다">#</a>Generic 클래스 한 번쯤은 직접 만들어봐야 한다</h2>
<p><code>List&#x3C;String></code>, <code>Map&#x3C;String, Integer></code> 처럼 꺽쇠 안에 타입을 넣는 건 누구나 쓴다. 그런데 저 꺽쇠를 <strong>내가 직접 만드는</strong> 건 처음엔 어색하다. 왠지 어렵고 고급진 느낌이라 손이 잘 안 간다.</p>
<p>핵심 개념만 먼저 짚고 가자. <code>&#x3C;T></code> 는 "어떤 타입이든 여기에 넣을 수 있어요" 라는 표시다. <code>T</code> 는 Type의 약자고 이름은 아무거나 써도 되는데 관례적으로 T, E, K, V 같은 걸 쓴다.</p>
<p>예를 들어 API 응답 포맷을 통일하고 싶다고 하자. 그냥 짜면 이렇게 된다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> UserApiResponse</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> boolean</span><span style="color:#E1E4E8"> success;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String message;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> UserDto data;      </span><span style="color:#6A737D">// 유저 조회 응답</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> OrderApiResponse</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> boolean</span><span style="color:#E1E4E8"> success;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String message;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> OrderDto data;     </span><span style="color:#6A737D">// 주문 조회 응답</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>success</code>, <code>message</code> 는 똑같은데 <code>data</code> 타입만 달라서 파일이 두 개다. API가 10개면 파일이 10개. Generic을 쓰면 하나로 통일된다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> ApiResponse</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> {          </span><span style="color:#6A737D">// T 자리에 어떤 타입이든 들어올 수 있다</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#F97583"> boolean</span><span style="color:#E1E4E8"> success;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> String message;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> T data;              </span><span style="color:#6A737D">// 실제 응답 데이터</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#B392F0"> ApiResponse</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">boolean</span><span style="color:#FFAB70"> success</span><span style="color:#E1E4E8">, String </span><span style="color:#FFAB70">message</span><span style="color:#E1E4E8">, T </span><span style="color:#FFAB70">data</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.success </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> success;</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.message </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> message;</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.data </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> data;</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> ApiResponse&#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(T </span><span style="color:#FFAB70">data</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> ApiResponse&#x3C;>(</span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"OK"</span><span style="color:#E1E4E8">, data);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> ApiResponse&#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">fail</span><span style="color:#E1E4E8">(String </span><span style="color:#FFAB70">message</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> ApiResponse&#x3C;>(</span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8">, message, </span><span style="color:#79B8FF">null</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>컨트롤러에서는 이렇게 쓴다. <code>ApiResponse&#x3C;UserDto></code> 라고 쓰면 T 자리에 <code>UserDto</code> 가 들어간다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">GetMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/users/{id}"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> ResponseEntity</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">ApiResponse</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">UserDto</span><span style="color:#F97583">>></span><span style="color:#B392F0"> getUser</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">PathVariable</span><span style="color:#E1E4E8"> Long id) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    UserDto user </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> userService.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id);</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(ApiResponse.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(user));</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>extends</code> 로 "이 타입의 자식만 받겠다" 는 제한도 걸 수 있다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// T가 Number를 상속한 타입이어야만 이 메서드를 쓸 수 있다</span></span>
<span data-line=""><span style="color:#6A737D">// Integer, Double, Long은 가능하지만 String은 컴파일 에러가 난다</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> &#x3C;</span><span style="color:#E1E4E8">T extends Number </span><span style="color:#F97583">&#x26;</span><span style="color:#E1E4E8"> Comparable</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">T</span><span style="color:#F97583">>></span><span style="color:#E1E4E8"> T </span><span style="color:#B392F0">max</span><span style="color:#E1E4E8">(List</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">T</span><span style="color:#F97583">></span><span style="color:#E1E4E8"> list) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> list.</span><span style="color:#B392F0">stream</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">        .</span><span style="color:#B392F0">max</span><span style="color:#E1E4E8">(Comparator.</span><span style="color:#B392F0">naturalOrder</span><span style="color:#E1E4E8">())</span></span>
<span data-line=""><span style="color:#E1E4E8">        .</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">-></span><span style="color:#F97583"> new</span><span style="color:#B392F0"> IllegalArgumentException</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"빈 리스트"</span><span style="color:#E1E4E8">));</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>?</code> 와일드카드는 "타입이 뭔지 아예 모르거나 신경 안 써도 될 때" 쓴다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 어떤 타입의 리스트든 출력만 하면 되니까 ? 로 충분</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> printAll</span><span style="color:#E1E4E8">(List</span><span style="color:#F97583">&#x3C;?></span><span style="color:#E1E4E8"> list) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    list.</span><span style="color:#B392F0">forEach</span><span style="color:#E1E4E8">(System.out</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">println);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// Number 자식 타입이면 다 받겠다 — 읽기만 하고 추가는 안 됨</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> double</span><span style="color:#B392F0"> sum</span><span style="color:#E1E4E8">(List</span><span style="color:#F97583">&#x3C;?</span><span style="color:#E1E4E8"> extends Number</span><span style="color:#F97583">></span><span style="color:#E1E4E8"> numbers) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> numbers.</span><span style="color:#B392F0">stream</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">mapToDouble</span><span style="color:#E1E4E8">(Number</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">doubleValue).</span><span style="color:#B392F0">sum</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>이걸 처음 보고 "왜 이렇게 만들어놨지?" 싶었는데, 나중에 내가 비슷한 중복 코드를 만들고 있는 걸 발견했다. 그때서야 Generic이 왜 필요한지 체감이 됐다.</p>
<hr>
<h2 id="메서드-이름이-거짓말을-하고-있을-때"><a class="anchor" aria-hidden="true" tabindex="-1" href="#메서드-이름이-거짓말을-하고-있을-때">#</a>메서드 이름이 거짓말을 하고 있을 때</h2>
<p>이름은 "조회"인데 내부에서 카운트를 올리고 로그를 남기고 알림까지 보내는 메서드를 본 적이 있다. 아마 처음엔 작은 기능이었는데 거기에 계속 뭔가를 붙이다 보니 그렇게 됐을 거다. 이런 코드는 보는 순간 피로감이 온다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> User </span><span style="color:#B392F0">getUser</span><span style="color:#E1E4E8">(Long id) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    User user </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> userRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id).</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    user.</span><span style="color:#B392F0">setViewCount</span><span style="color:#E1E4E8">(user.</span><span style="color:#B392F0">getViewCount</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">+</span><span style="color:#79B8FF"> 1</span><span style="color:#E1E4E8">);  </span><span style="color:#6A737D">// 조회수 올리기</span></span>
<span data-line=""><span style="color:#E1E4E8">    userRepository.</span><span style="color:#B392F0">save</span><span style="color:#E1E4E8">(user);</span></span>
<span data-line=""><span style="color:#E1E4E8">    auditLog.</span><span style="color:#B392F0">record</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"USER_VIEW"</span><span style="color:#E1E4E8">, id);             </span><span style="color:#6A737D">// 감사 로그 남기기</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> user;</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>getUser</code> 라는 이름에서 "조회수 올리기"와 "감사 로그"는 전혀 예상이 안 된다. 이게 왜 문제냐면:</p>
<ul>
<li><strong>테스트하기 어렵다.</strong> "그냥 유저 조회하는 테스트"를 쓰고 싶은데 카운트까지 올라가버린다.</li>
<li><strong>재사용이 안 된다.</strong> 다른 곳에서 유저를 조회할 때 카운트를 올리면 안 되는 상황이 오면 새 메서드를 또 만들어야 한다.</li>
<li><strong>이런 게 쌓이면</strong> 비슷하게 생긴 메서드가 여러 개 생기고 어디서 뭘 써야 할지 모르게 된다.</li>
</ul>
<p>책임을 쪼개야 한다. 각 메서드가 딱 한 가지 일만 하게.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> User </span><span style="color:#B392F0">getUser</span><span style="color:#E1E4E8">(Long id) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> userRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id)</span></span>
<span data-line=""><span style="color:#E1E4E8">        .</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">-></span><span style="color:#F97583"> new</span><span style="color:#B392F0"> UserNotFoundException</span><span style="color:#E1E4E8">(id));</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> recordUserView</span><span style="color:#E1E4E8">(Long id) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    userRepository.</span><span style="color:#B392F0">incrementViewCount</span><span style="color:#E1E4E8">(id);</span></span>
<span data-line=""><span style="color:#E1E4E8">    auditLog.</span><span style="color:#B392F0">record</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"USER_VIEW"</span><span style="color:#E1E4E8">, id);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>파라미터 개수도 신경 쓰인다. 3개까지는 봐줄 만한데 4개 넘어가면 파라미터 객체를 고민해봐야 한다. 인수가 6개 나란히 있으면 솔직히 읽기 싫다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 이게 뭔지 한눈에 파악이 안 된다</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> Order </span><span style="color:#B392F0">createOrder</span><span style="color:#E1E4E8">(Long userId, Long productId, </span><span style="color:#F97583">int</span><span style="color:#E1E4E8"> quantity,</span></span>
<span data-line=""><span style="color:#E1E4E8">                         String address, String couponCode, </span><span style="color:#F97583">boolean</span><span style="color:#E1E4E8"> expressDelivery) { ... }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// Record로 묶으면 이름이 생긴다</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> record</span><span style="color:#B392F0"> CreateOrderRequest</span><span style="color:#E1E4E8">(</span></span>
<span data-line=""><span style="color:#E1E4E8">    Long userId, Long productId, </span><span style="color:#F97583">int</span><span style="color:#E1E4E8"> quantity,</span></span>
<span data-line=""><span style="color:#E1E4E8">    String address, String couponCode, </span><span style="color:#F97583">boolean</span><span style="color:#E1E4E8"> expressDelivery</span></span>
<span data-line=""><span style="color:#E1E4E8">) {}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> Order </span><span style="color:#B392F0">createOrder</span><span style="color:#E1E4E8">(CreateOrderRequest request) { ... }</span></span></code></pre></figure>
<p><code>sendEmail(user, true)</code> 처럼 Boolean 파라미터를 넘기는 것도 마찬가지다. 저 <code>true</code> 가 뭘 의미하는지 메서드 시그니처를 직접 열어봐야 알 수 있다. 실제로 열어봤을 때 "이게 뭐지" 한 적이 한두 번이 아니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// true 가 뭘 의미하는 건지 알 수가 없다</span></span>
<span data-line=""><span style="color:#B392F0">sendEmail</span><span style="color:#E1E4E8">(user, </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 이름이 있으면 바로 이해된다</span></span>
<span data-line=""><span style="color:#B392F0">sendEmail</span><span style="color:#E1E4E8">(user, EmailType.WELCOME);</span></span>
<span data-line=""><span style="color:#B392F0">sendWelcomeEmail</span><span style="color:#E1E4E8">(user);</span></span></code></pre></figure>
<hr>
<h2 id="테스트-짜기-어려운-코드가-나쁜-코드다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#테스트-짜기-어려운-코드가-나쁜-코드다">#</a>테스트 짜기 어려운 코드가 나쁜 코드다</h2>
<p>테스트를 쓰다 보면 "이게 왜 이렇게 짜기 어렵지?" 하는 순간이 온다. 그때 짜증을 테스트 탓으로 하지 말고 코드 탓을 해야 한다. 테스트가 어려운 건 대부분 설계가 잘못됐다는 신호다. 이걸 알고 나서 오히려 테스트가 좋아졌다.</p>
<p>JUnit 5 + Mockito 기본 구조다. 처음 보는 분들을 위해 빠르게 설명하면:</p>
<ul>
<li><code>@Mock</code> — 진짜 DB 대신 가짜 Repository를 만든다</li>
<li><code>@InjectMocks</code> — 테스트할 서비스에 저 가짜 객체들을 주입한다</li>
<li><code>given(...)</code> — 가짜 객체가 어떻게 동작할지 미리 정의한다</li>
</ul>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">ExtendWith</span><span style="color:#E1E4E8">(MockitoExtension.class)</span></span>
<span data-line=""><span style="color:#F97583">class</span><span style="color:#B392F0"> OrderServiceTest</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Mock</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> OrderRepository orderRepository;   </span><span style="color:#6A737D">// 진짜 DB 대신 가짜</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Mock</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> NotificationService notificationService;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">InjectMocks</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> OrderService orderService;         </span><span style="color:#6A737D">// 진짜 서비스, 위 가짜들이 주입됨</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Test</span></span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">DisplayName</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"주문 승인 시 알림이 발송되어야 한다"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">    void</span><span style="color:#B392F0"> approveOrder_shouldSendNotification</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#6A737D">        // given: 이 상황을 가정한다</span></span>
<span data-line=""><span style="color:#E1E4E8">        Long orderId </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 1L</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#E1E4E8">        Order order </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> Order.</span><span style="color:#B392F0">builder</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">id</span><span style="color:#E1E4E8">(orderId)</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">status</span><span style="color:#E1E4E8">(OrderStatus.PENDING)</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">build</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#B392F0">        given</span><span style="color:#E1E4E8">(orderRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(orderId)).</span><span style="color:#B392F0">willReturn</span><span style="color:#E1E4E8">(Optional.</span><span style="color:#B392F0">of</span><span style="color:#E1E4E8">(order));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">        // when: 이 동작을 한다</span></span>
<span data-line=""><span style="color:#E1E4E8">        orderService.</span><span style="color:#B392F0">approve</span><span style="color:#E1E4E8">(orderId);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">        // then: 이 결과가 나와야 한다</span></span>
<span data-line=""><span style="color:#B392F0">        then</span><span style="color:#E1E4E8">(notificationService).</span><span style="color:#B392F0">should</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">notify</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">any</span><span style="color:#E1E4E8">(Order.class));</span></span>
<span data-line=""><span style="color:#B392F0">        assertThat</span><span style="color:#E1E4E8">(order.</span><span style="color:#B392F0">getStatus</span><span style="color:#E1E4E8">()).</span><span style="color:#B392F0">isEqualTo</span><span style="color:#E1E4E8">(OrderStatus.APPROVED);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Test</span></span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">DisplayName</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"존재하지 않는 주문 승인 시 예외가 발생해야 한다"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">    void</span><span style="color:#B392F0"> approveOrder_whenNotFound_shouldThrow</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#B392F0">        given</span><span style="color:#E1E4E8">(orderRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(</span><span style="color:#B392F0">anyLong</span><span style="color:#E1E4E8">())).</span><span style="color:#B392F0">willReturn</span><span style="color:#E1E4E8">(Optional.</span><span style="color:#B392F0">empty</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#B392F0">        assertThatThrownBy</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">-></span><span style="color:#E1E4E8"> orderService.</span><span style="color:#B392F0">approve</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">999L</span><span style="color:#E1E4E8">))</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">isInstanceOf</span><span style="color:#E1E4E8">(OrderNotFoundException.class)</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">hasMessageContaining</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"999"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>given / when / then</code> 구조로 나눠두면 나중에 읽기 편하다. <code>@DisplayName</code> 에 조건과 기대 결과를 문장으로 쓰는 게 습관이 되면 테스트 코드가 스펙 문서 역할을 한다. 6개월 뒤에 이 테스트를 열었을 때 고마워지는 게 이 부분이다.</p>
<p>여러 케이스를 한 번에 검증하고 싶을 때 <code>@ParameterizedTest</code> 를 쓰면 같은 테스트 구조를 여러 데이터로 돌릴 수 있다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">ParameterizedTest</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">CsvSource</span><span style="color:#E1E4E8">({</span></span>
<span data-line=""><span style="color:#9ECBFF">    "PENDING,   true"</span><span style="color:#E1E4E8">,   </span><span style="color:#6A737D">// 대기 상태 → 취소 가능</span></span>
<span data-line=""><span style="color:#9ECBFF">    "APPROVED,  false"</span><span style="color:#E1E4E8">,  </span><span style="color:#6A737D">// 승인됨 → 취소 불가</span></span>
<span data-line=""><span style="color:#9ECBFF">    "REJECTED,  false"</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#9ECBFF">    "SHIPPED,   false"</span></span>
<span data-line=""><span style="color:#E1E4E8">})</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">DisplayName</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"대기 상태인 주문만 취소 가능하다"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">void</span><span style="color:#B392F0"> isCancellable</span><span style="color:#E1E4E8">(OrderStatus status, </span><span style="color:#F97583">boolean</span><span style="color:#E1E4E8"> expected) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    Order order </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> Order.</span><span style="color:#B392F0">builder</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">status</span><span style="color:#E1E4E8">(status).</span><span style="color:#B392F0">build</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#B392F0">    assertThat</span><span style="color:#E1E4E8">(order.</span><span style="color:#B392F0">isCancellable</span><span style="color:#E1E4E8">()).</span><span style="color:#B392F0">isEqualTo</span><span style="color:#E1E4E8">(expected);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>커버리지 숫자보다 중요한 건 핵심 비즈니스 로직이 테스트되고 있느냐다. 100%를 채우려고 getter 테스트 만드는 건 솔직히 시간 낭비다.</p>
<hr>
<h2 id="멀티스레드-버그는-재현하기도-어렵다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#멀티스레드-버그는-재현하기도-어렵다">#</a>멀티스레드 버그는 재현하기도 어렵다</h2>
<p>동시성 버그는 특이하다. 개발 환경에서는 아무 문제 없다가 트래픽이 몰리면 갑자기 나온다. 재현하려고 하면 또 안 된다. 원인 찾는 동안 진짜 멘붕이 오는 케이스 중 하나다.</p>
<p>기본적인 것만 알고 있어도 흔한 실수는 피할 수 있다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> Counter</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> int</span><span style="color:#E1E4E8"> count </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> increment</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#E1E4E8">        count</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>이게 왜 위험하냐면 — <code>count++</code> 은 한 줄이지만 실제로 CPU 입장에서는 세 단계다.</p>
<ol>
<li>count 값을 읽는다</li>
<li>1을 더한다</li>
<li>저장한다</li>
</ol>
<p>두 스레드가 동시에 같은 값(예: 5)을 읽으면, 둘 다 6을 저장한다. 원래는 7이 되어야 하는데. 이런 게 수천 번 반복되면 카운트가 조용히 틀려진다. 단일 스레드 테스트에서는 절대 잡히지 않는다.</p>
<p><code>synchronized</code> 를 붙이면 한 번에 한 스레드만 들어올 수 있게 잠근다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> Counter</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> int</span><span style="color:#E1E4E8"> count </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> synchronized</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> increment</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#E1E4E8">        count</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> synchronized</span><span style="color:#F97583"> int</span><span style="color:#B392F0"> getCount</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> count;</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>단순한 정수 연산이라면 <code>AtomicInteger</code> 가 더 낫다. <code>synchronized</code> 는 잠금을 걸고 푸는 과정이 있어서 오버헤드가 있는데, <code>AtomicInteger</code> 는 그보다 가벼운 방식으로 원자성을 보장한다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> java.util.concurrent.atomic.AtomicInteger;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> Counter</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> AtomicInteger count </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> AtomicInteger</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> increment</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#E1E4E8">        count.</span><span style="color:#B392F0">incrementAndGet</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// 읽기-증가-저장을 원자적으로 처리</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> int</span><span style="color:#B392F0"> getCount</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> count.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>volatile</code> 이라는 것도 있다. 이건 좀 다른 문제를 해결하는데 — 여러 CPU 코어가 각자 캐시에 값을 가지고 있을 때, 한 스레드가 값을 바꿔도 다른 스레드가 못 보는 경우가 있다. <code>volatile</code> 은 항상 메인 메모리에서 읽고 쓰도록 강제해서 이 문제를 해결한다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 스레드 종료 플래그처럼 "쓰는 건 하나, 읽는 건 여럿"인 단순한 경우에 적합</span></span>
<span data-line=""><span style="color:#F97583">private</span><span style="color:#F97583"> volatile</span><span style="color:#F97583"> boolean</span><span style="color:#E1E4E8"> running </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> true</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> stop</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#E1E4E8">    running </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> false</span><span style="color:#E1E4E8">; </span><span style="color:#6A737D">// 이 변경이 다른 스레드에서 즉시 보임</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>주의할 점은 <code>volatile</code> 은 가시성만 보장한다. <code>count++</code> 처럼 "읽고 → 더하고 → 쓰는" 복합 연산은 여전히 위험하다. 그런 경우는 <code>AtomicInteger</code> 를 써야 한다. 처음에 이 차이를 제대로 몰라서 <code>volatile</code> 붙이고 안심했다가 낭패 본 적이 있다.</p>
<p>동시성 코드는 직접 구현하는 것보다 <code>java.util.concurrent</code> 의 검증된 구현체를 활용하는 편이 훨씬 낫다. <code>ConcurrentHashMap</code>, <code>CopyOnWriteArrayList</code>, <code>BlockingQueue</code> 같은 것들이 있다.</p>
<hr>
<h2 id="stream-filtermap-그-이상"><a class="anchor" aria-hidden="true" tabindex="-1" href="#stream-filtermap-그-이상">#</a>Stream, filter/map 그 이상</h2>
<p><code>filter</code>, <code>map</code>, <code>collect</code> 는 이제 기본인데 그 이상은 막히는 경우가 있다. <code>groupingBy</code> 같은 걸 처음 봤을 때 "이런 게 있었어?" 싶었다.</p>
<p>부서별로 직원을 그룹화한다고 하면 for 루프 짜지 않아도 된다. <code>groupingBy</code> 는 "이 기준으로 묶어줘"다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// employees 를 getDepartment() 값 기준으로 묶는다</span></span>
<span data-line=""><span style="color:#E1E4E8">Map&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">, List&#x3C;</span><span style="color:#F97583">Employee</span><span style="color:#E1E4E8">>> byDepartment </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> employees.</span><span style="color:#B392F0">stream</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">collect</span><span style="color:#E1E4E8">(Collectors.</span><span style="color:#B392F0">groupingBy</span><span style="color:#E1E4E8">(Employee</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">getDepartment));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 부서별 평균 연봉 — groupingBy 두 번째 인수로 집계 방식을 지정할 수 있다</span></span>
<span data-line=""><span style="color:#E1E4E8">Map&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">Double</span><span style="color:#E1E4E8">> avgSalaryByDept </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> employees.</span><span style="color:#B392F0">stream</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">collect</span><span style="color:#E1E4E8">(Collectors.</span><span style="color:#B392F0">groupingBy</span><span style="color:#E1E4E8">(</span></span>
<span data-line=""><span style="color:#E1E4E8">        Employee</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">getDepartment,</span></span>
<span data-line=""><span style="color:#E1E4E8">        Collectors.</span><span style="color:#B392F0">averagingInt</span><span style="color:#E1E4E8">(Employee</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">getSalary)</span></span>
<span data-line=""><span style="color:#E1E4E8">    ));</span></span></code></pre></figure>
<p><code>flatMap</code> 은 "리스트 안에 리스트" 를 하나로 펼칠 때 쓴다. <code>map</code> 이랑 자꾸 헷갈리는데 — <code>map</code> 은 각 요소를 변환하고, <code>flatMap</code> 은 각 요소를 리스트로 변환한 다음 그걸 하나로 합친다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// orders 가 [주문A, 주문B] 이고</span></span>
<span data-line=""><span style="color:#6A737D">// 주문A.getProducts() 가 [상품1, 상품2] 라면</span></span>
<span data-line=""><span style="color:#6A737D">// flatMap 으로 [상품1, 상품2, 상품3, ...] 처럼 하나의 리스트로 만들 수 있다</span></span>
<span data-line=""><span style="color:#E1E4E8">List&#x3C;</span><span style="color:#F97583">Product</span><span style="color:#E1E4E8">> allProducts </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> orders.</span><span style="color:#B392F0">stream</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">flatMap</span><span style="color:#E1E4E8">(order </span><span style="color:#F97583">-></span><span style="color:#E1E4E8"> order.</span><span style="color:#B392F0">getProducts</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">stream</span><span style="color:#E1E4E8">())</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">collect</span><span style="color:#E1E4E8">(Collectors.</span><span style="color:#B392F0">toList</span><span style="color:#E1E4E8">());</span></span></code></pre></figure>
<p>Stream은 <strong>게으르다(lazy)</strong>. <code>filter</code>, <code>map</code> 같은 중간 연산은 <code>collect</code>, <code>findFirst</code> 같은 최종 연산이 호출되기 전까지 아무것도 실행하지 않는다. 파이프라인을 조립만 해두고 실행은 나중에 한다고 보면 된다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">Optional&#x3C;</span><span style="color:#F97583">User</span><span style="color:#E1E4E8">> first </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> users.</span><span style="color:#B392F0">stream</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">filter</span><span style="color:#E1E4E8">(u </span><span style="color:#F97583">-></span><span style="color:#E1E4E8"> u.</span><span style="color:#B392F0">getAge</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">></span><span style="color:#79B8FF"> 30</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(User</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">toDto)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">findFirst</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// 여기서야 실행 시작, 조건 맞는 첫 번째 찾으면 바로 멈춤</span></span></code></pre></figure>
<p><code>findFirst()</code> 같은 경우 조건을 만족하는 첫 번째 요소를 찾는 순간 멈춰버린다. 리스트 전체를 다 처리하지 않아도 된다. 데이터가 많을 때 성능 차이가 꽤 난다.</p>
<hr>
<h2 id="optional-잘못-쓰면-더-복잡해진다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#optional-잘못-쓰면-더-복잡해진다">#</a>Optional, 잘못 쓰면 더 복잡해진다</h2>
<p><code>Optional</code> 은 "이 값이 있을 수도, 없을 수도 있다"는 걸 코드로 표현하는 방법이다. <code>null</code> 을 직접 다루는 대신 <code>Optional</code> 로 감싸서 처리한다.</p>
<p>처음 배우고 나서 뭔가 모든 곳에 쓰고 싶어지는 시기가 온다. 나도 그랬다. 근데 그러면 안 된다.</p>
<p><strong>써야 할 때</strong>: 메서드 반환값이 없을 수 있을 때. 특히 DB 단건 조회가 대표적이다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">Optional</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">User</span><span style="color:#F97583">></span><span style="color:#B392F0"> findByEmail</span><span style="color:#E1E4E8">(String email);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// orElseThrow 로 없으면 예외를 던지도록 처리</span></span>
<span data-line=""><span style="color:#E1E4E8">User user </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> userRepository.</span><span style="color:#B392F0">findByEmail</span><span style="color:#E1E4E8">(email)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">-></span><span style="color:#F97583"> new</span><span style="color:#B392F0"> UserNotFoundException</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"이메일: "</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> email));</span></span></code></pre></figure>
<p><strong>쓰면 안 되는 경우</strong>를 정리하면:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 1. 필드에 쓰지 말 것 — 직렬화가 안 되고 메모리도 낭비됨</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> User</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> Optional&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">> phoneNumber; </span><span style="color:#6A737D">// 그냥 null 허용 String 으로 쓰면 됨</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 2. 메서드 파라미터로 쓰지 말 것</span></span>
<span data-line=""><span style="color:#6A737D">// 호출할 때 Optional.of(name) 을 만들어서 넘겨야 하는게 더 불편하다</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> update</span><span style="color:#E1E4E8">(Long id, Optional</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">String</span><span style="color:#F97583">></span><span style="color:#E1E4E8"> name) { }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 3. 컬렉션을 감싸지 말 것 — 없으면 빈 리스트를 반환하면 된다</span></span>
<span data-line=""><span style="color:#E1E4E8">Optional</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">List</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">User</span><span style="color:#F97583">>></span><span style="color:#B392F0"> findAll</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// List&#x3C;User> 가 낫다, 없으면 Collections.emptyList()</span></span></code></pre></figure>
<p>파라미터에 <code>Optional</code> 을 쓴 API를 처음 봤을 때 뭔가 이상하다는 느낌이 들었는데, 막상 왜 이상한지 설명하기 어려웠다. 호출하는 쪽을 써보니까 바로 알게 됐다.</p>
<p>체이닝 패턴은 알아두면 편하다. null 체크를 if 중첩으로 하지 않아도 된다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// order 가 null 일 수도, address 가 null 일 수도, city 가 null 일 수도 있는 상황</span></span>
<span data-line=""><span style="color:#6A737D">// Optional 체이닝으로 null 체크 없이 안전하게 꺼낼 수 있다</span></span>
<span data-line=""><span style="color:#E1E4E8">String zipCode </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> Optional.</span><span style="color:#B392F0">ofNullable</span><span style="color:#E1E4E8">(order)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(Order</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">getAddress)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(Address</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">getCity)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(City</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">getZipCode)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">orElse</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"00000"</span><span style="color:#E1E4E8">); </span><span style="color:#6A737D">// 중간에 하나라도 null 이면 "00000" 반환</span></span></code></pre></figure>
<hr>
<h2 id="interface를-무조건-만들지-않아도-된다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#interface를-무조건-만들지-않아도-된다">#</a>Interface를 무조건 만들지 않아도 된다</h2>
<p>경험이 쌓이면 역설적으로 과도한 설계를 하게 되는 시기가 온다. Interface를 만들고, Factory를 만들고, Strategy 패턴을 넣는다. 뭔가 구조적으로 설계된 것 같고 좋아보인다. 실제로 나도 그 시기가 있었다.</p>
<p>구현체가 하나뿐인데 Interface가 있는 경우가 그렇다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> interface</span><span style="color:#B392F0"> UserFinder</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">    User </span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(Long </span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> UserFinderImpl</span><span style="color:#F97583"> implements</span><span style="color:#B392F0"> UserFinder</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> User </span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(Long </span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">) { ... }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>UserFinderImpl</code> 말고 다른 구현이 생길 가능성이 없다면 이 Interface는 없애도 된다.</p>
<p>"테스트할 때 Mock 만들려면 Interface가 있어야 하지 않냐" 는 생각도 있는데, Mockito는 구현 클래스도 바로 Mock할 수 있다. Interface가 없어도 <code>@Mock UserFinderImpl</code> 이 된다. 나중에 이런 코드들을 정리하면서 "내가 왜 이걸 만들었지" 하며 웃었다.</p>
<p>설계 원칙은 현재의 문제를 해결하기 위한 도구다. "나중에 혹시 쓸 것 같으니까" 미리 만들어두는 게 아니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 5줄짜리 로직을 위해 Helper 클래스를 따로 만들 필요 없다</span></span>
<span data-line=""><span style="color:#6A737D">// 같은 클래스 안에 private 메서드로 충분하다</span></span>
<span data-line=""><span style="color:#F97583">private</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">formatDisplayName</span><span style="color:#E1E4E8">(User user) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> user.</span><span style="color:#B392F0">getLastName</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> " "</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> user.</span><span style="color:#B392F0">getFirstName</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>코드를 줄이는 것 자체가 가치다. 지금 요구사항을 가장 단순하게 구현하면서 변경이 쉬운 구조를 만드는 게 좋은 설계다.</p>
<hr>
<p>지금까지 쓴 게 한꺼번에 머리에 들어올 필요는 없다. Enum 짤 때 "동작을 넣을 수 있지 않을까?", DTO 만들 때 "Record로 할 수 있지 않을까?", 자원 쓸 때 <code>try-with-resources</code> — 이것만 자연스럽게 나오기 시작해도 코드 리뷰에서 받는 질문의 색깔이 달라진다.</p>
<p>그때 기분이 나쁘지 않다.</p>
<hr>
<p><strong>관련 글</strong></p>
<ul>
<li><a href="/posts/java-tips-for-junior-developers">Java 신입 개발자가 꼭 알아야 할 실무 팁 10가지</a></li>
<li><a href="/posts/java-real-mistakes-backend">신입 백엔드 때 진짜로 혼났던 것들 — JPA, 트랜잭션, 예외처리</a></li>
</ul>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[클린코드]]></category>
      <category><![CDATA[백엔드]]></category>
      <category><![CDATA[실무]]></category>
      <category><![CDATA[테스트]]></category>
      <category><![CDATA[동시성]]></category>
    </item>

    <item>
      <title><![CDATA[Next.js + Supabase 실전 조합 — 이 스택으로 뭐든 만들 수 있다]]></title>
      <link>https://www.stragos.xyz/posts/nextjs-supabase-guide</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/nextjs-supabase-guide</guid>
      <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[Next.js App Router와 Supabase를 연동하는 방법을 처음부터 끝까지 정리했습니다. DB 조회, 인증, 실시간 기능까지 실제 코드 위주로 설명합니다.]]></description>
      <content:encoded><![CDATA[<p>사이드 프로젝트를 시작할 때마다 "이번엔 뭐 쓰지?" 하고 고민하는 게 루틴이 됐는데, 요즘은 Next.js + Supabase 조합으로 거의 굳혔습니다. 백엔드 서버 따로 안 올려도 되고, 인증부터 DB까지 한 방에 해결되니까요.</p>
<p>이 글은 <strong>계정 만들기부터 실제 데이터 꺼내 쓰는 것까지</strong> 따라만 하면 바로 동작하는 앱이 나오도록 순서대로 정리했습니다. 개념 설명보다는 실제 쓰는 코드 위주로 갑니다.</p>
<hr>
<h2 id="-왜-이-조합인가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#-왜-이-조합인가">#</a>🤔 왜 이 조합인가</h2>
<p>Next.js만 쓰면 DB가 없고, Firebase 쓰면 SQL이 그리워지고, 직접 서버 올리자니 귀찮습니다. Supabase는 PostgreSQL 기반이라 SQL 쿼리를 그대로 쓸 수 있고, JavaScript 클라이언트 라이브러리도 잘 만들어져 있습니다.</p>
<p>Next.js App Router와 궁합이 특히 좋은 이유는 <strong>Server Component에서 Supabase 클라이언트를 직접 써도 된다</strong>는 점입니다. API 라우트 없이 DB 결과를 컴포넌트에서 바로 렌더링할 수 있어서 코드가 훨씬 단순해집니다.</p>
<hr>
<h2 id="-1-프로젝트-세팅"><a class="anchor" aria-hidden="true" tabindex="-1" href="#-1-프로젝트-세팅">#</a>🚀 1. 프로젝트 세팅</h2>
<p>Node.js 18 이상이 설치된 환경이라면 지금 바로 시작할 수 있습니다.</p>
<h3 id="nextjs-앱-생성"><a class="anchor" aria-hidden="true" tabindex="-1" href="#nextjs-앱-생성">#</a>Next.js 앱 생성</h3>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#B392F0">npx</span><span style="color:#9ECBFF"> create-next-app@latest</span><span style="color:#9ECBFF"> my-app</span><span style="color:#79B8FF"> --typescript</span><span style="color:#79B8FF"> --app</span></span>
<span data-line=""><span style="color:#79B8FF">cd</span><span style="color:#9ECBFF"> my-app</span></span></code></pre></figure>
<p>실행하면 몇 가지를 물어봅니다. 취향대로 골라도 되지만 한 가지만 꼭 지켜야 합니다.</p>
<pre><code>Would you like to use App Router? › Yes  ← 반드시 Yes
</code></pre>
<p>이 글 전체가 App Router 기준입니다. Pages Router를 선택하면 뒤에 나오는 코드가 전혀 맞지 않습니다. ESLint, Tailwind CSS, <code>src/</code> 디렉터리 여부는 프로젝트 성격에 맞게 자유롭게 선택하면 됩니다.</p>
<blockquote>
<p><strong>⚠️ 흔한 실수</strong> — <code>--app</code> 플래그를 붙였는데도 설치 중 App Router 선택지에서 No를 누르면 Pages Router로 생성됩니다. 플래그보다 대화형 선택이 우선이니 주의하세요.</p>
</blockquote>
<h3 id="supabase-패키지-설치"><a class="anchor" aria-hidden="true" tabindex="-1" href="#supabase-패키지-설치">#</a>Supabase 패키지 설치</h3>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> @supabase/supabase-js</span><span style="color:#9ECBFF"> @supabase/ssr</span></span></code></pre></figure>
<p>두 패키지를 동시에 설치하는 이유가 있습니다.</p>
<ul>
<li><code>@supabase/supabase-js</code> — Supabase의 핵심 클라이언트입니다. DB 쿼리, Auth, Realtime 등 실제 기능이 여기 있습니다.</li>
<li><code>@supabase/ssr</code> — Next.js App Router처럼 SSR 환경에서 쿠키 기반 세션을 다루기 위한 래퍼입니다. 이게 없으면 서버 컴포넌트에서 로그인 상태를 유지하기가 복잡해집니다.</li>
</ul>
<blockquote>
<p><strong>⚠️ 주의</strong> — 예전에는 <code>@supabase/auth-helpers-nextjs</code>를 많이 썼는데 지금은 deprecated 됐습니다. 검색하면 이 패키지 기준 글이 아직 많이 나오니 헷갈리지 않도록 주의하세요. 새 프로젝트라면 반드시 <code>@supabase/ssr</code>로 시작하세요.</p>
</blockquote>
<h3 id="supabase-프로젝트-생성-및-키-발급"><a class="anchor" aria-hidden="true" tabindex="-1" href="#supabase-프로젝트-생성-및-키-발급">#</a>Supabase 프로젝트 생성 및 키 발급</h3>
<p><a href="https://supabase.com">supabase.com</a>에서 계정을 만들고 새 프로젝트를 생성합니다. 프로젝트 이름과 DB 비밀번호, 리전(Region)을 설정하면 1분 안에 프로비저닝이 끝납니다.</p>
<p><strong>리전은 <code>Northeast Asia (Seoul)</code>을 선택하면 국내 사용자 기준으로 응답 속도가 가장 빠릅니다.</strong></p>
<p>프로젝트가 생성되면 <strong>Settings → API</strong> 메뉴로 이동합니다. 여기서 세 가지 값을 확인할 수 있습니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>용도</th>
<th>공개 여부</th>
</tr>
</thead>
<tbody>
<tr>
<td>Project URL</td>
<td>Supabase 서버 주소</td>
<td>공개 가능</td>
</tr>
<tr>
<td>anon public key</td>
<td>클라이언트용 공개 키</td>
<td>공개 가능</td>
</tr>
<tr>
<td>service_role key</td>
<td>RLS 우회 관리자 키</td>
<td><strong>절대 노출 금지</strong></td>
</tr>
</tbody>
</table>
<p>이 중 <strong>Project URL과 anon key</strong>만 <code>.env.local</code>에 넣으면 됩니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">NEXT_PUBLIC_SUPABASE_URL</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">https://xxxxxxxxxxxx.supabase.co</span></span>
<span data-line=""><span style="color:#E1E4E8">NEXT_PUBLIC_SUPABASE_ANON_KEY</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...</span></span></code></pre></figure>
<p><code>NEXT_PUBLIC_</code> 접두사가 붙은 변수는 브라우저에서도 읽을 수 있습니다. anon key는 원래 공개되어도 되는 키로, 실제로 어떤 데이터에 접근할 수 있는지는 <strong>Row Level Security(RLS)</strong> 가 제어합니다.</p>
<p>반면 <code>service_role</code> 키는 RLS를 전부 우회하는 관리자 키입니다. 서버 전용 환경변수로만 쓰고 절대 클라이언트에 노출하면 안 됩니다.</p>
<p><code>.env.local</code>은 <code>.gitignore</code>에 이미 포함되어 있으니 Git에 올라갈 걱정은 안 해도 됩니다. Vercel에 배포할 때는 <strong>Settings → Environment Variables</strong>에 동일하게 등록해줘야 합니다.</p>
<blockquote>
<p><strong>⚠️ 트러블슈팅</strong> — 환경변수를 추가했는데 <code>undefined</code>가 뜬다면, 개발 서버를 재시작했는지 확인하세요. <code>.env.local</code> 변경 사항은 서버를 껐다 켜야 반영됩니다.</p>
</blockquote>
<hr>
<h2 id="-2-supabase-클라이언트-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#-2-supabase-클라이언트-만들기">#</a>🔧 2. Supabase 클라이언트 만들기</h2>
<p>App Router에서는 서버용과 클라이언트용 인스턴스를 <strong>반드시 분리</strong>해서 만들어야 합니다. 같은 파일로 통일하고 싶은 마음이 들 수 있는데, 서버에선 쿠키를 직접 다뤄야 하기 때문에 구조가 다릅니다.</p>
<p><strong>서버용 (<code>utils/supabase/server.ts</code>)</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="typescript" data-theme="github-dark"><code data-language="typescript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { createServerClient } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@supabase/ssr'</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { cookies } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'next/headers'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> async</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> createClient</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#79B8FF"> cookieStore</span><span style="color:#F97583"> =</span><span style="color:#F97583"> await</span><span style="color:#B392F0"> cookies</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  return</span><span style="color:#B392F0"> createServerClient</span><span style="color:#E1E4E8">(</span></span>
<span data-line=""><span style="color:#E1E4E8">    process.env.</span><span style="color:#79B8FF">NEXT_PUBLIC_SUPABASE_URL</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    process.env.</span><span style="color:#79B8FF">NEXT_PUBLIC_SUPABASE_ANON_KEY</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    {</span></span>
<span data-line=""><span style="color:#E1E4E8">      cookies: {</span></span>
<span data-line=""><span style="color:#B392F0">        getAll</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">          return</span><span style="color:#E1E4E8"> cookieStore.</span><span style="color:#B392F0">getAll</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">        },</span></span>
<span data-line=""><span style="color:#B392F0">        setAll</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">cookiesToSet</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">          cookiesToSet.</span><span style="color:#B392F0">forEach</span><span style="color:#E1E4E8">(({ </span><span style="color:#FFAB70">name</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">value</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">options</span><span style="color:#E1E4E8"> }) </span><span style="color:#F97583">=></span></span>
<span data-line=""><span style="color:#E1E4E8">            cookieStore.</span><span style="color:#B392F0">set</span><span style="color:#E1E4E8">(name, value, options)</span></span>
<span data-line=""><span style="color:#E1E4E8">          )</span></span>
<span data-line=""><span style="color:#E1E4E8">        },</span></span>
<span data-line=""><span style="color:#E1E4E8">      },</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">  )</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><strong>클라이언트용 (<code>utils/supabase/client.ts</code>)</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="typescript" data-theme="github-dark"><code data-language="typescript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { createBrowserClient } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@supabase/ssr'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> createClient</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">  return</span><span style="color:#B392F0"> createBrowserClient</span><span style="color:#E1E4E8">(</span></span>
<span data-line=""><span style="color:#E1E4E8">    process.env.</span><span style="color:#79B8FF">NEXT_PUBLIC_SUPABASE_URL</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    process.env.</span><span style="color:#79B8FF">NEXT_PUBLIC_SUPABASE_ANON_KEY</span><span style="color:#F97583">!</span></span>
<span data-line=""><span style="color:#E1E4E8">  )</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<blockquote>
<p><strong>⚠️ 흔한 실수</strong> — 서버 컴포넌트에서 <code>utils/supabase/client.ts</code>를 import하면 빌드 에러가 납니다. <code>createBrowserClient</code>는 <code>window</code> 객체에 의존하기 때문입니다. 서버 컴포넌트에는 반드시 <code>server.ts</code>를 쓰세요.</p>
</blockquote>
<hr>
<h2 id="-3-server-component에서-db-조회"><a class="anchor" aria-hidden="true" tabindex="-1" href="#-3-server-component에서-db-조회">#</a>📦 3. Server Component에서 DB 조회</h2>
<p>이게 이 스택의 핵심입니다. <code>async</code> 컴포넌트 안에서 DB 데이터를 바로 꺼내 쓸 수 있습니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="typescript" data-theme="github-dark"><code data-language="typescript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// app/posts/page.tsx</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { createClient } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/utils/supabase/server'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> async</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> PostsPage</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#79B8FF"> supabase</span><span style="color:#F97583"> =</span><span style="color:#F97583"> await</span><span style="color:#B392F0"> createClient</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> { </span><span style="color:#FFAB70">data</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">posts</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">error</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> supabase</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">from</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'posts'</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">select</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'id, title, created_at'</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">order</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'created_at'</span><span style="color:#E1E4E8">, { ascending: </span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8"> })</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">limit</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">10</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (error) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#B392F0">p</span><span style="color:#E1E4E8">>불러오기 실패</span><span style="color:#F97583">&#x3C;/</span><span style="color:#E1E4E8">p</span><span style="color:#F97583">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#B392F0">ul</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">      {</span><span style="color:#FFAB70">posts</span><span style="color:#E1E4E8">.</span><span style="color:#FFAB70">map</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">post</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#F97583">        &#x3C;</span><span style="color:#E1E4E8">li key</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{post.id}</span><span style="color:#F97583">></span><span style="color:#E1E4E8">{post.title}</span><span style="color:#F97583">&#x3C;/</span><span style="color:#E1E4E8">li</span><span style="color:#F97583">></span></span>
<span data-line=""><span style="color:#E1E4E8">      ))}</span></span>
<span data-line=""><span style="color:#F97583">    &#x3C;/</span><span style="color:#E1E4E8">ul</span><span style="color:#F97583">></span></span>
<span data-line=""><span style="color:#E1E4E8">  )</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><code>fetch</code> 따로 없고, <code>useEffect</code> 없고, 상태 관리도 없습니다. 그냥 읽어서 렌더링합니다.</p>
<blockquote>
<p><strong>⚠️ 트러블슈팅</strong> — <code>data</code>가 빈 배열로 오거나 <code>error</code>에 <code>permission denied</code>가 뜬다면 RLS 문제입니다. Supabase 대시보드에서 <strong>Authentication → Policies</strong> 메뉴로 이동해 해당 테이블에 SELECT 정책을 추가해주세요. 개발 중에 빠르게 테스트하고 싶다면 임시로 <code>Enable read access for all users</code> 정책을 켜도 됩니다. 다만 프로덕션에선 반드시 세분화된 정책을 설정해야 합니다.</p>
</blockquote>
<hr>
<h2 id="️-4-데이터-삽입--server-action-활용"><a class="anchor" aria-hidden="true" tabindex="-1" href="#️-4-데이터-삽입--server-action-활용">#</a>✏️ 4. 데이터 삽입 — Server Action 활용</h2>
<p>Next.js의 Server Action을 쓰면 API 라우트를 따로 만들지 않아도 서버에서 데이터를 쓸 수 있습니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="typescript" data-theme="github-dark"><code data-language="typescript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// app/posts/new/actions.ts</span></span>
<span data-line=""><span style="color:#9ECBFF">'use server'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { createClient } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/utils/supabase/server'</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { redirect } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'next/navigation'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> async</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> createPost</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">formData</span><span style="color:#F97583">:</span><span style="color:#B392F0"> FormData</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#79B8FF"> supabase</span><span style="color:#F97583"> =</span><span style="color:#F97583"> await</span><span style="color:#B392F0"> createClient</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#79B8FF"> title</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> formData.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'title'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">as</span><span style="color:#79B8FF"> string</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#79B8FF"> content</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> formData.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'content'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">as</span><span style="color:#79B8FF"> string</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> { </span><span style="color:#79B8FF">error</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> supabase</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">from</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'posts'</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">insert</span><span style="color:#E1E4E8">({ title, content })</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (error) </span><span style="color:#F97583">throw</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> Error</span><span style="color:#E1E4E8">(error.message)</span></span>
<span data-line=""><span style="color:#B392F0">  redirect</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'/posts'</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="typescript" data-theme="github-dark"><code data-language="typescript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// app/posts/new/page.tsx</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { createPost } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> './actions'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> NewPostPage</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#F97583">    &#x3C;</span><span style="color:#E1E4E8">form action</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{createPost}</span><span style="color:#F97583">></span></span>
<span data-line=""><span style="color:#F97583">      &#x3C;</span><span style="color:#E1E4E8">input name</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"title"</span><span style="color:#E1E4E8"> placeholder</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"제목"</span><span style="color:#F97583"> /></span></span>
<span data-line=""><span style="color:#F97583">      &#x3C;</span><span style="color:#E1E4E8">textarea name</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"content"</span><span style="color:#E1E4E8"> placeholder</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"내용"</span><span style="color:#F97583"> /></span></span>
<span data-line=""><span style="color:#F97583">      &#x3C;</span><span style="color:#E1E4E8">button type</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"submit"</span><span style="color:#F97583">></span><span style="color:#E1E4E8">저장</span><span style="color:#F97583">&#x3C;/</span><span style="color:#E1E4E8">button</span><span style="color:#F97583">></span></span>
<span data-line=""><span style="color:#F97583">    &#x3C;/</span><span style="color:#E1E4E8">form</span><span style="color:#F97583">></span></span>
<span data-line=""><span style="color:#E1E4E8">  )</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>폼을 제출하면 Server Action이 실행되고, 완료 후 <code>/posts</code>로 리다이렉트됩니다. 전통적인 HTML 폼처럼 보이지만 Next.js가 뒤에서 다 처리합니다.</p>
<blockquote>
<p><strong>⚠️ 트러블슈팅</strong> — <code>insert</code>에서 <code>new row violates row-level security policy</code> 에러가 나면, 해당 테이블에 INSERT 정책이 없는 겁니다. 로그인한 사용자만 삽입 가능하게 하려면 Supabase 대시보드에서 아래 정책을 추가하세요.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="sql" data-theme="github-dark"><code data-language="sql" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">CREATE</span><span style="color:#F97583"> POLICY</span><span style="color:#9ECBFF"> "로그인한 사용자만 삽입 가능"</span></span>
<span data-line=""><span style="color:#F97583">ON</span><span style="color:#E1E4E8"> posts </span><span style="color:#F97583">FOR</span><span style="color:#F97583"> INSERT</span></span>
<span data-line=""><span style="color:#F97583">TO</span><span style="color:#E1E4E8"> authenticated</span></span>
<span data-line=""><span style="color:#F97583">WITH</span><span style="color:#F97583"> CHECK</span><span style="color:#E1E4E8"> (true);</span></span></code></pre></figure>
</blockquote>
<hr>
<h2 id="-5-인증-연동"><a class="anchor" aria-hidden="true" tabindex="-1" href="#-5-인증-연동">#</a>🔐 5. 인증 연동</h2>
<p>Supabase Auth는 이메일/비밀번호, OAuth(Google, GitHub 등)를 지원합니다. 세션을 모든 요청에서 자동으로 확인하려면 미들웨어에 설정을 추가해야 합니다.</p>
<p><strong>미들웨어 (<code>middleware.ts</code>)</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="typescript" data-theme="github-dark"><code data-language="typescript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { createServerClient } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@supabase/ssr'</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { NextResponse, </span><span style="color:#F97583">type</span><span style="color:#E1E4E8"> NextRequest } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'next/server'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> async</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> middleware</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">request</span><span style="color:#F97583">:</span><span style="color:#B392F0"> NextRequest</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">  let</span><span style="color:#E1E4E8"> supabaseResponse </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> NextResponse.</span><span style="color:#B392F0">next</span><span style="color:#E1E4E8">({ request })</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#79B8FF"> supabase</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> createServerClient</span><span style="color:#E1E4E8">(</span></span>
<span data-line=""><span style="color:#E1E4E8">    process.env.</span><span style="color:#79B8FF">NEXT_PUBLIC_SUPABASE_URL</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    process.env.</span><span style="color:#79B8FF">NEXT_PUBLIC_SUPABASE_ANON_KEY</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    {</span></span>
<span data-line=""><span style="color:#E1E4E8">      cookies: {</span></span>
<span data-line=""><span style="color:#B392F0">        getAll</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">          return</span><span style="color:#E1E4E8"> request.cookies.</span><span style="color:#B392F0">getAll</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">        },</span></span>
<span data-line=""><span style="color:#B392F0">        setAll</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">cookiesToSet</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">          cookiesToSet.</span><span style="color:#B392F0">forEach</span><span style="color:#E1E4E8">(({ </span><span style="color:#FFAB70">name</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">value</span><span style="color:#E1E4E8"> }) </span><span style="color:#F97583">=></span></span>
<span data-line=""><span style="color:#E1E4E8">            request.cookies.</span><span style="color:#B392F0">set</span><span style="color:#E1E4E8">(name, value)</span></span>
<span data-line=""><span style="color:#E1E4E8">          )</span></span>
<span data-line=""><span style="color:#E1E4E8">          supabaseResponse </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> NextResponse.</span><span style="color:#B392F0">next</span><span style="color:#E1E4E8">({ request })</span></span>
<span data-line=""><span style="color:#E1E4E8">          cookiesToSet.</span><span style="color:#B392F0">forEach</span><span style="color:#E1E4E8">(({ </span><span style="color:#FFAB70">name</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">value</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">options</span><span style="color:#E1E4E8"> }) </span><span style="color:#F97583">=></span></span>
<span data-line=""><span style="color:#E1E4E8">            supabaseResponse.cookies.</span><span style="color:#B392F0">set</span><span style="color:#E1E4E8">(name, value, options)</span></span>
<span data-line=""><span style="color:#E1E4E8">          )</span></span>
<span data-line=""><span style="color:#E1E4E8">        },</span></span>
<span data-line=""><span style="color:#E1E4E8">      },</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">  )</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> { </span><span style="color:#FFAB70">data</span><span style="color:#E1E4E8">: { </span><span style="color:#79B8FF">user</span><span style="color:#E1E4E8"> } } </span><span style="color:#F97583">=</span><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> supabase.auth.</span><span style="color:#B392F0">getUser</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">  // 로그인이 필요한 페이지 보호</span></span>
<span data-line=""><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">user </span><span style="color:#F97583">&#x26;&#x26;</span><span style="color:#E1E4E8"> request.nextUrl.pathname.</span><span style="color:#B392F0">startsWith</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'/dashboard'</span><span style="color:#E1E4E8">)) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> NextResponse.</span><span style="color:#B392F0">redirect</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">new</span><span style="color:#B392F0"> URL</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'/login'</span><span style="color:#E1E4E8">, request.url))</span></span>
<span data-line=""><span style="color:#E1E4E8">  }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> supabaseResponse</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#79B8FF"> config</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">  matcher: [</span><span style="color:#9ECBFF">'/((?!_next/static|_next/image|favicon.ico).*)'</span><span style="color:#E1E4E8">],</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>로그인 처리는 Server Action으로 간단하게 만들 수 있습니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="typescript" data-theme="github-dark"><code data-language="typescript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// app/login/actions.ts</span></span>
<span data-line=""><span style="color:#9ECBFF">'use server'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { createClient } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/utils/supabase/server'</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { redirect } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'next/navigation'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> async</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> login</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">formData</span><span style="color:#F97583">:</span><span style="color:#B392F0"> FormData</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#79B8FF"> supabase</span><span style="color:#F97583"> =</span><span style="color:#F97583"> await</span><span style="color:#B392F0"> createClient</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> { </span><span style="color:#79B8FF">error</span><span style="color:#E1E4E8"> } </span><span style="color:#F97583">=</span><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> supabase.auth.</span><span style="color:#B392F0">signInWithPassword</span><span style="color:#E1E4E8">({</span></span>
<span data-line=""><span style="color:#E1E4E8">    email: formData.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'email'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">as</span><span style="color:#79B8FF"> string</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    password: formData.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'password'</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">as</span><span style="color:#79B8FF"> string</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">  })</span></span>
<span data-line=""><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (error) </span><span style="color:#F97583">throw</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> Error</span><span style="color:#E1E4E8">(error.message)</span></span>
<span data-line=""><span style="color:#B392F0">  redirect</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'/dashboard'</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<blockquote>
<p><strong>⚠️ 트러블슈팅</strong> — 로그인 후 서버 컴포넌트에서 여전히 <code>user</code>가 <code>null</code>로 뜬다면, 미들웨어가 세션 쿠키를 갱신하지 못하고 있는 겁니다. <code>middleware.ts</code>가 프로젝트 루트에 있는지, <code>matcher</code> 패턴이 해당 경로를 포함하는지 먼저 확인하세요. 미들웨어가 실행되지 않으면 쿠키 갱신 자체가 안 됩니다.</p>
</blockquote>
<hr>
<h2 id="-6-실시간-기능-realtime"><a class="anchor" aria-hidden="true" tabindex="-1" href="#-6-실시간-기능-realtime">#</a>⚡ 6. 실시간 기능 (Realtime)</h2>
<p>Supabase는 PostgreSQL 변경 사항을 WebSocket으로 실시간 스트리밍해줍니다. 채팅, 알림, 실시간 업데이트 같은 기능에 쓸 수 있습니다.</p>
<p>이건 클라이언트 컴포넌트에서만 동작합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="typescript" data-theme="github-dark"><code data-language="typescript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#9ECBFF">'use client'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { createClient } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '@/utils/supabase/client'</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { useEffect, useState } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'react'</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">type</span><span style="color:#B392F0"> Message</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> { </span><span style="color:#FFAB70">id</span><span style="color:#F97583">:</span><span style="color:#79B8FF"> string</span><span style="color:#E1E4E8">; </span><span style="color:#FFAB70">content</span><span style="color:#F97583">:</span><span style="color:#79B8FF"> string</span><span style="color:#E1E4E8"> }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> ChatFeed</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">messages</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setMessages</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#B392F0">Message</span><span style="color:#E1E4E8">[]>([])</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#79B8FF"> supabase</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> createClient</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#B392F0">  useEffect</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#6A737D">    // 기존 메시지 초기 로드</span></span>
<span data-line=""><span style="color:#E1E4E8">    supabase</span></span>
<span data-line=""><span style="color:#E1E4E8">      .</span><span style="color:#B392F0">from</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'messages'</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">      .</span><span style="color:#B392F0">select</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'*'</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">      .</span><span style="color:#B392F0">order</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'created_at'</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">      .</span><span style="color:#B392F0">then</span><span style="color:#E1E4E8">(({ </span><span style="color:#FFAB70">data</span><span style="color:#E1E4E8"> }) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">        if</span><span style="color:#E1E4E8"> (data) </span><span style="color:#B392F0">setMessages</span><span style="color:#E1E4E8">(data)</span></span>
<span data-line=""><span style="color:#E1E4E8">      })</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 실시간 구독 등록</span></span>
<span data-line=""><span style="color:#F97583">    const</span><span style="color:#79B8FF"> channel</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> supabase</span></span>
<span data-line=""><span style="color:#E1E4E8">      .</span><span style="color:#B392F0">channel</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'messages'</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">      .</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'postgres_changes'</span><span style="color:#E1E4E8">, {</span></span>
<span data-line=""><span style="color:#E1E4E8">        event: </span><span style="color:#9ECBFF">'INSERT'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">        schema: </span><span style="color:#9ECBFF">'public'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">        table: </span><span style="color:#9ECBFF">'messages'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">      }, (</span><span style="color:#FFAB70">payload</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#B392F0">        setMessages</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">prev</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> [</span><span style="color:#F97583">...</span><span style="color:#E1E4E8">prev, payload.new </span><span style="color:#F97583">as</span><span style="color:#B392F0"> Message</span><span style="color:#E1E4E8">])</span></span>
<span data-line=""><span style="color:#E1E4E8">      })</span></span>
<span data-line=""><span style="color:#E1E4E8">      .</span><span style="color:#B392F0">subscribe</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> () </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> { supabase.</span><span style="color:#B392F0">removeChannel</span><span style="color:#E1E4E8">(channel) }</span></span>
<span data-line=""><span style="color:#E1E4E8">  }, [])</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#B392F0">ul</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">      {</span><span style="color:#FFAB70">messages</span><span style="color:#E1E4E8">.</span><span style="color:#FFAB70">map</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">msg</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#F97583">        &#x3C;</span><span style="color:#E1E4E8">li key</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{msg.id}</span><span style="color:#F97583">></span><span style="color:#E1E4E8">{msg.content}</span><span style="color:#F97583">&#x3C;/</span><span style="color:#E1E4E8">li</span><span style="color:#F97583">></span></span>
<span data-line=""><span style="color:#E1E4E8">      ))}</span></span>
<span data-line=""><span style="color:#F97583">    &#x3C;/</span><span style="color:#E1E4E8">ul</span><span style="color:#F97583">></span></span>
<span data-line=""><span style="color:#E1E4E8">  )</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>INSERT 이벤트가 발생할 때마다 상태가 자동으로 업데이트됩니다.</p>
<blockquote>
<p><strong>⚠️ 트러블슈팅</strong> — 구독을 설정했는데 이벤트가 아무것도 안 온다면, Supabase 대시보드에서 <strong>Database → Replication</strong> 메뉴를 확인하세요. 해당 테이블의 Realtime이 비활성화되어 있으면 이벤트가 전송되지 않습니다. 토글을 켜주면 바로 동작합니다.</p>
</blockquote>
<hr>
<h2 id="-마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#-마무리">#</a>✅ 마무리</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>담당</th>
</tr>
</thead>
<tbody>
<tr>
<td>UI 렌더링</td>
<td>Next.js Server/Client Component</td>
</tr>
<tr>
<td>DB 읽기</td>
<td>Supabase (서버 컴포넌트에서 직접)</td>
</tr>
<tr>
<td>DB 쓰기</td>
<td>Supabase (Server Action 통해)</td>
</tr>
<tr>
<td>인증</td>
<td>Supabase Auth + 미들웨어</td>
</tr>
<tr>
<td>실시간</td>
<td>Supabase Realtime (클라이언트 컴포넌트)</td>
</tr>
</tbody>
</table>
<p>처음엔 서버 클라이언트/브라우저 클라이언트를 분리하는 게 헷갈릴 수 있는데, 한 번 손에 익으면 별거 아닙니다. <strong>"서버에서 읽고, 서버에서 쓰고, 실시간만 클라이언트"</strong> 이렇게 기억해두면 됩니다.</p>
<p>이 글의 코드는 전부 실제로 동작하는 코드입니다. 위에서부터 순서대로 따라오면 오늘 안에 데이터를 읽고 쓰는 앱 하나를 완성할 수 있습니다. 백엔드 서버 없이 이 정도 기능을 갖춘 앱을 만들 수 있다는 게 아직도 조금 신기합니다. 사이드 프로젝트 빠르게 띄우기엔 이만한 조합이 없는 것 같습니다. 🙌</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[Next.js]]></category>
      <category><![CDATA[Supabase]]></category>
      <category><![CDATA[App Router]]></category>
      <category><![CDATA[풀스택]]></category>
      <category><![CDATA[백엔드]]></category>
    </item>

    <item>
      <title><![CDATA[에어드랍이 뭔지 몰라도 괜찮아 — 공짜 코인 받는 법, 처음부터 설명해줌]]></title>
      <link>https://www.stragos.xyz/posts/crypto-airdrop-beginner-guide</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/crypto-airdrop-beginner-guide</guid>
      <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[에어드랍이 뭔지, 어떻게 받는지, 왜 주는지 — 크립토 9년 경력자가 초보자 눈높이에서 2026년 기준으로 싹 정리했습니다. 묻지도 따지지도 않고 그냥 받을 수 있습니다.]]></description>
      <content:encoded><![CDATA[<p>나는 크립토를 9년째 하고 있다.</p>
<p>ICO 붐도 겪었고, DeFi 여름도 살아남았다. NFT 광풍도 봤고, 루나 폭락도 현장에서 지켜봤다.</p>
<p>그러다 보니 주변에서 "코인 뭐 하면 되냐"는 질문을 정말 많이 받는다.</p>
<p>몇 년 전에는 지인이 카톡으로 이런 메시지를 보내왔다.</p>
<p>"야, 유니스왑에서 갑자기 내 지갑에 400달러가 들어와 있는데 이게 뭐야?"</p>
<p>그 친구는 DEX가 뭔지도 몰랐다. DeFi라는 말도 들어본 적 없었다. 그냥 내가 한번 써보라고 해서 클릭 몇 번 해본 게 전부였다.</p>
<p>근데 그게 에어드랍이었다.</p>
<p>유니스왑이 2020년 9월 UNI 토큰을 출시하면서, 과거에 자기네 서비스를 한 번이라도 써봤던 모든 지갑에 400 UNI를 그냥 뿌렸다. 발표 당일 UNI 가격은 약 3달러. 400개면 1,200달러다. 최고가(44달러)에는 17,600달러였다.</p>
<p>그 친구는 클릭 몇 번으로 1,200달러를 받았다.</p>
<p>이게 에어드랍이다.</p>
<hr>
<h2 id="에어드랍이-뭔지부터"><a class="anchor" aria-hidden="true" tabindex="-1" href="#에어드랍이-뭔지부터">#</a>에어드랍이 뭔지부터</h2>
<p>에어드랍(Airdrop)은 블록체인 프로젝트가 조건을 충족한 사용자 지갑에 토큰을 무료로 배포하는 이벤트다.</p>
<p>말 그대로 하늘에서 코인이 떨어지는 것이다.</p>
<p>대신 아무한테나 주는 게 아니다. <strong>자기네 서비스를 써본 사람</strong>, 혹은 <strong>일정 조건을 만족한 사람</strong>에게 준다.</p>
<p>왜 줄까?</p>
<p>프로젝트 입장에서 초기 사용자는 무엇보다 귀하다. 아직 인지도도 없고 유동성도 없을 때 먼저 써준 사람들이 있어야 서비스가 돌아간다. 그 보답으로 토큰을 나눠주는 것이다.</p>
<p>동시에 토큰을 받은 사람들은 자연스럽게 그 프로젝트의 주주가 된다. 토큰 가격이 올라야 본인도 이익이니 자연스럽게 홍보하고 응원하게 된다. 프로젝트와 사용자가 함께 이익을 보는 구조다. 윈-윈이다.</p>
<hr>
<h2 id="역사가-증명한-에어드랍들--이-정도-규모였다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#역사가-증명한-에어드랍들--이-정도-규모였다">#</a>역사가 증명한 에어드랍들 — 이 정도 규모였다</h2>
<p>이게 그냥 소소한 이벤트가 아니다. 9년 동안 봐온 에어드랍들 중 임팩트가 컸던 것들만 골라봤다.</p>
<h3 id="uniswap--uni-2020년-9월"><a class="anchor" aria-hidden="true" tabindex="-1" href="#uniswap--uni-2020년-9월">#</a>Uniswap — UNI (2020년 9월)</h3>
<p>DEX의 대명사 유니스왑이 첫 토큰을 발행하면서 과거 사용자 전원에게 <strong>400 UNI</strong>를 지급했다.</p>
<p>배포 당일 가격 기준 <strong>약 1,200달러</strong>, 최고가 기준 <strong>약 17,600달러</strong>였다.</p>
<p>조건은 단순했다. 2020년 9월 1일 이전에 유니스왑에서 스왑을 한 번이라도 해봤을 것. 트랜잭션 하나로 1,200달러를 받았다.</p>
<h3 id="dydx--dydx-2021년-8월"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dydx--dydx-2021년-8월">#</a>dYdX — DYDX (2021년 8월)</h3>
<p>탈중앙화 영구선물 거래소 dYdX가 토큰을 발행하면서 과거 트레이더들에게 대규모 에어드랍을 진행했다.</p>
<p>거래량에 따라 달랐는데, 평균적으로 <strong>수백에서 수천 달러</strong> 수준이었고 활발하게 거래했던 사용자 중에는 수만 달러를 받은 케이스도 있었다.</p>
<h3 id="optimism--op-2022년-5월-1차"><a class="anchor" aria-hidden="true" tabindex="-1" href="#optimism--op-2022년-5월-1차">#</a>Optimism — OP (2022년 5월, 1차)</h3>
<p>이더리움 레이어2 체인 옵티미즘의 1차 에어드랍. 대상 기준이 복잡했는데, DAO 참여자, L2 사용자, Gitcoin 기부자 등을 포함했다.</p>
<p>지갑당 최소 <strong>264 OP</strong>에서 시작했고 당시 가격 기준 <strong>약 600달러</strong> 수준이었다. 이후 2차, 3차 에어드랍이 추가로 진행됐다.</p>
<h3 id="arbitrum--arb-2023년-3월"><a class="anchor" aria-hidden="true" tabindex="-1" href="#arbitrum--arb-2023년-3월">#</a>Arbitrum — ARB (2023년 3월)</h3>
<p>이더리움 레이어2 최강자 아비트럼의 에어드랍. 역대 레이어2 에어드랍 중 가장 컸다.</p>
<p>625 ARB부터 시작해서 활동량에 따라 최대 10,250 ARB까지 지급됐다. 발행 당시 약 1.2달러였으니 평균적으로 <strong>약 750달러-12,000달러</strong> 범위였다.</p>
<p>아비트럼에서 스왑하고, 브릿지 써보고, 유동성 공급해본 사람이면 대부분 받았다. 단 몇 달러 가스비에 얼마가 돌아온 셈이다.</p>
<h3 id="hyperliquid--hype-2024년-11월"><a class="anchor" aria-hidden="true" tabindex="-1" href="#hyperliquid--hype-2024년-11월">#</a>Hyperliquid — HYPE (2024년 11월)</h3>
<p>2024년 최고의 에어드랍으로 꼽힌다.</p>
<p>Hyperliquid 퍼포인트(Points) 시스템 참여자들에게 HYPE 토큰을 배분했는데, 초기 참여자 중에는 <strong>수만 달러에서 수십만 달러</strong>를 받은 사람도 나왔다. HYPE 가격이 출시 후 급등했기 때문이다.</p>
<p>이게 포인트 시스템 에어드랍이 얼마나 강력한지를 보여준 사례다.</p>
<h3 id="blur--blur-2023년-2월"><a class="anchor" aria-hidden="true" tabindex="-1" href="#blur--blur-2023년-2월">#</a>Blur — BLUR (2023년 2월)</h3>
<p>NFT 마켓플레이스 Blur가 NFT 트레이더들에게 토큰을 배포했다.</p>
<p>오픈씨보다 수수료가 낮아서 트레이더들이 많이 쓰던 곳이었는데, 그 사용 이력을 기반으로 에어드랍을 진행했다. 활발하게 거래했던 트레이더들 중 일부는 <strong>수천 달러 이상</strong>을 받았다.</p>
<hr>
<h2 id="에어드랍-종류-네-가지면-끝"><a class="anchor" aria-hidden="true" tabindex="-1" href="#에어드랍-종류-네-가지면-끝">#</a>에어드랍 종류, 네 가지면 끝</h2>
<p><img src="/images/crypto/airdrop-types.svg" alt="에어드랍 종류 한눈에 보기" loading="lazy" decoding="async"></p>
<h3 id="소급형-retroactive"><a class="anchor" aria-hidden="true" tabindex="-1" href="#소급형-retroactive">#</a>소급형 (Retroactive)</h3>
<p>가장 대표적인 형태다. 프로젝트가 서비스를 먼저 운영하다가, 어느 시점에 토큰을 발행하면서 <strong>과거에 써본 사람들에게 소급 지급</strong>한다.</p>
<p>미리 공지 없이 진행되는 경우가 대부분이다. 그래서 받을 줄 몰랐던 사람이 갑자기 지갑에 코인이 들어와 있는 경험을 하게 된다.</p>
<p>위에서 소개한 유니스왑, 아비트럼이 다 이 방식이다.</p>
<p>이 방식의 핵심은, <strong>에어드랍 받으려고 특별히 뭔가를 하지 않아도 된다</strong>는 점이다. 그냥 서비스를 진짜 써보면 된다.</p>
<h3 id="포인트형-points-system"><a class="anchor" aria-hidden="true" tabindex="-1" href="#포인트형-points-system">#</a>포인트형 (Points System)</h3>
<p>2024년 이후로 가장 많이 쓰이는 방식이다.</p>
<p>서비스를 쓸수록 포인트가 쌓이고, 나중에 TGE(Token Generation Event, 토큰 발행)가 되면 포인트 비율에 따라 토큰으로 전환해 준다.</p>
<p>MetaMask Rewards, Hyperliquid 포인트 시스템이 이 방식이었다.</p>
<p>포인트를 더 많이 쌓으려면 더 많이, 더 자주 써야 한다. <strong>꾸준함이 핵심</strong>이다.</p>
<h3 id="태스크형-task-based"><a class="anchor" aria-hidden="true" tabindex="-1" href="#태스크형-task-based">#</a>태스크형 (Task-Based)</h3>
<p>트위터 팔로우, 리트윗, 디스코드 참여, 설문 응답 같은 간단한 미션을 수행하면 받는 방식이다.</p>
<p>주로 소규모 신규 프로젝트들이 쓴다. 조건이 가장 쉬운 대신, 받는 양도 적고 가치가 낮은 경우가 많다. 그리고 이쪽에 사기가 가장 많다. 각별히 주의가 필요하다.</p>
<h3 id="유동성형-liquidity-mining"><a class="anchor" aria-hidden="true" tabindex="-1" href="#유동성형-liquidity-mining">#</a>유동성형 (Liquidity Mining)</h3>
<p>DEX(탈중앙화 거래소)에 유동성을 공급하면 수수료 수익과 함께 토큰도 보상으로 받는 방식이다.</p>
<p>Meteora 시즌 2가 이 방식인데, 내가 공급한 유동성에서 발생한 수수료가 많을수록 더 많은 MET 토큰을 받는다.</p>
<p>수익은 가장 높을 수 있지만 임시손실(Impermanent Loss) 개념을 이해해야 하고 리스크도 있다. <strong>초보에겐 추천하지 않는다.</strong></p>
<hr>
<h2 id="2026년-현재-주목할-프로젝트들"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2026년-현재-주목할-프로젝트들">#</a>2026년 현재 주목할 프로젝트들</h2>
<div class="callout-author">
📝 <strong>글쓴이 기준 정리입니다.</strong><br>
아래 내용은 2026년 4월 기준으로 내가 직접 관심 있게 지켜보고 있는 프로젝트들입니다. 에어드랍 확정이 아니며, 언제 줄지·얼마나 줄지·줄지 안 줄지 모두 미정입니다. 크립토 9년 했지만 미래는 모릅니다. 참고 정보로만 활용하세요.
</div>
<h3 id="metamask-mask-토큰--미확정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#metamask-mask-토큰--미확정">#</a>MetaMask (MASK 토큰) <span class="badge-unconfirmed">🔴 미확정</span></h3>
<p>크립토 하는 사람이라면 모르는 사람이 없는 지갑. 공동창업자가 공식적으로 "예상보다 빨리 나올 것"이라고 언급한 적 있다.</p>
<p>MetaMask Rewards 포인트 시스템이 이미 돌아가고 있다. MetaMask로 스왑, 브릿지, 영구선물 거래를 하면 포인트가 쌓인다.</p>
<p>단, MetaMask는 이미 Linea(자체 레이어2) 생태계를 통한 에어드랍을 한 차례 진행한 바 있다. 앞으로 MASK 토큰이 나올지, 어떤 기준으로 줄지는 아직 공식 발표가 없다.</p>
<p>이미 MetaMask를 쓰고 있다면? <strong>그냥 계속 쓰면 된다.</strong> 특별히 더 할 게 없다.</p>
<h3 id="base-네트워크--미확정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#base-네트워크--미확정">#</a>Base 네트워크 <span class="badge-unconfirmed">🔴 미확정</span></h3>
<p>코인베이스가 만든 이더리움 레이어2 체인. 팀이 공식적으로 "베이스 네트워크 토큰을 검토 중"이라고 밝혔다. 확정은 아니다.</p>
<p>코인베이스가 상장사라 규제 이슈가 복잡하게 엮여있어서 언제, 어떤 형태로 나올지 예측하기 어렵다.</p>
<p>그래도 Base 위에서 활동하는 건 나쁠 게 없다. Aerodrome에서 스왑하거나, 베이스 도메인(.base.eth) 이름을 민팅하거나, Aave나 Uniswap의 Base 버전을 써보면 된다.</p>
<h3 id="polymarket--미확정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#polymarket--미확정">#</a>Polymarket <span class="badge-unconfirmed">🔴 미확정</span></h3>
<p>예측 시장 플랫폼. 2026년 2월에 <code>$POLY</code> 상표를 등록했다. 토큰 발행이 임박했다는 신호로 읽히지만, 공식 발표는 아직 없다.</p>
<p>대선, 스포츠, 경제 지표 등 다양한 예측 마켓에서 꾸준히 거래하면서 X(트위터) 계정을 연동해두면 참여 기록이 남는다.</p>
<h3 id="hyperliquid-2차-배분--미확정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#hyperliquid-2차-배분--미확정">#</a>Hyperliquid (2차 배분) <span class="badge-unconfirmed">🔴 미확정</span></h3>
<p>이미 2024년에 HYPE 토큰 에어드랍을 했던 Hyperliquid. 전체 토큰의 38.88%가 아직 미래 배분용으로 묶여 있다.</p>
<p>언제, 어떻게 배분할지는 팀만 알고 있다. 레버리지 트레이딩, 스테이킹, 유동성 공급으로 계속 활동 기록을 쌓아두는 것 정도가 할 수 있는 전부다.</p>
<h3 id="layerzero-2차-시즌--미확정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#layerzero-2차-시즌--미확정">#</a>LayerZero (2차 시즌) <span class="badge-unconfirmed">🔴 미확정</span></h3>
<p>크로스체인 메시징 프로토콜. 전체 ZRO 공급량의 약 30%가 아직 미래 배분용으로 남아 있다.</p>
<p>CEO가 직접 에어드랍 파밍을 하지 말라고 경고했는데, 역설적으로 이건 다음 시즌을 준비 중이라는 신호로 읽힌다.</p>
<p>Stargate 브릿지를 이용하거나, LayerZero를 지원하는 dApp을 실제로 사용하면 된다. 단, 진짜 사용 목적 없이 반복만 하면 시빌로 걸린다.</p>
<h3 id="variational--미확정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#variational--미확정">#</a>Variational <span class="badge-unconfirmed">🔴 미확정</span></h3>
<p>영구선물 DEX. 공식적으로 "전체 공급량의 50%를 커뮤니티에 배분"하겠다고 밝혔다. 퍼포인트 프로그램이 진행 중이다.</p>
<p>퍼페추얼 거래를 하거나 친구를 초대하면 포인트가 쌓인다. TGE 시점은 미발표.</p>
<h3 id="pacifica--미확정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#pacifica--미확정">#</a>Pacifica <span class="badge-unconfirmed">🔴 미확정</span></h3>
<p>솔라나 기반 퍼페추얼 DEX. 오프체인 매칭 + 온체인 결제 방식으로 속도와 탈중앙화를 동시에 잡은 구조다.</p>
<p>창업자는 전 FTX COO 컨스탄스 왕(Constance Wang). FTX 이후 이름을 버리지 않고 새로 시작했다는 점에서 커뮤니티 신뢰를 얻었다. Hyperliquid처럼 VC 투자 없이 자체 자금으로만 운영 중이다.</p>
<p>2025년 9월 메인넷 출시 이후 솔라나 퍼프 DEX 1위 자리를 유지하고 있다. 2026년 1월 21일 누적 거래량 1,000억 달러를 돌파했으며, 2026년 4월 기준 일 거래량은 10억 달러 이상이다. TVL은 약 3,800만~4,000만 달러 수준이다.</p>
<p><strong>포인트 시스템</strong>: 2025년 9월 4일부터 운영 중이며, 매주 목요일 00:00 UTC 스냅샷 후 24시간 이내에 배분된다. 포인트는 해당 주 전체 플랫폼 거래량 대비 내 거래량 비율로 계산된다. 초기 주당 50만 포인트였다가 2025년 10월 30일 20배 증가해 현재 <strong>주당 1,000만 포인트</strong>를 배분한다. 시즌 1이 완료됐고 현재 시즌 2(Boost Windows)가 진행 중이다. Boost Windows는 매주 특정 자산군을 선정해 해당 기간 포인트 효율을 높이는 방식이며, 약 1주 전 공식 X와 디스코드를 통해 사전 공지된다. 5일 이상 연속 거래 시 최대 23% 추가 포인트가 붙는 Streak 보너스도 있다. 레퍼럴은 누적 거래량 $10,000 달성 후 활성화된다.</p>
<p>워시 트레이딩이 탐지되면 포인트를 소급 삭감한다. 멀티 지갑은 이점이 없다.</p>
<p>단, 현재 <strong>초대 코드 기반 클로즈드 베타</strong>로 운영 중이다. X(트위터)나 디스코드 커뮤니티에서 초대 코드를 구해야 참여할 수 있다.</p>
<p>공식 토큰이나 TGE 일정은 아직 발표되지 않았다.</p>
<h3 id="dango--미확정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dango--미확정">#</a>Dango <span class="badge-unconfirmed">🔴 미확정</span></h3>
<p>퍼페추얼 거래에 특화된 레이어1 앱체인. "Grug" 실행 환경 기반으로 설계됐으며, 약 20개 노드로 구성된 허가형 검증 구조를 취하고 있다.</p>
<p>일반 DEX와 차별화된 기능들이 있다. 마진 계정(Margin Accounts)으로 포지션 간 증거금을 공유하고, 스마트 계정 기능으로 생체인식(지문, 페이스아이디)과 패스키 로그인을 지원한다. 크론 잡(Cron Jobs)으로 자동화 전략도 가능하다. 오더북은 CLOB(중앙화형 한도 주문서) 방식이다.</p>
<p>토큰 티커는 DNG, 총 공급량 3만 개로 고정되어 있다. 수수료는 USDC로 수납해 DNG를 매입 후 소각하는 디플레이션 구조다. 시드 투자는 Lemniscap, Hack VC, Delphi Labs가 참여했으며 총 360만 달러 규모다.</p>
<p><strong>포인트 프로그램</strong>: 4월 13일 론칭 예정이었다. 38주간 운영하며 Q4 2026 네이티브 토큰 출시와 함께 전체 공급량 약 50%를 에어드랍할 계획이었다. 매주 100만 포인트를 배분하며 퍼프 거래(3/4), DLP 예치(1/4), 레퍼럴로 나뉜다. OAT 보유자는 EVM 지갑 연동 후 4주간 최대 400% 부스트를 받을 수 있고, 거래량에 따라 루트박스(브론즈 $25k, 실버 $100k, 골드 $250k, 크리스탈 $500k)를 얻는 구조다.</p>
<div class="callout-author" style="border-left-color:#ef4444; background:#fef2f2;">
⚠️ <strong>2026년 4월 13일 보안 사고 발생</strong><br><br>
포인트 프로그램 론칭 당일, 보험 펀드 로직에서 취약점이 발견돼 공격자가 퍼프 컨트랙트의 USDC 담보를 탈취했다. 기부 금액에 대한 양수 확인이 없어 음수 금액 기부가 가능했던 버그다.<br><br>
브릿지 속도 제한 덕분에 <strong>$410,010 USDC만 이더리움으로 빠져나갔고</strong>, 나머지 <strong>$1,490,012는 Dango 체인에 묶여 회수 가능</strong>한 상태다. 취약 로직은 즉시 제거됐으며 주문 체결·청산·PnL 정산 등 핵심 거래 시스템에는 영향이 없다. SEAL_911, Circle, 주요 거래소에 통보됐고 피해 사용자 전액 보상 예정이다.<br><br>
이에 따라 <strong>포인트 프로그램은 추후로 연기됐다.</strong> 팀이 사태를 수습 중이므로 진행 상황을 지켜봐야 한다.
</div>
<h3 id="opensea--미확정--지연"><a class="anchor" aria-hidden="true" tabindex="-1" href="#opensea--미확정--지연">#</a>OpenSea <span class="badge-unconfirmed">🔴 미확정 · 지연</span></h3>
<p>NFT 마켓플레이스의 원조. SEA 토큰 발행을 공식 예고했으나 2026년 Q1 출시 약속을 번복하고 시장 상황을 이유로 연기했다.</p>
<p>NFT 거래 이력이 긴 계정, Pudgy Penguins 같은 주요 컬렉션 보유자가 유리할 것으로 예측된다. 언제 나올지 여전히 미정이다.</p>
<hr>
<h2 id="최근-완료된-에어드랍들--어떻게-됐나"><a class="anchor" aria-hidden="true" tabindex="-1" href="#최근-완료된-에어드랍들--어떻게-됐나">#</a>최근 완료된 에어드랍들 — 어떻게 됐나</h2>
<p>에어드랍이 항상 성공하는 건 아니다. 최근 완료된 것들을 보면 명암이 뚜렷하다.</p>
<h3 id="backpack--bp-2026년-3월"><a class="anchor" aria-hidden="true" tabindex="-1" href="#backpack--bp-2026년-3월">#</a>Backpack — BP (2026년 3월)</h3>
<p>솔라나 기반 거래소 Backpack이 2026년 3월 23일 BP 토큰을 발행했다. 전체 공급량의 25%(2억 5천만 BP)를 커뮤니티에 배분했는데, 포인트 보유자(24%)와 Mad Lads NFT 홀더(1%)가 대상이었다.</p>
<p>상장 직후 가격이 크게 뛰었지만 이후 빠르게 조정됐다.</p>
<p>커뮤니티 반응은 엇갈렸다. 고거래량 유저에게 너무 치우쳐 있다는 불만, 시빌 필터링이 과도했다는 지적이 많았다.</p>
<h3 id="edgex--edge-20252026년"><a class="anchor" aria-hidden="true" tabindex="-1" href="#edgex--edge-20252026년">#</a>EdgeX — EDGE (2025~2026년)</h3>
<p>이더리움 레이어2(StarkEx) 기반 영구선물 DEX. Amber Group이 인큐베이팅한 프로젝트로, Hyperliquid에 이어 수수료 기준 2위 퍼프 DEX였다.</p>
<p>전체 공급량의 40%(4억 EDGE)를 트레이더, 유동성 공급자, 얼리 어답터에게 배분했다. 거래량, LP 포지션, 파워 트레이더 여부 등 6개 티어로 나눠서 지급했다.</p>
<h3 id="based"><a class="anchor" aria-hidden="true" tabindex="-1" href="#based">#</a>Based</h3>
<p>Hyperliquid L1 위에서 운영되는 Web3 슈퍼앱이다. 퍼페추얼·스팟 거래, 밈 콘텐츠 예측 마켓(Internet Opinion Markets), 실물 Visa 카드(포트폴리오 잔고로 결제, 최대 8% 캐시백), 런치풀, AI 트레이딩 에이전트 등 여러 기능을 하나의 앱에 묶었다. 출시 8개월 만에 누적 거래량 400억 달러, 가입자 10만 명을 넘겼고 2026년 2월 Pantera Capital 주도로 1,150만 달러 시리즈 A 투자를 유치했다.</p>
<p><strong>토큰</strong>: $BASED, 총 공급량 10억 개 고정.</p>
<p><strong>에어드랍 경과</strong>:</p>
<ul>
<li><strong>시즌 1·2 제네시스</strong>: 전체 공급량의 23.5%(2억 3,500만 개)를 2026년 3월 30일 TGE 시점에 Hyperliquid Core 지갑으로 자동 지급했다. XP(시즌 1)·Gold(시즌 2)·$PUP·BasedPal NFT 보유자가 대상이었다. 2월 8일 이전 약관 동의 기록이 있어야 수령할 수 있었다.</li>
<li><strong>시즌 3 (Diamonds)</strong>: 2026년 1월 5일~5월 4일 진행 중이며 전체 공급량의 5%(5,000만 개) 배분 예정이다. 퍼프·스팟 거래량에 비례해 Diamonds가 적립되고, <strong>2026년 5월 11일 클레임</strong> 가능하다(베스팅 없음).</li>
</ul>
<p><strong>스테이킹</strong>: TGE 당일 론칭. OG / Based / Normie 3개 티어로 나뉘며 선착순으로 상한이 채워진다(OG·Based 티어 각 4,000만 개 한도). APR은 동적이라 초기 참여자일수록 유리하다. 3일 언스테이킹 대기 기간이 있다. 스테이킹 시 거래 수수료 할인, Visa 카드 한도 상향, 런치풀 접근 등 혜택이 추가된다.</p>
<p><strong>토큰 가격</strong>: 출시 당일(3월 30일) $0.1549로 ATH를 찍은 뒤 4월 초 $0.05 근처까지 조정됐다. 투자자·기여자 물량은 2027년 3월까지 락업된다.</p>
<p>시즌 3가 아직 진행 중이므로 Based에서 거래 활동을 이어가면 5월 클레임 물량을 추가로 받을 수 있다.</p>
<h3 id="starknet--strk-2024년-2월"><a class="anchor" aria-hidden="true" tabindex="-1" href="#starknet--strk-2024년-2월">#</a>Starknet — STRK (2024년 2월)</h3>
<p>출시 가격 5달러였던 STRK는 에어드랍 이후 <strong>91% 이상 폭락</strong>했다.</p>
<p>이유는 두 가지였다.</p>
<p>첫째, 대규모 고거래량 클레이머들이 상장 즉시 전량 매도했다. 둘째, 시빌 필터링이 허술해서 GitHub 계정을 조작한 봇 어드레스들이 대규모로 청구했다.</p>
<p>에어드랍 이후 활성 주소 수가 38만 개에서 8,300개로 98%나 급감했다.</p>
<p>실제 사용자보다 토큰 목적의 참여자가 훨씬 많았다는 방증이다.</p>
<h3 id="layerzero--zro-2024년"><a class="anchor" aria-hidden="true" tabindex="-1" href="#layerzero--zro-2024년">#</a>LayerZero — ZRO (2024년)</h3>
<p>ZRO는 기술적으로는 성공했지만 방식이 논란이었다.</p>
<p>에어드랍을 받으려면 토큰당 <strong>0.1달러를 이더리움 Protocol Guild에 기부</strong>해야 했다. 완전 무료가 아니었던 것이다.</p>
<p>"이게 에어드랍이냐, ICO냐"는 논쟁이 붙었고 커뮤니티 반발이 심했다.</p>
<p>그래도 가격 방어는 상대적으로 잘 됐다. STRK나 ZK에 비해서는 양호했는데, 강력한 시빌 필터링 덕분이었다.</p>
<hr>
<h2 id="에어드랍이-항상-좋은-건-아니다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#에어드랍이-항상-좋은-건-아니다">#</a>에어드랍이 항상 좋은 건 아니다</h2>
<p>솔직히 말하겠다.</p>
<p>에어드랍 받았다고 무조건 돈을 버는 시대는 지났다.</p>
<p>2020~2022년에는 에어드랍 받으면 그냥 돈이었다. 유니스왑처럼 받자마자 최소 수백 달러였고, 뭘 받아도 오르는 시절이었다. 그때는 참여만 해도 됐다.</p>
<p>지금은 다르다.</p>
<p><strong>ZKsync(ZK)는</strong> 2024년 에어드랍 당일 수백만 개 주소에 토큰을 뿌렸는데, 상장 직후부터 지속적으로 하락해서 장기 보유자 대부분이 손해를 봤다.</p>
<p><strong>Starknet(STRK)은</strong> 이미 위에서 말했다. 91% 폭락.</p>
<p><strong>Backpack(BP)는</strong> 많은 기대를 받았지만 상당수 유저들이 "이 정도 받으려고 몇 달을 썼나" 싶은 반응이 나왔다.</p>
<p>왜 이렇게 됐을까.</p>
<p>이유는 간단하다.</p>
<p><strong>에어드랍 파밍 인구가 너무 많아졌다.</strong> 수백만 명이 토큰만 받으려고 서비스를 쓰는 척하면서, 받자마자 전량 매도한다. 프로젝트 입장에서는 진짜 사용자가 아닌 사람들한테 토큰을 뿌린 셈이다.</p>
<p>그래서 요즘 프로젝트들은 시빌 필터링을 강화하고, 배분량을 줄이고, 조건을 까다롭게 만들고 있다.</p>
<p>그렇다면 에어드랍을 하면 안 되냐? 그건 아니다.</p>
<p>다만 기대치를 조정해야 한다. 에어드랍은 <strong>서비스를 써보는 김에 혹시 받을 수도 있는 보너스</strong>로 생각하는 게 맞다.</p>
<p>에어드랍 자체를 목적으로 삼으면 실망이 크다. 진짜 쓸만한 서비스를 쓰다 보면 어느 날 지갑에 코인이 들어와 있는 것이다.</p>
<hr>
<h2 id="어떻게-시작하나--5단계"><a class="anchor" aria-hidden="true" tabindex="-1" href="#어떻게-시작하나--5단계">#</a>어떻게 시작하나 — 5단계</h2>
<p><img src="/images/crypto/airdrop-steps.svg" alt="에어드랍 참여 5단계" loading="lazy" decoding="async"></p>
<h3 id="1단계-지갑-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1단계-지갑-만들기">#</a>1단계: 지갑 만들기</h3>
<p>거래소 계정(업비트, 빗썸, 바이낸스)으로는 에어드랍을 받을 수 없다. <strong>직접 키를 관리하는 셀프커스터디 지갑</strong>이 있어야 한다.</p>
<ul>
<li><strong>EVM 계열 (이더리움, Base, Arbitrum 등)</strong>: MetaMask</li>
<li><strong>솔라나 계열</strong>: Phantom</li>
</ul>
<p>둘 다 크롬 확장 프로그램으로 설치하면 된다.</p>
<p>처음 설치할 때 <strong>시드 구문(12-24개 영어 단어)</strong> 이 나온다. 이게 내 지갑의 열쇠다. 절대 디지털 기기에 저장하지 말고, 종이에 적어서 물리적으로 보관한다. 이걸 잃어버리거나 노출되면 지갑 안의 모든 자산이 사라진다.</p>
<h3 id="2단계-리스트-확인하기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2단계-리스트-확인하기">#</a>2단계: 리스트 확인하기</h3>
<ul>
<li><strong>airdrops.io</strong> — 검증된 에어드랍 모음</li>
<li><strong>CoinGecko</strong> — 업커밍 에어드랍 리스트</li>
<li><strong>dropstab.com</strong> — 활성화된 에어드랍 및 포인트 캠페인 정리</li>
<li>각 프로젝트 공식 트위터, 디스코드</li>
</ul>
<p>단, 공식 채널이 아닌 곳에서 온 에어드랍 링크는 <strong>절대 클릭하지 않는다.</strong></p>
<h3 id="3단계-프로토콜-써보기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3단계-프로토콜-써보기">#</a>3단계: 프로토콜 써보기</h3>
<p>찾은 프로젝트의 서비스를 실제로 사용한다. 어떤 행동이 에어드랍 대상이 될지는 프로젝트마다 다르지만 보통 이런 것들이다.</p>
<ul>
<li>DEX에서 토큰 스왑 (예: Uniswap에서 ETH → USDC 교환)</li>
<li>브릿지로 다른 체인에 자산 이동 (예: 이더리움 → Base로 ETH 이동)</li>
<li>스테이킹 또는 유동성 공급</li>
<li>테스트넷 참여</li>
<li>거버넌스 투표</li>
</ul>
<p>금액이 적어도 괜찮다. 얼마를 거래하느냐보다 <strong>거래했다는 온체인 기록</strong>이 더 중요한 경우가 많다.</p>
<h3 id="4단계-꾸준히-반복하기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4단계-꾸준히-반복하기">#</a>4단계: 꾸준히 반복하기</h3>
<p>2026년 에어드랍 트렌드에서 가장 중요한 키워드는 <strong>일관성</strong>이다.</p>
<p>하루에 대규모로 몰아서 하는 것보다, 주 1~2회씩 꾸준하게 쓰는 사람이 훨씬 유리하다.</p>
<p>프로젝트 팀들이 활동 패턴을 분석해서 진짜 사용자와 봇 계정을 구분하기 때문이다. Linea 에어드랍에서는 전체 신청자 130만 명 중 약 51만 7천 명(40%)이 봇 의심으로 탈락했다.</p>
<p>하나의 지갑으로, 자연스러운 패턴으로, 꾸준히. 이게 전부다.</p>
<h3 id="5단계-클레임하기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5단계-클레임하기">#</a>5단계: 클레임하기</h3>
<p>에어드랍이 발표되면 공식 사이트에서 지갑을 연결하고 클레임 버튼을 누르면 된다.</p>
<p>반드시 <strong>공식 트위터, 디스코드에서 확인한 링크</strong>만 사용한다. 피싱 사이트는 공식 사이트와 픽셀 단위로 똑같이 생겨있고, 지갑을 연결하는 순간 안에 있는 코인을 전부 빼간다.</p>
<hr>
<h2 id="절대-하면-안-되는-것들"><a class="anchor" aria-hidden="true" tabindex="-1" href="#절대-하면-안-되는-것들">#</a>절대 하면 안 되는 것들</h2>
<p>에어드랍에서 실수하면 내 코인이 통째로 사라진다. 9년 동안 이걸로 피해 본 사람을 수도 없이 봤다.</p>
<p><strong>코인을 먼저 보내라고 하면 100% 사기다.</strong></p>
<p>진짜 에어드랍은 절대 먼저 뭔가를 요구하지 않는다. "10 USDT 보내면 100 토큰을 돌려준다"는 건 세상에 없다.</p>
<p><strong>"지금 당장 클레임하지 않으면 없어져요"</strong></p>
<p>급박감을 조성해서 판단력을 흐리는 수법이다. 진짜 에어드랍의 클레임 기간은 보통 몇 달이다. 급할 이유가 없다.</p>
<p><strong>여러 지갑으로 나눠서 하면 다 막힌다.</strong></p>
<p>AI 기반 온체인 분석 도구(Trusta Labs 등)가 같은 IP, 같은 행동 패턴, 같은 타이밍을 전부 잡아낸다. Linea 에어드랍 때 130만 명 중 40%가 이렇게 탈락했다.</p>
<p><strong>시드 구문은 절대 입력하지 않는다.</strong></p>
<p>어떤 사이트도, 어떤 사람도, 어떤 이유로도 시드 구문을 물어볼 이유가 없다. 물어본다면 사기다.</p>
<hr>
<h2 id="솔직하게-말할게"><a class="anchor" aria-hidden="true" tabindex="-1" href="#솔직하게-말할게">#</a>솔직하게 말할게</h2>
<p>에어드랍이 무조건 돈이 되는 건 아니다.</p>
<p>열 개를 해서 실제로 의미 있는 금액이 되는 건 한두 개 정도다. 나머지는 시간 낭비처럼 느껴질 수도 있다.</p>
<p>위에서 소개한 2026년 프로젝트들도 마찬가지다. 에어드랍이 나올지 안 나올지, 나온다면 얼마나 줄지, 언제 줄지 — 아무것도 확정된 게 없다.</p>
<p>하지만 반대로 생각해보면, 지금 MetaMask로 스왑 한 번 해보는 건 10분이면 되고 비용은 가스비 몇 달러다. Base에서 유니스왑 써보는 것도 마찬가지다.</p>
<p>그 몇 달러와 10분이 나중에 수백, 수천 달러가 될 수도 있고 아닐 수도 있다.</p>
<p>확실한 건 하나다. <strong>안 하면 0%이고, 하면 확률이 생긴다.</strong></p>
<p>지인이 카톡으로 "야 갑자기 돈이 들어왔어"라고 보내왔을 때, 나는 그 친구한테 이렇게 말했다.</p>
<p>"그러니까 내가 써보라고 했잖아."</p>
<hr>
<p><em>에어드랍 참여 시 항상 공식 채널을 확인하고, 시드 구문과 개인키는 절대 공유하지 마세요. 이 글은 투자 권유가 아닙니다.</em></p>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[에어드랍]]></category>
      <category><![CDATA[아이드랍]]></category>
      <category><![CDATA[무료코인]]></category>
      <category><![CDATA[DeFi]]></category>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[코인]]></category>
      <category><![CDATA[암호화폐]]></category>
      <category><![CDATA[초보자]]></category>
    </item>

    <item>
      <title><![CDATA[코인 좀 모이면 반드시 고민하게 되는 것 — 지갑, 어디에 보관할까]]></title>
      <link>https://www.stragos.xyz/posts/crypto-wallet-comparison-ledger</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/crypto-wallet-comparison-ledger</guid>
      <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[MetaMask, OKX, Rabby — 소프트웨어 지갑 비교부터 렛저 하드웨어 지갑을 써야 하는 이유까지. 코인이 쌓이기 시작했을 때 진지하게 고민하게 되는 보관 방법을 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>처음엔 그냥 MetaMask 하나면 다 될 줄 알았다.</p>
<p>거래소에서 코인 사고, 지갑으로 옮기고, 필요하면 다시 보내고. 이게 전부인 줄 알았다. 근데 코인이 조금씩 쌓이기 시작하면서 어느 순간 이런 생각이 든다.</p>
<p>"이거 진짜 안전한 건가?"</p>
<p>그냥 지나칠 수도 있는 질문인데, 한 번 떠오르면 이상하게 신경이 쓰인다. 뉴스에서 "해킹으로 수십억 증발"이라는 기사를 볼 때마다 특히 그렇다. 남의 얘기 같지만, 막상 내 지갑 들여다보면 찜찜한 기분이 사라지질 않는다.</p>
<p>그 찜찜함이 괜한 게 아니다. 코인 지갑은 은행 계좌랑 다르다. 해킹 당해도, 실수로 잘못 보내도, 누군가 빼가도 — <strong>돌려받을 방법이 없다.</strong> 그래서 어디에, 어떻게 보관하느냐가 생각보다 중요한 문제다.</p>
<p>이 글은 그 찜찜함을 갖고 있는 사람들을 위해 쓴다.</p>
<hr>
<h2 id="소프트웨어-지갑--편리하지만-구조적-한계가-있다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#소프트웨어-지갑--편리하지만-구조적-한계가-있다">#</a>소프트웨어 지갑 — 편리하지만 구조적 한계가 있다</h2>
<p>소프트웨어 지갑은 PC나 폰에 설치하는 앱 또는 브라우저 확장 프로그램이다.</p>
<p>가장 많이 쓰이는 건 MetaMask, OKX 월렛, Rabby 세 가지다. 각각 성격이 조금씩 다른데, 어떤 걸 쓰느냐에 따라 경험이 꽤 달라진다.</p>
<hr>
<h3 id="metamask"><a class="anchor" aria-hidden="true" tabindex="-1" href="#metamask">#</a>MetaMask</h3>
<p>크립토 생태계의 사실상 표준 지갑이다.</p>
<p>2016년부터 운영해온 만큼 레퍼런스가 압도적이다. DeFi, DEX, NFT 할 것 없이 대부분의 서비스가 MetaMask 연결을 기본으로 지원한다. 처음 지갑을 만들 때 MetaMask를 쓰는 이유가 바로 이것이다. 어디서 막혀도 검색하면 답이 나온다.</p>
<p>2025~2026년 사이에 대규모 업데이트가 있었다. 기존에는 이더리움 계열(EVM) 네트워크만 됐는데, 이제 <strong>비트코인, 솔라나, 트론</strong>까지 같은 지갑에서 관리할 수 있다. 멀티체인 지원이 크게 넓어진 것이다. 거기에 트랜잭션 승인 전에 사람이 읽을 수 있는 형태로 내용을 미리 보여주는 기능도 추가됐다. 예전 MetaMask와는 꽤 달라진 셈이다.</p>
<p>심지어 MetaMask Card도 나왔다. 지갑 잔액으로 Mastercard 가맹점 어디서나 결제할 수 있는 카드다.</p>
<p><img src="/images/crypto/wallet-metamask-card.svg" alt="MetaMask 지갑 장단점 카드" loading="lazy" decoding="async"></p>
<p>기능이 많아진 만큼 "그냥 지갑"이라기보다 하나의 금융 플랫폼에 가까워지고 있다. 다만 기능이 늘어날수록 복잡해진다는 단점도 있다. 처음 쓰는 사람 입장에서는 여전히 UI가 직관적이지 않다는 말이 나온다.</p>
<hr>
<h3 id="okx-wallet"><a class="anchor" aria-hidden="true" tabindex="-1" href="#okx-wallet">#</a>OKX Wallet</h3>
<p>거래소 OKX에서 만든 지갑이다. 멀티체인 지원이 핵심 강점이다.</p>
<p>이더리움, 솔라나, 비트코인, 트론을 포함해 130개 이상의 체인을 하나의 지갑에서 관리할 수 있다. 체인마다 지갑을 따로 설치하고 관리하는 번거로움이 없다. 지갑 내부에 내장 DEX도 있어서 앱을 이동하지 않고도 바로 스왑이 된다.</p>
<p>2026년 3월에는 <strong>OKX Agentic Wallet</strong>을 출시했다. AI 에이전트가 직접 온체인 트랜잭션을 실행할 수 있는 기능이다. 아직 초기 단계지만, AI와 지갑의 결합이라는 방향성을 보여주는 업데이트다.</p>
<p><img src="/images/crypto/wallet-okx-card.svg" alt="OKX Wallet 장단점 카드" loading="lazy" decoding="async"></p>
<p>모바일 앱 완성도도 높아서, PC보다 폰으로 코인을 관리하는 사람에게 특히 편하다.</p>
<p>다만 OKX라는 중앙화 거래소 브랜드와 연결되어 있다는 점이 거슬리는 사람이 있다. 소프트웨어 지갑이니까 탈중앙화 지갑이긴 한데, "거래소가 만든 지갑"에 대한 신뢰 문제는 개인마다 받아들이는 방식이 다르다. MetaMask만큼 dApp 지원이 넓지 않아서 일부 서비스에서 연결이 안 되는 경우도 있다.</p>
<hr>
<h3 id="rabby"><a class="anchor" aria-hidden="true" tabindex="-1" href="#rabby">#</a>Rabby</h3>
<p>DeBank 팀이 만든 지갑으로, "MetaMask보다 안전하게"를 슬로건으로 내세운다.</p>
<p>가장 차별화되는 기능은 <strong>트랜잭션 시뮬레이션</strong>이다. 서명 버튼을 누르기 전에 "이 컨트랙트를 승인하면 내 지갑에서 실제로 뭐가 나가는지"를 미리 보여준다. 그냥 "Approve"만 뜨는 게 아니라, 예상 결과를 눈으로 확인하고 승인할 수 있다.</p>
<p>예를 들어 DEX에서 ETH를 USDC로 스왑하는 트랜잭션을 승인하려 할 때, Rabby는 "0.5 ETH 나가고 1,200 USDC 들어온다"는 식으로 미리 결과를 보여준다. 이게 예상과 다르면 그냥 취소하면 된다. 악성 컨트랙트가 지갑을 통째로 Approve 해가는 것도 이 단계에서 잡힌다.</p>
<p><img src="/images/crypto/wallet-rabby-card.svg" alt="Rabby 지갑 장단점 카드" loading="lazy" decoding="async"></p>
<p>여기에 <strong>피싱 사이트 감지</strong> 기능도 있다. 알려진 피싱 주소나 의심스러운 도메인에 연결하려 할 때 경고를 띄운다. 지갑 주소가 여러 개라면 <strong>멀티 주소 관리</strong>도 편하다. 각 주소의 자산을 한 화면에서 볼 수 있고, 체인별로 묶어서 보여주기 때문에 자산 파악이 MetaMask보다 직관적이다.</p>
<p>DeFi를 자주 쓰다 보면 한 번쯤은 의심스러운 컨트랙트 승인 창을 마주치게 된다. 그 순간 이게 안전한 건지 판단이 안 서는데, Rabby는 그 판단을 조금 더 쉽게 만들어준다. 보안에 신경 쓰는 사람들 사이에서 점점 쓰는 사람이 늘고 있는 이유다.</p>
<p>iOS와 안드로이드 모바일 앱도 있다. PC 브라우저 확장 프로그램만 있는 줄 알고 지나치는 사람이 많은데, 폰에서도 같은 시뮬레이션 기능이 동작한다.</p>
<p>단점은 MetaMask에 비해 검증된 역사가 짧다는 것, 그리고 일부 오래된 dApp에서 연결이 안 되는 경우가 있다는 것이다. 지원 체인 수도 MetaMask나 OKX에 비해 많지 않아서, 이더리움 계열 위주로 쓰는 사람에게 맞는 지갑이다.</p>
<hr>
<h2 id="셋-다-가진-공통적인-한계"><a class="anchor" aria-hidden="true" tabindex="-1" href="#셋-다-가진-공통적인-한계">#</a>셋 다 가진 공통적인 한계</h2>
<p>MetaMask를 쓰든, OKX를 쓰든, Rabby를 쓰든 — 결국 <strong>인터넷에 연결된 기기 위에서 돌아간다</strong>는 사실은 같다.</p>
<p>개인키가 암호화되어 있지만, PC 어딘가에 저장되어 있다는 의미다.</p>
<p>PC가 악성코드에 감염되거나, 피싱 사이트에 속아서 시드 문구를 입력하거나, 브라우저 확장 프로그램이 해킹되면 — 그 순간 지갑 안의 코인은 전부 사라진다.</p>
<p>더 무서운 건 속도다. 해커가 지갑에 접근하면 몇 초 안에 전부 빼간다. 은행처럼 "이상 거래 감지"로 막아주는 시스템이 없다. 알았을 때는 이미 늦다.</p>
<hr>
<h2 id="하드웨어-지갑이-다른-이유"><a class="anchor" aria-hidden="true" tabindex="-1" href="#하드웨어-지갑이-다른-이유">#</a>하드웨어 지갑이 다른 이유</h2>
<p>하드웨어 지갑은 USB처럼 생긴 물리적인 장치다.</p>
<p>렛저(Ledger), 트레저(Trezor) 같은 제품이 있다. 생긴 건 평범한데 작동 방식이 근본적으로 다르다.</p>
<p>핵심은 하나다. <strong>개인키가 절대로 인터넷에 나오지 않는다.</strong></p>
<p>소프트웨어 지갑은 서명(트랜잭션 승인)을 PC나 폰에서 처리한다. 하드웨어 지갑은 서명을 장치 내부에서만 처리한다. PC에 악성코드가 심어져 있어도, 피싱 사이트에 연결하더라도, 개인키 자체는 장치 바깥으로 나오지 않는다.</p>
<p>즉, <strong>해킹이 일어나려면 장치를 물리적으로 빼앗아야 한다.</strong></p>
<p>현실에서 해커가 집 문을 따고 들어와서 렛저를 훔쳐가는 시나리오는 인터넷 해킹보다 훨씬 어렵다. 그리고 PIN 번호를 모르면 장치를 가져가도 쓸 수가 없다.</p>
<p>이 차이가 생각보다 크다.</p>
<hr>
<h2 id="렛저를-쓰는-이유"><a class="anchor" aria-hidden="true" tabindex="-1" href="#렛저를-쓰는-이유">#</a>렛저를 쓰는 이유</h2>
<p>하드웨어 지갑 중에서도 렛저를 많이 쓰는 이유가 있다.</p>
<p><strong>지원하는 코인 수가 압도적이다.</strong> 렛저는 5,500개 이상의 코인과 토큰을 지원한다. 이더리움 기반 토큰은 물론이고, 비트코인, 솔라나, 폴카닷, 코스모스 등 메이저 코인은 거의 다 된다. <strong>Ledger Wallet</strong>(2026년 1분기에 Ledger Live에서 리브랜딩)이라는 공식 앱에서 잔액 확인, 전송, 스테이킹까지 할 수 있다. 여러 체인 자산을 앱 하나에서 관리할 수 있어서 따로 지갑을 오갈 필요가 없다.</p>
<p><strong>MetaMask와 연동된다.</strong> 렛저를 갖고 있어도 DEX나 DeFi를 쓸 수 있다. 렛저를 MetaMask에 연결하면 트랜잭션을 승인할 때 렛저 장치에서 직접 버튼을 눌러야 한다. 화면에서 클릭만으로 서명되지 않는다. 악성코드가 있어도 장치 없이는 승인이 안 된다. 즉, DeFi의 편리함을 유지하면서 보안 수준은 하드웨어 지갑 수준으로 올릴 수 있다.</p>
<p><strong>보안 칩이 다르다.</strong> 렛저는 신용카드나 여권에 쓰이는 보안 칩(SE, Secure Element)을 탑재한다. 일반 마이크로컨트롤러가 아니라 물리적 해킹 시도에 저항하도록 설계된 칩이다. PIN을 3번 틀리면 장치가 초기화되어 개인키를 추출할 수 없게 된다.</p>
<p><strong>제품 라인업이 다양하다.</strong> 가장 기본 모델인 Nano S Plus는 약 $79, 블루투스 지원 Nano X는 약 $149다. 2025년 10월 출시된 <strong>Nano Gen5</strong>($179~199)는 2.8인치 E Ink 터치스크린을 탑재해 Nano X보다 사용이 편리하고 현재 가성비 추천 모델이다. 더 상위로는 Flex($249), Stax($399)가 있다. 보안 수준은 모든 모델 동일하며 차이는 편의성이다.</p>
<p><strong>검증된 역사가 있다.</strong> 2014년부터 운영해온 프랑스 회사다. 전 세계 600만 개 이상 판매된 점에서 신뢰도를 가늠할 수 있다. 처음엔 가격이 비싸 보이지만, 지키려는 코인의 금액이 기기값보다 크다면 충분히 투자할 만하다.</p>
<hr>
<h2 id="렛저라고-100-안전하지는-않다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#렛저라고-100-안전하지는-않다">#</a>렛저라고 100% 안전하지는 않다</h2>
<p>솔직하게 써야 할 부분이 있다.</p>
<p><strong>2020년 고객 데이터 유출.</strong> 렛저 쇼핑몰 DB가 해킹되면서 이름, 이메일, 전화번호, 배송 주소 약 27만 건이 유출됐다. 코인 자체는 안전했지만, 이후에 고객들에게 피싱 문자와 이메일이 쏟아졌다. 심지어 "렛저 공식 업데이트"처럼 위장한 가짜 기기를 우편으로 보내는 사기도 있었다. "렛저 직원입니다"라며 시드 문구를 요청하는 전화 사기도 여럿 보고됐다. 기기 보안은 훌륭한데 회사 보안은 별개였던 셈이다.</p>
<p><strong>2023년 Ledger Recover 논란.</strong> 렛저가 시드 문구를 세 조각으로 분리해 서로 다른 서버에 암호화 백업해주는 유료 서비스를 발표했다. 시드 문구를 잃어버렸을 때 복구할 수 있도록 하는 취지였지만, 크립토 커뮤니티의 반응은 차가웠다. "개인키가 장치 밖으로 절대 나오지 않는다"는 렛저의 기존 원칙과 정면으로 충돌하기 때문이다. 렛저 측은 "선택 사항이며 강제가 아니다"라고 해명했지만, 이 기능 자체가 존재한다는 사실이 불신의 씨앗이 됐다. 이후로 트레저(Trezor)나 콜드카드(Coldcard) 같은 경쟁 제품으로 갈아탄 사람들도 나왔다.</p>
<p><strong>2023년 Connect Kit 공급망 해킹.</strong> 렛저가 배포하는 JavaScript 라이브러리(Connect Kit)가 해킹되어 악성 코드가 심어진 적이 있다. 이 라이브러리를 사용하는 DeFi 앱들이 일시적으로 영향을 받았고, 실제 자금 피해도 발생했다. 기기 자체는 안전했지만, 렛저 생태계에 연결된 소프트웨어가 공격 벡터가 될 수 있다는 걸 보여준 사건이다.</p>
<p>이런 부분을 알고 쓰는 것과 모르고 쓰는 건 다르다. 장치 자체의 보안은 여전히 소프트웨어 지갑보다 훨씬 낫다. 다만 "렛저를 갖고 있으면 무조건 안전하다"는 생각은 위험하다. 어떤 회사도 완벽하지 않고, 기기 보안과 회사 보안은 별개라는 걸 인식하고 쓰는 게 맞다.</p>
<hr>
<h2 id="정리--어떻게-나눠-쓸까"><a class="anchor" aria-hidden="true" tabindex="-1" href="#정리--어떻게-나눠-쓸까">#</a>정리 — 어떻게 나눠 쓸까</h2>
<p>실제로 많은 사람들이 두 가지를 같이 쓴다.</p>
<ul>
<li><strong>소프트웨어 지갑(MetaMask / OKX / Rabby)</strong> — 자주 쓰는 소액, DEX 연결, DeFi 활동</li>
<li><strong>하드웨어 지갑(렛저)</strong> — 한동안 안 건드릴 코인, 큰 금액</li>
</ul>
<p>은행에서 체크카드 쓰듯이 소프트웨어 지갑을 쓰고, 큰돈은 금고에 넣듯이 하드웨어 지갑에 옮겨두는 개념이다.</p>
<p>얼마부터 하드웨어 지갑을 써야 하느냐는 사람마다 다르다. 딱 정해진 기준은 없다. 다만 "이 코인 날리면 좀 많이 아프겠다"는 생각이 드는 시점이 오면, 그때가 고민해볼 타이밍이다.</p>
<p>보안을 귀찮게 여기다가 한 번 당하면 돌이킬 방법이 없다. 코인 세계에서 그게 가장 잔인한 규칙이다.</p>
<blockquote>
<p>MetaMask 지갑을 처음 만드는 방법은 <a href="/posts/metamask-wallet-setup-guide">MetaMask 지갑 세팅 완전 가이드</a>에 정리해뒀다.
거래소에서 지갑으로 코인 보내는 방법은 <a href="/posts/how-to-send-crypto-to-metamask">MetaMask로 코인 받는 방법</a>에서 확인할 수 있다.</p>
</blockquote>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[지갑]]></category>
      <category><![CDATA[MetaMask]]></category>
      <category><![CDATA[OKX]]></category>
      <category><![CDATA[Rabby]]></category>
      <category><![CDATA[렛저]]></category>
      <category><![CDATA[Ledger]]></category>
      <category><![CDATA[하드웨어지갑]]></category>
      <category><![CDATA[보안]]></category>
      <category><![CDATA[코인]]></category>
      <category><![CDATA[암호화폐]]></category>
    </item>

    <item>
      <title><![CDATA[코인 좀 하다 보면 반드시 마주치는 것들 — 스테이블코인, 가스비, DEX]]></title>
      <link>https://www.stragos.xyz/posts/crypto-stablecoin-gas-dex-guide</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/crypto-stablecoin-gas-dex-guide</guid>
      <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[지갑도 만들고 전송도 해봤는데 그 다음은? 스테이블코인이 뭔지, 가스비는 왜 내야 하는지, 유니스왑 같은 DEX는 어떻게 쓰는지 — 코인 입문 두 번째 관문을 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>MetaMask 지갑을 만들고, 거래소에서 코인을 사서 지갑으로 옮겨봤다.</p>
<p>여기까지 했다면 솔직히 꽤 한 거다. 처음엔 이것만 해도 뭔가 대단한 일을 한 것 같은 기분이 든다.</p>
<p>근데 딱 이 시점부터 머릿속이 다시 복잡해지기 시작한다.</p>
<p>거래소 앱 보다가 USDT가 눈에 밟힌다. "이게 달러랑 같은 건가?" 싶은데 물어보기도 애매하다. ETH 전송 누르면 수수료가 어떤 날은 500원이고 어떤 날은 3만 원이 뜬다. 왜 이렇게 들쭉날쭉한지 이해가 안 된다. 어디선가 "유니스왑에서 샀다"는 말을 듣는데, 유니스왑이 뭔지는 또 모른다.</p>
<p>이 세 가지 질문이 거의 동시에 온다. 그리고 알고 보면 서로 연결되어 있다.</p>
<hr>
<h2 id="스테이블코인--코인인데-가격이-안-변한다고"><a class="anchor" aria-hidden="true" tabindex="-1" href="#스테이블코인--코인인데-가격이-안-변한다고">#</a>스테이블코인 — 코인인데 가격이 안 변한다고?</h2>
<p>코인 시장에 오래 있다 보면 USDT, USDC를 쓸 일이 생각보다 많이 생긴다.</p>
<p>처음엔 별 관심이 없다. 가격도 안 변하고, 차트도 재미없고, 딱히 살 이유를 모르겠어서 그냥 넘긴다.</p>
<p>근데 코인을 좀 하다 보면 어느 순간 이게 왜 필요한지가 보이기 시작한다.</p>
<p>시장이 갑자기 무너질 것 같은 분위기가 온다. 들고 있는 알트코인을 팔고 싶은데, 원화로 출금하자니 거래소 수수료에 입출금 시간도 걸리고 번거롭다. 이럴 때 그냥 USDT로 바꿔두면 된다. 코인 생태계 안에 머물면서도 시장 등락에서 벗어날 수 있다.</p>
<p>일종의 <strong>대피소</strong> 같은 역할이다.</p>
<p>스테이블코인이란 <strong>1달러의 가치를 유지하도록 설계된 암호화폐</strong>다. 가격이 오르거나 내리지 않고, 항상 1달러 근처에 고정되어 있다. 그래서 '스테이블(stable, 안정적인)코인'이라고 부른다.</p>
<p>비트코인도 시황이 안 좋을 때는 하루에 5~10% 빠지는 날이 있고, 알트코인은 그보다 심하게 움직이는 경우도 흔하다. 코인으로만 자산을 들고 있으면 오늘 내 돈이 얼마인지 아침저녁이 다르다. 스테이블코인은 그 불안함에서 잠시 벗어나는 수단이다.</p>
<h3 id="usdt-vs-usdc-진짜-다른-건-뭔가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#usdt-vs-usdc-진짜-다른-건-뭔가">#</a>USDT vs USDC, 진짜 다른 건 뭔가</h3>
<p>둘 다 1달러짜리 스테이블코인이다.</p>
<p>쓰는 입장에서는 거의 똑같다고 느껴지는데, 발행하는 곳이 다르고 철학이 조금 다르다.</p>
<p><strong>USDT</strong>는 Tether라는 회사에서 만들었다. 시장에 가장 많이 풀려있고 거래소, DEX 어디서든 지원한다. 유동성이 압도적이라 교환이 빠르고 슬리피지(원하는 가격과 실제 체결 가격의 차이)가 적다. 다만 과거에 한 가지 논란이 있었다. "달러를 실제로 1:1로 갖고 있냐"는 의혹이 제기된 적이 있었는데, 당시에 꽤 시끄러웠다. 지금은 어느 정도 정리됐지만 그 이미지가 아직 남아 있는 사람들이 있다.</p>
<p><strong>USDC</strong>는 Circle이라는 회사가 발행한다. 미국 금융 당국의 규제를 받고, 준비금 감사 보고서를 공개하는 방식으로 투명성을 어필한다. 기관 투자자나 DeFi 프로토콜 쪽에서 선호하는 편이다.</p>
<p>일반 사용 입장에서는 크게 고민할 필요 없다. 어디서 쓰느냐에 따라 지원 여부가 다를 수 있으니 그냥 상황에 맞게 쓰면 된다.</p>
<h3 id="스테이블코인도-100-안전하지는-않다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#스테이블코인도-100-안전하지는-않다">#</a>스테이블코인도 100% 안전하지는 않다</h3>
<p>한 가지만 알아두면 좋다.</p>
<p>2022년에 <strong>UST(테라USD)</strong> 라는 스테이블코인이 하루아침에 1달러에서 거의 0원에 가깝게 무너진 사건이 있었다. 코인 역사에서 가장 충격적인 사건 중 하나였고, 수십조 원이 증발했다.</p>
<p>USDT나 USDC는 달러를 실제로 담보로 갖고 있는 방식이라 UST 같은 알고리즘 기반 스테이블코인과는 구조가 다르다. 리스크가 훨씬 낮다. 하지만 "스테이블코인이니까 무조건 안전하다"는 생각은 조심해야 한다.</p>
<p>발행 주체가 망하거나 문제가 생기면 담보도 의미가 없어질 수 있다. 그 가능성이 높다는 게 아니라, 코인 시장에서 "절대 안전"은 없다는 얘기다.</p>
<hr>
<h2 id="가스비--왜-내-돈-보내는-데-또-돈이-드나"><a class="anchor" aria-hidden="true" tabindex="-1" href="#가스비--왜-내-돈-보내는-데-또-돈이-드나">#</a>가스비 — 왜 내 돈 보내는 데 또 돈이 드나</h2>
<p>MetaMask에서 처음 전송을 시도하면 수수료 항목에 숫자가 붙어 있다.</p>
<p>어떤 날은 몇백 원이고, 어떤 날은 몇만 원이다. 같은 금액을 보내는데 왜 이렇게 다른지 이해가 안 된다.</p>
<p>이게 <strong>가스비(Gas Fee)</strong> 다.</p>
<p>처음엔 진짜 황당하다. 내 돈을 내 지갑에서 다른 지갑으로 보내는 건데 왜 수수료를 내야 하나. 은행 이체도 요즘은 무료인데.</p>
<p>원리를 알면 납득이 된다. 이더리움 블록체인은 전 세계에 분산된 수천 대의 컴퓨터가 돌아가면서 거래를 처리하고 검증한다. 이 작업에는 전기도 들고 연산도 든다. 그 비용을 내는 게 가스비다. 은행이나 카드사 같은 중간 기관이 없는 대신, 네트워크를 유지하는 참여자들에게 직접 보상이 돌아가는 구조다.</p>
<h3 id="왜-들쭉날쭉한가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#왜-들쭉날쭉한가">#</a>왜 들쭉날쭉한가</h3>
<p>가스비는 고정 금액이 아니다. <strong>네트워크가 얼마나 바쁘냐</strong>에 따라 실시간으로 달라진다.</p>
<p>이더리움 네트워크는 한 번에 처리할 수 있는 트랜잭션 수가 정해져 있다. 수요가 몰리면 자리 경쟁이 생기고, 가스비를 더 많이 낸 트랜잭션이 먼저 처리된다. 급한 사람일수록 더 많이 내게 되는 구조다.</p>
<p>시장이 크게 움직이는 날 — 비트코인이 급락하거나 급등하는 날 — 모두가 동시에 뭔가를 하려고 한다. 팔거나, 헷지하거나, 다른 코인으로 옮기거나. 이럴 때 가스비가 평소의 수십 배까지 튄다.</p>
<p>10달러짜리 ETH를 보내려는데 가스비가 30달러가 나오는 상황이 생긴다. 그 순간 진짜 허탈하다.</p>
<p>NFT 민팅 날도 마찬가지다. 인기 있는 프로젝트 민팅이 열리면 수천 명이 동시에 트랜잭션을 날린다. 더 황당한 건, 그렇게 비싼 가스비를 내고 민팅에 실패해도 가스비는 돌려받지 못한다는 거다. 처리를 시도했다는 것 자체에 비용이 붙으니까.</p>
<p>반대로 새벽에 네트워크가 한산할 때는 가스비가 몇 센트 수준으로 내려가기도 한다. 급하지 않은 전송이라면 이 시간대를 노리는 것도 방법이다.</p>
<h3 id="레이어2--가스비-문제의-현실적인-해답"><a class="anchor" aria-hidden="true" tabindex="-1" href="#레이어2--가스비-문제의-현실적인-해답">#</a>레이어2 — 가스비 문제의 현실적인 해답</h3>
<p>이더리움 메인넷에서만 쓰다가는 가스비에 치이는 날이 반드시 온다.</p>
<p>그래서 나온 게 <strong>레이어2(Layer 2, L2)</strong> 다.</p>
<p>쉽게 설명하면 이더리움 위에 올라탄 고속도로 같은 것이다. 트랜잭션을 한꺼번에 묶어서 처리하고, 그 결과만 이더리움에 기록한다. 이더리움의 보안성은 그대로 쓰면서 처리 비용을 대폭 낮춘다.</p>
<p>대표적인 게 <strong>Arbitrum</strong>, <strong>Base</strong>, <strong>Optimism</strong>이다.</p>
<p>이더리움 메인넷에서 ETH 전송 한 번에 5달러가 든다면, Arbitrum에서는 0.05달러도 안 든다. 같은 코인, 같은 지갑, 같은 화면인데 네트워크만 바꿨을 뿐이다.</p>
<p>요즘 유니스왑 같은 DEX나 DeFi 서비스들은 레이어2를 대부분 지원한다. 자주 트랜잭션을 날리거나 소액으로 뭔가를 해보고 싶다면 레이어2 네트워크를 먼저 세팅해두는 게 낫다. MetaMask에서 네트워크를 추가하는 방법은 <a href="/posts/metamask-wallet-setup-guide">MetaMask 지갑 만들기 가이드</a>에 정리해뒀다.</p>
<hr>
<h2 id="dex--거래소-없이-코인을-바꾼다는-게-어떤-의미인가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dex--거래소-없이-코인을-바꾼다는-게-어떤-의미인가">#</a>DEX — 거래소 없이 코인을 바꾼다는 게 어떤 의미인가</h2>
<p>업비트, 빗썸, 코인원. 이런 거래소를 <strong>CEX(중앙화 거래소)</strong> 라고 부른다.</p>
<p>회사가 있고, 서버가 있고, 고객센터가 있다. 내가 계좌를 만들고 코인을 맡기면 거래소가 대신 보관하고 거래를 처리해준다. 익숙한 구조다.</p>
<p><strong>DEX(탈중앙화 거래소)</strong> 는 이 중간 회사가 없다.</p>
<p>스마트컨트랙트라는 코드가 알아서 거래를 처리한다. 내 지갑에서 바로 이뤄지고, 회원가입도 신분 인증도 없다. 지갑 연결 하나로 끝이다.</p>
<p>가장 많이 쓰이는 게 <strong>유니스왑(Uniswap)</strong> 이다. 이더리움 생태계에서 가장 큰 DEX고, 하루 거래량이 수조 원이 넘는다.</p>
<h3 id="cex가-있는데-dex를-왜-쓰나"><a class="anchor" aria-hidden="true" tabindex="-1" href="#cex가-있는데-dex를-왜-쓰나">#</a>CEX가 있는데 DEX를 왜 쓰나</h3>
<p>처음엔 "그냥 업비트 쓰면 되는 거 아닌가" 싶다. 편하고 익숙하고 고객센터도 있는데.</p>
<p>이유는 두 가지다.</p>
<p><strong>첫 번째, 업비트에 없는 코인은 살 수 없다.</strong></p>
<p>CEX는 상장 심사를 거친 코인만 거래된다. 관심 있는 프로젝트 토큰이 국내 거래소에 없는 경우가 생각보다 많다. DEX에서는 이더리움 네트워크 위에 배포된 토큰이라면 뭐든 교환할 수 있다. 상장 심사 같은 게 없으니까. 새로운 프로젝트들은 CEX에 올라오기 훨씬 전에 DEX에서 먼저 거래된다.</p>
<p><strong>두 번째, 내 코인이 진짜 내 것이다.</strong></p>
<p>CEX에 코인을 맡기면 그건 거래소 서버에 기록된 숫자다. 거래소가 망하거나, 해킹당하거나, 출금을 막으면 내가 어떻게 할 수 없다.</p>
<p>2022년 FTX 사태를 기억하는 사람이라면 이게 단순한 가능성의 문제가 아니라는 걸 안다. 세계 2위 거래소가 며칠 만에 파산했다. 출금 요청이 밀렸고, 많은 사람이 자산을 돌려받지 못했다. 규모가 크고 유명한 곳이라고 무조건 안전한 게 아니라는 게 그때 확실히 증명됐다.</p>
<p>DEX는 내 지갑에서 직접 거래가 이뤄진다. 플랫폼이 어떻게 되든 내 자산은 내 지갑에 있다.</p>
<h3 id="유니스왑-처음-써보기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#유니스왑-처음-써보기">#</a>유니스왑 처음 써보기</h3>
<p>처음 들어가면 생각보다 단순하다.</p>
<p><a href="https://app.uniswap.org">app.uniswap.org</a> 에 접속하면 스왑 화면이 바로 나온다. Connect 버튼으로 MetaMask 연결하면 준비 끝이다. 위에 내가 갖고 있는 코인을 선택하고, 아래에 받고 싶은 코인을 선택한다. 금액을 입력하면 예상 수령량, 가스비, 슬리피지가 표시된다. 확인하고 Swap 누르면 MetaMask 승인 창이 뜨고, 거기서 한 번 더 확인하면 끝이다.</p>
<p>처음엔 뭔가 거창할 것 같은데, 막상 해보면 은행 이체보다 간단하다고 느끼는 사람도 있다.</p>
<p><strong>다만 한 가지는 꼭 챙겨야 한다.</strong></p>
<p>유니스왑 사칭 피싱 사이트가 생각보다 많다. 구글에서 "유니스왑"을 검색하면 광고 영역에 가짜 사이트가 올라와 있는 경우도 있었다. 주소창에 직접 입력하거나, 공식 북마크를 쓰는 습관이 중요하다.</p>
<h3 id="dex에서-조심해야-할-것들"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dex에서-조심해야-할-것들">#</a>DEX에서 조심해야 할 것들</h3>
<p>편한 만큼 책임이 전부 본인한테 있다.</p>
<p>CEX는 상장 심사가 있고, 사기성 짙은 프로젝트는 걸러진다. DEX는 코드를 배포하는 순간 바로 토큰이 생긴다. 쓰레기 토큰도, 러그풀(개발자가 갑자기 유동성 빼버리는 사기)도 아무 제약 없이 올라온다.</p>
<p>모르는 토큰을 살 때는 컨트랙트 주소를 직접 확인하는 게 필수다. 이름이 같아도 주소가 다르면 전혀 다른 토큰이다. 이더스캔(etherscan.io)에서 프로젝트 공식 주소를 확인하고 그걸 유니스왑에 직접 붙여넣는 방식이 안전하다.</p>
<p>그리고 중요한 게 하나 더 있다.</p>
<p><strong>가스비는 그 네트워크의 기본 코인으로 낸다.</strong></p>
<p>이더리움 메인넷과 Arbitrum, Base 같은 L2에서는 ETH로 낸다. Polygon이면 POL, BSC면 BNB다. 네트워크마다 다르다.</p>
<p>문제는 USDT만 들고 있다가 스왑하려고 하면 막힌다는 거다. ETH가 없으면 이더리움/L2 네트워크에서는 가스비를 낼 수 없어서 트랜잭션 자체가 안 된다. 유니스왑을 쓰려면 ETH를 조금은 갖고 있어야 한다. 처음에 이걸 몰라서 당황하는 경우가 있다.</p>
<hr>
<h2 id="세-개가-이렇게-연결된다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#세-개가-이렇게-연결된다">#</a>세 개가 이렇게 연결된다</h2>
<p>각각 따로 이해하면 잘 와닿지 않는다. 실제 흐름으로 보면 이렇다.</p>
<p>업비트에서 ETH를 산다 → MetaMask로 옮긴다 → 가스비를 내고 유니스왑에서 원하는 토큰으로 스왑한다 → 시황이 흔들릴 것 같으면 USDC로 바꿔서 대기한다 → 다시 기회가 왔다 싶으면 다른 토큰으로 교환한다</p>
<p>이게 DeFi의 기본 사이클이다.</p>
<p>스테이블코인은 대피소, 가스비는 통행료, DEX는 환전 창구다. 이 세 가지를 이해하면 이더리움 생태계에서 할 수 있는 것들의 폭이 달라진다.</p>
<p>처음엔 "이걸 왜 이렇게 복잡하게 해야 하나" 싶다. 근데 한 번 흐름이 익으면 오히려 거래소를 거치는 게 번거롭게 느껴지기 시작한다.</p>
<hr>
<h2 id="마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마무리">#</a>마무리</h2>
<p>이 세 가지를 머리로만 이해하는 것보다 직접 한 번씩 해보는 게 훨씬 빠르다.</p>
<p>소액으로 좋다. USDT 5달러어치를 USDC로 스왑해본다. 레이어2 네트워크로 바꿔서 가스비가 얼마나 다른지 본다. 이 과정에서 가스비가 왜 저렇게 뜨는지, ETH가 왜 있어야 하는지가 자연스럽게 이해된다.</p>
<p>설명을 열 번 읽는 것보다 한 번 해보는 게 낫다. 코인 공부는 결국 그렇다.</p>
<hr>
<p><em>MetaMask 처음 만들기는 <a href="/posts/metamask-wallet-setup-guide">MetaMask 지갑 처음 만들기 완전 가이드</a>에,<br>
거래소에서 지갑으로 보내는 법은 <a href="/posts/how-to-send-crypto-to-metamask">코인 사서 내 지갑으로 보내기</a>에 정리해뒀다.</em></p>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[스테이블코인]]></category>
      <category><![CDATA[USDT]]></category>
      <category><![CDATA[USDC]]></category>
      <category><![CDATA[가스비]]></category>
      <category><![CDATA[DEX]]></category>
      <category><![CDATA[유니스왑]]></category>
      <category><![CDATA[이더리움]]></category>
      <category><![CDATA[초보자]]></category>
      <category><![CDATA[DeFi]]></category>
    </item>

    <item>
      <title><![CDATA[코인 처음 할 때 이것만 알았어도 — 진짜 초보가 하는 실수들]]></title>
      <link>https://www.stragos.xyz/posts/crypto-beginner-mistakes</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/crypto-beginner-mistakes</guid>
      <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[네트워크 잘못 선택해서 코인 날리기, 잡코인에 묻히기, FOMO에 물리기. 코인 처음 시작할 때 거의 대부분 한 번씩은 겪는 실수들을 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>코인을 처음 시작했을 때 아무도 제대로 알려주지 않았다.</p>
<p>유튜브에는 "이 코인 지금 사야 함" 류의 영상이 넘쳐났고, 주변에서는 "나는 몇 배 벌었다"는 얘기만 들렸다. 다들 수익 인증만 하지, 얼마나 잃었는지는 아무도 얘기 안 했다.</p>
<p>그러다 직접 해보니까 생각보다 잃을 포인트가 너무 많았다. 코인 자체의 등락 말고도, 그냥 기본 개념을 몰라서 날리는 경우가 생각보다 훨씬 많더라.</p>
<p>이 글은 투자 조언이 아니다.</p>
<p>그냥 "처음에 이걸 몰라서 당황했다" 는 얘기다. 코인 자체보다 이런 기본기부터 알고 시작했으면 훨씬 덜 헤맸을 텐데 싶어서 쓴다.</p>
<hr>
<h2 id="1-네트워크-잘못-선택해서-코인이-사라진-것처럼-되는-경우"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-네트워크-잘못-선택해서-코인이-사라진-것처럼-되는-경우">#</a>1. 네트워크 잘못 선택해서 코인이 사라진 것처럼 되는 경우</h2>
<p>이게 진짜 초보한테 가장 흔한 사고다.</p>
<p>거래소에서 USDT를 출금할 때 이런 화면이 나온다.</p>
<pre><code>출금 네트워크 선택
> ERC20  (이더리움)
> TRC20  (트론)
> BEP20  (BNB 스마트체인)
> Polygon
</code></pre>
<p>처음 보면 그냥 아무거나 누르고 싶은 충동이 생긴다. 어차피 같은 USDT 아닌가 싶어서.</p>
<p>근데 이게 결정적으로 중요하다.</p>
<p>쉽게 설명하면 이렇다. 같은 1만 원짜리 지폐라도, 한국 은행 계좌가 있어야 입금이 된다. 미국 달러 계좌로는 원화 입금이 안 되는 것처럼, 코인도 <strong>어떤 네트워크로 보내냐에 따라 받을 수 있는 지갑이 다르다.</strong></p>
<p>MetaMask 기본 설정은 이더리움 메인넷이다. 여기서 TRC20 네트워크로 USDT를 보내버리면 MetaMask에서 아무리 찾아도 안 보인다.</p>
<p>없어진 게 아니라 <strong>다른 네트워크에 들어가 있는 것</strong>인데, 처음엔 그냥 사라진 것처럼 느껴진다. 멘탈이 흔들리는 순간이다.</p>
<p>복구가 아예 불가능한 건 아닌데, 과정이 꽤 복잡하고 잘못하면 진짜로 날릴 수도 있다. 처음부터 맞는 네트워크로 보내는 게 답이다.</p>
<blockquote>
<p><strong>기본 원칙 — 보내는 네트워크와 받는 지갑의 네트워크가 반드시 같아야 한다.</strong></p>
<p>MetaMask 기본 세팅이면 이더리움(ERC20) 또는 Polygon 네트워크로 받으면 된다.
폴리곤 네트워크 추가 방법은 <a href="/posts/metamask-wallet-setup-guide">MetaMask 지갑 만들기 가이드</a>에 정리해뒀다.</p>
</blockquote>
<hr>
<h2 id="2-주소-하나만-틀려도-코인은-돌아오지-않는다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-주소-하나만-틀려도-코인은-돌아오지-않는다">#</a>2. 주소 하나만 틀려도 코인은 돌아오지 않는다</h2>
<p>은행 계좌번호를 잘못 치면 고객센터에 전화해서 취소할 수 있다. 코인은 그런 게 없다.</p>
<p>한 번 전송하면 블록체인에 기록되고 끝이다. 존재하지 않는 주소로 보내도, 실수로 다른 주소로 보내도 — <strong>취소나 환불이 구조적으로 불가능하다.</strong> 관리 주체가 없으니까.</p>
<p>처음엔 주소가 너무 길어서 앞뒤 몇 글자만 대충 보고 "맞겠지" 하고 보내는 경우가 있다.</p>
<pre><code>0xAb5801a7D398351b8bE11C439e05C5B3259aeC9
</code></pre>
<p>이 주소에서 중간 글자 하나가 달라도 완전히 다른 지갑이다. 내가 보낸 코인은 영영 찾을 수 없다.</p>
<p>주소를 항상 복사-붙여넣기로 처리하고, 전송 전에 <strong>앞 5자리, 뒤 5자리</strong>만큼은 눈으로 직접 비교하는 습관을 들이는 게 좋다.</p>
<p>그리고 클립보드를 바꿔치기하는 악성코드가 실제로 존재한다.</p>
<p>복사한 주소가 붙여넣는 순간 몰래 다른 주소로 바뀌는 방식이다. 황당하게 들리지만 실제로 이것 때문에 코인 날린 사례가 있다. 출처 모를 프로그램을 설치했거나 의심스러운 링크를 클릭한 PC에서는 특히 조심해야 한다.</p>
<hr>
<h2 id="3-가격이-싸다고-좋은-코인이-아니다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-가격이-싸다고-좋은-코인이-아니다">#</a>3. 가격이 싸다고 좋은 코인이 아니다</h2>
<p>처음 코인 살 때 이런 생각을 했다.</p>
<p>"비트코인은 너무 비싸다. 100원짜리 코인이 100배 오르면 훨씬 낫지 않나."</p>
<p>이게 함정이다.</p>
<p>코인 가격 그 자체는 사실 별 의미가 없다. 중요한 건 <strong>시가총액(Market Cap)</strong> 이다.</p>
<blockquote>
<p><strong>시가총액 = 코인 가격 × 발행된 총 수량</strong></p>
</blockquote>
<p>예를 들어 1원짜리 코인이 1조 개 발행되어 있으면 시가총액은 1조 원이다.</p>
<p>100만 원짜리 코인이 100만 개만 있어도 시가총액은 똑같이 1조 원이다.</p>
<p>둘 다 같은 규모의 코인인데, 가격만 보면 전혀 다르게 느껴진다. 그래서 "싸 보인다"는 착각이 생기는 거다.</p>
<p>오히려 발행량이 무지막지하게 많고 가격이 낮은 코인일수록 세력이 물량으로 흔들기 쉽고, 한 번 빠지기 시작하면 회복이 거의 안 되는 경우가 많다.</p>
<p>시가총액은 코인마켓캡(coinmarketcap.com)이나 코인게코(coingecko.com)에서 확인할 수 있다. 코인을 살 때 가격보다 시가총액 순위를 먼저 보는 습관을 들이면 좋다.</p>
<hr>
<h2 id="4-차트를-모르면-읽기-전에-당한다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-차트를-모르면-읽기-전에-당한다">#</a>4. 차트를 모르면 읽기 전에 당한다</h2>
<p>코인 처음 하면 차트를 그냥 직관으로 해석하게 된다. "올라가고 있으니까 사야지", "내려가니까 팔아야지."</p>
<p>근데 차트에는 최소한 알아야 할 개념들이 있다.</p>
<p><strong>거래량</strong>은 그날 얼마나 많이 사고팔렸는지를 보여준다. 가격이 오를 때 거래량도 같이 늘어야 진짜 상승이다. 가격은 오르는데 거래량이 없다면 신뢰하기 어렵다.</p>
<p><strong>봉차트</strong>(캔들)는 일정 시간 동안의 시가, 고가, 저가, 종가를 한눈에 보여준다. 빨간 봉은 그 시간 동안 내렸다는 뜻이고, 파란(또는 초록) 봉은 올랐다는 뜻이다. 이것만 알아도 차트가 좀 다르게 보인다.</p>
<p>처음엔 차트가 너무 복잡해 보여서 그냥 넘기게 되는데, 최소한 이 두 가지 개념은 익히고 시작하는 게 낫다.</p>
<hr>
<h2 id="5-fomo--다들-오른다고-할-때-샀더니-그게-고점이었다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-fomo--다들-오른다고-할-때-샀더니-그게-고점이었다">#</a>5. FOMO — 다들 오른다고 할 때 샀더니 그게 고점이었다</h2>
<p>FOMO는 Fear Of Missing Out, 나만 기회를 놓치는 것 같은 불안함이다.</p>
<p>코인판에서는 이게 유독 강하게 온다.</p>
<p>단톡방, 유튜브, 트위터(X)에서 "지금 오르고 있다", "이거 지금 안 사면 후회한다" 는 분위기가 만들어지면 뭔가 급하게 사야 할 것 같은 기분이 든다. 이미 오른 걸 보면서 "나만 못 탄 것 같아서" 쫓아 들어가는 거다.</p>
<p>근데 그 분위기가 가장 뜨거울 때가 대체로 고점 근처다.</p>
<p>모두가 관심을 갖기 시작할 때는 이미 많이 오른 뒤다. 그 시점에 사면 오래 물려 있거나, 크게 빠지는 걸 고스란히 맞게 된다.</p>
<p>이건 솔직히 막기가 진짜 어렵다. 머리로는 알아도 감정이 먼저 반응하니까. 그나마 도움이 됐던 건 이 생각이었다.</p>
<blockquote>
<p><strong>"지금 안 사면 끝이야" 라는 생각이 드는 순간이 오히려 가장 위험한 타이밍일 수 있다.</strong></p>
</blockquote>
<p>조급함이 느껴질 때 잠깐 멈추는 것만으로도 꽤 많은 실수를 막을 수 있다.</p>
<hr>
<h2 id="6-거래소에만-두는-것도-리스크다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-거래소에만-두는-것도-리스크다">#</a>6. 거래소에만 두는 것도 리스크다</h2>
<p>코인을 샀으면 그냥 거래소 앱에 두면 되는 거 아닌가 싶었다. 편하긴 하다.</p>
<p>근데 거래소는 내 코인을 <strong>대신 보관해주는 곳</strong>이다. 내 지갑이 아니다.</p>
<p>거래소가 해킹당하거나, 운영 문제가 생기거나, 갑자기 출금을 막아버리면 내 코인에 접근하지 못하는 상황이 생길 수 있다.</p>
<p>이게 먼 나라 얘기가 아니다.</p>
<p>2022년 FTX라는 세계 2위 거래소가 갑자기 파산했다. 수많은 사람이 거래소 안에 있던 자산을 그대로 날렸다. 당시 한국 사용자들도 피해를 입었다. 거래소가 크다고 무조건 안전한 게 아니라는 게 그때 확실히 증명됐다.</p>
<p>금액이 크지 않고 단기 트레이딩 목적이라면 거래소에 두는 것도 현실적으로 괜찮다. 다만 장기 보유 목적이거나 금액이 의미 있는 수준이 됐다면, MetaMask 같은 개인 지갑으로 옮기는 걸 진지하게 고려해볼 필요가 있다.</p>
<p>개인 지갑으로 옮기면 거래소가 어떻게 되든 내 코인은 내 손에 있다.</p>
<p>MetaMask 세팅이 처음이라면 <a href="/posts/metamask-wallet-setup-guide">MetaMask 지갑 만들기 가이드</a>를,<br>
거래소에서 내 지갑으로 보내는 법은 <a href="/posts/how-to-send-crypto-to-metamask">코인 사서 내 지갑으로 보내기</a>에 정리해뒀다.</p>
<hr>
<h2 id="7-시드-문구복구-구문를-대충-보관했다가-지갑을-잃는다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7-시드-문구복구-구문를-대충-보관했다가-지갑을-잃는다">#</a>7. 시드 문구(복구 구문)를 대충 보관했다가 지갑을 잃는다</h2>
<p>MetaMask를 처음 만들면 12개 단어로 된 시드 문구가 나온다.</p>
<pre><code>w***h  c*******e  p*******e  f**d  s***e  o**n  d*****r  c***k  r**d  a***n  i*e  l***t
</code></pre>
<p><em>(실제 단어는 본인 지갑에서만 확인해야 한다. 절대 외부에 노출하지 말 것.)</em></p>
<p>이게 지갑의 <strong>마스터 열쇠</strong>다. 이 12개 단어를 순서대로 입력하면 어느 기기에서든 내 지갑을 그대로 복구할 수 있다.</p>
<p>핸드폰을 잃어버렸을 때, 앱을 실수로 삭제했을 때, 새 기기로 바꿨을 때 — 이게 있으면 모든 게 해결된다.</p>
<p>반대로 이걸 잃어버리면 지갑 안에 아무리 많은 코인이 있어도 영영 꺼낼 방법이 없다. MetaMask 고객센터도, 경찰도, 아무도 도와줄 수 없다. 그냥 없어지는 거다.</p>
<p>그래서 보관 방법이 중요하다.</p>
<ul>
<li><strong>스크린샷 저장 — 절대 안 된다.</strong> 갤러리가 클라우드에 자동 백업되면 끝이다.</li>
<li><strong>메모장이나 노트 앱 저장 — 위험하다.</strong> 기기가 해킹당하면 같이 털린다.</li>
<li><strong>카카오톡이나 메신저로 자신에게 전송 — 절대 안 된다.</strong> 계정이 털리는 순간 지갑도 같이 털린다.</li>
</ul>
<p>가장 안전한 방법은 <strong>종이에 직접 적어서 물리적으로 보관</strong>하는 거다. 디지털이 하나도 없으니 해킹으로 털릴 방법이 없다. 오래된 방식처럼 보이는데 이게 진짜 기본이다.</p>
<p>두 군데 이상 나눠서 보관하면 더 좋다.</p>
<hr>
<h2 id="8-검증-안-된-사이트에서-지갑-연결했다가-탈탈-털리는-경우"><a class="anchor" aria-hidden="true" tabindex="-1" href="#8-검증-안-된-사이트에서-지갑-연결했다가-탈탈-털리는-경우">#</a>8. 검증 안 된 사이트에서 지갑 연결했다가 탈탈 털리는 경우</h2>
<p>이건 경험하고 나면 너무 허탈한 케이스다.</p>
<p>"에어드랍 받으려면 지갑 연결하세요", "이 NFT 무료로 민팅하세요" 같은 링크를 눌러서 MetaMask를 연결했다가 지갑 안에 있는 자산이 전부 사라지는 사고가 실제로 자주 일어난다.</p>
<p>지갑을 연결할 때 서명 요청이 오는데, 그 내용을 제대로 읽지 않고 "승인"을 누르면 내 지갑에 대한 권한을 악의적인 스마트컨트랙트에 넘기게 된다. 그러면 그쪽에서 내 코인을 마음대로 가져갈 수 있다.</p>
<p>규칙은 간단하다.</p>
<ul>
<li>공식 사이트 주소를 항상 직접 입력하거나 북마크에서 열 것</li>
<li>출처 모를 링크로 들어간 사이트에서는 지갑 연결 자체를 안 하는 게 낫다</li>
<li>서명 요청이 왔을 때 내용을 이해 못 하겠다면 그냥 거절할 것</li>
</ul>
<p>공짜라는 말에 끌려서 지갑을 연결하는 순간이 가장 위험하다.</p>
<hr>
<h2 id="마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마무리">#</a>마무리</h2>
<p>여기 쓴 것들이 전부 대단한 내용은 아니다.</p>
<p>그냥 "이거 모르면 당할 수 있다" 는 것들이다. 코인 시장 자체의 리스크랑은 별개로, 기본 개념을 몰라서 날리는 건 사실 좀 억울하지 않나.</p>
<p>투자 판단은 각자 알아서 하는 거고, 오를지 내릴지는 아무도 모른다. 그래도 이 정도는 알고 시작하면 최소한 기본적인 실수는 피할 수 있다.</p>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[코인]]></category>
      <category><![CDATA[암호화폐]]></category>
      <category><![CDATA[초보자]]></category>
      <category><![CDATA[비트코인]]></category>
      <category><![CDATA[이더리움]]></category>
      <category><![CDATA[MetaMask]]></category>
      <category><![CDATA[실수]]></category>
      <category><![CDATA[투자]]></category>
    </item>

    <item>
      <title><![CDATA[신입 백엔드 때 진짜로 혼났던 것들 — JPA, 트랜잭션, 예외처리]]></title>
      <link>https://www.stragos.xyz/posts/java-real-mistakes-backend</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/java-real-mistakes-backend</guid>
      <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[교과서엔 없는데 현장에선 기본인 것들. JPA N+1, @Transactional 남발, 예외처리 패턴 등 실제로 코드 리뷰에서 지적받았던 내용들을 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>입사하고 처음 몇 달은 코드가 일단 돌아가면 된다고 생각했다.</p>
<p>그러다 코드 리뷰에서 조용히 코멘트가 달리기 시작했다. "이거 N+1 터집니다", "여기 트랜잭션 범위 다시 확인해보세요", "Entity 그대로 반환하면 안 됩니다" — 다 아는 말 같은데 왜 내 코드가 문제인지 처음엔 잘 몰랐다.</p>
<p>아래는 그때 지적받고 나서야 이해한 것들이다.</p>
<hr>
<h2 id="jpa-n1--모르면-운영서버에서-알게-된다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#jpa-n1--모르면-운영서버에서-알게-된다">#</a>JPA N+1 — 모르면 운영서버에서 알게 된다</h2>
<p>처음에 N+1이 뭔지 설명을 들어도 와닿지 않았다. 직접 겪기 전까진.</p>
<p>이름 자체가 좀 생소한데, 일단 이렇게 생각하면 된다.</p>
<blockquote>
<p><strong>주문 목록을 조회했더니, 각 주문마다 주문한 사람 정보를 따로 또 DB에 물어보는 상황</strong></p>
</blockquote>
<p>코드로 보면 이렇다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">List&#x3C;</span><span style="color:#F97583">Order</span><span style="color:#E1E4E8">> orders </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> orderRepository.</span><span style="color:#B392F0">findAll</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// 쿼리 1번</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">for</span><span style="color:#E1E4E8"> (Order order </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> orders) {</span></span>
<span data-line=""><span style="color:#6A737D">    // 루프 돌 때마다 getMember() 할 때마다 쿼리가 1번씩 더 나간다</span></span>
<span data-line=""><span style="color:#E1E4E8">    System.out.</span><span style="color:#B392F0">println</span><span style="color:#E1E4E8">(order.</span><span style="color:#B392F0">getMember</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">getName</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>주문이 100개면 쿼리가 총 <strong>101번</strong> 나간다. 주문 목록 가져올 때 1번, 루프 돌면서 각 주문 주인 조회할 때 100번.</p>
<p>왜 이런 일이 생기냐면, JPA가 연관된 엔티티(여기선 <code>Member</code>)를 기본적으로 <strong>필요할 때 가져오도록</strong> 설정되어 있기 때문이다. <code>order.getMember()</code> 를 호출하는 그 순간 "아, 멤버가 필요하구나" 하고 그때서야 DB에 쿼리를 날린다. 이걸 <strong>지연 로딩(Lazy Loading)</strong> 이라고 한다.</p>
<p>개발할 때는 데이터가 5~10개라 전혀 모르고 넘어간다. 운영에서 주문이 수천 건 쌓이고 나서야 슬로우쿼리 알람으로 알게 된다.</p>
<p>해결은 단순하다. <strong>처음부터 같이 가져오면 된다.</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// JPQL fetch join — "주문 가져올 때 멤버도 같이 JOIN해서 가져와"</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Query</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"SELECT o FROM Order o JOIN FETCH o.member"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">List</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">Order</span><span style="color:#F97583">></span><span style="color:#B392F0"> findAllWithMember</span><span style="color:#E1E4E8">();</span></span></code></pre></figure>
<p>이렇게 하면 쿼리 1번에 주문 + 멤버 정보를 한 번에 가져온다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="sql" data-theme="github-dark"><code data-language="sql" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">-- fetch join 시 실제로 나가는 쿼리 (1번)</span></span>
<span data-line=""><span style="color:#F97583">SELECT</span><span style="color:#E1E4E8"> o.</span><span style="color:#F97583">*</span><span style="color:#E1E4E8">, m.</span><span style="color:#F97583">*</span></span>
<span data-line=""><span style="color:#F97583">FROM</span><span style="color:#E1E4E8"> orders o</span></span>
<span data-line=""><span style="color:#F97583">INNER JOIN</span><span style="color:#E1E4E8"> member m </span><span style="color:#F97583">ON</span><span style="color:#79B8FF"> o</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">member_id</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> m</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">id</span></span></code></pre></figure>
<p><code>@EntityGraph</code> 를 쓰는 방법도 있다. JPQL 직접 안 쓰고 어노테이션으로 해결할 수 있어서 간단한 경우엔 이게 더 편하다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">EntityGraph</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">attributePaths</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span><span style="color:#9ECBFF">"member"</span><span style="color:#E1E4E8">})</span></span>
<span data-line=""><span style="color:#E1E4E8">List</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">Order</span><span style="color:#F97583">></span><span style="color:#B392F0"> findAll</span><span style="color:#E1E4E8">();</span></span></code></pre></figure>
<p>한 가지 주의할 점은 <code>fetch join</code> 과 페이징을 같이 쓰면 경고가 뜬다. <code>HibernateJpaDialect: firstResult/maxResults specified with collection fetch; applying in memory</code> 이 로그가 보이면 <strong>페이징이 메모리에서 처리되고 있다는 뜻</strong>이다. 데이터 많으면 OOM 난다. 이때는 <code>@BatchSize</code> 나 쿼리 분리로 풀어야 한다.</p>
<hr>
<h2 id="transactional-붙이면-다-해결된다는-착각"><a class="anchor" aria-hidden="true" tabindex="-1" href="#transactional-붙이면-다-해결된다는-착각">#</a><code>@Transactional</code> 붙이면 다 해결된다는 착각</h2>
<p>트랜잭션이 중요하다는 건 알겠는데, 처음엔 그냥 서비스 메서드마다 다 붙이면 되는 거 아닌가 싶었다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Service</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> UserService</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Transactional</span><span style="color:#6A737D"> // 일단 다 붙이고 보자</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> createUser</span><span style="color:#E1E4E8">(UserDto </span><span style="color:#FFAB70">dto</span><span style="color:#E1E4E8">) { ... }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Transactional</span><span style="color:#6A737D"> // 조회인데도 붙임</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> UserDto </span><span style="color:#B392F0">getUser</span><span style="color:#E1E4E8">(Long </span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">) { ... }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Transactional</span><span style="color:#6A737D"> // private 메서드에도 붙임 — 이건 아예 동작 안 함</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> sendWelcomeEmail</span><span style="color:#E1E4E8">(User </span><span style="color:#FFAB70">user</span><span style="color:#E1E4E8">) { ... }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>문제가 몇 가지 있다.</p>
<p><strong>조회에 쓰기 트랜잭션을 열면 불필요한 비용이 생긴다.</strong> 읽기만 하는 메서드엔 <code>@Transactional(readOnly = true)</code> 를 쓰는 게 맞다. DB 입장에선 읽기 전용으로 최적화할 수 있고, 실수로 영속성 컨텍스트에서 변경이 일어나도 flush가 안 된다.</p>
<p><strong><code>private</code> 메서드에 <code>@Transactional</code> 은 아무 효과가 없다.</strong> Spring 트랜잭션은 프록시 기반이라 외부에서 호출되는 <code>public</code> 메서드에서만 동작한다. 이걸 모르고 같은 클래스 내에서 <code>@Transactional</code> 붙은 메서드를 내부 호출하면 트랜잭션이 적용되지 않는다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Service</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> OrderService</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> placeOrder</span><span style="color:#E1E4E8">(OrderDto </span><span style="color:#FFAB70">dto</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#B392F0">        validate</span><span style="color:#E1E4E8">(dto);</span></span>
<span data-line=""><span style="color:#B392F0">        saveOrder</span><span style="color:#E1E4E8">(dto); </span><span style="color:#6A737D">// 이 호출은 트랜잭션 없이 실행됨</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Transactional</span><span style="color:#6A737D"> // 의미없음 — 내부 호출이라 프록시 거치지 않음</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> saveOrder</span><span style="color:#E1E4E8">(OrderDto </span><span style="color:#FFAB70">dto</span><span style="color:#E1E4E8">) { ... }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>의도한 대로 트랜잭션이 걸리길 원하면, 해당 로직을 별도 빈으로 분리하거나 <code>placeOrder</code> 자체에 트랜잭션을 걸어야 한다.</p>
<hr>
<h2 id="entity를-api-응답으로-그냥-반환하면-생기는-일"><a class="anchor" aria-hidden="true" tabindex="-1" href="#entity를-api-응답으로-그냥-반환하면-생기는-일">#</a>Entity를 API 응답으로 그냥 반환하면 생기는 일</h2>
<p>처음에 Controller에서 이렇게 짜면 편하긴 하다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">GetMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/users/{id}"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> User </span><span style="color:#B392F0">getUser</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">PathVariable</span><span style="color:#E1E4E8"> Long id) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> userRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id).</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>근데 이게 쌓이면 나중에 꽤 골치 아파진다.</p>
<p>일단 Entity에 <code>@JsonIgnore</code> 같은 어노테이션이 생기기 시작한다. 비밀번호 필드는 내보내면 안 되니까. 양방향 연관관계가 있으면 JSON 직렬화하다가 무한 루프로 <code>StackOverflowError</code> 가 터지기도 한다. 그리고 나중에 API 스펙을 바꾸려고 하면 DB 구조랑 응답 구조가 엮여있어서 꼼짝을 못 하게 된다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 이렇게 분리하는 게 맞다</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">GetMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/users/{id}"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> UserResponse </span><span style="color:#B392F0">getUser</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">PathVariable</span><span style="color:#E1E4E8"> Long id) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    User user </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> userService.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id);</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> UserResponse.</span><span style="color:#B392F0">from</span><span style="color:#E1E4E8">(user);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 응답 전용 DTO</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> record</span><span style="color:#B392F0"> UserResponse</span><span style="color:#E1E4E8">(Long id, String name, String email) {</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#E1E4E8"> UserResponse </span><span style="color:#B392F0">from</span><span style="color:#E1E4E8">(User </span><span style="color:#FFAB70">user</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> UserResponse</span><span style="color:#E1E4E8">(user.</span><span style="color:#B392F0">getId</span><span style="color:#E1E4E8">(), user.</span><span style="color:#B392F0">getName</span><span style="color:#E1E4E8">(), user.</span><span style="color:#B392F0">getEmail</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>처음엔 클래스가 늘어나는 게 번거롭게 느껴지는데, 한 번 경험하고 나면 당연하게 쓰게 된다.</p>
<hr>
<h2 id="예외처리--그냥-터지게-두거나-모든-걸-잡거나"><a class="anchor" aria-hidden="true" tabindex="-1" href="#예외처리--그냥-터지게-두거나-모든-걸-잡거나">#</a>예외처리 — 그냥 터지게 두거나, 모든 걸 잡거나</h2>
<p>초반에 예외 처리 방식이 둘 중 하나였다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 방법 1 — 그냥 터지게 둠</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> User </span><span style="color:#B392F0">findUser</span><span style="color:#E1E4E8">(Long id) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> userRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id).</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// NoSuchElementException</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 방법 2 — 무조건 잡고 로그만 남김</span></span>
<span data-line=""><span style="color:#F97583">try</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#B392F0">    process</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">} </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (Exception </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    e.</span><span style="color:#B392F0">printStackTrace</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// 이게 운영 코드에 있으면 안 됨</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>둘 다 문제다.</p>
<p><code>.get()</code> 은 값이 없을 때 <code>NoSuchElementException</code> 을 던지는데 어디서 왜 터졌는지 알기 어렵다. <code>Exception</code> 을 통으로 잡고 <code>e.printStackTrace()</code> 만 하면 로그 수집 시스템에 안 찍히는 경우가 많고, 예외가 삼켜지면 호출부에서 정상 처리된 것처럼 흘러간다.</p>
<p>실무에서 많이 쓰는 패턴은 <strong>커스텀 예외 + 전역 핸들러</strong> 조합이다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 커스텀 예외</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> UserNotFoundException</span><span style="color:#F97583"> extends</span><span style="color:#B392F0"> RuntimeException</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#B392F0"> UserNotFoundException</span><span style="color:#E1E4E8">(Long </span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#79B8FF">        super</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"사용자를 찾을 수 없습니다. id="</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> id);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 서비스</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> User </span><span style="color:#B392F0">findUser</span><span style="color:#E1E4E8">(Long id) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> userRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id)</span></span>
<span data-line=""><span style="color:#E1E4E8">        .</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">-></span><span style="color:#F97583"> new</span><span style="color:#B392F0"> UserNotFoundException</span><span style="color:#E1E4E8">(id));</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 전역 예외 핸들러</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RestControllerAdvice</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> GlobalExceptionHandler</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">ExceptionHandler</span><span style="color:#E1E4E8">(UserNotFoundException.class)</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> ResponseEntity&#x3C;</span><span style="color:#F97583">ErrorResponse</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">handleNotFound</span><span style="color:#E1E4E8">(UserNotFoundException </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        log.</span><span style="color:#B392F0">warn</span><span style="color:#E1E4E8">(e.</span><span style="color:#B392F0">getMessage</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">status</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">404</span><span style="color:#E1E4E8">).</span><span style="color:#B392F0">body</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">new</span><span style="color:#B392F0"> ErrorResponse</span><span style="color:#E1E4E8">(e.</span><span style="color:#B392F0">getMessage</span><span style="color:#E1E4E8">()));</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>이렇게 구조를 잡으면 예외가 터졌을 때 어디서 뭐가 문제인지 바로 보인다. 그리고 Controller마다 try-catch를 반복할 필요도 없어진다.</p>
<hr>
<h2 id="테스트-없이-배포하는-습관"><a class="anchor" aria-hidden="true" tabindex="-1" href="#테스트-없이-배포하는-습관">#</a>테스트 없이 배포하는 습관</h2>
<p>이건 기술 문제가 아닌데 넣는 이유가 있다.</p>
<p>신입 때 "일단 배포해보고 문제 생기면 고친다" 는 생각을 은근히 했다. 빠르게 치고 나가고 싶은 마음도 있었고, 테스트 짜는 시간이 아깝게 느껴지기도 했다.</p>
<p>근데 운영 서버에서 오류가 나면 디버깅하는 시간이 테스트 코드 짜는 시간보다 훨씬 길다. 특히 다른 사람 코드를 건드린 경우는 더 그렇다.</p>
<p>최소한 서비스 레이어 핵심 로직에는 테스트를 붙이는 게 낫다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Test</span></span>
<span data-line=""><span style="color:#F97583">void</span><span style="color:#E1E4E8"> 사용자</span><span style="color:#B392F0">_생성_성공</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#6A737D">    // given</span></span>
<span data-line=""><span style="color:#E1E4E8">    UserCreateRequest request </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> UserCreateRequest</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"홍길동"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"test@example.com"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // when</span></span>
<span data-line=""><span style="color:#E1E4E8">    UserResponse response </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> userService.</span><span style="color:#B392F0">createUser</span><span style="color:#E1E4E8">(request);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // then</span></span>
<span data-line=""><span style="color:#B392F0">    assertThat</span><span style="color:#E1E4E8">(response.</span><span style="color:#B392F0">name</span><span style="color:#E1E4E8">()).</span><span style="color:#B392F0">isEqualTo</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"홍길동"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#B392F0">    assertThat</span><span style="color:#E1E4E8">(response.</span><span style="color:#B392F0">email</span><span style="color:#E1E4E8">()).</span><span style="color:#B392F0">isEqualTo</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"test@example.com"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>given/when/then 구조로 짜면 나중에 읽을 때 뭘 테스트하는지 바로 보인다. 처음엔 어색하지만 한 달만 해보면 없는 게 더 불안해진다.</p>
<hr>
<h2 id="마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마무리">#</a>마무리</h2>
<p>솔직히 위에 적은 것들은 다 "들어봤다"에서 끝나지 않고 <strong>직접 겪어야</strong> 제대로 느껴진다.</p>
<p>그래도 미리 알고 있으면 최소한 어디서 막혔을 때 "아, 이거 그 얘기구나" 하고 빠르게 연결이 된다. 그게 있는 것과 없는 것의 차이가 꽤 크다.</p>
<p>JPA나 트랜잭션 쪽은 특히 Spring Boot 프로젝트라면 거의 반드시 마주치는 주제니까, 한 번씩 직접 실험해보는 걸 추천한다.</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[JPA]]></category>
      <category><![CDATA[신입개발자]]></category>
      <category><![CDATA[백엔드]]></category>
      <category><![CDATA[실무]]></category>
    </item>

    <item>
      <title><![CDATA[Spring Boot + React + Oracle + MyBatis 셋팅 — 4편: React 프로젝트 생성 & API 연동]]></title>
      <link>https://www.stragos.xyz/posts/springboot-react-oracle-mybatis-setup-4</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/springboot-react-oracle-mybatis-setup-4</guid>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[Vite로 React 프로젝트를 만들고 axios로 Spring Boot API를 호출해 회원 목록을 화면에 출력합니다. 프론트와 백이 연결되는 순간입니다.]]></description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>시리즈 목차</strong></p>
<ul>
<li>1편: 개발 환경 준비</li>
<li>2편: Spring Boot 프로젝트 생성 &#x26; Oracle 연결</li>
<li>3편: MyBatis 설정 &#x26; CRUD API 만들기</li>
<li><strong>4편: React 프로젝트 생성 &#x26; Spring Boot API 연동</strong> ← 지금 여기</li>
</ul>
</blockquote>
<hr>
<h2 id="react-프로젝트-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#react-프로젝트-만들기">#</a>React 프로젝트 만들기</h2>
<p>터미널을 열고 프로젝트를 만들 상위 폴더로 이동 후 실행합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> create</span><span style="color:#9ECBFF"> vite@latest</span><span style="color:#9ECBFF"> frontend</span><span style="color:#79B8FF"> --</span><span style="color:#79B8FF"> --template</span><span style="color:#9ECBFF"> react</span></span>
<span data-line=""><span style="color:#79B8FF">cd</span><span style="color:#9ECBFF"> frontend</span></span>
<span data-line=""><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span></span></code></pre></figure>
<blockquote>
<p><strong>왜 Vite인가요?</strong> 예전엔 <code>create-react-app</code>을 많이 썼는데, 빌드 속도가 너무 느립니다. 요즘은 <strong>Vite</strong>가 표준입니다. 개발 서버 시작이 1~2초면 됩니다.</p>
</blockquote>
<hr>
<h2 id="폴더-구조-정리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#폴더-구조-정리">#</a>폴더 구조 정리</h2>
<p>기본 생성된 파일 중 불필요한 것을 정리합니다.</p>
<pre><code>frontend/
├── src/
│   ├── api/           ← 새로 만들기 (API 호출 모음)
│   ├── components/    ← 새로 만들기 (재사용 컴포넌트)
│   ├── pages/         ← 새로 만들기 (페이지 컴포넌트)
│   ├── App.jsx
│   └── main.jsx
├── index.html
└── package.json
</code></pre>
<p><code>src/App.css</code>, <code>src/assets/react.svg</code> 등 기본 파일은 삭제해도 됩니다.</p>
<hr>
<h2 id="axios-설치"><a class="anchor" aria-hidden="true" tabindex="-1" href="#axios-설치">#</a>axios 설치</h2>
<p>API 호출에 axios를 씁니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> axios</span></span></code></pre></figure>
<hr>
<h2 id="api-설정-파일-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#api-설정-파일-만들기">#</a>API 설정 파일 만들기</h2>
<p><code>src/api/api.js</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="javascript" data-theme="github-dark"><code data-language="javascript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> axios </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'axios'</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">const</span><span style="color:#79B8FF"> api</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> axios.</span><span style="color:#B392F0">create</span><span style="color:#E1E4E8">({</span></span>
<span data-line=""><span style="color:#E1E4E8">  baseURL: </span><span style="color:#9ECBFF">'http://localhost:8080/api'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">  headers: {</span></span>
<span data-line=""><span style="color:#9ECBFF">    'Content-Type'</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'application/json'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">  },</span></span>
<span data-line=""><span style="color:#E1E4E8">});</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#E1E4E8"> api;</span></span></code></pre></figure>
<p><code>baseURL</code>을 여기서 한 번만 설정하면, 이후에는 <code>/members</code>처럼 경로만 쓸 수 있습니다.</p>
<hr>
<h2 id="회원-api-함수-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#회원-api-함수-만들기">#</a>회원 API 함수 만들기</h2>
<p><code>src/api/memberApi.js</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="javascript" data-theme="github-dark"><code data-language="javascript" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> api </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> './api'</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 전체 조회</span></span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#B392F0"> getMembers</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> () </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> api.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'/members'</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 단건 조회</span></span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#B392F0"> getMember</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> api.</span><span style="color:#B392F0">get</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">`/members/${</span><span style="color:#E1E4E8">id</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 등록</span></span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#B392F0"> createMember</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">data</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> api.</span><span style="color:#B392F0">post</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'/members'</span><span style="color:#E1E4E8">, data);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 수정</span></span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#B392F0"> updateMember</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">data</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> api.</span><span style="color:#B392F0">put</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">`/members/${</span><span style="color:#E1E4E8">id</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">, data);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 삭제</span></span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> const</span><span style="color:#B392F0"> deleteMember</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> api.</span><span style="color:#B392F0">delete</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">`/members/${</span><span style="color:#E1E4E8">id</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">);</span></span></code></pre></figure>
<hr>
<h2 id="회원-목록-페이지-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#회원-목록-페이지-만들기">#</a>회원 목록 페이지 만들기</h2>
<p><code>src/pages/MemberList.jsx</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="jsx" data-theme="github-dark"><code data-language="jsx" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { useEffect, useState } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'react'</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { getMembers, deleteMember } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '../api/memberApi'</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> MemberForm </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '../components/MemberForm'</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> MemberList</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">members</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setMembers</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">([]);</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">loading</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setLoading</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">showForm</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setShowForm</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">editTarget</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setEditTarget</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">null</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">  // 목록 불러오기</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#B392F0"> fetchMembers</span><span style="color:#F97583"> =</span><span style="color:#F97583"> async</span><span style="color:#E1E4E8"> () </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    try</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">      const</span><span style="color:#79B8FF"> res</span><span style="color:#F97583"> =</span><span style="color:#F97583"> await</span><span style="color:#B392F0"> getMembers</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#B392F0">      setMembers</span><span style="color:#E1E4E8">(res.data);</span></span>
<span data-line=""><span style="color:#E1E4E8">    } </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (err) {</span></span>
<span data-line=""><span style="color:#E1E4E8">      console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'목록 조회 실패:'</span><span style="color:#E1E4E8">, err);</span></span>
<span data-line=""><span style="color:#E1E4E8">    } </span><span style="color:#F97583">finally</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#B392F0">      setLoading</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">  };</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#B392F0">  useEffect</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#B392F0">    fetchMembers</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">  }, []);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">  // 삭제</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#B392F0"> handleDelete</span><span style="color:#F97583"> =</span><span style="color:#F97583"> async</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">window.</span><span style="color:#B392F0">confirm</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'정말 삭제하시겠습니까?'</span><span style="color:#E1E4E8">)) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#F97583">    try</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">      await</span><span style="color:#B392F0"> deleteMember</span><span style="color:#E1E4E8">(id);</span></span>
<span data-line=""><span style="color:#B392F0">      fetchMembers</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// 목록 새로고침</span></span>
<span data-line=""><span style="color:#E1E4E8">    } </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (err) {</span></span>
<span data-line=""><span style="color:#B392F0">      alert</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'삭제 실패'</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">  };</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">  // 수정 버튼</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#B392F0"> handleEdit</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">member</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#B392F0">    setEditTarget</span><span style="color:#E1E4E8">(member);</span></span>
<span data-line=""><span style="color:#B392F0">    setShowForm</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">  };</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">  // 폼 닫기</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#B392F0"> handleFormClose</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> () </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#B392F0">    setShowForm</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#B392F0">    setEditTarget</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">null</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#B392F0">    fetchMembers</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">  };</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  if</span><span style="color:#E1E4E8"> (loading) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">>불러오는 중...&#x3C;/</span><span style="color:#85E89D">p</span><span style="color:#E1E4E8">>;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ padding: </span><span style="color:#9ECBFF">'24px'</span><span style="color:#E1E4E8">, maxWidth: </span><span style="color:#9ECBFF">'800px'</span><span style="color:#E1E4E8">, margin: </span><span style="color:#9ECBFF">'0 auto'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ display: </span><span style="color:#9ECBFF">'flex'</span><span style="color:#E1E4E8">, justifyContent: </span><span style="color:#9ECBFF">'space-between'</span><span style="color:#E1E4E8">, alignItems: </span><span style="color:#9ECBFF">'center'</span><span style="color:#E1E4E8">, marginBottom: </span><span style="color:#9ECBFF">'16px'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">h1</span><span style="color:#E1E4E8">>회원 목록&#x3C;/</span><span style="color:#85E89D">h1</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">button</span></span>
<span data-line=""><span style="color:#B392F0">          onClick</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{() </span><span style="color:#F97583">=></span><span style="color:#B392F0"> setShowForm</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">)}</span></span>
<span data-line=""><span style="color:#B392F0">          style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ padding: </span><span style="color:#9ECBFF">'8px 16px'</span><span style="color:#E1E4E8">, background: </span><span style="color:#9ECBFF">'#4a90d9'</span><span style="color:#E1E4E8">, color: </span><span style="color:#9ECBFF">'white'</span><span style="color:#E1E4E8">, border: </span><span style="color:#9ECBFF">'none'</span><span style="color:#E1E4E8">, borderRadius: </span><span style="color:#9ECBFF">'6px'</span><span style="color:#E1E4E8">, cursor: </span><span style="color:#9ECBFF">'pointer'</span><span style="color:#E1E4E8"> }}</span></span>
<span data-line=""><span style="color:#E1E4E8">        ></span></span>
<span data-line=""><span style="color:#E1E4E8">          + 회원 등록</span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;/</span><span style="color:#85E89D">button</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">      &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">      {showForm </span><span style="color:#F97583">&#x26;&#x26;</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#79B8FF">MemberForm</span></span>
<span data-line=""><span style="color:#B392F0">          editTarget</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{editTarget}</span></span>
<span data-line=""><span style="color:#B392F0">          onClose</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{handleFormClose}</span></span>
<span data-line=""><span style="color:#E1E4E8">        /></span></span>
<span data-line=""><span style="color:#E1E4E8">      )}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">table</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ width: </span><span style="color:#9ECBFF">'100%'</span><span style="color:#E1E4E8">, borderCollapse: </span><span style="color:#9ECBFF">'collapse'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">thead</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;</span><span style="color:#85E89D">tr</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ background: </span><span style="color:#9ECBFF">'#f0f0f0'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">            &#x3C;</span><span style="color:#85E89D">th</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{thStyle}>ID&#x3C;/</span><span style="color:#85E89D">th</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">            &#x3C;</span><span style="color:#85E89D">th</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{thStyle}>이름&#x3C;/</span><span style="color:#85E89D">th</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">            &#x3C;</span><span style="color:#85E89D">th</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{thStyle}>이메일&#x3C;/</span><span style="color:#85E89D">th</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">            &#x3C;</span><span style="color:#85E89D">th</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{thStyle}>전화번호&#x3C;/</span><span style="color:#85E89D">th</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">            &#x3C;</span><span style="color:#85E89D">th</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{thStyle}>등록일&#x3C;/</span><span style="color:#85E89D">th</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">            &#x3C;</span><span style="color:#85E89D">th</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{thStyle}>관리&#x3C;/</span><span style="color:#85E89D">th</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;/</span><span style="color:#85E89D">tr</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;/</span><span style="color:#85E89D">thead</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">tbody</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">          {members.</span><span style="color:#79B8FF">length</span><span style="color:#F97583"> ===</span><span style="color:#79B8FF"> 0</span><span style="color:#F97583"> ?</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">            &#x3C;</span><span style="color:#85E89D">tr</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">              &#x3C;</span><span style="color:#85E89D">td</span><span style="color:#B392F0"> colSpan</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#79B8FF">6</span><span style="color:#E1E4E8">} </span><span style="color:#B392F0">style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ textAlign: </span><span style="color:#9ECBFF">'center'</span><span style="color:#E1E4E8">, padding: </span><span style="color:#9ECBFF">'24px'</span><span style="color:#E1E4E8">, color: </span><span style="color:#9ECBFF">'#888'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">                등록된 회원이 없습니다.</span></span>
<span data-line=""><span style="color:#E1E4E8">              &#x3C;/</span><span style="color:#85E89D">td</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">            &#x3C;/</span><span style="color:#85E89D">tr</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">          ) </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">            members.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">m</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">              &#x3C;</span><span style="color:#85E89D">tr</span><span style="color:#B392F0"> key</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{m.memberId} </span><span style="color:#B392F0">style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ borderBottom: </span><span style="color:#9ECBFF">'1px solid #eee'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">                &#x3C;</span><span style="color:#85E89D">td</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{tdStyle}>{m.memberId}&#x3C;/</span><span style="color:#85E89D">td</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">                &#x3C;</span><span style="color:#85E89D">td</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{tdStyle}>{m.userName}&#x3C;/</span><span style="color:#85E89D">td</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">                &#x3C;</span><span style="color:#85E89D">td</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{tdStyle}>{m.email}&#x3C;/</span><span style="color:#85E89D">td</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">                &#x3C;</span><span style="color:#85E89D">td</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{tdStyle}>{m.phone </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> '-'</span><span style="color:#E1E4E8">}&#x3C;/</span><span style="color:#85E89D">td</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">                &#x3C;</span><span style="color:#85E89D">td</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{tdStyle}>{m.regDate}&#x3C;/</span><span style="color:#85E89D">td</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">                &#x3C;</span><span style="color:#85E89D">td</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{tdStyle}></span></span>
<span data-line=""><span style="color:#E1E4E8">                  &#x3C;</span><span style="color:#85E89D">button</span><span style="color:#B392F0"> onClick</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{() </span><span style="color:#F97583">=></span><span style="color:#B392F0"> handleEdit</span><span style="color:#E1E4E8">(m)} </span><span style="color:#B392F0">style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#B392F0">btnStyle</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'#f0a500'</span><span style="color:#E1E4E8">)}>수정&#x3C;/</span><span style="color:#85E89D">button</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">                  {</span><span style="color:#9ECBFF">' '</span><span style="color:#E1E4E8">}</span></span>
<span data-line=""><span style="color:#E1E4E8">                  &#x3C;</span><span style="color:#85E89D">button</span><span style="color:#B392F0"> onClick</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{() </span><span style="color:#F97583">=></span><span style="color:#B392F0"> handleDelete</span><span style="color:#E1E4E8">(m.memberId)} </span><span style="color:#B392F0">style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#B392F0">btnStyle</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'#e53935'</span><span style="color:#E1E4E8">)}>삭제&#x3C;/</span><span style="color:#85E89D">button</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">                &#x3C;/</span><span style="color:#85E89D">td</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">              &#x3C;/</span><span style="color:#85E89D">tr</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">            ))</span></span>
<span data-line=""><span style="color:#E1E4E8">          )}</span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;/</span><span style="color:#85E89D">tbody</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">      &#x3C;/</span><span style="color:#85E89D">table</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">  );</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">const</span><span style="color:#79B8FF"> thStyle</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> { padding: </span><span style="color:#9ECBFF">'10px 12px'</span><span style="color:#E1E4E8">, textAlign: </span><span style="color:#9ECBFF">'left'</span><span style="color:#E1E4E8">, fontWeight: </span><span style="color:#9ECBFF">'600'</span><span style="color:#E1E4E8"> };</span></span>
<span data-line=""><span style="color:#F97583">const</span><span style="color:#79B8FF"> tdStyle</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> { padding: </span><span style="color:#9ECBFF">'10px 12px'</span><span style="color:#E1E4E8"> };</span></span>
<span data-line=""><span style="color:#F97583">const</span><span style="color:#B392F0"> btnStyle</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">bg</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> ({</span></span>
<span data-line=""><span style="color:#E1E4E8">  padding: </span><span style="color:#9ECBFF">'4px 10px'</span><span style="color:#E1E4E8">, background: bg, color: </span><span style="color:#9ECBFF">'white'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">  border: </span><span style="color:#9ECBFF">'none'</span><span style="color:#E1E4E8">, borderRadius: </span><span style="color:#9ECBFF">'4px'</span><span style="color:#E1E4E8">, cursor: </span><span style="color:#9ECBFF">'pointer'</span><span style="color:#E1E4E8">, fontSize: </span><span style="color:#9ECBFF">'12px'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">});</span></span></code></pre></figure>
<hr>
<h2 id="회원-등록수정-폼-컴포넌트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#회원-등록수정-폼-컴포넌트">#</a>회원 등록/수정 폼 컴포넌트</h2>
<p><code>src/components/MemberForm.jsx</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="jsx" data-theme="github-dark"><code data-language="jsx" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { useState, useEffect } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'react'</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> { createMember, updateMember } </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> '../api/memberApi'</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> MemberForm</span><span style="color:#E1E4E8">({ </span><span style="color:#FFAB70">editTarget</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">onClose</span><span style="color:#E1E4E8"> }) {</span></span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#E1E4E8"> [</span><span style="color:#79B8FF">form</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">setForm</span><span style="color:#E1E4E8">] </span><span style="color:#F97583">=</span><span style="color:#B392F0"> useState</span><span style="color:#E1E4E8">({</span></span>
<span data-line=""><span style="color:#E1E4E8">    userName: </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    email: </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    phone: </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">  });</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">  // 수정일 경우 기존 데이터 채우기</span></span>
<span data-line=""><span style="color:#B392F0">  useEffect</span><span style="color:#E1E4E8">(() </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (editTarget) {</span></span>
<span data-line=""><span style="color:#B392F0">      setForm</span><span style="color:#E1E4E8">({</span></span>
<span data-line=""><span style="color:#E1E4E8">        userName: editTarget.userName,</span></span>
<span data-line=""><span style="color:#E1E4E8">        email: editTarget.email,</span></span>
<span data-line=""><span style="color:#E1E4E8">        phone: editTarget.phone </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">      });</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">  }, [editTarget]);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#B392F0"> handleChange</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#B392F0">    setForm</span><span style="color:#E1E4E8">({ </span><span style="color:#F97583">...</span><span style="color:#E1E4E8">form, [e.target.name]: e.target.value });</span></span>
<span data-line=""><span style="color:#E1E4E8">  };</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  const</span><span style="color:#B392F0"> handleSubmit</span><span style="color:#F97583"> =</span><span style="color:#F97583"> async</span><span style="color:#E1E4E8"> (</span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">    e.</span><span style="color:#B392F0">preventDefault</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#F97583">    try</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">      if</span><span style="color:#E1E4E8"> (editTarget) {</span></span>
<span data-line=""><span style="color:#F97583">        await</span><span style="color:#B392F0"> updateMember</span><span style="color:#E1E4E8">(editTarget.memberId, form);</span></span>
<span data-line=""><span style="color:#B392F0">        alert</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'수정 완료!'</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">      } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">        await</span><span style="color:#B392F0"> createMember</span><span style="color:#E1E4E8">(form);</span></span>
<span data-line=""><span style="color:#B392F0">        alert</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'등록 완료!'</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">      }</span></span>
<span data-line=""><span style="color:#B392F0">      onClose</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    } </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (err) {</span></span>
<span data-line=""><span style="color:#B392F0">      alert</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'저장 실패: '</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> err.message);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">  };</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{</span></span>
<span data-line=""><span style="color:#E1E4E8">      background: </span><span style="color:#9ECBFF">'#f9f9f9'</span><span style="color:#E1E4E8">, border: </span><span style="color:#9ECBFF">'1px solid #ddd'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">      borderRadius: </span><span style="color:#9ECBFF">'8px'</span><span style="color:#E1E4E8">, padding: </span><span style="color:#9ECBFF">'20px'</span><span style="color:#E1E4E8">, marginBottom: </span><span style="color:#9ECBFF">'20px'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    }}></span></span>
<span data-line=""><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">h3</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ marginTop: </span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8"> }}>{editTarget </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> '회원 수정'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> '회원 등록'</span><span style="color:#E1E4E8">}&#x3C;/</span><span style="color:#85E89D">h3</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">form</span><span style="color:#B392F0"> onSubmit</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{handleSubmit}></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ marginBottom: </span><span style="color:#9ECBFF">'12px'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;</span><span style="color:#85E89D">label</span><span style="color:#E1E4E8">>이름 *&#x3C;/</span><span style="color:#85E89D">label</span><span style="color:#E1E4E8">>&#x3C;</span><span style="color:#85E89D">br</span><span style="color:#E1E4E8"> /></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;</span><span style="color:#85E89D">input</span></span>
<span data-line=""><span style="color:#B392F0">            name</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"userName"</span></span>
<span data-line=""><span style="color:#B392F0">            value</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{form.userName}</span></span>
<span data-line=""><span style="color:#B392F0">            onChange</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{handleChange}</span></span>
<span data-line=""><span style="color:#B392F0">            required</span></span>
<span data-line=""><span style="color:#B392F0">            style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{inputStyle}</span></span>
<span data-line=""><span style="color:#E1E4E8">          /></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ marginBottom: </span><span style="color:#9ECBFF">'12px'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;</span><span style="color:#85E89D">label</span><span style="color:#E1E4E8">>이메일 *&#x3C;/</span><span style="color:#85E89D">label</span><span style="color:#E1E4E8">>&#x3C;</span><span style="color:#85E89D">br</span><span style="color:#E1E4E8"> /></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;</span><span style="color:#85E89D">input</span></span>
<span data-line=""><span style="color:#B392F0">            name</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"email"</span></span>
<span data-line=""><span style="color:#B392F0">            type</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"email"</span></span>
<span data-line=""><span style="color:#B392F0">            value</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{form.email}</span></span>
<span data-line=""><span style="color:#B392F0">            onChange</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{handleChange}</span></span>
<span data-line=""><span style="color:#B392F0">            required</span></span>
<span data-line=""><span style="color:#B392F0">            style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{inputStyle}</span></span>
<span data-line=""><span style="color:#E1E4E8">          /></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ marginBottom: </span><span style="color:#9ECBFF">'16px'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;</span><span style="color:#85E89D">label</span><span style="color:#E1E4E8">>전화번호&#x3C;/</span><span style="color:#85E89D">label</span><span style="color:#E1E4E8">>&#x3C;</span><span style="color:#85E89D">br</span><span style="color:#E1E4E8"> /></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;</span><span style="color:#85E89D">input</span></span>
<span data-line=""><span style="color:#B392F0">            name</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"phone"</span></span>
<span data-line=""><span style="color:#B392F0">            value</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{form.phone}</span></span>
<span data-line=""><span style="color:#B392F0">            onChange</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{handleChange}</span></span>
<span data-line=""><span style="color:#B392F0">            placeholder</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"010-0000-0000"</span></span>
<span data-line=""><span style="color:#B392F0">            style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{inputStyle}</span></span>
<span data-line=""><span style="color:#E1E4E8">          /></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">div</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ display: </span><span style="color:#9ECBFF">'flex'</span><span style="color:#E1E4E8">, gap: </span><span style="color:#9ECBFF">'8px'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;</span><span style="color:#85E89D">button</span><span style="color:#B392F0"> type</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"submit"</span><span style="color:#B392F0"> style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ padding: </span><span style="color:#9ECBFF">'8px 20px'</span><span style="color:#E1E4E8">, background: </span><span style="color:#9ECBFF">'#4a90d9'</span><span style="color:#E1E4E8">, color: </span><span style="color:#9ECBFF">'white'</span><span style="color:#E1E4E8">, border: </span><span style="color:#9ECBFF">'none'</span><span style="color:#E1E4E8">, borderRadius: </span><span style="color:#9ECBFF">'6px'</span><span style="color:#E1E4E8">, cursor: </span><span style="color:#9ECBFF">'pointer'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">            {editTarget </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> '수정'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> '등록'</span><span style="color:#E1E4E8">}</span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;/</span><span style="color:#85E89D">button</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;</span><span style="color:#85E89D">button</span><span style="color:#B392F0"> type</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"button"</span><span style="color:#B392F0"> onClick</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{onClose} </span><span style="color:#B392F0">style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{{ padding: </span><span style="color:#9ECBFF">'8px 20px'</span><span style="color:#E1E4E8">, background: </span><span style="color:#9ECBFF">'#888'</span><span style="color:#E1E4E8">, color: </span><span style="color:#9ECBFF">'white'</span><span style="color:#E1E4E8">, border: </span><span style="color:#9ECBFF">'none'</span><span style="color:#E1E4E8">, borderRadius: </span><span style="color:#9ECBFF">'6px'</span><span style="color:#E1E4E8">, cursor: </span><span style="color:#9ECBFF">'pointer'</span><span style="color:#E1E4E8"> }}></span></span>
<span data-line=""><span style="color:#E1E4E8">            취소</span></span>
<span data-line=""><span style="color:#E1E4E8">          &#x3C;/</span><span style="color:#85E89D">button</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">      &#x3C;/</span><span style="color:#85E89D">form</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">div</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">  );</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">const</span><span style="color:#79B8FF"> inputStyle</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">  width: </span><span style="color:#9ECBFF">'100%'</span><span style="color:#E1E4E8">, padding: </span><span style="color:#9ECBFF">'8px 10px'</span><span style="color:#E1E4E8">, marginTop: </span><span style="color:#9ECBFF">'4px'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">  border: </span><span style="color:#9ECBFF">'1px solid #ccc'</span><span style="color:#E1E4E8">, borderRadius: </span><span style="color:#9ECBFF">'4px'</span><span style="color:#E1E4E8">, fontSize: </span><span style="color:#9ECBFF">'14px'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">  boxSizing: </span><span style="color:#9ECBFF">'border-box'</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">};</span></span></code></pre></figure>
<hr>
<h2 id="appjsx-수정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#appjsx-수정">#</a>App.jsx 수정</h2>
<p><code>src/App.jsx</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="jsx" data-theme="github-dark"><code data-language="jsx" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> MemberList </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> './pages/MemberList'</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">function</span><span style="color:#B392F0"> App</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">  return</span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#79B8FF">MemberList</span><span style="color:#E1E4E8"> />;</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">export</span><span style="color:#F97583"> default</span><span style="color:#E1E4E8"> App;</span></span></code></pre></figure>
<hr>
<h2 id="실행-및-테스트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#실행-및-테스트">#</a>실행 및 테스트</h2>
<p>터미널 두 개를 열어서 각각 실행합니다.</p>
<p><strong>터미널 1 — Spring Boot:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D"># IntelliJ에서 실행하거나</span></span>
<span data-line=""><span style="color:#79B8FF">cd</span><span style="color:#9ECBFF"> backend</span></span>
<span data-line=""><span style="color:#B392F0">./gradlew</span><span style="color:#9ECBFF"> bootRun</span></span></code></pre></figure>
<p><strong>터미널 2 — React:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#79B8FF">cd</span><span style="color:#9ECBFF"> frontend</span></span>
<span data-line=""><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> run</span><span style="color:#9ECBFF"> dev</span></span></code></pre></figure>
<p>브라우저에서 <code>http://localhost:5173</code> 접속 (Vite 기본 포트)</p>
<p>회원 목록이 보이면 성공입니다!</p>
<hr>
<h2 id="자주-나오는-에러"><a class="anchor" aria-hidden="true" tabindex="-1" href="#자주-나오는-에러">#</a>자주 나오는 에러</h2>
<h3 id="cors-에러"><a class="anchor" aria-hidden="true" tabindex="-1" href="#cors-에러">#</a>CORS 에러</h3>
<pre><code>Access to XMLHttpRequest at 'http://localhost:8080/api/members' 
from origin 'http://localhost:5173' has been blocked by CORS policy
</code></pre>
<p>3편에서 만든 <code>WebConfig.java</code>의 <code>allowedOrigins</code>에 <code>5173</code> 포트를 추가합니다:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">.</span><span style="color:#B392F0">allowedOrigins</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"http://localhost:3000"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"http://localhost:5173"</span><span style="color:#E1E4E8">)</span></span></code></pre></figure>
<h3 id="network-error"><a class="anchor" aria-hidden="true" tabindex="-1" href="#network-error">#</a>Network Error</h3>
<p>Spring Boot 서버가 실행 중인지 확인. 서버가 꺼져 있으면 axios 요청 자체가 실패합니다.</p>
<hr>
<h2 id="시리즈-마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#시리즈-마무리">#</a>시리즈 마무리</h2>
<p>4편에 걸쳐 아래 스택을 처음부터 완성했습니다:</p>
<pre><code>React (Vite, 5173)
  │  axios HTTP 요청
  ▼
Spring Boot (8080)
  │  MemberController → MemberService → MemberMapper
  ▼
Oracle XE (1521)
  └  MEMBER 테이블
</code></pre>
<p>실무에서 쓰는 구조와 거의 동일합니다. 여기서 테이블과 기능을 추가해가면서 진짜 프로젝트로 키울 수 있습니다.</p>
<h3 id="다음에-추가해볼-것"><a class="anchor" aria-hidden="true" tabindex="-1" href="#다음에-추가해볼-것">#</a>다음에 추가해볼 것</h3>
<ul>
<li><strong>페이징 처리</strong>: MyBatis + Oracle <code>ROWNUM</code>으로 페이징</li>
<li><strong>검색 기능</strong>: MyBatis <code>&#x3C;if></code> 동적 쿼리</li>
<li><strong>JWT 인증</strong>: Spring Security + JWT 토큰</li>
<li><strong>파일 업로드</strong>: MultipartFile + Oracle BLOB</li>
<li><strong>React Router</strong>: 여러 페이지 전환</li>
</ul>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[React]]></category>
      <category><![CDATA[SpringBoot]]></category>
      <category><![CDATA[axios]]></category>
      <category><![CDATA[풀스택]]></category>
      <category><![CDATA[초보자]]></category>
      <category><![CDATA[프론트엔드]]></category>
    </item>

    <item>
      <title><![CDATA[Spring Boot + React + Oracle + MyBatis 셋팅 — 3편: MyBatis 설정 & CRUD API 만들기]]></title>
      <link>https://www.stragos.xyz/posts/springboot-react-oracle-mybatis-setup-3</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/springboot-react-oracle-mybatis-setup-3</guid>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[MyBatis XML 매퍼를 작성해 회원 테이블 CRUD API를 완성합니다. 실무에서 쓰는 패턴 그대로 따라합니다.]]></description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>시리즈 목차</strong></p>
<ul>
<li>1편: 개발 환경 준비</li>
<li>2편: Spring Boot 프로젝트 생성 &#x26; Oracle 연결</li>
<li><strong>3편: MyBatis 설정 &#x26; CRUD API 만들기</strong> ← 지금 여기</li>
<li>4편: React 프로젝트 생성 &#x26; Spring Boot API 연동</li>
</ul>
</blockquote>
<hr>
<h2 id="이번-편에서-만들-것"><a class="anchor" aria-hidden="true" tabindex="-1" href="#이번-편에서-만들-것">#</a>이번 편에서 만들 것</h2>
<p>Oracle에 <code>MEMBER</code> 테이블을 만들고, 아래 4가지 API를 완성합니다.</p>
<table>
<thead>
<tr>
<th>HTTP 메서드</th>
<th>URL</th>
<th>기능</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET</td>
<td><code>/api/members</code></td>
<td>전체 회원 조회</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/members/{id}</code></td>
<td>특정 회원 조회</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/members</code></td>
<td>회원 등록</td>
</tr>
<tr>
<td>PUT</td>
<td><code>/api/members/{id}</code></td>
<td>회원 수정</td>
</tr>
<tr>
<td>DELETE</td>
<td><code>/api/members/{id}</code></td>
<td>회원 삭제</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="1-oracle-테이블-생성"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-oracle-테이블-생성">#</a>1. Oracle 테이블 생성</h2>
<p>DBeaver를 열고 <code>devuser</code> 계정으로 접속 후, SQL 에디터에서 실행합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="sql" data-theme="github-dark"><code data-language="sql" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">-- 회원 테이블</span></span>
<span data-line=""><span style="color:#F97583">CREATE</span><span style="color:#F97583"> TABLE</span><span style="color:#B392F0"> MEMBER</span><span style="color:#E1E4E8"> (</span></span>
<span data-line=""><span style="color:#E1E4E8">    MEMBER_ID   </span><span style="color:#F97583">NUMBER</span><span style="color:#F97583">          NOT NULL</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    USER_NAME   </span><span style="color:#F97583">VARCHAR2</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">50</span><span style="color:#E1E4E8">)    </span><span style="color:#F97583">NOT NULL</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    EMAIL       </span><span style="color:#F97583">VARCHAR2</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">100</span><span style="color:#E1E4E8">)   </span><span style="color:#F97583">NOT NULL</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">    PHONE       </span><span style="color:#F97583">VARCHAR2</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">20</span><span style="color:#E1E4E8">),</span></span>
<span data-line=""><span style="color:#E1E4E8">    REG_DATE    </span><span style="color:#F97583">DATE</span><span style="color:#F97583">            DEFAULT</span><span style="color:#F97583"> SYSDATE</span><span style="color:#F97583"> NOT NULL</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#F97583">    CONSTRAINT</span><span style="color:#E1E4E8"> PK_MEMBER </span><span style="color:#F97583">PRIMARY KEY</span><span style="color:#E1E4E8"> (MEMBER_ID)</span></span>
<span data-line=""><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">-- 시퀀스 (자동 증가 PK용)</span></span>
<span data-line=""><span style="color:#F97583">CREATE</span><span style="color:#F97583"> SEQUENCE</span><span style="color:#B392F0"> SEQ_MEMBER</span></span>
<span data-line=""><span style="color:#F97583">    START</span><span style="color:#F97583"> WITH</span><span style="color:#79B8FF"> 1</span></span>
<span data-line=""><span style="color:#E1E4E8">    INCREMENT </span><span style="color:#F97583">BY</span><span style="color:#79B8FF"> 1</span></span>
<span data-line=""><span style="color:#E1E4E8">    NOCACHE;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">-- 테스트 데이터</span></span>
<span data-line=""><span style="color:#F97583">INSERT INTO</span><span style="color:#E1E4E8"> MEMBER (MEMBER_ID, USER_NAME, EMAIL, PHONE)</span></span>
<span data-line=""><span style="color:#F97583">VALUES</span><span style="color:#E1E4E8"> (</span><span style="color:#79B8FF">SEQ_MEMBER</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">NEXTVAL</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'홍길동'</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'hong@example.com'</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'010-1234-5678'</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">INSERT INTO</span><span style="color:#E1E4E8"> MEMBER (MEMBER_ID, USER_NAME, EMAIL, PHONE)</span></span>
<span data-line=""><span style="color:#F97583">VALUES</span><span style="color:#E1E4E8"> (</span><span style="color:#79B8FF">SEQ_MEMBER</span><span style="color:#E1E4E8">.</span><span style="color:#79B8FF">NEXTVAL</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'김철수'</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'kim@example.com'</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'010-9876-5432'</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">COMMIT</span><span style="color:#E1E4E8">;</span></span></code></pre></figure>
<blockquote>
<p>Oracle은 MySQL의 <code>AUTO_INCREMENT</code>가 없습니다. 대신 <strong>시퀀스</strong>(SEQUENCE)를 따로 만들어서 씁니다. <code>SEQ_MEMBER.NEXTVAL</code>을 호출할 때마다 1씩 증가합니다.</p>
</blockquote>
<hr>
<h2 id="2-dto-클래스-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-dto-클래스-만들기">#</a>2. DTO 클래스 만들기</h2>
<p><code>dto/MemberDto.java</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">package</span><span style="color:#E1E4E8"> com.example.backend.dto;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> lombok.Getter;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> lombok.Setter;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> lombok.NoArgsConstructor;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> lombok.AllArgsConstructor;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> lombok.ToString;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Getter</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Setter</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">NoArgsConstructor</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">AllArgsConstructor</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">ToString</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> MemberDto</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> Long memberId;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String userName;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String email;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String phone;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String regDate;</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p><strong>Lombok 어노테이션 설명:</strong></p>
<ul>
<li><code>@Getter</code> / <code>@Setter</code>: get/set 메서드 자동 생성</li>
<li><code>@NoArgsConstructor</code>: 기본 생성자 자동 생성</li>
<li><code>@AllArgsConstructor</code>: 전체 필드 생성자 자동 생성</li>
<li><code>@ToString</code>: toString() 메서드 자동 생성</li>
</ul>
<blockquote>
<p>2편에서 <code>application.yml</code>에 <code>map-underscore-to-camel-case: true</code> 설정을 해뒀습니다. 덕분에 DB의 <code>USER_NAME</code> → Java의 <code>userName</code>으로 자동 변환됩니다.</p>
</blockquote>
<hr>
<h2 id="3-mapper-인터페이스-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-mapper-인터페이스-만들기">#</a>3. Mapper 인터페이스 만들기</h2>
<p><code>mapper/MemberMapper.java</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">package</span><span style="color:#E1E4E8"> com.example.backend.mapper;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> com.example.backend.dto.MemberDto;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.apache.ibatis.annotations.Mapper;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> java.util.List;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Mapper</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> interface</span><span style="color:#B392F0"> MemberMapper</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 전체 조회</span></span>
<span data-line=""><span style="color:#E1E4E8">    List&#x3C;</span><span style="color:#F97583">MemberDto</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">selectAllMembers</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 단건 조회</span></span>
<span data-line=""><span style="color:#E1E4E8">    MemberDto </span><span style="color:#B392F0">selectMemberById</span><span style="color:#E1E4E8">(Long </span><span style="color:#FFAB70">memberId</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 등록</span></span>
<span data-line=""><span style="color:#F97583">    int</span><span style="color:#B392F0"> insertMember</span><span style="color:#E1E4E8">(MemberDto </span><span style="color:#FFAB70">member</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 수정</span></span>
<span data-line=""><span style="color:#F97583">    int</span><span style="color:#B392F0"> updateMember</span><span style="color:#E1E4E8">(MemberDto </span><span style="color:#FFAB70">member</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 삭제</span></span>
<span data-line=""><span style="color:#F97583">    int</span><span style="color:#B392F0"> deleteMember</span><span style="color:#E1E4E8">(Long </span><span style="color:#FFAB70">memberId</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>인터페이스만 선언하고, 실제 SQL은 XML 파일에 작성합니다. <code>@Mapper</code> 어노테이션이 있으면 MyBatis가 자동으로 구현체를 만들어줍니다.</p>
<hr>
<h2 id="4-mybatis-xml-매퍼-작성"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-mybatis-xml-매퍼-작성">#</a>4. MyBatis XML 매퍼 작성</h2>
<p><code>src/main/resources/mapper/MemberMapper.xml</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="xml" data-theme="github-dark"><code data-language="xml" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">&#x3C;?</span><span style="color:#85E89D">xml</span><span style="color:#B392F0"> version</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"1.0"</span><span style="color:#B392F0"> encoding</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"UTF-8"</span><span style="color:#E1E4E8">?></span></span>
<span data-line=""><span style="color:#E1E4E8">&#x3C;!</span><span style="color:#F97583">DOCTYPE</span><span style="color:#79B8FF"> mapper</span><span style="color:#E1E4E8"> PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"</span></span>
<span data-line=""><span style="color:#E1E4E8">        "http://mybatis.org/dtd/mybatis-3-mapper.dtd"></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">mapper</span><span style="color:#B392F0"> namespace</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"com.example.backend.mapper.MemberMapper"</span><span style="color:#E1E4E8">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    &#x3C;!-- ResultMap: DB 컬럼 → Java 필드 매핑 --></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">resultMap</span><span style="color:#B392F0"> id</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"memberResultMap"</span><span style="color:#B392F0"> type</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"MemberDto"</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">id</span><span style="color:#B392F0">     property</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"memberId"</span><span style="color:#B392F0">  column</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"MEMBER_ID"</span><span style="color:#E1E4E8">/></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">result</span><span style="color:#B392F0"> property</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"userName"</span><span style="color:#B392F0">  column</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"USER_NAME"</span><span style="color:#E1E4E8">/></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">result</span><span style="color:#B392F0"> property</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"email"</span><span style="color:#B392F0">     column</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"EMAIL"</span><span style="color:#E1E4E8">/></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">result</span><span style="color:#B392F0"> property</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"phone"</span><span style="color:#B392F0">     column</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"PHONE"</span><span style="color:#E1E4E8">/></span></span>
<span data-line=""><span style="color:#E1E4E8">        &#x3C;</span><span style="color:#85E89D">result</span><span style="color:#B392F0"> property</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"regDate"</span><span style="color:#B392F0">   column</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"REG_DATE"</span><span style="color:#E1E4E8">/></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">resultMap</span><span style="color:#E1E4E8">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    &#x3C;!-- 전체 조회 --></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">select</span><span style="color:#B392F0"> id</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"selectAllMembers"</span><span style="color:#B392F0"> resultMap</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"memberResultMap"</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        SELECT MEMBER_ID, USER_NAME, EMAIL, PHONE,</span></span>
<span data-line=""><span style="color:#E1E4E8">               TO_CHAR(REG_DATE, 'YYYY-MM-DD') AS REG_DATE</span></span>
<span data-line=""><span style="color:#E1E4E8">        FROM MEMBER</span></span>
<span data-line=""><span style="color:#E1E4E8">        ORDER BY MEMBER_ID DESC</span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">select</span><span style="color:#E1E4E8">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    &#x3C;!-- 단건 조회 --></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">select</span><span style="color:#B392F0"> id</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"selectMemberById"</span><span style="color:#B392F0"> parameterType</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"Long"</span><span style="color:#B392F0"> resultMap</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"memberResultMap"</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        SELECT MEMBER_ID, USER_NAME, EMAIL, PHONE,</span></span>
<span data-line=""><span style="color:#E1E4E8">               TO_CHAR(REG_DATE, 'YYYY-MM-DD') AS REG_DATE</span></span>
<span data-line=""><span style="color:#E1E4E8">        FROM MEMBER</span></span>
<span data-line=""><span style="color:#E1E4E8">        WHERE MEMBER_ID = #{memberId}</span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">select</span><span style="color:#E1E4E8">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    &#x3C;!-- 등록 --></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">insert</span><span style="color:#B392F0"> id</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"insertMember"</span><span style="color:#B392F0"> parameterType</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"MemberDto"</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        INSERT INTO MEMBER (MEMBER_ID, USER_NAME, EMAIL, PHONE)</span></span>
<span data-line=""><span style="color:#E1E4E8">        VALUES (SEQ_MEMBER.NEXTVAL, #{userName}, #{email}, #{phone})</span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">insert</span><span style="color:#E1E4E8">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    &#x3C;!-- 수정 --></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">update</span><span style="color:#B392F0"> id</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"updateMember"</span><span style="color:#B392F0"> parameterType</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"MemberDto"</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        UPDATE MEMBER</span></span>
<span data-line=""><span style="color:#E1E4E8">        SET USER_NAME = #{userName},</span></span>
<span data-line=""><span style="color:#E1E4E8">            EMAIL     = #{email},</span></span>
<span data-line=""><span style="color:#E1E4E8">            PHONE     = #{phone}</span></span>
<span data-line=""><span style="color:#E1E4E8">        WHERE MEMBER_ID = #{memberId}</span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">update</span><span style="color:#E1E4E8">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    &#x3C;!-- 삭제 --></span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">delete</span><span style="color:#B392F0"> id</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"deleteMember"</span><span style="color:#B392F0"> parameterType</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"Long"</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">        DELETE FROM MEMBER</span></span>
<span data-line=""><span style="color:#E1E4E8">        WHERE MEMBER_ID = #{memberId}</span></span>
<span data-line=""><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">delete</span><span style="color:#E1E4E8">></span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#85E89D">mapper</span><span style="color:#E1E4E8">></span></span></code></pre></figure>
<h3 id="핵심-문법-설명"><a class="anchor" aria-hidden="true" tabindex="-1" href="#핵심-문법-설명">#</a>핵심 문법 설명</h3>
<ul>
<li><code>namespace</code>: 연결할 Mapper 인터페이스의 패키지 + 클래스명</li>
<li><code>#{변수명}</code>: 파라미터 바인딩. SQL Injection 방지를 위해 항상 <code>#{}</code> 사용</li>
<li><code>resultMap</code>: DB 컬럼과 Java 필드를 명시적으로 매핑</li>
<li><code>TO_CHAR(REG_DATE, 'YYYY-MM-DD')</code>: Oracle의 DATE 타입을 문자열로 변환</li>
</ul>
<hr>
<h2 id="5-service-클래스-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-service-클래스-만들기">#</a>5. Service 클래스 만들기</h2>
<p><code>service/MemberService.java</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">package</span><span style="color:#E1E4E8"> com.example.backend.service;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> com.example.backend.dto.MemberDto;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> com.example.backend.mapper.MemberMapper;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> lombok.RequiredArgsConstructor;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.stereotype.Service;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> java.util.List;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Service</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RequiredArgsConstructor</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> MemberService</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> MemberMapper memberMapper;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> List&#x3C;</span><span style="color:#F97583">MemberDto</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">getAllMembers</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> memberMapper.</span><span style="color:#B392F0">selectAllMembers</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> MemberDto </span><span style="color:#B392F0">getMemberById</span><span style="color:#E1E4E8">(Long </span><span style="color:#FFAB70">memberId</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> memberMapper.</span><span style="color:#B392F0">selectMemberById</span><span style="color:#E1E4E8">(memberId);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> int</span><span style="color:#B392F0"> createMember</span><span style="color:#E1E4E8">(MemberDto </span><span style="color:#FFAB70">member</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> memberMapper.</span><span style="color:#B392F0">insertMember</span><span style="color:#E1E4E8">(member);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> int</span><span style="color:#B392F0"> updateMember</span><span style="color:#E1E4E8">(MemberDto </span><span style="color:#FFAB70">member</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> memberMapper.</span><span style="color:#B392F0">updateMember</span><span style="color:#E1E4E8">(member);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> int</span><span style="color:#B392F0"> deleteMember</span><span style="color:#E1E4E8">(Long </span><span style="color:#FFAB70">memberId</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> memberMapper.</span><span style="color:#B392F0">deleteMember</span><span style="color:#E1E4E8">(memberId);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<blockquote>
<p>Controller → Service → Mapper 3계층 구조가 실무 표준입니다. Service에 비즈니스 로직을 담고, Mapper는 DB 접근만 담당합니다.</p>
</blockquote>
<hr>
<h2 id="6-controller-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-controller-만들기">#</a>6. Controller 만들기</h2>
<p><code>controller/MemberController.java</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">package</span><span style="color:#E1E4E8"> com.example.backend.controller;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> com.example.backend.dto.MemberDto;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> com.example.backend.service.MemberService;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> lombok.RequiredArgsConstructor;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.http.ResponseEntity;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.web.bind.annotation.</span><span style="color:#79B8FF">*</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> java.util.List;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RestController</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RequestMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/api/members"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RequiredArgsConstructor</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> MemberController</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> MemberService memberService;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 전체 조회</span></span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">GetMapping</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> ResponseEntity&#x3C;List&#x3C;</span><span style="color:#F97583">MemberDto</span><span style="color:#E1E4E8">>> </span><span style="color:#B392F0">getAllMembers</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(memberService.</span><span style="color:#B392F0">getAllMembers</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 단건 조회</span></span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">GetMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/{id}"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> ResponseEntity&#x3C;</span><span style="color:#F97583">MemberDto</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">getMember</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">PathVariable</span><span style="color:#E1E4E8"> Long </span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        MemberDto member </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> memberService.</span><span style="color:#B392F0">getMemberById</span><span style="color:#E1E4E8">(id);</span></span>
<span data-line=""><span style="color:#F97583">        if</span><span style="color:#E1E4E8"> (member </span><span style="color:#F97583">==</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">            return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">notFound</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">build</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">        }</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(member);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 등록</span></span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">PostMapping</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> ResponseEntity&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">createMember</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">RequestBody</span><span style="color:#E1E4E8"> MemberDto </span><span style="color:#FFAB70">member</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        memberService.</span><span style="color:#B392F0">createMember</span><span style="color:#E1E4E8">(member);</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"등록 완료"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 수정</span></span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">PutMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/{id}"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> ResponseEntity&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">updateMember</span><span style="color:#E1E4E8">(</span></span>
<span data-line=""><span style="color:#E1E4E8">            @</span><span style="color:#F97583">PathVariable</span><span style="color:#E1E4E8"> Long </span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#E1E4E8">            @</span><span style="color:#F97583">RequestBody</span><span style="color:#E1E4E8"> MemberDto </span><span style="color:#FFAB70">member</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        member.</span><span style="color:#B392F0">setMemberId</span><span style="color:#E1E4E8">(id);</span></span>
<span data-line=""><span style="color:#E1E4E8">        memberService.</span><span style="color:#B392F0">updateMember</span><span style="color:#E1E4E8">(member);</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"수정 완료"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 삭제</span></span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">DeleteMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/{id}"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> ResponseEntity&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">deleteMember</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">PathVariable</span><span style="color:#E1E4E8"> Long </span><span style="color:#FFAB70">id</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        memberService.</span><span style="color:#B392F0">deleteMember</span><span style="color:#E1E4E8">(id);</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"삭제 완료"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<hr>
<h2 id="7-cors-설정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7-cors-설정">#</a>7. CORS 설정</h2>
<p>React(3000포트)에서 Spring Boot(8080포트)를 호출할 때 브라우저가 <strong>CORS 에러</strong>를 냅니다. 미리 설정해둡니다.</p>
<p><code>config/WebConfig.java</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">package</span><span style="color:#E1E4E8"> com.example.backend.config;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.context.annotation.Configuration;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.web.servlet.config.annotation.CorsRegistry;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.web.servlet.config.annotation.WebMvcConfigurer;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Configuration</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> WebConfig</span><span style="color:#F97583"> implements</span><span style="color:#B392F0"> WebMvcConfigurer</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Override</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> addCorsMappings</span><span style="color:#E1E4E8">(CorsRegistry </span><span style="color:#FFAB70">registry</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        registry.</span><span style="color:#B392F0">addMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/api/**"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">                .</span><span style="color:#B392F0">allowedOrigins</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"http://localhost:3000"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">                .</span><span style="color:#B392F0">allowedMethods</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"GET"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"POST"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"PUT"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"DELETE"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"OPTIONS"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">                .</span><span style="color:#B392F0">allowedHeaders</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"*"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">                .</span><span style="color:#B392F0">allowCredentials</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<hr>
<h2 id="8-api-테스트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#8-api-테스트">#</a>8. API 테스트</h2>
<p>서버를 실행하고 브라우저나 Postman으로 테스트합니다.</p>
<h3 id="전체-조회"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전체-조회">#</a>전체 조회</h3>
<pre><code>GET http://localhost:8080/api/members
</code></pre>
<p>응답:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="json" data-theme="github-dark"><code data-language="json" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">[</span></span>
<span data-line=""><span style="color:#E1E4E8">  {</span></span>
<span data-line=""><span style="color:#79B8FF">    "memberId"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">2</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#79B8FF">    "userName"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"김철수"</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#79B8FF">    "email"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"kim@example.com"</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#79B8FF">    "phone"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"010-9876-5432"</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#79B8FF">    "regDate"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"2026-04-12"</span></span>
<span data-line=""><span style="color:#E1E4E8">  },</span></span>
<span data-line=""><span style="color:#E1E4E8">  {</span></span>
<span data-line=""><span style="color:#79B8FF">    "memberId"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#79B8FF">    "userName"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"홍길동"</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#79B8FF">    "email"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"hong@example.com"</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#79B8FF">    "phone"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"010-1234-5678"</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#79B8FF">    "regDate"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"2026-04-12"</span></span>
<span data-line=""><span style="color:#E1E4E8">  }</span></span>
<span data-line=""><span style="color:#E1E4E8">]</span></span></code></pre></figure>
<h3 id="회원-등록"><a class="anchor" aria-hidden="true" tabindex="-1" href="#회원-등록">#</a>회원 등록</h3>
<pre><code>POST http://localhost:8080/api/members
Content-Type: application/json

{
  "userName": "이영희",
  "email": "lee@example.com",
  "phone": "010-5555-6666"
}
</code></pre>
<p>콘솔에 실행된 SQL이 출력됩니다 (application.yml의 <code>log-impl</code> 설정 덕분):</p>
<pre><code>==>  Preparing: INSERT INTO MEMBER (MEMBER_ID, USER_NAME, EMAIL, PHONE) VALUES (SEQ_MEMBER.NEXTVAL, ?, ?, ?)
==> Parameters: 이영희(String), lee@example.com(String), 010-5555-6666(String)
&#x3C;==    Updates: 1
</code></pre>
<hr>
<h2 id="완성된-폴더-구조"><a class="anchor" aria-hidden="true" tabindex="-1" href="#완성된-폴더-구조">#</a>완성된 폴더 구조</h2>
<pre><code>src/main/
├── java/com/example/backend/
│   ├── BackendApplication.java
│   ├── config/
│   │   └── WebConfig.java
│   ├── controller/
│   │   └── MemberController.java
│   ├── service/
│   │   └── MemberService.java
│   ├── mapper/
│   │   └── MemberMapper.java
│   └── dto/
│       └── MemberDto.java
└── resources/
    ├── application.yml
    └── mapper/
        └── MemberMapper.xml
</code></pre>
<hr>
<h2 id="다음-편-예고"><a class="anchor" aria-hidden="true" tabindex="-1" href="#다음-편-예고">#</a>다음 편 예고</h2>
<p><strong>4편: React 프로젝트 생성 &#x26; Spring Boot API 연동</strong></p>
<p><code>create-react-app</code>(또는 Vite)으로 프론트엔드를 만들고, axios로 방금 만든 Spring Boot API를 호출해서 화면에 회원 목록을 뿌려봅니다. 프론트와 백이 실제로 연결되는 순간입니다.</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[SpringBoot]]></category>
      <category><![CDATA[MyBatis]]></category>
      <category><![CDATA[Oracle]]></category>
      <category><![CDATA[CRUD]]></category>
      <category><![CDATA[REST API]]></category>
      <category><![CDATA[초보자]]></category>
    </item>

    <item>
      <title><![CDATA[박효신 LIVE A & E 2026 — 7년 만의 귀환, 야생화 끝에 남은 것]]></title>
      <link>https://www.stragos.xyz/posts/park-hyoshin-concert-2026-review</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/park-hyoshin-concert-2026-review</guid>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[팬클럽 와이프 덕에 끌려간 남편의 박효신 콘서트 후기. 굿즈 줄부터 야생화 마지막 장면까지, 4월의 인천문학경기장을 솔직하게 기록했다.]]></description>
      <content:encoded><![CDATA[<p>솔직히 말하면, 내 의지로 간 건 아니었다.</p>
<p>아내는 박효신 공식 팬클럽 <strong>소울트리</strong> 멤버다. 20년 된, 진성 팬이다. 나는 그냥 남편이다.</p>
<p>"같이 가줘"라는 말 한마디에 이렇게 4월의 인천문학경기장까지 오게 됐다. 근데 다녀온 지금, 솔직히 후회는 없다. 오히려 더 일찍 왔었어야 했나 싶을 정도로.</p>
<hr>
<h2 id="도착-그리고-첫-번째-충격"><a class="anchor" aria-hidden="true" tabindex="-1" href="#도착-그리고-첫-번째-충격">#</a>도착, 그리고 첫 번째 충격</h2>
<p><img src="/images/phs/1.jpg" alt="도착 후 인증샷" loading="lazy" decoding="async"></p>
<p>인천문학경기장에 도착했을 때 가장 먼저 느낀 건 규모였다.</p>
<p><strong>PARK HYO SHIN LIVE A &#x26; E 2026.</strong> 박효신이 7년 만에 여는 단독 콘서트다. 2019년 'LOVERS' 이후 처음이다. 그리고 이번엔 스타디움이다.</p>
<p>국내 남자 솔로 가수가 인천문학경기장 주경기장을 꽉 채운다는 게 어떤 의미인지, 현장에 서니 바로 체감이 됐다. 3만 석 전석 매진. 숫자로 읽을 때와 직접 눈으로 볼 때는 차원이 다르다.</p>
<p>날씨는 도착할 때만 해도 흐렸다. 4월 특유의 꾸물거리는 하늘이었는데, 공연 시간이 가까워질수록 구름이 걷히면서 서서히 파란 하늘이 드러났다. 인증샷 하나 남겨뒀다. 아내가 요청한 거지만, 나도 나중에 이 사진이 고마워질 것 같아서.</p>
<hr>
<h2 id="굿즈-줄--이게-전쟁이구나"><a class="anchor" aria-hidden="true" tabindex="-1" href="#굿즈-줄--이게-전쟁이구나">#</a>굿즈 줄 — 이게 전쟁이구나</h2>
<p><img src="/images/phs/2.jpg" alt="굿즈 구매 대기" loading="lazy" decoding="async"></p>
<p>경기장 밖 굿즈 판매 줄은 이미 장사진을 이뤘다.</p>
<p>아내 말로는 <strong>"최소 공연 4-5시간 전엔 와야 후드집업 살 수 있어"</strong> 라고 했는데, 그 말이 틀리지 않았다. 비트라이트(공식 응원 팔찌), 에코백, 포스트카드는 그나마 여유가 있었지만, 후드집업와 키링은 이미 빠른 속도로 소진되고 있었다. 우리는 간신히 원하는 걸 챙겼다.</p>
<p>줄을 서면서 주변을 보니, 혼자 온 팬들, 커플, 가족 단위까지 연령층이 다양했다. 박효신의 음악이 한 세대를 관통했다는 걸 줄 서면서 느꼈다.</p>
<p>아내는 예전 러브콘, 해피투게더 때 구입한 굿즈를 직접 입고 왔다. 옷에서부터 이미 20년의 팬 역사가 느껴졌다. 응원 팔찌를 찬 그 표정이, 평소랑은 달랐다.</p>
<hr>
<h2 id="공연-시작--조명이-꺼지는-순간"><a class="anchor" aria-hidden="true" tabindex="-1" href="#공연-시작--조명이-꺼지는-순간">#</a>공연 시작 — 조명이 꺼지는 순간</h2>
<p><img src="/images/phs/3.jpg" alt="공연 시작" loading="lazy" decoding="async"></p>
<p>오후 6시 20분. 조명이 꺼지자 경기장이 한 번에 술렁였다.</p>
<p>박효신의 입장은 단순하지 않았다. 웅장한 인트로, 그리고 무대 위에 그가 등장하는 순간 3만 명의 응원 팔찌가 동시에 빛났다.</p>
<p>보라색과 파란색 빛이 스타디움을 가득 채우는 그 장면은, 사진으로는 절대 담을 수 없는 것이었다.</p>
<p>세트리스트는 신보 <em>A &#x26; E</em>의 곡들로 열었고, 중반부엔 <em>연인(LOVERS)</em>, <em>Happy Together</em>, <em>Shine Your Light</em> 같은 구작들이 이어졌다.</p>
<p>박효신의 목소리는 라이브에서 더 빛난다는 말이 괜한 소리가 아니었다. 마이크를 통해 나오는 음량인데도 배 속에서 무언가가 울리는 느낌. 아내가 왜 20년째 이 사람 팬인지 이해가 됐다.</p>
<blockquote>
<p>"팬들이 하나같이 말하는 것, '직접 들으면 다르다.' 이게 진짜였다."<br>
— DC인사이드 박효신 갤러리 후기 중</p>
</blockquote>
<hr>
<h2 id="저녁이-되며--추위와의-싸움"><a class="anchor" aria-hidden="true" tabindex="-1" href="#저녁이-되며--추위와의-싸움">#</a>저녁이 되며 — 추위와의 싸움</h2>
<p>4월의 야외 공연. 낮에는 흐렸다가 서서히 개었다. 진짜 문제는 해가 지고 나서였다.</p>
<p>기온이 확 떨어졌다. 체감온도는 낮에 비해 10도 가까이 차이 나는 느낌이었다. 스타디움은 탁 트인 구조라 바람이 그대로 들어온다.</p>
<p>주변에서도 꽁꽁 싸맨 관객들이 하나둘 보이기 시작했다.</p>
<p>다행히 우리는 미리 대비를 해뒀다. 아내는 러브콘 때 산 굿즈 위에 겉옷을 덧입고 손난로도 챙겼다. 나도 두꺼운 패딩을 챙겨갔는데 처음엔 짐스럽다 싶었지만, 공연 중반 이후엔 그게 신의 한 수였다.</p>
<p>엄청 추웠던 건 아니었지만, 아무 준비 없이 왔으면 꽤 힘들었을 거다.</p>
<p>야외 공연 처음 가는 분들한테 하고 싶은 말: <strong>4월이라도 저녁 야외는 꼭 겉옷 두껍게 챙겨라.</strong> 그리고 핫팩.</p>
<hr>
<h2 id="주차--이건-진짜-최악이었다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#주차--이건-진짜-최악이었다">#</a>주차 — 이건 진짜 최악이었다</h2>
<p>솔직히 공연 자체는 흠잡을 데가 없었는데, 딱 하나. <strong>주차.</strong></p>
<p>운 좋게 경기장 지하 1층에 주차할 수 있었다. 그 부분은 다행이었다.</p>
<p>문제는 돌아갈 때였다. 3만 명이 동시에 빠져나가려고 하면 경기장 주변 도로가 완전히 마비된다. 지하에서 나오는 것 자체가 이미 전쟁이었고, 도로에 올라와서도 거의 움직이지 못했다. 차 빼는 데만 1시간 가까이 걸렸다.</p>
<p>올 때는 운이 좋았지만, <strong>다음에 또 간다면 무조건 대중교통.</strong> 아니면 아예 인근 숙소 잡고 가는 게 낫다. 공연 끝나고 밤늦게 집에 들어오고 싶지 않다면.</p>
<hr>
<h2 id="야생화--마지막-장면"><a class="anchor" aria-hidden="true" tabindex="-1" href="#야생화--마지막-장면">#</a>야생화 — 마지막 장면</h2>
<p><img src="/images/phs/4.png" alt="마지막, 야생화" loading="lazy" decoding="async"></p>
<p>앙코르 구간이 끝나갈 즈음, 무대가 다시 한 번 바뀌었다.</p>
<p>그리고 첫 소절이 흘러나왔다. <em>야생화.</em></p>
<p>3만 명이 조용해지는 게 느껴졌다. 일제히 응원 팔찌를 차고, 몇몇은 따라 흥얼거렸다.</p>
<p>박효신의 목소리가 스타디움 전체를 감싸는 그 순간이, 솔직히 소름이었다.</p>
<p>아내 옆에서 나도 그냥 숨죽이고 봤다. 말이 필요 없었다.</p>
<p>야생화는 박효신의 대표곡이자, 이 공연에서 마지막을 장식한 곡이다. 수많은 후기에서 이 장면을 "두 번 다시 보기 힘든 장면"이라고 했는데, 직접 보고 나니 그 말이 이해됐다.</p>
<p>스타디움 전체가 하나의 감정으로 묶이는 경험이었다.</p>
<blockquote>
<p>"엔딩 야생화 구간, 3만 명이 동시에 조용해지는 그 순간이 이 공연의 하이라이트였다."<br>
— 팬클럽(소울트리) 후기 종합</p>
</blockquote>
<hr>
<h2 id="총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#총평">#</a>총평</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>평가</th>
</tr>
</thead>
<tbody>
<tr>
<td>음향</td>
<td>★★★★☆</td>
</tr>
<tr>
<td>무대 연출</td>
<td>★★★★★</td>
</tr>
<tr>
<td>세트리스트</td>
<td>★★★★☆</td>
</tr>
<tr>
<td>현장 분위기</td>
<td>★★★★★</td>
</tr>
<tr>
<td>굿즈</td>
<td>★★★★☆</td>
</tr>
<tr>
<td>주차</td>
<td>★☆☆☆☆</td>
</tr>
</tbody>
</table>
<p>박효신 LIVE A &#x26; E 2026은 단순한 콘서트가 아니었다. 7년이라는 시간을 기다려 온 팬들의 감정이 한꺼번에 터지는 자리였다.</p>
<p>아내가 팬클럽인 덕분에 나는 거기 서 있을 수 있었고, 그 감정의 끝자락을 조금이나마 느낄 수 있었다.</p>
<p>남편으로서 한마디 하자면:</p>
<p><strong>와이프 팬질 무시하지 마라. 거기에 진짜 좋은 게 있다.</strong></p>
<p>다음엔 내가 먼저 가자고 할 것 같다.</p>
<hr>
<p><em>박효신 LIVE A &#x26; E 2026 · 인천문학경기장 주경기장 · 2026년 4월</em></p>]]></content:encoded>
      <category><![CDATA[일상]]></category>
      <category><![CDATA[박효신]]></category>
      <category><![CDATA[콘서트]]></category>
      <category><![CDATA[LIVE A&E]]></category>
      <category><![CDATA[콘서트후기]]></category>
      <category><![CDATA[인천문학경기장]]></category>
      <category><![CDATA[야생화]]></category>
    </item>

    <item>
      <title><![CDATA[포켓몬 하트골드 — 포켓몬 시리즈 역대 최고작, 직접 클리어하며 느낀 것들]]></title>
      <link>https://www.stragos.xyz/posts/pokemon-heartgold-review</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/pokemon-heartgold-review</guid>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[갸라도스 2마리에 칠색조, 블레이범, 이브이로 챔피언과 레드까지 클리어했다. 포켓몬 시리즈 중 가장 명작이라 불리는 하트골드, 직접 해보니 그 말이 틀리지 않았다.]]></description>
      <content:encoded><![CDATA[<p>포켓몬 시리즈를 꽤 오래 해왔다고 생각했는데, 하트골드는 달랐다.</p>
<p>NDS로 출시된 포켓몬 골드/실버의 리메이크작. 성도지방을 클리어하면 관동지방까지 이어지는 두 개 지방 탐험. 역대 포켓몬 시리즈 중 가장 볼륨이 크고 완성도가 높다는 평가가 괜한 게 아니었다. 갸라도스 2마리에 칠색조, 이브이, 블레이범 파티로 챔피언 목호를 클리어하고, 결국 끝판왕 레드까지 잡았다. 그 과정에서 느꼈던 것들을 전부 써놓는다.</p>
<hr>
<h2 id="왜-하트골드인가--포켓몬-명작들-중에서도-특별한-이유"><a class="anchor" aria-hidden="true" tabindex="-1" href="#왜-하트골드인가--포켓몬-명작들-중에서도-특별한-이유">#</a>왜 하트골드인가 — 포켓몬 명작들 중에서도 특별한 이유</h2>
<p><img src="/images/heartgold/title-screen.webp" alt="포켓몬스터 하트골드 타이틀 화면" loading="lazy" decoding="async"></p>
<p>포켓몬 시리즈에는 명작이 많다. 파이어레드, 에메랄드, 플라티나, 블랙/화이트. 그런데 팬들이 "역대 최고"를 꼽을 때 가장 많이 나오는 이름이 하트골드·소울실버다.</p>
<p>이유가 뭔가 생각해보면 몇 가지가 겹친다.</p>
<p>첫째, <strong>두 개 지방</strong>이다. 포켓몬 게임은 보통 하나의 지방에서 8개 체육관을 클리어하고 끝난다. 하트골드는 성도지방 8개 체육관을 클리어하면 배를 타고 관동지방으로 건너간다. 거기서 또 8개 체육관을 다 깨야 진짜 끝판왕 레드와 싸울 수 있다. 총 16개 체육관이다. 이 볼륨이 다른 작품과 차원이 다르다.</p>
<p>둘째, <strong>선두 포켓몬이 따라다닌다</strong>. 포켓몬 피카츄처럼 파티 선두가 필드에서 플레이어 뒤를 졸졸 따라다닌다. 갸라도스가 따라다니는 걸 보면 진심으로 뿌듯하다. 교감 기능도 있어서 말을 걸면 현재 감정 상태를 알려준다.</p>
<p>셋째, <strong>포켓워커</strong>. 하트골드에는 만보계처럼 생긴 포켓워커 기기가 동봉됐다. 실제로 걸어다니면 포케카 포인트가 쌓이고 포켓몬도 잡을 수 있다. 지금 시점에 보면 닌텐도가 포켓몬 Go의 원형을 이미 2009년에 만들었다는 게 놀랍다.</p>
<p>넷째, <strong>스토리의 완성도</strong>. 초반 어썸의 로켓단 서사, 기모노 걸 퀘스트, 체육관 관장들과의 재대결, 그리고 결말의 레드. 하나의 긴 여정이 서로 유기적으로 연결돼 있고 감동적인 순간도 적지 않다.</p>
<hr>
<h2 id="시작--스타터-선택과-성도지방"><a class="anchor" aria-hidden="true" tabindex="-1" href="#시작--스타터-선택과-성도지방">#</a>시작 — 스타터 선택과 성도지방</h2>
<p><img src="/images/heartgold/starter-cyndaquil.png" alt="스타터 선택 화면" loading="lazy" decoding="async"></p>
<p>시작은 연두마을. 박사에게서 스타터를 받는다. 하트골드에서 고를 수 있는 스타터는 세 가지다.</p>
<ul>
<li><strong>치코리타</strong> — 풀 타입. 초반이 제일 힘들고 체육관 상성이 최악이지만 끝까지 키우면 메가니움이 된다</li>
<li><strong>브케인</strong> — 불 타입. 균형 잡힌 선택. 블레이범 진화 루트</li>
<li><strong>리아코</strong> — 물 타입. 초반 체육관 상성이 가장 좋다</li>
</ul>
<p>나는 <strong>브케인</strong>을 골랐다. 블레이범은 4세대 기준 특수공격과 스피드가 높은 불 타입 어태커로, 분화와 불꽃방사 화력이 매우 강하다. 이 선택이 나중에 레드전에서 결정적인 역할을 했다.</p>
<p>성도지방은 총 8개 도시에 체육관이 있다. 순서대로 말하면 이 정도다.</p>
<ol>
<li><strong>도라지시티 (비행)</strong> — 사나래를 잡으면 쉽게 넘어간다</li>
<li><strong>꽃무지마을 (벌레)</strong> — 어차피 브케인이면 1방</li>
<li><strong>금빛시티 (일반)</strong> — 안개꽃이나 물 타입이 있으면 무난</li>
<li><strong>인주시티 (유령)</strong> — 노말 기술이 통하지 않아 특수 계열로 처리</li>
<li><strong>진청시티 (격투)</strong> — 고스트 타입 약점은 악과 고스트. 은빛산 후 플레이어들이 자주 막히는 곳</li>
<li><strong>담청시티 (강철)</strong> — 재스민. 불 타입이 있으면 쉽다. 나는 블레이범이 있어서 수월</li>
<li><strong>흑빛시티 (얼음)</strong> — 기억에 남는 이유는 체육관 안이 스노보드 코스라서</li>
<li><strong>검은먹시티 (용)</strong> — 클레어. 용 타입에 갸라도스가 잘 버팀. 관통하면 챔피언</li>
</ol>
<hr>
<h2 id="갸라도스--이-게임-진짜-사기-캐릭터"><a class="anchor" aria-hidden="true" tabindex="-1" href="#갸라도스--이-게임-진짜-사기-캐릭터">#</a>갸라도스 — 이 게임 진짜 사기 캐릭터</h2>
<p><img src="/images/heartgold/lakeofrage.png" alt="분노의 호수 — 빨간 갸라도스" loading="lazy" decoding="async"></p>
<p>하트골드 하면 가장 먼저 떠오르는 게 이거다. <strong>분노의 호수</strong>. 게임 중반, 로켓단이 특정 주파수로 포켓몬을 강제 진화시키는 사건이 발생한다. 그 때문에 분노한 포켓몬들이 이상 행동을 보이고, 그 중심에 <strong>빨간 갸라도스</strong>가 있다.</p>
<p>빨간 갸라도스는 하트골드에서 스토리 진행 중 반드시 만나게 되는 <strong>색이 다른 포켓몬</strong>(이로치)다. 이 한 마리 때문에 많은 사람들이 처음으로 이로치 포켓몬을 잡는 경험을 한다.</p>
<p><img src="/images/heartgold/gyarados.png" alt="갸라도스 — 포켓몬계 최강의 물리 딜러" loading="lazy" decoding="async"></p>
<p>나는 이 빨간 갸라도스를 잡고 나서, 일반 갸라도스도 한 마리 더 키워서 <strong>갸라도스 2마리 체제</strong>를 구축했다.</p>
<p>갸라도스가 왜 사기냐고 물으면 이렇게 설명할 수 있다.</p>
<h3 id="갸라도스-능력치-4세대-기준"><a class="anchor" aria-hidden="true" tabindex="-1" href="#갸라도스-능력치-4세대-기준">#</a>갸라도스 능력치 (4세대 기준)</h3>
<table>
<thead>
<tr>
<th>능력치</th>
<th>수치</th>
</tr>
</thead>
<tbody>
<tr>
<td>HP</td>
<td>95</td>
</tr>
<tr>
<td>공격</td>
<td><strong>125</strong></td>
</tr>
<tr>
<td>방어</td>
<td>79</td>
</tr>
<tr>
<td>특수공격</td>
<td>60</td>
</tr>
<tr>
<td>특수방어</td>
<td>100</td>
</tr>
<tr>
<td>스피드</td>
<td>81</td>
</tr>
</tbody>
</table>
<p>공격 125에 특수방어 100. 물/비행 타입이라 약점이 전기 하나뿐이다(4배). 그 외에 약점이 바위 타입이지만 4배가 아니라 2배라 버틸 수 있다. 물 타입 어태커로도 쓰이지만 하트골드에서는 <strong>폭포오르기 + 지진 + 불꽃세례</strong> 조합으로 물리 어태커로 세팅하는 게 훨씬 강하다.</p>
<p>진화 전인 잉어킹이 레벨 20에 갸라도스로 진화하는데, <strong>잉어킹 구간이 지옥</strong>이다. 레벨 19까지 할 수 있는 기술이 물튀기기 하나뿐이다. 이 구간을 버티면 보상이 온다.</p>
<h3 id="내-갸라도스-파티-운용"><a class="anchor" aria-hidden="true" tabindex="-1" href="#내-갸라도스-파티-운용">#</a>내 갸라도스 파티 운용</h3>
<ul>
<li><strong>1번 갸라도스 (빨간 갸라도스)</strong>: 폭포오르기 / 지진 / 드래곤댄스 / 냉동빔</li>
<li><strong>2번 갸라도스 (일반)</strong>: 폭포오르기 / 불꽃세례 / 지진 / 흡혈</li>
</ul>
<p>드래곤댄스를 한 턴이라도 쌓으면 공격과 스피드가 동시에 오른다. 드래곤댄스 2회 이후 갸라도스는 목호의 파티 대부분을 <strong>1방</strong>에 정리했다. 이게 과장이 아니다. 목호가 용 타입 전문인데 갸라도스가 드래곤댄스 쌓은 상태에서 냉동빔을 쓰면 드래곤 타입이 전부 4배로 맞는다.</p>
<hr>
<h2 id="칠색조--성도의-수호신"><a class="anchor" aria-hidden="true" tabindex="-1" href="#칠색조--성도의-수호신">#</a>칠색조 — 성도의 수호신</h2>
<p><img src="/images/heartgold/kr_hooh_tower.jpg" alt="금빛탑 — 칠색조가 기다리는 곳" loading="lazy" decoding="async"></p>
<p><img src="/images/heartgold/kr_hooh_battle.jpg" alt="칠색조 배틀" loading="lazy" decoding="async"></p>
<p>하트골드의 전용 전설 포켓몬은 <strong>칠색조</strong>다. 불/비행 타입에 HP가 전설 중에서도 상당히 높다. 금빛탑에서 기모노 걸 퀘스트를 완료하고 레인보우윙을 입수하면 탑 꼭대기에서 레벨 45 칠색조와 조우한다.</p>
<p><img src="/images/heartgold/kr_hooh_bell_tower.jpg" alt="금빛탑 꼭대기" loading="lazy" decoding="async"></p>
<p>칠색조를 잡는 팁은 이렇다.</p>
<ul>
<li><strong>필수</strong>: 마스터볼 외에는 일반 볼로는 힘들다. 마스터볼을 아껴뒀다면 여기서 쓰면 된다</li>
<li><strong>약점</strong>: 바위, 물, 전기 타입이 효과적. 특히 바위 기술에 4배 약점</li>
<li><strong>황금 배틀 팁</strong>: HP를 빨간 게이지까지 깎은 뒤 잠재우기나 마비 상태이상 걸고 퀵볼/다이브볼 반복 투척</li>
<li>체력을 1로 만드는 기술을 가진 포켓몬이 있으면 더 쉽다 (거짓눈물+체력소진 콤보)</li>
</ul>
<p><img src="/images/heartgold/kr_hooh_battle2.jpg" alt="칠색조 배틀 2" loading="lazy" decoding="async"></p>
<p>칠색조는 파티에 넣는 순간 존재감이 다르다. 성스러운불꽃, 불꽃세례, 번개, 폭풍을 배우는데 화력도 강하고 특히 <strong>성스러운불꽃은 50% 확률로 화상</strong>을 입힌다. 실전에서 꽤나 귀찮은 상대다.</p>
<hr>
<h2 id="이브이--이-게임에서-선물로-받는다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#이브이--이-게임에서-선물로-받는다">#</a>이브이 — 이 게임에서 선물로 받는다</h2>
<p><img src="/images/heartgold/goldenrodcity.png" alt="골든로드 시티" loading="lazy" decoding="async"></p>
<p>하트골드에서 이브이는 <strong>빌의 할아버지</strong>에게서 받을 수 있다. 금빛시티 동쪽 일반도로에 있는 집에서 빌의 할아버지를 만나면 된다. 특별한 조건 없이 대화만 하면 이브이를 준다.</p>
<p>이브이를 뭘로 진화시키냐는 플레이 스타일에 따라 다르지만 내 선택은 <strong>에스퍼</strong>(사이킥)이었다. 이유는 간단하다. 당시 파티에 사이킥 타입이 없었고, 에스퍼의 특수공격과 스피드가 꽤 높기 때문이다.</p>
<p>문제는 진화 조건이 <strong>낮 시간대 + 높은 친밀도</strong>라는 점이다. 친밀도를 올리는 게 보통 노가다가 아니다. 파티 선두에 세워서 걷고, 이발소에 맡기고, 포켓워커에 넣어서 실제로 걸어다니고. 그렇게 한참을 공들여야 겨우 진화 조건이 충족된다. 낮에 레벨업시켜야 에스퍼로 진화하기 때문에, 밤에 레벨업시키면 블래키로 빠지니 주의해야 한다. 처음에 이걸 몰라서 밤에 레벨업 눌렀다가 블래키 될 뻔했다.</p>
<p>하트골드에서 이브이 진화 방법 정리:</p>
<table>
<thead>
<tr>
<th>진화형</th>
<th>조건</th>
</tr>
</thead>
<tbody>
<tr>
<td>부스터 (불꽃)</td>
<td>불의돌</td>
</tr>
<tr>
<td>쥬피썬더 (전기)</td>
<td>천둥의돌</td>
</tr>
<tr>
<td>시스루 (물)</td>
<td>물의돌</td>
</tr>
<tr>
<td>블래키 (악)</td>
<td>야간 친밀도 높음</td>
</tr>
<tr>
<td>에스퍼 (에스퍼)</td>
<td><strong>주간</strong> 친밀도 높음</td>
</tr>
<tr>
<td>글레이시아 (얼음)</td>
<td>4세대 없음 → 하트골드는 얼음의 돌 사용 불가, 소울실버에서 이누이 씨에게 부탁</td>
</tr>
<tr>
<td>리피아 (풀)</td>
<td>이끼의돌</td>
</tr>
</tbody>
</table>
<p>친밀도를 올리는 방법은 여러 가지다. 파티 선두에 세워두고 걷는 게 기본이고, 금빛시티 이발소에 맡기거나 포켓워커에 넣어서 실제로 걸어다니는 것도 효과적이다. 진화 직전에 낮인지 확인하고 레벨업시키는 것을 잊지 말자.</p>
<hr>
<h2 id="블레이범--브케인의-최종-진화-성도지방-에이스"><a class="anchor" aria-hidden="true" tabindex="-1" href="#블레이범--브케인의-최종-진화-성도지방-에이스">#</a>블레이범 — 브케인의 최종 진화, 성도지방 에이스</h2>
<p><img src="/images/heartgold/blbum.png" alt="블레이범" loading="lazy" decoding="async"></p>
<p><strong>블레이범</strong>은 2세대 스타터 브케인의 최종 진화형이다. 브케인이 레벨 14에 마그케인으로, 레벨 36에 블레이범으로 진화한다. 별도로 데려올 필요 없이 처음 스타터로 고른 브케인이 그대로 블레이범이 된다.</p>
<p>블레이범은 4세대 기준 <strong>특수공격과 스피드가 높은 불 타입 특수 어태커</strong>다.</p>
<ul>
<li>불꽃방사 / 분화 — 특수 불꽃 기술의 최강급. 분화는 체력이 가득 찼을 때 위력이 높다</li>
<li>썬더펀치 — 물 타입과 비행 타입 서브 대응</li>
<li>폭발펀치 — 격투 기술. 잠만보 처리에 유효</li>
<li>섀도볼 — 고스트·에스퍼 타입 대응</li>
</ul>
<p>특성 <strong>맹화</strong>는 체력이 1/3 이하로 떨어질 때 불꽃 기술 위력이 1.5배가 된다. 체력이 위험할수록 오히려 화력이 오르는 구조다. 레드전에서 눈보라 날씨 탓에 HP가 서서히 깎이는 상황에서 블레이범의 맹화가 터지면 분화 한 방에 상대가 날아가는 장면을 볼 수 있다.</p>
<p>레드전에서 블레이범이 결정적이었던 이유는 <strong>잠만보 처리</strong> 때문이다. 레벨 82 잠만보는 체력이 400 이상이고 구르기·자폭을 보유하고 있는데, 폭발펀치(격투)가 노말 타입 잠만보에 2배로 들어가서 빠르게 처리할 수 있었다.</p>
<hr>
<h2 id="짜증-포인트--이-게임이-즐겁기만-한-건-아니다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#짜증-포인트--이-게임이-즐겁기만-한-건-아니다">#</a>짜증 포인트 — 이 게임이 즐겁기만 한 건 아니다</h2>
<p>하트골드는 명작이다. 하지만 짜증나는 순간도 분명히 있다.</p>
<p><img src="/images/heartgold/rocketgoldenrod.jpg" alt="로켓단 골든로드 점거 사건" loading="lazy" decoding="async"></p>
<h3 id="1-로켓단-금빛시티-점거"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-로켓단-금빛시티-점거">#</a>1. 로켓단 금빛시티 점거</h3>
<p>중반부에 금빛시티 라디오탑이 로켓단에게 점령된다. 라디오탑 안에서 수차례 로켓단 조무래기들을 상대해야 하는데, 이 구간이 지나치게 길다. 체육관에 가기 전에 이 이벤트를 다 처리해야 하는 구조라 흐름이 끊긴다.</p>
<h3 id="2-루기아칠색조-잡기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-루기아칠색조-잡기">#</a>2. 루기아/칠색조 잡기</h3>
<p><img src="/images/heartgold/kr_lugia_whirl.jpg" alt="루기아 — 소용돌이 섬 접근" loading="lazy" decoding="async"></p>
<p><img src="/images/heartgold/kr_lugia_battle.jpg" alt="루기아 배틀" loading="lazy" decoding="async"></p>
<p>스토리에서 자연스럽게 만날 수 있다고 생각하면 오산이다. 루기아를 만나려면 태풍의 해로를 건너 소용돌이 섬에 들어가야 한다. 칠색조는 기모노 걸 퀘스트를 완료해야 금빛탑에 나타난다. 이 퀘스트 조건을 모르고 지나치면 전설 포켓몬 없이 클리어하게 된다.</p>
<h3 id="3-관동지방에-들어가는-순간의-허탈함"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-관동지방에-들어가는-순간의-허탈함">#</a>3. 관동지방에 들어가는 순간의 허탈함</h3>
<p>관동지방으로 넘어가는 순간의 설렘은 엄청나다. 그런데 막상 가보면 체육관 관장 레벨이 <strong>너무 낮다</strong>. 블루(그린)의 전진타운 체육관 관장도 레벨 50~60대라 성도 챔피언전 이후엔 싱거롭게 느껴진다. 관동 체육관 관장들이 리매치에서는 강해지지만 최초 조우 시엔 밍밍하다.</p>
<h3 id="4-잉어킹-레벨업"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-잉어킹-레벨업">#</a>4. 잉어킹 레벨업</h3>
<p>앞서 말했지만 잉어킹이 레벨 20까지 물튀기기 하나로 싸워야 한다는 건 고문이다. 경험치 공유 아이템도 초반엔 없고, 야생 포켓몬들이 세지는 구간에선 물튀기기가 효과 없는 포켓몬이 많다. 갸라도스 2마리를 목표로 한다면 이 구간을 두 번 거쳐야 한다. 정신력이 필요하다.</p>
<h3 id="5-분기-이벤트-가이드-없이는-놓친다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-분기-이벤트-가이드-없이는-놓친다">#</a>5. 분기 이벤트 가이드 없이는 놓친다</h3>
<p>기모노 걸 퀘스트, 빌의 할아버지 이벤트, 루기아 소용돌이 섬 등 중요한 이벤트가 필드를 돌아다니며 진행해야 하는 구조라 가이드 없이 하면 중요한 것들을 놓치기 쉽다. 특히 한글패치판 기준으로는 일부 텍스트가 어색할 수 있어 더 헷갈린다.</p>
<hr>
<h2 id="챔피언-목호--용-마스터-그러나-갸라도스가-답이다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#챔피언-목호--용-마스터-그러나-갸라도스가-답이다">#</a>챔피언 목호 — 용 마스터, 그러나 갸라도스가 답이다</h2>
<p><img src="/images/heartgold/elite5.png" alt="목호 챔피언방" loading="lazy" decoding="async"></p>
<p>사천왕과 챔피언 앞에서 하이라이트가 있다. 챔피언 <strong>목호</strong>다. 용 타입 전문 트레이너로, 포켓몬 시리즈 통틀어 가장 유명한 챔피언 중 한 명이다.</p>
<h3 id="목호의-파티-하트골드-기준"><a class="anchor" aria-hidden="true" tabindex="-1" href="#목호의-파티-하트골드-기준">#</a>목호의 파티 (하트골드 기준)</h3>
<table>
<thead>
<tr>
<th>포켓몬</th>
<th>레벨</th>
<th>타입</th>
</tr>
</thead>
<tbody>
<tr>
<td>갸라도스</td>
<td>44</td>
<td>물/비행</td>
</tr>
<tr>
<td>프테라</td>
<td>46</td>
<td>바위/비행</td>
</tr>
<tr>
<td>리자몽</td>
<td>46</td>
<td>불/비행</td>
</tr>
<tr>
<td>망나뇽</td>
<td>49</td>
<td>용/비행</td>
</tr>
<tr>
<td>망나뇽</td>
<td>49</td>
<td>용/비행</td>
</tr>
<tr>
<td>망나뇽</td>
<td>50</td>
<td>용/비행</td>
</tr>
</tbody>
</table>
<p>목호의 망나뇽이 세 마리다. 레벨 49~50이고 하이퍼빔 + 블리자드를 달고 나온다. 체력이 낮은 파티라면 하이퍼빔 한 방에 나가떨어질 수 있다.</p>
<h3 id="목호-공략-방법"><a class="anchor" aria-hidden="true" tabindex="-1" href="#목호-공략-방법">#</a>목호 공략 방법</h3>
<p><strong>기본 전략 — 드래곤댄스 갸라도스</strong></p>
<p>갸라도스로 드래곤댄스를 1~2회 쌓은 뒤 냉동빔을 사용하면 용/비행 타입인 망나뇽이 4배 약점으로 맞는다. 목호 파티의 대부분이 1방에 떨어진다.</p>
<p>갸라도스는 목호의 갸라도스에게도 유리하다. 상대 갸라도스에게는 지진 또는 냉동빔이 잘 통한다.</p>
<p><strong>주의해야 할 것</strong></p>
<ul>
<li>핫삼은 전기, 불 타입에 약함. 블레이범의 불꽃기술이나 칠색조의 성스러운불꽃으로 처리</li>
<li>목호 파티 전체가 교체 없이 연속으로 나오므로 체력 관리가 중요</li>
<li>사천왕을 잇달아 클리어해야 하기 때문에 회복 아이템을 넉넉히 챙겨야 함. 최소 기력의조각 15개 + 기력의 덩어리 10개 이상</li>
</ul>
<p><strong>어려운 이유</strong></p>
<p>솔직히 말하면 목호 자체는 처음 만날 때 충분히 어렵다. 망나뇽 레벨 49<del>50 세 마리가 연속으로 나오는데 HP가 60% 이하면 하이퍼빔을 바로 쓴다. 첫 도전에서 1</del>2마리 파티가 쓰러지는 건 흔한 일이다. 내 경우 두 번째 도전에서 드래곤댄스 전략을 쓰고 나서야 안정적으로 클리어했다.</p>
<hr>
<h2 id="관동지방--배를-타고-건너는-순간의-설렘"><a class="anchor" aria-hidden="true" tabindex="-1" href="#관동지방--배를-타고-건너는-순간의-설렘">#</a>관동지방 — 배를 타고 건너는 순간의 설렘</h2>
<p><img src="/images/heartgold/pallettown.jpg" alt="팔레트타운 (관동 출발점)" loading="lazy" decoding="async"></p>
<p>성도지방 챔피언을 쓰러뜨리면 관동지방으로 넘어갈 수 있게 된다. 기차나 배를 타고 이동하는데, 이 순간의 설렘은 표현하기가 어렵다.</p>
<p>어릴 때 포켓몬 레드/블루/황금/실버를 했던 사람이라면 팔레트타운에 처음 도착했을 때 무너지는 감정을 알 거다. BGM부터 다르고, 오크 박사 연구소가 보이고, 1번 도로가 펼쳐진다. "아, 여기야" 하는 느낌.</p>
<p><img src="/images/heartgold/route1.jpg" alt="관동 1번도로" loading="lazy" decoding="async"></p>
<h3 id="관동지방-클리어-팁"><a class="anchor" aria-hidden="true" tabindex="-1" href="#관동지방-클리어-팁">#</a>관동지방 클리어 팁</h3>
<p>관동지방 체육관은 처음엔 레벨이 낮아서 성도 이후 파티라면 어렵지 않다. 하지만 <strong>8개 체육관을 다 클리어해야 레드와 싸울 수 있다</strong>는 걸 기억해야 한다.</p>
<p><strong>관동 체육관 순서 및 주요 정보</strong></p>
<ol>
<li><strong>쥐포시티 (바위/땅)</strong> — 이수. 바위/땅 타입이라 물/풀로 대응</li>
<li><strong>연분홍시티 (물)</strong> — 미스티. 물 타입에 유의</li>
<li><strong>민트시티 (전기)</strong> — 서지. 전기 타입이라 땅 기술로</li>
<li><strong>담청시티 (풀)</strong> — 에리카. 불/벌레/독으로</li>
<li><strong>홍련시티 (독)</strong> — 도희. 독 타입이라 에스퍼나 땅으로</li>
<li><strong>계피시티 (에스퍼)</strong> — 사빈나. 에스퍼 타입이라 벌레/악/고스트로</li>
<li><strong>홍련섬 (불)</strong> — 블레인. 불 타입이라 물/바위로 대응. 하트골드에서는 소용돌이 섬 근처로 이동</li>
<li><strong>상록시티 (땅)</strong> — 블루(그린). 에스퍼/파이팅 등 다양한 타입 혼합</li>
</ol>
<p>블루는 완전 혼합 파티라 단일 타입 대응이 어렵다. 파티를 고루 키워서 가는 게 맞다.</p>
<h3 id="관동에서-놓치면-아쉬운-것들"><a class="anchor" aria-hidden="true" tabindex="-1" href="#관동에서-놓치면-아쉬운-것들">#</a>관동에서 놓치면 아쉬운 것들</h3>
<ul>
<li><strong>오크 박사 연구소</strong> 방문. 기술 데이터 등을 확인할 수 있고, 팔파크 이용 전 전국도감 완성 진행도를 볼 수 있음</li>
<li><strong>실버 산악</strong> 진입 전에 레벨 최소 55 이상 권장. 야생 포켓몬 레벨이 높고 레드가 레벨 80 이상 파티를 들고 나옴</li>
<li><strong>팔파크</strong> 이용 — 관동에서 전국도감을 쓰기 시작하면 3세대 포켓몬 이전이 가능</li>
</ul>
<hr>
<h2 id="끝판왕-레드--이-게임의-진짜-최종-보스"><a class="anchor" aria-hidden="true" tabindex="-1" href="#끝판왕-레드--이-게임의-진짜-최종-보스">#</a>끝판왕 레드 — 이 게임의 진짜 최종 보스</h2>
<p>챔피언을 잡고 다 끝났다고 생각한다면 오산이다. 이 게임의 진짜 끝판왕은 따로 있다.</p>
<p><strong>실버 산악 정상에 서 있는 레드.</strong></p>
<p>포켓몬 레드/블루의 주인공, 전설의 트레이너 레드가 모자 푹 눌러쓰고 눈보라 속에 서 있다. 말을 걸면 아무런 대화 없이 바로 배틀이 시작된다. "......" 대사 하나.</p>
<p><img src="/images/heartgold/red-battle.jpg" alt="레드와의 전투" loading="lazy" decoding="async"></p>
<h3 id="레드의-파티-하트골드-기준"><a class="anchor" aria-hidden="true" tabindex="-1" href="#레드의-파티-하트골드-기준">#</a>레드의 파티 (하트골드 기준)</h3>
<table>
<thead>
<tr>
<th>포켓몬</th>
<th>레벨</th>
<th>타입</th>
</tr>
</thead>
<tbody>
<tr>
<td>피카츄</td>
<td><strong>88</strong></td>
<td>전기</td>
</tr>
<tr>
<td>리자몽</td>
<td>84</td>
<td>불/비행</td>
</tr>
<tr>
<td>거북왕</td>
<td>84</td>
<td>물</td>
</tr>
<tr>
<td>이상해꽃</td>
<td>84</td>
<td>풀/독</td>
</tr>
<tr>
<td>라프라스</td>
<td>80</td>
<td>물/얼음</td>
</tr>
<tr>
<td>잠만보</td>
<td>82</td>
<td>노말</td>
</tr>
</tbody>
</table>
<p>레벨 88 피카츄가 선봉으로 나온다. 이 피카츄가 <strong>번개 + 신속 + 파도타기 + 전기자석파</strong>를 들고 있다. 전기자석파로 마비를 걸고 번개를 맞으면 파티 하나가 순식간에 날아간다.</p>
<p>레드를 이기려면 최소 레벨 70 이상의 파티가 필요하다. 체력 회복 아이템도 넉넉히 챙겨야 한다. 나는 고급회복약 30개 + 기력의 덩어리 15개를 가지고 들어갔는데 빠듯했다.</p>
<h3 id="레드-공략-팁"><a class="anchor" aria-hidden="true" tabindex="-1" href="#레드-공략-팁">#</a>레드 공략 팁</h3>
<p><strong>1. 피카츄 처리 — 최우선</strong></p>
<p>피카츄 레벨 88은 농담이 아니다. 파도타기와 번개를 동시에 보유하고 있어서 물 타입도 안전하지 않다. 내가 쓴 방법은 블레이범의 폭발펀치(격투). 격투 기술이 노말 타입 잠만보에도 효과적이어서 후반부 처리에도 유용했다.</p>
<p><strong>2. 리자몽 처리</strong></p>
<p>비행/불 타입이라 바위 기술에 4배 약점이다. 에스퍼의 사이킥은 효과가 없다. 갸라도스의 스톤에지나 물 기술을 활용할 것.</p>
<p><strong>3. 잠만보 처리</strong></p>
<p>체력이 400 이상이다. 구르기나 자폭을 쓰기 때문에 조심해야 한다. 격투 타입이 2배로 유효하다. 블레이범의 폭발펀치가 이 구간에서 빛난다.</p>
<p><strong>4. 눈보라 날씨</strong></p>
<p>레드전에서는 날씨가 <strong>눈보라</strong>다. 비얼음 타입 포켓몬은 매 턴 HP가 1/16씩 닳는다. 장기전이 될수록 내 포켓몬이 체력을 잃어간다. 최대한 빠르게 상대를 정리하는 게 핵심이다.</p>
<p><strong>5. 회복 아이템을 아끼지 말 것</strong></p>
<p>이미 6마리 포켓몬이 레벨 80 이상인 상대다. 회복 아이템을 쓰는 게 전혀 창피한 게 아니다. 고급회복약을 충분히 챙겨가고 아끼지 않는 게 맞다.</p>
<h3 id="레드를-이겼을-때"><a class="anchor" aria-hidden="true" tabindex="-1" href="#레드를-이겼을-때">#</a>레드를 이겼을 때</h3>
<p>레드를 쓰러뜨리면 세이브가 되고 마을로 귀환한다. 엔딩이 따로 없다. 그게 이 게임의 연출이다. "네가 챔피언이 됐다"는 화려한 연출 대신, 그냥 그것으로 끝이다. 담담하지만 여운이 크다.</p>
<hr>
<h2 id="성도지방-공략-핵심-팁-정리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#성도지방-공략-핵심-팁-정리">#</a>성도지방 공략 핵심 팁 정리</h2>
<p><img src="/images/heartgold/kr_raikou.jpg" alt="라이코 — 이동형 전설 포켓몬 추적" loading="lazy" decoding="async"></p>
<p><img src="/images/heartgold/kr_entei.jpg" alt="엔테이 — 분노의 호수 이후 전국을 떠도는 전설" loading="lazy" decoding="async"></p>
<p><img src="/images/heartgold/ecruteakcity.png" alt="인주시티" loading="lazy" decoding="async"></p>
<p>성도지방을 처음 플레이하는 사람들을 위한 핵심 정리.</p>
<p><strong>초반 (1-3 체육관)</strong></p>
<ul>
<li>스타터를 브케인으로 선택하면 3번 체육관까지 압도적으로 유리하다</li>
<li>잉어킹을 초반부터 파티에 넣어 갸라도스로 만들어두는 걸 권장. 레벨 20만 채우면 된다</li>
<li>분기 도구인 야다란의 열쇠를 꼭 입수할 것</li>
</ul>
<p><strong>중반 (4-6 체육관)</strong></p>
<ul>
<li>분노의 호수 이벤트 전에 <strong>파티를 레벨 30 이상</strong>으로 맞출 것</li>
<li>기모노 걸을 인주시티에서 만나면 퀘스트가 시작된다. 이후 각 도시에서 이벤트 진행 필수</li>
<li>체육관 순서를 무시하고 자유도 있게 진행할 수 있지만 체육관 관장 레벨이 고정돼 있어서 너무 앞서가면 싱겁다</li>
</ul>
<p><img src="/images/heartgold/kr_dratini_battle.jpg" alt="용굴 — 신속 미뇽 획득" loading="lazy" decoding="async"></p>
<ul>
<li><strong>검은먹시티 용굴</strong>에서 신속 미뇽을 받을 수 있다. 용굴 어른에게 퀴즈를 통과하면 특성 신속(늘 선제 행동)을 가진 미뇽을 준다. 실전에서 엄청 강하진 않지만 망나뇽까지 키우면 스토리 후반에 충분히 써먹는다</li>
</ul>
<p><strong>후반 (7-8 체육관 + 챔피언)</strong></p>
<ul>
<li>흑빛시티 이전에 <strong>강철 타입 포켓몬</strong>이 있으면 좋다. 7번 체육관 땅 타입에 약하다</li>
<li>검은먹시티 클레어는 드래곤 타입이라 얼음, 용 기술이 유효</li>
<li><strong>챔피언전 전에 회복 아이템 최대한 채워둘 것</strong>. 포켓몬 센터에서 충분히 구매</li>
</ul>
<hr>
<h2 id="관동지방-공략-핵심-팁-정리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#관동지방-공략-핵심-팁-정리">#</a>관동지방 공략 핵심 팁 정리</h2>
<p><img src="/images/heartgold/goldenrodcity.png" alt="골든로드 시티" loading="lazy" decoding="async"></p>
<p>관동지방에 처음 들어선다면 아래 내용을 꼭 기억해두자.</p>
<p><strong>반드시 해야 할 것</strong></p>
<ul>
<li>오크 박사 연구소 방문. 전국도감 진행도를 체크해야 팔파크 이용 가능</li>
<li><strong>전화번호 등록된 트레이너들과 리매치</strong> — 관동 체육관 관장들도 리매치에서 레벨이 높아진다. 진짜 도전 상대는 리매치 버전</li>
<li>실버 산악 진입 전 파티 레벨 최소 65 이상 맞추기 권장</li>
</ul>
<p><strong>관동 체육관 관장 중 어려운 것들</strong></p>
<ul>
<li><strong>블루(그린)</strong> — 상록시티. 레벨 60 혼합 파티. 챔피언급 난이도</li>
<li><strong>미스티</strong> — 연분홍시티. 물 전문이지만 리매치에서 레벨이 높고 스텔스록이 있음</li>
</ul>
<p><strong>레드 도전 전 체크리스트</strong></p>
<ul>
<li>파티 레벨 최소 70 이상</li>
<li>고급회복약 30개 이상</li>
<li>기력의 덩어리 15개 이상</li>
<li>부활초 5개 이상</li>
<li>전기 타입을 막을 수 있는 땅 타입 포켓몬 (피카츄 처리용)</li>
<li>격투 타입 기술 보유 (잠만보 처리용)</li>
</ul>
<hr>
<h2 id="총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#총평">#</a>총평</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>평가</th>
</tr>
</thead>
<tbody>
<tr>
<td>스토리</td>
<td>★★★★★ — 두 지방, 두 전설, 레드라는 결말</td>
</tr>
<tr>
<td>볼륨</td>
<td>★★★★★ — 포켓몬 시리즈 최대급</td>
</tr>
<tr>
<td>전투 시스템</td>
<td>★★★★☆ — 4세대 기준 균형 잘 맞음</td>
</tr>
<tr>
<td>음악</td>
<td>★★★★★ — 성도/관동 BGM 모두 레전드</td>
</tr>
<tr>
<td>짜증 요소</td>
<td>★★★☆☆ — 있긴 있다. 잉어킹 구간이 제일 힘들다</td>
</tr>
</tbody>
</table>
<p>포켓몬 하트골드를 클리어하고 나서 든 생각은 단 하나였다. "왜 이걸 이제 했지?"</p>
<p>성도지방 클리어에서 오는 성취감, 관동지방에 도착했을 때의 감동, 그리고 눈보라 속 레드와의 침묵 속 배틀. 이 세 가지가 합쳐지면 다른 포켓몬 게임이 흉내 낼 수 없는 경험이 된다.</p>
<p>갸라도스 2마리 파티를 추천하는 이유는, 이 게임이 갸라도스에게 맞춰진 게임이라는 느낌이 들어서다. 빨간 갸라도스를 분노의 호수에서 만나는 그 장면, 드래곤댄스를 쌓아 목호의 망나뇽을 전부 날려버리는 그 순간. 하트골드가 갸라도스의 게임임을 확신하게 된다.</p>
<p>아직 안 해봤다면 꼭 해봐라. 후회하지 않는다.</p>
<hr>
<p><em>플레이 기준: 포켓몬스터 하트골드 (NDS) 한글판</em>
<em>클리어 파티: 갸라도스(빨강) / 갸라도스(일반) / 칠색조 / 에스퍼 / 블레이범 / 루기아</em></p>]]></content:encoded>
      <category><![CDATA[리뷰]]></category>
      <category><![CDATA[포켓몬]]></category>
      <category><![CDATA[하트골드]]></category>
      <category><![CDATA[NDS]]></category>
      <category><![CDATA[RPG]]></category>
      <category><![CDATA[게임리뷰]]></category>
      <category><![CDATA[갸라도스]]></category>
      <category><![CDATA[칠색조]]></category>
      <category><![CDATA[레트로게임]]></category>
      <category><![CDATA[포켓몬스터]]></category>
    </item>

    <item>
      <title><![CDATA[Spring Boot + React + Oracle + MyBatis 셋팅 — 2편: Spring Boot 프로젝트 생성 & Oracle 연결]]></title>
      <link>https://www.stragos.xyz/posts/springboot-react-oracle-mybatis-setup-2</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/springboot-react-oracle-mybatis-setup-2</guid>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[Spring Initializr로 프로젝트를 만들고 application.yml에 Oracle 접속 정보를 설정해 실제로 DB에 연결하는 법을 알아봅니다.]]></description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>시리즈 목차</strong></p>
<ul>
<li>1편: 개발 환경 준비</li>
<li><strong>2편: Spring Boot 프로젝트 생성 &#x26; Oracle 연결</strong> ← 지금 여기</li>
<li>3편: MyBatis 설정 &#x26; CRUD API 만들기</li>
<li>4편: React 프로젝트 생성 &#x26; Spring Boot API 연동</li>
</ul>
</blockquote>
<hr>
<h2 id="spring-initializr로-프로젝트-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#spring-initializr로-프로젝트-만들기">#</a>Spring Initializr로 프로젝트 만들기</h2>
<p><a href="https://start.spring.io">start.spring.io</a> 에 접속합니다. 직접 설정할 수도 있고, IntelliJ에서 바로 만들 수도 있습니다. 여기선 웹에서 만드는 방법을 먼저 설명합니다.</p>
<h3 id="설정값"><a class="anchor" aria-hidden="true" tabindex="-1" href="#설정값">#</a>설정값</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>선택</th>
</tr>
</thead>
<tbody>
<tr>
<td>Project</td>
<td><strong>Gradle - Groovy</strong></td>
</tr>
<tr>
<td>Language</td>
<td>Java</td>
</tr>
<tr>
<td>Spring Boot</td>
<td><strong>3.x.x</strong> (학습용 권장, 실무 최신은 4.x)</td>
</tr>
<tr>
<td>Packaging</td>
<td>Jar</td>
</tr>
<tr>
<td>Java</td>
<td><strong>17</strong></td>
</tr>
</tbody>
</table>
<p><strong>Project Metadata:</strong></p>
<ul>
<li>Group: <code>com.example</code></li>
<li>Artifact: <code>backend</code></li>
<li>Name: <code>backend</code></li>
<li>Package name: <code>com.example.backend</code></li>
</ul>
<h3 id="의존성dependencies-추가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#의존성dependencies-추가">#</a>의존성(Dependencies) 추가</h3>
<p><strong>ADD DEPENDENCIES</strong> 버튼 클릭 후 아래 항목 추가:</p>
<table>
<thead>
<tr>
<th>의존성</th>
<th>설명</th>
</tr>
</thead>
<tbody>
<tr>
<td>Spring Web</td>
<td>REST API 만들기</td>
</tr>
<tr>
<td>MyBatis Framework</td>
<td>SQL 매핑</td>
</tr>
<tr>
<td>Oracle Driver</td>
<td>Oracle JDBC 드라이버</td>
</tr>
<tr>
<td>Lombok</td>
<td>반복 코드 줄이기</td>
</tr>
</tbody>
</table>
<p>설정 완료 후 <strong>GENERATE</strong> → ZIP 다운로드 → 원하는 폴더에 압축 해제</p>
<hr>
<h2 id="intellij에서-프로젝트-열기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#intellij에서-프로젝트-열기">#</a>IntelliJ에서 프로젝트 열기</h2>
<ol>
<li>IntelliJ 실행 → <strong>Open</strong> → 압축 해제한 <code>backend</code> 폴더 선택</li>
<li>Gradle 빌드가 자동으로 시작됩니다 (우측 하단 진행바 확인)</li>
<li>빌드 완료까지 잠깐 기다립니다</li>
</ol>
<blockquote>
<p>처음엔 인터넷에서 라이브러리를 받아야 해서 시간이 걸립니다. 2~3분 정도 기다리면 됩니다.</p>
</blockquote>
<hr>
<h2 id="buildgradle-확인"><a class="anchor" aria-hidden="true" tabindex="-1" href="#buildgradle-확인">#</a>build.gradle 확인</h2>
<p><code>build.gradle</code> 파일을 열어 의존성이 제대로 들어왔는지 확인합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="groovy" data-theme="github-dark"><code data-language="groovy" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">dependencies {</span></span>
<span data-line=""><span style="color:#E1E4E8">    implementation </span><span style="color:#9ECBFF">'org.springframework.boot:spring-boot-starter-web'</span></span>
<span data-line=""><span style="color:#E1E4E8">    implementation </span><span style="color:#9ECBFF">'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'</span></span>
<span data-line=""><span style="color:#E1E4E8">    runtimeOnly </span><span style="color:#9ECBFF">'com.oracle.database.jdbc:ojdbc11'</span></span>
<span data-line=""><span style="color:#E1E4E8">    compileOnly </span><span style="color:#9ECBFF">'org.projectlombok:lombok'</span></span>
<span data-line=""><span style="color:#E1E4E8">    annotationProcessor </span><span style="color:#9ECBFF">'org.projectlombok:lombok'</span></span>
<span data-line=""><span style="color:#E1E4E8">    testImplementation </span><span style="color:#9ECBFF">'org.springframework.boot:spring-boot-starter-test'</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<blockquote>
<p>MyBatis 버전(<code>3.0.3</code>)은 Spring Boot 3.x와 맞춰야 합니다. 자동으로 맞는 버전이 들어오지만, 혹시 에러가 나면 버전을 확인해보세요.</p>
</blockquote>
<hr>
<h2 id="applicationyml-설정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#applicationyml-설정">#</a>application.yml 설정</h2>
<p><code>src/main/resources/application.properties</code> 파일이 기본으로 있을 겁니다. 이걸 <strong>application.yml</strong>로 바꾸면 더 보기 편합니다.</p>
<ol>
<li>파일명을 <code>application.yml</code>로 변경</li>
<li>아래 내용 작성:</li>
</ol>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="yaml" data-theme="github-dark"><code data-language="yaml" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#85E89D">spring</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">  datasource</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">    url</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">jdbc:oracle:thin:@localhost:1521/XE</span></span>
<span data-line=""><span style="color:#85E89D">    username</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">devuser</span></span>
<span data-line=""><span style="color:#85E89D">    password</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">devpass123</span></span>
<span data-line=""><span style="color:#85E89D">    driver-class-name</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">oracle.jdbc.OracleDriver</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#85E89D">  sql</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">    init</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">      platform</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">oracle</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#85E89D">mybatis</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">  mapper-locations</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">classpath:mapper/**/*.xml</span></span>
<span data-line=""><span style="color:#85E89D">  type-aliases-package</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">com.example.backend.dto</span></span>
<span data-line=""><span style="color:#85E89D">  configuration</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">    map-underscore-to-camel-case</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span></span>
<span data-line=""><span style="color:#85E89D">    log-impl</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">org.apache.ibatis.logging.stdout.StdOutImpl</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#85E89D">server</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">  port</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">8080</span></span></code></pre></figure>
<h3 id="각-설정-설명"><a class="anchor" aria-hidden="true" tabindex="-1" href="#각-설정-설명">#</a>각 설정 설명</h3>
<p><strong>datasource:</strong></p>
<ul>
<li><code>url</code>: Oracle 접속 주소. <code>@localhost:1521/XE</code> 형식입니다. XE는 서비스명입니다.</li>
<li><code>username</code> / <code>password</code>: 1편에서 만든 계정 정보</li>
</ul>
<p><strong>mybatis:</strong></p>
<ul>
<li><code>mapper-locations</code>: SQL XML 파일 위치. <code>resources/mapper/</code> 폴더 아래에 둡니다.</li>
<li><code>map-underscore-to-camel-case</code>: DB의 <code>USER_NAME</code> → Java의 <code>userName</code> 자동 변환</li>
<li><code>log-impl</code>: 실행되는 SQL을 콘솔에 출력 (개발할 때 편합니다)</li>
</ul>
<hr>
<h2 id="폴더-구조-만들기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#폴더-구조-만들기">#</a>폴더 구조 만들기</h2>
<p><code>src/main/java/com/example/backend/</code> 아래에 폴더를 만듭니다:</p>
<pre><code>backend/
└── src/main/java/com/example/backend/
    ├── BackendApplication.java  ← 자동 생성됨
    ├── controller/
    ├── service/
    ├── mapper/
    └── dto/
</code></pre>
<p><code>src/main/resources/</code> 아래:</p>
<pre><code>resources/
├── application.yml
└── mapper/          ← SQL XML 파일 놓을 곳
</code></pre>
<p>IntelliJ에서 폴더 만드는 법: 해당 패키지 우클릭 → New → Package</p>
<hr>
<h2 id="연결-테스트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#연결-테스트">#</a>연결 테스트</h2>
<p>프로젝트가 Oracle에 제대로 붙는지 확인해봅니다.</p>
<h3 id="간단한-테스트-컨트롤러-작성"><a class="anchor" aria-hidden="true" tabindex="-1" href="#간단한-테스트-컨트롤러-작성">#</a>간단한 테스트 컨트롤러 작성</h3>
<p><code>controller/TestController.java</code> 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">package</span><span style="color:#E1E4E8"> com.example.backend.controller;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.web.bind.annotation.GetMapping;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.web.bind.annotation.RestController;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RestController</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> TestController</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">GetMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/api/hello"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">hello</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#9ECBFF"> "Spring Boot 연결 성공!"</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<h3 id="실행"><a class="anchor" aria-hidden="true" tabindex="-1" href="#실행">#</a>실행</h3>
<p><code>BackendApplication.java</code> 파일을 열고 왼쪽 초록 삼각형 클릭 → <strong>Run</strong></p>
<p>콘솔에 이런 로그가 보이면 성공:</p>
<pre><code>  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
...
Tomcat started on port 8080
Started BackendApplication in 3.xxx seconds
</code></pre>
<p>브라우저에서 <code>http://localhost:8080/api/hello</code> 접속 → <code>Spring Boot 연결 성공!</code> 텍스트 확인</p>
<hr>
<h2 id="자주-나오는-에러와-해결법"><a class="anchor" aria-hidden="true" tabindex="-1" href="#자주-나오는-에러와-해결법">#</a>자주 나오는 에러와 해결법</h2>
<h3 id="ora-01017-invalid-usernamepassword"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ora-01017-invalid-usernamepassword">#</a>ORA-01017: invalid username/password</h3>
<p><code>application.yml</code>의 username/password가 틀렸습니다. 1편에서 만든 계정 정보를 다시 확인하세요.</p>
<h3 id="listener-refused-the-connection"><a class="anchor" aria-hidden="true" tabindex="-1" href="#listener-refused-the-connection">#</a>Listener refused the connection</h3>
<p>Oracle 서비스가 안 켜져 있습니다. Windows 서비스에서 <code>OracleServiceXE</code>와 <code>OracleXETNSListener</code>가 실행 중인지 확인하세요.</p>
<pre><code>작업 관리자 → 서비스 탭 → OracleServiceXE 검색
</code></pre>
<p>상태가 "중지됨"이면 우클릭 → 시작</p>
<h3 id="classnotfoundexception-oraclejdbcoracledriver"><a class="anchor" aria-hidden="true" tabindex="-1" href="#classnotfoundexception-oraclejdbcoracledriver">#</a>ClassNotFoundException: oracle.jdbc.OracleDriver</h3>
<p><code>build.gradle</code>에 Oracle 드라이버 의존성이 없거나, Gradle 새로고침이 안 됐습니다. IntelliJ 우측 Gradle 패널 → 새로고침 버튼 클릭.</p>
<hr>
<h2 id="db-연결까지-확인하기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#db-연결까지-확인하기">#</a>DB 연결까지 확인하기</h2>
<p>단순히 서버가 뜨는 것만으로는 Oracle 연결이 됐는지 알 수 없습니다. 실제 쿼리를 날려보겠습니다.</p>
<h3 id="dual-테이블로-테스트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#dual-테이블로-테스트">#</a>DUAL 테이블로 테스트</h3>
<p>Oracle에는 기본 제공 테이블인 <code>DUAL</code>이 있습니다. 아무 데이터도 없지만 간단한 쿼리 테스트에 씁니다.</p>
<p><code>mapper/TestMapper.java</code> 인터페이스 생성:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">package</span><span style="color:#E1E4E8"> com.example.backend.mapper;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.apache.ibatis.annotations.Mapper;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.apache.ibatis.annotations.Select;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Mapper</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> interface</span><span style="color:#B392F0"> TestMapper</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Select</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"SELECT 'DB 연결 성공' AS msg FROM DUAL"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#E1E4E8">    String </span><span style="color:#B392F0">testConnection</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>컨트롤러 수정:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">package</span><span style="color:#E1E4E8"> com.example.backend.controller;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> com.example.backend.mapper.TestMapper;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> lombok.RequiredArgsConstructor;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.web.bind.annotation.GetMapping;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.springframework.web.bind.annotation.RestController;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RestController</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RequiredArgsConstructor</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> TestController</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> TestMapper testMapper;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">GetMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/api/hello"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">hello</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> testMapper.</span><span style="color:#B392F0">testConnection</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>다시 실행 후 <code>http://localhost:8080/api/hello</code> 접속 → <code>DB 연결 성공</code> 텍스트가 나오면 <strong>Oracle까지 완벽하게 연결된 겁니다.</strong></p>
<hr>
<h2 id="정리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#정리">#</a>정리</h2>
<ul>
<li>Spring Initializr로 프로젝트 생성</li>
<li><code>application.yml</code>에 Oracle 접속 정보 설정</li>
<li>서버 실행 &#x26; DUAL 테이블로 DB 연결 확인</li>
</ul>
<hr>
<h2 id="다음-편-예고"><a class="anchor" aria-hidden="true" tabindex="-1" href="#다음-편-예고">#</a>다음 편 예고</h2>
<p><strong>3편: MyBatis 설정 &#x26; CRUD API 만들기</strong></p>
<p>실제 테이블을 만들고, MyBatis XML 매퍼를 작성해서 회원 목록 조회 / 등록 / 수정 / 삭제 API를 만들어봅니다. 실무에서 가장 자주 쓰는 패턴으로 작성합니다.</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[SpringBoot]]></category>
      <category><![CDATA[Oracle]]></category>
      <category><![CDATA[MyBatis]]></category>
      <category><![CDATA[풀스택]]></category>
      <category><![CDATA[초보자]]></category>
      <category><![CDATA[백엔드]]></category>
    </item>

    <item>
      <title><![CDATA[Spring Boot + React + Oracle + MyBatis 셋팅 — 1편: 개발 환경 준비]]></title>
      <link>https://www.stragos.xyz/posts/springboot-react-oracle-mybatis-setup-1</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/springboot-react-oracle-mybatis-setup-1</guid>
      <pubDate>Sun, 12 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[초보도 쉽게 따라하는 풀스택 셋팅 시리즈 1편. JDK, Node.js, Oracle XE, IDE까지 개발 환경을 처음부터 세팅합니다.]]></description>
      <content:encoded><![CDATA[<blockquote>
<p><strong>시리즈 목차</strong></p>
<ul>
<li><strong>1편: 개발 환경 준비</strong> ← 지금 여기</li>
<li>2편: Spring Boot 프로젝트 생성 &#x26; Oracle 연결</li>
<li>3편: MyBatis 설정 &#x26; CRUD API 만들기</li>
<li>4편: React 프로젝트 생성 &#x26; Spring Boot API 연동</li>
</ul>
</blockquote>
<hr>
<h2 id="이-시리즈가-뭔가요"><a class="anchor" aria-hidden="true" tabindex="-1" href="#이-시리즈가-뭔가요">#</a>이 시리즈가 뭔가요?</h2>
<p>회사에서 가장 많이 쓰는 기술 스택 중 하나가 바로 <strong>Spring Boot + React + Oracle + MyBatis</strong> 조합입니다. 특히 국내 SI, 금융, 공공 분야에서 거의 표준처럼 쓰입니다.</p>
<p>그런데 막상 처음 시작하려면 막막합니다. 어디서부터 설치해야 하는지, 왜 에러가 나는지 모르겠고... 이 시리즈는 그런 분들을 위해 <strong>처음부터 하나씩</strong> 같이 세팅합니다.</p>
<hr>
<h2 id="필요한-것-목록"><a class="anchor" aria-hidden="true" tabindex="-1" href="#필요한-것-목록">#</a>필요한 것 목록</h2>
<table>
<thead>
<tr>
<th>도구</th>
<th>역할</th>
<th>버전</th>
</tr>
</thead>
<tbody>
<tr>
<td>JDK 17</td>
<td>Spring Boot 실행 환경</td>
<td>17 LTS</td>
</tr>
<tr>
<td>Node.js</td>
<td>React 실행 환경</td>
<td>22 LTS</td>
</tr>
<tr>
<td>Oracle XE</td>
<td>데이터베이스</td>
<td>21c</td>
</tr>
<tr>
<td>IntelliJ IDEA</td>
<td>백엔드 IDE</td>
<td>Community 무료</td>
</tr>
<tr>
<td>VS Code</td>
<td>프론트엔드 IDE</td>
<td>최신</td>
</tr>
<tr>
<td>DBeaver</td>
<td>DB 관리 GUI</td>
<td>Community 무료</td>
</tr>
</tbody>
</table>
<p>다 무료입니다. 걱정 마세요.</p>
<hr>
<h2 id="1-jdk-17-설치"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-jdk-17-설치">#</a>1. JDK 17 설치</h2>
<h3 id="왜-17인가요"><a class="anchor" aria-hidden="true" tabindex="-1" href="#왜-17인가요">#</a>왜 17인가요?</h3>
<p>JDK 8, 11, 17, 21 중에서 고르라면 <strong>17을 추천</strong>합니다. Spring Boot 3.x 이상이 JDK 17을 최소 요구사항으로 하고 있습니다. 실무에서는 Java 21 LTS가 가상 스레드(Virtual Threads) 등 성능 개선을 포함해 더 권장되지만, 처음 배우는 환경이라면 17로 시작해도 무방합니다.</p>
<h3 id="설치-방법"><a class="anchor" aria-hidden="true" tabindex="-1" href="#설치-방법">#</a>설치 방법</h3>
<ol>
<li><a href="https://adoptium.net">Eclipse Temurin</a> 접속 (OpenJDK 무료 배포판)</li>
<li><strong>Temurin 17 (LTS)</strong> 선택 → Windows x64 Installer <code>.msi</code> 다운로드</li>
<li>설치 시 <strong>"Add to PATH"</strong> 옵션 반드시 체크</li>
</ol>
<p>설치 후 터미널에서 확인:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#B392F0">java</span><span style="color:#79B8FF"> -version</span></span></code></pre></figure>
<pre><code>openjdk version "17.0.x" 2024-xx-xx
</code></pre>
<p>이렇게 나오면 성공입니다.</p>
<blockquote>
<p><strong>JAVA_HOME 환경변수 설정 (안 되어있을 경우)</strong></p>
<p>시스템 환경 변수 → 새로 만들기</p>
<ul>
<li>변수 이름: <code>JAVA_HOME</code></li>
<li>변수 값: <code>C:\Program Files\Eclipse Adoptium\jdk-17.x.x.x-hotspot</code></li>
</ul>
<p>PATH에 <code>%JAVA_HOME%\bin</code> 추가</p>
</blockquote>
<hr>
<h2 id="2-nodejs-설치"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-nodejs-설치">#</a>2. Node.js 설치</h2>
<h3 id="설치-방법-1"><a class="anchor" aria-hidden="true" tabindex="-1" href="#설치-방법-1">#</a>설치 방법</h3>
<ol>
<li><a href="https://nodejs.org">nodejs.org</a> 접속</li>
<li><strong>LTS 버전</strong> (현재 22.x) 다운로드 및 설치</li>
<li>기본 옵션 그대로 Next → Next → Install</li>
</ol>
<p>확인:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#B392F0">node</span><span style="color:#79B8FF"> -v</span><span style="color:#6A737D">   # v22.x.x</span></span>
<span data-line=""><span style="color:#B392F0">npm</span><span style="color:#79B8FF"> -v</span><span style="color:#6A737D">    # 10.x.x</span></span></code></pre></figure>
<hr>
<h2 id="3-oracle-database-xe-설치"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-oracle-database-xe-설치">#</a>3. Oracle Database XE 설치</h2>
<h3 id="xe가-뭔가요"><a class="anchor" aria-hidden="true" tabindex="-1" href="#xe가-뭔가요">#</a>XE가 뭔가요?</h3>
<p>Oracle XE(Express Edition)는 Oracle DB 무료 버전입니다. CPU 2코어, 2GB RAM, 12GB 스토리지 제한이 있지만 개발·학습 용도로는 충분합니다.</p>
<h3 id="설치-방법-2"><a class="anchor" aria-hidden="true" tabindex="-1" href="#설치-방법-2">#</a>설치 방법</h3>
<ol>
<li><a href="https://www.oracle.com/database/technologies/xe-downloads.html">Oracle XE 다운로드</a> → Oracle 계정 필요 (무료 가입)</li>
<li>Windows x64 ZIP 다운로드 후 압축 해제</li>
<li><code>setup.exe</code> 실행</li>
<li>설치 중 <strong>SYS, SYSTEM 비밀번호 설정</strong> — 꼭 기억해두세요!</li>
</ol>
<p>설치 완료 후 기본 포트:</p>
<ul>
<li>Oracle Listener: <strong>1521</strong></li>
<li>Oracle EM Express: <strong>5500</strong></li>
</ul>
<h3 id="설치-확인"><a class="anchor" aria-hidden="true" tabindex="-1" href="#설치-확인">#</a>설치 확인</h3>
<p>시작 메뉴에서 <strong>SQL Plus</strong> 실행:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="sql" data-theme="github-dark"><code data-language="sql" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">sqlplus sys</span><span style="color:#F97583">/</span><span style="color:#E1E4E8">[비밀번호]@localhost:</span><span style="color:#79B8FF">1521</span><span style="color:#F97583">/</span><span style="color:#E1E4E8">XE </span><span style="color:#F97583">as</span><span style="color:#E1E4E8"> sysdba</span></span></code></pre></figure>
<p><code>SQL></code> 프롬프트가 뜨면 성공입니다.</p>
<h3 id="개발용-계정-생성"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개발용-계정-생성">#</a>개발용 계정 생성</h3>
<p>SYS로 접속 후 개발에서 쓸 전용 계정을 만듭니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="sql" data-theme="github-dark"><code data-language="sql" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">-- 사용자 생성</span></span>
<span data-line=""><span style="color:#F97583">CREATE</span><span style="color:#F97583"> USER</span><span style="color:#B392F0"> devuser</span><span style="color:#E1E4E8"> IDENTIFIED </span><span style="color:#F97583">BY</span><span style="color:#E1E4E8"> devpass123;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">-- 권한 부여</span></span>
<span data-line=""><span style="color:#F97583">GRANT</span><span style="color:#F97583"> CONNECT</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">RESOURCE</span><span style="color:#E1E4E8">, DBA </span><span style="color:#F97583">TO</span><span style="color:#E1E4E8"> devuser;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">-- 커넥션 가능한 테이블스페이스 허용</span></span>
<span data-line=""><span style="color:#F97583">ALTER</span><span style="color:#F97583"> USER</span><span style="color:#E1E4E8"> devuser QUOTA </span><span style="color:#F97583">UNLIMITED</span><span style="color:#F97583"> ON</span><span style="color:#E1E4E8"> USERS;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">EXIT;</span></span></code></pre></figure>
<blockquote>
<p><code>devuser</code> / <code>devpass123</code> 부분을 원하는 아이디/비밀번호로 바꾸세요.</p>
</blockquote>
<hr>
<h2 id="4-dbeaver-설치-db-gui-도구"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-dbeaver-설치-db-gui-도구">#</a>4. DBeaver 설치 (DB GUI 도구)</h2>
<p>SQL Plus는 터미널 환경이라 불편합니다. <strong>DBeaver</strong>를 쓰면 GUI로 편하게 DB를 관리할 수 있습니다.</p>
<ol>
<li><a href="https://dbeaver.io/download/">dbeaver.io</a> → Community Edition 다운로드</li>
<li>설치 후 실행</li>
<li>새 연결 → Oracle 선택</li>
</ol>
<p>연결 정보:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody>
<tr>
<td>Host</td>
<td>localhost</td>
</tr>
<tr>
<td>Port</td>
<td>1521</td>
</tr>
<tr>
<td>Database</td>
<td>XE</td>
</tr>
<tr>
<td>Username</td>
<td>devuser</td>
</tr>
<tr>
<td>Password</td>
<td>devpass123</td>
</tr>
</tbody>
</table>
<p><strong>Test Connection</strong> 클릭해서 <code>Connected</code> 뜨면 성공.</p>
<blockquote>
<p>처음 Oracle 드라이버 설치 요청이 뜨면 <strong>Download</strong> 버튼 클릭하면 자동으로 설치됩니다.</p>
</blockquote>
<hr>
<h2 id="5-intellij-idea-설치"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-intellij-idea-설치">#</a>5. IntelliJ IDEA 설치</h2>
<p>Spring Boot 개발엔 IntelliJ가 압도적으로 편합니다.</p>
<ol>
<li><a href="https://www.jetbrains.com/idea/download/">jetbrains.com/idea</a> → <strong>Community Edition</strong> (무료) 다운로드</li>
<li>설치 시 <strong>"Add launchers dir to the PATH"</strong> 체크</li>
</ol>
<blockquote>
<p>Community Edition으로도 Spring Boot 개발 가능합니다. Ultimate는 유료인데 DB 툴, HTTP Client 등이 추가되어 있습니다. 처음엔 Community로 충분.</p>
</blockquote>
<hr>
<h2 id="6-vs-code-설치"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-vs-code-설치">#</a>6. VS Code 설치</h2>
<p>React 개발엔 VS Code를 씁니다.</p>
<ol>
<li><a href="https://code.visualstudio.com">code.visualstudio.com</a> 다운로드 설치</li>
<li>확장 플러그인 설치 (Extensions 탭에서 검색):
<ul>
<li><strong>ES7+ React/Redux/React-Native snippets</strong></li>
<li><strong>Prettier - Code formatter</strong></li>
<li><strong>ESLint</strong></li>
</ul>
</li>
</ol>
<hr>
<h2 id="전체-구조-미리보기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전체-구조-미리보기">#</a>전체 구조 미리보기</h2>
<p>이 시리즈를 다 따라하면 이런 구조가 완성됩니다:</p>
<pre><code>프로젝트/
├── backend/          ← Spring Boot (포트 8080)
│   ├── src/
│   │   ├── controller/   ← API 엔드포인트
│   │   ├── service/      ← 비즈니스 로직
│   │   ├── mapper/       ← MyBatis 인터페이스
│   │   └── resources/
│   │       └── mapper/   ← SQL XML 파일
│   └── build.gradle
│
└── frontend/         ← React (포트 3000)
    ├── src/
    │   ├── pages/
    │   ├── components/
    │   └── api/          ← Spring Boot 호출
    └── package.json
</code></pre>
<p><strong>React(3000) → Spring Boot(8080) → Oracle(1521)</strong> 이 흐름으로 데이터가 오갑니다.</p>
<hr>
<h2 id="설치-완료-체크리스트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#설치-완료-체크리스트">#</a>설치 완료 체크리스트</h2>
<pre><code>☐ java -version  → openjdk 17.x.x 확인
☐ node -v        → v22.x.x 확인
☐ npm -v         → 10.x.x 확인
☐ Oracle XE 설치 &#x26; devuser 계정 생성
☐ DBeaver에서 Oracle 연결 성공
☐ IntelliJ IDEA 설치 완료
☐ VS Code 설치 완료
</code></pre>
<p>모두 체크됐나요? 그러면 다음 편으로 넘어갈 준비가 됐습니다.</p>
<hr>
<h2 id="다음-편-예고"><a class="anchor" aria-hidden="true" tabindex="-1" href="#다음-편-예고">#</a>다음 편 예고</h2>
<p><strong>2편: Spring Boot 프로젝트 생성 &#x26; Oracle 연결</strong></p>
<p>Spring Initializr로 프로젝트를 만들고, <code>application.yml</code>에 Oracle 접속 정보를 설정하고, 실제로 DB에 붙는 것까지 해봅니다.</p>
<p>에러 없이 연결되는 그 순간이 생각보다 짜릿합니다 😄</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[SpringBoot]]></category>
      <category><![CDATA[React]]></category>
      <category><![CDATA[Oracle]]></category>
      <category><![CDATA[MyBatis]]></category>
      <category><![CDATA[풀스택]]></category>
      <category><![CDATA[초보자]]></category>
    </item>

    <item>
      <title><![CDATA[크로노 트리거 회고 — 고딩 때 클리어한 JRPG가 30년 뒤에도 역대 1위인 이유]]></title>
      <link>https://www.stragos.xyz/posts/chrono-trigger-review</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/chrono-trigger-review</guid>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[고등학교 때 클리어했던 크로노 트리거를 다시 돌아봤다. 왜 1995년작이 아직도 역대 최고 JRPG로 꼽히는지, 직접 겪은 감상과 공략 포인트까지 정리했다.]]></description>
      <content:encoded><![CDATA[<p>크로노 트리거(Chrono Trigger)는 고등학교 때 클리어했다.</p>
<p>당시에는 막연히 "재밌는 게임"으로만 기억했는데, 시간이 지나고 다시 돌아보니 이 게임이 나에게 남긴 게 꽤 컸다는 걸 알게 됐다. 지금 나온 게임이라고 해도 수준급인 시스템과 스토리가 1995년에 이미 완성돼 있었다.</p>
<p>그래서 이 글을 쓴다. 고전게임 회고이자, 아직 안 해본 사람을 위한 안내서다.</p>
<hr>
<h2 id="이-게임을-만든-사람들"><a class="anchor" aria-hidden="true" tabindex="-1" href="#이-게임을-만든-사람들">#</a>이 게임을 만든 사람들</h2>
<p>크로노 트리거를 특별하게 만드는 첫 번째 이유는 개발진 자체다.</p>
<ul>
<li><strong>사카구치 히로노부</strong> — 파이널 판타지 시리즈 창시자</li>
<li><strong>호리이 유지</strong> — 드래곤 퀘스트 시리즈 창시자</li>
<li><strong>토리야마 아키라</strong> — 드래곤볼 원작자</li>
</ul>
<p>세 명이 동시에 붙었다. 당시 일본 게임계에서 이 조합은 있을 수 없는 드림팀이었다. 음악은 야스노리 미츠다가 맡았고, 노부오 우에마츠가 일부를 도왔다. 이 조합이 나온 것 자체가 기적에 가깝고, 결과물도 그 기대를 배신하지 않았다.</p>
<hr>
<h2 id="스토리--소박하게-시작해서-거대하게-끝난다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#스토리--소박하게-시작해서-거대하게-끝난다">#</a>스토리 — 소박하게 시작해서 거대하게 끝난다</h2>
<p><img src="/images/review/ct1.jpg" alt="크로노 트리거 — 이야기의 시작" loading="lazy" decoding="async"></p>
<p>주인공 크로노는 왕국 축제에서 마를이라는 소녀를 만나고, 친구 루카의 발명품 사고로 시간의 문에 빠진다. 시작은 소박하다.</p>
<p>그런데 게임을 진행하면서 서서히 드러나는 진짜 이야기의 규모가 다르다. 개인의 모험이었던 것이 점점 인류의 존속 문제로 확장된다. 중반부에 예상치 못한 충격적인 사건이 터지는데, 고등학교 때 처음 봤을 때 그 장면에서 진짜 입이 벌어졌다.</p>
<p>도트 캐릭터인데도 감정이 전달된다는 게 놀라웠다. 연출력이라는 게 해상도와 무관하다는 걸 이 게임이 증명한다.</p>
<hr>
<h2 id="시간여행--세계관의-핵심"><a class="anchor" aria-hidden="true" tabindex="-1" href="#시간여행--세계관의-핵심">#</a>시간여행 — 세계관의 핵심</h2>
<p><img src="/images/review/ct2.jpg" alt="시간의 문을 통해 다른 시대로" loading="lazy" decoding="async"></p>
<p>크로노 일행은 총 6개의 시대를 오간다.</p>
<ul>
<li><strong>65,000,000 BC</strong> — 공룡이 지배하는 선사시대. 원시인 에이라를 만나는 곳</li>
<li><strong>12,000 BC</strong> — 하늘 위에 떠 있는 마법왕국 자러. 시각적으로 가장 인상적인 시대</li>
<li><strong>600 AD</strong> — 중세왕국. 개구리 기사 글렌의 이야기가 펼쳐지는 곳</li>
<li><strong>1000 AD</strong> — 크로노의 현재. 이야기의 출발점</li>
<li><strong>1999 AD</strong> — 모든 사건의 원인이 된 날</li>
<li><strong>2300 AD</strong> — 황폐한 미래. 인류 문명의 잔해가 남은 세상</li>
</ul>
<p>단순히 배경이 바뀌는 게 아니다. 600년대에 한 일이 1000년대에 영향을 미치고, 2300년의 미래를 바꾸기 위해 과거로 돌아간다. 이 인과관계가 게임 전체를 관통하는 쾌감이다.</p>
<hr>
<h2 id="전투-시스템--당시로서는-혁신이었던-것들"><a class="anchor" aria-hidden="true" tabindex="-1" href="#전투-시스템--당시로서는-혁신이었던-것들">#</a>전투 시스템 — 당시로서는 혁신이었던 것들</h2>
<p><img src="/images/review/ct4.jpg" alt="전투 장면 — ATB와 듀얼테크" loading="lazy" decoding="async"></p>
<h3 id="랜덤-인카운터가-없다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#랜덤-인카운터가-없다">#</a>랜덤 인카운터가 없다</h3>
<p>1995년 JRPG의 표준은 걷다가 갑자기 전투 화면으로 튀어들어가는 랜덤 인카운터였다. 크로노 트리거는 그게 없다. 맵에서 적이 눈에 보이고, 피할 수도 있고, 달려들어 선제 공격할 수도 있다. 지금은 당연하게 느껴지지만 당시엔 완전히 새로운 방식이었다.</p>
<h3 id="듀얼-테크--트리플-테크"><a class="anchor" aria-hidden="true" tabindex="-1" href="#듀얼-테크--트리플-테크">#</a>듀얼 테크 / 트리플 테크</h3>
<p>파티원 두 명 또는 세 명이 기술을 합쳐 강력한 합체기를 사용할 수 있다. 크로노의 번개와 마를의 회복이 합쳐지면 새로운 기술이 나오는 식이다. 파티 구성에 따라 사용할 수 있는 합체기가 완전히 달라지기 때문에, "저 조합은 무슨 기술이 나오지?" 하는 호기심이 생긴다.</p>
<h3 id="위치가-중요하다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#위치가-중요하다">#</a>위치가 중요하다</h3>
<p>기술에 따라 공격 범위가 다르다. 일직선으로 쓸어담는 기술, 원형 범위 기술, 단일 대상 기술이 구분돼 있고, 적의 배치를 보고 어떤 기술을 쓸지 선택하는 재미가 있다.</p>
<hr>
<h2 id="캐릭터들--도트인데-기억에-남는다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#캐릭터들--도트인데-기억에-남는다">#</a>캐릭터들 — 도트인데 기억에 남는다</h2>
<p><img src="/images/review/ct3.jpg" alt="크로노 일행" loading="lazy" decoding="async"></p>
<p>클리어하고 나서도 기억에 남는 캐릭터가 여럿이다.</p>
<p><strong>글렌 (개구리)</strong> — 이 게임 최고의 캐릭터다. 마법으로 개구리 모습이 되어버린 기사로, 중세 문어체를 쓰고 자신의 과거와 죄책감을 안고 살아간다. 그 스토리가 생각보다 훨씬 묵직하다. 고딩 때 이 캐릭터가 왜 그렇게 멋있어 보였는지 지금도 이해된다.</p>
<p><strong>마왕</strong> — 처음엔 최대의 적으로 등장하는데, 게임 후반부에 파티에 합류시킬 수 있다. 합류 여부가 선택이라는 점이 재미있고, 그에 대한 해석이 플레이어마다 완전히 달라진다.</p>
<p><strong>에이라</strong> — 65,000,000 BC의 원시인 족장. 말투는 투박하지만 행동으로 보여주는 타입이다. 파티에서 가장 든든하기도 하다.</p>
<hr>
<h2 id="음악--귀에서-안-떠난다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#음악--귀에서-안-떠난다">#</a>음악 — 귀에서 안 떠난다</h2>
<p>야스노리 미츠다가 작곡한 크로노 트리거 OST는 JRPG 역사상 손꼽히는 명반이다.</p>
<p><strong>Frog's Theme</strong> — 개구리가 등장할 때마다 흐르는 비장하고 웅장한 테마. 이 곡 하나로 캐릭터가 살아났다.</p>
<p><strong>Corridors of Time</strong> — 12,000 BC 마법왕국에서 흐르는 곡. 신비롭고 몽환적인데, 고등학교 때 이 구간에서 한참 멈췄던 기억이 난다.</p>
<p><strong>크로노 트리거 메인 테마</strong> — 오프닝부터 나오는 멜로디. 게임 내내 변주되며 흐르다가 클라이맥스에 터질 때의 감동은 직접 경험해봐야 안다.</p>
<hr>
<h2 id="공략-포인트--처음-하는-사람을-위해"><a class="anchor" aria-hidden="true" tabindex="-1" href="#공략-포인트--처음-하는-사람을-위해">#</a>공략 포인트 — 처음 하는 사람을 위해</h2>
<p><img src="/images/review/ct5.jpg" alt="보스 전투" loading="lazy" decoding="async"></p>
<p>게임 자체는 어렵지 않지만, 놓치기 쉬운 포인트들이 있다.</p>
<h3 id="파티-구성--합체기-중심으로"><a class="anchor" aria-hidden="true" tabindex="-1" href="#파티-구성--합체기-중심으로">#</a>파티 구성 — 합체기 중심으로</h3>
<p>크로노(빛) + 마를(회복) + 루카(불) 조합이 초중반 가장 무난하다. 크로노 단독 합체기 <strong>루미나이트</strong>는 전체 번개 공격으로 사실상 필수급이고, 크로노+마를 합체기 <strong>오라 소용돌이</strong>(Aura Whirl)는 전체 회복으로 위기 상황을 역전시켜준다.</p>
<p>후반부엔 <strong>크로노+글렌+루카</strong>의 <strong>트리플 레이드</strong> 조합이 화력 면에서 최강급이다. 글렌을 파티에 넣는 순간 느끼는 가속감이 있다.</p>
<h3 id="절대-놓치면-안-되는-이벤트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#절대-놓치면-안-되는-이벤트">#</a>절대 놓치면 안 되는 이벤트</h3>
<ul>
<li><strong>600 AD 마을 복구</strong> — 왕국 전쟁이 끝난 뒤 지나가면 완전히 다른 풍경을 볼 수 있다. 그냥 지나치기 쉬운데 보면 감동적이다.</li>
<li><strong>루카의 과거 이벤트</strong> — 특정 시점에 선택지가 나오는데 제대로 진행하면 루카의 스토리에서 가장 감동적인 장면이 연출된다.</li>
<li><strong>글렌의 검 이벤트</strong> — 중반부 마사무네 수리 퀘스트. 글렌의 스토리 핵심이자 이 게임에서 가장 기억에 남는 연출 중 하나다.</li>
</ul>
<h3 id="마왕-합류-vs-처치--선택의-무게"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마왕-합류-vs-처치--선택의-무게">#</a>마왕 합류 vs 처치 — 선택의 무게</h3>
<p>후반부에 마왕을 파티에 합류시킬 수 있는 선택지가 나온다. 처치하면 게임은 진행되지만 합류시키면 전혀 다른 스토리 관점이 열린다. 처음이라면 합류를 추천한다. 두 번 플레이할 때 반대 선택을 해보는 재미도 있다.</p>
<h3 id="보스-공략-팁"><a class="anchor" aria-hidden="true" tabindex="-1" href="#보스-공략-팁">#</a>보스 공략 팁</h3>
<ul>
<li><strong>마그나스(마왕 1차)</strong> — 약점은 빛 계열. 크로노의 루미나 계열 기술이 특히 잘 통한다.</li>
<li><strong>라보스(최종보스)</strong> — 본체보다 양쪽 발이 더 위협적이다. 발부터 처리하지 않으면 계속 회복해서 장기전이 된다. 트리플 테크를 아끼지 말 것.</li>
<li><strong>엘리나이트</strong> — 감전 상태이상에 매우 취약하다. 일부러 상태이상을 걸면 순식간에 끝난다.</li>
</ul>
<h3 id="뉴게임-활용법"><a class="anchor" aria-hidden="true" tabindex="-1" href="#뉴게임-활용법">#</a>뉴게임+ 활용법</h3>
<p>클리어 후 능력치를 유지한 상태로 처음부터 다시 시작할 수 있다. 뉴게임+ 상태에서는 게임 시작 직후 바로 최종 보스에게 도전하는 것도 가능하다. 나머지 엔딩 12개를 수집하는 데 뉴게임+가 필수적으로 필요하다.</p>
<hr>
<h2 id="멀티-엔딩--13가지-결말"><a class="anchor" aria-hidden="true" tabindex="-1" href="#멀티-엔딩--13가지-결말">#</a>멀티 엔딩 — 13가지 결말</h2>
<p><img src="/images/review/ct7.jpg" alt="엔딩 장면" loading="lazy" decoding="async"></p>
<p>크로노 트리거에는 13가지 엔딩이 있다. 언제, 어떤 조건으로 최종 보스에게 도전하느냐에 따라 결말이 바뀐다.</p>
<p>모든 엔딩 중 가장 기억에 남는 건 노말 엔딩이다. 모든 걸 다 해결하고 나서의 결말은 담담한데, 그래서 더 감동적이다. 고등학교 때 처음 이 엔딩을 봤을 때의 여운이 아직도 남아 있다.</p>
<hr>
<h2 id="총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#총평">#</a>총평</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>평가</th>
</tr>
</thead>
<tbody>
<tr>
<td>스토리</td>
<td>★★★★★ — 시간여행 서사의 교과서</td>
</tr>
<tr>
<td>게임플레이</td>
<td>★★★★★ — 30년 후에도 전혀 낡지 않음</td>
</tr>
<tr>
<td>음악</td>
<td>★★★★★ — 레전드</td>
</tr>
<tr>
<td>그래픽</td>
<td>★★★★☆ — 도트지만 아름답다</td>
</tr>
<tr>
<td>볼륨</td>
<td>★★★★☆ — 노말 20h, 뉴게임+ 포함 40h+</td>
</tr>
</tbody>
</table>
<p>플레이타임은 노말 엔딩 기준 약 20시간이다. JRPG치고 긴 게임이 아닌데, 오히려 짧게 느껴진다. 난이도는 낮은 편이라 전략보다 스토리에 집중하게 설계돼 있다. JRPG 입문에 가장 적합한 게임이기도 하다.</p>
<hr>
<h2 id="어디서-할-수-있나"><a class="anchor" aria-hidden="true" tabindex="-1" href="#어디서-할-수-있나">#</a>어디서 할 수 있나</h2>
<p><strong>Steam</strong> (PC) — 공식 한국어 지원. 가장 편하게 접근 가능한 버전이다.</p>
<p><strong>iOS / Android</strong> — 모바일 공식 한국어 버전. 터치 인터페이스라 약간 어색할 수 있다.</p>
<p><strong>SNES 에뮬레이터</strong> — 한글 팬 패치 버전. 원작 감성 그대로지만 직접 세팅이 필요하다.</p>
<p>Steam 버전이 원작 도트 그래픽과 고해상도 필터 중 선택 가능하고, 한국어도 공식 지원하니 지금 시작한다면 Steam을 추천한다.</p>
<hr>
<p>30년 된 게임이 왜 지금도 역대 1위로 꼽히는지, 고등학교 때 직접 해보고 나서야 이해했다. 지금도 충분히 재밌고, 오히려 요즘 게임들 사이에서 더 선명하게 빛난다. 아직 안 해봤다면 한번 해봐라. 후회하지 않는다.</p>]]></content:encoded>
      <category><![CDATA[리뷰]]></category>
      <category><![CDATA[크로노트리거]]></category>
      <category><![CDATA[JRPG]]></category>
      <category><![CDATA[슈퍼패미컴]]></category>
      <category><![CDATA[SNES]]></category>
      <category><![CDATA[레트로게임]]></category>
      <category><![CDATA[RPG]]></category>
      <category><![CDATA[게임리뷰]]></category>
      <category><![CDATA[스퀘어]]></category>
    </item>

    <item>
      <title><![CDATA[코인 사서 내 지갑으로 보내기 — 네트워크 실수 없이 완벽 정리]]></title>
      <link>https://www.stragos.xyz/posts/how-to-send-crypto-to-metamask</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/how-to-send-crypto-to-metamask</guid>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[거래소에서 구매한 ETH, USDT, POL을 MetaMask로 출금하는 법. 네트워크 불일치로 코인을 잃는 실수를 막는 방법까지 초보자 눈높이에 맞춰 설명합니다.]]></description>
      <content:encoded><![CDATA[<p>거래소에서 코인을 샀다. 그런데 그 코인이 진짜 "내 것"이 되려면 한 단계가 더 남아 있다.</p>
<p>거래소는 사실 은행과 비슷하다. 내 계좌에 돈이 있는 것처럼 보이지만, 진짜 돈은 은행 금고 안에 있다. 거래소에 있는 코인도 마찬가지다. 거래소가 보관하고 있고, 거래소가 문을 닫거나 해킹당하면 위험해진다.</p>
<p>내 MetaMask 지갑으로 옮기면 그때부터 아무도 건드릴 수 없는 <strong>진짜 내 코인</strong>이 된다.</p>
<p>이 글에서는 거래소(업비트 기준)에서 ETH를 사서 MetaMask로 보내는 전 과정을 다룬다. USDT, POL처럼 다른 코인을 보낼 때의 차이점도 함께 정리했다.</p>
<hr>
<h2 id="준비물"><a class="anchor" aria-hidden="true" tabindex="-1" href="#준비물">#</a>준비물</h2>
<ul>
<li>업비트 또는 빗썸 계정 (KYC 인증 완료)</li>
<li>MetaMask 지갑 (없다면 <a href="/posts/metamask-wallet-setup-guide">MetaMask 지갑 만들기 가이드</a> 참고)</li>
<li>출금할 코인 (이 글에서는 ETH 기준)</li>
</ul>
<hr>
<h2 id="1단계--거래소에서-코인-구매"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1단계--거래소에서-코인-구매">#</a>1단계 — 거래소에서 코인 구매</h2>
<p>업비트 앱 또는 웹에서 원하는 코인을 구매한다.</p>
<p><img src="/images/crypto/send-step1-buy.svg" alt="거래소에서 코인 구매" loading="lazy" decoding="async"></p>
<p>처음이라면 <strong>시장가 매수</strong>가 편하다. 현재 시세로 즉시 체결된다. 지정가는 내가 원하는 가격을 입력하고 기다리는 방식인데, 가격이 안 맞으면 체결이 안 되기도 한다.</p>
<p><strong>얼마부터 시작할까?</strong> 처음 출금을 연습하는 거라면 1~3만원 정도 소액으로 해보는 걸 추천한다. 출금 과정에 실수가 있더라도 손해가 크지 않다. 과정이 익숙해지면 금액을 늘리면 된다.</p>
<blockquote>
<p><strong>주의:</strong> 업비트는 첫 출금 시 24시간 지연 정책이 있다. KYC 인증과 출금 주소 등록을 미리 해두면 기다리는 시간을 줄일 수 있다.</p>
</blockquote>
<hr>
<h2 id="2단계--네트워크-개념-이해-가장-중요"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2단계--네트워크-개념-이해-가장-중요">#</a>2단계 — 네트워크 개념 이해 (가장 중요)</h2>
<p>이 부분을 모르면 코인을 잃을 수 있다. 딱 이것만 기억하면 된다.</p>
<p><strong>같은 코인이라도 여러 네트워크로 보낼 수 있다. 그리고 받는 지갑도 같은 네트워크여야 한다.</strong></p>
<p><img src="/images/crypto/send-step2-network.svg" alt="네트워크 불일치 위험" loading="lazy" decoding="async"></p>
<p>은행으로 비유하면 이렇다. 같은 금액의 돈이라도 국내 계좌로 보내는지, 해외 계좌로 보내는지에 따라 경로가 다르다. 국내 계좌 번호에 해외 송금으로 보내면 돈이 엉뚱한 곳으로 가거나 사라진다.</p>
<h3 id="metamask가-지원하는-네트워크"><a class="anchor" aria-hidden="true" tabindex="-1" href="#metamask가-지원하는-네트워크">#</a>MetaMask가 지원하는 네트워크</h3>
<p>MetaMask는 <strong>이더리움 계열 네트워크</strong>만 지원한다. 이 점이 핵심이다.</p>
<table>
<thead>
<tr>
<th>네트워크</th>
<th>MetaMask 지원</th>
<th>비고</th>
</tr>
</thead>
<tbody>
<tr>
<td>ERC-20 (이더리움)</td>
<td>✅ 기본 지원</td>
<td>가장 일반적</td>
</tr>
<tr>
<td>Polygon</td>
<td>✅ 추가 후 지원</td>
<td>chainlist.org</td>
</tr>
<tr>
<td>BSC (바이낸스)</td>
<td>✅ 추가 후 지원</td>
<td>chainlist.org</td>
</tr>
<tr>
<td>Arbitrum / Base</td>
<td>✅ 추가 후 지원</td>
<td>chainlist.org</td>
</tr>
<tr>
<td>TRC-20 (트론)</td>
<td>❌ 지원 안 함</td>
<td>주소 형식 자체가 다름</td>
</tr>
<tr>
<td>BTC 네이티브</td>
<td>❌ 지원 안 함</td>
<td>비트코인 전용 지갑 필요</td>
</tr>
</tbody>
</table>
<h3 id="초보자가-가장-많이-하는-실수"><a class="anchor" aria-hidden="true" tabindex="-1" href="#초보자가-가장-많이-하는-실수">#</a>초보자가 가장 많이 하는 실수</h3>
<p><strong>"USDT를 TRC-20으로 출금했더니 MetaMask에 안 들어왔어요."</strong></p>
<p>이 경우 코인이 사라진 게 아니라 트론 네트워크 어딘가에 있는 것이다. 트론 지갑(예: TronLink)이 있다면 복구할 수 있지만, 없다면 매우 곤란해진다.</p>
<p><strong>MetaMask로 보낼 때는 반드시 ERC-20 또는 MetaMask에 추가된 네트워크를 선택해야 한다.</strong></p>
<hr>
<h2 id="3단계--출금-신청"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3단계--출금-신청">#</a>3단계 — 출금 신청</h2>
<p>MetaMask를 열어 지갑 주소를 복사한다. 주소 부분을 클릭하면 클립보드에 복사된다.</p>
<p>업비트에서 ETH를 선택하고 출금 화면으로 이동한다.</p>
<p><img src="/images/crypto/send-step3-withdraw.svg" alt="출금 신청 화면" loading="lazy" decoding="async"></p>
<p>출금 화면에서 두 가지를 반드시 확인한다.</p>
<h3 id="-출금-주소"><a class="anchor" aria-hidden="true" tabindex="-1" href="#-출금-주소">#</a>① 출금 주소</h3>
<p>MetaMask에서 복사한 주소를 붙여넣는다. <strong>앞 4자리와 뒤 4자리를 눈으로 꼭 확인</strong>한다. 클립보드를 바꿔치기하는 악성코드가 존재하기 때문이다.</p>
<p>예시: <code>0x71C7656E ... 401B5f6d</code> → 앞 <code>71C7</code>, 뒤 <code>6d</code> 확인</p>
<h3 id="-네트워크-선택"><a class="anchor" aria-hidden="true" tabindex="-1" href="#-네트워크-선택">#</a>② 네트워크 선택</h3>
<p>ETH를 MetaMask로 보낸다면 → <strong>ERC-20 선택</strong></p>
<p>Polygon 네트워크를 MetaMask에 추가했고 POL을 Polygon 네트워크로 받겠다면 → <strong>Polygon 선택</strong></p>
<p><strong>규칙: MetaMask에서 선택한 네트워크 = 거래소에서 선택하는 출금 네트워크</strong></p>
<hr>
<h2 id="4단계--도착-확인"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4단계--도착-확인">#</a>4단계 — 도착 확인</h2>
<p>출금 신청 후 보통 1~10분이면 MetaMask에 잔액이 반영된다. 네트워크가 혼잡할 때는 더 걸릴 수 있다.</p>
<p><img src="/images/crypto/send-step4-confirm.svg" alt="MetaMask 수신 확인" loading="lazy" decoding="async"></p>
<p>MetaMask를 열었을 때 잔액이 아직 바뀌지 않았다면 당황하지 말고 기다리면 된다. 거래소에서 <strong>TX ID</strong>(트랜잭션 해시)를 제공하는데, 이걸 <a href="https://etherscan.io">etherscan.io</a> 에 검색하면 현재 처리 상태를 확인할 수 있다.</p>
<ul>
<li><strong>Pending</strong> → 아직 처리 중</li>
<li><strong>Success</strong> → 완료, MetaMask에 곧 반영됨</li>
<li><strong>Failed</strong> → 실패 (가스비 부족 등), 거래소에 문의</li>
</ul>
<hr>
<h2 id="다른-코인을-보낼-때는"><a class="anchor" aria-hidden="true" tabindex="-1" href="#다른-코인을-보낼-때는">#</a>다른 코인을 보낼 때는?</h2>
<p>ETH 외에 다른 코인도 원리는 같다. 다만 코인마다 지원 네트워크가 다르다.</p>
<p><img src="/images/crypto/send-step5-other-coins.svg" alt="코인별 네트워크 정리" loading="lazy" decoding="async"></p>
<h3 id="usdt-테더"><a class="anchor" aria-hidden="true" tabindex="-1" href="#usdt-테더">#</a>USDT (테더)</h3>
<p>USDT는 여러 네트워크에서 돌아다닌다. MetaMask로 받을 때는 <strong>ERC-20만 선택</strong>하면 된다.</p>
<ul>
<li><strong>ERC-20</strong> ✅ → MetaMask 기본 지원. 이더리움 네트워크에서 받음</li>
<li><strong>TRC-20</strong> ❌ → MetaMask 지원 안 함. 트론 전용. MetaMask 주소로 절대 보내지 말 것</li>
<li><strong>BEP-20</strong> △ → MetaMask에 BSC 네트워크 추가 후 사용 가능</li>
<li><strong>Polygon</strong> △ → MetaMask에 Polygon 네트워크 추가 후 사용 가능</li>
</ul>
<blockquote>
<p><strong>초보자라면 USDT는 ERC-20으로만</strong> 받자. 수수료가 다른 네트워크보다 비싸지만, 가장 안전하고 MetaMask 기본 설정에서 바로 보인다.</p>
</blockquote>
<hr>
<h3 id="pol-폴리곤"><a class="anchor" aria-hidden="true" tabindex="-1" href="#pol-폴리곤">#</a>POL (폴리곤)</h3>
<p>폴리곤 토큰은 이더리움 네트워크(ERC-20)와 폴리곤 네트워크 두 곳에서 존재한다.</p>
<ul>
<li><strong>Polygon 네트워크</strong> △ → 수수료 저렴. MetaMask에 Polygon 추가 필요</li>
<li><strong>ERC-20</strong> ✅ → MetaMask 기본 지원. 수수료는 더 비쌈</li>
</ul>
<p>MetaMask에 Polygon 네트워크를 추가한 상태라면 Polygon 네트워크로 받는 게 수수료면에서 훨씬 이득이다.</p>
<hr>
<h3 id="btc-비트코인"><a class="anchor" aria-hidden="true" tabindex="-1" href="#btc-비트코인">#</a>BTC (비트코인)</h3>
<p>비트코인은 MetaMask로 직접 받을 수 없다. MetaMask는 이더리움 계열 지갑이고, 비트코인은 완전히 다른 네트워크를 사용한다.</p>
<p>비트코인을 보관하려면 <strong>Trust Wallet</strong>, <strong>Coinbase Wallet</strong>, <strong>Ledger</strong> 같은 멀티체인 지갑이 필요하다.</p>
<p>WBTC(Wrapped BTC)라는 이더리움 네트워크 위의 비트코인 대체 토큰은 MetaMask에서 사용할 수 있지만, 거래소에서 직접 WBTC를 출금하는 경우는 드물다.</p>
<hr>
<h2 id="실수-방지-체크리스트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#실수-방지-체크리스트">#</a>실수 방지 체크리스트</h2>
<p>출금 버튼 누르기 전에 다시 한번 확인하자.</p>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> MetaMask 주소를 복사했다 (0x로 시작하는 42자리)</li>
<li class="task-list-item"><input type="checkbox" disabled> 출금 주소의 앞 4자리, 뒤 4자리를 눈으로 확인했다</li>
<li class="task-list-item"><input type="checkbox" disabled> 출금 네트워크가 MetaMask에 설정된 네트워크와 같다</li>
<li class="task-list-item"><input type="checkbox" disabled> 소액 테스트 전송을 먼저 했다 (큰 금액이라면 꼭)</li>
<li class="task-list-item"><input type="checkbox" disabled> 출금 수수료를 확인했다</li>
</ul>
<hr>
<h2 id="소액-테스트-전송을-꼭-해라"><a class="anchor" aria-hidden="true" tabindex="-1" href="#소액-테스트-전송을-꼭-해라">#</a>소액 테스트 전송을 꼭 해라</h2>
<p>큰 금액을 처음 보낸다면, 먼저 소액을 보내 실제로 도착하는지 확인한 뒤 나머지를 보내는 것을 강하게 권장한다. 주소를 새로 등록할 때마다 이 방법을 쓰면 실수로 큰 손실을 막을 수 있다.</p>
<hr>
<h2 id="마치며"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마치며">#</a>마치며</h2>
<p>코인을 거래소에서 내 지갑으로 옮기는 과정은 처음엔 복잡해 보이지만, 핵심은 하나다.</p>
<p><strong>보내는 네트워크와 받는 네트워크를 반드시 일치시켜야 한다.</strong></p>
<p>이것만 지키면 크게 실수할 일이 없다. 처음엔 소액으로 연습하고, 익숙해지면 본격적으로 DeFi나 에어드랍 참여를 시작할 수 있다.</p>
<p>다음 글에서는 MetaMask에 ETH가 있는 상태에서 Base 네트워크로 브릿지하고, 에어드랍을 위한 온체인 활동을 시작하는 방법을 다룰 예정이다.</p>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[MetaMask]]></category>
      <category><![CDATA[출금]]></category>
      <category><![CDATA[이더리움]]></category>
      <category><![CDATA[네트워크]]></category>
      <category><![CDATA[업비트]]></category>
      <category><![CDATA[초보자]]></category>
      <category><![CDATA[USDT]]></category>
      <category><![CDATA[코인전송]]></category>
    </item>

    <item>
      <title><![CDATA[MetaMask 지갑 처음 만들기 완전 가이드 (2026)]]></title>
      <link>https://www.stragos.xyz/posts/metamask-wallet-setup-guide</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/metamask-wallet-setup-guide</guid>
      <pubDate>Sat, 11 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[크립토 입문자를 위한 메타마스크 설치부터 시드 구문 백업, 네트워크 추가까지 단계별 완전 가이드]]></description>
      <content:encoded><![CDATA[<p>크립토를 시작하려면 제일 먼저 필요한 게 <strong>지갑</strong>이다.</p>
<p>MetaMask는 현재 가장 널리 쓰이는 이더리움 계열 지갑이다. 크롬 확장 프로그램으로 설치하고, DeFi·NFT·에어드랍 등 대부분의 크립토 서비스와 연결할 수 있다.</p>
<p>이 글에서는 처음 설치부터 시드 구문 백업, 기본 네트워크 추가까지 실제 화면 흐름대로 정리한다.</p>
<hr>
<h2 id="1단계--크롬-확장-프로그램-설치"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1단계--크롬-확장-프로그램-설치">#</a>1단계 — 크롬 확장 프로그램 설치</h2>
<p>MetaMask는 <strong>공식 사이트(metamask.io)에서만</strong> 설치해야 한다. 검색 광고에 가짜 MetaMask가 올라오는 경우가 있어서 광고 링크 클릭은 피해야 한다.</p>
<p><img src="/images/crypto/step1-install.svg" alt="MetaMask 크롬 확장 설치 화면" loading="lazy" decoding="async"></p>
<p>공식 사이트 접속 → "Download for Chrome" → Chrome 웹 스토어 → "Chrome에 추가" 순서로 진행한다.</p>
<p>설치 후에는 퍼즐 아이콘(확장 프로그램 메뉴)을 눌러 MetaMask를 <strong>핀 고정</strong>해두면 편하다.</p>
<blockquote>
<p><strong>확인 포인트:</strong> Chrome 웹 스토어 URL이 <code>chromewebstore.google.com/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn</code> 인지 확인. 확장 프로그램 ID <code>nkbihf...</code> 가 맞는지 체크하면 피싱 방지에 좋다.</p>
</blockquote>
<hr>
<h2 id="2단계--새-지갑-만들기-선택"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2단계--새-지갑-만들기-선택">#</a>2단계 — 새 지갑 만들기 선택</h2>
<p>설치 후 MetaMask 아이콘을 클릭하면 시작 화면이 뜬다.</p>
<p><img src="/images/crypto/step2-create-wallet.svg" alt="지갑 생성 선택 화면" loading="lazy" decoding="async"></p>
<p><strong>"새 지갑 만들기"</strong> 를 선택한다. 기존에 MetaMask 지갑이 있다면 "기존 지갑 가져오기"로 복구할 수 있다.</p>
<p>"새 지갑 만들기"를 누르면 MetaMask 사용 데이터 수집 동의 화면이 나온다. 동의 여부는 기능에 영향 없으니 원하는 대로 선택하면 된다.</p>
<hr>
<h2 id="3단계--시드-구문복구-구문-백업-"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3단계--시드-구문복구-구문-백업-">#</a>3단계 — 시드 구문(복구 구문) 백업 ⚠</h2>
<p>가장 중요한 단계다. <strong>시드 구문(Secret Recovery Phrase)</strong> 은 12개의 영단어로 구성된 지갑의 마스터 키다.</p>
<p><img src="/images/crypto/step3-seed-phrase.svg" alt="시드 구문 백업 화면" loading="lazy" decoding="async"></p>
<p>이 12단어가 있으면 지갑 전체를 복구할 수 있다. 반대로 이 단어가 유출되면 지갑의 모든 자산을 잃을 수 있다.</p>
<h3 id="반드시-지켜야-할-규칙"><a class="anchor" aria-hidden="true" tabindex="-1" href="#반드시-지켜야-할-규칙">#</a>반드시 지켜야 할 규칙</h3>
<table>
<thead>
<tr>
<th>❌ 절대 금지</th>
<th>✅ 올바른 방법</th>
</tr>
</thead>
<tbody>
<tr>
<td>사진 촬영</td>
<td>종이에 손으로 직접 적기</td>
</tr>
<tr>
<td>구글 드라이브, 클라우드 저장</td>
<td>2부 작성 후 각각 다른 곳에 보관</td>
</tr>
<tr>
<td>카카오톡, 이메일 전송</td>
<td>금고나 안전한 서랍에 보관</td>
</tr>
<tr>
<td>화면 캡처</td>
<td>아무에게도 공유하지 않기</td>
</tr>
<tr>
<td>텍스트 파일로 컴퓨터에 저장</td>
<td>—</td>
</tr>
</tbody>
</table>
<p>단어를 적을 때 <strong>순서가 중요</strong>하다. 1번부터 12번까지 순서가 틀리면 복구가 안 된다. 다 적었으면 다음 화면에서 무작위 순서로 단어를 배치해 순서를 맞추는 확인 과정이 있다.</p>
<blockquote>
<p><strong>비유하자면:</strong> 시드 구문은 은행 금고의 마스터 열쇠다. 비밀번호를 잊어도 이 열쇠가 있으면 열 수 있고, 이 열쇠를 남이 갖게 되면 내 지갑은 내 것이 아니게 된다.</p>
</blockquote>
<hr>
<h2 id="4단계--비밀번호-설정"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4단계--비밀번호-설정">#</a>4단계 — 비밀번호 설정</h2>
<p>시드 구문 확인이 끝나면 비밀번호를 설정한다.</p>
<p><img src="/images/crypto/step4-password.svg" alt="비밀번호 설정 화면" loading="lazy" decoding="async"></p>
<p>이 비밀번호는 <strong>이 기기에서 MetaMask 팝업을 잠금 해제</strong>할 때만 사용한다. 은행 앱 비밀번호처럼 일상적으로 쓰는 용도다.</p>
<ul>
<li>최소 8자 이상</li>
<li>대문자, 숫자, 특수문자 조합 권장</li>
<li>시드 구문과 동일하게 쓰면 안 됨</li>
<li>비밀번호를 잊어도 시드 구문으로 복구 가능 (비밀번호는 시드 구문보다 중요도가 낮음)</li>
</ul>
<p>이용 약관 체크박스에 동의 후 <strong>"지갑 만들기"</strong> 버튼을 누르면 완료다.</p>
<hr>
<h2 id="5단계--지갑-생성-확인-및-주소-복사"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5단계--지갑-생성-확인-및-주소-복사">#</a>5단계 — 지갑 생성 확인 및 주소 복사</h2>
<p>설정이 끝나면 MetaMask 메인 화면이 나타난다.</p>
<p><img src="/images/crypto/step5-mainnet.svg" alt="지갑 생성 완료 화면" loading="lazy" decoding="async"></p>
<p>상단에 <code>0x71C7...3E4B</code> 형식의 <strong>지갑 주소</strong>가 보인다. 이 주소가 내 크립토 지갑 주소다. 은행 계좌번호처럼 코인을 받을 때 사용한다.</p>
<p>주소를 복사하려면 표시된 주소 부분을 클릭하면 된다. <code>Receive</code> 버튼을 누르면 QR코드도 볼 수 있다.</p>
<p><strong>지갑 주소는 공개해도 된다.</strong> 누군가 내 주소를 안다고 해서 지갑을 탈취하거나 자산을 가져갈 수 없다. 받기만 가능하다.</p>
<hr>
<h2 id="6단계--네트워크-추가-선택사항"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6단계--네트워크-추가-선택사항">#</a>6단계 — 네트워크 추가 (선택사항)</h2>
<p>기본으로 이더리움 메인넷이 설정되어 있다. 에어드랍이나 DeFi를 위해 다른 네트워크를 추가할 수 있다.</p>
<p><img src="/images/crypto/step6-add-network.svg" alt="네트워크 추가 화면" loading="lazy" decoding="async"></p>
<p>상단 네트워크 선택 드롭다운 → "네트워크 추가"를 클릭하면 Polygon, Base, Arbitrum 등을 바로 추가할 수 있다.</p>
<p>커스텀 네트워크를 수동으로 추가하려면 <strong><a href="https://chainlist.org">chainlist.org</a></strong> 에서 원하는 네트워크를 검색해 "Add to MetaMask" 버튼을 누르면 RPC 정보가 자동으로 입력된다.</p>
<h3 id="네트워크-선택-기준"><a class="anchor" aria-hidden="true" tabindex="-1" href="#네트워크-선택-기준">#</a>네트워크 선택 기준</h3>
<ul>
<li><strong>Ethereum Mainnet</strong> — 시작은 여기서. 가장 안전하고 검증됨. 수수료(가스비)가 높음</li>
<li><strong>Base</strong> — Coinbase의 L2 체인. 수수료 낮고 에어드랍 기회 많음. 입문자에게 추천</li>
<li><strong>Polygon</strong> — 게임, NFT 많음. 수수료 매우 낮음</li>
<li><strong>Arbitrum</strong> — DeFi 프로토콜 많음. 이더리움 호환</li>
</ul>
<hr>
<h2 id="보안-체크리스트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#보안-체크리스트">#</a>보안 체크리스트</h2>
<p>지갑을 만들었다면 아래 항목을 확인하자.</p>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 시드 구문 12단어를 종이에 적어 안전한 곳에 보관했다</li>
<li class="task-list-item"><input type="checkbox" disabled> 시드 구문을 디지털 기기나 클라우드에 저장하지 않았다</li>
<li class="task-list-item"><input type="checkbox" disabled> 비밀번호를 어딘가 기록해뒀다 (시드 구문과 별도로)</li>
<li class="task-list-item"><input type="checkbox" disabled> MetaMask 확장이 크롬에 핀 고정되어 있다</li>
<li class="task-list-item"><input type="checkbox" disabled> 지갑 주소를 복사해 어딘가 메모해뒀다</li>
</ul>
<hr>
<h2 id="마치며"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마치며">#</a>마치며</h2>
<p>MetaMask 설정은 5분이면 끝나지만, <strong>시드 구문 관리를 어떻게 하느냐가 지갑의 수명을 결정</strong>한다.</p>
<p>하드웨어 지갑(Ledger, Trezor)을 구매하기 전까지는 MetaMask 소프트웨어 지갑으로 충분히 시작할 수 있다. 큰 금액을 보관하게 되면 그때 하드웨어 지갑으로 넘어가는 것을 추천한다.</p>
<p>다음 글에서는 MetaMask에 실제로 ETH를 입금하고, Base 네트워크에서 온체인 활동을 시작하는 방법을 다룰 예정이다.</p>]]></content:encoded>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[MetaMask]]></category>
      <category><![CDATA[지갑]]></category>
      <category><![CDATA[크립토]]></category>
      <category><![CDATA[이더리움]]></category>
      <category><![CDATA[초보자]]></category>
    </item>

    <item>
      <title><![CDATA[Java 신입 개발자가 꼭 알아야 할 실무 팁 10가지]]></title>
      <link>https://www.stragos.xyz/posts/java-tips-for-junior-developers</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/java-tips-for-junior-developers</guid>
      <pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[현장에서 자주 보이는 실수와 놓치기 쉬운 포인트를 중심으로, 신입 Java 개발자가 빠르게 실력을 끌어올릴 수 있는 핵심 팁 10가지를 정리했습니다.]]></description>
      <content:encoded><![CDATA[<p>입사 후 처음 몇 달은 코드를 짜는 것보다 <strong>"왜 이렇게 짜야 하는지"</strong> 를 모르는 채 따라가는 시간이 많습니다.</p>
<p>이 글은 그 시간을 조금이라도 줄이기 위해 씁니다.<br>
학교에서는 잘 안 가르쳐주지만, 현장에서는 기본으로 여기는 것들입니다.</p>
<hr>
<h2 id="1--와-equals-는-완전히-다르다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1--와-equals-는-완전히-다르다">#</a>1. <code>==</code> 와 <code>equals()</code> 는 완전히 다르다</h2>
<p>Java를 처음 배우면 가장 먼저 틀리는 부분입니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">String a </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> String</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"hello"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">String b </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> String</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"hello"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">System.out.</span><span style="color:#B392F0">println</span><span style="color:#E1E4E8">(a </span><span style="color:#F97583">==</span><span style="color:#E1E4E8"> b);       </span><span style="color:#6A737D">// false — 주소 비교</span></span>
<span data-line=""><span style="color:#E1E4E8">System.out.</span><span style="color:#B392F0">println</span><span style="color:#E1E4E8">(a.</span><span style="color:#B392F0">equals</span><span style="color:#E1E4E8">(b));  </span><span style="color:#6A737D">// true  — 값 비교</span></span></code></pre></figure>
<p><code>==</code> 는 메모리 주소를 비교합니다. 문자열 비교는 반드시 <code>equals()</code> 를 써야 합니다.<br>
단, 문자열 리터럴은 String Pool을 사용해서 <code>==</code> 가 <code>true</code> 로 나오기도 하지만, 이 동작에 의존하면 언젠가 반드시 버그가 납니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 나쁜 예</span></span>
<span data-line=""><span style="color:#F97583">if</span><span style="color:#E1E4E8"> (status </span><span style="color:#F97583">==</span><span style="color:#9ECBFF"> "ACTIVE"</span><span style="color:#E1E4E8">) { ... }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 좋은 예</span></span>
<span data-line=""><span style="color:#F97583">if</span><span style="color:#E1E4E8"> (</span><span style="color:#9ECBFF">"ACTIVE"</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">equals</span><span style="color:#E1E4E8">(status)) { ... }  </span><span style="color:#6A737D">// null-safe 하게 상수를 앞에</span></span></code></pre></figure>
<hr>
<h2 id="2-nullpointerexception-은-막을-수-있다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-nullpointerexception-은-막을-수-있다">#</a>2. NullPointerException 은 막을 수 있다</h2>
<p>NPE는 Java 개발자의 오랜 숙적입니다. 그런데 대부분은 <strong>방어 코드 한 줄</strong>로 막을 수 있습니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 위험한 코드</span></span>
<span data-line=""><span style="color:#E1E4E8">String name </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> user.</span><span style="color:#B392F0">getName</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">toUpperCase</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 방어적 코드</span></span>
<span data-line=""><span style="color:#E1E4E8">String name </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> (user </span><span style="color:#F97583">!=</span><span style="color:#79B8FF"> null</span><span style="color:#F97583"> &#x26;&#x26;</span><span style="color:#E1E4E8"> user.</span><span style="color:#B392F0">getName</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">!=</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">    ?</span><span style="color:#E1E4E8"> user.</span><span style="color:#B392F0">getName</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">toUpperCase</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#F97583">    :</span><span style="color:#9ECBFF"> "UNKNOWN"</span><span style="color:#E1E4E8">;</span></span></code></pre></figure>
<p>Java 8부터는 <code>Optional</code> 을 활용하면 더 깔끔합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">Optional&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">> name </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> Optional.</span><span style="color:#B392F0">ofNullable</span><span style="color:#E1E4E8">(user)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(User</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">getName)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(String</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">toUpperCase);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">String result </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> name.</span><span style="color:#B392F0">orElse</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"UNKNOWN"</span><span style="color:#E1E4E8">);</span></span></code></pre></figure>
<p>메서드에서 <code>null</code> 을 반환하는 습관도 버려야 합니다. 빈 컬렉션이나 <code>Optional</code> 로 반환하세요.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 나쁜 예</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> List</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">User</span><span style="color:#F97583">></span><span style="color:#B392F0"> getUsers</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (없으면) </span><span style="color:#F97583">return</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 좋은 예</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> List</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">User</span><span style="color:#F97583">></span><span style="color:#B392F0"> getUsers</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (없으면) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> Collections.</span><span style="color:#B392F0">emptyList</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<hr>
<h2 id="3-stringbuilder-를-써야-할-때를-알자"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-stringbuilder-를-써야-할-때를-알자">#</a>3. <code>StringBuilder</code> 를 써야 할 때를 알자</h2>
<p>문자열을 반복문 안에서 <code>+</code> 로 연결하면 성능이 급격히 떨어집니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 나쁜 예 — 반복마다 새 String 객체 생성</span></span>
<span data-line=""><span style="color:#E1E4E8">String result </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> ""</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#F97583">for</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">int</span><span style="color:#E1E4E8"> i </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">; i </span><span style="color:#F97583">&#x3C;</span><span style="color:#79B8FF"> 10000</span><span style="color:#E1E4E8">; i</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    result </span><span style="color:#F97583">+=</span><span style="color:#E1E4E8"> i;</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 좋은 예 — 내부 버퍼에 추가</span></span>
<span data-line=""><span style="color:#E1E4E8">StringBuilder sb </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> StringBuilder</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#F97583">for</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">int</span><span style="color:#E1E4E8"> i </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">; i </span><span style="color:#F97583">&#x3C;</span><span style="color:#79B8FF"> 10000</span><span style="color:#E1E4E8">; i</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    sb.</span><span style="color:#B392F0">append</span><span style="color:#E1E4E8">(i);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""><span style="color:#E1E4E8">String result </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> sb.</span><span style="color:#B392F0">toString</span><span style="color:#E1E4E8">();</span></span></code></pre></figure>
<p>단순한 <code>"Hello " + name</code> 한 줄은 컴파일러가 알아서 최적화해줍니다.<br>
문제는 <strong>반복문 안</strong>입니다. 습관적으로 구분하세요.</p>
<hr>
<h2 id="4-예외는-구체적으로-잡아라"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-예외는-구체적으로-잡아라">#</a>4. 예외는 구체적으로 잡아라</h2>
<p><code>Exception</code> 을 통으로 잡는 건 현장에서 가장 자주 보이는 나쁜 습관 중 하나입니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 나쁜 예 — 모든 예외를 삼켜버림</span></span>
<span data-line=""><span style="color:#F97583">try</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#B392F0">    process</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">} </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (Exception </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    e.</span><span style="color:#B392F0">printStackTrace</span><span style="color:#E1E4E8">(); </span><span style="color:#6A737D">// 로그도 제대로 안 남음</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 좋은 예 — 예외를 명확히 구분</span></span>
<span data-line=""><span style="color:#F97583">try</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#B392F0">    process</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">} </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (IOException </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    log.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"파일 처리 중 오류 발생: {}"</span><span style="color:#E1E4E8">, e.</span><span style="color:#B392F0">getMessage</span><span style="color:#E1E4E8">(), e);</span></span>
<span data-line=""><span style="color:#F97583">    throw</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> RuntimeException</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"파일 처리 실패"</span><span style="color:#E1E4E8">, e);</span></span>
<span data-line=""><span style="color:#E1E4E8">} </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (IllegalArgumentException </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    log.</span><span style="color:#B392F0">warn</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"잘못된 입력값: {}"</span><span style="color:#E1E4E8">, e.</span><span style="color:#B392F0">getMessage</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#F97583">    throw</span><span style="color:#E1E4E8"> e;</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>예외를 <code>catch</code> 하고 아무 처리도 안 하는 <strong>빈 catch 블록</strong>은 절대 금물입니다.<br>
버그가 생겨도 추적이 불가능해집니다.</p>
<hr>
<h2 id="5-final-을-적극적으로-활용하라"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-final-을-적극적으로-활용하라">#</a>5. <code>final</code> 을 적극적으로 활용하라</h2>
<p><code>final</code> 은 단순히 "변경 금지" 이상의 의미가 있습니다.<br>
코드를 읽는 사람에게 <strong>"이 값은 바뀌지 않는다"</strong> 는 의도를 전달합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 지역 변수에 final</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> process</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">final</span><span style="color:#E1E4E8"> String input) {</span></span>
<span data-line=""><span style="color:#F97583">    final</span><span style="color:#F97583"> int</span><span style="color:#E1E4E8"> length </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> input.</span><span style="color:#B392F0">length</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#6A737D">    // length = 10; // 컴파일 에러 — 실수를 미리 막아줌</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 상수 정의</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> OrderStatus</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> String PENDING  </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "PENDING"</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> String APPROVED </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "APPROVED"</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> String REJECTED </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "REJECTED"</span><span style="color:#E1E4E8">;</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>특히 멀티스레드 환경에서 <code>final</code> 필드는 가시성(visibility)을 보장해줘서 동시성 버그를 예방하는 효과도 있습니다.</p>
<hr>
<h2 id="6-컬렉션은-인터페이스-타입으로-선언하라"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-컬렉션은-인터페이스-타입으로-선언하라">#</a>6. 컬렉션은 인터페이스 타입으로 선언하라</h2>
<p>구현체 타입으로 변수를 선언하면 나중에 변경이 어려워집니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 나쁜 예 — 구현체에 종속</span></span>
<span data-line=""><span style="color:#E1E4E8">ArrayList&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">> list </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> ArrayList&#x3C;>();</span></span>
<span data-line=""><span style="color:#E1E4E8">HashMap&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">Integer</span><span style="color:#E1E4E8">> map </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> HashMap&#x3C;>();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 좋은 예 — 인터페이스 타입으로 선언</span></span>
<span data-line=""><span style="color:#E1E4E8">List&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">> list </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> ArrayList&#x3C;>();</span></span>
<span data-line=""><span style="color:#E1E4E8">Map&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">Integer</span><span style="color:#E1E4E8">> map </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> HashMap&#x3C;>();</span></span></code></pre></figure>
<p><code>ArrayList</code> 를 <code>LinkedList</code> 로 바꿔야 할 때, 인터페이스로 선언했다면 선언부 한 줄만 바꾸면 됩니다.<br>
구현체로 선언했다면 해당 타입을 사용하는 모든 곳을 수정해야 합니다.</p>
<hr>
<h2 id="7-stream-api-를-알면-코드가-읽기-쉬워진다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7-stream-api-를-알면-코드가-읽기-쉬워진다">#</a>7. Stream API 를 알면 코드가 읽기 쉬워진다</h2>
<p>Java 8 이후 현장에서는 Stream을 기본으로 씁니다. 익숙해지면 코드량이 줄고 의도가 명확해집니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">List&#x3C;</span><span style="color:#F97583">User</span><span style="color:#E1E4E8">> users </span><span style="color:#F97583">=</span><span style="color:#B392F0"> getUserList</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 전통적인 방식</span></span>
<span data-line=""><span style="color:#E1E4E8">List&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">> activeNames </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> ArrayList&#x3C;>();</span></span>
<span data-line=""><span style="color:#F97583">for</span><span style="color:#E1E4E8"> (User user </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> users) {</span></span>
<span data-line=""><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (user.</span><span style="color:#B392F0">isActive</span><span style="color:#E1E4E8">()) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        activeNames.</span><span style="color:#B392F0">add</span><span style="color:#E1E4E8">(user.</span><span style="color:#B392F0">getName</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">toUpperCase</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// Stream 방식</span></span>
<span data-line=""><span style="color:#E1E4E8">List&#x3C;</span><span style="color:#F97583">String</span><span style="color:#E1E4E8">> activeNames </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> users.</span><span style="color:#B392F0">stream</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">filter</span><span style="color:#E1E4E8">(User</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">isActive)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(User</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">getName)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(String</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">toUpperCase)</span></span>
<span data-line=""><span style="color:#E1E4E8">    .</span><span style="color:#B392F0">collect</span><span style="color:#E1E4E8">(Collectors.</span><span style="color:#B392F0">toList</span><span style="color:#E1E4E8">());</span></span></code></pre></figure>
<p>처음엔 낯설지만, 익숙해지면 훨씬 직관적으로 읽힙니다.<br>
<code>filter</code> → 거른다, <code>map</code> → 변환한다, <code>collect</code> → 모은다. 영어 그대로입니다.</p>
<hr>
<h2 id="8-로그는-습관이다--systemoutprintln-은-쓰지-마라"><a class="anchor" aria-hidden="true" tabindex="-1" href="#8-로그는-습관이다--systemoutprintln-은-쓰지-마라">#</a>8. 로그는 습관이다 — <code>System.out.println</code> 은 쓰지 마라</h2>
<p>개발할 때 <code>System.out.println</code> 으로 디버깅하는 건 이해합니다.<br>
하지만 현장 코드에 남기면 안 됩니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 절대 쓰면 안 됨 (운영 서버에서 콘솔 출력은 성능 저하)</span></span>
<span data-line=""><span style="color:#E1E4E8">System.out.</span><span style="color:#B392F0">println</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"user: "</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> user);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 올바른 방법 — SLF4J + Logback/Log4j</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.slf4j.Logger;</span></span>
<span data-line=""><span style="color:#F97583">import</span><span style="color:#E1E4E8"> org.slf4j.LoggerFactory;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> UserService</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> static</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> Logger log </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> LoggerFactory.</span><span style="color:#B392F0">getLogger</span><span style="color:#E1E4E8">(UserService.class);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> createUser</span><span style="color:#E1E4E8">(User </span><span style="color:#FFAB70">user</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        log.</span><span style="color:#B392F0">info</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"사용자 생성 시작: id={}"</span><span style="color:#E1E4E8">, user.</span><span style="color:#B392F0">getId</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#6A737D">        // ...</span></span>
<span data-line=""><span style="color:#E1E4E8">        log.</span><span style="color:#B392F0">debug</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"생성된 사용자 정보: {}"</span><span style="color:#E1E4E8">, user);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>로그 레벨을 구분하는 것도 중요합니다.</p>
<table>
<thead>
<tr>
<th>레벨</th>
<th>용도</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ERROR</code></td>
<td>즉시 대응이 필요한 장애</td>
</tr>
<tr>
<td><code>WARN</code></td>
<td>잠재적 문제, 주의 필요</td>
</tr>
<tr>
<td><code>INFO</code></td>
<td>정상 흐름의 주요 이벤트</td>
</tr>
<tr>
<td><code>DEBUG</code></td>
<td>개발/디버깅용 상세 정보</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="9-객체는-불변immutable으로-만들수록-좋다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#9-객체는-불변immutable으로-만들수록-좋다">#</a>9. 객체는 불변(Immutable)으로 만들수록 좋다</h2>
<p>객체가 생성된 후 상태가 바뀌지 않으면 버그가 줄어듭니다.<br>
특히 멀티스레드 환경에서 불변 객체는 동기화 없이 안전하게 공유됩니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 가변 객체 — 외부에서 마음대로 바꿀 수 있음</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> User</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String name;</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> setName</span><span style="color:#E1E4E8">(String </span><span style="color:#FFAB70">name</span><span style="color:#E1E4E8">) { </span><span style="color:#79B8FF">this</span><span style="color:#E1E4E8">.name </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> name; }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 불변 객체 — 생성 후 상태 변경 불가</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> final</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> User</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> String name;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> String email;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#B392F0"> User</span><span style="color:#E1E4E8">(String </span><span style="color:#FFAB70">name</span><span style="color:#E1E4E8">, String </span><span style="color:#FFAB70">email</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.name </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> name;</span></span>
<span data-line=""><span style="color:#79B8FF">        this</span><span style="color:#E1E4E8">.email </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> email;</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">getName</span><span style="color:#E1E4E8">()  { </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> name; }</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> String </span><span style="color:#B392F0">getEmail</span><span style="color:#E1E4E8">() { </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> email; }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>Lombok을 쓴다면 <code>@Value</code> 어노테이션 하나로 불변 클래스를 만들 수 있습니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Value</span><span style="color:#6A737D"> // 모든 필드 final, getter만 생성, 생성자 자동 생성</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> UserDto</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#E1E4E8">    String name;</span></span>
<span data-line=""><span style="color:#E1E4E8">    String email;</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<hr>
<h2 id="10-코드-리뷰를-두려워하지-마라"><a class="anchor" aria-hidden="true" tabindex="-1" href="#10-코드-리뷰를-두려워하지-마라">#</a>10. 코드 리뷰를 두려워하지 마라</h2>
<p>마지막은 기술이 아닙니다.</p>
<p>신입 때 코드 리뷰 피드백을 받으면 움츠러들기 쉽습니다.<br>
하지만 <strong>리뷰는 공격이 아니라 함께 코드를 개선하는 과정</strong>입니다.</p>
<p>피드백을 받았을 때 좋은 태도는 이렇습니다.</p>
<ul>
<li>"왜 이렇게 해야 하나요?" 라고 물어보는 것 — 이해 없이 수정만 하면 같은 실수를 반복합니다.</li>
<li>리뷰어가 지적한 내용을 메모해두는 것 — 패턴이 보이면 자신의 약점을 알 수 있습니다.</li>
<li>좋은 코드를 발견하면 "왜 좋은지" 분석하는 것 — 모방이 실력 향상의 지름길입니다.</li>
</ul>
<p>코드는 혼자 짜는 게 아닙니다. 팀이 읽고 유지보수하는 것입니다.<br>
<strong>"내가 6개월 후에 봐도 이해할 수 있는가?"</strong> 를 항상 질문해보세요.</p>
<hr>
<h2 id="마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마무리">#</a>마무리</h2>
<p>이 10가지는 모두 현장에서 반복적으로 보이는 내용입니다.<br>
한 번에 다 외우려 하지 말고, 코드를 짤 때마다 하나씩 떠올리는 것만으로도 충분합니다.</p>
<p>시간이 지나면 자연스럽게 몸에 밸 것입니다.</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[신입개발자]]></category>
      <category><![CDATA[백엔드]]></category>
      <category><![CDATA[실무팁]]></category>
      <category><![CDATA[클린코드]]></category>
    </item>

    <item>
      <title><![CDATA[파이널 판타지 3·4·5·6 — 중학생이 패미컴과 슈퍼패미컴으로 클리어한 이야기]]></title>
      <link>https://www.stragos.xyz/posts/final-fantasy-series-review</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/final-fantasy-series-review</guid>
      <pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[FF3은 패미컴으로, FF4·5·6은 슈퍼패미컴으로 중학교 시절 플레이했습니다. 공략집 없이 막힌 보스들, 밤새 돌린 던전, 그리고 지금도 지워지지 않는 장면들을 기록합니다.]]></description>
      <content:encoded><![CDATA[<h2 id="지금-돌아보면"><a class="anchor" aria-hidden="true" tabindex="-1" href="#지금-돌아보면">#</a>지금 돌아보면</h2>
<p>중학교 때였다.</p>
<p>동네 친구가 패미컴을 갖고 있었고, 거기 꽂혀 있던 게임팩 중 하나가 파이널 판타지 3이었다. 한글화는 당연히 없었다. 일본어를 모르는 채로, 그림과 소리와 감으로만 플레이했다.</p>
<p>이상한 건, 그게 더 재밌었다는 거다.</p>
<p>무슨 말인지 모르는 NPC의 대사를 해석하려고 혼자 상상하고, 맵 곳곳을 뒤지면서 "여기 뭔가 있겠지"를 반복했다. 지금처럼 공략 유튜브도 없고, 인터넷도 없던 시절이니까. 동네 친구한테 물어보거나, 운 좋으면 공략집을 빌려보는 게 전부였다.</p>
<p>FF3부터 FF6까지 네 작품을 중학교 시절에 모두 클리어했다. 그때의 기억을 지금 여기 꺼내 놓는다.</p>
<hr>
<h2 id="final-fantasy-iii--패미컴-1990"><a class="anchor" aria-hidden="true" tabindex="-1" href="#final-fantasy-iii--패미컴-1990">#</a>FINAL FANTASY III — 패미컴 (1990)</h2>
<h3 id="처음-켰을-때"><a class="anchor" aria-hidden="true" tabindex="-1" href="#처음-켰을-때">#</a>처음 켰을 때</h3>
<p>패미컴 특유의 게임팩. 블로우(후후 불기)를 두 번 하고 꽂으면 타이틀 화면이 뜬다. 그 음악이 지금도 머릿속에 남아 있다.</p>
<p>4명의 주인공이 동굴에서 깨어나는 오프닝. 이름을 지을 수 있다는 게 신기했다. 당시 중학생 특유의 감수성으로 이름을 다 별명으로 지었다가 나중에 후회했던 기억이 있다.</p>
<h3 id="잡job-시스템--처음-만나는-직업-선택의-자유"><a class="anchor" aria-hidden="true" tabindex="-1" href="#잡job-시스템--처음-만나는-직업-선택의-자유">#</a>잡(Job) 시스템 — 처음 만나는 직업 선택의 자유</h3>
<p>FF3의 핵심은 <strong>잡 시스템</strong>이다. 파이터, 몽크, 화이트메이지, 블랙메이지, 레드메이지... 각자 특성이 다른 직업들 사이에서 파티를 구성하는 게 재미의 핵심이다.</p>
<p>문제는 초반에 이게 뭔지 몰랐다는 거다.</p>
<p>나는 처음에 4명 다 파이터로 만들었다. "제일 세 보이니까." 그게 오판이었다. 물리공격만으로 초반을 버티다가 첫 번째 제대로 된 보스에서 막혔다. 회복수단이 없으니 마을로 돌아가는 루트를 계속 반복했다.</p>
<p>며칠 고민하다가 <strong>화이트메이지를 한 명 넣기 시작했다.</strong> 그게 게임을 바꿨다. 케어 한 방이 이렇게 소중한 거구나를 몸으로 배웠다.</p>
<p><img src="/images/ff-review/ff3-screenshot-1.png" alt="파이널판타지3 패미컴 플레이 화면" loading="lazy" decoding="async"></p>
<h3 id="사비나-사막과-물속-던전"><a class="anchor" aria-hidden="true" tabindex="-1" href="#사비나-사막과-물속-던전">#</a>사비나 사막과 물속 던전</h3>
<p>잡 시스템이 강제로 발동되는 구간이 있다. <strong>물속 던전</strong>이다.</p>
<p>특정 구간에서 파티가 미니 상태로 줄어들어야만 통과할 수 있고, 그 상태에서 싸워야 한다. 미니 상태에서는 무기를 제대로 쓰지 못한다. 공략집 없이 여기서 한참을 헤맸다. "왜 갑자기 캐릭터가 작아져?" 하면서.</p>
<p>결국 방법을 찾았다. <strong>블랙메이지를 메인으로 세우는 것.</strong> 마법은 미니 상태에서도 위력이 유지된다. 그 발견이 소소하게 뿌듯했다.</p>
<p>이후 등장하는 <strong>비행정 던전</strong> 계열도 난관이었다. 지도도 없고, 세이브 포인트도 없이 긴 던전을 탐색하는 구조다. 아직도 그 긴장감이 생각난다. 세이브 포인트를 못 찾고 전멸할까 봐 조마조마하며 플레이하던 그 느낌.</p>
<p><img src="/images/ff-review/ff3-screenshot-2.png" alt="파이널판타지3 패미컴 전투 화면" loading="lazy" decoding="async"></p>
<h3 id="도가와-우네--처음-받은-jrpg의-감동"><a class="anchor" aria-hidden="true" tabindex="-1" href="#도가와-우네--처음-받은-jrpg의-감동">#</a>도가와 우네 — 처음 받은 JRPG의 감동</h3>
<p>파이널 판타지 3에서 처음으로 JRPG 특유의 감동을 받았다.</p>
<p><strong>도가와 우네.</strong> 두 마법사가 파티의 길을 열어주기 위해 스스로 적으로 변해 싸워달라고 한다. 일본어를 몰라도 그 장면의 분위기는 느껴졌다. 캐릭터들의 도트 스프라이트가 떨리는 연출에, 중학생인 나도 뭔가 찡한 느낌을 받았다.</p>
<p>적으로 등장한 두 사람을 쓰러뜨리고 나서 얻는 마법들. "이분들이 목숨을 내놓은 거구나"가 게임 안에서 느껴졌다. 그게 인상적이었다.</p>
<h3 id="최종-던전과-어둠의-구름"><a class="anchor" aria-hidden="true" tabindex="-1" href="#최종-던전과-어둠의-구름">#</a>최종 던전과 어둠의 구름</h3>
<p>FF3의 최종 던전은 <strong>무자비하다.</strong></p>
<p>세이브 포인트 없이 5개 층을 내려가야 하고, 마지막 보스 앞에서 연속으로 보스를 상대해야 한다. 도중에 전멸하거나 게임을 끄면 처음부터다.</p>
<p>나는 이 구간에서 두 번을 실패했다.</p>
<p>첫 번째는 회복 아이템이 부족했다. 최종 보스 직전에 엘릭서를 다 썼다.<br>
두 번째는 레벨이 낮았다. 파이널 보스 <strong>어둠의 구름</strong>의 광선 공격이 파티를 2방에 갈아버렸다.</p>
<p>세 번째 도전에서 방법을 바꿨다.</p>
<ul>
<li><strong>세이지와 닌자</strong> 육성 — 게임 후반 해금 직업. 희귀 아이템 각 1개로만 해금 가능해서 신중하게 써야 한다</li>
<li>세이브 포인트에서 회복 아이템 최대로 채우기</li>
<li>진입 전 레벨 45 이상 확보</li>
</ul>
<p>어둠의 구름을 쓰러뜨렸을 때, 그 엔딩 크레딧의 음악이 흘러나왔다. 패미컴 스피커에서 나오는 소리였지만, 그 순간은 진짜 짜릿했다.</p>
<p><img src="/images/ff-review/ff3-screenshot-3.png" alt="파이널판타지3 패미컴 던전 화면" loading="lazy" decoding="async"></p>
<h3 id="ff3-총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ff3-총평">#</a>FF3 총평</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>평가</th>
</tr>
</thead>
<tbody>
<tr>
<td>스토리</td>
<td>★★★☆☆ — 캐릭터가 희박하지만 감동 포인트는 있음</td>
</tr>
<tr>
<td>시스템</td>
<td>★★★★☆ — 잡 시스템의 원형, 지금도 훌륭한 설계</td>
</tr>
<tr>
<td>난이도</td>
<td>★★★★★ — 최종 던전은 현재도 악명 높음</td>
</tr>
<tr>
<td>음악</td>
<td>★★★★☆ — 노부오 우에마츠 특유의 서정적 멜로디</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="final-fantasy-iv--슈퍼패미컴-1991"><a class="anchor" aria-hidden="true" tabindex="-1" href="#final-fantasy-iv--슈퍼패미컴-1991">#</a>FINAL FANTASY IV — 슈퍼패미컴 (1991)</h2>
<h3 id="슈퍼패미컴과의-첫-만남"><a class="anchor" aria-hidden="true" tabindex="-1" href="#슈퍼패미컴과의-첫-만남">#</a>슈퍼패미컴과의 첫 만남</h3>
<p>FF3을 클리어하고 나서 얼마 지나지 않아 슈퍼패미컴이 생겼다. 당시에 그 기계를 처음 봤을 때의 충격을 아직도 기억한다. 화면이 다르다. 색이 다르다.</p>
<p>FF4의 오프닝에서 <strong>세실</strong>이 이끄는 비행선단이 마을에 폭탄을 투하하는 장면이 나온다. 전작과는 달랐다. 주인공이 처음부터 악역의 편에 있다.</p>
<p>"이건 다르다" 싶었다.</p>
<h3 id="atb--시간이-흐르는-전투"><a class="anchor" aria-hidden="true" tabindex="-1" href="#atb--시간이-흐르는-전투">#</a>ATB — 시간이 흐르는 전투</h3>
<p><strong>Active Time Battle(ATB)</strong> 시스템이 처음 도입된 작품이다.</p>
<p>이전까지는 턴이 돌아올 때 명령을 입력하면 됐다. FF4부터는 <strong>게이지가 차야 행동할 수 있고, 적도 그 사이에 공격한다.</strong> 처음에는 익숙하지 않아서 당황했다. 명령 선택하다가 갑자기 적한테 맞고, 허둥지둥하다 파티가 전멸하는 경험을 초반에 몇 번 했다.</p>
<p>그런데 익숙해지니까 전투가 살아있는 느낌이었다. 전작의 정적인 턴제와는 확실히 다른 긴장감이 있었다.</p>
<p><img src="/images/ff-review/ff4-screenshot-1.png" alt="파이널판타지4 슈퍼패미컴 세실 전투 화면 (일본판)" loading="lazy" decoding="async"></p>
<h3 id="캐릭터가-죽는다--진짜로"><a class="anchor" aria-hidden="true" tabindex="-1" href="#캐릭터가-죽는다--진짜로">#</a>캐릭터가 죽는다 — 진짜로</h3>
<p>FF4에서 처음 경험한 것 중 하나가 <strong>파티 멤버가 스토리상으로 죽거나 이탈하는 것</strong>이다.</p>
<p><strong>팔롬과 포롬.</strong> 두 쌍둥이 마법사가 일행을 구하기 위해 스스로 돌로 변한다. 일본어를 몰라도 그 장면은 이해가 됐다. 슈퍼패미컴의 표현력으로 담아낸 그 연출이, 당시 중학생에게는 진짜 충격이었다.</p>
<p><strong>양.</strong> 카이나초에서 폭탄을 막기 위해 혼자 남는 장면. 이것도 마찬가지였다.</p>
<p><strong>텔라.</strong> 원수를 갚기 위해 메테오를 쓰고 쓰러지는 장면. 메테오 시전 전에 흘러나오는 음악과 대사가 맞물리는 그 연출을, 일본어 모르고 봤는데도 무슨 장면인지 알았다.</p>
<p>파티 멤버가 오고 가는 구조였는데, 이게 처음엔 불편했다. "아니 왜 또 바뀌어?" 하면서. 근데 나중에 생각해보면 그게 스토리를 풍성하게 만드는 요소였다.</p>
<h3 id="카인의-배신"><a class="anchor" aria-hidden="true" tabindex="-1" href="#카인의-배신">#</a>카인의 배신</h3>
<p><strong>카인.</strong> 세실의 절친이자 드라군 전사.</p>
<p>그가 중반부에 배신한다. 처음에 나는 "게임 버그인가?" 했다. 분명 같은 편이었는데 갑자기 적이 된다. 공략집도 없이 플레이하다 보니 이게 스토리의 일부인지조차 처음엔 몰랐다.</p>
<p>적으로 등장한 카인과 싸울 때, 그 배경음악이 달랐다. "이건 일반 적이 아니다"라는 걸 음악만으로도 알 수 있었다. FF 시리즈가 음악으로 분위기를 전달하는 방식을 FF4에서 처음 제대로 느꼈다.</p>
<h3 id="골베자--이길-수-없는-적"><a class="anchor" aria-hidden="true" tabindex="-1" href="#골베자--이길-수-없는-적">#</a>골베자 — 이길 수 없는 적</h3>
<p>텔라가 메테오를 쓰고 쓰러지는 장면 직전에 <strong>골베자</strong>와 싸우는 이벤트 전투가 있다. 이건 <strong>이길 수 없는 전투</strong>다.</p>
<p>당시 나는 몰랐다. 진짜로 이기려고 죽어라 아이템을 쓰면서 버텼다. 물론 이벤트 전투라서 어떻게 해도 패배한다. 그걸 한참 뒤에 알았다.</p>
<p>그때 쓴 엘릭서가 너무 아까웠다.</p>
<h3 id="달과-지하-던전"><a class="anchor" aria-hidden="true" tabindex="-1" href="#달과-지하-던전">#</a>달과 지하 던전</h3>
<p>FF4의 후반부는 <strong>달로 간다.</strong> 로켓을 타고 달에 착륙하는 연출에서, 당시 중학생으로서는 스케일에 압도됐다.</p>
<p>달 지하 던전(Lunar Subterrane)은 당시 기준으로도 꽤 어려웠다. 랜덤 인카운터 빈도가 높고, 보스들이 버프/디버프를 자유롭게 쓰는 구조라 케어만으로는 버티기 어려운 국면이 자주 왔다.</p>
<p>이 구간에서 발견한 공략은 <strong>로자의 슬로 마법</strong>이었다. 강력한 보스에게 슬로를 걸면 행동 빈도가 줄어든다. ATB 게이지가 느리게 차니까 피아의 격차가 벌어진다. 이 방법을 혼자 발견했을 때 꽤 뿌듯했다.</p>
<p><img src="/images/ff-review/ff4-screenshot-2.png" alt="파이널판타지4 슈퍼패미컴 후반 전투 화면 (일본판)" loading="lazy" decoding="async"></p>
<h3 id="제로무스"><a class="anchor" aria-hidden="true" tabindex="-1" href="#제로무스">#</a>제로무스</h3>
<p>최종 보스 <strong>제로무스</strong>는 처음엔 무적 상태다. <strong>크리스탈을 사용해야 비로소 데미지를 줄 수 있는 구조.</strong></p>
<p>공략집 없이 이걸 몰랐다면 막혔을 게 분명하다. 운 좋게 이 힌트를 플레이 중에 어떤 NPC에게서 얻었다. 크리스탈이 인벤토리에 있는 걸 보고 "혹시?" 하며 써봤더니 발동됐다.</p>
<p>제로무스의 <strong>빅뱅</strong> 공격은 전체에게 큰 피해를 주는 기술이다. 이게 올 때마다 파티 전멸의 공포가 있었다. 엘릭서를 아껴가며 타이밍 맞춰 회복하는 패턴을 익히는 데 두 번의 도전이 걸렸다.</p>
<p>클리어 후 엔딩에서 카인이 홀로 달을 향해 걷는 장면. 그 여운이 오래 남았다.</p>
<h3 id="ff4-총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ff4-총평">#</a>FF4 총평</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>평가</th>
</tr>
</thead>
<tbody>
<tr>
<td>스토리</td>
<td>★★★★★ — 시리즈 내에서도 손꼽히는 감동</td>
</tr>
<tr>
<td>시스템</td>
<td>★★★★☆ — ATB 도입, 전투의 긴장감 완성</td>
</tr>
<tr>
<td>난이도</td>
<td>★★★★☆ — SFC 오리지널 버전은 꽤 가혹</td>
</tr>
<tr>
<td>음악</td>
<td>★★★★★ — 카인의 테마, 텔라의 테마 등 명곡 다수</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="final-fantasy-v--슈퍼패미컴-1992"><a class="anchor" aria-hidden="true" tabindex="-1" href="#final-fantasy-v--슈퍼패미컴-1992">#</a>FINAL FANTASY V — 슈퍼패미컴 (1992)</h2>
<h3 id="잡-시스템이-돌아왔다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#잡-시스템이-돌아왔다">#</a>"잡 시스템이 돌아왔다"</h3>
<p>FF5는 처음 접했을 때 "FF3이 돌아왔다"는 인상이었다. 잡 시스템이 다시 등장했기 때문이다.</p>
<p>그런데 FF3의 잡 시스템과는 급이 달랐다. <strong>ABP</strong>(Ability Point)를 쌓아서 각 직업의 능력을 배우고, 그걸 다른 직업에 세팅할 수 있다. 화이트메이지가 나이트의 방어 기술을 쓰거나, 블랙메이지가 기사의 장비를 착용하는 식의 조합이 가능해졌다.</p>
<p>이 시스템을 처음 이해했을 때, 게임의 깊이가 전혀 달라 보였다.</p>
<p><img src="/images/ff-review/ff5-screenshot-1.jpg" alt="파이널판타지5 슈퍼패미컴 필드 화면" loading="lazy" decoding="async"></p>
<h3 id="바츠--가장-가볍고-가장-따뜻한-주인공"><a class="anchor" aria-hidden="true" tabindex="-1" href="#바츠--가장-가볍고-가장-따뜻한-주인공">#</a>바츠 — 가장 가볍고, 가장 따뜻한 주인공</h3>
<p>FF4의 세실이 무거운 죄의식을 짊어진 주인공이었다면, FF5의 <strong>바츠</strong>는 가볍고 낙천적이다. 말도 쉽게 하고, 파티 분위기를 풀어주는 역할을 한다.</p>
<p>중학교 때는 그냥 "이 주인공 성격 좋네" 정도였는데, 돌이켜보면 바츠라는 캐릭터가 FF5의 분위기를 결정짓는 핵심이었다. 시나리오가 무겁지 않고, 모험의 즐거움을 중심에 두는 방향성이 바츠의 성격과 잘 맞았다.</p>
<h3 id="길가메쉬--시리즈-최고의-조연"><a class="anchor" aria-hidden="true" tabindex="-1" href="#길가메쉬--시리즈-최고의-조연">#</a>길가메쉬 — 시리즈 최고의 조연</h3>
<p><strong>길가메쉬.</strong> 이 캐릭터를 처음 봤을 때 그냥 적이라고 생각했다.</p>
<p>바람의 신전에서 처음 등장했을 때, 혼자서 떠들다가 파티에게 지는 익살스러운 악당이었다. 그런데 계속 만날수록 캐릭터에 정이 붙었다. 잡몹 취급받는 게 본인도 속상한 듯, 매번 "다음엔 진짜다!"를 외치다가 또 진다.</p>
<p><strong>길가메쉬와의 마지막 전투.</strong> 엑스데스가 그를 허공으로 추방하는 장면에서, 길가메쉬가 파티를 도우며 사라진다. 일본어를 몰라도 그 장면에서 뭔가 느껴졌다. 지금 다시 보면 확실히 명장면이다.</p>
<h3 id="케미스트의-믹스"><a class="anchor" aria-hidden="true" tabindex="-1" href="#케미스트의-믹스">#</a>케미스트의 믹스</h3>
<p>FF5 잡 시스템 중 가장 사기적인 직업이 <strong>케미스트</strong>다.</p>
<p>케미스트의 고유 능력 <strong>믹스</strong>는 두 가지 아이템을 섞어 강력한 효과를 만들어낸다. 이 조합이 200가지가 넘는다. 공략집 없이 혼자 발견한 조합들이 있었는데, 그중 기억에 남는 건 <strong>에테르 + 에테르 = 터보에테르</strong>였다.</p>
<p>더 강력한 조합은 <strong>영웅의 물약</strong> 계열이다. 전 파티의 능력치를 대폭 올려주는 믹스인데, 이걸 발견했을 때 "이게 게임이 맞나?" 싶었다. 최종 보스 공략이 이 조합 하나로 상당히 쉬워졌다.</p>
<h3 id="타임-메이지와-x-매직"><a class="anchor" aria-hidden="true" tabindex="-1" href="#타임-메이지와-x-매직">#</a>타임 메이지와 X-매직</h3>
<p>잡 중에서 가장 좋아했던 건 <strong>타임 메이지</strong>였다. 헤이스트로 아군 ATB 게이지를 빠르게 만들고, 슬로우로 적을 느리게 만드는 전략이 체계화됐다.</p>
<p>후반에 <strong>X-매직</strong>(연속마)과 조합하면 한 번의 행동으로 마법을 2번 발동할 수 있다. 이걸 처음 발견하고 강력한 보스에게 써봤을 때, 순식간에 보스가 녹아내리는 걸 보고 혼자 환호했던 기억이 있다.</p>
<p><img src="/images/ff-review/ff5-screenshot-2.jpg" alt="파이널판타지5 슈퍼패미컴 전투 화면" loading="lazy" decoding="async"></p>
<h3 id="엑스데스--나무다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#엑스데스--나무다">#</a>엑스데스 — 나무다</h3>
<p>FF5의 최종 보스 <strong>엑스데스</strong>는 사실 나무다.</p>
<p>그것도 악의 힘이 모인 고목. 이 설정이 처음엔 황당했다. "최종 보스가 나무야?" 근데 게임을 진행하면서 보면 이 설정이 나름 맞는다. FF5에서 등장하는 <strong>허공</strong>(공)이라는 개념과 연결되면서, 엑스데스는 단순한 악당이 아니라 세계 자체를 무(無)로 돌리려는 존재가 된다.</p>
<p><strong>네오 엑스데스.</strong> 최종 전투에서 엑스데스가 허공에 흡수되어 괴물로 변하는 폼이다. 이 형태에서 사용하는 공격들이 불규칙하고, 파티 중 한 명을 순삭하는 <strong>앨마게스트</strong>라는 기술이 있다.</p>
<p>앨마게스트 직전에 반드시 회복한다는 패턴을 익히는 데 두 번의 도전이 걸렸다. 타이밍 게임이었다.</p>
<h3 id="ff5-총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ff5-총평">#</a>FF5 총평</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>평가</th>
</tr>
</thead>
<tbody>
<tr>
<td>스토리</td>
<td>★★★☆☆ — 캐릭터 매력은 높지만 시나리오는 비교적 단순</td>
</tr>
<tr>
<td>시스템</td>
<td>★★★★★ — 잡 시스템의 완성형, 지금 해도 재밌음</td>
</tr>
<tr>
<td>난이도</td>
<td>★★★☆☆ — 시스템을 이해하면 쉽고, 모르면 어렵다</td>
</tr>
<tr>
<td>음악</td>
<td>★★★★★ — 바람의 신전, 길가메쉬의 테마는 명곡</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="final-fantasy-vi--슈퍼패미컴-1994"><a class="anchor" aria-hidden="true" tabindex="-1" href="#final-fantasy-vi--슈퍼패미컴-1994">#</a>FINAL FANTASY VI — 슈퍼패미컴 (1994)</h2>
<h3 id="이건-차원이-달랐다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#이건-차원이-달랐다">#</a>이건 차원이 달랐다</h3>
<p>FF6을 처음 켰을 때, 오프닝 연출에서 멈칫했다.</p>
<p>눈 덮인 마을을 향해 마법 장갑(마기텍 아머)을 입은 세 명의 병사가 걸어오는 장면. 그 연출의 음악이 지금까지도 FF 시리즈 최고의 오프닝 음악으로 회자된다. 슈퍼패미컴 사운드 칩으로 만들어낸 음악의 완성도가 달랐다.</p>
<p>세 명 중 한 명이 <strong>티나</strong>다. 마법을 강제로 사용당하는 소녀. 이 캐릭터의 배경이 오프닝부터 짙게 깔린다.</p>
<p><img src="/images/ff-review/ff6-screenshot-2.jpg" alt="파이널판타지6 슈퍼패미컴 플레이 화면" loading="lazy" decoding="async"></p>
<h3 id="캐릭터가-14명"><a class="anchor" aria-hidden="true" tabindex="-1" href="#캐릭터가-14명">#</a>캐릭터가 14명</h3>
<p>FF6는 플레이어블 캐릭터가 <strong>14명</strong>이다. 당시 기준으로 충격적인 숫자였다.</p>
<p>각자 고유한 능력이 있다.</p>
<ul>
<li><strong>티나</strong>: 에스퍼 형태로 변신, 마법 자동 습득</li>
<li><strong>록</strong>: 도적 기술, 적의 아이템 훔치기</li>
<li><strong>세리스</strong>: 마법 흡수, 스토리 후반의 주인공</li>
<li><strong>마쉬</strong>: 격투 명령 블리츠, 버튼 커맨드 입력</li>
<li><strong>섀도우</strong>: 수리검, 혼자 이탈하는 미스터리 닌자</li>
<li><strong>가우</strong>: 몬스터의 기술을 쓰는 야생아</li>
<li><strong>카이엔</strong>: 검도 기술 부시도</li>
<li><strong>모그</strong>: 춤 기술</li>
<li><strong>우마로</strong>: AI 제어, 자동 전투</li>
<li><strong>고고</strong>: 아군의 명령을 모방하는 흉내쟁이</li>
</ul>
<p>이 중에서 마쉬의 <strong>블리츠</strong>가 가장 인상적이었다. 격투게임처럼 방향키와 버튼을 입력하면 강력한 기술이 나온다. 처음엔 커맨드 입력이 익숙하지 않아서 실패가 잦았지만, 익숙해지면 전투의 쾌감이 다르다.</p>
<h3 id="오페라-장면"><a class="anchor" aria-hidden="true" tabindex="-1" href="#오페라-장면">#</a>오페라 장면</h3>
<p>FF6에는 <strong>오페라 장면</strong>이 있다.</p>
<p>세리스가 오페라 무대에 올라 아리아를 부르는 장면. 슈퍼패미컴이 음성을 출력할 수 없으니, 사운드 칩으로 재현한 오케스트라 음악과 화면의 연출로만 표현한다. 그런데도 당시 중학생에게 그 장면은 진짜 뮤지컬 한 편을 본 것 같은 느낌을 줬다.</p>
<p>도중에 마스코비처라는 쥐가 오페라를 망치려고 방해하는데, 이걸 막는 미니게임도 있다. 대사를 외워서 틀리지 않아야 하는데, 일본어를 모르니까 처음엔 완전히 찍어야 했다. 틀릴 때마다 오페라가 망가지는 연출이 나왔고, 그 결과가 보기 싫어서 여러 번 재도전했다.</p>
<p><img src="/images/ff-review/ff6-screenshot-3.jpg" alt="파이널판타지6 슈퍼패미컴 필드 화면" loading="lazy" decoding="async"></p>
<h3 id="케프카--jrpg-역사상-가장-성공한-빌런"><a class="anchor" aria-hidden="true" tabindex="-1" href="#케프카--jrpg-역사상-가장-성공한-빌런">#</a>케프카 — JRPG 역사상 가장 성공한 빌런</h3>
<p><strong>케프카 팔라초.</strong></p>
<p>이 캐릭터는 특별하다. 단순히 "세계를 정복하겠다"는 악당이 아니다. 케프카는 <strong>실제로 세계를 망가뜨리는 데 성공한다.</strong></p>
<p>FF6의 중반부에 케프카가 세계를 개편해버리는 장면이 있다. 그 후 게임은 <strong>붕괴된 세계</strong>(World of Ruin)로 이어진다. 주인공 파티도 각지에 뿔뿔이 흩어진다.</p>
<p>이 구조가 당시 정말 충격이었다. RPG에서 주인공이 막지 못하고 빌런이 목표를 이루는 전개. "이게 실패한 건가?" 싶었지만, 이후 게임은 흩어진 동료를 다시 모으며 케프카에게 복수하는 구성으로 이어진다.</p>
<p>케프카는 권력도 탐하고, 남을 괴롭히는 걸 즐기며, 세계를 허무주의적으로 보는 인물이다. 기존의 RPG 빌런처럼 "나는 세계를 지배할 것이다"가 아니라, "의미 없어, 다 태워버려"에 가까운 캐릭터다.</p>
<p>이 캐릭터가 JRPG 역사상 가장 인상적인 빌런 중 하나로 지금도 언급되는 건 이유가 있다.</p>
<h3 id="세리스의-각성--붕괴-후-세계에서"><a class="anchor" aria-hidden="true" tabindex="-1" href="#세리스의-각성--붕괴-후-세계에서">#</a>세리스의 각성 — 붕괴 후 세계에서</h3>
<p>세계 붕괴 후, 티나가 사라진다. 잠시 동안 <strong>세리스</strong>가 사실상의 주인공이 된다.</p>
<p>홀로 섬에서 깨어난 세리스가 늙은 시드를 간호하는 장면. 물고기를 잡아서 먹이고, 시드의 상태에 따라 엔딩 분기가 달라진다. 시드가 죽으면 세리스가 절망해 바다에 뛰어드는 장면이 나온다.</p>
<p>공략집 없이 처음 플레이할 때, 시드를 살리는 방법을 몰랐다. 상태가 좋은 물고기를 잡아야 하는데 그걸 몰라서 시드를 죽게 뒀다. 세리스가 뛰어드는 연출에서, 게임을 잘못 진행한 건지 스토리인지 구분이 안 됐다.</p>
<p>나중에 시드를 살리는 루트를 알고 다시 해봤을 때, 그쪽이 훨씬 희망적인 전개로 이어졌다.</p>
<h3 id="섀도우의-진실"><a class="anchor" aria-hidden="true" tabindex="-1" href="#섀도우의-진실">#</a>섀도우의 진실</h3>
<p><strong>섀도우</strong>는 파티 합류를 거부하다가 이탈하기를 반복하는 미스터리한 닌자다.</p>
<p>그의 과거가 꿈 이벤트를 통해 단편적으로 공개된다. 이 꿈 이벤트가 언제 뜨는지는 숙박 타이밍에 달려 있다. 공략 없이 자연스럽게 다 보기는 어렵다.</p>
<p>섀도우의 과거를 다 보면, 그가 왜 그렇게 사람을 밀어내는지가 이해된다. 그리고 최후의 던전에서 그를 기다리는 선택지가 나온다. 반드시 기다려야 섀도우가 생존하는데, 당시 나는 그 타이밍을 놓쳐서 섀도우가 최후의 던전에서 사라졌다.</p>
<p>그게 너무 아쉬워서 나중에 다시 플레이했다.</p>
<h3 id="마지막-던전--케프카의-탑"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마지막-던전--케프카의-탑">#</a>마지막 던전 — 케프카의 탑</h3>
<p>최종 던전 <strong>케프카의 탑</strong>은 세 갈래 루트로 나뉘어진다. 파티를 3팀으로 나눠 각 루트를 동시에 진행하는 구조다.</p>
<p>여기서 문제가 생겼다. 파티를 적절히 분배하지 않고 강한 캐릭터를 한 팀에 몰아넣었더니, 나머지 팀이 중간 보스에게 막혔다. 파티 구성을 다시 짜야 했다.</p>
<p>이 던전의 구조 자체가 FF 시리즈 전체를 통틀어도 독특하다. 보스들이 합체해 더 강한 형태로 변하는 연출도 있고, 최종 보스까지 가는 루트에 BGM이 계속 바뀌는 점도 인상적이었다.</p>
<p><img src="/images/ff-review/ff6-screenshot-4.jpg" alt="파이널판타지6 슈퍼패미컴 전투 화면" loading="lazy" decoding="async"></p>
<h3 id="케프카--최종-전투"><a class="anchor" aria-hidden="true" tabindex="-1" href="#케프카--최종-전투">#</a>케프카 — 최종 전투</h3>
<p>케프카와의 최종 전투는 <strong>4단계</strong>로 나뉜다.</p>
<p>조각상들이 합쳐지며 케프카의 진짜 형태가 등장하는 구조인데, 각 단계마다 다른 전략이 필요하다.</p>
<p>마지막 형태의 케프카가 사용하는 <strong>심판의 빛</strong>은 전파티에 막대한 피해를 준다. 이걸 받기 전에 배리어와 미러 상태를 유지하는 게 핵심이었다.</p>
<p>케프카를 쓰러뜨리고 흘러나오는 음악 <strong>"기쁨과 슬픔"(Balance is Restored)</strong>. 그 음악과 함께 세계가 서서히 복원되는 장면. 흩어진 동료들이 탈출하는 연출.</p>
<p>엔딩 크레딧이 끝나고 화면이 꺼질 때, 중학생이었던 나는 한동안 게임팩을 내려놓지 못했다.</p>
<h3 id="ff6-총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#ff6-총평">#</a>FF6 총평</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>평가</th>
</tr>
</thead>
<tbody>
<tr>
<td>스토리</td>
<td>★★★★★ — 시리즈 역대 최고 수준의 각본</td>
</tr>
<tr>
<td>시스템</td>
<td>★★★★☆ — 에스퍼 마법 시스템, 캐릭터별 고유 능력</td>
</tr>
<tr>
<td>난이도</td>
<td>★★★☆☆ — 시스템을 알면 중간 수준</td>
</tr>
<tr>
<td>음악</td>
<td>★★★★★ — 노부오 우에마츠 커리어 최고작 중 하나</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="4작품-비교--어떻게-달라졌나"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4작품-비교--어떻게-달라졌나">#</a>4작품 비교 — 어떻게 달라졌나</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>FF3 (패미컴)</th>
<th>FF4 (SFC)</th>
<th>FF5 (SFC)</th>
<th>FF6 (SFC)</th>
</tr>
</thead>
<tbody>
<tr>
<td>하드웨어</td>
<td>패미컴</td>
<td>슈퍼패미컴</td>
<td>슈퍼패미컴</td>
<td>슈퍼패미컴</td>
</tr>
<tr>
<td>주인공</td>
<td>이름 없는 4인</td>
<td>세실</td>
<td>바츠</td>
<td>티나 / 세리스</td>
</tr>
<tr>
<td>전투</td>
<td>턴제</td>
<td>ATB 도입</td>
<td>ATB</td>
<td>ATB</td>
</tr>
<tr>
<td>핵심 시스템</td>
<td>잡 시스템</td>
<td>캐릭터 교체</td>
<td>잡 + 어빌리티</td>
<td>에스퍼 마법</td>
</tr>
<tr>
<td>스토리 비중</td>
<td>낮음</td>
<td>높음</td>
<td>중간</td>
<td>매우 높음</td>
</tr>
<tr>
<td>플레이어블 수</td>
<td>4명 고정</td>
<td>다수 교체</td>
<td>5명 (+α)</td>
<td>14명</td>
</tr>
<tr>
<td>빌런</td>
<td>어둠의 구름</td>
<td>제로무스</td>
<td>엑스데스</td>
<td>케프카</td>
</tr>
<tr>
<td>특징</td>
<td>어려운 최종 던전</td>
<td>감동적인 희생</td>
<td>자유로운 육성</td>
<td>세계 붕괴</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="지금-다시-한다면"><a class="anchor" aria-hidden="true" tabindex="-1" href="#지금-다시-한다면">#</a>지금 다시 한다면</h2>
<p>가끔 앤버닉 같은 레트로 게임기로 이 시리즈를 다시 돌린다.</p>
<p>화면이 좋아져서 도트가 선명하게 보이고, 세이브 스테이트 기능으로 언제든 저장하고 불러올 수 있다. 근데 묘하게 그게 아쉬울 때도 있다. 세이브 포인트까지 달려가면서 느끼던 그 긴장감이 없으니까.</p>
<p>중학생 때는 몰랐던 대사들이 지금은 이해된다. 텔라의 마지막 대사, 팔롬과 포롬의 자기희생, 케프카 팔라초의 허무주의 독백. 일본어를 알고 나서 다시 하니까 완전히 다른 게임처럼 느껴졌다.</p>
<p>그래도 <strong>처음 아무것도 모르고, 일본어도 모르고, 공략집도 없이 클리어했던 그때</strong>의 기억이 제일 강하다.</p>
<p>그게 게임이 줄 수 있는 경험의 진짜 형태였다고 생각한다.</p>
<hr>
<p><em>이 시리즈가 궁금하다면 FF6부터 권한다. 지금 해도 충분히 재미있다.</em></p>]]></content:encoded>
      <category><![CDATA[리뷰]]></category>
      <category><![CDATA[파이널판타지]]></category>
      <category><![CDATA[FF3]]></category>
      <category><![CDATA[FF4]]></category>
      <category><![CDATA[FF5]]></category>
      <category><![CDATA[FF6]]></category>
      <category><![CDATA[레트로게임]]></category>
      <category><![CDATA[슈퍼패미컴]]></category>
      <category><![CDATA[패미컴]]></category>
      <category><![CDATA[추억]]></category>
    </item>

    <item>
      <title><![CDATA[시니어가 절대 하지 않는 Spring Boot API 설계 실수 7가지 — 실무 아키텍처 가이드]]></title>
      <link>https://www.stragos.xyz/posts/spring_api_design_guide</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/spring_api_design_guide</guid>
      <pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[돌아가는 API가 아니라 유지되는 API를 만들기 위해 반드시 피해야 할 설계 실수들을 실무 기준으로 정리했습니다.]]></description>
      <content:encoded><![CDATA[<h2 id="왜-api는-나중에-망할까"><a class="anchor" aria-hidden="true" tabindex="-1" href="#왜-api는-나중에-망할까">#</a>왜 API는 "나중에" 망할까?</h2>
<p>Spring Boot로 API 만드는 건 어렵지 않습니다.<br>
문제는 시간이 지나면서 점점 망가진다는 점입니다.</p>
<p>처음에는 빠르게 개발하려고 만든 구조가<br>
나중에는 수정 하나 할 때마다 전체를 건드려야 하는 상태가 됩니다.</p>
<p><strong>API는 처음이 아니라 "변경이 쌓일 때" 무너집니다</strong></p>
<hr>
<h2 id="1-controller에-비즈니스-로직-넣기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-controller에-비즈니스-로직-넣기">#</a>1. Controller에 비즈니스 로직 넣기</h2>
<p>가장 흔하게 보이는 패턴입니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">PostMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/orders"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> ResponseEntity</span><span style="color:#F97583">&#x3C;?></span><span style="color:#B392F0"> createOrder</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">RequestBody</span><span style="color:#E1E4E8"> OrderRequest request) {</span></span>
<span data-line=""><span style="color:#6A737D">    // validation</span></span>
<span data-line=""><span style="color:#F97583">    if</span><span style="color:#E1E4E8"> (request.</span><span style="color:#B392F0">getAmount</span><span style="color:#E1E4E8">() </span><span style="color:#F97583">&#x3C;=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">throw</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> RuntimeException</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"금액 오류"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 비즈니스 로직</span></span>
<span data-line=""><span style="color:#E1E4E8">    Order order </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> Order</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    order.</span><span style="color:#B392F0">setUserId</span><span style="color:#E1E4E8">(request.</span><span style="color:#B392F0">getUserId</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#E1E4E8">    order.</span><span style="color:#B392F0">setAmount</span><span style="color:#E1E4E8">(request.</span><span style="color:#B392F0">getAmount</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#E1E4E8">    order.</span><span style="color:#B392F0">setStatus</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"PENDING"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 외부 API 호출</span></span>
<span data-line=""><span style="color:#E1E4E8">    paymentClient.</span><span style="color:#B392F0">requestPayment</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // DB 저장</span></span>
<span data-line=""><span style="color:#E1E4E8">    orderRepository.</span><span style="color:#B392F0">save</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"success"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>Controller는 HTTP 요청을 받아서 응답을 내려주는 역할만 해야 합니다.<br>
비즈니스 로직이 들어오는 순간, 이 코드는 테스트하기 어려워지고 재사용도 불가능해집니다.</p>
<h3 id="개선-방법"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개선-방법">#</a>개선 방법</h3>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">PostMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/orders"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> ResponseEntity</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">OrderResponse</span><span style="color:#F97583">></span><span style="color:#B392F0"> createOrder</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">RequestBody</span><span style="color:#E1E4E8"> @</span><span style="color:#F97583">Valid</span><span style="color:#E1E4E8"> OrderRequest request) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    OrderResponse response </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> orderService.</span><span style="color:#B392F0">createOrder</span><span style="color:#E1E4E8">(request);</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(response);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Service</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RequiredArgsConstructor</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> OrderService</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> OrderRepository orderRepository;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> PaymentClient paymentClient;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Transactional</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> OrderResponse </span><span style="color:#B392F0">createOrder</span><span style="color:#E1E4E8">(OrderRequest </span><span style="color:#FFAB70">request</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        Order order </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> Order.</span><span style="color:#B392F0">create</span><span style="color:#E1E4E8">(request.</span><span style="color:#B392F0">getUserId</span><span style="color:#E1E4E8">(), request.</span><span style="color:#B392F0">getAmount</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#E1E4E8">        paymentClient.</span><span style="color:#B392F0">requestPayment</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">        orderRepository.</span><span style="color:#B392F0">save</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> OrderResponse.</span><span style="color:#B392F0">from</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>Controller는 얇게, Service는 두껍게.<br>
이 원칙을 지켜야 나중에 로직을 수정할 때 Controller를 건드리지 않아도 됩니다.</p>
<hr>
<h2 id="2-entity-그대로-api-응답으로-사용"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-entity-그대로-api-응답으로-사용">#</a>2. Entity 그대로 API 응답으로 사용</h2>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">GetMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/orders"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> List</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">Order</span><span style="color:#F97583">></span><span style="color:#B392F0"> getOrders</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> orderRepository.</span><span style="color:#B392F0">findAll</span><span style="color:#E1E4E8">();  </span><span style="color:#6A737D">// Entity 직접 반환</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>이 코드는 처음엔 빠르게 동작합니다.<br>
그런데 실제로 벌어지는 일은 이렇습니다.</p>
<ul>
<li>DB 컬럼이 API 스펙에 그대로 노출됨</li>
<li>비밀번호, 내부 상태값 같은 민감 정보 유출 가능</li>
<li>JPA 연관관계 때문에 무한 직렬화(<code>StackOverflowError</code>) 발생</li>
<li>Entity 수정이 곧바로 API 스펙 변경으로 이어짐</li>
</ul>
<h3 id="개선-방법-1"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개선-방법-1">#</a>개선 방법</h3>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Getter</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> OrderResponse</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> Long orderId;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String status;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> int</span><span style="color:#E1E4E8"> amount;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> LocalDateTime createdAt;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#E1E4E8"> OrderResponse </span><span style="color:#B392F0">from</span><span style="color:#E1E4E8">(Order </span><span style="color:#FFAB70">order</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        OrderResponse dto </span><span style="color:#F97583">=</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> OrderResponse</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">        dto.orderId </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> order.</span><span style="color:#B392F0">getId</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">        dto.status </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> order.</span><span style="color:#B392F0">getStatus</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">name</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">        dto.amount </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> order.</span><span style="color:#B392F0">getAmount</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">        dto.createdAt </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> order.</span><span style="color:#B392F0">getCreatedAt</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> dto;</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>DTO는 귀찮은 작업이 맞습니다.<br>
그러나 Entity와 API 스펙을 분리하는 순간, DB 구조를 바꿔도 API 계약이 깨지지 않습니다.<br>
이게 1년 뒤의 나를 살려줍니다.</p>
<hr>
<h2 id="3-예외-처리-기준-없음"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-예외-처리-기준-없음">#</a>3. 예외 처리 기준 없음</h2>
<p>실무에서 가장 많이 보이는 두 가지 패턴입니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 패턴 1 — 예외를 그냥 던짐</span></span>
<span data-line=""><span style="color:#F97583">throw</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> RuntimeException</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"주문을 찾을 수 없습니다"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// 패턴 2 — 예외 응답이 제각각</span></span>
<span data-line=""><span style="color:#E1E4E8">{ </span><span style="color:#9ECBFF">"error"</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> "fail"</span><span style="color:#E1E4E8"> }</span></span>
<span data-line=""><span style="color:#E1E4E8">{ </span><span style="color:#9ECBFF">"message"</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> "not found"</span><span style="color:#E1E4E8"> }</span></span>
<span data-line=""><span style="color:#E1E4E8">{ </span><span style="color:#9ECBFF">"result"</span><span style="color:#F97583">:</span><span style="color:#9ECBFF"> "error"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"code"</span><span style="color:#F97583">:</span><span style="color:#79B8FF"> 404</span><span style="color:#E1E4E8"> }</span></span></code></pre></figure>
<p>API 소비자(프론트, 외부 시스템)는 이 응답들을 예측할 수 없습니다.<br>
예외 응답 구조가 다르면 클라이언트 에러 처리 코드도 매번 달라져야 합니다.</p>
<h3 id="개선-방법-2"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개선-방법-2">#</a>개선 방법</h3>
<p>먼저 공통 예외 응답 구조를 정의합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Getter</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">AllArgsConstructor</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> ErrorResponse</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String code;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String message;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#E1E4E8"> ErrorResponse </span><span style="color:#B392F0">of</span><span style="color:#E1E4E8">(String </span><span style="color:#FFAB70">code</span><span style="color:#E1E4E8">, String </span><span style="color:#FFAB70">message</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> ErrorResponse</span><span style="color:#E1E4E8">(code, message);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>그리고 <code>@RestControllerAdvice</code>로 예외를 중앙에서 처리합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RestControllerAdvice</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> GlobalExceptionHandler</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">ExceptionHandler</span><span style="color:#E1E4E8">(OrderNotFoundException.class)</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> ResponseEntity&#x3C;</span><span style="color:#F97583">ErrorResponse</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">handleOrderNotFound</span><span style="color:#E1E4E8">(OrderNotFoundException </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> ResponseEntity</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">status</span><span style="color:#E1E4E8">(HttpStatus.NOT_FOUND)</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">body</span><span style="color:#E1E4E8">(ErrorResponse.</span><span style="color:#B392F0">of</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"ORDER_NOT_FOUND"</span><span style="color:#E1E4E8">, e.</span><span style="color:#B392F0">getMessage</span><span style="color:#E1E4E8">()));</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">ExceptionHandler</span><span style="color:#E1E4E8">(MethodArgumentNotValidException.class)</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> ResponseEntity&#x3C;</span><span style="color:#F97583">ErrorResponse</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">handleValidation</span><span style="color:#E1E4E8">(MethodArgumentNotValidException </span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        String message </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> e.</span><span style="color:#B392F0">getBindingResult</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">getFieldErrors</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">stream</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(FieldError</span><span style="color:#F97583">::</span><span style="color:#E1E4E8">getDefaultMessage)</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">findFirst</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">orElse</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"입력값 오류"</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> ResponseEntity</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">badRequest</span><span style="color:#E1E4E8">()</span></span>
<span data-line=""><span style="color:#E1E4E8">            .</span><span style="color:#B392F0">body</span><span style="color:#E1E4E8">(ErrorResponse.</span><span style="color:#B392F0">of</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"INVALID_INPUT"</span><span style="color:#E1E4E8">, message));</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>모든 예외 응답이 같은 구조로 나오면,<br>
클라이언트 개발자는 에러 응답 파싱 코드를 한 번만 짜도 됩니다.</p>
<hr>
<h2 id="4-트랜잭션-범위를-크게-잡는-설계"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-트랜잭션-범위를-크게-잡는-설계">#</a>4. 트랜잭션 범위를 크게 잡는 설계</h2>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Transactional</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> processOrder</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#E1E4E8">    Order order </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> orderRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id).</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    </span></span>
<span data-line=""><span style="color:#6A737D">    // 외부 결제 API 호출 — 최대 3초 소요</span></span>
<span data-line=""><span style="color:#E1E4E8">    PaymentResult result </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> paymentClient.</span><span style="color:#B392F0">requestPayment</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">    </span></span>
<span data-line=""><span style="color:#6A737D">    // 파일 업로드 처리 — 최대 5초 소요</span></span>
<span data-line=""><span style="color:#E1E4E8">    fileService.</span><span style="color:#B392F0">uploadReceipt</span><span style="color:#E1E4E8">(result);</span></span>
<span data-line=""><span style="color:#E1E4E8">    </span></span>
<span data-line=""><span style="color:#6A737D">    // DB 저장</span></span>
<span data-line=""><span style="color:#E1E4E8">    order.</span><span style="color:#B392F0">confirm</span><span style="color:#E1E4E8">(result);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>이 코드의 문제는 트랜잭션이 "외부 API 호출 + 파일 처리 + DB 저장"을 전부 감싸고 있다는 점입니다.</p>
<ul>
<li>외부 API 호출 중 DB 커넥션을 잡고 있음</li>
<li>동시 요청이 많아지면 커넥션 풀 고갈</li>
<li>외부 API 타임아웃 3초 동안 락 유지</li>
</ul>
<h3 id="개선-방법-3"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개선-방법-3">#</a>개선 방법</h3>
<p>트랜잭션은 실제로 DB를 건드리는 구간에만 최소 범위로 적용합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> processOrder</span><span style="color:#E1E4E8">(Long orderId) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    Order order </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> orderRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(orderId).</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 외부 API — 트랜잭션 불필요</span></span>
<span data-line=""><span style="color:#E1E4E8">    PaymentResult result </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> paymentClient.</span><span style="color:#B392F0">requestPayment</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // 파일 처리 — 트랜잭션 불필요</span></span>
<span data-line=""><span style="color:#E1E4E8">    fileService.</span><span style="color:#B392F0">uploadReceipt</span><span style="color:#E1E4E8">(result);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">    // DB 저장 — 트랜잭션 필요</span></span>
<span data-line=""><span style="color:#B392F0">    confirmOrder</span><span style="color:#E1E4E8">(orderId, result);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Transactional</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> confirmOrder</span><span style="color:#E1E4E8">(Long orderId, PaymentResult result) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    Order order </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> orderRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(orderId).</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">    order.</span><span style="color:#B392F0">confirm</span><span style="color:#E1E4E8">(result);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>트랜잭션 시간 = DB 커넥션 점유 시간입니다.<br>
짧을수록 동시 처리량이 늘어납니다.</p>
<hr>
<h2 id="5-api-응답-구조가-일관되지-않음"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-api-응답-구조가-일관되지-않음">#</a>5. API 응답 구조가 일관되지 않음</h2>
<p>팀이 커지면 이런 상황이 생깁니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="json" data-theme="github-dark"><code data-language="json" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// A 개발자가 만든 API</span></span>
<span data-line=""><span style="color:#E1E4E8">{ </span><span style="color:#79B8FF">"data"</span><span style="color:#E1E4E8">: { </span><span style="color:#79B8FF">"orderId"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">"status"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"PENDING"</span><span style="color:#E1E4E8"> } }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// B 개발자가 만든 API</span></span>
<span data-line=""><span style="color:#E1E4E8">{ </span><span style="color:#79B8FF">"result"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"ok"</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">"order"</span><span style="color:#E1E4E8">: { </span><span style="color:#79B8FF">"id"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8"> } }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// C 개발자가 만든 API</span></span>
<span data-line=""><span style="color:#E1E4E8">{ </span><span style="color:#79B8FF">"orderId"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">"status"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"PENDING"</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">"success"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8"> }</span></span></code></pre></figure>
<p>프론트 개발자는 API마다 응답 구조를 다르게 파싱해야 합니다.<br>
사소해 보이지만 API가 수십 개 쌓이면 유지보수 비용이 급격히 커집니다.</p>
<h3 id="개선-방법-4"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개선-방법-4">#</a>개선 방법</h3>
<p>공통 응답 wrapper를 정의하고 팀 전체가 동일하게 사용합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Getter</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">AllArgsConstructor</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> ApiResponse</span><span style="color:#E1E4E8">&#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> {</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> boolean</span><span style="color:#E1E4E8"> success;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> T data;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#E1E4E8"> String message;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> ApiResponse&#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(T </span><span style="color:#FFAB70">data</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> ApiResponse&#x3C;>(</span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">, data, </span><span style="color:#79B8FF">null</span><span style="color:#E1E4E8">);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> static</span><span style="color:#E1E4E8"> &#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> ApiResponse&#x3C;</span><span style="color:#F97583">T</span><span style="color:#E1E4E8">> </span><span style="color:#B392F0">fail</span><span style="color:#E1E4E8">(String </span><span style="color:#FFAB70">message</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> ApiResponse&#x3C;>(</span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">null</span><span style="color:#E1E4E8">, message);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 모든 Controller에서 동일한 구조로 응답</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">GetMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/orders/{id}"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> ResponseEntity</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">ApiResponse</span><span style="color:#F97583">&#x3C;</span><span style="color:#E1E4E8">OrderResponse</span><span style="color:#F97583">>></span><span style="color:#B392F0"> getOrder</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">PathVariable</span><span style="color:#E1E4E8"> Long id) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    OrderResponse order </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> orderService.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id);</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> ResponseEntity.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(ApiResponse.</span><span style="color:#B392F0">ok</span><span style="color:#E1E4E8">(order));</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="json" data-theme="github-dark"><code data-language="json" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">{</span></span>
<span data-line=""><span style="color:#79B8FF">  "success"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
<span data-line=""><span style="color:#79B8FF">  "data"</span><span style="color:#E1E4E8">: { </span><span style="color:#79B8FF">"orderId"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">"status"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"PENDING"</span><span style="color:#E1E4E8"> },</span></span>
<span data-line=""><span style="color:#79B8FF">  "message"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">null</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>응답 구조를 통일하면 프론트 개발자와의 협업 비용이 확연히 줄어듭니다.</p>
<hr>
<h2 id="6-api-버전-관리-없음"><a class="anchor" aria-hidden="true" tabindex="-1" href="#6-api-버전-관리-없음">#</a>6. API 버전 관리 없음</h2>
<pre><code>GET /api/orders
</code></pre>
<p>서비스가 성장하면 반드시 마주치는 상황입니다.</p>
<ul>
<li>모바일 앱 구버전 사용자는 아직 옛날 API를 씁니다</li>
<li>프론트를 새로 바꿨는데 기존 스펙을 바꾸면 구버전 앱이 깨집니다</li>
<li>결국 레거시 코드를 무한정 유지해야 합니다</li>
</ul>
<h3 id="개선-방법-5"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개선-방법-5">#</a>개선 방법</h3>
<p>처음부터 URL에 버전을 박아 넣습니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// v1 Controller — 기존 클라이언트용</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RestController</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RequestMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/api/v1/orders"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> OrderV1Controller</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#6A737D">    // ...</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">// v2 Controller — 신규 스펙 적용</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RestController</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RequestMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/api/v2/orders"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> OrderV2Controller</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""><span style="color:#6A737D">    // ...</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<pre><code>GET /api/v1/orders  → 구버전 모바일 앱
GET /api/v2/orders  → 신규 웹/앱
</code></pre>
<p>버전 없이 API를 제공하면<br>
"한 번 배포한 스펙을 절대 바꿀 수 없는" 상태가 됩니다.<br>
나중에 버전을 추가하는 건 이미 늦습니다.</p>
<hr>
<h2 id="7-확장성-고려-없는-구조"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7-확장성-고려-없는-구조">#</a>7. 확장성 고려 없는 구조</h2>
<p>단일 기능 추가가 전체를 건드리게 되는 구조입니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#6A737D">// 하나의 메서드가 너무 많은 것을 알고 있음</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> createOrder</span><span style="color:#E1E4E8">(OrderRequest request) {</span></span>
<span data-line=""><span style="color:#B392F0">    validateUser</span><span style="color:#E1E4E8">(request.</span><span style="color:#B392F0">getUserId</span><span style="color:#E1E4E8">());       </span><span style="color:#6A737D">// 유저 검증</span></span>
<span data-line=""><span style="color:#B392F0">    checkInventory</span><span style="color:#E1E4E8">(request.</span><span style="color:#B392F0">getProductId</span><span style="color:#E1E4E8">()); </span><span style="color:#6A737D">// 재고 확인</span></span>
<span data-line=""><span style="color:#B392F0">    calculateDiscount</span><span style="color:#E1E4E8">(request);              </span><span style="color:#6A737D">// 할인 계산</span></span>
<span data-line=""><span style="color:#B392F0">    processPayment</span><span style="color:#E1E4E8">(request);                 </span><span style="color:#6A737D">// 결제 처리</span></span>
<span data-line=""><span style="color:#B392F0">    sendNotification</span><span style="color:#E1E4E8">(request.</span><span style="color:#B392F0">getUserId</span><span style="color:#E1E4E8">());  </span><span style="color:#6A737D">// 알림 발송</span></span>
<span data-line=""><span style="color:#B392F0">    updateStatistics</span><span style="color:#E1E4E8">(request);               </span><span style="color:#6A737D">// 통계 갱신</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>기능이 추가될수록 이 메서드는 계속 커집니다.<br>
할인 로직을 바꾸려면 결제 코드 옆을 건드려야 하고,<br>
테스트하려면 전체 의존성을 다 준비해야 합니다.</p>
<h3 id="개선-방법-6"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개선-방법-6">#</a>개선 방법</h3>
<p>책임을 분리하고, 후처리는 이벤트로 분리합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Service</span></span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">RequiredArgsConstructor</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> OrderService</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> OrderValidator orderValidator;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> PaymentService paymentService;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> OrderRepository orderRepository;</span></span>
<span data-line=""><span style="color:#F97583">    private</span><span style="color:#F97583"> final</span><span style="color:#E1E4E8"> ApplicationEventPublisher eventPublisher;</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">Transactional</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#E1E4E8"> OrderResponse </span><span style="color:#B392F0">createOrder</span><span style="color:#E1E4E8">(OrderRequest </span><span style="color:#FFAB70">request</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#6A737D">        // 핵심 비즈니스 로직만</span></span>
<span data-line=""><span style="color:#E1E4E8">        orderValidator.</span><span style="color:#B392F0">validate</span><span style="color:#E1E4E8">(request);</span></span>
<span data-line=""><span style="color:#E1E4E8">        Order order </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> Order.</span><span style="color:#B392F0">create</span><span style="color:#E1E4E8">(request);</span></span>
<span data-line=""><span style="color:#E1E4E8">        paymentService.</span><span style="color:#B392F0">process</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">        orderRepository.</span><span style="color:#B392F0">save</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#6A737D">        // 부가 처리는 이벤트로 분리</span></span>
<span data-line=""><span style="color:#E1E4E8">        eventPublisher.</span><span style="color:#B392F0">publishEvent</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">new</span><span style="color:#B392F0"> OrderCreatedEvent</span><span style="color:#E1E4E8">(order));</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#F97583">        return</span><span style="color:#E1E4E8"> OrderResponse.</span><span style="color:#B392F0">from</span><span style="color:#E1E4E8">(order);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Component</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> class</span><span style="color:#B392F0"> OrderEventListener</span><span style="color:#E1E4E8"> {</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">    @</span><span style="color:#F97583">EventListener</span></span>
<span data-line=""><span style="color:#F97583">    public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> onOrderCreated</span><span style="color:#E1E4E8">(OrderCreatedEvent </span><span style="color:#FFAB70">event</span><span style="color:#E1E4E8">) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        notificationService.</span><span style="color:#B392F0">sendOrderConfirm</span><span style="color:#E1E4E8">(event.</span><span style="color:#B392F0">getOrder</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#E1E4E8">        statisticsService.</span><span style="color:#B392F0">record</span><span style="color:#E1E4E8">(event.</span><span style="color:#B392F0">getOrder</span><span style="color:#E1E4E8">());</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>핵심 로직과 부가 로직을 이벤트로 분리하면<br>
알림 발송 방식이 바뀌어도 <code>OrderService</code>는 건드리지 않아도 됩니다.</p>
<hr>
<h2 id="7가지-요약"><a class="anchor" aria-hidden="true" tabindex="-1" href="#7가지-요약">#</a>7가지 요약</h2>
<table>
<thead>
<tr>
<th>번호</th>
<th>문제</th>
<th>핵심 원칙</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Controller에 비즈니스 로직</td>
<td>Controller는 얇게, Service는 두껍게</td>
</tr>
<tr>
<td>2</td>
<td>Entity 직접 반환</td>
<td>항상 DTO/Response 객체로 변환</td>
</tr>
<tr>
<td>3</td>
<td>예외 처리 기준 없음</td>
<td><code>@RestControllerAdvice</code>로 중앙화</td>
</tr>
<tr>
<td>4</td>
<td>트랜잭션 범위 과도</td>
<td>DB 작업 구간에만 최소 범위 적용</td>
</tr>
<tr>
<td>5</td>
<td>응답 구조 불일치</td>
<td>공통 <code>ApiResponse&#x3C;T></code> wrapper 정의</td>
</tr>
<tr>
<td>6</td>
<td>API 버전 관리 없음</td>
<td>처음부터 <code>/api/v1/</code> 구조로 시작</td>
</tr>
<tr>
<td>7</td>
<td>확장성 고려 없는 구조</td>
<td>부가 처리는 이벤트로 분리</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마무리">#</a>마무리</h2>
<p>API 설계는 "미래를 대비하는 선택"입니다.</p>
<p>처음 개발할 때 조금 더 신경 쓰는 것이<br>
6개월 후에 전체를 뜯어고치는 것보다 훨씬 적은 비용입니다.</p>
<p><strong>돌아가는 API보다 유지되는 API를 만드는 것</strong><br>
그게 시니어 개발자가 가장 먼저 생각하는 기준입니다.</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[API 설계]]></category>
      <category><![CDATA[백엔드]]></category>
      <category><![CDATA[아키텍처]]></category>
      <category><![CDATA[실무팁]]></category>
    </item>

    <item>
      <title><![CDATA[삼성 QLED 85인치 KQ85QF8A 리뷰 — 거실이 바뀌는 경험]]></title>
      <link>https://www.stragos.xyz/posts/samsung-kq85qf8a-review</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/samsung-kq85qf8a-review</guid>
      <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[85인치 대형 TV를 처음 들여놨습니다. 삼성 QLED QF8A 시리즈, 설치부터 화질·사운드·스마트 기능까지 직접 써보고 정리했습니다.]]></description>
      <content:encoded><![CDATA[<h2 id="왜-85인치인가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#왜-85인치인가">#</a>왜 85인치인가</h2>
<p>처음엔 75인치를 보러 갔다.</p>
<p>매장에서 75인치 옆에 85인치를 나란히 놓고 보는 순간, 그냥 85인치로 결정했다. 가격 차이는 있지만 한 번 사면 오래 쓸 물건인데, 화면 크기는 나중에 후회해봤자 바꿀 수가 없다.</p>
<p>거거익선(巨巨益善)이라는 말이 TV에서만큼 정확하게 맞는 데가 없는 것 같다.</p>
<p><img src="/images/samsung-q8f/product_main.jpg" alt="삼성 QLED QF8A 85인치" loading="lazy" decoding="async"></p>
<hr>
<h2 id="스펙-정리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#스펙-정리">#</a>스펙 정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>사양</th>
</tr>
</thead>
<tbody>
<tr>
<td>모델명</td>
<td>KQ85QF8A (국내) / QN85Q8FA (북미)</td>
</tr>
<tr>
<td>화면 크기</td>
<td>85인치 (214cm)</td>
</tr>
<tr>
<td>패널</td>
<td>QLED (퀀텀닷) 4K UHD</td>
</tr>
<tr>
<td>해상도</td>
<td>3840 × 2160</td>
</tr>
<tr>
<td>프로세서</td>
<td>Q4 AI 프로세서</td>
</tr>
<tr>
<td>HDR</td>
<td>HDR10+, HLG 지원 / Dolby Vision 미지원</td>
</tr>
<tr>
<td>주사율</td>
<td>120Hz (HDMI 2.1, VRR, FreeSync Premium)</td>
</tr>
<tr>
<td>사운드</td>
<td>2.0ch 20W, Dolby Atmos, Object Tracking Sound Lite</td>
</tr>
<tr>
<td>OS</td>
<td>Tizen</td>
</tr>
<tr>
<td>연결</td>
<td>HDMI 4개, USB 2개, Wi-Fi, Bluetooth, AirPlay 2</td>
</tr>
<tr>
<td>소비전력</td>
<td>약 310W</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="배송설치"><a class="anchor" aria-hidden="true" tabindex="-1" href="#배송설치">#</a>배송·설치</h2>
<p>85인치는 혼자 설치가 불가능하다.</p>
<p>박스 크기부터 일반 엘리베이터에 간신히 들어오는 수준이고, 무게도 만만치 않다. 삼성 공식 설치 기사가 오면 2인 1조로 작업한다. 스탠드 조립부터 케이블 연결까지 한 번에 처리해주고, 기존 TV 수거까지 해간다.</p>
<p>설치 자체는 30~40분 안에 끝났다. 벽걸이로 설치했는데, 85인치는 VESA 규격 확인과 벽 강도가 핵심이다. 설치 기사가 벽 앵커 위치를 잡아주고 수평까지 맞춰줘서 생각보다 깔끔하게 마무리됐다. 벽에 붙이고 나니 공간이 훨씬 넓어 보이는 효과가 있었다.</p>
<hr>
<h2 id="화질--qled다운-색감"><a class="anchor" aria-hidden="true" tabindex="-1" href="#화질--qled다운-색감">#</a>화질 — QLED다운 색감</h2>
<p><img src="/images/samsung-q8f/lifestyle_room.jpg" alt="삼성 QLED QF8A 설치 모습" loading="lazy" decoding="async"></p>
<p>퀀텀닷 100% 컬러 볼륨이 실제로 어떤 의미인지, 화면을 켜는 순간 바로 이해된다.</p>
<p>자연 다큐멘터리를 틀었을 때 초록색 숲, 파란 바다의 색 재현이 진짜 예쁘다. 특히 HDR10+ 콘텐츠에서 밝은 부분과 어두운 부분의 디테일이 동시에 살아 있는 게 인상적이다. 최대 밝기는 약 460~490nits 수준으로, 낮에도 커튼 없이 시청할 수 있다.</p>
<p>다만 <strong>Dolby Vision은 지원하지 않는다.</strong> Netflix나 Apple TV+의 Dolby Vision 콘텐츠는 HDR10+로 다운컨버트되어 재생된다. 눈에 띄는 차이가 있는지는 솔직히 일반 시청 환경에서 잘 모르겠다.</p>
<p>OLED와 비교하면 완전한 블랙 표현이나 명암비에서 차이가 있는 건 사실이다. 그러나 85인치급에서 OLED는 가격이 훨씬 비싸고, 번인 걱정도 있다. QLED 85인치가 현실적인 선택인 이유다.</p>
<p><strong>영화 모드</strong>로 세팅했을 때 색 온도와 감마 설정이 잘 맞아서, 따로 캘리브레이션 없이도 자연스러운 화면이 나온다.</p>
<hr>
<h2 id="게이밍-성능"><a class="anchor" aria-hidden="true" tabindex="-1" href="#게이밍-성능">#</a>게이밍 성능</h2>
<p>HDMI 2.1 포트를 통해 4K 120Hz, VRR, FreeSync Premium을 지원한다.</p>
<p>PlayStation 5와 연결했을 때 게임 모드 자동 전환이 부드럽게 작동하고, 입력 지연(인풋렉)도 게임 모드 기준 약 10~13ms 수준으로 나쁘지 않다.</p>
<p>85인치 화면으로 즐기는 오픈월드 게임의 몰입감은 정말 다르다. 한 번 경험하면 돌아가기 어렵다.</p>
<p>다만 <strong>FPS 게임에서 잔상(모션 블러)</strong> 은 OLED에 비해 약간 있는 편이다. 퍼포먼스 게이머보다는 넓은 화면에서 RPG·액션 게임을 즐기는 분들에게 더 적합하다.</p>
<hr>
<h2 id="사운드"><a class="anchor" aria-hidden="true" tabindex="-1" href="#사운드">#</a>사운드</h2>
<p>내장 스피커는 2.0ch 20W로, 기대를 크게 안 하는 게 맞다.</p>
<p>대화나 효과음은 무난하게 들리지만, 저음은 얇다. 85인치 화면에 걸맞은 사운드를 원한다면 사운드바는 필수다. 삼성 사운드바와 연결하면 <strong>Q-Symphony</strong> 기능으로 TV 스피커와 사운드바가 동시에 작동해서 더 풍성한 입체 사운드가 나온다.</p>
<hr>
<h2 id="스마트-tv--tizen"><a class="anchor" aria-hidden="true" tabindex="-1" href="#스마트-tv--tizen">#</a>스마트 TV — Tizen</h2>
<p>삼성 Tizen OS는 스마트 TV 플랫폼 중에서 완성도가 높은 편이다.</p>
<p>초기 설정이 빠르고, 앱 설치도 간단하다. Netflix, 유튜브, 웨이브, 왓챠, 쿠팡플레이 모두 앱이 있고, 반응 속도도 끊김 없이 빠르다. <strong>삼성 TV 플러스</strong>로 무료 채널도 꽤 있어서 별도 셋톱박스 없이도 볼 거리가 충분하다.</p>
<p>리모컨은 솔라셀 방식이라 배터리 없이 실내 조명으로 충전된다. 작고 가벼운데 자주 쓰는 기능은 다 있어서 편하다.</p>
<hr>
<h2 id="아쉬운-점"><a class="anchor" aria-hidden="true" tabindex="-1" href="#아쉬운-점">#</a>아쉬운 점</h2>
<p>솔직하게 쓴다.</p>
<ol>
<li><strong>Dolby Vision 미지원</strong> — 경쟁사 LG OLED는 지원하는데 삼성 QLED는 HDR10+만 지원한다. 삼성이 의도적으로 배제한 것이라 아쉽다.</li>
<li><strong>내장 스피커 부실</strong> — 20W로 85인치를 채우기엔 부족하다. 사운드바 예산을 따로 잡아야 한다.</li>
<li><strong>반사율</strong> — 유광 패널이라 낮에 햇빛이 강하게 들어오면 반사가 생긴다. 암막 커튼이나 TV 배치에 신경을 써야 한다.</li>
<li><strong>설치 혼자 불가</strong> — 특히 벽걸이는 반드시 2인 이상 필요하다. 1인 가구라면 설치 스케줄 맞추는 게 번거롭다.</li>
<li><strong>전기료</strong> — 정격 소비전력 310W. 매일 4~5시간 시청 시 한 달 전기료가 체감으로 좀 오른다.</li>
</ol>
<hr>
<h2 id="총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#총평">#</a>총평</h2>
<p><strong>솔직한 점수: 8.5 / 10</strong></p>
<p>85인치 QLED를 집에 들여놓는 경험 자체가 달랐다.</p>
<p>크다고 해서 화질이 퍼진다거나 색이 뭉친다는 느낌이 전혀 없고, 4K 콘텐츠는 멀리서 봐도 선명하다. Dolby Vision 미지원이나 사운드 부족이 아쉽긴 하지만, 사운드바 하나 추가하면 아쉬운 점의 절반은 해결된다.</p>
<p>가격 대비 화면 크기와 화질의 균형에서 이 가격대 85인치 QLED 중 삼성 QF8A는 충분히 경쟁력 있는 선택이다.</p>
<p>"TV는 거거익선"이라는 말, 직접 써보니 틀린 말이 아니다.</p>
<hr>
<blockquote>
<p>이 포스팅은 순수 개인 구매 후기입니다. 협찬 없음, 뒷광고 없음.</p>
</blockquote>]]></content:encoded>
      <category><![CDATA[리뷰]]></category>
      <category><![CDATA[삼성TV]]></category>
      <category><![CDATA[QLED]]></category>
      <category><![CDATA[85인치]]></category>
      <category><![CDATA[KQ85QF8A]]></category>
      <category><![CDATA[TV후기]]></category>
    </item>

    <item>
      <title><![CDATA[Claude 제대로 쓰는 법 — 처음 써보는 사람을 위한 실전 가이드]]></title>
      <link>https://www.stragos.xyz/posts/claude-ai-guide-for-beginners</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/claude-ai-guide-for-beginners</guid>
      <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[Anthropic의 Claude AI, 그냥 대화만 하면 반만 쓰는 겁니다. 초보자도 바로 따라할 수 있는 핵심 팁을 정리했습니다.]]></description>
      <content:encoded><![CDATA[<h2 id="claude가-뭔데"><a class="anchor" aria-hidden="true" tabindex="-1" href="#claude가-뭔데">#</a>Claude가 뭔데?</h2>
<p>Claude는 Anthropic이라는 AI 안전 연구 회사에서 만든 대화형 AI입니다. ChatGPT랑 비슷하지만, 몇 가지 차이가 있습니다.</p>
<p>가장 두드러지는 특징은 <strong>긴 맥락 처리 능력</strong>입니다. 최신 모델(Opus 4.6, Sonnet 4.6) 기준으로 한 번의 대화에서 약 75만 단어(1백만 토큰, 책 10권 분량)까지 한꺼번에 넣고 분석할 수 있습니다. 거기다 한국어 이해도가 꽤 좋고, 코딩이나 글쓰기 작업에서 특히 빛납니다.</p>
<p>무료로 시작할 수 있고, <a href="https://claude.ai">claude.ai</a> 에서 바로 써볼 수 있습니다.</p>
<hr>
<h2 id="플랜-뭐-써야-해"><a class="anchor" aria-hidden="true" tabindex="-1" href="#플랜-뭐-써야-해">#</a>플랜 뭐 써야 해?</h2>
<table>
<thead>
<tr>
<th>플랜</th>
<th>가격</th>
<th>특징</th>
</tr>
</thead>
<tbody>
<tr>
<td>Free</td>
<td>무료</td>
<td>Claude Sonnet 사용, 하루 메시지 제한 있음</td>
</tr>
<tr>
<td>Pro</td>
<td>$20/월</td>
<td>더 빠른 응답, 더 많은 메시지, 최신 모델 사용</td>
</tr>
<tr>
<td>Max</td>
<td>$100/월</td>
<td>Pro보다 5배 이상 높은 사용량, 최신 모델 우선 제공</td>
</tr>
</tbody>
</table>
<p>처음엔 Free로 충분합니다. 매일 쓰다 보면 제한이 느껴지는 시점이 오는데, 그때 Pro로 올리는 게 맞습니다.</p>
<hr>
<h2 id="핵심-팁-1--구체적으로-말해야-제대로-된-답이-나온다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#핵심-팁-1--구체적으로-말해야-제대로-된-답이-나온다">#</a>핵심 팁 1 — 구체적으로 말해야 제대로 된 답이 나온다</h2>
<p>Claude한테 모호하게 말하면, 모호한 답이 돌아옵니다.</p>
<p><strong>❌ 이렇게 하면 안 됨:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="text" data-theme="github-dark"><code data-language="text" data-theme="github-dark" style="display: grid;"><span data-line=""><span>블로그 글 써줘</span></span></code></pre></figure>
<p><strong>✅ 이렇게 하면 좋음:</strong></p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="text" data-theme="github-dark"><code data-language="text" data-theme="github-dark" style="display: grid;"><span data-line=""><span>Next.js 블로그를 처음 만들어보려는 초보 개발자를 대상으로,</span></span>
<span data-line=""><span>파일 기반 라우팅이 무엇인지 설명하는 500자 내외의 블로그 글 써줘.</span></span>
<span data-line=""><span>전문 용어는 최대한 쉽게 풀어서 써줘.</span></span></code></pre></figure>
<p>두 번째처럼 <strong>대상, 주제, 분량, 톤</strong>을 같이 알려주면 첫 번째 시도에서 원하는 결과가 나올 확률이 훨씬 높아집니다.</p>
<hr>
<h2 id="핵심-팁-2--역할을-줘라"><a class="anchor" aria-hidden="true" tabindex="-1" href="#핵심-팁-2--역할을-줘라">#</a>핵심 팁 2 — 역할을 줘라</h2>
<p>Claude한테 "너는 ○○야" 하고 역할을 부여하면 답변의 깊이가 달라집니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="text" data-theme="github-dark"><code data-language="text" data-theme="github-dark" style="display: grid;"><span data-line=""><span>너는 10년 경력의 시니어 백엔드 개발자야.</span></span>
<span data-line=""><span>내가 짠 아래 코드를 리뷰해줘. 성능 문제나 보안 취약점 위주로.</span></span>
<span data-line=""> </span>
<span data-line=""><span>[코드 붙여넣기]</span></span></code></pre></figure>
<p>이렇게 하면 일반적인 코드 리뷰가 아니라, 경험 있는 개발자 시각에서 실질적인 피드백이 나옵니다.</p>
<p>역할 예시:</p>
<ul>
<li>"너는 영어 원어민이야. 아래 문장을 자연스러운 영어로 교정해줘."</li>
<li>"너는 스타트업 마케터야. 이 기능의 소개 문구를 짧고 임팩트 있게 작성해줘."</li>
<li>"너는 까다로운 면접관이야. 이 자기소개서에서 약점을 찾아줘."</li>
</ul>
<hr>
<h2 id="핵심-팁-3--예시를-보여줘라"><a class="anchor" aria-hidden="true" tabindex="-1" href="#핵심-팁-3--예시를-보여줘라">#</a>핵심 팁 3 — 예시를 보여줘라</h2>
<p>원하는 결과물의 스타일이 있다면, 직접 예시를 보여주는 게 말로 설명하는 것보다 훨씬 효과적입니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="text" data-theme="github-dark"><code data-language="text" data-theme="github-dark" style="display: grid;"><span data-line=""><span>아래처럼 간결하고 직접적인 스타일로 제품 소개 문구 3개 써줘.</span></span>
<span data-line=""> </span>
<span data-line=""><span>예시:</span></span>
<span data-line=""><span>"설치 5분. 이후는 알아서."</span></span>
<span data-line=""><span>"코드 없이 자동화."</span></span>
<span data-line=""> </span>
<span data-line=""><span>제품: 매일 아침 날씨를 슬랙으로 보내주는 앱</span></span></code></pre></figure>
<p>예시를 3~5개 정도 주면 Claude가 스타일을 정확히 잡아냅니다.</p>
<hr>
<h2 id="핵심-팁-4--projects-기능-활용하기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#핵심-팁-4--projects-기능-활용하기">#</a>핵심 팁 4 — Projects 기능 활용하기</h2>
<p>Claude Pro에는 <strong>Projects</strong>라는 기능이 있습니다. 특정 프로젝트에 관련된 문서, 코드, 배경 정보를 미리 등록해두면, 이후 대화마다 그 맥락을 기억합니다.</p>
<p>예를 들어:</p>
<ul>
<li>진행 중인 프로젝트의 기획서 PDF를 올려두면</li>
<li>"이 기획서 기준으로 DB 스키마 설계해줘" 같은 질문에 즉시 맥락 있는 답변이 나옵니다</li>
</ul>
<p>매번 배경 설명을 반복하는 게 귀찮았다면, Projects가 그 문제를 해결해줍니다.</p>
<hr>
<h2 id="실전-활용-예시"><a class="anchor" aria-hidden="true" tabindex="-1" href="#실전-활용-예시">#</a>실전 활용 예시</h2>
<h3 id="코딩할-때"><a class="anchor" aria-hidden="true" tabindex="-1" href="#코딩할-때">#</a>코딩할 때</h3>
<p>Claude는 코드 작성, 리뷰, 디버깅 모두 잘합니다. 코드를 그냥 붙여넣으면서 "뭐가 문제야?" 하면 에러 원인 찾아주고, "이거 TypeScript로 바꿔줘" 하면 바꿔줍니다.</p>
<p>특히 에러 메시지를 그대로 붙여넣으면서 "이게 왜 나는 거야?" 하면 원인과 해결책을 같이 알려줍니다. 스택오버플로우 뒤지는 것보다 빠릅니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="text" data-theme="github-dark"><code data-language="text" data-theme="github-dark" style="display: grid;"><span data-line=""><span>아래 에러가 계속 나오는데 왜 그런지, 어떻게 고쳐야 하는지 알려줘.</span></span>
<span data-line=""> </span>
<span data-line=""><span>Error: Cannot read properties of undefined (reading 'map')</span></span>
<span data-line=""> </span>
<span data-line=""><span>[코드 붙여넣기]</span></span></code></pre></figure>
<h3 id="긴-문서-요약할-때"><a class="anchor" aria-hidden="true" tabindex="-1" href="#긴-문서-요약할-때">#</a>긴 문서 요약할 때</h3>
<p>PDF나 긴 글을 통째로 붙여넣고 "핵심만 5줄로 요약해줘" 또는 "이 내용에서 내 상황에 맞는 부분만 추려줘" 하는 식으로 쓰면 됩니다.</p>
<p>회의록 정리, 계약서 핵심 조항 파악, 논문 요약 등에 유용합니다.</p>
<h3 id="글-쓸-때"><a class="anchor" aria-hidden="true" tabindex="-1" href="#글-쓸-때">#</a>글 쓸 때</h3>
<p>초안을 직접 쓰기 막막할 때, 구조만 잡아달라고 하면 됩니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="text" data-theme="github-dark"><code data-language="text" data-theme="github-dark" style="display: grid;"><span data-line=""><span>아래 주제로 블로그 글의 목차와 각 섹션의 핵심 내용을 잡아줘.</span></span>
<span data-line=""><span>내가 직접 쓸 거니까 초안은 필요 없고 구조만 잡아줘.</span></span>
<span data-line=""> </span>
<span data-line=""><span>주제: 레트로 게임기 입문 가이드</span></span></code></pre></figure>
<hr>
<h2 id="claude-code--개발자라면-이것도"><a class="anchor" aria-hidden="true" tabindex="-1" href="#claude-code--개발자라면-이것도">#</a>Claude Code — 개발자라면 이것도</h2>
<p>Claude AI 웹 말고, <strong>Claude Code</strong>라는 CLI 도구도 있습니다. 터미널에서 직접 돌리면서 코드베이스 전체를 분석하거나, 파일 수정, 테스트 실행까지 시킬 수 있습니다.</p>
<p>설치:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#79B8FF"> -g</span><span style="color:#9ECBFF"> @anthropic-ai/claude-code</span></span></code></pre></figure>
<p>실행:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="bash" data-theme="github-dark"><code data-language="bash" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#B392F0">claude</span></span></code></pre></figure>
<p>프로젝트 폴더에서 실행하면 Claude가 전체 코드 구조를 파악하고, "이 프로젝트에 댓글 기능 추가해줘" 같은 지시에 직접 파일을 만들고 수정합니다.</p>
<p>웹 인터페이스보다 훨씬 강력하고, 코딩에서 체감 차이가 큽니다. 다만 API 키가 필요하고, 사용한 만큼 비용이 청구됩니다.</p>
<hr>
<h2 id="자주-하는-실수"><a class="anchor" aria-hidden="true" tabindex="-1" href="#자주-하는-실수">#</a>자주 하는 실수</h2>
<p><strong>1. 너무 짧게 말한다</strong>
"이거 고쳐줘"처럼 짧게 말하면 Claude도 뭘 어떻게 고쳐야 할지 모릅니다. 배경 설명을 조금만 더 추가하면 결과가 확 달라집니다.</p>
<p><strong>2. 첫 번째 답변에서 멈춘다</strong>
첫 답변이 마음에 안 들면 "더 간결하게", "예시 추가해줘", "다른 방향으로" 하고 이어서 요청하면 됩니다. 대화를 이어가면서 점점 원하는 결과로 다듬어가는 게 Claude를 잘 쓰는 방식입니다.</p>
<p><strong>3. 사실 확인을 안 한다</strong>
Claude가 틀린 정보를 자신감 있게 말하는 경우가 있습니다. 특히 최신 정보나 구체적인 수치는 꼭 직접 확인하세요.</p>
<hr>
<p>처음엔 그냥 대화하는 것처럼 쓰다가, 팁들을 하나씩 적용해보면 체감 차이가 납니다. 일단 써보면서 감을 잡는 게 제일 빠릅니다.</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[Claude]]></category>
      <category><![CDATA[AI]]></category>
      <category><![CDATA[프롬프트]]></category>
      <category><![CDATA[AI활용]]></category>
      <category><![CDATA[Anthropic]]></category>
    </item>

    <item>
      <title><![CDATA[앤버닉 RG40XXV 리뷰 — 4인치 수직형, 이 가격에 이 화면이라고?]]></title>
      <link>https://www.stragos.xyz/posts/anbernic-rg40xxv-review</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/anbernic-rg40xxv-review</guid>
      <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[수직형 레트로 게임기의 새 기준을 세웠다는 앤버닉 RG40XXV, 직접 써보고 솔직하게 적었습니다. 화면은 진짜 예쁩니다.]]></description>
      <content:encoded><![CDATA[<h2 id="왜-또-수직형인가"><a class="anchor" aria-hidden="true" tabindex="-1" href="#왜-또-수직형인가">#</a>왜 또 수직형인가</h2>
<p>앤버닉이 2024년 한 해 동안 쏟아낸 신제품이 11종이 넘는다.</p>
<p>솔직히 말하면 "또 나왔어?" 싶었다. 그런데 RG40XXV 화면 사진을 보는 순간 멈칫했다. 수직형에 4인치 IPS, 게다가 가격이 7만 원대라고. 이게 맞나 싶어서 질러봤다.</p>
<p><img src="/images/rg40xxv/indigo.jpg" alt="앤버닉 RG40XXV 인디고 컬러" loading="lazy" decoding="async"></p>
<hr>
<h2 id="스펙-한눈에-보기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#스펙-한눈에-보기">#</a>스펙 한눈에 보기</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>사양</th>
</tr>
</thead>
<tbody>
<tr>
<td>화면</td>
<td>4.0인치 IPS, 640×480, 4:3 비율, OCA 풀 라미네이션</td>
</tr>
<tr>
<td>AP</td>
<td>Allwinner H700 (Cortex-A53 쿼드코어 1.5GHz)</td>
</tr>
<tr>
<td>GPU</td>
<td>Mali-G31 MP2</td>
</tr>
<tr>
<td>RAM</td>
<td>LPDDR4 1GB</td>
</tr>
<tr>
<td>배터리</td>
<td>3,200mAh / 최대 6시간</td>
</tr>
<tr>
<td>무선</td>
<td>Wi-Fi 802.11ac (2.4/5GHz), Bluetooth 4.2</td>
</tr>
<tr>
<td>포트</td>
<td>USB-C (충전), Micro HDMI (TV 출력), 3.5mm 이어폰</td>
</tr>
<tr>
<td>저장</td>
<td>microSD 슬롯 2개 (최대 512GB)</td>
</tr>
<tr>
<td>가격</td>
<td>약 67,000~90,000원 (용량별 상이)</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="디자인--게임보이-감성-그대로"><a class="anchor" aria-hidden="true" tabindex="-1" href="#디자인--게임보이-감성-그대로">#</a>디자인 — 게임보이 감성 그대로</h2>
<p>폼팩터가 딱 게임보이다. 수직형 클래식 레이아웃에 아날로그 스틱이 왼쪽 하단에 달려 있는 구조인데, 손에 쥐었을 때 위화감이 없다.</p>
<p>색상은 세 가지다. <strong>화이트, 투명 블랙, 인디고 블루.</strong></p>
<p><img src="/images/rg40xxv/white.jpg" alt="앤버닉 RG40XXV 화이트 컬러" loading="lazy" decoding="async"></p>
<p>인디고 블루를 골랐는데, 사진보다 실물이 훨씬 예쁘다. 뒷면이 평평해서 처음엔 손 피로가 좀 걸리는데, 며칠 지나면 적응된다.</p>
<p>버튼 조작감은 <strong>클리키하면서 적당히 탄성이 있다.</strong> 이전 세대 앤버닉 제품들과 비교하면 확실히 개선된 느낌이다. 다만 Start/Select 버튼이 셸 안으로 약간 파고드는 구조라 장시간 사용하면 걸리는 경우가 있다.</p>
<p>L2/R2 트리거가 너무 민감하다는 이야기가 리뷰마다 나온다. 실제로 처음엔 헐겁다 싶을 만큼 가볍게 눌렸는데, 게임 자체에서는 큰 문제가 되진 않았다. 액션 게임을 주로 하는 편이 아니라서일 수도 있다.</p>
<p><img src="/images/rg40xxv/black.jpg" alt="앤버닉 RG40XXV 투명 블랙 컬러" loading="lazy" decoding="async"></p>
<hr>
<h2 id="화면--이-가격에-이게-말이-돼"><a class="anchor" aria-hidden="true" tabindex="-1" href="#화면--이-가격에-이게-말이-돼">#</a>화면 — 이 가격에 이게 말이 돼?</h2>
<p>솔직히 말해서 <strong>화면이 이 기기의 전부다.</strong></p>
<p>4인치 IPS, 640×480, 4:3 비율. 수치만 보면 대단할 것도 없는데, OCA 풀 라미네이션 처리가 들어가면서 체감이 확 달라진다. 유리와 패널 사이 공기층이 없어서 색감이 바로 눈 앞에 붙어 있는 것처럼 선명하다.</p>
<p>GBA, SNES, 패미컴 타이틀을 켜면 화면이 꽉 차면서 4:3 비율이 딱 맞아 떨어진다. 필러박스(검은 테두리) 없이 출력되는 그 느낌이 작지만 중요한 차이다.</p>
<p>밝기도 실외에서 무난하게 볼 수 있는 수준이고, 시야각도 IPS답게 어느 방향에서 봐도 색이 안 변한다.</p>
<p>3.5인치에서 4인치로의 반 인치 차이가 별거 아닌 것 같아도, 실제로 비교해보면 <strong>가독성과 몰입감이 체감으로 다르다.</strong> 이게 RG40XXV를 사는 이유다.</p>
<hr>
<h2 id="에뮬레이션-성능"><a class="anchor" aria-hidden="true" tabindex="-1" href="#에뮬레이션-성능">#</a>에뮬레이션 성능</h2>
<p>칩셋은 Allwinner H700으로, 이미 여러 앤버닉 기기에 쓰인 것이다. 고성능은 아니지만 검증된 칩셋이다.</p>
<table>
<thead>
<tr>
<th>플랫폼</th>
<th>구동 상태</th>
</tr>
</thead>
<tbody>
<tr>
<td>패미컴 / 슈퍼패미컴 / 게임보이 / GBA</td>
<td>완벽</td>
</tr>
<tr>
<td>메가드라이브 / 게임기어 / PC엔진</td>
<td>완벽</td>
</tr>
<tr>
<td>PlayStation 1</td>
<td>완벽</td>
</tr>
<tr>
<td>닌텐도 DS</td>
<td>완벽</td>
</tr>
<tr>
<td>닌텐도 64</td>
<td>대부분 양호, 무거운 타이틀은 프레임 드랍</td>
</tr>
<tr>
<td>드림캐스트</td>
<td>양호, 3D 집중 타이틀은 최적화 필요</td>
</tr>
<tr>
<td>PSP</td>
<td>미흡 — 기대 낮추는 게 맞음</td>
</tr>
<tr>
<td>새턴</td>
<td>불안정</td>
</tr>
</tbody>
</table>
<p><strong>PS1까지는 편안하게 즐길 수 있다.</strong> 포켓몬스터, 파이널 판타지 VII, 록맨 X 시리즈 같은 타이틀은 아무 설정 없이 바로 돌아간다.</p>
<p>N64는 젤다의 전설 오카리나, 마리오 64 같은 타이틀은 잘 돌아가지만 배틀필드나 레이싱 계열 타이틀에서는 가끔 끊긴다. PSP는 2D 중심 타이틀은 되는 게 있고 안 되는 게 있어서 복불복이다. 드래곤볼 Z 버스트 리밋 같은 3D PSP 게임은 무리다.</p>
<hr>
<h2 id="커스텀-펌웨어"><a class="anchor" aria-hidden="true" tabindex="-1" href="#커스텀-펌웨어">#</a>커스텀 펌웨어</h2>
<p>기본 OS도 쓸 만하지만, <strong>Knulli나 muOS로 올리면 체감이 완전히 달라진다.</strong></p>
<p>두 펌웨어 모두 RG40XXV를 공식 지원한다. muOS는 인터페이스가 깔끔하고 RetroAchievements 연동이 편하다. Knulli는 설정 유연성이 높아서 고급 사용자에게 맞다.</p>
<p>커스텀 펌웨어 설치가 처음이라면 아래 영상이 잘 정리되어 있다.</p>
<p>▶ <a href="https://www.youtube.com/results?search_query=RG40XXV+muOS+custom+firmware">RetroHandhelds YouTube — muOS / Knulli 설치 가이드</a></p>
<hr>
<h2 id="배터리--편의-기능"><a class="anchor" aria-hidden="true" tabindex="-1" href="#배터리--편의-기능">#</a>배터리 &#x26; 편의 기능</h2>
<p>3,200mAh 배터리로 GBA 게임 기준 <strong>5-6시간</strong>은 무난하게 간다. Wi-Fi를 켜면 약간 줄어들지만 그래도 4시간 이상은 나온다.</p>
<p>충전은 USB-C라 요즘 충전기 그냥 쓰면 되고, <strong>Micro HDMI로 TV에 연결하는 것도 된다.</strong> 소파에 앉아서 큰 화면으로 레트로 게임 하고 싶을 때 쓸 수 있다.</p>
<p>Wi-Fi + RetroAchievements 연동도 지원된다. 포켓몬 클리어, 록맨 노데미지 같은 도전과제 모드로 즐기면 중독성이 꽤 있다.</p>
<hr>
<h2 id="아쉬운-점"><a class="anchor" aria-hidden="true" tabindex="-1" href="#아쉬운-점">#</a>아쉬운 점</h2>
<p>좋은 것만 쓰면 광고글이니까 솔직하게:</p>
<ol>
<li><strong>L2/R2 트리거가 너무 가볍다</strong> — 레이싱이나 슈팅 게임에서 오작동이 간간이 생긴다. 익숙해지긴 하지만.</li>
<li><strong>뒷면이 평평하다</strong> — 장시간 잡고 있으면 손이 피로해진다. 그립감보다 디자인을 택한 느낌.</li>
<li><strong>Start/Select 버튼 위치</strong> — 셸에 약간 파고드는 구조라 빠르게 누를 때 걸린다.</li>
<li><strong>PSP는 기대하지 말 것</strong> — 타이틀에 따라 다르긴 한데, 메인 기기로는 무리다.</li>
<li><strong>H700 칩셋 포화 상태</strong> — 2024년에 이 칩셋을 쓴 기기가 얼마나 많은지 알면... 그래도 검증된 칩셋이긴 하다.</li>
</ol>
<hr>
<h2 id="총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#총평">#</a>총평</h2>
<p><strong>솔직한 점수: 8.0 / 10</strong></p>
<p>7만 원 초반에 4인치 OCA 풀 라미네이션 화면, PS1/DS까지 완벽한 에뮬, 커스텀 펌웨어 지원. 이 가격대에서 이 정도 화면을 가진 수직형 기기는 달리 없다.</p>
<p>혁신적이진 않다. 칩셋도 오래됐고, 앤버닉이 2024년에 비슷한 기기를 10개 넘게 쏟아낸 것도 사실이다. 그러나 <strong>"GBA부터 PS1까지를 예쁜 화면으로 편하게 즐기고 싶다"</strong> 는 목적 하나로 보면, 이 가격대 최선의 선택이다.</p>
<p>게임보이 감성 그리운 분들, 수직형 좋아하는 분들, 무조건 화면 크고 선명한 게 좋은 분들한테 추천한다.</p>
<hr>
<blockquote>
<p>이 포스팅은 순수 개인 구매 후기입니다. 협찬 없음, 뒷광고 없음.</p>
</blockquote>]]></content:encoded>
      <category><![CDATA[리뷰]]></category>
      <category><![CDATA[앤버닉]]></category>
      <category><![CDATA[RG40XXV]]></category>
      <category><![CDATA[레트로게임기]]></category>
      <category><![CDATA[휴대용게임기]]></category>
      <category><![CDATA[구매후기]]></category>
    </item>

    <item>
      <title><![CDATA[Spring Boot 성능이 느릴 때 바로 의심해야 할 5가지 — 실무 디버깅 가이드]]></title>
      <link>https://www.stragos.xyz/posts/spring_boot_performance_guide</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/spring_boot_performance_guide</guid>
      <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
      <description><![CDATA[Spring Boot API가 갑자기 느려졌을 때 어디부터 봐야 하는지, 실무 기준으로 가장 효과적인 점검 포인트를 정리했습니다.]]></description>
      <content:encoded><![CDATA[<h2 id="왜-갑자기-느려지는-걸까"><a class="anchor" aria-hidden="true" tabindex="-1" href="#왜-갑자기-느려지는-걸까">#</a>왜 갑자기 느려지는 걸까?</h2>
<p>Spring Boot는 기본적으로 빠릅니다. 그래서 "느려졌다"는 건 거의 항상 <strong>내부 어딘가가 병목이 된 상태</strong>입니다.</p>
<p>문제는 대부분 코드 한 줄이 아니라<br>
DB, 네트워크, 캐시, 스레드 같은 구조적인 요소에서 발생합니다.</p>
<hr>
<h2 id="1-db-쿼리--가장-먼저-의심해야-하는-구간"><a class="anchor" aria-hidden="true" tabindex="-1" href="#1-db-쿼리--가장-먼저-의심해야-하는-구간">#</a>1. DB 쿼리 — 가장 먼저 의심해야 하는 구간</h2>
<p>실무에서 성능 문제의 대부분은 DB에서 발생합니다.</p>
<p>특히 이런 쿼리는 위험 신호입니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="sql" data-theme="github-dark"><code data-language="sql" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">SELECT</span><span style="color:#F97583"> *</span><span style="color:#F97583"> FROM</span><span style="color:#E1E4E8"> orders </span><span style="color:#F97583">WHERE</span><span style="color:#E1E4E8"> user_id </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 10</span><span style="color:#E1E4E8">;</span></span></code></pre></figure>
<p>겉보기엔 문제 없어 보이지만 데이터가 많아지면 인덱스 유무에 따라 성능이 급격히 달라집니다.</p>
<p>핵심은 단순합니다.<br>
인덱스를 타지 못하는 순간 성능은 데이터 양에 비례해서 무너집니다.</p>
<h3 id="체크-포인트"><a class="anchor" aria-hidden="true" tabindex="-1" href="#체크-포인트">#</a>체크 포인트</h3>
<ul>
<li>인덱스가 실제로 사용되는지 (<code>EXPLAIN</code>)</li>
<li>불필요한 <code>SELECT *</code> 사용 여부</li>
<li>N+1 쿼리 발생 여부 (JPA / MyBatis 모두 포함)</li>
</ul>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="sql" data-theme="github-dark"><code data-language="sql" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">EXPLAIN </span><span style="color:#F97583">SELECT</span><span style="color:#F97583"> *</span><span style="color:#F97583"> FROM</span><span style="color:#E1E4E8"> orders </span><span style="color:#F97583">WHERE</span><span style="color:#E1E4E8"> user_id </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 10</span><span style="color:#E1E4E8">;</span></span></code></pre></figure>
<hr>
<h2 id="2-mybatis-batch-미사용"><a class="anchor" aria-hidden="true" tabindex="-1" href="#2-mybatis-batch-미사용">#</a>2. MyBatis batch 미사용</h2>
<p>대량 insert/update에서 가장 흔한 실수입니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">for</span><span style="color:#E1E4E8"> (Item item </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> items) {</span></span>
<span data-line=""><span style="color:#E1E4E8">    mapper.</span><span style="color:#B392F0">insert</span><span style="color:#E1E4E8">(item);</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>이 방식은 DB를 N번 호출하는 구조입니다.</p>
<h3 id="개선-방법"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개선-방법">#</a>개선 방법</h3>
<p>MyBatis <code>&#x3C;foreach></code>로 단일 쿼리로 묶어서 처리합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="xml" data-theme="github-dark"><code data-language="xml" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">insert</span><span style="color:#B392F0"> id</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"batchInsert"</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">  INSERT INTO orders (id, user_id, amount)</span></span>
<span data-line=""><span style="color:#E1E4E8">  VALUES</span></span>
<span data-line=""><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">foreach</span><span style="color:#B392F0"> collection</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"list"</span><span style="color:#B392F0"> item</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"item"</span><span style="color:#B392F0"> separator</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">","</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">    (#{item.id}, #{item.userId}, #{item.amount})</span></span>
<span data-line=""><span style="color:#E1E4E8">  &#x3C;/</span><span style="color:#85E89D">foreach</span><span style="color:#E1E4E8">></span></span>
<span data-line=""><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#85E89D">insert</span><span style="color:#E1E4E8">></span></span></code></pre></figure>
<p>또는 JDBC batch 처리:</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">try</span><span style="color:#E1E4E8"> (SqlSession session </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> sqlSessionFactory.</span><span style="color:#B392F0">openSession</span><span style="color:#E1E4E8">(ExecutorType.BATCH)) {</span></span>
<span data-line=""><span style="color:#F97583">    for</span><span style="color:#E1E4E8"> (Item item </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> items) {</span></span>
<span data-line=""><span style="color:#E1E4E8">        session.</span><span style="color:#B392F0">getMapper</span><span style="color:#E1E4E8">(ItemMapper.class).</span><span style="color:#B392F0">insert</span><span style="color:#E1E4E8">(item);</span></span>
<span data-line=""><span style="color:#E1E4E8">    }</span></span>
<span data-line=""><span style="color:#E1E4E8">    session.</span><span style="color:#B392F0">flushStatements</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>성능 차이는 단순 개선 수준이 아니라<br>
수 초 → 수 ms로 줄어드는 경우도 많습니다.</p>
<hr>
<h2 id="3-redis-없는-구조-캐싱-부재"><a class="anchor" aria-hidden="true" tabindex="-1" href="#3-redis-없는-구조-캐싱-부재">#</a>3. Redis 없는 구조 (캐싱 부재)</h2>
<p>같은 데이터를 계속 DB에서 조회하고 있다면 구조 문제입니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">GetMapping</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">"/user/{id}"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> User </span><span style="color:#B392F0">getUser</span><span style="color:#E1E4E8">(@</span><span style="color:#F97583">PathVariable</span><span style="color:#E1E4E8"> Long id) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> userService.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id);  </span><span style="color:#6A737D">// 매 요청마다 DB 조회</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>이 구조는 트래픽이 늘어날수록 DB가 그대로 부담을 받습니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="text" data-theme="github-dark"><code data-language="text" data-theme="github-dark" style="display: grid;"><span data-line=""><span>Client → Redis (캐시 히트) → 응답</span></span>
<span data-line=""><span>Client → Redis (캐시 미스) → DB → Redis 저장 → 응답</span></span></code></pre></figure>
<p>캐싱만 제대로 적용해도 DB 부하는 크게 줄어듭니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Cacheable</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">value</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> "user"</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">key</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> "#id"</span><span style="color:#E1E4E8">)</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#E1E4E8"> User </span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(Long id) {</span></span>
<span data-line=""><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> userRepository.</span><span style="color:#B392F0">findById</span><span style="color:#E1E4E8">(id).</span><span style="color:#B392F0">orElseThrow</span><span style="color:#E1E4E8">();</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<hr>
<h2 id="4-트랜잭션-범위-과도하게-큰-경우"><a class="anchor" aria-hidden="true" tabindex="-1" href="#4-트랜잭션-범위-과도하게-큰-경우">#</a>4. 트랜잭션 범위 과도하게 큰 경우</h2>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Transactional</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> process</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#B392F0">    step1</span><span style="color:#E1E4E8">();  </span><span style="color:#6A737D">// 외부 API 호출</span></span>
<span data-line=""><span style="color:#B392F0">    step2</span><span style="color:#E1E4E8">();  </span><span style="color:#6A737D">// 파일 처리</span></span>
<span data-line=""><span style="color:#B392F0">    step3</span><span style="color:#E1E4E8">();  </span><span style="color:#6A737D">// DB 저장</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<p>문제는 "전체를 하나의 트랜잭션으로 묶는 것"입니다.</p>
<ul>
<li>락 유지 시간 증가</li>
<li>커넥션 점유 시간 증가</li>
<li>동시 요청 처리량 감소</li>
</ul>
<h3 id="개선-방법-1"><a class="anchor" aria-hidden="true" tabindex="-1" href="#개선-방법-1">#</a>개선 방법</h3>
<p>DB 작업이 필요한 구간에만 <code>@Transactional</code>을 최소 범위로 적용합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="java" data-theme="github-dark"><code data-language="java" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> process</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#B392F0">    step1</span><span style="color:#E1E4E8">();          </span><span style="color:#6A737D">// 트랜잭션 불필요 — 외부 API</span></span>
<span data-line=""><span style="color:#B392F0">    step2</span><span style="color:#E1E4E8">();          </span><span style="color:#6A737D">// 트랜잭션 불필요 — 파일 처리</span></span>
<span data-line=""><span style="color:#B392F0">    step3Transactional</span><span style="color:#E1E4E8">();  </span><span style="color:#6A737D">// 트랜잭션 필요 — DB 저장만</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span>
<span data-line=""> </span>
<span data-line=""><span style="color:#E1E4E8">@</span><span style="color:#F97583">Transactional</span></span>
<span data-line=""><span style="color:#F97583">public</span><span style="color:#F97583"> void</span><span style="color:#B392F0"> step3Transactional</span><span style="color:#E1E4E8">() {</span></span>
<span data-line=""><span style="color:#6A737D">    // DB 저장 로직</span></span>
<span data-line=""><span style="color:#E1E4E8">}</span></span></code></pre></figure>
<hr>
<h2 id="5-tomcat-스레드-풀-기본값"><a class="anchor" aria-hidden="true" tabindex="-1" href="#5-tomcat-스레드-풀-기본값">#</a>5. Tomcat 스레드 풀 기본값</h2>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="yaml" data-theme="github-dark"><code data-language="yaml" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#85E89D">server</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">  tomcat</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">    threads</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">      max</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">200</span><span style="color:#6A737D">       # 기본값</span></span>
<span data-line=""><span style="color:#85E89D">      min-spare</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">10</span></span></code></pre></figure>
<p>트래픽이 증가하면 이 설정이 병목이 됩니다.</p>
<ul>
<li>너무 낮으면 요청 대기 증가</li>
<li>너무 높으면 DB 커넥션 폭발</li>
</ul>
<p>HikariCP 커넥션 풀 설정과 함께 조정하는 것이 중요합니다.</p>
<figure data-rehype-pretty-code-figure=""><pre style="background-color:#24292e;color:#e1e4e8" tabindex="0" data-language="yaml" data-theme="github-dark"><code data-language="yaml" data-theme="github-dark" style="display: grid;"><span data-line=""><span style="color:#85E89D">spring</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">  datasource</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">    hikari</span><span style="color:#E1E4E8">:</span></span>
<span data-line=""><span style="color:#85E89D">      maximum-pool-size</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">20</span></span>
<span data-line=""><span style="color:#85E89D">      minimum-idle</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">5</span></span>
<span data-line=""><span style="color:#85E89D">      connection-timeout</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">3000</span></span></code></pre></figure>
<hr>
<h2 id="성능-문제를-보는-순서"><a class="anchor" aria-hidden="true" tabindex="-1" href="#성능-문제를-보는-순서">#</a>성능 문제를 보는 순서</h2>
<table>
<thead>
<tr>
<th>순서</th>
<th>점검 항목</th>
<th>주요 도구</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>DB 쿼리 (인덱스, N+1)</td>
<td>EXPLAIN, p6spy</td>
</tr>
<tr>
<td>2</td>
<td>캐시 여부</td>
<td>Redis, Spring Cache</td>
</tr>
<tr>
<td>3</td>
<td>API 응답 시간</td>
<td>Actuator, Prometheus</td>
</tr>
<tr>
<td>4</td>
<td>트랜잭션 범위</td>
<td>코드 리뷰</td>
</tr>
<tr>
<td>5</td>
<td>스레드 / 커넥션 풀</td>
<td>HikariCP 로그</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="마무리"><a class="anchor" aria-hidden="true" tabindex="-1" href="#마무리">#</a>마무리</h2>
<p>성능 최적화의 핵심은 복잡한 기술이 아니라<br>
병목 지점을 빠르게 찾는 능력입니다.</p>
<p>그리고 대부분의 문제는 새로운 기술이 아니라<br>
DB, 캐시, 쿼리 같은 기본기에서 발생합니다.</p>
<p>결국 중요한 건 "잘 짜는 코드"보다<br>
"느려진 이유를 빨리 찾는 감각"입니다.</p>]]></content:encoded>
      <category><![CDATA[개발]]></category>
      <category><![CDATA[Spring Boot]]></category>
      <category><![CDATA[성능최적화]]></category>
      <category><![CDATA[백엔드]]></category>
      <category><![CDATA[MyBatis]]></category>
      <category><![CDATA[Redis]]></category>
      <category><![CDATA[DB튜닝]]></category>
    </item>

    <item>
      <title><![CDATA[앤버닉 RG34XX SP 한 달 써본 후기 — 충동구매였는데 후회 없다]]></title>
      <link>https://www.stragos.xyz/posts/anbernic-rg34xxsp-review</link>
      <guid isPermaLink="true">https://www.stragos.xyz/posts/anbernic-rg34xxsp-review</guid>
      <pubDate>Sat, 05 Apr 2025 00:00:00 GMT</pubDate>
      <description><![CDATA[레트로 게임기 입문으로 앤버닉 RG34XX SP를 질렀습니다. GBA SP 감성에 현대 기술을 얹은 이 녀석, 솔직하게 털어놓겠습니다.]]></description>
      <content:encoded><![CDATA[<h2 id="지르게-된-사연"><a class="anchor" aria-hidden="true" tabindex="-1" href="#지르게-된-사연">#</a>지르게 된 사연</h2>
<p>솔직히 말하면 계획에 없던 구매였다.</p>
<p>유튜브 알고리즘이 어느 날 갑자기 레트로 게임기 영상을 밀어주기 시작했는데, 보다 보니 어린 시절 GBA SP 가지고 싶어서 엄마한테 졸랐던 기억이 스멀스멀 올라오는 거다. 결국 장바구니에 담고 이틀 버티다가 결제 눌렀다.</p>
<p>가격은 알리익스프레스 기준 약 <strong>6만 8천 원-7만 5천 원</strong> 사이. 쿠폰 잘 쓰면 좀 더 저렴하게 살 수 있다. 국내 레트로게임 커뮤니티(레게마갤)에서 후기를 몇 개 읽었는데 대체로 긍정적이었고, 특히 힌지가 튼튼하다는 말에 혹해서 결정했다. 미요 플립 쓰다가 힌지 나간 분들이 많아서 그게 좀 걱정이었거든.</p>
<p><img src="https://anbernic.com/cdn/shop/files/RG34XXSP_bf5e7b06-dd08-4806-b3b6-3797796be2e2.jpg?v=1759138613&#x26;width=1200" alt="앤버닉 RG34XX SP — 인디고 컬러" loading="lazy" decoding="async"></p>
<hr>
<h2 id="박스-열어보기"><a class="anchor" aria-hidden="true" tabindex="-1" href="#박스-열어보기">#</a>박스 열어보기</h2>
<p>배송은 알리 기준 약 2주 정도 걸렸다. 박스는 앤버닉 특유의 흰 바탕에 제품 사진 들어간 심플한 디자인이고, 열어보면:</p>
<ul>
<li>본체</li>
<li>USB-C 케이블</li>
<li>마이크로SD 카드 (미리 롬 세팅된 버전 구매 시 포함)</li>
<li>설명서 (영어/중국어)</li>
</ul>
<p>구성 자체는 별 거 없다. 근데 본체를 꺼내는 순간 "어, 생각보다 작네" 싶은 느낌이 온다. 실제 무게는 178g인데, 들었을 때 적당히 묵직하고 싸구려 느낌이 전혀 없다.</p>
<hr>
<h2 id="디자인--빌드퀄리티--gba-sp-닮은-꼴"><a class="anchor" aria-hidden="true" tabindex="-1" href="#디자인--빌드퀄리티--gba-sp-닮은-꼴">#</a>디자인 &#x26; 빌드퀄리티 — GBA SP 닮은 꼴</h2>
<p><img src="https://anbernic.com/cdn/shop/files/RG34XXSP_a9f1d76f-4045-488a-8c78-67a117c424a0.jpg?v=1759138613&#x26;width=1200" alt="앤버닉 RG34XX SP 색상 라인업" loading="lazy" decoding="async"></p>
<p><strong>색상 라인업</strong>: 옐로우, 그레이, 블랙, 인디고 4가지.
나는 인디고로 골랐는데, 사진으로 보는 것보다 실물이 훨씬 예쁘다. 약간 보라빛이 도는 그 색감이 진짜 GBA SP 코발트 블루 생각나게 한다.</p>
<p>폼팩터는 진짜로 GBA SP랑 거의 똑같다. 실제로 둘을 나란히 놓으면 크기가 거의 동일하다는 리뷰들이 많은데, 내가 GBA SP 원본이 없어서 비교는 못 해봤지만, 사진으로 봤을 때 진짜 판박이 수준이다.</p>
<h3 id="힌지--마그넷"><a class="anchor" aria-hidden="true" tabindex="-1" href="#힌지--마그넷">#</a>힌지 &#x26; 마그넷</h3>
<p>이게 제일 마음에 드는 부분이다. 힌지가 <strong>금속 재질</strong>이라 여닫을 때 묵직한 질감이 느껴진다. 원하는 각도에 딱 고정되고, 흔들리거나 헐거운 느낌이 없다. 닫을 때는 <strong>마그넷</strong>이 딸깍 붙으면서 동시에 자동으로 슬립 모드로 진입한다. 이게 생각보다 편하다. 통근할 때 지하철에서 잠깐 내려야 할 일 생기면 그냥 접으면 되니까.</p>
<p>미요 플립 힌지 이슈 생각하면 이 정도 빌드는 진짜 합격점이다.</p>
<h3 id="버튼--조작감"><a class="anchor" aria-hidden="true" tabindex="-1" href="#버튼--조작감">#</a>버튼 &#x26; 조작감</h3>
<p>버튼들이 전반적으로 <strong>클리키한 편</strong>이다. 처음엔 약간 딱딱하다 싶었는데 며칠 쓰다 보니 오히려 레트로 게임 하는 맛이 난다. D패드 정밀도도 나쁘지 않아서 격겜이나 플랫포머 게임에서 실수 입력이 거의 없다.</p>
<p>아날로그 스틱 2개가 달려 있는데, 솔직히 이게 장점이자 단점이다. PSP나 PS1 3D 게임 할 때는 분명히 필요한데, 클램쉘 폼팩터에 아날로그 스틱이 달리면서 전체 레이아웃이 좀 빡빡해 보이는 게 있다. 스틱 크기 자체가 작아서 장시간 조작하면 엄지가 좀 불편할 수 있다.</p>
<hr>
<h2 id="화면--진짜-예쁘다"><a class="anchor" aria-hidden="true" tabindex="-1" href="#화면--진짜-예쁘다">#</a>화면 — 진짜 예쁘다</h2>
<p><img src="https://anbernic.com/cdn/shop/files/RG34XXSP_87a11893-9691-436b-96bd-b2d53ccdb157.jpg?v=1759138613&#x26;width=1200" alt="RG34XX SP 화면 &#x26; 조작부" loading="lazy" decoding="async"></p>
<p><strong>3.4인치 IPS 패널, 720×480 해상도, 3:2 비율.</strong></p>
<p>스펙만 보면 "그게 뭐야" 싶을 수 있는데, 3:2 비율이 GBA 화면 비율이랑 정확히 맞아떨어진다. GBA 게임 켜면 화면이 꽉 차면서 필러박스(검은 테두리) 없이 딱 맞게 출력된다. 포켓몬스터 에메랄드 켰을 때 진짜 감동받았다.</p>
<p>색감은 약간 채도가 높은 편이다. 취향 탈 수 있는데 나는 선명한 거 좋아해서 만족. 야외에서도 밝기 충분하고, 시야각도 IPS답게 어느 방향에서 봐도 색이 안 변한다.</p>
<p>베젤은 좀 두꺼운 편이다. 리뷰들에서 다들 베젤 지적하는 거 보고 "설마 그렇게 두껍겠어?" 했는데, 실제로 보니 확실히 요즘 스마트폰이나 고급 핸드헬드 기기에 비하면 두껍다. 그래도 게임하다 보면 금방 적응된다.</p>
<hr>
<h2 id="성능--어디까지-돌아가냐"><a class="anchor" aria-hidden="true" tabindex="-1" href="#성능--어디까지-돌아가냐">#</a>성능 — 어디까지 돌아가냐</h2>
<p>칩셋은 <strong>Allwinner H700</strong>, RAM은 <strong>2GB LPDDR4</strong>. 고사양 에뮬레이터 기기는 아니다. 그냥 솔직하게 말하면:</p>
<table>
<thead>
<tr>
<th>플랫폼</th>
<th>구동 상태</th>
</tr>
</thead>
<tbody>
<tr>
<td>NES / SNES / GB / GBC / GBA</td>
<td>완벽</td>
</tr>
<tr>
<td>메가드라이브 / PC엔진</td>
<td>완벽</td>
</tr>
<tr>
<td>PS1</td>
<td>대부분 완벽, 일부 무거운 타이틀 살짝 버벅</td>
</tr>
<tr>
<td>PSP</td>
<td>대부분 잘 됨, 3D 중심 타이틀은 설정 필요</td>
</tr>
<tr>
<td>N64</td>
<td>최적화된 타이틀은 되는데, 안 되는 것도 꽤 있음</td>
</tr>
<tr>
<td>드림캐스트</td>
<td>가벼운 2D는 되는데 3D는 불안정</td>
</tr>
<tr>
<td>NDS</td>
<td>됨, 조금 느린 타이틀 있음</td>
</tr>
</tbody>
</table>
<p>한 마디로 <strong>PS1 이하는 편안하게 즐길 수 있고</strong>, PSP는 타이틀 골라서 해야 한다. N64부터는 기대를 낮추는 게 정신건강에 좋다.</p>
<p>나는 주로 GBA, PS1, 패미컴 시절 추억의 게임들 돌리는 용도로 사서 성능은 충분하다. 사실 이 가격대에서 GBA 에뮬이 이렇게 완벽하게 돌아가는 게 어디냐.</p>
<p><strong>커스텀 펌웨어</strong>도 이미 지원된다. Rocknix, MuOS, Knulli 다 된다. 나는 MuOS 올려서 쓰는 중인데 기본 OS보다 훨씬 쾌적하다. 커펌 잘 모른다면 그냥 기본 OS도 나쁘지는 않다.</p>
<hr>
<h2 id="배터리--편의-기능"><a class="anchor" aria-hidden="true" tabindex="-1" href="#배터리--편의-기능">#</a>배터리 &#x26; 편의 기능</h2>
<p>배터리는 <strong>3300mAh</strong>로, 실제 사용해보면 GBA 게임 기준 <strong>5-6시간</strong> 정도 간다. 무선 켜면 좀 줄어드는데 그래도 4시간은 넘는다. 충전은 <strong>USB-C</strong>라 요즘 스마트폰 충전기 그냥 쓰면 된다.</p>
<p>마그넷 자동 슬립 기능 덕분에 배터리 낭비가 꽤 줄어든다. 그냥 접으면 자동으로 꺼지니까 충전 없이도 하루 왔다갔다 하면서 틈틈이 즐기기 충분하다.</p>
<p>Wi-Fi랑 블루투스 4.2도 내장되어 있어서 RetroAchievements 연동도 되고, Moonlight로 PC 스트리밍도 된다. 나는 레트로어치브먼트 연동해서 포켓몬 클리어 도전 중인데 생각보다 재밌다.</p>
<hr>
<h2 id="아쉬운-점"><a class="anchor" aria-hidden="true" tabindex="-1" href="#아쉬운-점">#</a>아쉬운 점</h2>
<p>좋은 것만 쓰면 뭔가 홍보글 같으니 솔직하게:</p>
<ol>
<li><strong>베젤이 두껍다</strong> — 화면 자체는 예쁜데 베젤이 약간 구리다. 익숙해지긴 하지만 처음엔 눈에 걸린다.</li>
<li><strong>아날로그 스틱이 작다</strong> — 레이아웃상 어쩔 수 없는 것 같은데, 장시간 PSP 게임하다 보면 엄지 불편하다.</li>
<li><strong>N64는 복불복</strong> — 기대하고 오는 사람들은 실망할 수 있다.</li>
<li><strong>국내 AS는 없다</strong> — 알리에서 사는 거라 문제 생기면 셀러랑 싸워야 한다. 아직 이상 없긴 한데 마음 한 켠에 걸린다.</li>
</ol>
<hr>
<h2 id="총평"><a class="anchor" aria-hidden="true" tabindex="-1" href="#총평">#</a>총평</h2>
<p><img src="https://anbernic.com/cdn/shop/files/RG34XXSP.jpg?v=1759138613&#x26;width=1200" alt="앤버닉 RG34XX SP 블랙" loading="lazy" decoding="async"></p>
<p><strong>한 달 쓴 솔직한 점수: 8.5 / 10</strong></p>
<p>7만 원 초반 가격에 이 정도 빌드와 화면, 레트로 에뮬 성능이면 진짜 가성비다. GBA SP 감성 그리워하는 분들이나, 버스·지하철 같은 이동 중에 레트로 게임 즐기고 싶은 분들한테 강력 추천한다.</p>
<p>다만 고성능 에뮬레이터 기기를 원하거나 N64, 드림캐스트 메인으로 즐기려는 분들은 상위 기기를 보는 게 맞다. 이건 그냥 GBA~PS1 시대의 게임을 제대로 즐기기 위한 기기다. 그 목적에서만큼은 이 가격대 최고라고 생각한다.</p>
<p>충동구매였는데, 후회는 없다. 오히려 더 일찍 살걸 싶은 그런 물건.</p>
<hr>
<blockquote>
<p>이 포스팅은 순수 개인 구매 후기입니다. 협찬 없음, 뒷광고 없음.</p>
</blockquote>]]></content:encoded>
      <category><![CDATA[리뷰]]></category>
      <category><![CDATA[앤버닉]]></category>
      <category><![CDATA[레트로게임기]]></category>
      <category><![CDATA[RG34XXSP]]></category>
      <category><![CDATA[휴대용게임기]]></category>
      <category><![CDATA[구매후기]]></category>
    </item>
  </channel>
</rss>