<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jun.log</title>
        <link>https://velog.io/</link>
        <description>나의 기록</description>
        <lastBuildDate>Fri, 29 May 2026 08:52:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jun.log</title>
            <url>https://velog.velcdn.com/images/jun-log/profile/bae46d47-9490-48f3-83cf-d5aa0c5182d0/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jun.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jun-log" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[DB]튜플 비교]]></title>
            <link>https://velog.io/@jun-log/DB%ED%8A%9C%ED%94%8C-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@jun-log/DB%ED%8A%9C%ED%94%8C-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Fri, 29 May 2026 08:52:39 GMT</pubDate>
            <description><![CDATA[<h2 id="1-튜플-비교tuple-comparison란">1. 튜플 비교(Tuple Comparison)란?</h2>
<p>튜플(Tuple)은 데이터베이스에서 하나의 행(Row) 또는 여러 컬럼의 묶음을 의미합니다.
튜플 비교는 <strong>여러 개의 컬럼 값을 하나의 단위로 묶어서, 다른 값의 묶음과 한 번에 비교</strong>하는 문법입니다.</p>
<p>가장 기본적인 형태는 다음과 같습니다.</p>
<pre><code class="language-sql">-- 기존 방식
WHERE col1 = 10 AND col2 = 20

-- 튜플 비교 방식
WHERE (col1, col2) = (10, 20)
</code></pre>
<p>위와 같이 <code>(</code>와 <code>)</code>로 컬럼들을 묶어 하나의 세트처럼 비교할 수 있습니다. 동등 비교(<code>=</code>)에서는 기존 방식과 큰 차이가 없어 보이지만, 부등호(&gt;, &lt;)를 사용할 때 튜플 비교의 진가가 발휘됩니다.</p>
<hr>
<h2 id="2-부등호-비교에서의-동작-원리-핵심-⭐">2. 부등호 비교에서의 동작 원리 (핵심 ⭐)</h2>
<p>튜플 비교에서 <code>&gt;</code> 나 <code>&lt;</code> 같은 부등호를 사용할 때, 데이터베이스는 <strong>왼쪽 컬럼부터 오른쪽 컬럼 순서대로(Lexicographical order, 사전식 순서)</strong> 값을 평가합니다.</p>
<p>가장 헷갈리기 쉬운 <code>(A, B) &gt; (C, D)</code> 의 동작 원리를 풀어서 설명해 보겠습니다.</p>
<h3 id="💡-a-b--c-d-의-논리적-의미">💡 <code>(A, B) &gt; (C, D)</code> 의 논리적 의미</h3>
<p>이 조건은 다음의 복합 조건과 완전히 동일하게 동작합니다.</p>
<pre><code class="language-sql">WHERE A &gt; C 
   OR (A = C AND B &gt; D)
</code></pre>
<p><strong>해석하자면:</strong></p>
<ol>
<li>먼저 첫 번째 값인 <code>A</code>와 <code>C</code>를 비교합니다. <code>A</code>가 <code>C</code>보다 크면, 두 번째 값(<code>B</code>, <code>D</code>)은 <strong>볼 필요도 없이</strong> 조건이 참(True)이 됩니다.</li>
<li>만약 <code>A</code>와 <code>C</code>가 같다면, 그제야 두 번째 값인 <code>B</code>와 <code>D</code>를 비교하여 <code>B</code>가 <code>D</code>보다 큰지 확인합니다.</li>
</ol>
<p>이러한 원리는 3개 이상의 컬럼을 묶을 때도 동일하게 확장됩니다.</p>
<ul>
<li><code>(A, B, C) &gt; (X, Y, Z)</code></li>
<li>$\Leftrightarrow$ <code>A &gt; X OR (A = X AND B &gt; Y) OR (A = X AND B = Y AND C &gt; Z)</code></li>
</ul>
<hr>
<h2 id="3-실무-활용-예시-커서-기반-페이지네이션-no-offset">3. 실무 활용 예시: 커서 기반 페이지네이션 (No Offset)</h2>
<p>튜플 비교가 실무에서 가장 빛을 발하는 순간은 커서 기반 페이지네이션(Cursor-based Pagination)을 구현할 때입니다.</p>
<p>예를 들어, 게시판에서 &quot;조회수(view_count)가 높은 순, 조회수가 같다면 최신 글(id) 순&quot;으로 정렬된 데이터를 페이징 처리한다고 가정해 보겠습니다.</p>
<p>이전 페이지의 마지막 게시글 정보가 <code>조회수: 100</code>, <code>ID: 50</code> 이었다면, 다음 페이지를 조회하기 위해 어떻게 쿼리를 짜야 할까요?</p>
<h3 id="❌-기존-방식-and-or-조합">❌ 기존 방식 (AND, OR 조합)</h3>
<pre><code class="language-sql">SELECT *
  FROM board
 WHERE view_count &lt; 100 
    OR (view_count = 100 AND id &lt; 50)
 ORDER BY view_count DESC, id DESC
 LIMIT 10;
</code></pre>
<p>조건이 길어지고, 괄호가 들어가면서 쿼리를 한눈에 파악하기 어렵습니다. 만약 정렬 조건이 3개로 늘어난다면 <code>WHERE</code> 절은 훨씬 더 복잡해집니다.</p>
<h3 id="⭕-튜플-비교-적용">⭕ 튜플 비교 적용</h3>
<pre><code class="language-sql">SELECT *
  FROM board
 WHERE (view_count, id) &lt; (100, 50)
 ORDER BY view_count DESC, id DESC
 LIMIT 10;
</code></pre>
<p>쿼리가 놀랍도록 간결해졌습니다! <code>(view_count, id)</code>를 하나의 커서(Cursor) 튜플로 취급하여, 이전 커서보다 &#39;작은&#39; 데이터들을 가져오라는 의미가 직관적으로 전달됩니다.</p>
<hr>
<h2 id="4-인덱스index-활용과-성능">4. 인덱스(Index) 활용과 성능</h2>
<p>튜플 비교를 사용한다고 해서 성능이 떨어지지는 않습니다. 오히려 복합 인덱스(Composite Index)가 적절히 구성되어 있다면 매우 효율적으로 동작합니다.</p>
<p>위의 게시판 예시에서 <code>(view_count, id)</code> 로 복합 인덱스가 걸려있다면, 데이터베이스의 옵티마이저는 튜플 비교 조건을 보고 인덱스를 순차적으로(Range Scan) 잘 타게 됩니다. 기존의 <code>OR</code> 조건보다 실행 계획을 최적화하기 더 유리한 경우가 많습니다.</p>
<blockquote>
<p><strong>주의사항:</strong> 다만, 데이터베이스 제품이나 버전에 따라 <code>IN</code> 절 내에서 튜플 비교를 사용하거나, 특정 복잡한 조건이 섞일 경우 인덱스를 제대로 타지 못하는 엣지 케이스가 있을 수 있으므로 실행 계획(Explain Plan)을 확인하는 습관을 들이는 것이 좋습니다.</p>
</blockquote>
<hr>
<h2 id="5-데이터베이스-지원-여부">5. 데이터베이스 지원 여부</h2>
<p>튜플 비교는 <strong>SQL-92 표준</strong>에 정의된 문법이므로, 현대의 대부분의 RDBMS에서 지원합니다.</p>
<ul>
<li><strong>Oracle:</strong> 9i 이상부터 완벽하게 지원 (12c, 19c 등 최신 버전 모두 원활하게 동작)</li>
<li><strong>MySQL:</strong> 5.7 이상, 8.0에서 잘 지원됨</li>
<li><strong>PostgreSQL:</strong> 완벽하게 지원</li>
</ul>
<p>단, SQL Server(MSSQL)의 경우 버전에 따라 튜플 비교 문법 지원이 제한적이거나 불완전할 수 있으므로, MSSQL 환경이라면 기존의 <code>AND/OR</code> 방식을 사용해야 할 수도 있습니다.</p>
<hr>
<h2 id="📝-요약">📝 요약</h2>
<ol>
<li><strong>튜플 비교</strong>는 여러 컬럼을 <code>(A, B)</code> 형태로 묶어 비교하는 SQL 표준 문법이다.</li>
<li>부등호(<code>&gt;</code>, <code>&lt;</code>) 비교 시 사전식 순서(Lexicographical order)로 논리가 전개된다.</li>
<li>쿼리의 <strong>가독성</strong>을 극대화하며, 복잡한 <code>AND/OR</code> 조건을 깔끔하게 대체할 수 있다.</li>
<li>커서 기반 페이지네이션(무한 스크롤 등)을 구현할 때 특히 유용하다.</li>
</ol>
<p>복잡한 다중 정렬 조건으로 인해 쿼리가 지저분해져서 고민이었다면, 지금 바로 튜플 비교를 도입해 보세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[대량데이터 페이지네이션기법]]></title>
            <link>https://velog.io/@jun-log/%EB%8C%80%EB%9F%89%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98%EA%B8%B0%EB%B2%95</link>
            <guid>https://velog.io/@jun-log/%EB%8C%80%EB%9F%89%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98%EA%B8%B0%EB%B2%95</guid>
            <pubDate>Fri, 29 May 2026 05:32:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<h2 id="페이지네이션이란">페이지네이션이란?</h2>
<p>전체 결과 집합을 페이지 단위로 나눠, 요청된 페이지에 해당하는 행만 조회하는것을 말한다.</p>
<p>가장 쉬운 페이지네이션은 OFFSET을 이용한 페이지네이션이다.</p>
<pre><code class="language-sql">SELECT 
    *
FROM
    table
WHERE 
    ~
LIMIT &lt;페이지당 보여줄 개수&gt; OFFSET (&lt;현재 페이지&gt; - 1) * &lt;페이지당 보여줄 개수&gt;</code></pre>
<p>OFFSET 키워드는 <strong>SELECT 결과에서 앞쪽 N개 행을 읽어 버리고</strong>, 그다음 행부터 반환하게 한다.</p>
<h2 id="1-offset의-문제">1. OFFSET의 문제</h2>
<pre><code class="language-sql">SELECT * FROM posts
ORDER BY created_at DESC
OFFSET 100000 LIMIT 20;</code></pre>
<p><code>OFFSET 100000</code>은 조건에 맞는 행을 <strong>처음부터 100,000건 읽어서 전부 버린 뒤</strong>, 그다음 20건을 반환한다.
<strong>즉 버려질 행도 전부 읽어야</strong> 하므로 비용이 OFFSET 값에 비례해서 커진다.
1페이지는 빠르지만 5000페이지는 느린 이유가 이것이다.</p>
<hr>
<h2 id="해결방안">해결방안</h2>
<h3 id="1-지연-조인-deferred-join--late-row-lookup">1. 지연 조인 (Deferred Join / Late Row Lookup)</h3>
<h3 id="아이디어">아이디어</h3>
<p>버려질 행에 대해서는 <strong>무거운 본문 데이터까지 읽지 말고</strong>, 좁은 <code>커버링 인덱스</code>에서 <strong>id(키)만</strong> 빠르게 얻어 스킵한다.</p>
<blockquote>
</blockquote>
<h3 id="커버링-인덱스란">커버링 인덱스란?</h3>
<p>쿼리가 필요로 하는 컬럼을 인덱스 자체가 전부 들고 있어서, 테이블 본문(힙)까지 가지 않고 인덱스만 읽어 답하는 것이 커버링 인덱스</p>
<p>최종적으로 살아남은 20건에 대해서만 본 테이블에 접근해 실제 데이터를 가져온다.</p>
<pre><code class="language-sql">SELECT p.*
FROM posts p
JOIN (
    SELECT id
    FROM posts
    ORDER BY created_at DESC
    OFFSET 100000 LIMIT 20
) AS sub ON p.id = sub.id
ORDER BY p.created_at DESC;</code></pre>
<h3 id="반드시-짚을-점-오해-주의">반드시 짚을 점 (오해 주의)</h3>
<p>지연 조인은 <strong>여전히 100,000개의 인덱스 엔트리를 스캔해서 버린다.</strong>
따라서 시간 복잡도는 <strong>O(N) 그대로</strong>이고, 줄어드는 것은 &quot;버려질 행 1건당 비용(상수 인자)&quot;뿐이다.
페이지가 충분히 깊어지면 지연 조인도 결국 느려진다.
<strong>이건 OFFSET 회피가 아니라 OFFSET 최적화다.</strong></p>
<hr>
<h3 id="2-커서-기반--키셋-페이지네이션-keyset-pagination-seek-method">2. 커서 기반 / 키셋 페이지네이션 (Keyset Pagination, Seek Method)</h3>
<h3 id="아이디어-1">아이디어</h3>
<p>OFFSET을 아예 쓰지 않는다. 직전 페이지의 <strong>마지막 행의 정렬 기준값</strong>을 다음 페이지의 시작 조건으로 넘긴다.
인덱스를 타고 시작 위치로 곧장 &quot;점프&quot;하므로, 몇 페이지째인지와 무관하게 항상 LIMIT 건수만큼만 읽는다.</p>
<h4 id="a-정렬-기준이-고유-컬럼예-pk-id일-때">(A) 정렬 기준이 고유 컬럼(예: PK id)일 때</h4>
<pre><code class="language-sql">SELECT * FROM posts
WHERE id &lt; :last_id        -- 직전 페이지 마지막 행의 id
ORDER BY id DESC
LIMIT 20;</code></pre>
<p>이때만 &quot;마지막 id를 커서로 쓴다&quot;는 설명이 정확히 성립한다.</p>
<h4 id="b-정렬-기준이-비고유-컬럼예-created_at일-때--실전에서-더-흔함">(B) 정렬 기준이 비고유 컬럼(예: created_at)일 때 — 실전에서 더 흔함</h4>
<p>created_at은 중복될 수 있으므로 id를 <strong>타이브레이커</strong>로 함께 넘겨야 경계에서 행이 누락/중복되지 않는다.</p>
<pre><code class="language-sql">-- row-value 비교 (PostgreSQL이 복합 인덱스로 잘 최적화함)
SELECT * FROM posts
WHERE (created_at, id) &lt; (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;</code></pre>
<ul>
<li>필요한 인덱스: <code>(created_at, id)</code> (또는 <code>(created_at DESC, id DESC)</code>).</li>
<li><code>(a, b) &lt; (c, d)</code>는 <code>a &lt; c OR (a = c AND b &lt; d)</code>를 의미한다. 직접 <code>OR</code>로 풀어 써도 되지만 row-value 비교가 더 깔끔하고 인덱스 활용도 좋다.</li>
</ul>
<h4 id="한계와-적합한-ui">한계와 적합한 UI</h4>
<p>keyset의 빠른 속도는 &quot;직전 페이지의 마지막 행&quot;을 알아야 다음 페이지를 구할 수 있다는 구조에서 나온다. 즉 <strong>순차적으로만 이동</strong>할 수 있다. 이 특성이 UI 선택을 가른다.</p>
<ul>
<li><strong>페이지 번호 UI([1][2][3]...[1000])에는 부적합하다.</strong> &quot;7페이지로 바로 점프&quot;하려면 6페이지 마지막 행의 커서값이 필요한데, 그 페이지를 거치지 않으면 알 수 없다. 임의 페이지로의 직접 점프가 구조적으로 불가능하다. 또한 전체 페이지 수를 알려면 별도의 <code>COUNT(*)</code>가 필요한데, 이는 keyset이 피하려던 비용을 다시 불러온다.</li>
<li><strong>&quot;다음 / 이전&quot; 버튼이나 무한 스크롤(infinite scroll)에는 최적이다.</strong> 두 방식 모두 &quot;지금 보고 있는 마지막 행 다음&quot;만 요구하므로 keyset의 동작 방식과 정확히 맞는다. 페이지가 깊어져도 속도가 일정하다는 장점이 그대로 살아난다. SNS 피드, 타임라인, 무한 스크롤 목록이 대부분 이 방식을 쓰는 이유다.</li>
</ul>
<blockquote>
<p>정리하면, <strong>임의 페이지 점프가 필요하면 OFFSET, 순방향 탐색만 필요하면 keyset</strong>이다. 깊은 페이지에서의 성능 때문에 무조건 keyset이 정답인 것은 아니고, UI 요구사항이 먼저다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[kotlin(1)]]></title>
            <link>https://velog.io/@jun-log/kotlin1</link>
            <guid>https://velog.io/@jun-log/kotlin1</guid>
            <pubDate>Wed, 27 May 2026 02:30:28 GMT</pubDate>
            <description><![CDATA[<h1 id="이-프로젝트에서-쓰인-kotlin-문법-정리">이 프로젝트에서 쓰인 Kotlin 문법 정리</h1>
<blockquote>
<p>각 항목마다 <strong>이 프로젝트의 실제 코드</strong>를 예시로 사용합니다.</p>
</blockquote>
<hr>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#1-%EB%B3%80%EC%88%98-%EC%84%A0%EC%96%B8--val-vs-var">변수 선언 — <code>val</code> vs <code>var</code></a></li>
<li><a href="#2-%ED%83%80%EC%9E%85-%EC%B6%94%EB%A1%A0">타입 추론</a></li>
<li><a href="#3-%ED%95%A8%EC%88%98-%EC%84%A0%EC%96%B8--fun">함수 선언 — <code>fun</code></a></li>
<li><a href="#4-%ED%91%9C%ED%98%84%EC%8B%9D-%ED%95%A8%EC%88%98-%EB%8B%A8%EC%9D%BC-%ED%91%9C%ED%98%84%EC%8B%9D">표현식 함수 (단일 표현식)</a></li>
<li><a href="#5-%EA%B8%B0%EB%B3%B8-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%EA%B0%92--%EC%9D%B4%EB%A6%84-%EC%9E%88%EB%8A%94-%EC%9D%B8%EC%88%98">기본 파라미터값 &amp; 이름 있는 인수</a></li>
<li><a href="#6-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EC%A3%BC-%EC%83%9D%EC%84%B1%EC%9E%90">클래스와 주 생성자</a></li>
<li><a href="#7-data-class">data class</a></li>
<li><a href="#8-companion-object">companion object</a></li>
<li><a href="#9-null-%EC%95%88%EC%A0%84%EC%84%B1---">Null 안전성 — <code>?</code>, <code>?:</code>, <code>!!</code></a></li>
<li><a href="#10-%EB%AC%B8%EC%9E%90%EC%97%B4-%ED%85%9C%ED%94%8C%EB%A6%BF--">문자열 템플릿 — <code>$</code></a></li>
<li><a href="#11-%EB%9E%8C%EB%8B%A4%EC%99%80-%EA%B3%A0%EC%B0%A8-%ED%95%A8%EC%88%98--map-it">람다와 고차 함수 — <code>map</code>, <code>it</code></a></li>
<li><a href="#12-%EC%8A%A4%EC%BD%94%ED%94%84-%ED%95%A8%EC%88%98--also">스코프 함수 — <code>also</code></a></li>
<li><a href="#13-require--%EC%A0%84%EC%A0%9C-%EC%A1%B0%EA%B1%B4-%EA%B2%80%EC%82%AC"><code>require()</code> — 전제 조건 검사</a></li>
<li><a href="#14-mapof--to-%EC%A4%91%EC%9C%84-%ED%95%A8%EC%88%98"><code>mapOf()</code> &amp; <code>to</code> 중위 함수</a></li>
<li><a href="#15-%EB%B2%94%EC%9C%84-%EC%97%B0%EC%82%B0%EC%9E%90----in">범위 연산자 — <code>..</code> &amp; <code>in</code></a></li>
<li><a href="#16-%EB%A9%80%ED%8B%B0%EB%9D%BC%EC%9D%B8-%EB%AC%B8%EC%9E%90%EC%97%B4---trimindent">멀티라인 문자열 — <code>&quot;&quot;&quot;</code> &amp; <code>trimIndent()</code></a></li>
<li><a href="#17-%EC%88%AB%EC%9E%90-%EB%A6%AC%ED%84%B0%EB%9F%B4%EC%9D%98-%EB%B0%91%EC%A4%84--1_000_000">숫자 리터럴의 밑줄 — <code>1_000_000</code></a></li>
<li><a href="#18-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4--interface">인터페이스 — <code>interface</code></a></li>
<li><a href="#19-%EC%99%80%EC%9D%BC%EB%93%9C%EC%B9%B4%EB%93%9C-import--">와일드카드 import — <code>*</code></a></li>
<li><a href="#20-%EC%BD%94%ED%8B%80%EB%A6%B0%EC%97%90%EC%84%9C-%EC%9E%90%EB%B0%94-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%B0%B8%EC%A1%B0--classjava">코틀린에서 자바 클래스 참조 — <code>::class.java</code></a></li>
<li><a href="#21-unit-vs-void">Unit vs Void</a></li>
<li><a href="#22-%ED%9B%84%ED%96%89-%EC%89%BC%ED%91%9C-trailing-comma">후행 쉼표 (Trailing Comma)</a></li>
</ol>
<hr>
<h2 id="1-변수-선언--val-vs-var">1. 변수 선언 — <code>val</code> vs <code>var</code></h2>
<table>
<thead>
<tr>
<th>키워드</th>
<th>의미</th>
<th>Java 대응</th>
</tr>
</thead>
<tbody><tr>
<td><code>val</code></td>
<td>한 번만 할당 가능 (불변)</td>
<td><code>final</code></td>
</tr>
<tr>
<td><code>var</code></td>
<td>재할당 가능 (가변)</td>
<td>일반 변수</td>
</tr>
</tbody></table>
<pre><code class="language-kotlin">// PostService.kt
val offset = (page - 1) * size   // 이후 바꿀 일 없음 → val
val posts  = postMapper.findAll(offset, size)</code></pre>
<pre><code class="language-kotlin">// Post.kt (domain)
// MyBatis 가 리플렉션으로 값을 채워넣어야 하므로 var 사용
var id: Long = 0
var title: String = &quot;&quot;</code></pre>
<pre><code class="language-kotlin">// PostService.kt — updatePost()
// 조회 후 값을 바꿔야 하므로 var 이어야 가능
post.title   = request.title    // Post.title 이 var 이기 때문에 가능
post.content = request.content</code></pre>
<blockquote>
<p><strong>원칙</strong>: 바꿀 필요가 없으면 <code>val</code>, 바꿔야 한다면 <code>var</code>.  
DTO(요청/응답 객체)는 한 번 만들고 끝이니 <code>val</code>, 도메인 객체는 DB에서 값을 주입받아야 하니 <code>var</code>.</p>
</blockquote>
<hr>
<h2 id="2-타입-추론">2. 타입 추론</h2>
<p>Kotlin 컴파일러가 오른쪽 값을 보고 타입을 자동으로 알아냅니다.</p>
<pre><code class="language-kotlin">val offset = (page - 1) * size   // Int 로 추론
val start  = System.currentTimeMillis()  // Long 으로 추론
val posts  = postMapper.findAll(offset, size)  // List&lt;Post&gt; 로 추론</code></pre>
<p>타입을 명시할 수도 있습니다 (선택 사항).</p>
<pre><code class="language-kotlin">val offset: Int  = (page - 1) * size   // 명시적으로 써도 됨
val start: Long  = System.currentTimeMillis()</code></pre>
<hr>
<h2 id="3-함수-선언--fun">3. 함수 선언 — <code>fun</code></h2>
<pre><code class="language-kotlin">// 기본 형태
fun 함수이름(파라미터: 타입): 반환타입 {
    // 본문
}</code></pre>
<pre><code class="language-kotlin">// PostService.kt
fun getPosts(page: Int, size: Int): PostListResponse {
    val offset = (page - 1) * size
    val posts  = postMapper.findAll(offset, size)
    // ...
    return PostListResponse(...)
}</code></pre>
<p>반환값이 없으면 반환 타입을 생략하거나 <code>Unit</code> 을 씁니다.</p>
<pre><code class="language-kotlin">fun deletePost(id: Long) {           // 반환 타입 생략 = Unit
    postMapper.delete(id)
}</code></pre>
<hr>
<h2 id="4-표현식-함수-단일-표현식">4. 표현식 함수 (단일 표현식)</h2>
<p>함수 본문이 <code>return 표현식</code> 딱 한 줄이면, <code>=</code> 으로 줄여 쓸 수 있습니다.</p>
<pre><code class="language-kotlin">// PostController.kt — 일반 형태
fun getPost(@PathVariable id: Long): ResponseEntity&lt;PostResponse&gt; {
    return ResponseEntity.ok(postService.getPost(id))
}

// ↓ 표현식 함수로 줄이면
fun getPost(@PathVariable id: Long): ResponseEntity&lt;PostResponse&gt; =
    ResponseEntity.ok(postService.getPost(id))</code></pre>
<p>이 프로젝트의 Controller 메서드 대부분이 이 형태입니다.</p>
<hr>
<h2 id="5-기본-파라미터값--이름-있는-인수">5. 기본 파라미터값 &amp; 이름 있는 인수</h2>
<h3 id="기본-파라미터값-default-parameter">기본 파라미터값 (Default Parameter)</h3>
<p>Java 의 메서드 오버로딩 없이, 파라미터에 기본값을 지정할 수 있습니다.</p>
<pre><code class="language-kotlin">// PostController.kt
fun getPosts(
    @RequestParam(defaultValue = &quot;1&quot;)  page: Int = 1,
    @RequestParam(defaultValue = &quot;20&quot;) size: Int = 20,
): ResponseEntity&lt;PostListResponse&gt;</code></pre>
<pre><code class="language-kotlin">// Post.kt (domain) — 모든 파라미터에 기본값 → &quot;인수 없는 생성자&quot; 효과
data class Post(
    var id: Long = 0,
    var title: String = &quot;&quot;,
    var content: String = &quot;&quot;,
    // ...
)</code></pre>
<h3 id="이름-있는-인수-named-argument">이름 있는 인수 (Named Argument)</h3>
<p>함수를 호출할 때 파라미터 이름을 명시해서 가독성을 높입니다.</p>
<pre><code class="language-kotlin">// PostService.kt — createPost()
val post = Post(
    title   = request.title,    // 이름을 붙여서 호출
    content = request.content,
    author  = request.author,
)</code></pre>
<p>이름을 붙이면 순서를 바꿔도 되고, 어떤 값이 어떤 파라미터인지 한눈에 보입니다.</p>
<hr>
<h2 id="6-클래스와-주-생성자">6. 클래스와 주 생성자</h2>
<p>Kotlin 클래스는 선언과 동시에 생성자를 정의합니다.</p>
<pre><code class="language-kotlin">// 클래스 이름 뒤 괄호가 &quot;주 생성자(Primary Constructor)&quot;
class PostController(private val postService: PostService) {
    // postService 는 클래스 전체에서 사용 가능한 필드가 됨
}</code></pre>
<p><code>private val postService</code> 처럼 생성자 파라미터 앞에 접근 제어자를 붙이면,
<strong>생성자 파라미터이자 클래스 필드</strong> 가 됩니다.</p>
<p>Java 로 표현하면:</p>
<pre><code class="language-java">// Java
public class PostController {
    private final PostService postService;  // 필드 선언

    public PostController(PostService postService) {  // 생성자
        this.postService = postService;
    }
}</code></pre>
<p>Spring 은 이 생성자를 보고 <code>PostService</code> 빈을 자동 주입(DI)합니다.</p>
<hr>
<h2 id="7-data-class">7. data class</h2>
<p><code>data</code> 키워드를 붙이면 컴파일러가 아래 메서드를 <strong>자동 생성</strong>합니다.</p>
<table>
<thead>
<tr>
<th>자동 생성 메서드</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>toString()</code></td>
<td><code>Post(id=1, title=안녕, ...)</code> 형태의 문자열 반환</td>
</tr>
<tr>
<td><code>equals()</code></td>
<td>모든 필드값이 같으면 <code>true</code></td>
</tr>
<tr>
<td><code>hashCode()</code></td>
<td><code>equals()</code> 와 일관된 해시값</td>
</tr>
<tr>
<td><code>copy()</code></td>
<td>일부 필드만 바꾼 새 객체 생성</td>
</tr>
<tr>
<td><code>componentN()</code></td>
<td>구조 분해 선언 지원</td>
</tr>
</tbody></table>
<pre><code class="language-kotlin">// Post.kt
data class Post(
    var id: Long = 0,
    var title: String = &quot;&quot;,
    var content: String = &quot;&quot;,
    var author: String = &quot;&quot;,
    var viewCount: Int = 0,
    var createdAt: LocalDateTime = LocalDateTime.now(),
    var updatedAt: LocalDateTime = LocalDateTime.now(),
)</code></pre>
<h3 id="copy-활용-예시"><code>copy()</code> 활용 예시</h3>
<pre><code class="language-kotlin">val original = Post(title = &quot;제목&quot;, content = &quot;내용&quot;, author = &quot;홍길동&quot;)

// title 만 바꾼 새 Post 객체 생성 (original 은 그대로)
val updated = original.copy(title = &quot;새 제목&quot;)</code></pre>
<blockquote>
<p><strong>data class 는 언제 쓰나?</strong><br>데이터를 담는 것이 주 목적인 클래스 (DTO, 도메인 객체 등).
비즈니스 로직이 많은 클래스는 일반 <code>class</code> 가 낫습니다.</p>
</blockquote>
<hr>
<h2 id="8-companion-object">8. companion object</h2>
<p>Java 의 <code>static</code> 에 해당하는 개념입니다.<br>Kotlin 에는 <code>static</code> 키워드가 없고, 대신 클래스 안에 <code>companion object</code> 블록을 만듭니다.</p>
<pre><code class="language-kotlin">// PostDto.kt
data class PostResponse(
    val id: Long,
    val title: String,
    // ...
) {
    companion object {                         // ← 여기
        fun from(post: Post) = PostResponse(   // 정적 팩토리 메서드
            id    = post.id,
            title = post.title,
            // ...
        )
    }
}</code></pre>
<h3 id="호출-방법">호출 방법</h3>
<pre><code class="language-kotlin">// Java 의 PostResponse.from(post) 와 동일하게 호출
val response = PostResponse.from(post)</code></pre>
<h3 id="java-와-비교">Java 와 비교</h3>
<pre><code class="language-java">// Java
public class PostResponse {
    // ...
    public static PostResponse from(Post post) {   // static 메서드
        return new PostResponse(post.getId(), post.getTitle(), ...);
    }
}</code></pre>
<blockquote>
<p><strong>왜 팩토리 메서드 패턴을 쓰나?</strong><br>도메인 객체(<code>Post</code>)를 응답 DTO(<code>PostResponse</code>)로 변환하는 로직을 한 곳에 모아두면,<br>변환 방식이 바뀔 때 <code>from()</code> 만 수정하면 됩니다.</p>
</blockquote>
<hr>
<h2 id="9-null-안전성----">9. Null 안전성 — <code>?</code>, <code>?:</code>, <code>!!</code></h2>
<p>Kotlin 은 <code>null</code> 이 될 수 있는 타입과 없는 타입을 <strong>컴파일 시점에</strong> 구분합니다.</p>
<table>
<thead>
<tr>
<th>표기</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>String</code></td>
<td>절대 null 이 될 수 없음</td>
</tr>
<tr>
<td><code>String?</code></td>
<td>null 이 될 수도 있음 (nullable)</td>
</tr>
</tbody></table>
<h3 id="--nullable-타입"><code>?</code> — nullable 타입</h3>
<pre><code class="language-kotlin">// PostMapper.kt
fun findById(@Param(&quot;id&quot;) id: Long): Post?
//                                       ↑ DB에 없으면 null 을 반환할 수 있음</code></pre>
<h3 id="--엘비스-연산자-elvis-operator"><code>?:</code> — 엘비스 연산자 (Elvis Operator)</h3>
<p>&quot;null 이면 오른쪽을 실행해라&quot; 라는 뜻입니다.</p>
<pre><code class="language-kotlin">// PostService.kt
val post = postMapper.findById(id)
    ?: throw NoSuchElementException(&quot;게시글을 찾을 수 없습니다. id=$id&quot;)
//  ↑ findById 가 null 을 반환하면 예외를 던짐</code></pre>
<pre><code class="language-kotlin">// DataSeederController.kt
val totalCount = jdbc.queryForObject(&quot;SELECT COUNT(*) FROM posts&quot;, Long::class.java) ?: 0
//                                                                                    ↑ null 이면 0 으로 대체</code></pre>
<h3 id="--non-null-단언-강제-언박싱"><code>!!</code> — non-null 단언 (강제 언박싱)</h3>
<p>&quot;나는 이 값이 null 이 아님을 확신한다&quot; 는 선언입니다.<br>null 이면 <code>NullPointerException</code> 이 발생하므로, 사용에 주의해야 합니다.</p>
<pre><code class="language-kotlin">val name: String? = &quot;홍길동&quot;
val length = name!!.length   // null 이 아님을 개발자가 보장</code></pre>
<blockquote>
<p>이 프로젝트에서는 <code>!!</code> 대신 <code>?:</code> 로 안전하게 처리하고 있습니다.</p>
</blockquote>
<hr>
<h2 id="10-문자열-템플릿--">10. 문자열 템플릿 — <code>$</code></h2>
<p>문자열 안에 변수나 표현식을 직접 삽입할 수 있습니다.</p>
<pre><code class="language-kotlin">// PostService.kt
throw NoSuchElementException(&quot;게시글을 찾을 수 없습니다. id=$id&quot;)
//                                                            ↑ $변수명</code></pre>
<p>중괄호로 감싸면 더 복잡한 표현식도 넣을 수 있습니다.</p>
<pre><code class="language-kotlin">// DataSeederController.kt
&quot;rate&quot; to &quot;${count * 1000 / elapsed.coerceAtLeast(1)} 건/초&quot;
//          ↑ ${ 표현식 }</code></pre>
<p>Java 와 비교하면:</p>
<pre><code class="language-java">// Java
throw new NoSuchElementException(&quot;게시글을 찾을 수 없습니다. id=&quot; + id);

// Kotlin
throw NoSuchElementException(&quot;게시글을 찾을 수 없습니다. id=$id&quot;)</code></pre>
<hr>
<h2 id="11-람다와-고차-함수--map-it">11. 람다와 고차 함수 — <code>map</code>, <code>it</code></h2>
<h3 id="람다-lambda">람다 (Lambda)</h3>
<p>중괄호 <code>{ }</code> 로 감싼 코드 블록입니다. 함수의 인수로 전달할 수 있습니다.</p>
<pre><code class="language-kotlin">// PostService.kt
posts.map { PostResponse.from(it) }
//    ↑ map 에 람다를 전달</code></pre>
<h3 id="it--암묵적-파라미터"><code>it</code> — 암묵적 파라미터</h3>
<p>람다 파라미터가 하나뿐일 때, 이름을 직접 지어주는 대신 <code>it</code> 으로 참조합니다.</p>
<pre><code class="language-kotlin">posts.map { it -&gt; PostResponse.from(it) }  // it 명시적으로 쓴 것
posts.map { PostResponse.from(it) }        // it 생략한 것 (같은 의미)</code></pre>
<h3 id="map"><code>map</code></h3>
<p>컬렉션의 각 요소를 변환해서 새 리스트를 만드는 함수입니다.</p>
<pre><code class="language-kotlin">// [Post, Post, Post] → [PostResponse, PostResponse, PostResponse]
val responses: List&lt;PostResponse&gt; = posts.map { PostResponse.from(it) }</code></pre>
<p>Java 의 Stream 으로 표현하면:</p>
<pre><code class="language-java">// Java
List&lt;PostResponse&gt; responses = posts.stream()
    .map(PostResponse::from)
    .collect(Collectors.toList());</code></pre>
<h3 id="함수-참조-">함수 참조 (<code>::</code>)</h3>
<p>람다 대신 메서드 참조를 쓸 수도 있습니다.</p>
<pre><code class="language-kotlin">posts.map { PostResponse.from(it) }  // 람다
posts.map(PostResponse::from)        // 함수 참조 (동일한 의미)</code></pre>
<hr>
<h2 id="12-스코프-함수--also">12. 스코프 함수 — <code>also</code></h2>
<p><strong>스코프 함수</strong>는 객체에 대해 코드 블록을 실행하는 함수들입니다.<br>이 프로젝트에서는 <code>also</code> 를 사용합니다.</p>
<h3 id="also"><code>also</code></h3>
<p>&quot;이것도 해라&quot; 라는 의미입니다.<br>객체 자신을 <code>it</code> 으로 받아 부수 작업을 한 뒤, <strong>원래 객체를 그대로 반환</strong>합니다.</p>
<pre><code class="language-kotlin">// PostService.kt
return PostResponse.from(post.also { it.viewCount++ })
//                            ↑ post.viewCount 를 증가시키고, post 자체를 반환
//                       ↑ 증가된 post 를 from() 에 전달</code></pre>
<p>흐름을 풀어 쓰면:</p>
<pre><code class="language-kotlin">// also 없이 쓴다면
post.viewCount++
return PostResponse.from(post)

// also 를 쓰면 한 줄로
return PostResponse.from(post.also { it.viewCount++ })</code></pre>
<h3 id="스코프-함수-한눈에-비교">스코프 함수 한눈에 비교</h3>
<table>
<thead>
<tr>
<th>함수</th>
<th>참조 방식</th>
<th>반환값</th>
<th>주 용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>let</code></td>
<td><code>it</code></td>
<td>람다 결과</td>
<td>null 체크 후 변환</td>
</tr>
<tr>
<td><code>run</code></td>
<td><code>this</code></td>
<td>람다 결과</td>
<td>초기화 + 계산</td>
</tr>
<tr>
<td><code>apply</code></td>
<td><code>this</code></td>
<td>객체 자신</td>
<td>객체 설정(빌더 패턴)</td>
</tr>
<tr>
<td><code>also</code></td>
<td><code>it</code></td>
<td>객체 자신</td>
<td>부수 작업 (로깅, 증가 등)</td>
</tr>
<tr>
<td><code>with</code></td>
<td><code>this</code></td>
<td>람다 결과</td>
<td>특정 객체에 여러 작업</td>
</tr>
</tbody></table>
<hr>
<h2 id="13-require--전제-조건-검사">13. <code>require()</code> — 전제 조건 검사</h2>
<p>파라미터의 유효성을 검사할 때 씁니다.<br>조건이 <code>false</code> 이면 <code>IllegalArgumentException</code> 을 자동으로 던집니다.</p>
<pre><code class="language-kotlin">// DataSeederController.kt
require(count in 1..10_000_000) { &quot;count 는 1 ~ 10,000,000 사이여야 합니다.&quot; }
//      ↑ 조건                    ↑ 실패 시 메시지를 반환하는 람다</code></pre>
<p>Java 로 표현하면:</p>
<pre><code class="language-java">// Java
if (!(count &gt;= 1 &amp;&amp; count &lt;= 10_000_000)) {
    throw new IllegalArgumentException(&quot;count 는 1 ~ 10,000,000 사이여야 합니다.&quot;);
}</code></pre>
<blockquote>
<p>비슷한 함수: <code>check()</code> 는 상태 검사 (<code>IllegalStateException</code>), <code>error()</code> 는 무조건 예외.</p>
</blockquote>
<hr>
<h2 id="14-mapof--to-중위-함수">14. <code>mapOf()</code> &amp; <code>to</code> 중위 함수</h2>
<h3 id="mapof"><code>mapOf()</code></h3>
<p>불변 Map 을 만드는 표준 함수입니다.</p>
<pre><code class="language-kotlin">// DataSeederController.kt
return mapOf(
    &quot;inserted&quot;   to count,
    &quot;totalPosts&quot; to totalCount,
    &quot;elapsedMs&quot;  to elapsed,
)</code></pre>
<h3 id="to--중위-함수-infix-function"><code>to</code> — 중위 함수 (Infix Function)</h3>
<p><code>A to B</code> 는 <code>Pair(A, B)</code> 와 같습니다.<br><code>mapOf()</code> 는 <code>Pair</code> 들을 받아 Map 을 만듭니다.</p>
<pre><code class="language-kotlin">&quot;inserted&quot; to count        // Pair&lt;String, Int&gt;
// = Pair(&quot;inserted&quot;, count)  // 동일한 의미</code></pre>
<p>Java 로 표현하면:</p>
<pre><code class="language-java">// Java
Map&lt;String, Object&gt; result = new HashMap&lt;&gt;();
result.put(&quot;inserted&quot;, count);
result.put(&quot;totalPosts&quot;, totalCount);</code></pre>
<hr>
<h2 id="15-범위-연산자----in">15. 범위 연산자 — <code>..</code> &amp; <code>in</code></h2>
<h3 id="--범위-생성"><code>..</code> — 범위 생성</h3>
<pre><code class="language-kotlin">1..10          // 1 이상 10 이하 (IntRange)
1..10_000_000  // 1 이상 천만 이하</code></pre>
<h3 id="in--범위-포함-여부-검사"><code>in</code> — 범위 포함 여부 검사</h3>
<pre><code class="language-kotlin">// DataSeederController.kt
require(count in 1..10_000_000) { &quot;...&quot; }
//             ↑ count 가 1~10,000,000 범위 안에 있는지 확인</code></pre>
<p>Java 로 표현하면:</p>
<pre><code class="language-java">// Java
count &gt;= 1 &amp;&amp; count &lt;= 10_000_000</code></pre>
<p>컬렉션에도 사용할 수 있습니다.</p>
<pre><code class="language-kotlin">val list = listOf(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;)
&quot;b&quot; in list    // true</code></pre>
<hr>
<h2 id="16-멀티라인-문자열----trimindent">16. 멀티라인 문자열 — <code>&quot;&quot;&quot;</code> &amp; <code>trimIndent()</code></h2>
<h3 id="-삼중-따옴표-문자열"><code>&quot;&quot;&quot;</code> — 삼중 따옴표 문자열</h3>
<p>줄바꿈과 들여쓰기를 그대로 포함한 문자열입니다.</p>
<pre><code class="language-kotlin">// DataSeederController.kt
jdbc.execute(&quot;&quot;&quot;
    INSERT INTO posts (title, content, author, ...)
    SELECT
        CASE (i % 10)
            WHEN 0 THEN &#39;공지사항: ...&#39; || i
            ...
        END
    FROM generate_series(1, $count) AS t(i)
&quot;&quot;&quot;.trimIndent())</code></pre>
<h3 id="trimindent"><code>trimIndent()</code></h3>
<p>삼중 따옴표 문자열에서 <strong>공통 들여쓰기를 제거</strong>합니다.<br>붙이지 않으면 SQL 앞에 공백이 그대로 포함됩니다.</p>
<pre><code class="language-kotlin">val sql = &quot;&quot;&quot;
    SELECT *
    FROM posts
&quot;&quot;&quot;.trimIndent()

// trimIndent() 적용 결과:
// &quot;SELECT *\nFROM posts\n&quot;
// (앞의 공백 4칸이 사라짐)</code></pre>
<hr>
<h2 id="17-숫자-리터럴의-밑줄--1_000_000">17. 숫자 리터럴의 밑줄 — <code>1_000_000</code></h2>
<p>긴 숫자를 읽기 쉽게 구분하는 문법입니다. 값 자체에는 영향이 없습니다.</p>
<pre><code class="language-kotlin">// DataSeederController.kt
require(count in 1..10_000_000) { &quot;...&quot; }
//                  ↑ 10,000,000 과 완전히 같은 값</code></pre>
<pre><code class="language-kotlin">val million = 1_000_000   // 1000000 과 동일
val billion = 1_000_000_000</code></pre>
<hr>
<h2 id="18-인터페이스--interface">18. 인터페이스 — <code>interface</code></h2>
<p>Java 와 거의 같습니다. 구현 없이 메서드 시그니처만 선언합니다.</p>
<pre><code class="language-kotlin">// PostMapper.kt
@Mapper
interface PostMapper {

    fun findAll(
        @Param(&quot;offset&quot;) offset: Int,
        @Param(&quot;limit&quot;)  limit: Int,
    ): List&lt;Post&gt;

    fun findById(@Param(&quot;id&quot;) id: Long): Post?   // null 반환 가능

    fun insert(post: Post)   // 반환값 없음 (Unit)
}</code></pre>
<p>MyBatis 의 <code>@Mapper</code> 어노테이션을 붙이면,<br>Spring 이 시작할 때 이 인터페이스의 구현체를 자동으로 만들어줍니다.</p>
<hr>
<h2 id="19-와일드카드-import--">19. 와일드카드 import — <code>*</code></h2>
<p>패키지 안의 모든 것을 한 번에 가져옵니다.</p>
<pre><code class="language-kotlin">// PostController.kt
import me.study.index.dto.*   // dto 패키지의 모든 클래스를 가져옴

// 덕분에 아래를 따로 import 하지 않아도 됨:
// import me.study.index.dto.PostCreateRequest
// import me.study.index.dto.PostUpdateRequest
// import me.study.index.dto.PostResponse
// import me.study.index.dto.PostListResponse</code></pre>
<hr>
<h2 id="20-코틀린에서-자바-클래스-참조--classjava">20. 코틀린에서 자바 클래스 참조 — <code>::class.java</code></h2>
<p>Kotlin 의 타입 시스템과 Java 의 타입 시스템을 연결할 때 씁니다.</p>
<pre><code class="language-kotlin">// DataSeederController.kt
jdbc.queryForObject(&quot;SELECT COUNT(*) FROM posts&quot;, Long::class.java)
//                                                ↑ Java 의 Long.class 에 해당</code></pre>
<pre><code class="language-kotlin">// javaClass 프로퍼티: 현재 인스턴스의 Java 클래스를 반환
private val log = LoggerFactory.getLogger(javaClass)
//                                        ↑ this.getClass() 와 동일</code></pre>
<table>
<thead>
<tr>
<th>Kotlin 표현</th>
<th>Java 표현</th>
</tr>
</thead>
<tbody><tr>
<td><code>String::class</code></td>
<td>— (KClass)</td>
</tr>
<tr>
<td><code>String::class.java</code></td>
<td><code>String.class</code></td>
</tr>
<tr>
<td><code>javaClass</code></td>
<td><code>this.getClass()</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="21-unit-vs-void">21. Unit vs Void</h2>
<table>
<thead>
<tr>
<th></th>
<th>Kotlin</th>
<th>Java</th>
</tr>
</thead>
<tbody><tr>
<td>반환값 없는 함수</td>
<td><code>Unit</code> (생략 가능)</td>
<td><code>void</code></td>
</tr>
<tr>
<td>제네릭에서 &quot;없음&quot;</td>
<td><code>Unit</code></td>
<td><code>Void</code></td>
</tr>
</tbody></table>
<pre><code class="language-kotlin">// 함수 반환값이 없으면 Unit (보통 생략)
fun deletePost(id: Long): Unit { ... }
fun deletePost(id: Long)       { ... }  // 위와 동일</code></pre>
<pre><code class="language-kotlin">// ResponseEntity 에서 반환 바디가 없을 때 Java 의 Void 를 써야 하는 경우
// PostController.kt
fun deletePost(@PathVariable id: Long): ResponseEntity&lt;Void&gt; {
    postService.deletePost(id)
    return ResponseEntity.noContent().build()
}</code></pre>
<blockquote>
<p>Spring 의 <code>ResponseEntity</code> 는 Java 클래스라 <code>Void</code> 를 사용합니다.<br>순수 Kotlin 코드에서 &quot;반환값 없음&quot;은 <code>Unit</code> 입니다.</p>
</blockquote>
<hr>
<h2 id="22-후행-쉼표-trailing-comma">22. 후행 쉼표 (Trailing Comma)</h2>
<p>마지막 파라미터/요소 뒤에 쉼표를 붙여도 됩니다.</p>
<pre><code class="language-kotlin">// PostDto.kt
data class PostCreateRequest(
    val title: String,
    val content: String,
    val author: String,   // ← 마지막에도 쉼표 OK
)</code></pre>
<p>Java 에서는 문법 오류지만 Kotlin 에서는 허용됩니다.<br>나중에 줄을 추가하거나 순서를 바꿀 때 git diff 가 깔끔해지는 장점이 있습니다.</p>
<hr>
<h2 id="전체-흐름으로-보는-문법-총정리">전체 흐름으로 보는 문법 총정리</h2>
<pre><code>HTTP 요청
   ↓
[Controller]  — 표현식 함수(=), @PathVariable, @RequestParam, 기본 파라미터값
   ↓
[Service]     — val/var, 엘비스 연산자(?:), also, map, require
   ↓
[Mapper]      — interface, nullable 반환(Post?), @Param
   ↓
[Domain/DTO]  — data class, companion object, val/var, 기본값
   ↓
DB (PostgreSQL + MyBatis)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[RSA]]></title>
            <link>https://velog.io/@jun-log/RSA</link>
            <guid>https://velog.io/@jun-log/RSA</guid>
            <pubDate>Mon, 18 May 2026 07:08:37 GMT</pubDate>
            <description><![CDATA[<h1 id="rsa-알고리즘-학습-정리">RSA 알고리즘 학습 정리</h1>
<blockquote>
<p>공개키 암호화 시스템 핵심 개념 노트</p>
</blockquote>
<hr>
<h2 id="1-rsa란">1. RSA란?</h2>
<p>RSA는 1977년 Rivest, Shamir, Adleman 세 사람이 만든 공개키 암호화 알고리즘입니다.
HTTPS, 디지털 서명, SSH 등 인터넷 보안의 근간이 되는 기술입니다.</p>
<h3 id="핵심-아이디어">핵심 아이디어</h3>
<ul>
<li><strong>두 개의 키 사용:</strong> 누구나 아는 공개키(Public Key) + 나만 아는 개인키(Private Key)</li>
<li><strong>비대칭 암호화:</strong> 암호화와 복호화에 서로 다른 키를 사용</li>
<li><strong>수학적 근거:</strong> 큰 수의 소인수분해는 현실적으로 불가능</li>
</ul>
<hr>
<h2 id="2-수학적-원리">2. 수학적 원리</h2>
<h3 id="키-생성-과정">키 생성 과정</h3>
<ol>
<li>두 소수 p, q를 선택 → 예: p=13, q=7</li>
<li><code>n = p × q</code> 계산 → 예: n = 91</li>
<li><code>φ(n) = (p−1)(q−1)</code> 계산 → 예: φ(91) = 72</li>
<li><code>gcd(e, φ(n)) = 1</code> 인 e 선택 → 공개키 지수</li>
<li><code>e × d ≡ 1 (mod φ(n))</code> 를 만족하는 d 계산 → 개인키 지수</li>
</ol>
<h3 id="암호화--복호화-공식">암호화 / 복호화 공식</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>공식</th>
<th>사용 키</th>
</tr>
</thead>
<tbody><tr>
<td>암호화</td>
<td><code>C = Mᵉ mod n</code></td>
<td>공개키 (n, e)</td>
</tr>
<tr>
<td>복호화</td>
<td><code>M = Cᵈ mod n</code></td>
<td>개인키 (n, d)</td>
</tr>
</tbody></table>
<h3 id="보안-근거">보안 근거</h3>
<p>n은 공개되지만, n에서 p와 q를 역으로 구하는 소인수분해는 현재 컴퓨터로 불가능합니다.
2048비트 RSA 키 기준, 인수분해에 우주 나이보다 긴 시간이 소요됩니다.</p>
<hr>
<h2 id="3-키의-실제-형태">3. 키의 실제 형태</h2>
<h3 id="수학적-본질은-숫자">수학적 본질은 숫자</h3>
<ul>
<li><strong>공개키:</strong> 숫자 쌍 (n, e)</li>
<li><strong>개인키:</strong> 숫자 쌍 (n, d)</li>
<li>모든 암복호화 연산은 결국 정수 연산(mod)</li>
</ul>
<h3 id="파일로-저장-시">파일로 저장 시</h3>
<p>실제 키 파일을 열면 Base64로 인코딩된 문자열로 보입니다:</p>
<pre><code>-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----</code></pre><p>이 문자열을 디코딩하면 결국 수천 자리의 정수 (n, e)가 나옵니다.
PEM, DER 등 표준 포맷으로 감싸는 이유는 다루기 편하고 명확한 경계를 표시하기 위해서입니다.</p>
<hr>
<h2 id="4-암호화-vs-디지털-서명">4. 암호화 vs 디지털 서명</h2>
<p>RSA는 두 가지 방향으로 사용할 수 있습니다. 목적이 다르면 키 사용 방향도 반대입니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>목적</th>
<th>사용 키</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>암호화</td>
<td>받는 사람만 읽게</td>
<td>받는 사람의 공개키로 잠금 → 개인키로 열기</td>
<td>철수→영희: 영희 공개키로 암호화</td>
</tr>
<tr>
<td>디지털 서명</td>
<td>내가 보냈음을 증명</td>
<td>보내는 사람 개인키로 서명 → 공개키로 검증</td>
<td>철수→영희: 철수 개인키로 서명</td>
</tr>
</tbody></table>
<h3 id="헷갈리지-않는-법">헷갈리지 않는 법</h3>
<ul>
<li><strong>암호화:</strong> &quot;영희만 열 수 있게&quot; → 영희의 공개키로 잠근다</li>
<li><strong>서명:</strong> &quot;나만 만들 수 있음을 증명&quot; → 나의 개인키로 서명한다</li>
<li><strong>핵심:</strong> 암호화는 받는 사람 공개키 / 서명은 보내는 사람 개인키</li>
</ul>
<hr>
<h2 id="5-실제-활용">5. 실제 활용</h2>
<h3 id="https-tls-핸드셰이크">HTTPS (TLS 핸드셰이크)</h3>
<p>RSA는 느리기 때문에 모든 데이터를 직접 암호화하지 않습니다.</p>
<ul>
<li>RSA로 AES 대칭키를 안전하게 교환</li>
<li>본 통신은 빠른 AES로 암호화</li>
<li>이를 <strong>하이브리드 암호화</strong> 방식이라 합니다</li>
</ul>
<h3 id="ssh">SSH</h3>
<ul>
<li>서버에 공개키를 등록해두고, 개인키로 인증</li>
<li>비밀번호 없이 안전하게 서버에 접속 가능</li>
</ul>
<h3 id="이메일-서명-pgpgpg">이메일 서명 (PGP/GPG)</h3>
<ul>
<li>개인키로 이메일에 서명 → 수신자가 공개키로 검증</li>
<li>메일이 위조되지 않았음을 보장</li>
</ul>
<hr>
<h2 id="6-rsa의-한계와-미래">6. RSA의 한계와 미래</h2>
<h3 id="현재-한계">현재 한계</h3>
<ul>
<li><strong>속도:</strong> AES 등 대칭키 대비 수백 배 느림 → 직접 암호화에 비효율적</li>
<li><strong>키 크기:</strong> 보안을 위해 2048비트 이상 필요 → 저장/전송 용량 부담</li>
</ul>
<h3 id="양자컴퓨터-위협">양자컴퓨터 위협</h3>
<p>양자컴퓨터의 Shor 알고리즘은 소인수분해를 빠르게 풀 수 있어 RSA를 이론적으로 깰 수 있습니다.
현재 NIST를 중심으로 양자내성 암호(Post-Quantum Cryptography) 표준화가 진행 중입니다.</p>
<hr>
<h2 id="7-핵심-요약-한눈에-보기">7. 핵심 요약 한눈에 보기</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>핵심 한 줄 요약</th>
</tr>
</thead>
<tbody><tr>
<td>RSA란</td>
<td>큰 수 소인수분해의 어려움을 이용한 공개키 암호화</td>
</tr>
<tr>
<td>공개키 (n, e)</td>
<td>누구나 알 수 있는 키 — 암호화·서명 검증에 사용</td>
</tr>
<tr>
<td>개인키 (n, d)</td>
<td>절대 공개하면 안 되는 키 — 복호화·서명 생성에 사용</td>
</tr>
<tr>
<td>암호화 방향</td>
<td>받는 사람 공개키 → 받는 사람 개인키</td>
</tr>
<tr>
<td>서명 방향</td>
<td>보내는 사람 개인키 → 보내는 사람 공개키</td>
</tr>
<tr>
<td>실제 사용</td>
<td>HTTPS에서 AES 키 교환, SSH 인증, 이메일 서명</td>
</tr>
<tr>
<td>보안 크기</td>
<td>현재 최소 2048비트, 권장 4096비트</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[완전제곱식]]></title>
            <link>https://velog.io/@jun-log/%EC%99%84%EC%A0%84%EC%A0%9C%EA%B3%B1%EC%8B%9D</link>
            <guid>https://velog.io/@jun-log/%EC%99%84%EC%A0%84%EC%A0%9C%EA%B3%B1%EC%8B%9D</guid>
            <pubDate>Mon, 18 May 2026 01:34:04 GMT</pubDate>
            <description><![CDATA[<h2 id="완전제곱식">완전제곱식</h2>
<p>다항식 전체가 어떤 식의 제곱 형태로 묶이는 식.</p>
<p>$$
(x+3)^2 \
(2a-b)^2
$$</p>
<h3 id="부호에-따른-전개-형태">부호에 따른 전개 형태</h3>
<p><code>(앞항)² ± 2×(앞항)×(뒷항) + (뒷항)²</code> 형태로 전개된다.</p>
<p>$$
(x+a)^2 = x^2 + 2ax + a^2 \
(x-a)^2 = x^2 - 2ax + a^2
$$</p>
<p>부호가 결정되는 위치는 가운데 항(2ax)뿐이며, 양 끝의 제곱항은 항상 양수다.</p>
<h3 id="완전제곱식이-되는-조건">완전제곱식이 되는 조건</h3>
<p>이차식 $x^2 + ax + b$ 가 완전제곱식이 되려면, 상수항 $b$ 가 일차항 계수의 절반의 제곱과 같아야 한다.</p>
<p>$$
b = \left(\frac{a}{2}\right)^2
$$</p>
<p><strong>유도 과정</strong></p>
<p>$(x+k)^2 = x^2 + 2kx + k^2$ 이므로, $x^2 + ax + b$ 와 비교하면:</p>
<ul>
<li>일차항 계수 비교: $a = 2k \Rightarrow k = \dfrac{a}{2}$</li>
<li>상수항 비교: $b = k^2 = \left(\dfrac{a}{2}\right)^2$</li>
</ul>
<p><strong>예시</strong></p>
<p>$x^2 + 6x + 9$ 에서 $a = 6$, $\left(\dfrac{6}{2}\right)^2 = 9$ 이므로 완전제곱식이다.<br>$\Rightarrow (x+3)^2$</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[HS256]]></title>
            <link>https://velog.io/@jun-log/HS256</link>
            <guid>https://velog.io/@jun-log/HS256</guid>
            <pubDate>Wed, 13 May 2026 06:20:45 GMT</pubDate>
            <description><![CDATA[<h2 id="hs256">HS256</h2>
<p><code>HMAC</code> + <code>SHA-256</code> 을 의미하는 서명 알고리즘
<code>SHA-256</code>을 사용하여 <code>HMAC</code>한 서명 알고리즘</p>
<h3 id="서명-과정">서명 과정</h3>
<pre><code>서명 = HMAC-SHA256(
  Base64url(Header) + &quot;.&quot; + Base64url(Payload),
  비밀키(Secret Key)
)</code></pre><h3 id="검증-과정">검증 과정</h3>
<pre><code>받은 토큰의 서명 == 서버가 직접 계산한 서명?
  → 같으면: 유효한 토큰 ✅
  → 다르면: 위변조된 토큰 ❌</code></pre><h3 id="hmac이-필요한-이유">HMAC이 필요한 이유</h3>
<p>단순히, SHA-256만으로 서명을 하면 payload가 조작된 것을 감지할 수 없음.</p>
<pre><code>서명 = SHA256(&quot;header.payload&quot;) 

탈취된 토큰의 payload를 위조하여, 재서명하여도 조작된것을 검증 할 수 없음.
재서명 = SHA256(header.변경된payload)</code></pre><h2 id="hmac">HMAC</h2>
<p><code>Hash-based Message Authentication Code</code> 의 줄임말로 <strong>키를 받아</strong> 해시값을 만들어내는 <strong>방식</strong>을 말한다.</p>
<h3 id="hmac-동작방식">HMAC 동작방식</h3>
<pre><code>HMAC(K, M) = SHA256(
                (K ⊕ opad) +           ← 바깥 패딩과 키를 XOR
                SHA256(
                  (K ⊕ ipad) + M       ← 안쪽 패딩과 키를 XOR한 뒤 메시지 붙이기
                )
             )</code></pre><ul>
<li><code>K</code> : 비밀키</li>
<li><code>M</code> : 서명할 메시지 (header.payload)</li>
<li><code>ipad</code> : 0x36(<code>6</code>) 반복 (inner padding)</li>
<li><code>opad</code> : 0x5C(<code>\</code>) 반복 (outer padding)</li>
</ul>
<p>패딩은 key의 길이만큼 반복하여 생성한다.</p>
<blockquote>
</blockquote>
<p>key가 5글자라면, 바깥 패딩은 &quot;66666&quot; 이 된다.</p>
<p>같은 키를 그대로 두 번 쓰면 내부/외부 해시가 너무 비슷한 구조가 되어서 수학적 취약점이 생기기때문에,
key를 패딩과 XOR연산하여 한번 더 꼬아주는게 핵심이다.</p>
<h3 id="길이-확장-공격-length-extension-attack">길이 확장 공격 (Length Extension Attack)</h3>
<p>SHA-256은 Merkle-Damgård 구조로, 최종 해시값이 곧 마지막 내부 상태값이다.
따라서 SHA256(K + M)의 결과를 알면, K를 모르더라도
SHA256(K + M + 추가데이터)를 계산할 수 있다.</p>
<p>HMAC은 내부 해시값을 외부 SHA256의 입력으로 한번 더 감싸기 때문에
공격자가 내부 상태를 이어서 계산할 수 있는 접점 자체가 사라진다.</p>
<h3 id="같은-키를-두-번-쓰면-안-되는-이유">같은 키를 두 번 쓰면 안 되는 이유</h3>
<p>내부/외부 해시에 동일한 키가 반복되면, 여러 서명값을 수집했을 때
두 해시 계산 사이의 수학적 관계를 추론할 수 있게 된다.
ipad/opad로 키를 XOR 변형하면, 내부/외부가 수학적으로
독립된 키를 쓰는 것과 같은 효과가 나서 이 관계를 끊어낼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이썬 (2)]]></title>
            <link>https://velog.io/@jun-log/%ED%8C%8C%EC%9D%B4%EC%8D%AC2</link>
            <guid>https://velog.io/@jun-log/%ED%8C%8C%EC%9D%B4%EC%8D%AC2</guid>
            <pubDate>Wed, 13 May 2026 01:19:26 GMT</pubDate>
            <description><![CDATA[<h2 id="목차">목차</h2>
<ol>
<li><a href="#with">with</a></li>
<li><a href="#%ED%95%A8%EC%88%98">함수</a></li>
<li><a href="#global--nonlocal">global / nonlocal</a></li>
<li><a href="#%ED%81%B4%EB%9E%98%EC%8A%A4">클래스</a></li>
<li><a href="#%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC">예외처리</a></li>
<li><a href="#raise">raise</a></li>
<li><a href="#import">import</a></li>
<li><a href="#__name__"><strong>name</strong></a></li>
<li><a href="#decorator">Decorator</a></li>
<li><a href="#generator--yield">Generator &amp; yield</a></li>
</ol>
<h2 id="with">with</h2>
<p>Java의 <code>try-with-resources</code> 와 완전히 동일한 개념입니다.<br>블록이 끝나면 <strong>예외가 발생하더라도</strong> 자원을 자동으로 해제합니다.</p>
<pre><code class="language-java">// Java
try (FileReader f = new FileReader(&quot;file.txt&quot;)) {
    // 블록 끝나면 자동으로 close()
}</code></pre>
<pre><code class="language-python"># Python
with open(&quot;file.txt&quot;, &quot;r&quot;) as f:
    # 블록 끝나면 자동으로 close()
    data = f.read()</code></pre>
<h3 id="with-없이-쓰면-위험한-이유">with 없이 쓰면 위험한 이유</h3>
<pre><code class="language-python"># with 없이 - 예외 발생 시 close() 가 실행 안 될 수 있음 💥
f = open(&quot;file.txt&quot;, &quot;r&quot;)
data = f.read()   # 여기서 예외 발생하면?
f.close()         # 실행 안 됨 → 파일이 열린 채로 남음!

# with 사용 - 예외 발생해도 반드시 close() 실행 ✅
with open(&quot;file.txt&quot;, &quot;r&quot;) as f:
    data = f.read()</code></pre>
<h3 id="동작-원리---dunder-method">동작 원리 - dunder method</h3>
<p>내부적으로는 <code>__enter__</code>, <code>__exit__</code> 라는 특수 메서드로 동작합니다.</p>
<pre><code class="language-python"># with 는 사실 이것과 동일
f = open(&quot;file.txt&quot;, &quot;r&quot;)
f.__enter__()
try:
    data = f.read()
finally:
    f.__exit__()   # 항상 실행 → close()</code></pre>
<p><code>__init__</code> 처럼 <code>__</code> 로 감싸진 Python의 특수 메서드(dunder method) 중 하나입니다.</p>
<hr>
<h2 id="함수">함수</h2>
<h3 id="default-parameter-기본값-매개변수">Default Parameter (기본값 매개변수)</h3>
<p>매개변수에 기본값을 지정하면, 호출 시 해당 인자를 생략할 수 있습니다.<br>Java에서는 오버로딩으로 해결해야 했지만, Python은 기본값으로 한 번에 처리합니다.</p>
<pre><code class="language-python"># Java - 오버로딩 필요
void greet(String name) { greet(name, &quot;안녕하세요&quot;); }
void greet(String name, String message) { ... }</code></pre>
<pre><code class="language-python"># Python - 기본값으로 한 번에
def greet(name, message=&quot;안녕하세요&quot;):
    print(f&quot;{message}, {name}!&quot;)

greet(&quot;Alice&quot;)               # 안녕하세요, Alice!
greet(&quot;Bob&quot;, &quot;반갑습니다&quot;)    # 반갑습니다, Bob!</code></pre>
<hr>
<h3 id="keyword-argument-키워드-인자">Keyword Argument (키워드 인자)</h3>
<p>함수 호출 시 <code>매개변수명=값</code> 형태로 전달하면, <strong>순서와 무관하게</strong> 인자를 넘길 수 있습니다.<br>Java에는 없는 개념으로, 매개변수가 많을 때 코드 가독성이 크게 높아집니다.</p>
<pre><code class="language-python">def introduce(name, age, city):
    print(f&quot;이름: {name}, 나이: {age}, 도시: {city}&quot;)

introduce(&quot;Alice&quot;, 30, &quot;서울&quot;)                # 순서대로 전달
introduce(age=30, city=&quot;서울&quot;, name=&quot;Alice&quot;)  # 이름으로 전달 - 순서 무관
introduce(&quot;Alice&quot;, city=&quot;서울&quot;, age=30)       # 혼합 가능 (positional이 먼저)</code></pre>
<blockquote>
<p>혼합 사용 시 주의: 위치 인자(positional)는 반드시 키워드 인자보다 <strong>앞에</strong> 와야 합니다.</p>
</blockquote>
<hr>
<h3 id="multiple-return--unpacking">Multiple Return &amp; Unpacking</h3>
<p>Python 함수는 여러 값을 동시에 반환할 수 있습니다.<br>내부적으로는 값들을 <strong>tuple로 묶어서 반환</strong>하는 것입니다.</p>
<pre><code class="language-python">def min_max(numbers):
    return min(numbers), max(numbers)  # 내부적으로 (min, max) tuple 반환

result = min_max([3, 1, 4, 1, 5, 9])
print(result)        # (1, 9)
print(type(result))  # &lt;class &#39;tuple&#39;&gt;</code></pre>
<p>반환된 tuple을 각 변수에 바로 분해하는 것을 <strong>Unpacking</strong> 이라고 합니다.</p>
<pre><code class="language-python">a, b = min_max([3, 1, 4, 1, 5, 9])
print(a)  # 1
print(b)  # 9</code></pre>
<p>Java였다면 별도 클래스나 배열로 감싸야 했지만, Python은 자연스럽게 처리됩니다.</p>
<h4 id="unpacking-활용">Unpacking 활용</h4>
<pre><code class="language-python"># swap - temp 변수 없이 한 줄로
a, b = b, a

# * 로 나머지를 한 번에 받기
first, *rest = [1, 2, 3, 4, 5]
print(first)  # 1
print(rest)   # [2, 3, 4, 5]

*head, last = [1, 2, 3, 4, 5]
print(head)   # [1, 2, 3, 4]
print(last)   # 5</code></pre>
<hr>
<h3 id="args-kwargs"><em>args, *</em>kwargs</h3>
<p>인자의 개수가 정해지지 않을 때 사용합니다.</p>
<ul>
<li><code>*args</code>: 위치 인자를 <strong>tuple</strong> 로 받음</li>
<li><code>**kwargs</code>: 키워드 인자를 <strong>dict</strong> 로 받음</li>
</ul>
<pre><code class="language-python">def func(*args, **kwargs):
    print(args)    # tuple
    print(kwargs)  # dict

func(1, 2, 3, name=&quot;Alice&quot;, age=30)
# (1, 2, 3)
# {&#39;name&#39;: &#39;Alice&#39;, &#39;age&#39;: 30}</code></pre>
<h4 id="활용-예시">활용 예시</h4>
<pre><code class="language-python"># 개수에 상관없이 합산
def my_sum(*args):
    return sum(args)

my_sum(1, 2, 3)        # 6
my_sum(1, 2, 3, 4, 5)  # 15</code></pre>
<pre><code class="language-python"># dict를 ** 로 풀어서 함수 인자로 전달
options = {&quot;reverse&quot;: True, &quot;key&quot;: lambda x: x}
sorted(numbers, **options)
# == sorted(numbers, reverse=True, key=lambda x: x)</code></pre>
<blockquote>
<p>Decorator를 만들 때 <code>wrapper(*args, **kwargs)</code> 로 자주 쓰입니다.<br>어떤 함수든 인자를 그대로 받아 전달할 수 있기 때문입니다.</p>
</blockquote>
<hr>
<h3 id="lambda">lambda</h3>
<p>이름 없이 한 줄로 정의하는 <strong>익명 함수</strong>입니다.<br>Java의 람다식과 유사하며, 주로 <code>sorted()</code> 의 <code>key</code> 인자처럼 간단한 함수가 필요할 때 사용합니다.</p>
<pre><code class="language-python"># 기본 문법
lambda 매개변수: 표현식

# 예시
add = lambda a, b: a + b
add(3, 4)  # 7</code></pre>
<h4 id="sorted의-key와-함께-사용">sorted()의 key와 함께 사용</h4>
<p><code>key</code> 는 각 요소를 <strong>어떤 기준으로 비교할지</strong> 변환 함수를 넘기는 것입니다.<br>실제 값을 변환하는 게 아니라 <strong>정렬 기준만</strong> 바꿉니다.</p>
<pre><code class="language-python">words = [&quot;banana&quot;, &quot;apple&quot;, &quot;kiwi&quot;]

sorted(words)                          # 알파벳 순 → [&quot;apple&quot;, &quot;banana&quot;, &quot;kiwi&quot;]
sorted(words, key=lambda x: len(x))   # 길이 순 → [&quot;kiwi&quot;, &quot;apple&quot;, &quot;banana&quot;]
sorted(words, key=lambda x: x[-1])    # 마지막 글자 순 정렬</code></pre>
<pre><code class="language-python">numbers = [3, 1, 4, 1, 5]

sorted(numbers)                      # 오름차순 → [1, 1, 3, 4, 5]
sorted(numbers, reverse=True)        # 내림차순 → [5, 4, 3, 1, 1]
sorted(numbers, key=lambda x: -x)   # 내림차순 (동일한 결과)</code></pre>
<blockquote>
<p><code>key=lambda x: -x</code> 는 <code>-x</code> 를 기준으로 오름차순 정렬합니다.<br>큰 수에 <code>-</code> 가 붙어 작아지므로 앞으로 오게 됩니다.<br>반환값은 원본 값 그대로입니다.</p>
</blockquote>
<hr>
<h2 id="global--nonlocal">global / nonlocal</h2>
<p>Python 함수 안에서 외부 변수에 <strong>값을 할당(<code>=</code>)하는 순간</strong>, 그 변수를 <strong>지역 변수로 간주</strong>합니다.<br>외부 변수를 수정하려면 <code>global</code> 또는 <code>nonlocal</code> 을 명시해야 합니다.</p>
<h3 id="왜-에러가-발생할까">왜 에러가 발생할까?</h3>
<pre><code class="language-python">count = 0

def increment():
    count += 1   # UnboundLocalError 발생 💥

# count += 1 은 count = count + 1 과 동일
# 오른쪽 count를 읽으려는데, 이미 지역변수로 간주해서
# &quot;아직 할당 안 된 지역변수를 읽으려 했다!&quot; → 에러</code></pre>
<p>단순히 <strong>읽기만</strong> 할 때는 괜찮습니다.</p>
<pre><code class="language-python">count = 0

def print_count():
    print(count)   # 읽기만 함 → 전역변수 접근 가능 ✅

def increment():
    count += 1     # 할당 시도 → 지역변수로 간주 → 에러 💥</code></pre>
<h3 id="global---전역-변수-접근">global - 전역 변수 접근</h3>
<pre><code class="language-python">count = 0

def increment():
    global count   # 전역 변수 count를 사용하겠다고 선언
    count += 1

increment()
increment()
print(count)  # 2</code></pre>
<h3 id="nonlocal---바깥-함수-변수-접근">nonlocal - 바깥 함수 변수 접근</h3>
<p><code>global</code> 이 전역 변수라면, <code>nonlocal</code> 은 <strong>바로 바깥 함수의 변수</strong>에 접근할 때 사용합니다.</p>
<pre><code class="language-python">def outer():
    count = 0

    def inner():
        nonlocal count   # 바깥 함수의 count를 사용
        count += 1

    inner()
    inner()
    print(count)  # 2

outer()</code></pre>
<table>
<thead>
<tr>
<th>키워드</th>
<th>접근 범위</th>
<th>Java 유사 개념</th>
</tr>
</thead>
<tbody><tr>
<td>없음</td>
<td>지역 변수</td>
<td>메서드 내 지역 변수</td>
</tr>
<tr>
<td><code>global</code></td>
<td>전역 변수</td>
<td><code>static</code> 변수</td>
</tr>
<tr>
<td><code>nonlocal</code></td>
<td>바깥 함수 변수</td>
<td>없음 (Java는 중첩 함수 없음)</td>
</tr>
</tbody></table>
<hr>
<h2 id="클래스">클래스</h2>
<h3 id="self와-메서드">self와 메서드</h3>
<p>Python 클래스의 메서드는 반드시 <strong>첫 번째 매개변수로 <code>self</code>를 선언</strong>해야 합니다.<br><code>self</code>는 Java의 <code>this</code>와 동일하게, 인스턴스 자기 자신을 가리킵니다.</p>
<pre><code class="language-python">class MyClass:
    def my_method(self):       # 클래스 메서드 - self 필요
        print(self)

def my_function():             # 클래스 밖 일반 함수 - self 불필요
    print(&quot;일반 함수&quot;)</code></pre>
<p><code>self</code>는 컨벤션일 뿐 다른 이름을 써도 동작하지만, 반드시 <strong>첫 번째 매개변수</strong>여야 합니다.<br>Python이 메서드를 호출할 때 인스턴스를 첫 번째 인자로 자동 전달하기 때문입니다.</p>
<hr>
<h3 id="생성자-__init__">생성자 <code>__init__</code></h3>
<p>Python의 생성자는 반드시 <code>__init__</code> 이라는 이름을 사용합니다.<br>클래스를 <code>ClassName()</code> 형태로 호출하면 자동으로 실행됩니다.</p>
<p>Java와 가장 큰 차이는 <strong>별도의 필드 선언부가 없다</strong>는 점입니다.<br><code>__init__</code> 안에서 <code>self.변수명 = 값</code> 으로 쓰는 순간, 그 변수가 인스턴스 변수로 생성됩니다.</p>
<pre><code class="language-python"># Java
class Car {
    private String name;  // 필드 선언
    private String color;

    public Car(String name, String color) {
        this.name = name;
        this.color = color;
    }
}</code></pre>
<pre><code class="language-python"># Python - 별도 선언 없이 __init__ 에서 바로 생성
class Car:
    def __init__(self, name, color):
        self.name = name    # 이 순간 인스턴스 변수 생성
        self.color = color

# new 없이 클래스 이름 호출 → __init__ 자동 실행
my_car = Car(&quot;Sonata&quot;, &quot;White&quot;)</code></pre>
<p><code>__init__</code> 이 없어도 클래스 선언은 가능합니다.</p>
<pre><code class="language-python">class MyClass:
    pass          # 빈 클래스

obj = MyClass()   # 빈 객체 생성</code></pre>
<hr>
<h3 id="클래스-변수-vs-인스턴스-변수">클래스 변수 vs 인스턴스 변수</h3>
<pre><code class="language-python">class Car:
    count = 0        # 클래스 변수 - 모든 인스턴스가 공유 (Java의 static 변수)

    def __init__(self, name):
        Car.count += 1
        self.name = name   # 인스턴스 변수 - 각 인스턴스마다 독립적

car1 = Car(&quot;Sonata&quot;)
car2 = Car(&quot;Avante&quot;)

print(Car.count)    # 2 - 클래스 변수는 클래스명으로 접근
print(car1.name)    # &quot;Sonata&quot;
print(car2.name)    # &quot;Avante&quot;</code></pre>
<table>
<thead>
<tr>
<th></th>
<th>클래스 변수</th>
<th>인스턴스 변수</th>
</tr>
</thead>
<tbody><tr>
<td>선언 위치</td>
<td>클래스 바로 아래</td>
<td><code>__init__</code> 안에서 <code>self.변수명</code></td>
</tr>
<tr>
<td>공유 여부</td>
<td>모든 인스턴스가 공유</td>
<td>각 인스턴스마다 독립적</td>
</tr>
<tr>
<td>Java 유사 개념</td>
<td><code>static</code> 변수</td>
<td>일반 인스턴스 변수</td>
</tr>
</tbody></table>
<hr>
<h3 id="상속">상속</h3>
<p><code>class 자식클래스(부모클래스)</code> 형태로 상속을 나타냅니다.<br>Java의 <code>extends</code> 와 동일한 역할입니다.</p>
<pre><code class="language-python">class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f&quot;{self.name}이 소리를 냅니다&quot;


class Dog(Animal):              # Animal 상속
    def speak(self):            # 메서드 오버라이딩
        return f&quot;{self.name}이 짖습니다&quot;


dog = Dog(&quot;멍멍이&quot;)
print(dog.speak())              # 멍멍이이 짖습니다
print(isinstance(dog, Animal))  # True - Java의 instanceof</code></pre>
<p><code>super()</code> 로 부모 클래스의 메서드를 호출할 수 있습니다. Java와 동일합니다.</p>
<pre><code class="language-python">class Dog(Animal):
    def speak(self):
        parent_result = super().speak()   # 부모의 speak() 호출
        return f&quot;{self.name}이 짖습니다&quot;</code></pre>
<p>Python은 <strong>다중 상속</strong>도 지원합니다.</p>
<pre><code class="language-python">class MyClass:              # 상속 없음
    pass

class MyClass(ParentClass): # 단일 상속
    pass

class MyClass(A, B):        # 다중 상속 - Java에는 없는 기능
    pass</code></pre>
<hr>
<h3 id="타입-힌트">타입 힌트</h3>
<p>Python은 <strong>동적 타이핑</strong> 언어라 타입 선언이 필요 없습니다.<br>하지만 코드 가독성과 IDE 자동완성 지원을 위해 타입 힌트를 사용할 수 있습니다.</p>
<pre><code class="language-python"># 변수 타입 힌트
age: int = 10
name: str = &quot;jay&quot;

# 함수 매개변수 + 반환 타입 힌트
def greet(name: str, age: int) -&gt; str:
    return f&quot;{name}은 {age}살입니다&quot;

# 반환값 없을 때
def print_hello(name: str) -&gt; None:
    print(f&quot;안녕, {name}&quot;)</code></pre>
<blockquote>
<p><strong>주의</strong>: 타입 힌트는 <strong>강제성이 없습니다.</strong><br><code>age: int = 10</code> 으로 선언해도 <code>age = &quot;열&quot;</code> 로 바꿔도 에러가 발생하지 않습니다.<br>단지 힌트일 뿐이며, 강제하려면 별도 라이브러리(예: <code>pydantic</code>)를 사용해야 합니다.</p>
</blockquote>
<hr>
<h2 id="예외처리">예외처리</h2>
<p>Java의 <code>try-catch-finally</code>와 유사하지만, Python에는 <strong><code>else</code> 블록</strong>이 추가됩니다.</p>
<pre><code class="language-python">try:
    result = 10 / 2          # 예외 가능성 있는 코드
except ZeroDivisionError:    # 특정 예외 처리
    print(&quot;0으로 나눌 수 없어요&quot;)
else:
    print(f&quot;결과: {result}&quot;) # 예외가 발생하지 않았을 때만 실행
finally:
    print(&quot;항상 실행&quot;)        # 예외 여부와 관계없이 항상 실행</code></pre>
<h3 id="else-블록의-장점">else 블록의 장점</h3>
<p><code>else</code> 블록 덕분에 <strong>&quot;예외 가능성 있는 코드&quot;</strong> 와 <strong>&quot;성공했을 때 실행할 코드&quot;</strong> 를 명확히 분리할 수 있습니다.</p>
<pre><code class="language-python"># else 없이 - 예외와 무관한 코드가 try 안에 섞임 ❌
try:
    result = 10 / 2
    print(f&quot;결과: {result}&quot;)   # 이 코드는 예외 위험이 없는데 try 안에 있음
except ZeroDivisionError:
    print(&quot;0으로 나눌 수 없어요&quot;)

# else 사용 - 역할이 명확하게 분리됨 ✅
try:
    result = 10 / 2            # 예외 가능성 있는 코드만
except ZeroDivisionError:
    print(&quot;0으로 나눌 수 없어요&quot;)
else:
    print(f&quot;결과: {result}&quot;)   # 성공했을 때 실행할 코드
finally:
    print(&quot;항상 실행&quot;)</code></pre>
<h3 id="여러-예외-처리">여러 예외 처리</h3>
<pre><code class="language-python">try:
    value = int(input())
    result = 10 / value
except ValueError:
    print(&quot;숫자를 입력해주세요&quot;)
except ZeroDivisionError:
    print(&quot;0은 입력할 수 없어요&quot;)
except Exception as e:        # 모든 예외를 잡는 catch-all
    print(f&quot;알 수 없는 오류: {e}&quot;)</code></pre>
<hr>
<h2 id="raise">raise</h2>
<p>Java의 <code>throw</code> 와 동일한 역할입니다.<br>예외를 강제로 발생시킬 때 사용합니다.</p>
<h3 id="기본-사용법">기본 사용법</h3>
<pre><code class="language-python"># Java
throw new IllegalArgumentException(&quot;나이는 0 이상이어야 합니다&quot;);

# Python
raise ValueError(&quot;나이는 0 이상이어야 합니다&quot;)</code></pre>
<p><code>new</code> 키워드 없이 예외 클래스를 바로 호출하면 됩니다.</p>
<h3 id="커스텀-예외">커스텀 예외</h3>
<p>Java처럼 직접 예외 클래스를 만들 수 있습니다.</p>
<pre><code class="language-python"># Java
class AgeException extends RuntimeException {
    public AgeException(String message) {
        super(message);
    }
}</code></pre>
<pre><code class="language-python"># Python
class AgeException(Exception):
    def __init__(self, age):
        self.age = age
        super().__init__(f&quot;{age}는 유효하지 않은 나이입니다&quot;)</code></pre>
<h3 id="실전-활용-패턴">실전 활용 패턴</h3>
<pre><code class="language-python">def set_age(age):
    if not isinstance(age, int):
        raise TypeError(&quot;나이는 정수여야 합니다&quot;)
    if age &lt; 0:
        raise ValueError(&quot;나이는 0 이상이어야 합니다&quot;)
    return age</code></pre>
<h3 id="raise-단독-사용---예외-다시-던지기">raise 단독 사용 - 예외 다시 던지기</h3>
<p><code>raise</code> 를 단독으로 쓰면 현재 예외를 <strong>그대로 다시 던집니다.</strong><br>Java의 <code>throw e</code> 와 동일합니다.</p>
<pre><code class="language-python">try:
    set_age(-1)
except ValueError as e:
    print(f&quot;로그 기록: {e}&quot;)
    raise   # 잡은 예외를 그대로 다시 던짐</code></pre>
<h3 id="예외-체이닝">예외 체이닝</h3>
<p>Python에는 Java에 없는 <strong>예외 체이닝</strong> 문법도 있습니다.<br>원인 예외를 명시적으로 연결할 수 있습니다.</p>
<pre><code class="language-python">try:
    int(&quot;abc&quot;)
except ValueError as e:
    raise RuntimeError(&quot;변환 실패&quot;) from e

# RuntimeError: 변환 실패
#   caused by
# ValueError: invalid literal for int()...</code></pre>
<p>Java의 <code>new RuntimeException(&quot;변환 실패&quot;, e)</code> 와 동일한 개념입니다.</p>
<hr>
<h2 id="import">import</h2>
<p>Java의 <code>import</code> 와 유사하지만, Python은 <strong>모듈(파일) 단위</strong>로 import합니다.</p>
<h3 id="기본-import">기본 import</h3>
<pre><code class="language-python"># Java - 클래스 단위
import java.util.ArrayList;

# Python - 모듈 단위
import math

math.sqrt(16)   # 4.0 - 모듈명.함수명 으로 접근
math.pi         # 3.14159...</code></pre>
<h3 id="from-import---특정-대상만-가져오기">from import - 특정 대상만 가져오기</h3>
<pre><code class="language-python">from math import sqrt, pi

sqrt(16)   # 4.0 - 모듈명 없이 바로 사용
pi         # 3.14159...</code></pre>
<pre><code class="language-python"># 전부 가져오기 - 권장하지 않음 ⚠️
from math import *
# 어디서 온 함수인지 불분명해져 가독성이 떨어짐</code></pre>
<h3 id="as---별칭">as - 별칭</h3>
<pre><code class="language-python">import numpy as np              # 긴 이름을 줄여서 사용
from math import sqrt as sq     # 이름 충돌 방지

np.array([1, 2, 3])
sq(16)   # 4.0</code></pre>
<h3 id="모듈-vs-패키지">모듈 vs 패키지</h3>
<pre><code># Java
패키지 = com.example.project
클래스 = Calculator.java

# Python
모듈   = .py 파일
패키지 = 폴더 (+ __init__.py)</code></pre><pre><code>my_project/
├── main.py
├── calculator.py        # 모듈
└── utils/               # 패키지 (폴더)
    ├── __init__.py      # 이 파일이 있어야 패키지로 인식
    ├── string_utils.py
    └── math_utils.py</code></pre><pre><code class="language-python"># 모듈 import
import calculator
from calculator import add

# 패키지 import
from utils.math_utils import multiply
from utils import string_utils</code></pre>
<h3 id="__init__py"><code>__init__.py</code></h3>
<p>패키지 폴더 안에 있는 특수 파일입니다.<br>이 파일이 있어야 Python이 해당 폴더를 <strong>패키지로 인식</strong>합니다.</p>
<pre><code class="language-python"># utils/__init__.py 에 아래처럼 작성하면
from .string_utils import trim
from .math_utils import multiply

# 외부에서 이렇게 바로 접근 가능
from utils import trim, multiply</code></pre>
<blockquote>
<p>Python 3.3 부터는 <code>__init__.py</code> 없어도 패키지로 인식되지만,<br>명시적으로 두는 것이 관례입니다.</p>
</blockquote>
<h3 id="절대-import-vs-상대-import">절대 import vs 상대 import</h3>
<pre><code class="language-python"># 절대 import - 프로젝트 루트 기준 (권장 ✅)
from utils.math_utils import multiply

# 상대 import - 현재 파일 위치 기준
from .math_utils import multiply    # 같은 패키지 내
from ..main import something        # 상위 패키지</code></pre>
<table>
<thead>
<tr>
<th></th>
<th>절대 import</th>
<th>상대 import</th>
</tr>
</thead>
<tbody><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="내장-모듈-vs-외부-라이브러리">내장 모듈 vs 외부 라이브러리</h3>
<pre><code class="language-python"># 내장 모듈 - Python 설치 시 기본 포함 (Java의 java.util.* 과 동일)
import math
import os
import sys
import json
import datetime

# 외부 라이브러리 - pip 로 설치 필요 (Java의 Maven/Gradle 과 동일)
# pip install numpy
import numpy as np</code></pre>
<h3 id="자주-쓰는-내장-모듈">자주 쓰는 내장 모듈</h3>
<table>
<thead>
<tr>
<th>모듈</th>
<th>역할</th>
<th>Java 유사</th>
</tr>
</thead>
<tbody><tr>
<td><code>os</code></td>
<td>파일/디렉토리 조작</td>
<td><code>java.io.File</code></td>
</tr>
<tr>
<td><code>sys</code></td>
<td>인터프리터 정보</td>
<td><code>System</code></td>
</tr>
<tr>
<td><code>json</code></td>
<td>JSON 파싱/직렬화</td>
<td><code>ObjectMapper</code></td>
</tr>
<tr>
<td><code>datetime</code></td>
<td>날짜/시간</td>
<td><code>LocalDateTime</code></td>
</tr>
<tr>
<td><code>math</code></td>
<td>수학 함수</td>
<td><code>Math</code></td>
</tr>
<tr>
<td><code>random</code></td>
<td>난수 생성</td>
<td><code>Random</code></td>
</tr>
<tr>
<td><code>re</code></td>
<td>정규표현식</td>
<td><code>Pattern</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="__name__"><code>__name__</code></h2>
<p><code>__name__</code> 은 Python이 자동으로 관리하는 <strong>특수 변수(dunder variable)</strong> 입니다.<br>현재 파일이 <strong>직접 실행되는지, import 되는지</strong>에 따라 값이 달라집니다.</p>
<table>
<thead>
<tr>
<th>실행 방식</th>
<th><code>__name__</code> 값</th>
</tr>
</thead>
<tbody><tr>
<td><code>python calculator.py</code> 로 직접 실행</td>
<td><code>&quot;__main__&quot;</code></td>
</tr>
<tr>
<td><code>import calculator</code> 로 import</td>
<td><code>&quot;calculator&quot;</code> (모듈명)</td>
</tr>
</tbody></table>
<h3 id="왜-쓸까">왜 쓸까?</h3>
<pre><code class="language-python"># calculator.py

def add(a, b):
    return a + b

# if __name__ 없이 - import 시에도 print 가 실행돼버림 💥
print(add(3, 4))</code></pre>
<pre><code class="language-python"># calculator.py

def add(a, b):
    return a + b

# if __name__ 사용 - 직접 실행할 때만 실행 ✅
if __name__ == &quot;__main__&quot;:
    print(add(3, 4))</code></pre>
<p>Java의 <code>main()</code> 메서드와 비슷한 역할로,<br><strong>&quot;이 파일이 직접 실행될 때만 이 코드를 실행해라&quot;</strong> 는 진입점 역할을 합니다.</p>
<hr>
<h2 id="decorator">Decorator</h2>
<p>함수를 인자로 받아 기능을 추가한 새로운 함수를 반환하는 문법입니다.<br>Spring의 <strong>AOP(관점 지향 프로그래밍)</strong> 와 매우 유사한 개념입니다.</p>
<p>로깅, 실행 시간 측정, 권한 체크 등 <strong>핵심 로직과 부가 기능을 분리</strong>할 때 사용합니다.</p>
<pre><code class="language-python">def logger(func):
    def wrapper(*args, **kwargs):
        print(f&quot;{func.__name__} 호출됨&quot;)   # 함수 실행 전
        result = func(*args, **kwargs)      # 원래 함수 실행
        print(f&quot;{func.__name__} 종료됨&quot;)   # 함수 실행 후
        return result
    return wrapper

@logger
def add(a, b):
    return a + b

print(add(3, 4))
# add 호출됨
# add 종료됨
# 7</code></pre>
<h3 id="동작-원리">동작 원리</h3>
<p><code>@logger</code> 는 아래 코드와 <strong>완전히 동일</strong>합니다.</p>
<pre><code class="language-python">add = logger(add)
# add 함수를 logger로 감싸서 재할당
# 이후 add() 를 호출하면 실제로는 wrapper() 가 실행됨</code></pre>
<p>즉, <code>@</code> 문법은 &quot;이 함수를 decorator로 감싸라&quot; 는 문법적 설탕(syntactic sugar)입니다.</p>
<h3 id="wrapper에서-args-kwargs를-쓰는-이유">wrapper에서 <em>args, *</em>kwargs를 쓰는 이유</h3>
<pre><code class="language-python">def wrapper(*args, **kwargs):
    result = func(*args, **kwargs)</code></pre>
<p>decorator는 어떤 함수에도 붙을 수 있어야 합니다.<br><code>*args, **kwargs</code> 로 인자를 그대로 받아서 그대로 전달하면,<br>인자 형태가 어떻든 상관없이 범용적으로 동작합니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Spring AOP</th>
<th>Python Decorator</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>어노테이션 + 프레임워크 처리</td>
<td>언어 자체에서 지원</td>
</tr>
<tr>
<td>구현</td>
<td>프레임워크 의존</td>
<td>직접 구현 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="generator--yield">Generator &amp; yield</h2>
<p>일반 함수는 <code>return</code> 으로 값을 반환하면 함수가 종료됩니다.<br>반면 <code>yield</code> 는 값을 반환한 뒤 <strong>함수 실행을 일시정지</strong>하고, 다음 호출 시 그 자리에서 재개합니다.</p>
<p>이런 함수를 <strong>Generator 함수</strong>라고 하며, 호출하면 <strong>Generator 객체</strong>를 반환합니다.</p>
<pre><code class="language-python">def countdown(n):
    while n &gt; 0:
        yield n    # 값 반환 후 &quot;일시정지&quot;
        n -= 1     # next() 호출 시 여기서 재개

gen = countdown(3)   # Generator 객체 생성 (아직 실행 안 됨)

next(gen)   # 3 반환 → yield 지점에서 일시정지
next(gen)   # n -= 1 실행 → n=2 → 2 반환 → 다시 일시정지
next(gen)   # n -= 1 실행 → n=1 → 1 반환 → 다시 일시정지
next(gen)   # n=0 → while 조건 False → StopIteration 예외 발생</code></pre>
<p><code>for</code> 문으로도 자연스럽게 사용할 수 있습니다.</p>
<pre><code class="language-python">for num in countdown(3):
    print(num)
# 3
# 2
# 1</code></pre>
<h3 id="java와-비교">Java와 비교</h3>
<pre><code class="language-java">// Java - Iterator 직접 구현
class Countdown implements Iterator&lt;Integer&gt; {
    private int n;
    Countdown(int n) { this.n = n; }
    public boolean hasNext() { return n &gt; 0; }
    public Integer next() { return n--; }
}</code></pre>
<pre><code class="language-python"># Python - yield 한 줄로 끝
def countdown(n):
    while n &gt; 0:
        yield n
        n -= 1</code></pre>
<h3 id="메모리-효율">메모리 효율</h3>
<p>Generator의 핵심 장점은 <strong>메모리 효율</strong>입니다.<br>리스트는 모든 요소를 한 번에 메모리에 올리지만,<br>Generator는 <code>next()</code> 호출 시마다 값을 하나씩 계산합니다.</p>
<pre><code class="language-python"># 리스트 - 100만 개를 한 번에 메모리에 올림 💥
result = [x * 2 for x in range(1000000)]

# Generator - 필요할 때 하나씩 계산 ✅
result = (x * 2 for x in range(1000000))</code></pre>
<blockquote>
<p>List Comprehension의 <code>[]</code> 를 <code>()</code> 로 바꾸면 Generator가 됩니다.<br>데이터가 매우 크거나, 전체를 한 번에 쓰지 않을 때 유용합니다.</p>
</blockquote>
<hr>
<p><em>Java 개발자 관점에서 꼭 알아야 할 Python 핵심 개념 정리 완료!</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이썬 (1)]]></title>
            <link>https://velog.io/@jun-log/%ED%8C%8C%EC%9D%B4%EC%8D%AC1</link>
            <guid>https://velog.io/@jun-log/%ED%8C%8C%EC%9D%B4%EC%8D%AC1</guid>
            <pubDate>Wed, 13 May 2026 01:19:10 GMT</pubDate>
            <description><![CDATA[<h2 id="목차">목차</h2>
<ol>
<li><a href="#pass">pass</a></li>
<li><a href="#%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0">자료구조</a></li>
<li><a href="#-vs-is">== vs is</a></li>
<li><a href="#%EC%82%BC%ED%95%AD%EC%97%B0%EC%82%B0%EC%9E%90-%EB%8C%80%EC%B2%B4">삼항연산자 대체</a></li>
<li><a href="#%EC%8A%AC%EB%9D%BC%EC%9D%B4%EC%8B%B1">슬라이싱</a></li>
<li><a href="#list-comprehension">List Comprehension</a></li>
<li><a href="#%EB%94%95%EC%85%94%EB%84%88%EB%A6%AC">딕셔너리 (Dictionary)</a></li>
<li><a href="#f-string">f-string</a></li>
<li><a href="#enumerate">enumerate</a></li>
<li><a href="#zip">zip</a></li>
<li><a href="#for-else">for-else</a></li>
</ol>
<h2 id="pass">pass</h2>
<p>블록 안에 <strong>아무것도 하지 않겠다</strong> 는 의미입니다.</p>
<p>Python은 Java와 달리 빈 블록 <code>{}</code> 을 허용하지 않습니다.<br>Python은 들여쓰기로 블록을 구분하기 때문에, 블록 안에 반드시 뭔가 있어야 합니다.</p>
<pre><code class="language-java">// Java - 빈 블록 허용
class MyClass {}
void myFunction() {}</code></pre>
<pre><code class="language-python"># Python - 빈 블록 불가 → SyntaxError 발생
class MyClass:
    # 아무것도 없으면 에러!

# pass 로 해결
class MyClass:
    pass

def my_function():
    pass

if True:
    pass</code></pre>
<p>나중에 구현할 예정인 코드 자리를 잡아둘 때 자주 사용합니다.</p>
<pre><code class="language-python"># 인터페이스처럼 구조만 잡아두기
class Animal:
    def speak(self):
        pass   # 나중에 구현 예정

    def move(self):
        pass   # 나중에 구현 예정</code></pre>
<hr>
<h2 id="자료구조">자료구조</h2>
<h3 id="리스트-list">리스트 (List)</h3>
<p>Java의 <code>ArrayList</code>와 유사한 <strong>mutable(가변)</strong> 자료형입니다.<br><code>[]</code> 로 표기하며, 서로 다른 타입의 요소도 함께 담을 수 있습니다.</p>
<pre><code class="language-python">a = [1, 2, 3]
a.append(4)        # [1, 2, 3, 4] - 요소 추가
a + [5, 6]         # [1, 2, 3, 4, 5, 6] - 리스트 합치기
a * 2              # [1, 2, 3, 4, 1, 2, 3, 4] - 반복</code></pre>
<p>Java였다면 리스트 합치기에 <code>addAll()</code>, 반복은 루프를 직접 돌려야 했지만<br>Python은 <code>+</code>, <code>*</code> 연산자로 직관적으로 표현할 수 있습니다.</p>
<h4 id="참조-타입-주의">참조 타입 주의</h4>
<p>리스트는 <strong>참조 타입</strong>이므로 단순 대입은 복사가 아닙니다.<br>Java의 객체 참조와 동일하게 동작합니다.</p>
<pre><code class="language-python">a = [1, 2, 3]
b = a           # b는 a와 동일한 리스트를 가리킴
b.append(4)
print(a)        # [1, 2, 3, 4] - a도 같이 변경됨!

# 진짜 복사를 원한다면
b = a.copy()    # shallow copy
b = a[:]        # 슬라이싱으로 복사 (Python스러운 방식)
b = list(a)     # list() 생성자 활용</code></pre>
<hr>
<h3 id="튜플-tuple">튜플 (Tuple)</h3>
<p><code>()</code> 로 표기하며, <strong>immutable(불변)</strong> 자료형입니다.<br>한 번 생성하면 요소를 추가, 수정, 삭제할 수 없습니다.</p>
<pre><code class="language-python">a = (1, 2, 3)
a[0] = 99   # TypeError: &#39;tuple&#39; object does not support item assignment</code></pre>
<blockquote>
<p>Java의 <code>final List</code>는 참조만 못 바꾸는 것이지만,<br>Python의 tuple은 <strong>내부 요소 자체를 아예 변경할 수 없습니다.</strong></p>
</blockquote>
<p>언제 튜플을 쓸까요?<br>변경되면 안 되는 데이터(좌표, 설정값 등)를 담을 때, 또는 함수에서 여러 값을 반환할 때 자연스럽게 사용됩니다.</p>
<pre><code class="language-python">point = (10, 20)           # 좌표처럼 변하면 안 되는 값
return min(a), max(a)      # 함수의 다중 반환값 (내부적으로 tuple)</code></pre>
<table>
<thead>
<tr>
<th></th>
<th>List</th>
<th>Tuple</th>
</tr>
</thead>
<tbody><tr>
<td>문법</td>
<td><code>[1, 2, 3]</code></td>
<td><code>(1, 2, 3)</code></td>
</tr>
<tr>
<td>수정</td>
<td>✅ 가능</td>
<td>❌ 불가능</td>
</tr>
<tr>
<td>Java 유사 개념</td>
<td><code>ArrayList</code></td>
<td>불변 객체</td>
</tr>
</tbody></table>
<hr>
<h2 id="-vs-is">== vs is</h2>
<p>Python의 <code>==</code> 와 <code>is</code> 는 전혀 다른 비교를 합니다.</p>
<table>
<thead>
<tr>
<th>Python</th>
<th>Java</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>==</code></td>
<td><code>.equals()</code></td>
<td><strong>값</strong> 비교</td>
</tr>
<tr>
<td><code>is</code></td>
<td><code>==</code></td>
<td><strong>객체 동일성</strong> 비교 (같은 객체인가?)</td>
</tr>
</tbody></table>
<pre><code class="language-python">a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)   # True  - 값이 같음
print(a is b)   # False - 서로 다른 객체</code></pre>
<h3 id="none-비교는-is-를-권장">None 비교는 is 를 권장</h3>
<p><code>None</code> 은 전역에 딱 하나만 존재하는 객체이기 때문에,<br>값 비교보다 객체 동일성 비교가 더 정확하고 빠릅니다.</p>
<pre><code class="language-python">a = None

if a is None:      # ✅ 권장
    pass
if a == None:      # 동작하지만 비권장
    pass</code></pre>
<h3 id="주의---정수-캐싱">주의 - 정수 캐싱</h3>
<p>Python은 <strong>-5 ~ 256 사이의 정수</strong>를 미리 캐싱해두기 때문에,<br>해당 범위 안의 숫자는 <code>is</code> 비교가 <code>True</code> 로 나옵니다.</p>
<pre><code class="language-python">a = 1
b = 1
print(a is b)     # True  - 캐싱 범위 내

a = 1000
b = 1000
print(a is b)     # False - 캐싱 범위 밖, 다른 객체</code></pre>
<p>Java의 <code>Integer</code> 캐싱(<code>-128 ~ 127</code>)과 동일한 개념입니다.<br>따라서 <strong>숫자, 문자열에는 <code>is</code> 를 쓰지 않는 것이 원칙</strong>이고,<br><code>is</code> 는 <code>None</code>, <code>True</code>, <code>False</code> 비교에만 쓰는 것이 관례입니다.</p>
<hr>
<h2 id="삼항연산자-대체">삼항연산자 대체</h2>
<p>Java의 삼항연산자 <code>? :</code> 를 Python에서는 아래와 같이 표현합니다.</p>
<pre><code class="language-python"># 기본 문법
참일때의_값 if 조건식 else 거짓일때의_값</code></pre>
<pre><code class="language-java">// Java
String result = x &gt; 5 ? &quot;크다&quot; : &quot;작다&quot;;</code></pre>
<pre><code class="language-python"># Python
result = &quot;크다&quot; if x &gt; 5 else &quot;작다&quot;</code></pre>
<p>Java와 <strong>순서가 반대</strong>라는 점을 주의하세요.<br>Java는 <code>조건 ? 참 : 거짓</code> 이지만, Python은 <code>참 if 조건 else 거짓</code> 입니다.</p>
<p>f-string 안에서도 사용할 수 있습니다.</p>
<pre><code class="language-python">age = 20
print(f&quot;{&#39;성인&#39; if age &gt;= 18 else &#39;미성년자&#39;}&quot;)  # &quot;성인&quot;</code></pre>
<hr>
<h2 id="슬라이싱">슬라이싱</h2>
<p>순서가 있는 자료형(list, tuple, string)의 일부를 잘라내는 기법입니다.<br>Java의 <code>subList()</code>, <code>substring()</code> 을 훨씬 간결하게 표현할 수 있습니다.</p>
<h3 id="기본-문법">기본 문법</h3>
<pre><code>[start:end:step]</code></pre><ul>
<li><code>start</code>: 시작 인덱스 (<strong>포함</strong>, 기본값 0)</li>
<li><code>end</code>: 끝 인덱스 (<strong>미포함</strong>, 기본값 len(a))</li>
<li><code>step</code>: 간격 (<strong>기본값 1</strong>, 생략 가능)</li>
</ul>
<p><code>end</code>가 <strong>미포함</strong>이라는 점을 꼭 기억하세요.<br><code>a[1:4]</code> 는 index 1, 2, 3 까지이고 4는 포함되지 않습니다.</p>
<h3 id="생략-예시">생략 예시</h3>
<pre><code class="language-python">a = [0, 1, 2, 3, 4]

a[1:4]    # [1, 2, 3]       - index 1~3 (4 미포함)
a[:3]     # [0, 1, 2]       - start 생략 → 0부터
a[2:]     # [2, 3, 4]       - end 생략 → 끝까지
a[:]      # [0, 1, 2, 3, 4] - 전체 복사
a[::2]    # [0, 2, 4]       - 하나씩 건너뜀 (step=2)
a[::-1]   # [4, 3, 2, 1, 0] - 역순 (step=-1)</code></pre>
<h3 id="step이-음수일-때">step이 음수일 때</h3>
<p>step이 음수이면 <strong>역방향</strong>으로 순회하기 때문에 기본값이 바뀝니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>step &gt; 0</th>
<th>step &lt; 0</th>
</tr>
</thead>
<tbody><tr>
<td>start 기본값</td>
<td>0 (처음부터)</td>
<td>len(a) - 1 (끝부터)</td>
</tr>
<tr>
<td>end 기본값</td>
<td>len(a) (끝까지)</td>
<td>처음 요소까지 포함</td>
</tr>
</tbody></table>
<pre><code class="language-python">a = [0, 1, 2, 3, 4]
a[::-1]     # [4, 3, 2, 1, 0] - 끝에서 처음까지 역순
a[3::-1]    # [3, 2, 1, 0]    - index 3부터 역순</code></pre>
<h3 id="슬라이싱-가능한-자료형">슬라이싱 가능한 자료형</h3>
<p>순서가 있는(sequential) 자료형이면 모두 슬라이싱이 가능합니다.</p>
<pre><code class="language-python">[1, 2, 3][1:]    # 리스트 ✅ → [2, 3]
(1, 2, 3)[1:]    # 튜플 ✅  → (2, 3)
&quot;hello&quot;[1:3]     # 문자열 ✅ → &quot;el&quot;

# Java와 비교
&quot;hello&quot;.substring(1, 3)  // Java  → &quot;el&quot;
&quot;hello&quot;[1:3]             # Python → &quot;el&quot;</code></pre>
<hr>
<h2 id="list-comprehension">List Comprehension</h2>
<p>리스트를 간결하게 생성하는 Python의 핵심 문법입니다.<br>Java의 Stream API와 개념이 유사하지만, 훨씬 짧게 표현할 수 있습니다.</p>
<h3 id="기본-문법-1">기본 문법</h3>
<pre><code class="language-python">[표현식 for 변수 in 반복가능객체 if 조건]</code></pre>
<p>읽는 순서대로 해석하면 이렇습니다.</p>
<blockquote>
<p>&quot;반복가능객체에서 변수를 하나씩 꺼내서, 조건을 만족하는 것만, 표현식으로 변환해 리스트로 만든다&quot;</p>
</blockquote>
<p><code>if 조건</code> 은 생략 가능합니다.</p>
<h3 id="예시">예시</h3>
<pre><code class="language-python">numbers = [1, 2, 3, 4, 5]

# 조건 없이 - 모든 요소에 *2
[x * 2 for x in numbers]
# → [2, 4, 6, 8, 10]

# 조건 있이 - 짝수만 추출 후 *2
[x * 2 for x in numbers if x % 2 == 0]
# → [4, 8]</code></pre>
<h3 id="java-stream-api와-비교">Java Stream API와 비교</h3>
<pre><code class="language-java">// Java
numbers.stream()
    .filter(x -&gt; x % 2 == 0)
    .map(x -&gt; x * 2)
    .collect(Collectors.toList());</code></pre>
<pre><code class="language-python"># Python - 한 줄로 끝
[x * 2 for x in numbers if x % 2 == 0]</code></pre>
<h3 id="중첩-list-comprehension">중첩 List Comprehension</h3>
<p>2차원 리스트를 1차원으로 펼칠 때도 사용할 수 있습니다.</p>
<pre><code class="language-python">matrix = [[1, 2, 3], [4, 5, 6]]

# &quot;matrix의 각 row에서 x를 꺼내 펼친다&quot;
[x for row in matrix for x in row]
# → [1, 2, 3, 4, 5, 6]</code></pre>
<hr>
<h2 id="딕셔너리">딕셔너리</h2>
<p>Java의 <code>HashMap</code>에 해당하는 자료형입니다.<br><code>{key: value}</code> 형태로 표기하며, key-value 쌍으로 데이터를 저장합니다.</p>
<h3 id="기본-사용법">기본 사용법</h3>
<pre><code class="language-python">person = {
    &quot;name&quot;: &quot;Alice&quot;,
    &quot;age&quot;: 30
}

# 요소 추가 / 수정
person[&quot;email&quot;] = &quot;alice@example.com&quot;

# 요소 삭제
del person[&quot;age&quot;]</code></pre>
<h3 id="요소-접근">요소 접근</h3>
<p>딕셔너리에서 값을 꺼내는 방법은 두 가지입니다.</p>
<pre><code class="language-python">person[&quot;name&quot;]              # &quot;Alice&quot;
person[&quot;email&quot;]             # 키가 없으면 KeyError 발생 💥

person.get(&quot;email&quot;)         # 키가 없으면 None 반환 ✅
person.get(&quot;email&quot;, &quot;없음&quot;) # 키가 없으면 기본값 반환 ✅</code></pre>
<blockquote>
<p><strong><code>[]</code> 접근 vs <code>.get()</code> 접근</strong><br>키가 반드시 존재한다고 확신할 때 → <code>[]</code><br>키가 없을 수도 있을 때 → <code>.get()</code> (안전)</p>
</blockquote>
<blockquote>
<p>Python의 <code>None</code>은 Java의 <code>null</code>에 해당합니다.</p>
</blockquote>
<table>
<thead>
<tr>
<th></th>
<th>Java HashMap</th>
<th>Python dict</th>
</tr>
</thead>
<tbody><tr>
<td>생성</td>
<td><code>new HashMap&lt;&gt;()</code></td>
<td><code>{}</code></td>
</tr>
<tr>
<td>값 접근</td>
<td><code>map.get(&quot;key&quot;)</code></td>
<td><code>dict[&quot;key&quot;]</code> 또는 <code>dict.get(&quot;key&quot;)</code></td>
</tr>
<tr>
<td>키 없을 때</td>
<td><code>null</code> 반환</td>
<td><code>dict[&quot;key&quot;]</code> → KeyError 발생</td>
</tr>
<tr>
<td>기본값 지정</td>
<td><code>getOrDefault(&quot;key&quot;, &quot;기본값&quot;)</code></td>
<td><code>dict.get(&quot;key&quot;, &quot;기본값&quot;)</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="f-string">f-string</h2>
<p>Python 3.6부터 도입된 문자열 포맷팅 방법으로,<br>Java의 <code>String.format()</code> 보다 훨씬 직관적입니다.</p>
<p>문자열 앞에 <code>f</code> 를 붙이고, <code>{}</code> 안에 변수나 표현식을 넣으면 됩니다.</p>
<pre><code class="language-python">name = &quot;Alice&quot;
age = 30

# Java
String.format(&quot;이름: %s, 나이: %d&quot;, name, age)  // &quot;이름: Alice, 나이: 30&quot;

# Python f-string
f&quot;이름: {name}, 나이: {age}&quot;                     # &quot;이름: Alice, 나이: 30&quot;</code></pre>
<h3 id="-안에-표현식도-사용-가능">{} 안에 표현식도 사용 가능</h3>
<p>단순 변수 삽입뿐 아니라, 연산이나 메서드 호출도 바로 넣을 수 있습니다.</p>
<pre><code class="language-python">f&quot;{2 + 3}&quot;                                    # &quot;5&quot;
f&quot;{name.upper()}&quot;                             # &quot;ALICE&quot;
f&quot;{&#39;짝수&#39; if age % 2 == 0 else &#39;홀수&#39;}&quot;       # &quot;짝수&quot;
f&quot;원주율: {3.14159:.2f}&quot;                      # &quot;원주율: 3.14&quot; - 소수점 2자리</code></pre>
<hr>
<h2 id="enumerate">enumerate</h2>
<p>리스트를 순회할 때 <strong>인덱스와 요소를 함께</strong> 가져올 수 있게 해줍니다.</p>
<pre><code class="language-python">fruits = [&quot;apple&quot;, &quot;banana&quot;, &quot;kiwi&quot;]

# Java
for (int i = 0; i &lt; fruits.size(); i++) {
    System.out.println(i + &quot; &quot; + fruits.get(i));
}

# Python - enumerate 없이
for i in range(len(fruits)):
    print(i, fruits[i])

# Python - enumerate 사용 ✅
for i, fruit in enumerate(fruits):
    print(i, fruit)
# 0 apple
# 1 banana
# 2 kiwi</code></pre>
<h3 id="동작-원리">동작 원리</h3>
<p><code>enumerate</code> 는 내부적으로 <strong>(index, 요소) 형태의 tuple</strong> 을 반환하는 iterator입니다.<br><code>for i, fruit in</code> 은 이 tuple을 <strong>Unpacking</strong> 하는 것입니다.</p>
<pre><code class="language-python">enumerate(fruits)
# → (0, &quot;apple&quot;), (1, &quot;banana&quot;), (2, &quot;kiwi&quot;) 를 하나씩 반환하는 iterator

# 즉, 아래와 동일
for item in enumerate(fruits):
    i, fruit = item   # Unpacking
    print(i, fruit)</code></pre>
<h3 id="시작-인덱스-지정">시작 인덱스 지정</h3>
<p>기본값은 0이지만, 시작 인덱스를 변경할 수 있습니다.</p>
<pre><code class="language-python">for i, fruit in enumerate(fruits, start=1):
    print(i, fruit)
# 1 apple
# 2 banana
# 3 kiwi</code></pre>
<hr>
<h2 id="zip">zip</h2>
<p><strong>여러 iterable을 같은 인덱스끼리 묶어서</strong> tuple로 반환합니다.<br>두 리스트를 동시에 순회할 때 유용합니다.</p>
<pre><code class="language-python">fruits = [&quot;apple&quot;, &quot;banana&quot;, &quot;kiwi&quot;]
prices = [1000, 2000, 3000]

# Java - 인덱스로 직접 접근
for (int i = 0; i &lt; fruits.size(); i++) {
    System.out.println(fruits.get(i) + &quot; &quot; + prices.get(i));
}

# Python - zip 사용 ✅
for fruit, price in zip(fruits, prices):
    print(fruit, price)
# apple 1000
# banana 2000
# kiwi 3000</code></pre>
<h3 id="동작-원리-1">동작 원리</h3>
<p><code>zip</code> 은 내부적으로 <strong>같은 인덱스의 요소를 tuple로 묶은</strong> iterator입니다.</p>
<pre><code class="language-python">zip(fruits, prices)
# → (&quot;apple&quot;, 1000), (&quot;banana&quot;, 2000), (&quot;kiwi&quot;, 3000) 을 하나씩 반환

# for fruit, price in 은 tuple을 Unpacking 하는 것</code></pre>
<h3 id="길이가-다를-때">길이가 다를 때</h3>
<p>리스트 길이가 다르면 <strong>짧은 쪽에 맞춰서</strong> 끊깁니다.</p>
<pre><code class="language-python">a = [1, 2, 3, 4, 5]
b = [10, 20, 30]

for x, y in zip(a, b):
    print(x, y)
# 1 10
# 2 20
# 3 30   ← b가 끝나면 중단</code></pre>
<h3 id="enumerate--zip-함께-사용">enumerate + zip 함께 사용</h3>
<pre><code class="language-python">for i, (fruit, price) in enumerate(zip(fruits, prices)):
    print(f&quot;{i}번: {fruit} - {price}원&quot;)
# 0번: apple - 1000원
# 1번: banana - 2000원
# 2번: kiwi - 3000원</code></pre>
<hr>
<h2 id="for-else">for-else</h2>
<p>반복문이 <strong><code>break</code> 없이 정상적으로 끝났을 때</strong> <code>else</code> 블록이 실행됩니다.<br><code>break</code> 로 탈출하면 <code>else</code> 는 실행되지 않습니다.</p>
<pre><code class="language-python">numbers = [1, 2, 3, 4, 5]

# break 없이 끝난 경우 → else 실행
for n in numbers:
    print(n)
else:
    print(&quot;반복 완료!&quot;)   # 실행됨

# break로 탈출한 경우 → else 실행 안 됨
for n in numbers:
    if n == 3:
        break
else:
    print(&quot;반복 완료!&quot;)   # 실행 안 됨</code></pre>
<p>예외처리의 <code>try-else</code> 와 같은 철학입니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>else 실행 O</th>
<th>else 실행 X</th>
</tr>
</thead>
<tbody><tr>
<td><code>try-else</code></td>
<td>예외 없이 정상 종료</td>
<td>예외 발생</td>
</tr>
<tr>
<td><code>for-else</code></td>
<td>break 없이 정상 종료</td>
<td>break로 탈출</td>
</tr>
</tbody></table>
<h3 id="실전-활용---찾으면-break-못-찾으면-else-패턴">실전 활용 - &quot;찾으면 break, 못 찾으면 else&quot; 패턴</h3>
<pre><code class="language-python"># Java - 별도 flag 변수 필요
boolean found = false;
for (int n : numbers) {
    if (n == 3) { found = true; break; }
}
if (!found) System.out.println(&quot;못 찾았어요&quot;);</code></pre>
<pre><code class="language-python"># Python - flag 변수 없이 깔끔하게
for n in numbers:
    if n == 3:
        break
else:
    print(&quot;못 찾았어요&quot;)  # break 없이 끝나면 실행</code></pre>
<hr>
<hr>
<p><em>→ 2편에서 with, 함수, 클래스, 예외처리, import, Decorator, Generator 를 이어서 다룹니다.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[벡터]]></title>
            <link>https://velog.io/@jun-log/%EB%B2%A1%ED%84%B0</link>
            <guid>https://velog.io/@jun-log/%EB%B2%A1%ED%84%B0</guid>
            <pubDate>Wed, 06 May 2026 02:09:17 GMT</pubDate>
            <description><![CDATA[<h2 id="벡터">벡터</h2>
<p>크기와 방향을 동시에 갖는 양</p>
<p>평면 위의 화살표 하나를 떠올려 봐요. <strong>시작점은 원점(0,0)</strong>이고, <strong>끝점이 (3, 2)</strong>인 화살표예요.</p>
<pre><code>y
│
2 ─ ─ ─ •  ← 끝점 (3, 2)
│      ╱
│     ╱
│    ╱   ← 화살표
│   ╱
│  ╱
│ ╱
└────────── x
0    3</code></pre><p>이 화살표를 묘사하는 두 가지 방법:</p>
<p><strong>방법 A (기하학적):</strong> &quot;원점에서 출발해서, 길이 $\sqrt{13}$만큼, 오른쪽 위 약 33.7도 방향으로 뻗은 화살표&quot;</p>
<p><strong>방법 B (대수적):</strong> &quot;오른쪽으로 3, 위로 2 가는 화살표&quot; → $(3, 2)$</p>
<p>$$$
\vec{b} = \begin{pmatrix} 3 \ 2 \end{pmatrix}
$$$</p>
<h2 id="벡터의-기본-연산">벡터의 기본 연산</h2>
<h3 id="1-덧셈--이어-붙이기">1) 덧셈 — &quot;이어 붙이기&quot;</h3>
<p>두 벡터를 더하면 같은 자리끼리 더합니다.</p>
<p>$$\begin{pmatrix} 3 \ 2 \end{pmatrix} + \begin{pmatrix} 1 \ 4 \end{pmatrix} = \begin{pmatrix} 4 \ 6 \end{pmatrix}$$</p>
<h3 id="2-스칼라-곱--늘이기줄이기">2) 스칼라 곱 — &quot;늘이기/줄이기&quot;</h3>
<p>벡터에 숫자(스칼라)를 곱하면 모든 성분에 그 숫자가 곱해집니다.</p>
<p>$$2 \cdot \begin{pmatrix} 3 \ 2 \end{pmatrix} = \begin{pmatrix} 6 \ 4 \end{pmatrix}$$</p>
<p>기하학적으로는 <strong>방향은 그대로, 길이만 2배</strong>가 됩니다. 음수를 곱하면 방향이 반대가 되고요.</p>
<p>$$-1 \cdot \begin{pmatrix} 3 \ 2 \end{pmatrix} = \begin{pmatrix} -3 \ -2 \end{pmatrix} \quad (\text{반대 방향})$$</p>
<h3 id="3-크기-길이-구하기">3) 크기 (길이) 구하기</h3>
<p>벡터의 &quot;크기&quot;는 <strong>피타고라스 정리</strong>로 구합니다.</p>
<p>$$\vec{v} = \begin{pmatrix} 3 \ 4 \end{pmatrix} \Rightarrow |\vec{v}| = \sqrt{3^2 + 4^2} = \sqrt{25} = 5$$</p>
<p>직각삼각형의 빗변 길이를 구하는 것과 똑같아요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[행렬]]></title>
            <link>https://velog.io/@jun-log/%ED%96%89%EB%A0%AC</link>
            <guid>https://velog.io/@jun-log/%ED%96%89%EB%A0%AC</guid>
            <pubDate>Wed, 06 May 2026 01:36:24 GMT</pubDate>
            <description><![CDATA[<h2 id="행렬matrix">행렬(Matrix)</h2>
<p>숫자나 기호를 직사각형 모양으로 배열한 것.
m개의 행(row)과 n개의 열(column)로 이루어진 행렬을 <code>m × n 행렬</code>이라고 한다.</p>
<p>ex) 2 × 3 행렬</p>
<p>$$A = \begin{pmatrix} 1 &amp; 2 &amp; 3 \ 4 &amp; 5 &amp; 6 \end{pmatrix}$$</p>
<h3 id="단위행렬-identity-matrix-i">단위행렬 (Identity Matrix, $I$)</h3>
<p>n × n 정사각행렬로, 대각선 원소가 1이고 그 외 나머지 원소는 0인 행렬.</p>
<p>$$I_3 = \begin{pmatrix} 1 &amp; 0 &amp; 0 \ 0 &amp; 1 &amp; 0 \ 0 &amp; 0 &amp; 1 \end{pmatrix}$$</p>
<h3 id="단위행렬과의-곱셈">단위행렬과의 곱셈</h3>
<p>어떤 수에 1을 곱하면 그대로인 것처럼, 어떤 행렬에 단위행렬을 곱하면 그대로이다.
$$AI = IA = A$$</p>
<h2 id="행렬-곱셈">행렬 곱셈</h2>
<blockquote>
<p>&quot;앞 행렬의 가로줄(행)과 뒤 행렬의 세로줄(열)을 짝지어, 곱하고 더한다&quot;
&quot;i번째 줄, j번째 칸&quot;의 값은 → A의 i번째 가로줄과 B의 j번째 세로줄의 만남으로 만들어진다.</p>
</blockquote>
<h3 id="계산-예시">계산 예시</h3>
<p>$$\begin{pmatrix} 1 &amp; 2 \ 3 &amp; 4 \end{pmatrix} \begin{pmatrix} 5 &amp; 6 \ 7 &amp; 8 \end{pmatrix} = \begin{pmatrix} 19 &amp; 22 \ 43 &amp; 50 \end{pmatrix}$$</p>
<p>(예: 좌상단 19 = 1×5 + 2×7)</p>
<h3 id="행렬-곱셈-가능-조건">행렬 곱셈 가능 조건</h3>
<p>$$\underbrace{(m \times \boxed{n})}<em>{\text{앞 행렬}} \cdot \underbrace{(\boxed{n} \times p)}</em>{\text{뒤 행렬}} = (m \times p)$$</p>
<p>→ <strong>가운데 숫자는 같아야 하고(곱셈 가능 조건), 바깥 숫자가 결과 크기가 된다.</strong></p>
<h3 id="행렬-곱셈의-성질">행렬 곱셈의 성질</h3>
<ul>
<li><strong>교환법칙 ❌</strong>: 일반적으로 $AB \neq BA$</li>
<li><strong>결합법칙 ✅</strong>: $(AB)C = A(BC)$</li>
<li><strong>분배법칙 ✅</strong>: $A(B+C) = AB + AC$</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ML]]></title>
            <link>https://velog.io/@jun-log/ML</link>
            <guid>https://velog.io/@jun-log/ML</guid>
            <pubDate>Mon, 04 May 2026 01:24:15 GMT</pubDate>
            <description><![CDATA[<h2 id="1-모델의-기초-model-basics">1. 모델의 기초 (Model Basics)</h2>
<ul>
<li><strong>모델 (Model):</strong> 아키텍처(Architecture) + 파라미터(Parameters)<ul>
<li><strong>아키텍처:</strong> 모델의 뼈대이자 구조. (비유: 함수의 코드 로직)</li>
<li><strong>파라미터 (가중치):</strong> 아키텍처 내부를 채우는 수많은 숫자들. 학습을 통해 얻어진 결과물. (비유: 그 함수가 사용하는 거대한 상수 테이블)</li>
</ul>
</li>
<li><strong>사전 학습된 모델 (Pre-trained Model):</strong> 아키텍처의 구조는 그대로 둔 채, 방대한 텍스트 데이터를 학습시켜 파라미터 값을 최적의 상태로 채워 넣은 완성된 모델.</li>
</ul>
<h2 id="2-모델은-어떻게-배우는가-학습-프로세스">2. 모델은 어떻게 배우는가? (학습 프로세스)</h2>
<p>비어있던 파라미터를 똑똑하게 갱신하여 모델을 만드는 과정입니다.</p>
<ol>
<li><strong>Forward Pass (순전파):</strong> 입력값을 모델에 통과시켜 결과 예측값인 <strong>Logits</strong>을 반환받음.<ul>
<li><em>예) 입력: &quot;오늘 저녁에 밥&quot; $\rightarrow$ 모델 통과 $\rightarrow$ Logits 출력</em></li>
</ul>
</li>
<li><strong>Loss 계산 (오차 측정):</strong> 모델의 예측(Logits)과 실제 정답 토큰(예: &quot; 먹었어&quot;)을 비교.<ul>
<li>정답 토큰에 모델이 매긴 점수가 낮다면 틀렸다고 판정하고, 그 격차만큼 오차(Loss)를 계산함.</li>
</ul>
</li>
<li><strong>Backward Pass (역전파):</strong> 이 오차(Loss)를 줄이기 위해, 수많은 파라미터 각각을 &#39;어느 방향으로, 얼마나 바꿔야 하는지&#39; 역으로 계산하며 내려감.</li>
<li><strong>파라미터 갱신:</strong> 계산된 방향대로 파라미터(숫자들)를 미세하게 조정함.</li>
</ol>
<p><em>(이 과정을 수만 번 반복하며, 학습이 완전히 끝나면 파라미터는 고정됨)</em></p>
<h2 id="3-모델은-어떻게-글을-생성하는가-추론-프로세스">3. 모델은 어떻게 글을 생성하는가? (추론 프로세스)</h2>
<p>학습이 끝나고 <strong>고정된 파라미터</strong>를 이용해 실제로 답변을 만들어내는 과정입니다.</p>
<h3 id="핵심-동작-원리">핵심 동작 원리</h3>
<ul>
<li><strong>자기회귀 생성 (Autoregressive Generation):</strong> 모델이 입력을 받아 바로 다음에 올 &#39;가장 적절한 토큰 1개&#39;를 생성하고, 그것을 다시 기존 입력 뒤에 붙여서(append) 다음 토큰을 예측하는 행위. 이를 지정된 횟수(<code>max_new_tokens</code>)만큼 반복하여 문장을 완성함.</li>
<li><strong>추론 시의 Forward Pass:</strong> 입력을 모델에 한 번 통과시켜 다음 토큰에 대한 점수판(Logits)을 얻어내는 1회성 과정.</li>
</ul>
<h3 id="점수에서-확률로-logits--softmax">점수에서 확률로 (Logits &amp; Softmax)</h3>
<ul>
<li><strong>Logits (로짓):</strong> 모델이 어휘집(Vocabulary) 전체의 모든 토큰 각각에 대해 매긴 원시 점수 벡터.<ul>
<li>벡터의 길이는 어휘집 크기와 동일.</li>
<li>확률이 아니므로 음수도 될 수 있고, 총합이 1도 아님.</li>
</ul>
</li>
<li><strong>Softmax (소프트맥스):</strong> 다루기 힘든 Logits 점수들을 <strong>&quot;총합이 1인 확률 분포&quot;</strong>로 예쁘게 변환해 주는 수학 함수.</li>
</ul>
<h3 id="어떤-단어를-선택할-것인가-디코딩-전략">어떤 단어를 선택할 것인가? (디코딩 전략)</h3>
<p>Softmax를 통해 확률 분포가 나왔다면, 이제 다음 토큰을 뽑을 차례입니다.</p>
<ul>
<li><strong>Greedy (탐욕 탐색):</strong> 1등만 뽑는다. 확률 분포 중 무조건 가장 확률이 높은 토큰만 선택. (항상 똑같은 답변이 나옴)</li>
<li><strong>Sampling (샘플링):</strong> 확률대로 뽑는다. 분포 비율에 비례한 &#39;추첨&#39;을 통해 토큰을 선택. (창의적이고 다양한 답변이 나옴)</li>
</ul>
<blockquote>
<p><strong>💡 다양한 샘플링 기법 (Temperature &amp; Top-p)</strong></p>
<ul>
<li><strong>Temperature (온도 조절):</strong> Softmax를 적용하기 전, Logits 값을 $T$로 나누어 분포의 모양 자체를 바꿈.<ul>
<li><strong>$T &gt; 1$ 일 때 (예: 2로 나눔):</strong> 값들이 전체적으로 작아지며 토큰 간의 점수 격차가 줄어듦 $\rightarrow$ <strong>확률 분포가 평탄해짐</strong> (다양하고 엉뚱한 단어가 나올 확률 증가).</li>
<li><strong>$T &lt; 1$ 일 때 (예: 0.5로 나눔):</strong> 0.5로 나누는 것은 2를 곱하는 것과 같음. 값들이 커지며 점수 격차가 벌어짐 $\rightarrow$ <strong>확률 분포가 뾰족해짐</strong> (1등 단어가 뽑힐 확률이 압도적으로 높아짐).</li>
</ul>
</li>
<li><strong>Top-p (후보 압축):</strong> 확률이 높은 순서대로 누적합을 구하다가, 지정한 $p$값(예: 0.9)에 도달하면 그 아래의 꼬리(확률이 낮은 토큰들)를 추첨 후보에서 아예 잘라버림.<ul>
<li>분포의 모양(비율)은 유지하되, 지나치게 엉뚱한 단어가 나오는 것을 막아주는 <strong>보수적인 추첨</strong> 방식.</li>
</ul>
</li>
</ul>
</blockquote>
<h2 id="4-완성된-모델을-입맛에-맞게-쓰는-법">4. 완성된 모델을 입맛에 맞게 쓰는 법</h2>
<ul>
<li><strong>프롬프트 엔지니어링 (Prompt Engineering):</strong><ul>
<li>파라미터 변경 없음.</li>
<li>원하는 응답의 형태나 맥락을 입력값(프롬프트)에 잘 적어서 원하는 결과를 유도하는 방식.</li>
<li><strong>In-context Learning (문맥 내 학습):</strong> 프롬프트 엔지니어링의 핵심 원리로, 모델이 문맥을 읽고 그 패턴을 따라 하도록 만드는 것.</li>
<li><strong>Few-shot (퓨샷):</strong> 모델에게 소수의 예시(2~5개)를 직접 프롬프트에 제공하여 새로운 작업에 빠르게 적응하도록 유도하는 대표적인 기법.</li>
</ul>
</li>
<li><strong>파인튜닝 (Fine-Tuning):</strong><ul>
<li>파라미터 변경 있음 (영구적).</li>
<li>특정 도메인이나 말투에 맞게 모델을 &#39;추가 학습&#39; 시켜 내부 파라미터를 살짝 깎고 다듬는 작업.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[양방향 암호화]]></title>
            <link>https://velog.io/@jun-log/%EC%96%91%EB%B0%A9%ED%96%A5-%EC%95%94%ED%98%B8%ED%99%94</link>
            <guid>https://velog.io/@jun-log/%EC%96%91%EB%B0%A9%ED%96%A5-%EC%95%94%ED%98%B8%ED%99%94</guid>
            <pubDate>Wed, 29 Apr 2026 07:51:29 GMT</pubDate>
            <description><![CDATA[<h2 id="양방향-암호화">양방향 암호화</h2>
<p>복호화가 가능한 암호화 방식</p>
<h3 id="대칭키-vs-비대칭키">대칭키 VS 비대칭키</h3>
<ul>
<li>대칭키: 암/복호화시 사용하는 키가 동일한 방식</li>
<li>비대칭키: 암호화 -&gt; 공개키, 복호화 -&gt; 비밀(개인)키를 사용하는 방식</li>
</ul>
<h3 id="aes">AES</h3>
<p>대표적인 대칭키 양방향 암호화 방식</p>
<h3 id="블록-암호">블록 암호</h3>
<p>평문을 고정된 크기의 블록으로 잘라서 한 블록씩 암호화 하는방식
AES의 블록크기는 16바이트로 고정되어있다.</p>
<p>예를들어, 평문의 길이가 32바이트라면 2개의 암호화블록으로 구성된 암호가 생성된다.
만약, 평문의 길이가 16의 배수가 아니라면 마지막 블록은 빈 자리를 채워넣어 16바이트로 맞춘다.
이것을 <strong>패딩</strong> 이라고 부른다.</p>
<h3 id="운영모드">운영모드</h3>
<p>암호화하는 여러가지 방식이 있다.</p>
<h3 id="ecb모드">ECB모드</h3>
<p>가장 직관적인 방법으로</p>
<pre><code>블록1 → AES(키) → 암호화 블록1
블록2 → AES(키) → 암호화 블록2
블록3 → AES(키) → 암호화 블록3</code></pre><p>문제는, 같은 평문 블록은 항상 같은 암호문 블록이 되니, 암호안에서 동일한 암호문 블록이 반복될 수 있다.
이러한 패턴이 읽혀 암호가 풀릴 수 있기에 사용하면 안되는 방식으로 분류된다.</p>
<h3 id="cbc모드">CBC모드</h3>
<p>IV를 이용해 암호화블록을 한번더 꼬는 방식</p>
<blockquote>
</blockquote>
<p>IV란? 암호화에 사용되는 랜덤한 초기값이다.
같은 평문이 매번 다른 암호문이 되도록 보장해준다.
해시의 Salt와 동일한 역할이다.</p>
<h3 id="암호화-프로세스">암호화 프로세스</h3>
<pre><code>블록1: 평문블록1 XOR IV       → AES(키) → 암호문블록1
블록2: 평문블록2 XOR 암호문블록1 → AES(키) → 암호문블록2
블록3: 평문블록3 XOR 암호문블록2 → AES(키) → 암호문블록3
저장형식: [IV(16바이트)] [암호문] //Salt처럼 공개되어도 상관이없다. </code></pre><p>실제로 IV가 사용되는건 첫번째 평문블록을 암호화할때만이다.
매 암호화마다 IV를 수정해서 암호화해줘야 한다. 동일한 IV를 지속적으로 사용하면, 동일한 입력에 동일한 암호가 생성되어진다.</p>
<h3 id="복호화-프로세스">복호화 프로세스</h3>
<pre><code>평문블록1 = AES_decrypt(암호문블록1, 키) XOR IV
평문블록2 = AES_decrypt(암호문블록2, 키) XOR 암호문블록1
평문블록3 = AES_decrypt(암호문블록3, 키) XOR 암호문블록2</code></pre><p>AES는 암호화보다 복호화가 더 빠르다.
왜냐하면 복호화는 병렬로 실행이 가능하기때문이다.
평문블록으로 복호화하는데 필요한건 자기 블록과 이전 블록뿐이기때문이다.
이러한 이유로, 특정 블록이 손상되어도 손상이 전파되지않는다.
필요한건 자기 자신과 이전 블록 뿐이기때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[bcrypt]]></title>
            <link>https://velog.io/@jun-log/bcrypt-1d265to5</link>
            <guid>https://velog.io/@jun-log/bcrypt-1d265to5</guid>
            <pubDate>Wed, 29 Apr 2026 05:11:47 GMT</pubDate>
            <description><![CDATA[<h2 id="bcrypt">bcrypt</h2>
<p>대표적인 해시함수 <code>SHA-256</code>은 입력값을 아주 빠르게 해시값으로 변환할 수 있다.
요즘 GPU는 SHA-256을 초당 수십억 번 계산할 수 있어서, 입력값을 Salting해도 무차별 대입으로 암호를 풀 가능성이 있다.</p>
<p>이러한 경우를 보완하기위해 고안된것이 <strong>비밀번호 저장 전용 해시 알고리즘</strong>인 <code>bcrypt</code>이다.</p>
<h3 id="bcrypt의-핵심-아이디어">bcrypt의 핵심 아이디어</h3>
<blockquote>
</blockquote>
<p>&quot;느리게 만든다.&quot;</p>
<p>내부적으로 해시 연산을 수천 ~ 수십만번 반복시켜 한 번의 해싱에 더 많은 시간이 걸리게 만든다.
그래서, 무차별 대입시 SHA-256보다 훨씬 더 많은 시간이 소요된다.</p>
<p>이때, cost factor는 해당 해시값에 해시연산이 몇번 반복되었는지를 나타낸다.
cost factor가 12라면 $2^{12}$ 번만큼 해시 연산되었다는 의미이다.</p>
<pre><code>초기 테이블 → 비밀번호+salt로 1번 변형 → 비밀번호로 다시 변형 → salt로 다시 변형 → ...
                                          (이걸 무수히 반복)
                ↓
            최종 테이블로 입력값 암호화 → 해시 결과</code></pre><h3 id="bcrypt-출력-형식">bcrypt 출력 형식</h3>
<pre><code>$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
│  │  │  └────── salt(22자) ──────┘└────── hash(31자) ──────┘
│  │  └ cost factor (12)
│  └ 알고리즘 식별자 (2b = bcrypt)
└ 구분자</code></pre><ul>
<li>bcrypt의 해시값은 60자로 고정되어있다.</li>
<li>hash안에 salt와 cost factor가 포함되어있다.</li>
</ul>
<h3 id="bcrypt-해싱-프로세스">bcrypt 해싱 프로세스</h3>
<p>bcrypt에선 특정값을 해싱할때, 매번 랜덤한 salt값을 적용하여 해싱한다.
그래서, 같은 입력에도 매번 다른 해시값을 반환한다.</p>
<p>그럼 bcrypt은 어떻게 해시값을 검증할까?</p>
<h3 id="bcrypt-검증방식">bcrypt 검증방식</h3>
<p>bcrypt은 일반 해시처럼 단순 해시값끼리 비교하여 검증하지않는다.
검증하기위해 입력값과 해시값이 필요하다.</p>
<pre><code>bcrypt.verify(입력_비밀번호, 저장 해시값)</code></pre><p>해시값안에 salt와 cost factor가 저장되는 이유가 여기에 있다.
파라미터로 받은 해시값에서 salt와 cost factor를 추출해, 입력값에 동일하게 해싱하여 해싱된 값을 비교하여 검증한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[해시 함수]]></title>
            <link>https://velog.io/@jun-log/%ED%95%B4%EC%8B%9C%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@jun-log/%ED%95%B4%EC%8B%9C%ED%95%A8%EC%88%98</guid>
            <pubDate>Wed, 29 Apr 2026 03:00:51 GMT</pubDate>
            <description><![CDATA[<h2 id="해시-함수란">해시 함수란?</h2>
<p>임의 길이의 입력값을 받아 고정 길이의 출력으로 반환하는 함수.</p>
<h2 id="왜-쓰는가">왜 쓰는가?</h2>
<ul>
<li>비밀번호 저장 (원본 없이 검증)</li>
<li>무결성 검증 (데이터 변조 확인)</li>
<li>데이터 식별 (Git 커밋 ID, 중복 제거)</li>
<li>디지털 서명, HMAC 등 더 큰 암호 시스템의 빌딩 블록</li>
</ul>
<p>※ 흔히 &quot;단방향 암호화&quot;라고 부르지만 엄밀히는 암호화 아님.
   복원 불가능한 변환이라 정확한 용어는 &quot;암호학적 해시 함수&quot;.</p>
<h2 id="핵심-성질">핵심 성질</h2>
<ol>
<li><p>결정성 (Deterministic)</p>
<ul>
<li>같은 입력은 항상 같은 출력.</li>
<li>해시값 비교로 사용자 인증이 가능한 이유.</li>
</ul>
</li>
<li><p>단방향성 (One-way)</p>
<ul>
<li>출력에서 입력을 역산할 수 없음.</li>
<li>비밀번호를 해시값으로 저장하는 이유.</li>
</ul>
</li>
<li><p>충돌 저항성 (Collision Resistance)</p>
<ul>
<li>다른 입력이 같은 출력을 내는 경우 = 충돌.</li>
<li>입력은 무한, 출력은 고정 길이라 충돌은 이론적으로 존재.</li>
<li>하지만 좋은 해시 함수는 그 충돌을 &quot;찾는 것&quot;이 사실상 불가능해야 함.</li>
<li>충돌이 쉬우면 디지털 서명 위조, 인증 우회 같은 공격이 가능해짐.</li>
<li>MD5(2004), SHA-1(2017)은 이미 깨짐. 현재는 SHA-256 이상 사용 권장.</li>
</ul>
</li>
</ol>
<h2 id="레인보우테이블">레인보우테이블</h2>
<p>특정 입력값에는 특정 해시값이 나오는것을 미리 계산하여 저장해둔 테이블이다.
공격자들은 레인보우테이블을 미리 만들어 해시값을 유추하여 해킹공격을 할 수 있었다.</p>
<p>이러한 공격을 어렵게 만들기 위한 방법이 <strong>salting</strong>이다.</p>
<h2 id="salt">Salt</h2>
<p>Salt란 해시값을 꼬기 위해 입력값에 추가로 붙이는 랜덤 값.
<code>해시할_값 = {사용자 비밀번호} + salt</code></p>
<h3 id="효과">효과</h3>
<ol>
<li><p>레인보우 테이블 무력화</p>
<ul>
<li>사용자마다 고유한 salt를 부여하면, 공격자는 사용자 수만큼
별도의 테이블을 만들어야 함 → 비용이 비현실적으로 커짐</li>
</ul>
</li>
<li><p>같은 비밀번호 사용자 식별 방지</p>
<ul>
<li>사용자 A와 B가 우연히 같은 비밀번호를 써도, salt가 다르면
DB에 저장된 해시값이 완전히 달라짐</li>
<li>한 명이 뚫려도 다른 사람은 영향 없음</li>
</ul>
</li>
</ol>
<h3 id="중요-salt는-비밀이-아님">중요: Salt는 비밀이 아님</h3>
<ul>
<li>DB에 평문으로 저장됨 (검증 시 다시 사용해야 하므로)</li>
<li>Salt의 목적은 &quot;비밀 유지&quot;가 아니라 &quot;각 비밀번호를 유일하게 만드는 것&quot;</li>
</ul>
<h2 id="대표-해시-함수">대표 해시 함수</h2>
<ul>
<li>안전: SHA-256, SHA-512, SHA-3</li>
<li>깨짐: MD5, SHA-1 (보안 용도 사용 금지)</li>
</ul>
<h2 id="비밀번호-저장-전용">비밀번호 저장 전용</h2>
<ul>
<li>일반 해시 함수는 너무 빠름 → 무차별 대입에 취약</li>
<li>bcrypt, scrypt, Argon2 같은 의도적으로 느린 함수 사용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB]Index]]></title>
            <link>https://velog.io/@jun-log/DBIndex</link>
            <guid>https://velog.io/@jun-log/DBIndex</guid>
            <pubDate>Tue, 28 Apr 2026 07:33:34 GMT</pubDate>
            <description><![CDATA[<h2 id="인덱스란-무엇인가">인덱스란 무엇인가</h2>
<p>인덱스는 <strong>데이터를 빨리 찾기 위해, 미리 정렬해서 따로 저장해둔 자료구조</strong>다.</p>
<p>도서관에 비유하면 이렇다.</p>
<ul>
<li><strong>테이블</strong> = 책장 전체. 1만 권의 책이 꽂혀 있다.</li>
<li><strong>인덱스</strong> = 검색용 컴퓨터. 책 자체는 없고, &quot;책 제목 → 위치&quot;만 정렬해서 보관한다.</li>
</ul>
<p>책을 찾으려면 두 가지 방법이 있다.</p>
<ol>
<li>1층부터 5층까지 모든 책장을 한 권씩 다 뒤진다 → <strong>풀 테이블 스캔</strong></li>
<li>검색 컴퓨터로 위치를 먼저 찾고, 그곳으로 간다 → <strong>인덱스 조회</strong></li>
</ol>
<p>당연히 2번이 빠르다. 인덱스는 &quot;검색 컴퓨터를 만들어두는 작업&quot;이라고 생각하면 된다.</p>
<hr>
<h2 id="인덱스는-언제-쓰이는가">인덱스는 언제 쓰이는가</h2>
<table>
<thead>
<tr>
<th>트리거</th>
<th>무슨 일을 하는가</th>
</tr>
</thead>
<tbody><tr>
<td><strong>1. 필터링</strong></td>
<td><code>WHERE</code> / <code>JOIN ON</code> 으로 읽을 행을 좁힌다</td>
</tr>
<tr>
<td><strong>2. 정렬</strong></td>
<td><code>ORDER BY</code> 를 인덱스의 정렬 순서로 대체해서, 정렬 연산 자체를 생략한다</td>
</tr>
<tr>
<td><strong>3. 커버링</strong></td>
<td><code>SELECT</code> 에 필요한 컬럼이 전부 인덱스 안에 있어, 테이블 접근을 생략한다</td>
</tr>
</tbody></table>
<p>아래에서 각 트리거를 예제와 함께 하나씩 본다. 예제는 모두 같은 인덱스를 가정한다.</p>
<pre><code class="language-sql">-- 이 섹션 전체에서 가정하는 인덱스
CREATE INDEX idx_orders ON orders(user_id, created_at);</code></pre>
<h3 id="트리거-1--필터링-where--join-on">트리거 1 — 필터링 (WHERE / JOIN ON)</h3>
<p>가장 익숙한 경우다. 조건에 맞는 행만 골라내기 위해 인덱스로 점프한다.</p>
<pre><code class="language-sql">-- O 선두 컬럼 user_id로 바로 좁힌다
SELECT * FROM orders WHERE user_id = 100;

-- O JOIN ON도 결국 필터링이다 (조인 키로 인덱스를 탄다)
SELECT *
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.id = 100;</code></pre>
<p>여기까지가 &quot;WHERE가 있어야 인덱스를 쓴다&quot;는 직관에 들어맞는 영역이다. 문제는 인덱스가 <strong>이것만 하는 게 아니라는</strong> 점이다.</p>
<h3 id="트리거-2--정렬-order-by">트리거 2 — 정렬 (ORDER BY)</h3>
<p><code>ORDER BY</code> 컬럼이 인덱스의 정렬 순서와 일치하면, DB는 인덱스를 순서대로 읽기만 하면 된다. 
즉 <strong>정렬 연산(filesort) 자체가 사라진다.</strong> </p>
<pre><code class="language-sql">-- O WHERE 없이 정렬 용도로만 인덱스 사용
--   인덱스가 (user_id, created_at)로 이미 정렬돼 있으니
--   그 순서 그대로 읽으면 정렬 끝
SELECT * FROM orders ORDER BY user_id, created_at LIMIT 10;

-- O 역방향도 가능 (인덱스를 거꾸로 읽으면 됨)
SELECT * FROM orders ORDER BY user_id DESC, created_at DESC LIMIT 10;

-- X 인덱스 정렬 순서와 어긋나면 정렬 연산이 다시 필요
--   (created_at만으로는 선두 컬럼 user_id 순서가 보장되지 않음)
SELECT * FROM orders ORDER BY created_at LIMIT 10;</code></pre>
<h3 id="트리거-3--커버링--select-컬럼">트리거 3 — 커버링  (SELECT 컬럼)</h3>
<p><code>SELECT</code> 에 필요한 컬럼이 전부 인덱스 안에 들어 있으면, 테이블 본체를 읽으러 갈 필요가 없다. 
이걸 <code>커버링(Index Only Scan)</code>이라 한다.</p>
<pre><code class="language-sql">-- O 필요한 컬럼(user_id, created_at)이 인덱스에 다 있다 → 테이블 룩업 생략
SELECT user_id, created_at FROM orders ORDER BY user_id, created_at;

-- X amount는 인덱스에 없다 → 결국 테이블로 가야 함 (커버링 깨짐)
SELECT user_id, created_at, amount FROM orders WHERE user_id = 100;</code></pre>
<p>확인하는 법: InnoDB는 <code>EXPLAIN</code>의 <code>Extra</code>에 <code>Using index</code>가 뜨면 커버링이다. PostgreSQL은 <code>Index Only Scan</code>으로 표기된다.</p>
<blockquote>
<p>InnoDB 보너스: 세컨더리 인덱스 리프에 PK가 자동으로 붙으므로, <code>(user_id, created_at)</code> 인덱스는 사실상 <code>(user_id, created_at, id)</code>처럼 동작한다. 그래서 <code>SELECT id</code>나 <code>SELECT user_id, created_at, id</code>도 커버링이 된다.</p>
</blockquote>
<h3 id="그리고--트리거는-겹쳐서-발동한다">그리고 — 트리거는 겹쳐서 발동한다</h3>
<p>실전에서는 한 쿼리가 여러 트리거를 동시에 건다. 이게 인덱스 설계의 묘미다.</p>
<pre><code class="language-sql">-- 필터링(WHERE user_id) + 정렬(ORDER BY created_at)을 한 인덱스로 동시에 처리
-- (user_id, created_at) 인덱스 하나로 둘 다 공짜
SELECT * FROM orders
WHERE user_id = 100
ORDER BY created_at DESC
LIMIT 10;</code></pre>
<p><code>user_id = 100</code> 으로 구간을 좁힌 뒤, 그 구간 안은 이미 <code>created_at</code> 순으로 정렬돼 있으니 정렬도 따라온다. </p>
<hr>
<h2 id="데이터를-읽는-4가지-방식">데이터를 읽는 4가지 방식</h2>
<p>DB가 SELECT 쿼리를 처리할 때 데이터를 읽어오는 방식은 크게 4가지다. 빠른 순서로 정리하면 이렇다.</p>
<h3 id="1-풀-테이블-스캔-seq-scan">1. 풀 테이블 스캔 (Seq Scan)</h3>
<p>테이블의 모든 행을 처음부터 끝까지 읽는 방식이다.</p>
<blockquote>
<p>도서관 비유: <strong>모든 책장의 모든 책을 한 권씩 다 꺼내본다.</strong> 1만 권을 다 보는 것.</p>
</blockquote>
<p>인덱스가 없거나, 있어도 활용할 수 없을 때 이 방식이 쓰인다. 또는 옵티마이저가 &quot;어차피 거의 모든 행을 가져와야 하니 그냥 다 읽는 게 빠르다&quot;고 판단할 때도 발생한다.</p>
<blockquote>
<p>용어 메모: <code>Seq Scan</code> 은 PostgreSQL의 실행계획 표기다. MySQL/InnoDB 계열에서는 <code>EXPLAIN</code> 의 <code>type</code> 이 <code>ALL</code> 로 나오는 것이 이에 해당한다.</p>
</blockquote>
<h3 id="2-인덱스-풀-스캔-index-scan">2. 인덱스 풀 스캔 (Index Scan)</h3>
<p>인덱스의 처음부터 끝까지 다 훑는 방식이다.</p>
<blockquote>
<p>도서관 비유: <strong>검색 컴퓨터에 키워드를 입력하지 않고, 전체 도서 목록을 처음부터 끝까지 스크롤해서 본다.</strong></p>
</blockquote>
<p>테이블 자체보다 인덱스가 작기 때문에, 풀 테이블 스캔보다는 빠르다. 하지만 정상적인 인덱스 활용에 비하면 여전히 비효율적이다.</p>
<h3 id="3-인덱스-액세스--테이블-룩업">3. 인덱스 액세스 + 테이블 룩업</h3>
<p>가장 일반적인 인덱스 활용 방식이다. 두 단계로 진행된다.</p>
<ol>
<li>인덱스에서 조건에 맞는 행의 <strong>위치</strong>를 찾는다</li>
<li>그 위치를 들고 실제 테이블로 가서 행 데이터를 읽어온다</li>
</ol>
<blockquote>
<p>도서관 비유: <strong>검색 컴퓨터로 &quot;해리포터 → 3층 F구역&quot;을 알아낸 뒤, 실제로 3층까지 가서 책을 꺼낸다.</strong></p>
</blockquote>
<p>여기서 2단계, 즉 &quot;테이블로 다시 가는 행위&quot;를 <strong>테이블 룩업</strong> 또는 <strong>북마크 룩업</strong>이라고 부른다. 룩업이 많아지면 디스크 I/O 비용이 커져서 느려진다.</p>
<blockquote>
<p>용어 메모: PostgreSQL은 테이블 본체를 <strong>힙(heap)</strong> 이라 부르고, 인덱스는 힙의 위치를 가리키는 포인터만 갖는다. 그래서 &quot;힙 방문(heap fetch)&quot;이라는 표현을 쓴다. <strong>반면 InnoDB(MariaDB 기본 엔진)에는 힙 개념이 없다.</strong> InnoDB는 테이블 자체가 PK 기준으로 정렬된 <strong>클러스터드 인덱스</strong>라서, 세컨더리 인덱스 → 클러스터드 인덱스 순으로 타고 들어간다. 같은 &quot;룩업&quot;이라도 엔진별 내부 구조가 다르다.</p>
</blockquote>
<h3 id="4-커버링-인덱스-index-only-scan">4. 커버링 인덱스 (Index Only Scan)</h3>
<p>쿼리에 필요한 모든 컬럼이 이미 인덱스 안에 들어있는 경우, 테이블 룩업 자체를 생략할 수 있다.</p>
<blockquote>
<p>도서관 비유: <strong>검색 컴퓨터에 &quot;위치, 작가, 출판년도&quot;가 다 표시돼 있어서 굳이 책장까지 안 가도 답이 나온다.</strong></p>
</blockquote>
<p>이런 인덱스를 <strong>커버링 인덱스</strong>라고 한다. 4가지 방식 중 가장 빠르다.</p>
<pre><code class="language-sql">-- 인덱스: (user_id, name, email)
SELECT name, email FROM users WHERE user_id = 100;
-- → 인덱스에 name, email이 다 있으니 테이블 안 봐도 됨</code></pre>
<blockquote>
<p>엔진 차이 주의: InnoDB는 세컨더리 인덱스의 리프에 PK가 자동으로 따라붙는다.
그래서 인덱스를 <code>(created_at)</code> 으로만 만들어도 사실상 <code>(created_at, id)</code> 처럼 동작해서 <code>SELECT id</code> 가 저절로 커버된다.
PostgreSQL은 그렇지 않다. 진짜 Index Only Scan을 원하면 <code>(created_at, id)</code> 또는 <code>created_at INCLUDE (id)</code> 처럼 명시해야 한다.</p>
</blockquote>
<hr>
<h2 id="복합-인덱스">복합 인덱스</h2>
<p>복합 인덱스는 <strong>여러 컬럼</strong>을 묶어서 만든 인덱스다.</p>
<p>예를 들어, 아래와 같은 복합 인덱스를 생성한다고 하면</p>
<pre><code class="language-sql">CREATE INDEX idx_user_created ON orders(user_id, created_at);</code></pre>
<blockquote>
<p>1순위로 <code>user_id</code> 기준 정렬, 같은 <code>user_id</code> 안에서 2순위로 <code>created_at</code> 기준 정렬하여 인덱싱한다는 의미다.</p>
</blockquote>
<p>비유하자면 전화번호부와 같다. 성으로 먼저 정렬되고, 같은 성을 가진 사람들끼리 다시 이름순으로 정렬된다.</p>
<pre><code>전화번호부:
  김민수
  김지영
  김철수
  박서준
  박혜수</code></pre><p>이 구조에서 어떤 검색이 가능할까?</p>
<ul>
<li>&quot;김씨 찾기&quot; → 가능. 정렬돼 있으니 한 번에 점프</li>
<li>&quot;김씨 중 가나다순 첫 사람&quot; → 가능. 이미 정렬돼 있음</li>
<li>&quot;전체에서 &#39;철수&#39; 찾기&quot; → 어렵다. 이름으로 정렬된 게 아니라서</li>
</ul>
<p>이걸 SQL로 옮기면 이렇다.</p>
<pre><code class="language-sql">-- 인덱스: (user_id, created_at)

-- O 인덱스 잘 활용 (필터링 용도)
SELECT * FROM orders WHERE user_id = 100;

-- O 정렬도 공짜 (같은 user_id 안에서 이미 시간순)
SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 10;

-- O WHERE가 아예 없어도 인덱스가 켜진다 (정렬 + 커버링 용도)
--   ORDER BY가 인덱스 정렬 순서와 일치 → 정렬 연산 생략
--   SELECT 컬럼이 인덱스로 전부 덮임 → 테이블 룩업 생략
SELECT user_id, created_at FROM orders ORDER BY user_id, created_at LIMIT 10;

-- X 인덱스 활용 못 함 (선두 컬럼 user_id가 조건/정렬에 없음)
SELECT * FROM orders WHERE created_at &gt; &#39;2025-01-01&#39;;</code></pre>
<p>세 번째 쿼리가 핵심이다. <strong>WHERE 없이도, ORDER BY와 SELECT 컬럼만으로 인덱스가 쓰인다.</strong></p>
<h3 id="leftmost-prefix-rule">Leftmost Prefix Rule</h3>
<p>위 패턴에서 발견할 수 있는 규칙이 <strong>&quot;왼쪽 컬럼부터 순서대로 써야 인덱스가 효율적으로 동작한다&quot;</strong>는 원칙이다. 흔히 <strong>Leftmost Prefix Rule</strong>이라고 부른다.</p>
<p>이 규칙 때문에 복합 인덱스를 설계할 때는 <strong>첫 번째 컬럼이 항상 조건(또는 정렬)으로 들어온다는 전제</strong>로 만들어야 한다. 첫 번째 컬럼 없이 두 번째 컬럼만으로 조회하면, 앞서 설명한 인덱스 풀 스캔으로 떨어지거나 풀 테이블 스캔으로 떨어진다.</p>
<hr>
<h2 id="인덱스-설계-시-고려할-점">인덱스 설계 시 고려할 점</h2>
<h3 id="카디널리티">카디널리티</h3>
<p><strong>카디널리티(Cardinality)는 컬럼 값의 다양성</strong>을 의미한다.</p>
<ul>
<li><code>gender</code> 컬럼: M / F 두 가지 → 카디널리티 낮음</li>
<li><code>email</code> 컬럼: 사용자마다 거의 다름 → 카디널리티 높음</li>
</ul>
<p>일반적으로 <strong>카디널리티가 높은 컬럼을 첫 번째에 두는 것이 유리</strong>하다. 검색 결과를 더 좁게 좁힐 수 있기 때문이다.</p>
<pre><code class="language-sql">-- (board_id, user_id) vs (user_id, board_id)
-- 게시판이 10개, 유저가 10만 명일 때
-- → user_id를 앞에 두는 게 일반적으로 유리</code></pre>
<p>다만 절대적인 규칙은 아니다. <strong>실제 쿼리 패턴</strong>에 따라 달라진다. &quot;항상 board_id로만 조회한다면&quot; board_id를 앞에 둬야 한다.</p>
<h3 id="인덱스의-비용">인덱스의 비용</h3>
<p>인덱스는 공짜가 아니다.</p>
<ul>
<li><strong>저장 공간</strong>을 추가로 차지한다</li>
<li><strong>INSERT, UPDATE, DELETE 시 인덱스도 갱신</strong>해야 하므로 쓰기 성능이 느려진다</li>
<li>인덱스가 많을수록 옵티마이저가 어떤 인덱스를 쓸지 고민하는 시간이 늘어난다</li>
</ul>
<p>그래서 인덱스는 <strong>실제로 자주 사용되는 쿼리 패턴을 분석한 뒤</strong>에 만들어야 한다. &quot;혹시 모르니 일단 만들자&quot;는 좋지 않은 접근이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[mysql: MAKEDATE]]></title>
            <link>https://velog.io/@jun-log/mysql-MAKEDATE</link>
            <guid>https://velog.io/@jun-log/mysql-MAKEDATE</guid>
            <pubDate>Wed, 01 Apr 2026 05:51:56 GMT</pubDate>
            <description><![CDATA[<h2 id="makedate함수-완전-정리">MAKEDATE()함수 완전 정리</h2>
<hr>
<h3 id="기본-문법">기본 문법</h3>
<pre><code class="language-sql">MAKEDATE(year, dayofyear) : date타입 반환</code></pre>
<table>
<thead>
<tr>
<th>매개변수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>year</code></td>
<td>연도 (숫자)</td>
</tr>
<tr>
<td><code>dayofyear</code></td>
<td>해당 연도의 <strong>몇 번째 날</strong>인지 (1 = 1월 1일)</td>
</tr>
</tbody></table>
<hr>
<h3 id="핵심-개념-dayofyear">핵심 개념: dayofyear</h3>
<p>두 번째 인자가 <strong>월/일이 아니라 &quot;연중 몇 번째 날&quot;</strong> 이라는 점이 포인트예요.</p>
<pre><code class="language-sql">MAKEDATE(2025, 1)    -- 2025-01-01  (1월 1일 = 연중 1번째 날)
MAKEDATE(2025, 31)   -- 2025-01-31  (1월 31일 = 연중 31번째 날)
MAKEDATE(2025, 32)   -- 2025-02-01  (2월 1일 = 연중 32번째 날)
MAKEDATE(2025, 365)  -- 2025-12-31
MAKEDATE(2024, 366)  -- 2024-12-31  (2024년은 윤년이라 366일)</code></pre>
<hr>
<h3 id="특이-동작-365-초과값">특이 동작: 365 초과값</h3>
<p>dayofyear가 365를 넘으면 <strong>다음 연도로 넘어가요.</strong></p>
<pre><code class="language-sql">MAKEDATE(2025, 366)  -- 2026-01-01  (2025년은 윤년 아님)
MAKEDATE(2025, 400)  -- 2026-02-04</code></pre>
<hr>
<h3 id="null-반환-케이스">NULL 반환 케이스</h3>
<pre><code class="language-sql">MAKEDATE(2025, 0)    -- NULL (0번째 날은 없음)
MAKEDATE(0, 1)       -- NULL (연도 0은 허용 안 함)
MAKEDATE(NULL, 1)    -- NULL
MAKEDATE(2025, NULL) -- NULL</code></pre>
<hr>
<h3 id="실전-활용-예시">실전 활용 예시</h3>
<pre><code class="language-sql">-- 해당 연도의 첫째날
MAKEDATE(YEAR(NOW()), 1)          -- 2025-01-01

-- 특정 날짜가 속한 연도의 첫째날
MAKEDATE(YEAR(&#39;2025-11-03&#39;), 1)   -- 2025-01-01

-- DAYOFYEAR()와 조합 (날짜 → 연중 일수 → 다시 날짜)
MAKEDATE(2025, DAYOFYEAR(&#39;2025-03-15&#39;))  -- 2025-03-15</code></pre>
<hr>
<h3 id="dayofyear와-세트로-기억하기">DAYOFYEAR()와 세트로 기억하기</h3>
<p><code>MAKEDATE()</code>의 반대 함수가 <code>DAYOFYEAR()</code>예요.</p>
<pre><code class="language-sql">DAYOFYEAR(&#39;2025-03-15&#39;)  -- 74  (3월 15일은 연중 74번째 날)
MAKEDATE(2025, 74)       -- 2025-03-15</code></pre>
<hr>
<h3 id="주의사항-요약">주의사항 요약</h3>
<ul>
<li>두 번째 인자는 <strong>월/일이 아닌 연중 일수</strong></li>
<li><code>0</code> 입력 시 NULL 반환</li>
<li>365 초과 시 다음 해로 넘어감</li>
<li>반환 타입은 항상 <strong>DATE</strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MYSQL 함수정리]]></title>
            <link>https://velog.io/@jun-log/MYSQL-%ED%95%A8%EC%88%98%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jun-log/MYSQL-%ED%95%A8%EC%88%98%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 31 Mar 2026 05:26:44 GMT</pubDate>
            <description><![CDATA[<h2 id="oracle-→-mysql-함수-매핑">Oracle → MySQL 함수 매핑</h2>
<h3 id="1-to_date-→-str_to_date">1. TO_DATE → STR_TO_DATE</h3>
<p>문자열을 날짜로 변환하는 함수예요.</p>
<pre><code class="language-sql">-- Oracle
SELECT TO_DATE(&#39;2024-01-15&#39;, &#39;YYYY-MM-DD&#39;) FROM dual;

-- MySQL
SELECT STR_TO_DATE(&#39;2024-01-15&#39;, &#39;%Y-%m-%d&#39;);</code></pre>
<p><strong>포맷 문자열이 다릅니다</strong> — 이게 가장 헷갈리는 부분이에요.</p>
<table>
<thead>
<tr>
<th>Oracle 포맷</th>
<th>MySQL 포맷</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>YYYY</code></td>
<td><code>%Y</code></td>
<td>4자리 연도</td>
</tr>
<tr>
<td><code>YY</code></td>
<td><code>%y</code></td>
<td>2자리 연도</td>
</tr>
<tr>
<td><code>MM</code></td>
<td><code>%m</code></td>
<td>월 (01~12)</td>
</tr>
<tr>
<td><code>DD</code></td>
<td><code>%d</code></td>
<td>일 (01~31)</td>
</tr>
<tr>
<td><code>HH24</code></td>
<td><code>%H</code></td>
<td>24시간 형식 시</td>
</tr>
<tr>
<td><code>HH</code></td>
<td><code>%h</code></td>
<td>12시간 형식 시</td>
</tr>
<tr>
<td><code>MI</code></td>
<td><code>%i</code></td>
<td>분</td>
</tr>
<tr>
<td><code>SS</code></td>
<td><code>%s</code></td>
<td>초</td>
</tr>
</tbody></table>
<pre><code class="language-sql">-- Oracle
TO_DATE(&#39;2024-01-15 14:30:00&#39;, &#39;YYYY-MM-DD HH24:MI:SS&#39;)

-- MySQL
STR_TO_DATE(&#39;2024-01-15 14:30:00&#39;, &#39;%Y-%m-%d %H:%i:%s&#39;)</code></pre>
<hr>
<h3 id="2-trunc-→-date_format--date--floor">2. TRUNC → DATE_FORMAT / DATE / FLOOR</h3>
<p>Oracle의 <code>TRUNC</code>는 날짜/숫자 양쪽 다 쓰는 함수라 MySQL에선 목적에 따라 나뉘어요.</p>
<p><strong>날짜 절삭 (시분초 제거)</strong></p>
<pre><code class="language-sql">-- Oracle: 시분초 날려서 날짜만
SELECT TRUNC(SYSDATE) FROM dual;
SELECT TRUNC(created_at) FROM orders;

-- MySQL: DATE()로 동일하게
SELECT DATE(NOW());
SELECT DATE(created_at) FROM orders;</code></pre>
<p><strong>특정 단위로 절삭</strong></p>
<pre><code class="language-sql">-- Oracle: 월의 첫날로
SELECT TRUNC(SYSDATE, &#39;MM&#39;) FROM dual;  -- 2024-01-01

-- MySQL: DATE_FORMAT으로 동일 효과
SELECT DATE_FORMAT(NOW(), &#39;%Y-%m-01&#39;);  -- 2024-01-01

-- Oracle: 연도의 첫날로
SELECT TRUNC(SYSDATE, &#39;YYYY&#39;) FROM dual;  -- 2024-01-01

-- MySQL
SELECT DATE_FORMAT(NOW(), &#39;%Y-01-01&#39;);</code></pre>
<p><strong>숫자 절삭 (소수점)</strong></p>
<pre><code class="language-sql">-- Oracle
SELECT TRUNC(3.14159, 2) FROM dual;  -- 3.14

-- MySQL: TRUNCATE (함수명 다름!)
SELECT TRUNCATE(3.14159, 2);  -- 3.14</code></pre>
<blockquote>
<p>Oracle은 <code>TRUNC</code>, MySQL은 숫자에 <code>TRUNCATE</code>를 써요. <code>TRUNC</code>라고 쓰면 MySQL에서 에러 납니다.</p>
</blockquote>
<hr>
<h3 id="3-to_char-→-date_format--cast">3. TO_CHAR → DATE_FORMAT / CAST</h3>
<p>날짜나 숫자를 문자열로 변환하는 함수예요.</p>
<p><strong>날짜 → 문자열</strong></p>
<pre><code class="language-sql">-- Oracle
SELECT TO_CHAR(SYSDATE, &#39;YYYY-MM-DD&#39;) FROM dual;
SELECT TO_CHAR(created_at, &#39;YYYY년 MM월 DD일&#39;) FROM orders;

-- MySQL: DATE_FORMAT
SELECT DATE_FORMAT(NOW(), &#39;%Y-%m-%d&#39;);
SELECT DATE_FORMAT(created_at, &#39;%Y년 %m월 %d일&#39;) FROM orders;</code></pre>
<p><strong>숫자 → 문자열</strong></p>
<pre><code class="language-sql">-- Oracle
SELECT TO_CHAR(12345.6, &#39;999,999.9&#39;) FROM dual;  -- 12,345.6

-- MySQL: FORMAT 또는 CAST
SELECT FORMAT(12345.6, 1);   -- 12,345.6 (천단위 콤마 자동)
SELECT CAST(12345.6 AS CHAR);  -- 12345.6 (단순 변환)</code></pre>
<hr>
<h3 id="한눈에-보는-요약표">한눈에 보는 요약표</h3>
<table>
<thead>
<tr>
<th>목적</th>
<th>Oracle</th>
<th>MySQL</th>
</tr>
</thead>
<tbody><tr>
<td>문자 → 날짜</td>
<td><code>TO_DATE(str, fmt)</code></td>
<td><code>STR_TO_DATE(str, fmt)</code></td>
</tr>
<tr>
<td>날짜 시분초 제거</td>
<td><code>TRUNC(date)</code></td>
<td><code>DATE(date)</code></td>
</tr>
<tr>
<td>날짜 월/연 단위 절삭</td>
<td><code>TRUNC(date, &#39;MM&#39;)</code></td>
<td><code>DATE_FORMAT(date, &#39;%Y-%m-01&#39;)</code></td>
</tr>
<tr>
<td>숫자 소수점 절삭</td>
<td><code>TRUNC(num, n)</code></td>
<td><code>TRUNCATE(num, n)</code></td>
</tr>
<tr>
<td>날짜 → 문자</td>
<td><code>TO_CHAR(date, fmt)</code></td>
<td><code>DATE_FORMAT(date, fmt)</code></td>
</tr>
<tr>
<td>숫자 → 문자</td>
<td><code>TO_CHAR(num, fmt)</code></td>
<td><code>FORMAT(num, n)</code> / <code>CAST</code></td>
</tr>
</tbody></table>
<hr>
<h3 id="추가로-자주-마주치는-차이점">추가로 자주 마주치는 차이점</h3>
<p>Oracle에서 습관적으로 쓰던 것들이 MySQL에서 달라서 막히는 경우가 많아요.</p>
<pre><code class="language-sql">-- Oracle의 dual 테이블 → MySQL은 그냥 생략
SELECT SYSDATE FROM dual;   -- Oracle
SELECT NOW();               -- MySQL (FROM 없어도 됨)

-- Oracle SYSDATE → MySQL NOW() 또는 CURDATE()
SYSDATE          → NOW()       -- 날짜+시간
SYSDATE (날짜만) → CURDATE()   -- 날짜만

-- NULL 처리
NVL(col, 0)      → IFNULL(col, 0)
NVL2(col, a, b)  → IF(col IS NOT NULL, a, b)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Context7]]></title>
            <link>https://velog.io/@jun-log/Context7</link>
            <guid>https://velog.io/@jun-log/Context7</guid>
            <pubDate>Tue, 31 Mar 2026 01:24:10 GMT</pubDate>
            <description><![CDATA[<h1 id="context7-정리-노트">Context7 정리 노트</h1>
<h2 id="context7란">Context7란?</h2>
<p><strong>AI 코딩 어시스턴트에게 최신 공식 문서를 실시간으로 주입해주는 MCP 서버</strong></p>
<p>Claude Code, Gemini CLI 같은 AI 코딩 어시스턴트는 학습 데이터가 과거 기준이라,
최신 라이브러리 API를 모르거나 이미 deprecated된 코드를 생성하는 문제가 있다.</p>
<p>Context7은 이 문제를 해결하기 위해 AI에게 <strong>버전별 최신 공식 문서와 코드 예제를 직접 제공</strong>하여
AI가 올바르게 동작하는 코드를 작성하도록 도와준다.</p>
<hr>
<h2 id="핵심-개념">핵심 개념</h2>
<h3 id="왜-필요한가">왜 필요한가?</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>Context7 없이</th>
<th>Context7 사용 시</th>
</tr>
</thead>
<tbody><tr>
<td>Next.js 15 코드 요청</td>
<td>구버전 API 기반 코드 생성</td>
<td>최신 App Router 기반 코드 생성</td>
</tr>
<tr>
<td>없어진 함수 사용</td>
<td>deprecated 함수 그대로 사용</td>
<td>현재 권장 방식으로 작성</td>
</tr>
<tr>
<td>라이브러리 설정</td>
<td>오래된 설정 방식 안내</td>
<td>최신 공식 문서 기반 안내</td>
</tr>
</tbody></table>
<h3 id="서버-구조">서버 구조</h3>
<p>GitHub처럼 <strong>중앙화된 서버</strong>(<code>mcp.context7.com</code>)가 존재하며,
9,000개 이상의 라이브러리 문서가 인덱싱되어 있다.</p>
<p>사용 방식은 두 가지다.</p>
<ul>
<li><strong>리모트 MCP</strong> — 중앙 서버 URL에 직접 연결 (설치 불필요)</li>
<li><strong>로컬 MCP</strong> — <code>npx</code>로 로컬 프로세스를 실행하되, 문서 데이터는 중앙 서버에서 가져옴 (오프라인 불가)</li>
</ul>
<hr>
<h2 id="사용법">사용법</h2>
<h3 id="기본-원리">기본 원리</h3>
<p>프롬프트 끝에 <code>use context7</code>을 붙이면, Context7이 자동으로:</p>
<ol>
<li>어떤 라이브러리를 묻는지 감지</li>
<li>해당 라이브러리의 최신 공식 문서를 조회</li>
<li>관련 문서를 AI 컨텍스트에 주입</li>
<li>AI가 최신 문서 기반으로 코드 생성</li>
</ol>
<h3 id="예제">예제</h3>
<h4 id="✅-기본-사용법">✅ 기본 사용법</h4>
<pre><code>Next.js 미들웨어에서 JWT를 쿠키로 검증하고,
미인증 사용자는 /login으로 리다이렉트하는 코드 작성해줘. use context7</code></pre><blockquote>
<p>프롬프트 끝에 <code>use context7</code>만 추가하면 끝.
Context7이 Next.js 최신 문서를 가져와 현재 버전에 맞는 미들웨어 코드를 생성해준다.</p>
</blockquote>
<hr>
<h4 id="✅-특정-라이브러리-명시">✅ 특정 라이브러리 명시</h4>
<pre><code>Supabase로 회원가입/로그인 기능 구현해줘. use context7 /supabase/supabase</code></pre><blockquote>
<p>라이브러리 ID(<code>/supabase/supabase</code>)를 직접 지정하면
Context7이 라이브러리 탐색 과정을 건너뛰고 바로 문서를 가져온다. 더 빠르고 정확하다.</p>
</blockquote>
<hr>
<h4 id="✅-특정-주제-조회">✅ 특정 주제 조회</h4>
<pre><code>MongoDB Node.js 드라이버로 커넥션 풀링 설정하는 방법 알려줘. use context7</code></pre><blockquote>
<p>&quot;커넥션 풀링&quot;이라는 키워드를 기반으로 MongoDB 문서 중 관련 섹션만 골라서 주입한다.</p>
</blockquote>
<hr>
<h4 id="✅-claude-code에서-자동-적용-rules-설정">✅ Claude Code에서 자동 적용 (rules 설정)</h4>
<p><code>.claude/rules</code> 파일에 아래 내용을 추가하면, 매번 <code>use context7</code>을 쓰지 않아도 자동으로 동작한다.</p>
<pre><code>코드 생성, 라이브러리 설정, API 사용이 필요할 때는
항상 Context7 MCP 도구를 사용해서 최신 문서를 먼저 확인하고 답변해줘.</code></pre><hr>
<h2 id="설치-방법-claude-desktop-기준">설치 방법 (Claude Desktop 기준)</h2>
<p><code>claude_desktop_config.json</code>에 아래 내용 추가:</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
    &quot;context7&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@upstash/context7-mcp&quot;]
    }
  }
}</code></pre>
<p>또는 리모트 MCP 방식으로 URL만 등록:</p>
<pre><code>https://mcp.context7.com/mcp</code></pre><hr>
<h2 id="참고">참고</h2>
<ul>
<li>공식 GitHub: <a href="https://github.com/upstash/context7">github.com/upstash/context7</a></li>
<li>대시보드 (API 키 발급): <a href="https://context7.com/dashboard">context7.com/dashboard</a></li>
<li>API 키 없이도 무료로 사용 가능 (요청 수 제한 있음)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[nginx 설정정리(2)]]></title>
            <link>https://velog.io/@jun-log/nginx-%EC%84%A4%EC%A0%95%EC%A0%95%EB%A6%AC2</link>
            <guid>https://velog.io/@jun-log/nginx-%EC%84%A4%EC%A0%95%EC%A0%95%EB%A6%AC2</guid>
            <pubDate>Mon, 30 Mar 2026 01:30:17 GMT</pubDate>
            <description><![CDATA[<h1 id="nginx-서버-설정-완전-정리">Nginx 서버 설정 완전 정리</h1>
<blockquote>
<p>Nginx는 고성능 웹 서버이자 리버스 프록시 서버로, 가볍고 빠른 처리 성능으로 널리 사용된다.<br>이 문서는 실무에서 자주 접하는 핵심 설정들을 정리한다.</p>
</blockquote>
<hr>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#1-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0">기본 구조</a></li>
<li><a href="#2-worker-%EC%84%A4%EC%A0%95">worker 설정</a></li>
<li><a href="#3-http-%EB%B8%94%EB%A1%9D-%ED%95%B5%EC%8B%AC-%EC%84%A4%EC%A0%95">http 블록 핵심 설정</a></li>
<li><a href="#4-server-%EB%B8%94%EB%A1%9D">server 블록</a></li>
<li><a href="#5-location-%EC%84%A4%EC%A0%95">location 설정</a></li>
<li><a href="#6-proxy_pass-%EC%84%A4%EC%A0%95">proxy_pass 설정</a></li>
<li><a href="#7-https--ssl-%EC%84%A4%EC%A0%95">HTTPS / SSL 설정</a></li>
<li><a href="#8-%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1">로드 밸런싱</a></li>
<li><a href="#9-%EC%A0%95%EC%A0%81-%ED%8C%8C%EC%9D%BC-%EC%84%9C%EB%B9%99">정적 파일 서빙</a></li>
<li><a href="#10-%EC%BA%90%EC%8B%9C-%EC%84%A4%EC%A0%95">캐시 설정</a></li>
<li><a href="#11-%EB%A1%9C%EA%B7%B8-%EC%84%A4%EC%A0%95">로그 설정</a></li>
<li><a href="#12-%EB%B3%B4%EC%95%88-%EC%84%A4%EC%A0%95">보안 설정</a></li>
<li><a href="#13-gzip-%EC%95%95%EC%B6%95">gzip 압축</a></li>
<li><a href="#14-%ED%83%80%EC%9E%84%EC%95%84%EC%9B%83-%EC%84%A4%EC%A0%95">타임아웃 설정</a></li>
</ol>
<hr>
<h2 id="1-기본-구조">1. 기본 구조</h2>
<p>Nginx 설정 파일(<code>nginx.conf</code>)은 <strong>블록(block)</strong> 단위로 구성된다.</p>
<pre><code>main (전역)
├── events { }         # 연결 처리 방식
└── http { }           # HTTP 관련 설정
    ├── upstream { }   # 로드 밸런싱 대상 서버 그룹
    └── server { }     # 가상 호스트 설정
        └── location { } # 요청 경로별 처리</code></pre><pre><code class="language-nginx"># nginx.conf 기본 구조

worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    server {
        listen 80;
        server_name example.com;

        location / {
            root /var/www/html;
            index index.html;
        }
    }
}</code></pre>
<hr>
<h2 id="2-worker-설정">2. worker 설정</h2>
<p>Nginx는 <strong>멀티 프로세스</strong> 모델로 동작한다.<br><code>master process</code> 1개가 <code>worker process</code> 여러 개를 관리한다.</p>
<pre><code class="language-nginx"># CPU 코어 수에 맞게 자동 설정 (권장)
worker_processes auto;

events {
    # worker 하나가 동시에 처리할 수 있는 최대 연결 수
    worker_connections 1024;

    # 여러 연결을 한 번에 수락 (성능 향상)
    multi_accept on;

    # Linux 에서 가장 효율적인 이벤트 처리 방식
    use epoll;
}</code></pre>
<blockquote>
<p>💡 최대 동시 처리 가능 연결 수 = <code>worker_processes</code> × <code>worker_connections</code></p>
</blockquote>
<hr>
<h2 id="3-http-블록-핵심-설정">3. http 블록 핵심 설정</h2>
<pre><code class="language-nginx">http {
    # MIME 타입 정의 파일 포함
    include mime.types;
    default_type application/octet-stream;

    # sendfile: OS 커널이 직접 파일 전송 (성능 향상)
    sendfile on;

    # 패킷을 모아서 한 번에 전송 (sendfile on 일때 효과적)
    tcp_nopush on;

    # 작은 패킷도 즉시 전송 (지연 감소)
    tcp_nodelay on;

    # Keep-Alive 연결 유지 시간 (초)
    keepalive_timeout 65;
}</code></pre>
<hr>
<h2 id="4-server-블록">4. server 블록</h2>
<p>하나의 Nginx에서 <strong>여러 도메인(가상 호스트)</strong> 을 운영할 수 있다.</p>
<pre><code class="language-nginx"># HTTP 서버
server {
    listen 80;
    server_name example.com www.example.com;

    # ...
}

# 다른 도메인
server {
    listen 80;
    server_name another.com;

    # ...
}</code></pre>
<h3 id="http-→-https-리다이렉트">HTTP → HTTPS 리다이렉트</h3>
<pre><code class="language-nginx">server {
    listen 80;
    server_name example.com;

    return 301 https://$host$request_uri;
}</code></pre>
<hr>
<h2 id="5-location-설정">5. location 설정</h2>
<h3 id="매칭-방식-및-우선순위">매칭 방식 및 우선순위</h3>
<table>
<thead>
<tr>
<th>우선순위</th>
<th>기호</th>
<th>방식</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td><code>=</code></td>
<td>Exact Match</td>
<td>경로 정확히 일치</td>
</tr>
<tr>
<td>2</td>
<td><code>^~</code></td>
<td>Prefix Match (우선)</td>
<td>prefix 일치 시 정규식 검사 중단</td>
</tr>
<tr>
<td>3</td>
<td><code>~</code> / <code>~*</code></td>
<td>Regex Match</td>
<td>정규식 일치 (<code>~*</code> 는 대소문자 무시)</td>
</tr>
<tr>
<td>4</td>
<td>(없음)</td>
<td>Prefix Match</td>
<td>일반 전방 일치</td>
</tr>
</tbody></table>
<pre><code class="language-nginx"># ① 정확 일치 (최우선)
location = /health {
    return 200 &#39;OK&#39;;
}

# ② prefix 우선 일치 (정규식보다 우선)
location ^~ /static/ {
    root /var/www;
}

# ③ 정규식 일치 (대소문자 구분)
location ~ ^/api/v\d+/ {
    proxy_pass http://localhost:8080;
}

# ③ 정규식 일치 (대소문자 무시)
location ~* \.(jpg|jpeg|png|gif)$ {
    expires 30d;
}

# ④ 일반 prefix 일치
location /app/ {
    proxy_pass http://localhost:3000/;
}</code></pre>
<h3 id="path-vs-path-주의사항"><code>/path</code> vs <code>/path/</code> 주의사항</h3>
<p><code>location /detector/</code> 는 <code>/detector</code> (trailing slash 없음) 요청을 매칭하지 않는다.<br>두 경우를 모두 처리하려면 아래처럼 설정한다.</p>
<pre><code class="language-nginx">location = /detector {
    return 301 /detector/;
}

location /detector/ {
    proxy_pass http://localhost:8080/;
}</code></pre>
<hr>
<h2 id="6-proxy_pass-설정">6. proxy_pass 설정</h2>
<h3 id="trailing-slash-의-의미">trailing slash(<code>/</code>) 의 의미</h3>
<p><code>proxy_pass</code> 끝에 <code>/</code> 가 붙는지 여부에 따라, <strong>location prefix를 백엔드로 전달할지 말지</strong>가 결정된다.</p>
<table>
<thead>
<tr>
<th>설정</th>
<th>클라이언트 요청</th>
<th>백엔드로 전달되는 경로</th>
</tr>
</thead>
<tbody><tr>
<td><code>proxy_pass http://localhost:8080;</code></td>
<td><code>/detector/api</code></td>
<td><code>/detector/api</code> (prefix 유지)</td>
</tr>
<tr>
<td><code>proxy_pass http://localhost:8080/;</code></td>
<td><code>/detector/api</code></td>
<td><code>/api</code> (prefix 제거)</td>
</tr>
</tbody></table>
<pre><code class="language-nginx"># prefix 유지: /detector/api → localhost:8080/detector/api
location /detector/ {
    proxy_pass http://localhost:8080;
}

# prefix 제거: /detector/api → localhost:8080/api
location /detector/ {
    proxy_pass http://localhost:8080/;
}</code></pre>
<h3 id="정규식-location에서의-proxy_pass">정규식 location에서의 proxy_pass</h3>
<p>정규식 location에서는 <code>proxy_pass</code>에 URI(trailing slash 포함)를 명시하면 <strong>nginx가 설정 로드 시 에러를 발생</strong>시킨다.</p>
<pre><code class="language-nginx"># ❌ 설정 에러 발생
location ~ ^/detector/\d+ {
    proxy_pass http://localhost:8080/;
}

# ✅ URI 없이만 사용 가능
location ~ ^/detector/\d+ {
    proxy_pass http://localhost:8080;
}</code></pre>
<p>prefix를 제거하고 싶다면 <code>rewrite</code> 를 사용한다.</p>
<pre><code class="language-nginx">location ~ ^/detector/(.*)$ {
    rewrite ^/detector/(.*)$ /$1 break;
    proxy_pass http://localhost:8080;
    # /detector/api/test → localhost:8080/api/test
}</code></pre>
<h3 id="프록시-헤더-설정">프록시 헤더 설정</h3>
<p>리버스 프록시 시 클라이언트 정보를 백엔드로 전달하기 위해 헤더를 설정한다.</p>
<pre><code class="language-nginx">location /api/ {
    proxy_pass http://localhost:8080/;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}</code></pre>
<table>
<thead>
<tr>
<th>헤더</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>Host</code></td>
<td>원본 요청의 호스트명</td>
</tr>
<tr>
<td><code>X-Real-IP</code></td>
<td>클라이언트 실제 IP</td>
</tr>
<tr>
<td><code>X-Forwarded-For</code></td>
<td>프록시를 거친 IP 체인</td>
</tr>
<tr>
<td><code>X-Forwarded-Proto</code></td>
<td>원본 요청 프로토콜 (http/https)</td>
</tr>
</tbody></table>
<hr>
<h2 id="7-https--ssl-설정">7. HTTPS / SSL 설정</h2>
<pre><code class="language-nginx">server {
    listen 443 ssl;
    server_name example.com;

    # 인증서 경로
    ssl_certificate     /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    # 권장 프로토콜 (구버전 TLS 제외)
    ssl_protocols TLSv1.2 TLSv1.3;

    # 권장 암호화 알고리즘
    ssl_ciphers HIGH:!aNULL:!MD5;

    # 서버 암호화 알고리즘 우선 사용
    ssl_prefer_server_ciphers on;

    # SSL 세션 캐시 (성능 향상)
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    location / {
        proxy_pass http://localhost:3000;
    }
}</code></pre>
<blockquote>
<p>💡 Let&#39;s Encrypt + Certbot 을 사용하면 무료 SSL 인증서를 자동으로 발급/갱신할 수 있다.</p>
</blockquote>
<hr>
<h2 id="8-로드-밸런싱">8. 로드 밸런싱</h2>
<p>여러 백엔드 서버에 트래픽을 분산한다.</p>
<pre><code class="language-nginx">upstream backend {
    # 기본: Round Robin (순서대로 분산)
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
    server 127.0.0.1:8003;
}

server {
    location /api/ {
        proxy_pass http://backend/;
    }
}</code></pre>
<h3 id="로드-밸런싱-방식">로드 밸런싱 방식</h3>
<pre><code class="language-nginx">upstream backend {
    # least_conn: 연결 수가 가장 적은 서버로 분산
    least_conn;

    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
}</code></pre>
<pre><code class="language-nginx">upstream backend {
    # ip_hash: 같은 클라이언트 IP는 항상 같은 서버로 (세션 유지)
    ip_hash;

    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
}</code></pre>
<pre><code class="language-nginx">upstream backend {
    # weight: 가중치 설정 (8001이 트래픽 3배 더 받음)
    server 127.0.0.1:8001 weight=3;
    server 127.0.0.1:8002 weight=1;
}</code></pre>
<h3 id="서버-상태-옵션">서버 상태 옵션</h3>
<pre><code class="language-nginx">upstream backend {
    server 127.0.0.1:8001;
    server 127.0.0.1:8002 backup;   # 나머지 서버 장애시에만 사용
    server 127.0.0.1:8003 down;     # 사용 안 함 (점검용)
}</code></pre>
<hr>
<h2 id="9-정적-파일-서빙">9. 정적 파일 서빙</h2>
<pre><code class="language-nginx">server {
    listen 80;
    server_name example.com;

    # 루트 디렉토리 지정
    root /var/www/html;

    # 기본 인덱스 파일
    index index.html index.htm;

    location / {
        # 파일 → 디렉토리 → 404 순으로 시도
        try_files $uri $uri/ =404;
    }

    # SPA(React, Vue 등) 라우팅 처리
    location / {
        try_files $uri $uri/ /index.html;
    }
}</code></pre>
<h3 id="특정-확장자-캐싱">특정 확장자 캐싱</h3>
<pre><code class="language-nginx">location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
    expires 30d;
    add_header Cache-Control &quot;public, no-transform&quot;;
}</code></pre>
<hr>
<h2 id="10-캐시-설정">10. 캐시 설정</h2>
<h3 id="프록시-캐시">프록시 캐시</h3>
<p>백엔드 응답을 Nginx가 캐싱하여 백엔드 부하를 줄인다.</p>
<pre><code class="language-nginx">http {
    # 캐시 저장 경로 및 옵션 정의
    proxy_cache_path /var/cache/nginx
                     levels=1:2
                     keys_zone=my_cache:10m
                     max_size=1g
                     inactive=60m;

    server {
        location /api/ {
            proxy_pass http://localhost:8080/;
            proxy_cache my_cache;
            proxy_cache_valid 200 10m;   # 200 응답은 10분 캐시
            proxy_cache_valid 404 1m;    # 404 응답은 1분 캐시

            # 캐시 상태를 응답 헤더에 표시 (HIT/MISS/BYPASS)
            add_header X-Cache-Status $upstream_cache_status;
        }
    }
}</code></pre>
<hr>
<h2 id="11-로그-설정">11. 로그 설정</h2>
<pre><code class="language-nginx">http {
    # 로그 포맷 정의
    log_format main &#39;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#39;
                    &#39;$status $body_bytes_sent &quot;$http_referer&quot; &#39;
                    &#39;&quot;$http_user_agent&quot;&#39;;

    # 접근 로그
    access_log /var/log/nginx/access.log main;

    # 에러 로그 (레벨: debug, info, notice, warn, error, crit)
    error_log /var/log/nginx/error.log warn;

    server {
        # 특정 서버만 로그 비활성화
        access_log off;
    }
}</code></pre>
<hr>
<h2 id="12-보안-설정">12. 보안 설정</h2>
<pre><code class="language-nginx">http {
    # Nginx 버전 정보 숨기기
    server_tokens off;

    server {
        # 클릭재킹 방지
        add_header X-Frame-Options &quot;SAMEORIGIN&quot;;

        # XSS 필터 활성화
        add_header X-XSS-Protection &quot;1; mode=block&quot;;

        # MIME 타입 스니핑 방지
        add_header X-Content-Type-Options &quot;nosniff&quot;;

        # HTTPS 강제 (HSTS)
        add_header Strict-Transport-Security &quot;max-age=31536000; includeSubDomains&quot; always;

        # 특정 HTTP 메서드만 허용
        if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE)$) {
            return 405;
        }
    }
}</code></pre>
<h3 id="요청-크기-제한">요청 크기 제한</h3>
<pre><code class="language-nginx">http {
    # 클라이언트 요청 body 최대 크기 (파일 업로드 등)
    client_max_body_size 10m;
}</code></pre>
<h3 id="ip-접근-제한">IP 접근 제한</h3>
<pre><code class="language-nginx">location /admin/ {
    allow 192.168.1.0/24;   # 특정 대역만 허용
    allow 127.0.0.1;
    deny all;               # 나머지 차단
}</code></pre>
<hr>
<h2 id="13-gzip-압축">13. gzip 압축</h2>
<p>응답 데이터를 압축하여 전송 속도를 높인다.</p>
<pre><code class="language-nginx">http {
    gzip on;

    # 압축 레벨 (1~9, 높을수록 압축률 높고 CPU 사용량 증가)
    gzip_comp_level 6;

    # 최소 압축 대상 크기 (너무 작은 파일은 압축 불필요)
    gzip_min_length 1000;

    # 압축 대상 MIME 타입
    gzip_types
        text/plain
        text/css
        text/javascript
        application/json
        application/javascript
        application/xml
        image/svg+xml;

    # 프록시 캐시와 함께 사용 시 필요
    gzip_vary on;
}</code></pre>
<hr>
<h2 id="14-타임아웃-설정">14. 타임아웃 설정</h2>
<pre><code class="language-nginx">http {
    # 클라이언트로부터 요청 헤더를 읽는 타임아웃
    client_header_timeout 10s;

    # 클라이언트로부터 요청 body를 읽는 타임아웃
    client_body_timeout 10s;

    # 클라이언트에게 응답을 전송하는 타임아웃
    send_timeout 10s;

    # 프록시 서버 연결 타임아웃
    proxy_connect_timeout 5s;

    # 프록시 서버로부터 응답을 받는 타임아웃
    proxy_read_timeout 60s;

    # 프록시 서버로 요청을 전송하는 타임아웃
    proxy_send_timeout 60s;
}</code></pre>
<hr>
<h2 id="요약">요약</h2>
<table>
<thead>
<tr>
<th>설정 항목</th>
<th>핵심 포인트</th>
</tr>
</thead>
<tbody><tr>
<td>worker</td>
<td><code>worker_processes auto</code> + <code>worker_connections</code> 로 동시 처리량 결정</td>
</tr>
<tr>
<td>location</td>
<td>우선순위: <code>=</code> &gt; <code>^~</code> &gt; <code>~</code> &gt; prefix</td>
</tr>
<tr>
<td>proxy_pass</td>
<td>trailing <code>/</code> 유무로 prefix 제거 여부 결정</td>
</tr>
<tr>
<td>정규식 location</td>
<td><code>proxy_pass</code>에 URI 불가 → <code>rewrite</code> 활용</td>
</tr>
<tr>
<td>SSL</td>
<td>TLSv1.2 이상 사용, Let&#39;s Encrypt 권장</td>
</tr>
<tr>
<td>로드 밸런싱</td>
<td>Round Robin / least_conn / ip_hash / weight</td>
</tr>
<tr>
<td>보안</td>
<td><code>server_tokens off</code>, 보안 헤더, IP 제한</td>
</tr>
<tr>
<td>성능</td>
<td>gzip, sendfile, proxy_cache, keepalive 활용</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[nginx 설정정리(1)]]></title>
            <link>https://velog.io/@jun-log/nginx-%EC%84%A4%EC%A0%95%EC%A0%95%EB%A6%AC1</link>
            <guid>https://velog.io/@jun-log/nginx-%EC%84%A4%EC%A0%95%EC%A0%95%EB%A6%AC1</guid>
            <pubDate>Mon, 30 Mar 2026 01:29:58 GMT</pubDate>
            <description><![CDATA[<h1 id="nginx-proxy-설정-정리">Nginx Proxy 설정 정리</h1>
<h2 id="1-proxy_pass-의-trailing-slash-의미">1. <code>proxy_pass</code> 의 trailing slash(<code>/</code>) 의미</h2>
<p><code>proxy_pass</code> 끝에 <code>/</code> 가 붙는지 여부에 따라, <strong>location prefix를 백엔드로 전달할지 말지</strong>가 결정된다.</p>
<table>
<thead>
<tr>
<th>설정</th>
<th>클라이언트 요청</th>
<th>백엔드로 전달되는 경로</th>
</tr>
</thead>
<tbody><tr>
<td><code>proxy_pass http://localhost:8080;</code></td>
<td><code>/detector/api</code></td>
<td><code>/detector/api</code> (prefix 유지)</td>
</tr>
<tr>
<td><code>proxy_pass http://localhost:8080/;</code></td>
<td><code>/detector/api</code></td>
<td><code>/api</code> (prefix 제거)</td>
</tr>
</tbody></table>
<h3 id="정리">정리</h3>
<ul>
<li><strong><code>/</code> 없음</strong> → location prefix를 <strong>유지</strong>한 채로 전달</li>
<li><strong><code>/</code> 있음</strong> → location prefix를 <strong>제거</strong>하고 나머지 경로만 전달</li>
</ul>
<h3 id="예시">예시</h3>
<pre><code class="language-nginx"># prefix 유지: /detector/api → localhost:8080/detector/api
location /detector/ {
    proxy_pass http://localhost:8080;
}

# prefix 제거: /detector/api → localhost:8080/api
location /detector/ {
    proxy_pass http://localhost:8080/;
}</code></pre>
<hr>
<h2 id="2-location-설정-3가지-방법">2. <code>location</code> 설정 3가지 방법</h2>
<h3 id="①-prefix-match-전방-일치">① Prefix Match (전방 일치)</h3>
<p>입력한 prefix로 시작하는 모든 경로를 매칭한다.</p>
<pre><code class="language-nginx">location /detector/ {
    # /detector/, /detector/api/test 등 모두 매칭
}</code></pre>
<blockquote>
<p>⚠️ <code>/detector/</code> 로 설정하면 <code>/detector</code> (trailing slash 없음) 는 매칭되지 않는다.
두 경우를 모두 처리하려면 아래처럼 리다이렉트를 추가한다.</p>
<pre><code class="language-nginx">location = /detector {
    return 301 /detector/;
}</code></pre>
</blockquote>
<hr>
<h3 id="②-exact-match-정확-일치">② Exact Match (정확 일치)</h3>
<p>입력한 경로와 <strong>정확히 일치</strong>하는 요청만 매칭한다.</p>
<pre><code class="language-nginx">location = /detector {
    # /detector 와 정확히 일치하는 요청만 매칭
}</code></pre>
<hr>
<h3 id="③-regex-match-정규식-일치">③ Regex Match (정규식 일치)</h3>
<p>정규식 패턴에 매칭되는 경로를 처리한다.</p>
<pre><code class="language-nginx">location ~ ^/detector/\d+ {
    # /detector/123, /detector/456 등 매칭
}</code></pre>
<blockquote>
<p>⚠️ <strong>정규식 location에서는 <code>proxy_pass</code>에 URI(trailing slash)를 붙일 수 없다.</strong>
nginx가 설정 로드 시점에 <strong>에러를 발생</strong>시키므로, 아예 허용되지 않는다.</p>
<pre><code class="language-nginx"># ❌ 설정 에러 발생
location ~ ^/detector/\d+ {
    proxy_pass http://localhost:8080/;
}

# ✅ URI 없이만 사용 가능
location ~ ^/detector/\d+ {
    proxy_pass http://localhost:8080;
}</code></pre>
<p>따라서 정규식 location에서 prefix를 제거하고 싶을 때는 <code>rewrite</code> 를 사용한다.</p>
</blockquote>
<hr>
<h2 id="3-rewrite-란">3. <code>rewrite</code> 란?</h2>
<p>요청 URI를 <strong>내부적으로 변경</strong>하는 기능이다.<br>정규식 location에서 prefix 제거가 필요할 때 주로 활용한다.</p>
<pre><code class="language-nginx">location ~ ^/detector/(.*)$ {
    rewrite ^/detector/(.*)$ /$1 break;
    proxy_pass http://localhost:8080;
    # /detector/api/test → localhost:8080/api/test
}</code></pre>
]]></description>
        </item>
    </channel>
</rss>