<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>차곡차곡 데일리코딩</title>
        <link>https://velog.io/</link>
        <description>비전공자의 개발 성장 기록. 바이브코딩 프리랜서 경험부터 Python·SQL·AI 서비스 개발까지</description>
        <lastBuildDate>Fri, 02 Oct 2026 00:33:50 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>차곡차곡 데일리코딩</title>
            <url>https://velog.velcdn.com/images/8113jsl-ai/profile/94038430-d3a7-4bda-8701-137b9bcb1c89/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 차곡차곡 데일리코딩. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/8113jsl-ai" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[IT·AI 용어사전] RAG란? 문서를 검색해서 답한다는 의미]]></title>
            <link>https://velog.io/@8113jsl-ai/it-ai-rag-retrieval-generation</link>
            <guid>https://velog.io/@8113jsl-ai/it-ai-rag-retrieval-generation</guid>
            <pubDate>Fri, 02 Oct 2026 00:33:50 GMT</pubDate>
            <description><![CDATA[<p>AI에게 내부 문서에 관한 질문을 했을 때, 모델이 그 문서를 학습한 적이 없다면 어떻게 답할 수 있을까요? 한 가지 방법은 질문과 관련된 문서를 찾아 답변에 필요한 자료로 함께 제공하는 것입니다. 이 접근을 이해할 때 등장하는 용어가 <strong>RAG</strong>입니다.</p>
<h2 id="검색한-자료를-답변의-맥락에-넣습니다">검색한 자료를 답변의 맥락에 넣습니다</h2>
<p>RAG는 Retrieval-Augmented Generation, 우리말로는 검색 증강 생성이라고 부릅니다. 외부 자료에서 관련 정보를 검색하고, 그 내용을 바탕으로 모델이 답변을 생성하도록 구성하는 방식입니다.</p>
<p>예를 들어 가상의 도서관 안내 서비스에 “주말에는 몇 시까지 운영하나요?”라고 질문했다고 가정하겠습니다.</p>
<pre><code class="language-text">질문 입력
  ↓
운영시간과 관련된 문서 검색
  ↓
질문 + 검색된 문서 내용을 모델에 제공
  ↓
문서를 근거로 답변 생성</code></pre>
<p>모델이 모든 문서를 한꺼번에 기억하는 구조라고 이해하면 부정확합니다. 검색 시스템이 고른 자료를 해당 답변의 맥락으로 제공하는 것이 핵심입니다.</p>
<h2 id="문서를-준비하는-과정과-답하는-과정을-구분합니다">문서를 준비하는 과정과 답하는 과정을 구분합니다</h2>
<p>문서 준비 단계에서는 자료를 수집하고 검색하기 좋은 단위로 나누어 색인을 만듭니다. 긴 문서를 작은 단위로 나누는 작업은 청킹(chunking)이라고 부릅니다.</p>
<p>의미가 비슷한 내용을 찾기 위해 텍스트를 벡터로 표현하는 임베딩(embedding)을 사용할 수 있습니다. 다만 RAG가 반드시 벡터 검색만 사용하는 것은 아닙니다. 키워드 검색이나 두 방식을 결합한 검색도 가능합니다.</p>
<p>질문이 들어오면 관련 자료를 검색하고, 필요한 내용을 선별해 모델에 전달합니다. 문서를 너무 잘게 나누면 앞뒤 조건이 떨어져 나갈 수 있고, 너무 크게 나누면 불필요한 내용까지 함께 들어갈 수 있습니다.</p>
<h2 id="파인튜닝과는-무엇이-다를까요">파인튜닝과는 무엇이 다를까요?</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>RAG</th>
<th>파인튜닝</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>RAG와 조합 가능</td>
</tr>
</tbody></table>
<p>RAG의 검색·참고 단계 자체가 모델의 가중치를 다시 학습시키는 것은 아닙니다. 반면 파인튜닝은 추가 학습으로 모델의 동작을 조정하는 방법입니다. 어느 쪽이 항상 우월하다기보다 해결하려는 문제에 따라 선택하거나 조합합니다.</p>
<h2 id="근거가-있어도-정답인지는-따로-확인해야-합니다">근거가 있어도 정답인지는 따로 확인해야 합니다</h2>
<p>도서관 안내 문서에서 평일 운영시간만 검색됐다면, 생성 모델이 아무리 자연스럽게 답해도 주말 질문에는 맞지 않을 수 있습니다. 검색된 문서가 오래됐거나, 답변이 문서의 예외 조건을 빠뜨리는 경우도 생각해야 합니다.</p>
<p>따라서 다음 세 단계를 나눠 점검할 수 있습니다.</p>
<ol>
<li>질문에 답할 수 있는 자료가 검색됐는지 확인합니다.</li>
<li>자료가 최신이며 해당 질문의 조건에 맞는지 확인합니다.</li>
<li>답변의 주장과 제시된 근거가 실제로 일치하는지 확인합니다.</li>
</ol>
<p>RAG는 외부 지식을 활용하는 유용한 구성 방식이지만, 사실 오류를 자동으로 없애는 장치는 아닙니다. 서비스를 살펴볼 때는 “문서를 연결했다”는 설명에서 한 단계 더 나아가 <strong>어떤 자료를 검색했고, 답변이 그 자료를 정확히 반영했는지</strong>를 확인하는 것이 중요합니다.</p>
<p>참고: <a href="https://aws.amazon.com/what-is/retrieval-augmented-generation/">AWS — RAG란 무엇인가요?</a></p>
<p>확인일: 2026년 10월 2일</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코딩테스트 기초 08] 누적 합으로 여러 구간의 합 구하기]]></title>
            <link>https://velog.io/@8113jsl-ai/python-prefix-sum-range-queries</link>
            <guid>https://velog.io/@8113jsl-ai/python-prefix-sum-range-queries</guid>
            <pubDate>Fri, 02 Oct 2026 00:32:54 GMT</pubDate>
            <description><![CDATA[<p>같은 리스트에서 여러 구간의 합을 반복해서 구한다면, 매번 구간을 순회하는 대신 앞부분의 합을 저장해 둘 수 있습니다. 이번에는 누적 합을 이용해 구간 합을 구하는 자체 연습 문제를 풀어 봅니다.</p>
<blockquote>
<p>특정 코딩테스트 사이트의 문제 원문이나 테스트 데이터를 사용하지 않았습니다. 아래 문제와 예제는 직접 구성했으며 플랫폼 채점 결과를 의미하지 않습니다.</p>
</blockquote>
<h2 id="구간의-끝은-포함하지-않습니다">구간의 끝은 포함하지 않습니다</h2>
<p>정수 리스트 <code>values</code>와 구간 목록 <code>queries</code>가 주어집니다. 각 구간 <code>(left, right)</code>에 대해 <code>values[left:right]</code>의 합을 반환합니다.</p>
<p>인덱스는 0부터 시작하며, <code>0 &lt;= left &lt;= right &lt;= len(values)</code>를 만족한다고 가정합니다. 오른쪽 끝은 포함하지 않습니다. 입력값은 정수이고 음수도 허용합니다.</p>
<pre><code class="language-python">values = [3, -1, 4, 2]
queries = [(0, 4), (1, 3), (2, 2)]
# 기대 결과: [8, 3, 0]</code></pre>
<p>두 번째 구간은 <code>-1 + 4 = 3</code>입니다. 세 번째처럼 시작과 끝이 같으면 빈 구간이므로 합은 0입니다.</p>
<h2 id="앞에서부터-더한-값을-하나-더-저장합니다">앞에서부터 더한 값을 하나 더 저장합니다</h2>
<p><code>prefix[k]</code>를 “처음 k개 원소의 합”으로 정의합니다. 그러면 <code>prefix[0]</code>은 아무것도 더하지 않은 0입니다.</p>
<table>
<thead>
<tr>
<th align="right">k</th>
<th align="right">prefix[k]</th>
</tr>
</thead>
<tbody><tr>
<td align="right">0</td>
<td align="right">0</td>
</tr>
<tr>
<td align="right">1</td>
<td align="right">3</td>
</tr>
<tr>
<td align="right">2</td>
<td align="right">2</td>
</tr>
<tr>
<td align="right">3</td>
<td align="right">6</td>
</tr>
<tr>
<td align="right">4</td>
<td align="right">8</td>
</tr>
</tbody></table>
<p><code>[left, right)</code>의 합은 <code>prefix[right] - prefix[left]</code>입니다. 오른쪽 경계까지의 합에서 왼쪽 경계 앞부분의 합을 빼는 방식입니다.</p>
<pre><code class="language-python">def range_sums(values, queries):
    prefix = [0]
    for value in values:
        prefix.append(prefix[-1] + value)

    result = []
    for left, right in queries:
        result.append(prefix[right] - prefix[left])
    return result</code></pre>
<p>맨 앞에 0을 두었기 때문에 0번 인덱스에서 시작하는 구간도 별도 분기 없이 계산할 수 있습니다.</p>
<h2 id="경계와-음수-입력을-함께-확인합니다">경계와 음수 입력을 함께 확인합니다</h2>
<pre><code class="language-python">assert range_sums([3, -1, 4, 2], [(0, 4), (1, 3), (2, 2)]) == [8, 3, 0]
assert range_sums([], [(0, 0)]) == [0]
assert range_sums([5], [(0, 1), (1, 1)]) == [5, 0]
assert range_sums([-3, -2], [(0, 2)]) == [-5]
assert range_sums([1, 2], []) == []</code></pre>
<p>이 함수는 문제에서 보장한 유효 구간을 입력받는 풀이입니다. 범위를 벗어난 값을 자동으로 보정하지 않으며, 실제 서비스에 사용할 때는 입력 검증을 별도로 설계해야 합니다.</p>
<p>리스트 길이가 n, 질의 수가 q라면 누적 합을 만드는 데 O(n), 각 질의에 O(1)이므로 전체 시간은 O(n + q)입니다. 누적 합에 O(n), 반환 결과에 O(q) 공간을 사용합니다.</p>
<p>구간 합이 한 번만 필요하다면 <code>sum(values[left:right])</code>가 더 간단할 수 있습니다. 누적 합은 <strong>원본 값이 바뀌지 않고 여러 구간을 반복 조회할 때</strong> 특히 유용합니다. 값이 바뀌면 저장해 둔 누적 합도 더 이상 유효하지 않다는 점을 함께 기억해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] UNION과 UNION ALL — 중복을 지워도 되는 데이터인가요?]]></title>
            <link>https://velog.io/@8113jsl-ai/sql-union-all-duplicate-rows</link>
            <guid>https://velog.io/@8113jsl-ai/sql-union-all-duplicate-rows</guid>
            <pubDate>Fri, 02 Oct 2026 00:32:07 GMT</pubDate>
            <description><![CDATA[<p>두 테이블의 조회 결과를 합칠 때 <code>UNION</code>과 <code>UNION ALL</code>을 사용할 수 있습니다. 선택 기준은 단순히 “중복이 있느냐”가 아니라 <strong>같은 값의 행이 서로 다른 기록을 뜻할 수 있느냐</strong>입니다.</p>
<p>예제는 SQLite에서 실행했습니다. 온라인과 오프라인에서 발생한 구매 금액을 합치는 상황입니다.</p>
<h2 id="같은-금액의-구매도-별개의-기록입니다">같은 금액의 구매도 별개의 기록입니다</h2>
<pre><code class="language-sql">CREATE TABLE online_sales (amount INTEGER NOT NULL);
CREATE TABLE offline_sales (amount INTEGER NOT NULL);

INSERT INTO online_sales VALUES (1000), (2000);
INSERT INTO offline_sales VALUES (1000), (3000);</code></pre>
<p>1000원 구매가 양쪽에 한 건씩 있습니다. 금액은 같아도 총 네 번의 구매입니다.</p>
<pre><code class="language-sql">SELECT amount FROM online_sales
UNION ALL
SELECT amount FROM offline_sales
ORDER BY amount;</code></pre>
<p>결과는 <code>1000, 1000, 2000, 3000</code>입니다. <code>UNION ALL</code>은 각 조회 결과의 행을 중복 제거 없이 합칩니다.</p>
<h2 id="union은-조회한-행-전체를-기준으로-중복을-제거합니다">UNION은 조회한 행 전체를 기준으로 중복을 제거합니다</h2>
<pre><code class="language-sql">SELECT amount FROM online_sales
UNION
SELECT amount FROM offline_sales
ORDER BY amount;</code></pre>
<p>이번 결과는 <code>1000, 2000, 3000</code>입니다. 조회한 열이 <code>amount</code> 하나이므로 같은 금액의 두 행이 하나로 합쳐졌습니다.</p>
<p>두 열을 조회했다면 두 열의 값이 모두 같은 행이 중복 제거 대상입니다. <code>UNION</code>이 실제 구매의 동일성을 판단하거나 기본키를 알아서 찾아 비교하는 것은 아닙니다.</p>
<table>
<thead>
<tr>
<th>연산</th>
<th align="right">결과 행 수</th>
<th align="right">금액 합계</th>
</tr>
</thead>
<tbody><tr>
<td>UNION ALL</td>
<td align="right">4</td>
<td align="right">7000</td>
</tr>
<tr>
<td>UNION</td>
<td align="right">3</td>
<td align="right">6000</td>
</tr>
</tbody></table>
<p>서로 다른 구매를 합산하려는 상황에서 <code>UNION</code>을 쓰면 실제 금액보다 적게 집계될 수 있습니다.</p>
<h2 id="합친-결과를-집계할-때도-기준은-같습니다">합친 결과를 집계할 때도 기준은 같습니다</h2>
<pre><code class="language-sql">WITH all_sales AS (
    SELECT amount FROM online_sales
    UNION ALL
    SELECT amount FROM offline_sales
)
SELECT COUNT(*) AS sale_count, SUM(amount) AS total_amount
FROM all_sales;</code></pre>
<p>결과는 <code>sale_count = 4</code>, <code>total_amount = 7000</code>입니다. 반대로 “구매에 사용된 서로 다른 금액의 목록”이 목적이라면 <code>UNION</code>이 적절할 수 있습니다.</p>
<p>두 SELECT는 열 개수가 같아야 하고, 같은 위치의 열끼리 의미와 자료형을 맞춰야 합니다. 이 예제는 모두 정수 금액으로 통일했습니다. 결과의 순서가 필요하면 전체 쿼리 끝에 <code>ORDER BY</code>를 명시합니다.</p>
<p>성능만 보고 무조건 하나를 선택하기보다, <strong>결과의 한 행이 무엇을 의미해야 하는지</strong>부터 정하는 것이 우선입니다. 중복 제거가 불필요한 거래 내역에는 <code>UNION ALL</code>을, 서로 다른 값의 목록이 필요할 때는 <code>UNION</code>을 검토할 수 있습니다.</p>
<p>참고: <a href="https://www.sqlite.org/lang_select.html#compound_select_statements">SQLite 공식 문서 — Compound SELECT</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] 기본 인자에 빈 리스트를 쓰면 값이 남는 이유]]></title>
            <link>https://velog.io/@8113jsl-ai/python-mutable-default-arguments</link>
            <guid>https://velog.io/@8113jsl-ai/python-mutable-default-arguments</guid>
            <pubDate>Fri, 02 Oct 2026 00:31:15 GMT</pubDate>
            <description><![CDATA[<p>함수에 리스트를 전달하지 않으면 빈 리스트부터 시작하도록 만들고 싶을 때가 있습니다. <code>items=[]</code>라고 작성하면 자연스러워 보이지만, 호출할 때마다 새 리스트가 만들어지는 것은 아닙니다.</p>
<h2 id="두-번째-호출에-첫-번째-값이-남습니다">두 번째 호출에 첫 번째 값이 남습니다</h2>
<pre><code class="language-python">def add_topic(topic, topics=[]):
    topics.append(topic)
    return topics

print(add_topic(&quot;Python&quot;))
print(add_topic(&quot;SQL&quot;))</code></pre>
<pre><code class="language-text">[&#39;Python&#39;]
[&#39;Python&#39;, &#39;SQL&#39;]</code></pre>
<p>기본 인자는 함수가 정의될 때 평가됩니다. 위 함수는 정의 시 만들어진 리스트를 기본값으로 보관하고, 인자를 생략한 호출에서 그 객체를 다시 사용합니다. <code>append()</code>가 리스트 자체를 변경하므로 다음 호출에도 이전 값이 남습니다.</p>
<p>이 현상은 리스트만의 문제가 아닙니다. 딕셔너리나 집합처럼 변경 가능한 객체를 기본값으로 두고 수정할 때도 주의해야 합니다.</p>
<h2 id="호출마다-새-리스트가-필요하면-함수-안에서-만듭니다">호출마다 새 리스트가 필요하면 함수 안에서 만듭니다</h2>
<pre><code class="language-python">def add_topic(topic, topics=None):
    if topics is None:
        topics = []
    topics.append(topic)
    return topics

first = add_topic(&quot;Python&quot;)
second = add_topic(&quot;SQL&quot;)

assert first == [&quot;Python&quot;]
assert second == [&quot;SQL&quot;]
assert first is not second</code></pre>
<p>여기서는 <code>None</code>을 “리스트를 전달하지 않았다”는 표시로 사용합니다. 실제 빈 리스트 생성은 함수가 실행될 때 이루어지므로 두 호출의 결과가 서로 다른 객체입니다.</p>
<p><code>if not topics</code>로 바꾸면 빈 리스트를 직접 전달한 경우까지 새 리스트로 교체합니다. <code>None</code>과 빈 리스트의 의미를 구분하려면 <code>is None</code>으로 확인하는 것이 정확합니다.</p>
<h2 id="전달받은-리스트는-여전히-바뀝니다">전달받은 리스트는 여전히 바뀝니다</h2>
<pre><code class="language-python">original = [&quot;Git&quot;]
result = add_topic(&quot;Python&quot;, original)

assert original == [&quot;Git&quot;, &quot;Python&quot;]
assert result is original</code></pre>
<p>기본값 문제를 해결했다고 해서 원본 보존까지 보장되는 것은 아닙니다. 이 함수는 전달받은 리스트에 항목을 추가하도록 작성되어 있습니다.</p>
<p>원본을 변경하지 않는 함수가 필요하다면 별도의 리스트를 만들어 반환할 수 있습니다.</p>
<pre><code class="language-python">def with_topic(topic, topics=None):
    result = [] if topics is None else list(topics)
    result.append(topic)
    return result

original = [&quot;Git&quot;]
assert with_topic(&quot;Python&quot;, original) == [&quot;Git&quot;, &quot;Python&quot;]
assert original == [&quot;Git&quot;]</code></pre>
<p><code>list(topics)</code>는 얕은 복사입니다. 중첩된 객체까지 복제하지는 않지만, 이 예제처럼 문자열 항목을 새 리스트에 추가하는 용도에는 충분합니다.</p>
<p>함수를 검토할 때는 <strong>기본 객체가 언제 만들어지는지</strong>와 <strong>전달받은 객체를 변경하는지</strong>를 별도로 확인하려고 합니다. 두 질문을 나누면 호출 간 값이 섞이는 원인도 더 명확하게 찾을 수 있습니다.</p>
<p>참고: <a href="https://docs.python.org/3/tutorial/controlflow.html#default-argument-values">Python 공식 문서 — 기본 인자</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 같은 코드가 다르게 실행될 때 — Python 실행 환경 기록하기]]></title>
            <link>https://velog.io/@8113jsl-ai/til-python-environment-reproducibility</link>
            <guid>https://velog.io/@8113jsl-ai/til-python-environment-reproducibility</guid>
            <pubDate>Fri, 02 Oct 2026 00:29:10 GMT</pubDate>
            <description><![CDATA[<p>코드를 공유할 때 소스 파일만 있으면 같은 결과가 나올 것이라고 생각하기 쉽습니다. 하지만 Python 버전이나 설치된 패키지가 다르면 같은 코드도 다르게 동작할 수 있습니다. 이번 복습에서는 <strong>코드와 함께 남겨야 할 실행 환경 정보</strong>를 정리했습니다.</p>
<h2 id="먼저-어떤-python으로-실행했는지-확인합니다">먼저 어떤 Python으로 실행했는지 확인합니다</h2>
<p>터미널에서 다음 명령을 실행하면 버전과 실행 파일의 경로를 확인할 수 있습니다.</p>
<pre><code class="language-bash">python --version
python -c &quot;import sys; print(sys.executable)&quot;
python -m pip --version</code></pre>
<p>첫 번째는 Python 버전, 두 번째는 실제 인터프리터 경로를 보여 줍니다. 세 번째는 해당 Python으로 실행한 pip의 버전과 설치 위치를 확인하는 명령입니다.</p>
<p><code>pip</code>만 실행하면 의도한 Python과 다른 환경에 패키지를 설치할 수 있습니다. <code>python -m pip</code>처럼 실행하면 선택한 Python을 기준으로 pip를 호출하므로 연결 관계를 확인하기 쉽습니다.</p>
<p>IDE에서도 같은 인터프리터가 선택되어 있는지 확인해야 합니다. 터미널의 Python과 편집기의 실행 버튼이 사용하는 Python이 항상 같지는 않습니다.</p>
<h2 id="가상환경은-프로젝트의-패키지를-분리합니다">가상환경은 프로젝트의 패키지를 분리합니다</h2>
<p>프로젝트 폴더에서 다음과 같이 가상환경을 만들 수 있습니다.</p>
<pre><code class="language-bash">python -m venv .venv</code></pre>
<p>Windows PowerShell에서는 활성화 없이 실행 파일을 직접 지정하는 방법도 있습니다.</p>
<pre><code class="language-powershell">.\.venv\Scripts\python.exe --version
.\.venv\Scripts\python.exe -m pip --version</code></pre>
<p>이 방식은 어떤 Python을 실행하는지 명령에 드러납니다. macOS나 Linux에서는 일반적으로 <code>.venv/bin/python</code> 경로를 사용합니다.</p>
<p>가상환경은 기본적으로 프로젝트별 패키지를 분리하지만 운영체제 전체를 격리하지는 않습니다. 외부 프로그램이나 시스템 라이브러리까지 동일하게 만들어 주는 기능은 아니라는 점을 구분해야 합니다.</p>
<h2 id="패키지-목록과-실행-조건을-함께-남깁니다">패키지 목록과 실행 조건을 함께 남깁니다</h2>
<p>Windows PowerShell에서 현재 가상환경의 패키지 목록을 기록하는 예시입니다. 기존 파일이 있다면 덮어쓰므로 먼저 내용을 확인합니다.</p>
<pre><code class="language-powershell">.\.venv\Scripts\python.exe -m pip freeze | Set-Content -Encoding utf8 requirements.txt</code></pre>
<p>새 환경에서는 다음과 같이 설치할 수 있습니다.</p>
<pre><code class="language-powershell">.\.venv\Scripts\python.exe -m pip install -r requirements.txt</code></pre>
<p><code>pip freeze</code>는 설치된 패키지를 requirements 형식으로 출력합니다. 프로젝트에 필요한 패키지만 자동으로 골라 주거나, 모든 환경에서 동일한 설치를 보장하는 잠금 파일을 만들어 주는 것은 아닙니다.</p>
<p>따라서 README에는 Python 버전, 운영체제, 실행 명령, 필요한 입력 파일의 위치도 함께 기록하는 편이 좋습니다. 외부 API를 사용한다면 필요한 환경변수의 이름을 안내하되 실제 키는 포함하지 않습니다.</p>
<p><strong>실행 환경 기록은 “내 컴퓨터에서는 됐다”를 다른 환경에서도 검증할 수 있게 만드는 출발점</strong>입니다. 오류를 조사할 때도 코드 수정에 앞서 인터프리터와 패키지 버전부터 비교할 수 있습니다.</p>
<p>참고: <a href="https://docs.python.org/3/library/venv.html">Python venv 문서</a>, <a href="https://pip.pypa.io/en/stable/cli/pip_freeze/">pip freeze 문서</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IT·AI 용어사전] 토큰과 컨텍스트 윈도우는 무엇이 다를까요?]]></title>
            <link>https://velog.io/@8113jsl-ai/it-ai-token-context-window</link>
            <guid>https://velog.io/@8113jsl-ai/it-ai-token-context-window</guid>
            <pubDate>Tue, 29 Sep 2026 11:18:54 GMT</pubDate>
            <description><![CDATA[<p>AI 서비스에서 “토큰을 많이 사용했다”거나 “컨텍스트가 길다”는 표현을 볼 수 있습니다. 두 용어는 관련되어 있지만 같은 뜻은 아닙니다. 텍스트를 처리하는 단위와 한 번에 다룰 수 있는 범위를 구분하면 이해하기 쉬워요.</p>
<h2 id="토큰은-모델이-텍스트를-처리하는-단위입니다">토큰은 모델이 텍스트를 처리하는 단위입니다</h2>
<p>텍스트는 토크나이저를 거쳐 토큰 ID의 나열로 변환됩니다. 토큰은 단어 전체일 수도 있고 단어의 일부나 문장 부호 등에 대응할 수도 있어요. 따라서 토큰 하나를 한 글자나 한 단어와 같다고 보면 안 됩니다.</p>
<p>같은 문장도 사용하는 토크나이저에 따라 토큰 수가 달라질 수 있습니다. 한국어와 영어에 같은 글자 수 환산 비율을 일괄 적용하는 것도 정확하지 않아요. 비용이나 길이 제한을 계산해야 한다면 해당 모델에 맞는 토크나이저 또는 서비스의 토큰 계산 기능을 사용하는 편이 낫습니다.</p>
<p>예를 들어 “오늘도 코딩을 공부합니다.”라는 문장을 보고 눈으로 정확한 토큰 수를 정할 수는 없습니다. 이 글에서도 특정 토크나이저로 측정하지 않은 숫자는 제시하지 않겠습니다.</p>
<h2 id="컨텍스트-윈도우는-현재-처리할-수-있는-범위예요">컨텍스트 윈도우는 현재 처리할 수 있는 범위예요</h2>
<p>컨텍스트에는 질문 한 줄만 들어가는 것이 아닙니다. 모델에 전달된 지시사항, 대화 기록, 참고 문서, 도구 결과 등이 함께 포함될 수 있어요. 입력과 생성할 출력이 어떻게 한도를 공유하는지는 모델과 서비스의 규칙을 확인해야 합니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>먼저 확인할 질문</th>
</tr>
</thead>
<tbody><tr>
<td>토큰</td>
<td>이 내용을 어떤 단위로 나누어 처리하나요?</td>
</tr>
<tr>
<td>컨텍스트 윈도우</td>
<td>현재 요청에서 얼마만큼의 내용을 다룰 수 있나요?</td>
</tr>
<tr>
<td>최대 출력 길이</td>
<td>이번 응답은 최대 얼마나 길게 생성할 수 있나요?</td>
</tr>
</tbody></table>
<p>컨텍스트 윈도우가 크다고 그 크기만큼 한 번에 답변을 출력할 수 있다는 뜻은 아닙니다. 최대 출력 길이는 별도로 제한될 수 있습니다.</p>
<h2 id="단순화한-예산-계산">단순화한 예산 계산</h2>
<p>다음은 실제 제품 사양이 아니라 개념 설명용 가정입니다. 전체 한도가 10000토큰이고 입력이 7500토큰이며, 출력도 같은 한도를 공유하는 모델을 생각해 보겠습니다.</p>
<pre><code class="language-text">전체 한도 10000 - 입력 7500 = 남은 여유 2500</code></pre>
<p>이 계산에서는 출력에 사용할 수 있는 공간이 최대 2500토큰입니다. 그러나 별도의 출력 제한이 1000토큰이라면 그것도 따라야 합니다. 실제 요청에서는 메시지 형식이나 도구 정의 등 추가 요소도 고려해야 하므로 문서 본문의 길이만으로 정확한 사용량을 계산하면 안 됩니다.</p>
<h2 id="대화-기록이-남아-있어도-전부-전달된다는-뜻은-아닙니다">대화 기록이 남아 있어도 전부 전달된다는 뜻은 아닙니다</h2>
<p>화면에 보이는 대화 기록, 서비스가 저장한 정보, 모델이 현재 전달받은 컨텍스트는 구분해야 합니다. 서비스는 긴 대화를 요약하거나 일부를 선택해 전달할 수 있어요. 컨텍스트 윈도우 자체가 영구 기억을 의미하는 것은 아닙니다.</p>
<p>또한 많은 내용을 넣을 수 있다는 것과 그 내용을 빠짐없이 정확하게 활용한다는 것은 별개의 문제입니다. 중요한 조건은 명확하게 정리하고, 답변에서 실제로 반영되었는지 확인하는 과정이 필요합니다.</p>
<h2 id="자료를-전달할-때의-기준">자료를 전달할 때의 기준</h2>
<p>질문, 반드시 지켜야 할 조건, 관련 근거를 구분해서 전달하면 무엇을 확인해야 하는지 명확해집니다. 그렇다고 무조건 짧게 줄이는 것이 목표는 아니에요. 예외 조건이나 핵심 데이터를 삭제하면 오히려 필요한 판단 근거가 사라집니다.</p>
<p>토큰은 처리 단위이고, 컨텍스트 윈도우는 현재 처리 범위라는 구분부터 기억하면 됩니다. 구체적인 한도와 비용은 모델마다 달라지므로 사용할 때 해당 문서를 확인하겠습니다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://huggingface.co/docs/transformers/main/en/tokenizer_summary">Hugging Face 공식 문서 — 토큰화</a></li>
<li><a href="https://platform.claude.com/docs/en/build-with-claude/context-windows">Anthropic 공식 문서 — 컨텍스트 윈도우</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 오류 메시지를 읽고 최소 재현 예제로 줄이는 방법]]></title>
            <link>https://velog.io/@8113jsl-ai/til-minimal-reproducible-example</link>
            <guid>https://velog.io/@8113jsl-ai/til-minimal-reproducible-example</guid>
            <pubDate>Tue, 29 Sep 2026 11:17:58 GMT</pubDate>
            <description><![CDATA[<p>오류가 발생했을 때 전체 코드를 한꺼번에 바꾸면 원인을 확인하기 어려워집니다. 수정 후 정상 실행되더라도 어떤 변경이 문제를 해결했는지 설명하기 힘들 수 있어요.</p>
<p>이번에는 짧은 예제를 통해 오류를 재현하고, 관련 없는 부분을 줄인 다음 수정 이유를 검증하는 흐름을 정리합니다. 아래 코드는 설명을 위해 만든 예제입니다.</p>
<h2 id="예외-종류와-실패한-연산부터-확인합니다">예외 종류와 실패한 연산부터 확인합니다</h2>
<pre><code class="language-python">scores = [80, &quot;90&quot;]
try:
    print(sum(scores))
except TypeError as error:
    print(type(error).__name__)</code></pre>
<pre><code class="language-text">TypeError</code></pre>
<p>합계 계산에 정수와 문자열이 섞여 있습니다. 실제 프로그램의 traceback에서는 마지막 줄의 예외 종류와 메시지를 읽고, 그 위에서 자신의 코드가 실패한 위치를 확인하면 됩니다. 마지막 줄만으로 모든 원인을 알 수 있는 것은 아니므로 호출 경로도 함께 봐야 해요.</p>
<p>예제의 try/except는 오류 종류를 일정한 출력으로 보여주기 위한 것입니다. 실제 진단에서는 원본 traceback을 먼저 보존합니다.</p>
<h2 id="같은-오류가-남는-가장-작은-입력을-만듭니다">같은 오류가 남는 가장 작은 입력을 만듭니다</h2>
<p>이 문제가 긴 CSV 처리 과정에서 발생했더라도, 파일 전체가 재현에 꼭 필요한 것은 아닐 수 있습니다. 오류를 일으키는 값과 연산만 남겨 <code>[80, &quot;90&quot;]</code>과 <code>sum()</code>으로 줄여볼 수 있어요.</p>
<p>단, 코드를 줄인 뒤에도 원래와 같은 실패가 발생하는지 매 단계 확인해야 합니다. 다른 종류의 오류가 생겼다면 원래 문제를 재현한 것으로 볼 수 없습니다.</p>
<p>재현 예제를 공유할 때는 비밀번호, 토큰, 실제 고객 정보 등을 가상의 값으로 바꿉니다. 오류에 영향을 주는 자료형과 구조는 유지해야 합니다.</p>
<h2 id="변환이-맞는지는-입력의-의미에-달려-있어요">변환이 맞는지는 입력의 의미에 달려 있어요</h2>
<pre><code class="language-python">def total_score(values):
    return sum(int(value) for value in values)

print(total_score([80, &quot;90&quot;]))
print(total_score([]))</code></pre>
<pre><code class="language-text">170
0</code></pre>
<p>이 수정은 정수 또는 정수로 표현된 문자열을 허용한다는 전제에서 사용할 수 있습니다. 소수 점수를 허용한다면 정수 변환은 적절하지 않을 수 있어요. 입력이 반드시 정수여야 한다면 문자열을 조용히 바꾸기보다 잘못된 입력으로 거부하는 설계가 더 맞을 수도 있습니다.</p>
<h2 id="정상-사례와-실패-사례를-함께-남깁니다">정상 사례와 실패 사례를 함께 남깁니다</h2>
<pre><code class="language-python">assert total_score([80, &quot;90&quot;]) == 170
assert total_score([]) == 0

try:
    total_score([80, &quot;unknown&quot;])
except ValueError:
    print(&quot;invalid score&quot;)
else:
    raise AssertionError(&quot;invalid input was accepted&quot;)</code></pre>
<pre><code class="language-text">invalid score</code></pre>
<p>오류를 없애기 위해 무조건 0을 넣거나 모든 예외를 무시하면 잘못된 값이 정상 결과에 섞일 수 있습니다. 수정의 목표는 예외가 보이지 않게 만드는 것이 아니라, 입력 규칙에 맞게 처리하는 것입니다.</p>
<h2 id="다음에-남길-디버깅-기록">다음에 남길 디버깅 기록</h2>
<p>기대 결과, 실제 결과, 최소 입력, 예외 메시지, 실행 환경을 함께 적어 두려고 합니다. 수정한 뒤에는 재현 예제가 해결되었는지와 기존 정상 입력이 여전히 통과하는지를 확인합니다. 이렇게 남긴 작은 사례는 같은 오류가 다시 생기는지 확인하는 회귀 테스트로도 사용할 수 있어요.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://docs.python.org/3.14/tutorial/errors.html">Python 공식 문서 — 오류와 예외</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] ROW_NUMBER()로 고객별 최신 주문 한 건 찾기]]></title>
            <link>https://velog.io/@8113jsl-ai/sql-row-number-latest-order</link>
            <guid>https://velog.io/@8113jsl-ai/sql-row-number-latest-order</guid>
            <pubDate>Tue, 29 Sep 2026 11:17:00 GMT</pubDate>
            <description><![CDATA[<p>고객별 가장 최근 주문의 날짜뿐 아니라 주문 번호와 금액도 함께 필요할 때가 있습니다. 날짜에 <code>MAX()</code>를 적용하는 것만으로는 그 날짜에 해당하는 행의 다른 열까지 자동으로 선택되지 않아요.</p>
<p>이번 예제에서는 고객마다 주문을 최신순으로 정렬해 번호를 붙인 뒤 첫 행을 선택하겠습니다. 문법은 MySQL 8.4 기준이며, 예제 결과는 SQLite에서 검증합니다.</p>
<h2 id="같은-시각에-주문한-경우도-정해-둡니다">같은 시각에 주문한 경우도 정해 둡니다</h2>
<p>주문 시각이 같으면 주문 번호가 큰 행을 선택한다는 규칙을 사용하겠습니다. 주문 번호는 기본 키이므로 이 기준까지 적용하면 순서가 하나로 정해집니다. 이것은 동률 처리 규칙이지 큰 번호가 실제로 더 늦게 주문되었다는 증명은 아닙니다.</p>
<pre><code class="language-sql">CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    ordered_at DATETIME NOT NULL,
    amount INTEGER NOT NULL
);
INSERT INTO orders VALUES
    (101, 1, &#39;2026-09-27 09:00:00&#39;, 10000),
    (102, 1, &#39;2026-09-28 10:00:00&#39;, 20000),
    (103, 1, &#39;2026-09-28 10:00:00&#39;, 15000),
    (201, 2, &#39;2026-09-27 11:00:00&#39;, 7000);</code></pre>
<h2 id="고객별로-번호를-다시-매겨요">고객별로 번호를 다시 매겨요</h2>
<pre><code class="language-sql">WITH ranked AS (
    SELECT order_id, customer_id, ordered_at, amount,
           ROW_NUMBER() OVER (
               PARTITION BY customer_id
               ORDER BY ordered_at DESC, order_id DESC
           ) AS rn
    FROM orders
)
SELECT order_id, customer_id, ordered_at, amount
FROM ranked
WHERE rn = 1
ORDER BY customer_id;</code></pre>
<table>
<thead>
<tr>
<th>order_id</th>
<th>customer_id</th>
<th>ordered_at</th>
<th>amount</th>
</tr>
</thead>
<tbody><tr>
<td>103</td>
<td>1</td>
<td>2026-09-28 10:00:00</td>
<td>15000</td>
</tr>
<tr>
<td>201</td>
<td>2</td>
<td>2026-09-27 11:00:00</td>
<td>7000</td>
</tr>
</tbody></table>
<p><code>PARTITION BY</code>는 번호를 매길 구간을 고객별로 나눕니다. 그 안에서 주문 시각과 번호를 내림차순으로 정렬하고 <code>ROW_NUMBER()</code>가 1부터 번호를 붙여요. 바깥 쿼리는 1번 행만 남깁니다.</p>
<p>윈도 함수 안의 ORDER BY는 번호를 붙일 기준입니다. 최종 결과의 표시 순서는 바깥쪽 ORDER BY로 별도 지정했습니다.</p>
<h2 id="max를-각각-구하면-서로-다른-주문이-섞일-수-있어요">MAX를 각각 구하면 서로 다른 주문이 섞일 수 있어요</h2>
<pre><code class="language-sql">SELECT customer_id,
       MAX(ordered_at) AS latest_time,
       MAX(amount) AS max_amount
FROM orders
GROUP BY customer_id
ORDER BY customer_id;</code></pre>
<table>
<thead>
<tr>
<th>customer_id</th>
<th>latest_time</th>
<th>max_amount</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>2026-09-28 10:00:00</td>
<td>20000</td>
</tr>
<tr>
<td>2</td>
<td>2026-09-27 11:00:00</td>
<td>7000</td>
</tr>
</tbody></table>
<p>이 쿼리는 최신 시각과 최대 금액을 각각 구합니다. 선택 규칙에 따른 103번 주문의 금액은 15000인데, 최대 금액은 102번 주문의 20000입니다. 두 집계 결과가 같은 원본 행에서 왔다고 볼 수는 없어요.</p>
<h2 id="한-건과-공동-최신은-다른-요구입니다">한 건과 공동 최신은 다른 요구입니다</h2>
<p>같은 최신 시각의 주문을 모두 반환해야 한다면 지금의 한 건 선택 규칙은 맞지 않습니다. 시각만 기준으로 순위를 매기는 <code>RANK()</code> 등 다른 방법을 검토해야 해요. 또한 주문이 없는 고객은 orders 테이블에 행이 없으므로 이 결과에 나타나지 않습니다.</p>
<p>이 예제는 모든 주문 시각이 같은 시간 기준으로 저장되며 NULL이 없다고 가정합니다. 실제 데이터에서는 시간대와 누락 값의 처리 기준도 함께 확인해야 합니다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.4/en/window-function-descriptions.html">MySQL 공식 문서 — 윈도 함수</a></li>
<li><a href="https://velog.io/@8113jsl-ai/sql-sum-over-partition-by">SUM OVER와 GROUP BY 비교</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코딩테스트 기초 07] 처음 나온 순서를 유지하며 중복 제거하기]]></title>
            <link>https://velog.io/@8113jsl-ai/python-unique-preserve-order</link>
            <guid>https://velog.io/@8113jsl-ai/python-unique-preserve-order</guid>
            <pubDate>Tue, 29 Sep 2026 11:16:06 GMT</pubDate>
            <description><![CDATA[<p>정수 리스트에서 중복을 제거하되, 각 값이 처음 등장한 순서는 유지하는 문제입니다. 빈 입력은 빈 리스트를 반환하고 원본은 변경하지 않겠습니다.</p>
<pre><code class="language-text">[3, 1, 3, 2, 1] → [3, 1, 2]</code></pre>
<p><code>sorted(set(numbers))</code>는 중복을 제거하지만 결과를 크기순으로 바꿉니다. 이 문제에서 필요한 것은 정렬된 순서가 아니라 처음 등장한 순서예요.</p>
<h2 id="확인용-집합과-결과-리스트를-나눕니다">확인용 집합과 결과 리스트를 나눕니다</h2>
<pre><code class="language-python">def unique_in_order(numbers):
    seen = set()
    result = []
    for number in numbers:
        if number not in seen:
            seen.add(number)
            result.append(number)
    return result

print(unique_in_order([3, 1, 3, 2, 1]))
print(unique_in_order([]))
print(unique_in_order([0, -1, 0, -1, 2]))</code></pre>
<pre><code class="language-text">[3, 1, 2]
[]
[0, -1, 2]</code></pre>
<p><code>seen</code>은 이미 처리한 값을 확인하고, <code>result</code>는 출력 순서를 보존합니다. 집합을 순회해서 결과를 만드는 것이 아니라 입력 리스트를 순서대로 읽으므로 첫 등장 순서가 유지됩니다.</p>
<h2 id="두-자료구조의-역할이-다릅니다">두 자료구조의 역할이 다릅니다</h2>
<table>
<thead>
<tr>
<th>읽은 값</th>
<th>처리</th>
<th>결과 리스트</th>
</tr>
</thead>
<tbody><tr>
<td>3</td>
<td>처음이므로 추가</td>
<td><code>[3]</code></td>
</tr>
<tr>
<td>1</td>
<td>처음이므로 추가</td>
<td><code>[3, 1]</code></td>
</tr>
<tr>
<td>3</td>
<td>이미 있으므로 건너뜀</td>
<td><code>[3, 1]</code></td>
</tr>
<tr>
<td>2</td>
<td>처음이므로 추가</td>
<td><code>[3, 1, 2]</code></td>
</tr>
<tr>
<td>1</td>
<td>이미 있으므로 건너뜀</td>
<td><code>[3, 1, 2]</code></td>
</tr>
</tbody></table>
<p>앞에서 k개를 처리한 시점에 <code>seen</code>은 그 구간의 서로 다른 값을 담고, <code>result</code>는 각 값의 첫 등장 순서를 담습니다. 다음 값이 처음이면 두 곳에 추가하고, 이미 있다면 그대로 유지하면 됩니다.</p>
<h2 id="결과를-집합으로만-비교하면-부족해요">결과를 집합으로만 비교하면 부족해요</h2>
<pre><code class="language-python">assert unique_in_order([]) == []
assert unique_in_order([5, 5, 5]) == [5]
assert unique_in_order([3, 2, 1]) == [3, 2, 1]
assert unique_in_order([0, -1, 0]) == [0, -1]

numbers = [3, 1, 3, 2, 1]
before = numbers.copy()
assert unique_in_order(numbers) == [3, 1, 2]
assert numbers == before</code></pre>
<p><code>set(result)</code>만 비교하면 <code>[1, 2, 3]</code>도 통과할 수 있습니다. 순서까지 요구되는 문제이므로 기대 리스트와 직접 비교해야 합니다.</p>
<h2 id="복잡도와-적용-범위">복잡도와 적용 범위</h2>
<p>입력 길이가 n, 서로 다른 값의 수가 k일 때 집합 연산을 평균 O(1)로 보면 전체 평균 시간은 O(n), 집합과 결과를 저장하는 공간은 O(k)입니다. 결과 리스트에서만 <code>number not in result</code>를 검사하면 매번 선형 탐색이 필요해 최악 O(n²)이 될 수 있어요.</p>
<p>이 코드는 정수 입력을 전제로 합니다. 리스트처럼 해시할 수 없는 객체를 그대로 집합에 넣으면 오류가 납니다. 입력 범위가 달라지면 중복 판단 기준도 다시 정해야 합니다.</p>
<h3 id="함께-보기">함께 보기</h3>
<ul>
<li><a href="https://velog.io/@8113jsl-ai/python-dictionary-frequency-count">딕셔너리로 빈도표 만들기</a></li>
<li><a href="https://docs.python.org/3.14/library/stdtypes.html#set-types-set-frozenset">Python 공식 문서 — 집합</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] zip()으로 데이터를 묶을 때, 길이가 다르면 어떻게 될까요?]]></title>
            <link>https://velog.io/@8113jsl-ai/python-zip-strict-length-mismatch</link>
            <guid>https://velog.io/@8113jsl-ai/python-zip-strict-length-mismatch</guid>
            <pubDate>Tue, 29 Sep 2026 11:15:03 GMT</pubDate>
            <description><![CDATA[<p>이름과 점수를 별도 리스트로 받았다고 가정해 보겠습니다. 같은 위치의 원소를 함께 처리할 때는 <code>zip()</code>이 편리해요. 다만 두 리스트의 길이가 다를 때 어떤 결과가 필요한지 먼저 정해야 합니다.</p>
<h2 id="기본-동작은-짧은-쪽에서-멈춥니다">기본 동작은 짧은 쪽에서 멈춥니다</h2>
<pre><code class="language-python">names = [&quot;Ara&quot;, &quot;Min&quot;, &quot;Jun&quot;]
scores = [90, 80]
print(list(zip(names, scores)))</code></pre>
<pre><code class="language-text">[(&#39;Ara&#39;, 90), (&#39;Min&#39;, 80)]</code></pre>
<p>Jun은 결과에 포함되지 않습니다. 기본 <code>zip()</code>은 가장 짧은 입력이 끝나면 함께 종료하기 때문이에요. 짝이 있는 데이터만 필요하다면 적절하지만, 모든 학생에게 점수가 있어야 한다면 누락을 놓칠 수 있습니다.</p>
<h2 id="길이가-같아야-한다면-stricttrue를-사용해요">길이가 같아야 한다면 strict=True를 사용해요</h2>
<p>Python 3.10부터는 길이 불일치를 오류로 드러낼 수 있습니다.</p>
<pre><code class="language-python">names = [&quot;Ara&quot;, &quot;Min&quot;, &quot;Jun&quot;]
scores = [90, 80]

try:
    pairs = list(zip(names, scores, strict=True))
except ValueError:
    print(&quot;length mismatch&quot;)</code></pre>
<pre><code class="language-text">length mismatch</code></pre>
<p>중요한 점은 <code>zip()</code>이 이터레이터라는 사실입니다. 실제로 원소를 꺼내는 과정에서 불일치를 발견해요. <code>zip(..., strict=True)</code> 객체만 만들었다고 모든 길이가 검증된 것은 아닙니다. 중간에 반복을 멈추면 불일치가 드러나지 않을 수도 있어요.</p>
<p>파일 저장처럼 되돌리기 어려운 작업을 반복문 안에서 한다면, 길이 오류가 나기 전에 일부 작업이 이미 실행될 수도 있습니다. 작은 데이터라면 위처럼 먼저 리스트로 만들어 검증한 뒤 후속 작업을 하는 방법을 고려할 수 있어요.</p>
<h2 id="누락된-자리도-남기고-싶다면">누락된 자리도 남기고 싶다면</h2>
<pre><code class="language-python">from itertools import zip_longest

names = [&quot;Ara&quot;, &quot;Min&quot;, &quot;Jun&quot;]
scores = [90, 80]
print(list(zip_longest(names, scores, fillvalue=None)))</code></pre>
<pre><code class="language-text">[(&#39;Ara&#39;, 90), (&#39;Min&#39;, 80), (&#39;Jun&#39;, None)]</code></pre>
<p><code>zip_longest()</code>는 긴 입력이 끝날 때까지 진행하면서 부족한 자리를 지정한 값으로 채웁니다. 다만 실제 데이터에도 <code>None</code>이 들어갈 수 있다면, 저장된 None과 누락을 구분할 기준이 따로 필요합니다.</p>
<h2 id="길이가-같아도-올바른-짝이라는-보장은-없어요">길이가 같아도 올바른 짝이라는 보장은 없어요</h2>
<p>길이 검사는 개수만 확인합니다. 이름이 <code>[&quot;Ara&quot;, &quot;Min&quot;]</code>, 점수가 Min과 Ara 순서로 전달되었다면 길이가 같아도 잘못 연결됩니다. 서로 독립적으로 조회한 데이터라면 위치가 아니라 학생 ID 같은 공통 키로 연결해야 할 수 있어요.</p>
<p>정리하면, 짝이 있는 부분만 쓸지, 불일치를 오류로 볼지, 누락을 표시할지부터 결정하면 됩니다. <code>zip()</code>의 선택보다 먼저 정할 것은 데이터 누락의 의미입니다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://docs.python.org/3.14/library/functions.html#zip">Python 공식 문서 — zip</a></li>
<li><a href="https://docs.python.org/3.14/library/itertools.html#itertools.zip_longest">Python 공식 문서 — zip_longest</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IT·AI 용어사전] API와 MCP는 뭐가 다를까요?]]></title>
            <link>https://velog.io/@8113jsl-ai/it-ai-api-mcp-difference</link>
            <guid>https://velog.io/@8113jsl-ai/it-ai-api-mcp-difference</guid>
            <pubDate>Mon, 28 Sep 2026 08:32:07 GMT</pubDate>
            <description><![CDATA[<p>AI 도구를 살펴보다 보면 “API를 연결한다”, “MCP 서버를 붙인다”는 말을 만나게 돼요. 둘 다 외부 기능을 연결하는 이야기 같아서 처음에는 차이가 잘 와닿지 않을 수 있어요.</p>
<p>이 카테고리에서는 기본 IT 용어부터 AI 관련 용어까지 하나씩 풀어 보려고 해요. 첫 글은 <strong>API와 MCP의 관계</strong>예요.</p>
<h2 id="api는-다른-프로그램의-기능을-쓰는-접점이에요">API는 다른 프로그램의 기능을 쓰는 접점이에요</h2>
<p>API는 Application Programming Interface의 줄임말이에요. 프로그램이 다른 프로그램의 기능이나 데이터를 정해진 방식으로 이용할 수 있게 해 주는 인터페이스예요.</p>
<p>예를 들어 날씨 서비스가 도시를 입력받아 기온을 돌려주는 API를 제공한다고 해 볼게요. 개발자는 요청 형식에 맞춰 도시를 보내고, 응답으로 받은 기온을 화면에 보여 줄 수 있어요. 날씨 정보를 모으는 내부 구현을 전부 알아야 하는 것은 아니에요.</p>
<p>API가 꼭 인터넷을 거치는 것도 아니에요. 운영체제나 라이브러리가 제공하는 기능에도 API라는 말을 써요. 웹 API는 그중 웹을 통해 이용하는 경우라고 생각하면 됩니다.</p>
<h2 id="mcp는-ai-앱과-외부-도구를-연결하는-공통-규약이에요">MCP는 AI 앱과 외부 도구를 연결하는 공통 규약이에요</h2>
<p>MCP는 Model Context Protocol의 줄임말이에요. AI 애플리케이션이 외부 데이터와 도구를 연결해 사용하는 방식을 표준화한 프로토콜이에요.</p>
<p>여기서 모델과 AI 앱은 구분해 두면 좋아요. 모델 혼자 내 파일에 바로 접근하는 게 아니라, 앱이 제공하는 연결 기능과 허용된 권한 안에서 도구를 이용하게 됩니다.</p>
<p>날씨 도구를 제공하는 MCP 서버가 있다고 가정해 볼게요. AI 앱은 서버가 어떤 도구를 제공하는지 확인하고, 지원되는 방식으로 그 도구를 호출할 수 있어요.</p>
<h2 id="둘은-함께-쓰일-수도-있어요">둘은 함께 쓰일 수도 있어요</h2>
<p>다음은 이해를 위한 가상의 연결 예시예요.</p>
<pre><code class="language-text">사용자: “서울 날씨 알려 줘요”
        ↓
AI 앱의 MCP 클라이언트
        ↓ MCP로 도구 호출
날씨 MCP 서버
        ↓ 날씨 서비스 API 요청
날씨 서비스</code></pre>
<p>MCP 서버가 내부에서 기존 날씨 API를 호출하고, 받은 결과를 AI 앱에 전달할 수 있어요. MCP가 등장했다고 API가 없어지는 관계는 아닌 거죠.</p>
<table>
<thead>
<tr>
<th>용어</th>
<th>먼저 떠올릴 질문</th>
</tr>
</thead>
<tbody><tr>
<td>API</td>
<td>이 프로그램의 기능을 어떤 방식으로 이용하나요?</td>
</tr>
<tr>
<td>MCP</td>
<td>AI 앱과 외부 도구를 어떤 공통 규약으로 연결하나요?</td>
</tr>
</tbody></table>
<p>이 표는 입문용 구분이에요. API는 폭넓은 개념이고 MCP는 특정 프로토콜이라, 같은 범위의 기술 두 개를 비교하는 것은 아니라는 점도 함께 기억해 두면 좋아요.</p>
<h2 id="연결됐다는-말이-모든-권한을-뜻하지는-않아요">연결됐다는 말이 모든 권한을 뜻하지는 않아요</h2>
<p>MCP를 쓴다고 AI가 내 계정의 모든 정보를 읽거나 수정할 수 있는 것은 아니에요. 실제 가능한 일은 서버가 제공하는 기능, 로그인 상태, 부여한 권한, 앱의 승인 방식에 따라 달라져요.</p>
<p>도구를 붙일 때는 어떤 데이터를 읽는지와 수정·삭제 같은 동작도 가능한지를 따로 확인하면 좋아요. MCP 사용 자체가 안전성을 보장하는 것은 아니에요.</p>
<p>새로운 연결 기능을 만나면 “MCP니까 다 되겠지”보다 <strong>어떤 도구가 있고 어디까지 허용했을까?</strong>를 먼저 살펴보려고 해요. 이름을 외우는 것보다 실제 동작을 이해하는 데 도움이 됩니다.</p>
<p>참고 자료 · 2026년 9월 28일 확인</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Glossary/API">MDN — API</a></li>
<li><a href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP 공식 소개</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코딩테스트 기초 06] 목표를 가장 오래 연속으로 달성한 날 세기]]></title>
            <link>https://velog.io/@8113jsl-ai/python-longest-study-streak</link>
            <guid>https://velog.io/@8113jsl-ai/python-longest-study-streak</guid>
            <pubDate>Mon, 28 Sep 2026 08:30:30 GMT</pubDate>
            <description><![CDATA[<p>목표를 채운 날이 총 며칠인지와, 며칠 연속으로 채웠는지는 다른 질문이에요. 이번에는 기록이 중간에 끊기는 상황까지 생각하면서 가장 긴 연속 기록을 구해 볼게요.</p>
<blockquote>
<p>이 글은 직접 만든 연습 문제예요. 코드업이나 프로그래머스의 문제 원문·테스트 데이터를 옮긴 글이 아니며, 해당 플랫폼의 채점 결과도 아닙니다.</p>
</blockquote>
<h2 id="하루-기록을-하나씩-넘겨-봐요">하루 기록을 하나씩 넘겨 봐요</h2>
<p>날짜순으로 정리된 하루 공부 시간 <code>minutes</code>와 목표 시간 <code>target</code>이 주어져요. 목표 이상 공부한 날이 가장 길게 이어진 기간을 정수로 반환하면 됩니다.</p>
<p>공부 시간은 0 이상의 정수, 목표는 양의 정수라고 가정해요. 리스트의 원소 하나가 하루이며, 빠진 날짜는 없어요. 빈 리스트의 답은 0이에요.</p>
<pre><code class="language-python">minutes = [30, 45, 10, 60, 30, 40, 0]
target = 30</code></pre>
<p>처음 이틀을 달성한 뒤 하루 목표를 못 채우고, 다시 사흘을 달성했어요. 따라서 답은 <code>3</code>이에요. 달성한 날의 총합인 <code>5</code>와 구분해야 해요.</p>
<h2 id="현재-기록과-최고-기록을-따로-둬요">현재 기록과 최고 기록을 따로 둬요</h2>
<pre><code class="language-python">def longest_streak(minutes, target):
    current = 0
    best = 0

    for minute in minutes:
        if minute &gt;= target:
            current += 1
            best = max(best, current)
        else:
            current = 0

    return best</code></pre>
<p><code>current</code>는 지금까지 이어지고 있는 기록이에요. 목표를 못 채운 날을 만나면 0으로 돌아가요.</p>
<p>반면 <code>best</code>는 이미 달성했던 가장 긴 기록이에요. 오늘 기록이 끊겨도 과거의 최고 기록까지 사라지면 안 되니 그대로 남겨 둡니다.</p>
<p>예제에서 두 변수는 다음처럼 바뀌어요.</p>
<table>
<thead>
<tr>
<th align="right">공부 시간</th>
<th align="right">current</th>
<th align="right">best</th>
</tr>
</thead>
<tbody><tr>
<td align="right">30</td>
<td align="right">1</td>
<td align="right">1</td>
</tr>
<tr>
<td align="right">45</td>
<td align="right">2</td>
<td align="right">2</td>
</tr>
<tr>
<td align="right">10</td>
<td align="right">0</td>
<td align="right">2</td>
</tr>
<tr>
<td align="right">60</td>
<td align="right">1</td>
<td align="right">2</td>
</tr>
<tr>
<td align="right">30</td>
<td align="right">2</td>
<td align="right">2</td>
</tr>
<tr>
<td align="right">40</td>
<td align="right">3</td>
<td align="right">3</td>
</tr>
<tr>
<td align="right">0</td>
<td align="right">0</td>
<td align="right">3</td>
</tr>
</tbody></table>
<p>마지막 날이 0분이어도 답은 3이에요. 그래서 마지막에 반환할 값은 <code>current</code>가 아니라 <code>best</code>예요.</p>
<h2 id="끝까지-이어지는-경우도-확인해요">끝까지 이어지는 경우도 확인해요</h2>
<pre><code class="language-python">assert longest_streak([], 30) == 0
assert longest_streak([10, 20], 30) == 0
assert longest_streak([30], 30) == 1
assert longest_streak([30, 40, 50], 30) == 3
assert longest_streak([30, 30, 0, 30], 30) == 2
assert longest_streak([30, 45, 10, 60, 30, 40, 0], 30) == 3</code></pre>
<p>목표와 정확히 같은 날도 포함하므로 비교는 <code>&gt;=</code>예요. 최고 기록을 달성할 때마다 갱신해서 마지막 날까지 계속 달성한 경우도 놓치지 않아요.</p>
<p>리스트를 한 번 읽으므로 시간 복잡도는 O(n), 추가 공간은 O(1)이에요. 원본 리스트는 바꾸지 않습니다.</p>
<p>비슷한 반복문이라도 무엇을 기억할지에 따라 결과가 달라져요. 이번 문제에서는 <strong>지금 이어지는 기록과 지금까지의 최고 기록</strong>을 나눠 두는 게 핵심이에요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] 주문 내역 옆에 고객별 합계를 붙이고 싶다면? SUM OVER]]></title>
            <link>https://velog.io/@8113jsl-ai/sql-sum-over-partition-by</link>
            <guid>https://velog.io/@8113jsl-ai/sql-sum-over-partition-by</guid>
            <pubDate>Mon, 28 Sep 2026 08:29:27 GMT</pubDate>
            <description><![CDATA[<p>고객별 주문 합계를 구할 때는 <code>GROUP BY</code>를 쓰면 돼요. 그런데 주문 한 건의 금액과 그 고객의 전체 주문 금액을 나란히 보고 싶다면 어떨까요?</p>
<p>이번에는 행을 합치지 않고 합계만 옆에 붙여 보려고 해요. 예제는 직접 만든 데이터이며 SQLite에서 실행했어요.</p>
<h2 id="주문-세-건으로-시작해-볼게요">주문 세 건으로 시작해 볼게요</h2>
<pre><code class="language-sql">CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    customer TEXT NOT NULL,
    amount INTEGER NOT NULL
);

INSERT INTO orders VALUES
    (1, &#39;가온&#39;, 12000),
    (2, &#39;나래&#39;, 8000),
    (3, &#39;가온&#39;, 5000);</code></pre>
<p>가온은 두 번, 나래는 한 번 주문했어요. 고객별로 묶으면 결과는 두 행이 됩니다.</p>
<pre><code class="language-sql">SELECT customer, SUM(amount) AS total
FROM orders
GROUP BY customer
ORDER BY customer;</code></pre>
<table>
<thead>
<tr>
<th>customer</th>
<th align="right">total</th>
</tr>
</thead>
<tbody><tr>
<td>가온</td>
<td align="right">17000</td>
</tr>
<tr>
<td>나래</td>
<td align="right">8000</td>
</tr>
</tbody></table>
<p>전체 금액을 비교할 때는 이 결과로 충분해요. 다만 개별 주문 번호와 금액은 보이지 않죠.</p>
<h2 id="over를-붙이면-주문-행을-그대로-볼-수-있어요">OVER를 붙이면 주문 행을 그대로 볼 수 있어요</h2>
<pre><code class="language-sql">SELECT
    order_id,
    customer,
    amount,
    SUM(amount) OVER (PARTITION BY customer) AS customer_total
FROM orders
ORDER BY order_id;</code></pre>
<table>
<thead>
<tr>
<th align="right">order_id</th>
<th>customer</th>
<th align="right">amount</th>
<th align="right">customer_total</th>
</tr>
</thead>
<tbody><tr>
<td align="right">1</td>
<td>가온</td>
<td align="right">12000</td>
<td align="right">17000</td>
</tr>
<tr>
<td align="right">2</td>
<td>나래</td>
<td align="right">8000</td>
<td align="right">8000</td>
</tr>
<tr>
<td align="right">3</td>
<td>가온</td>
<td align="right">5000</td>
<td align="right">17000</td>
</tr>
</tbody></table>
<p><code>PARTITION BY customer</code>는 계산할 범위를 고객별로 나눠 줘요. 같은 고객의 금액을 더한 뒤 각 주문 행 옆에 그 결과를 보여 줍니다. 이런 방식으로 계산하는 함수를 <strong>윈도우 함수</strong>라고 해요.</p>
<p>이 쿼리에서는 원래 주문 세 행이 그대로 남아요. 가온의 합계가 두 번 나오는 것도 중복 오류가 아니라, 두 주문에 같은 고객 합계가 붙은 결과예요.</p>
<h2 id="합계가-반복된다고-다시-더하면-안-돼요">합계가 반복된다고 다시 더하면 안 돼요</h2>
<p>위 결과의 <code>customer_total</code>을 모두 더하면 42000이 돼요. 실제 전체 주문 금액인 25000과 다르죠. 가온의 17000이 두 번 더해졌기 때문이에요.</p>
<p>고객별 합계만 필요한지, 주문 내역과 함께 볼 것인지에 따라 결과의 한 행이 무엇을 뜻하는지 먼저 정하면 좋아요.</p>
<p>또 마지막의 <code>ORDER BY order_id</code>는 출력 순서를 정해요. <code>OVER</code> 안에 <code>ORDER BY</code>를 추가하는 것은 계산 범위에도 영향을 줄 수 있으므로, 보기 좋게 정렬하려고 무심코 넣지는 않는 편이 좋아요. 이번 예제에서는 고객 전체 합계를 구하려고 <code>OVER</code> 안의 정렬을 생략했어요.</p>
<p>참고: <a href="https://www.sqlite.org/windowfunctions.html">SQLite 공식 문서 — Window Functions</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] enumerate()로 순서와 값을 함께 꺼내요]]></title>
            <link>https://velog.io/@8113jsl-ai/python-enumerate-start-index</link>
            <guid>https://velog.io/@8113jsl-ai/python-enumerate-start-index</guid>
            <pubDate>Mon, 28 Sep 2026 08:28:24 GMT</pubDate>
            <description><![CDATA[<p>리스트를 출력하다 보면 값 옆에 번호도 붙이고 싶을 때가 있어요. 공부할 주제를 1번, 2번, 3번으로 보여 주는 식이죠. 이럴 때 쓰기 좋은 함수가 <code>enumerate()</code>예요.</p>
<h2 id="번호를-직접-늘리지-않아도-돼요">번호를 직접 늘리지 않아도 돼요</h2>
<pre><code class="language-python">topics = [&quot;변수&quot;, &quot;조건문&quot;, &quot;반복문&quot;]

for index, topic in enumerate(topics):
    print(index, topic)</code></pre>
<p>출력은 다음과 같아요.</p>
<pre><code class="language-text">0 변수
1 조건문
2 반복문</code></pre>
<p><code>enumerate()</code>는 반복할 때마다 번호와 값을 한 쌍으로 건네줘요. 기본 번호는 0부터 시작하므로, 이 예제에서는 리스트 인덱스와 같아요.</p>
<p>번호만 필요하다면 <code>range()</code>를 쓰면 되고, 값만 필요하다면 <code>for topic in topics</code>로 충분해요. 둘 다 필요할 때 <code>enumerate()</code>를 떠올리면 됩니다.</p>
<h2 id="사람에게-보여-줄-번호라면-1부터-시작해요">사람에게 보여 줄 번호라면 1부터 시작해요</h2>
<pre><code class="language-python">for number, topic in enumerate(topics, start=1):
    print(f&quot;{number}. {topic}&quot;)</code></pre>
<pre><code class="language-text">1. 변수
2. 조건문
3. 반복문</code></pre>
<p><code>start=1</code>은 붙여 주는 번호의 시작값을 바꿔요. <strong>리스트 자체의 인덱스를 바꾸는 것은 아니에요.</strong></p>
<p>그래서 이 반복문 안에서 <code>topics[number]</code>로 값을 꺼내면 의도와 달라져요. 첫 번째 반복에서 이미 두 번째 원소를 가리키고, 마지막에는 범위를 벗어나요. 옆에서 함께 받은 <code>topic</code>을 사용하면 됩니다.</p>
<h2 id="조건에-맞는-항목만-골라도-원래-번호는-남아요">조건에 맞는 항목만 골라도 원래 번호는 남아요</h2>
<pre><code class="language-python">minutes = [20, 45, 30, 10]

for day, minute in enumerate(minutes, start=1):
    if minute &gt;= 30:
        print(f&quot;{day}일차: {minute}분&quot;)</code></pre>
<pre><code class="language-text">2일차: 45분
3일차: 30분</code></pre>
<p>목표를 채운 날만 출력하지만 번호가 1, 2로 다시 매겨지지는 않아요. 원래 기록을 순서대로 돌면서 번호를 붙였기 때문이에요. 달성한 날을 1번부터 다시 세고 싶다면 필터링한 결과에 번호를 붙여야 해요.</p>
<p>빈 리스트라면 반복문 본문은 한 번도 실행되지 않아요. 별도 예외 처리 없이 자연스럽게 끝납니다.</p>
<p>번호가 <strong>원래 위치인지, 화면에 보여 줄 순번인지</strong>를 정해 두면 변수 이름도 고르기 쉬워요. 인덱스에는 <code>index</code>, 표시용 번호에는 <code>number</code>처럼 역할을 드러내 두려고 해요.</p>
<p>참고: <a href="https://docs.python.org/3/library/functions.html#enumerate">Python 공식 문서 — enumerate</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] .gitignore에 적었는데 왜 계속 올라갈까요?]]></title>
            <link>https://velog.io/@8113jsl-ai/til-gitignore-tracked-files</link>
            <guid>https://velog.io/@8113jsl-ai/til-gitignore-tracked-files</guid>
            <pubDate>Mon, 28 Sep 2026 08:26:51 GMT</pubDate>
            <description><![CDATA[<p>개발 메모를 복습하면서 <code>.env</code>와 <code>.gitignore</code>의 역할을 함께 정리해 봅니다. 둘 다 이름 앞에 점이 있어서 비슷해 보이지만, 하는 일은 달라요.</p>
<h2 id="파일-이름-앞에-점만-붙이면-될까요">파일 이름 앞에 점만 붙이면 될까요?</h2>
<p><code>.env</code>는 환경 설정을 적어 두는 파일 이름으로 자주 쓰여요. 이름이 <code>.env</code>라는 이유만으로 Git이 자동으로 제외해 주지는 않아요. 비밀값을 담았다면 파일 이름만 믿으면 안 되겠죠.</p>
<p>프로젝트 루트의 <code>.gitignore</code>에 아래처럼 적을 수 있어요.</p>
<pre><code class="language-gitignore">.env
.venv/
__pycache__/</code></pre>
<p>이 예제에서는 <code>.env</code> 파일, <code>.venv</code> 디렉터리, Python이 만드는 <code>__pycache__</code> 디렉터리를 무시하도록 지정했어요. 실제로 어떤 파일을 제외할지는 프로젝트에 맞춰 정하면 됩니다.</p>
<p>여기서 기억할 점은 <strong>이미 Git이 추적하는 파일에는 이 규칙만 추가해도 효과가 없다는 것</strong>이에요. <code>.gitignore</code>는 과거 커밋을 지우는 기능도 아니에요.</p>
<h2 id="먼저-추적-중인지-살펴봐요">먼저 추적 중인지 살펴봐요</h2>
<p>다음 명령은 프로젝트 저장소 안에서 실행해요.</p>
<pre><code class="language-bash">git ls-files -- .env</code></pre>
<p><code>.env</code>가 출력된다면 Git이 이미 추적하고 있는 상태예요. 아직 추적하지 않는 파일에 어떤 무시 규칙이 적용되는지 알고 싶다면 다음 명령을 사용할 수 있어요.</p>
<pre><code class="language-bash">git check-ignore -v .env</code></pre>
<p>규칙과 파일 경로가 출력되면 어느 설정에서 제외했는지 확인할 수 있어요. 출력이 없을 때는 파일이 이미 추적 중인 상황인지도 함께 살펴봐야 해요.</p>
<h2 id="추적을-멈추는-것과-기록을-지우는-것은-달라요">추적을 멈추는 것과 기록을 지우는 것은 달라요</h2>
<p>이미 추적 중인 <code>.env</code>를 다음 커밋부터 제외하려면, 무시 규칙을 추가한 뒤 아래 명령을 사용할 수 있어요.</p>
<pre><code class="language-bash">git rm --cached -- .env
git status</code></pre>
<p><code>--cached</code>를 붙이면 작업 폴더의 파일은 남기고 Git의 추적 대상에서 제거해요. 변경 내용을 확인한 뒤 커밋해야 저장소에도 반영됩니다.</p>
<p>다만 이 작업으로 예전 커밋의 내용까지 사라지지는 않아요. 실제 키가 공개됐다면 파일만 지우고 끝낼 게 아니라, 해당 서비스에서 키를 폐기하고 새로 발급해야 합니다.</p>
<p>이번 복습에서 남겨 둘 문장은 간단해요. <strong>파일 이름, 무시 규칙, 기존 추적 여부를 따로 확인하자.</strong> 이 세 가지를 구분하면 “적어 뒀는데 왜 계속 보이지?”라는 상황을 이해하기 쉬워져요.</p>
<p>참고: <a href="https://git-scm.com/docs/gitignore">Git 공식 문서 — gitignore</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 정답이 여러 개일 때 — 동률 처리와 출력 순서를 명시하기]]></title>
            <link>https://velog.io/@8113jsl-ai/til-tie-breaking-output-order</link>
            <guid>https://velog.io/@8113jsl-ai/til-tie-breaking-output-order</guid>
            <pubDate>Sun, 27 Sep 2026 03:40:49 GMT</pubDate>
            <description><![CDATA[<h2 id="값이-맞는-것과-순서까지-맞는-것은-다르다">값이 맞는 것과 순서까지 맞는 것은 다르다</h2>
<p>최빈값을 찾거나 점수순으로 정렬할 때, 첫 번째 기준만으로 결과가 하나로 결정되지 않을 수 있다. 이때 반복문을 먼저 작성하면 “먼저 만난 값”이나 “마지막에 만난 값”이 별다른 근거 없이 답이 되기 쉽다.</p>
<p>오늘 정리할 기준은 동률 처리와 출력 순서를 요구사항의 일부로 보는 것이다.</p>
<h2 id="같은-데이터에도-서로-다른-정답-규칙이-있다">같은 데이터에도 서로 다른 정답 규칙이 있다</h2>
<p>입력이 <code>[4, 2, 4, 2, 7]</code>이면 4와 2가 각각 두 번 등장한다.</p>
<table>
<thead>
<tr>
<th>동률 규칙</th>
<th>기대 결과</th>
</tr>
</thead>
<tbody><tr>
<td>최빈값 중 가장 작은 값</td>
<td><code>2</code></td>
</tr>
<tr>
<td>최빈값 중 먼저 등장한 값</td>
<td><code>4</code></td>
</tr>
<tr>
<td>모든 최빈값을 오름차순으로</td>
<td><code>[2, 4]</code></td>
</tr>
</tbody></table>
<p>세 결과는 서로 모순되지 않는다. 서로 다른 질문의 답이기 때문이다. 문제에서 규칙을 정했다면 그대로 구현하고, 직접 설계하는 함수라면 호출자가 알 수 있도록 규칙을 명시해야 한다.</p>
<h2 id="안정-정렬이-동률-규칙을-대신하지는-않는다">안정 정렬이 동률 규칙을 대신하지는 않는다</h2>
<pre><code class="language-python">records = [(&quot;B&quot;, 90), (&quot;A&quot;, 90), (&quot;C&quot;, 80)]
by_score = sorted(records, key=lambda row: -row[1])
by_score_and_name = sorted(records, key=lambda row: (-row[1], row[0]))
print(by_score)
print(by_score_and_name)</code></pre>
<pre><code class="language-text">[(&#39;B&#39;, 90), (&#39;A&#39;, 90), (&#39;C&#39;, 80)]
[(&#39;A&#39;, 90), (&#39;B&#39;, 90), (&#39;C&#39;, 80)]</code></pre>
<p>첫 정렬은 동점자의 입력 순서를 유지한다. 두 번째 정렬은 이름을 다음 기준으로 사용한다. 입력 순서가 곧 업무상 우선순위가 아니라면, 안정 정렬이라는 성질만으로 원하는 결과를 얻었다고 판단할 수 없다.</p>
<h2 id="입력-순서를-바꾸는-테스트">입력 순서를 바꾸는 테스트</h2>
<pre><code class="language-python">def rank_records(records):
    return sorted(records, key=lambda row: (-row[1], row[0]))

first = [(&quot;B&quot;, 90), (&quot;A&quot;, 90), (&quot;C&quot;, 80)]
second = [(&quot;C&quot;, 80), (&quot;A&quot;, 90), (&quot;B&quot;, 90)]
assert rank_records(first) == rank_records(second)</code></pre>
<p>이 예제는 이름이 서로 다른 데이터다. 점수와 이름으로 순서가 결정되므로 입력 순서를 바꿔도 같은 결과가 나와야 한다. 반대로 “먼저 접수한 항목 우선”이 조건이라면 순서를 바꾸었을 때 결과가 달라지는 것이 정상일 수 있다. 테스트의 성질도 요구사항에서 도출해야 한다.</p>
<h2 id="sql에서도-다음-정렬-기준이-필요하다">SQL에서도 다음 정렬 기준이 필요하다</h2>
<p><code>ORDER BY score DESC</code>만 사용하면 동점 행 사이의 순서는 정해지지 않는다. 동점자를 고유한 학생 번호순으로 보여주려면 <code>ORDER BY score DESC, student_id ASC</code>처럼 끝까지 구분 가능한 기준을 추가한다.</p>
<p>특히 상위 일부 행만 조회할 때는 경계에 동점자가 있는지 확인해야 한다. “정해진 개수만 반환”과 “경계 점수의 동점자도 모두 포함”은 서로 다른 요구다. 정렬 기준을 추가하는 것만으로 후자의 포함 규칙까지 해결되지는 않는다.</p>
<h2 id="다음-풀이에서-확인할-항목">다음 풀이에서 확인할 항목</h2>
<ol>
<li>최고 점수나 최대 빈도가 같은 후보가 여러 개일 수 있는가.</li>
<li>하나만 선택하는가, 전부 반환하는가.</li>
<li>하나만 선택한다면 최소값·입력 순서 등 어떤 기준을 쓰는가.</li>
<li>여러 개를 반환한다면 순서도 채점이나 사용 조건에 포함되는가.</li>
</ol>
<p>코드가 반복해서 같은 값을 반환하는 것과 명세에 맞는 값을 반환하는 것은 다르다. 우연한 순서에 기대지 않고 선택 규칙을 코드와 테스트 양쪽에 드러내는 것이 이번 정리의 핵심이다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://docs.python.org/3.14/howto/sorting.html#sort-stability-and-complex-sorts">Python 공식 문서 — 안정 정렬</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.4/en/limit-optimization.html">MySQL 공식 문서 — LIMIT과 정렬</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] CASE WHEN 조건부 집계 — 상태별 주문 수와 NULL 처리]]></title>
            <link>https://velog.io/@8113jsl-ai/sql-case-conditional-aggregation</link>
            <guid>https://velog.io/@8113jsl-ai/sql-case-conditional-aggregation</guid>
            <pubDate>Sun, 27 Sep 2026 03:40:04 GMT</pubDate>
            <description><![CDATA[<h2 id="상태별-건수를-한-행에-표현하기">상태별 건수를 한 행에 표현하기</h2>
<p>고객별 전체 주문 수와 결제·취소 주문 수를 나란히 조회하려고 한다. <code>WHERE status = &#39;paid&#39;</code>로 먼저 제한하면 취소 주문이 집계 대상에서 사라진다. 서로 다른 조건의 건수를 같은 행에 표시하려면 조건부 집계를 사용할 수 있다.</p>
<p>예제는 MySQL 8.4에서 사용할 수 있는 문법으로 작성했으며, 아래 결과는 SQLite에서 실행해 검증했다. MySQL 실행 계획이나 성능을 측정한 예제는 아니다.</p>
<h2 id="실습-데이터">실습 데이터</h2>
<pre><code class="language-sql">CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    status VARCHAR(20)
);
INSERT INTO orders VALUES
    (1, 10, &#39;paid&#39;),
    (2, 10, &#39;paid&#39;),
    (3, 10, &#39;cancelled&#39;),
    (4, 20, &#39;pending&#39;),
    (5, 20, NULL),
    (6, 30, &#39;cancelled&#39;);</code></pre>
<h2 id="case로-1과-0을-만든-뒤-더한다">CASE로 1과 0을 만든 뒤 더한다</h2>
<pre><code class="language-sql">SELECT customer_id,
       COUNT(*) AS total_count,
       SUM(CASE WHEN status = &#39;paid&#39; THEN 1 ELSE 0 END) AS paid_count,
       SUM(CASE WHEN status = &#39;cancelled&#39; THEN 1 ELSE 0 END) AS cancelled_count,
       SUM(CASE WHEN status IS NULL THEN 1 ELSE 0 END) AS unknown_count
FROM orders
GROUP BY customer_id
ORDER BY customer_id;</code></pre>
<table>
<thead>
<tr>
<th>customer_id</th>
<th>total_count</th>
<th>paid_count</th>
<th>cancelled_count</th>
<th>unknown_count</th>
</tr>
</thead>
<tbody><tr>
<td>10</td>
<td>3</td>
<td>2</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>20</td>
<td>2</td>
<td>0</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>30</td>
<td>1</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
</tbody></table>
<p>결제 건수의 CASE 식은 결제 행에서 1, 나머지 행에서 0이 된다. 고객별로 이 값을 더하면 조건에 맞는 건수만 남는다. NULL 여부는 <code>= NULL</code>이 아니라 <code>IS NULL</code>로 검사한다.</p>
<p>20번 고객의 전체 주문은 2건이지만 표시한 상태별 건수의 합은 1이다. <code>pending</code>을 별도 열로 집계하지 않았기 때문이다. 일부 상태만 집계했다면 그 합이 전체 건수와 같을 것이라고 가정하면 안 된다.</p>
<h2 id="count와-sum에-같은-식을-넣으면-안-된다">COUNT와 SUM에 같은 식을 넣으면 안 된다</h2>
<pre><code class="language-sql">SELECT
    COUNT(CASE WHEN status = &#39;paid&#39; THEN 1 ELSE 0 END) AS wrong_count,
    COUNT(CASE WHEN status = &#39;paid&#39; THEN 1 END) AS paid_count
FROM orders;</code></pre>
<table>
<thead>
<tr>
<th>wrong_count</th>
<th>paid_count</th>
</tr>
</thead>
<tbody><tr>
<td>6</td>
<td>2</td>
</tr>
</tbody></table>
<p>COUNT는 0도 NULL이 아닌 값으로 센다. 따라서 첫 번째 식은 모든 주문을 센다. 두 번째 식은 ELSE를 생략했으므로 미일치 행에서 NULL이 되고, COUNT의 대상에서 제외된다.</p>
<p><code>SUM</code>으로 1과 0을 더하는 방식과 <code>COUNT</code>로 1과 NULL을 세는 방식은 구분해서 읽어야 한다.</p>
<h2 id="입력이-아예-없을-때">입력이 아예 없을 때</h2>
<pre><code class="language-sql">SELECT
    SUM(CASE WHEN status = &#39;paid&#39; THEN 1 ELSE 0 END) AS raw_sum,
    COALESCE(SUM(CASE WHEN status = &#39;paid&#39; THEN 1 ELSE 0 END), 0) AS paid_count,
    COUNT(CASE WHEN status = &#39;paid&#39; THEN 1 END) AS paid_count_by_count
FROM orders
WHERE customer_id = 999;</code></pre>
<table>
<thead>
<tr>
<th>raw_sum</th>
<th>paid_count</th>
<th>paid_count_by_count</th>
</tr>
</thead>
<tbody><tr>
<td>NULL</td>
<td>0</td>
<td>0</td>
</tr>
</tbody></table>
<p>행이 있지만 조건에 맞지 않는 경우와, 집계할 행 자체가 없는 경우는 다르다. 후자의 SUM은 NULL이므로 0을 표시하려면 COALESCE 같은 처리가 필요하다. 이 쿼리는 GROUP BY가 없어 집계 결과 한 행을 반환한다.</p>
<p>반면 첫 쿼리의 GROUP BY에는 주문이 없는 고객의 그룹 자체가 없다. COALESCE만 추가한다고 그런 고객 행이 생기지는 않는다. 고객 전체가 필요하면 고객 테이블을 기준으로 한 LEFT JOIN 등을 별도로 설계해야 한다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.4/en/flow-control-functions.html">MySQL 공식 문서 — CASE 표현식</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.4/en/aggregate-functions.html">MySQL 공식 문서 — 집계 함수</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코딩테스트 기초 05] 최빈값 찾기 — 동률이면 가장 작은 값 선택하기]]></title>
            <link>https://velog.io/@8113jsl-ai/python-mode-frequency-tie-break</link>
            <guid>https://velog.io/@8113jsl-ai/python-mode-frequency-tie-break</guid>
            <pubDate>Sun, 27 Sep 2026 03:39:15 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-정의-최빈값이-여러-개라면-무엇을-반환할까">문제 정의: 최빈값이 여러 개라면 무엇을 반환할까</h2>
<p>정수 리스트에서 가장 많이 등장한 값을 반환한다. 최빈값이 여러 개라면 가장 작은 정수를 선택하고, 빈 리스트라면 <code>None</code>을 반환한다. 입력 리스트는 변경하지 않는다.</p>
<p>이 글의 동률 규칙은 직접 만든 연습 문제의 조건이다. 다른 문제에서는 첫 등장 값이나 모든 최빈값을 요구할 수 있으므로 그대로 적용하면 안 된다.</p>
<pre><code class="language-text">[4, 2, 4, 2, 7] → 2
[3, 3, 1] → 3
[] → None</code></pre>
<h2 id="빈도표와-후보-선택을-분리한다">빈도표와 후보 선택을 분리한다</h2>
<pre><code class="language-python">def smallest_mode(numbers):
    counts = {}
    for number in numbers:
        counts[number] = counts.get(number, 0) + 1

    best_value = None
    best_count = 0
    for value, count in counts.items():
        if (best_value is None
                or count &gt; best_count
                or (count == best_count and value &lt; best_value)):
            best_value = value
            best_count = count
    return best_value

print(smallest_mode([4, 2, 4, 2, 7]))
print(smallest_mode([3, 3, 1]))
print(smallest_mode([]))</code></pre>
<pre><code class="language-text">2
3
None</code></pre>
<p>첫 반복문은 값별 등장 횟수를 만든다. 두 번째 반복문은 현재까지 확인한 값 중 가장 좋은 후보를 유지한다. 더 많이 등장한 값이면 교체하고, 횟수가 같으면 더 작은 값으로 교체한다.</p>
<p><code>best_value is None</code>은 아직 후보가 없음을 의미한다. 입력은 정수로 제한했으므로 실제 값과 구분된다. 또한 <code>or</code>는 앞 조건이 참이면 뒤 조건을 평가하지 않으므로 첫 후보에서 정수와 <code>None</code>을 크기 비교하지 않는다.</p>
<h2 id="횟수만-비교하면-입력-순서에-끌려간다">횟수만 비교하면 입력 순서에 끌려간다</h2>
<p><code>[4, 2, 4, 2, 7]</code>의 빈도표에서 4와 2의 횟수는 모두 2다. 후보 교체 조건을 <code>count &gt; best_count</code>만으로 두면 먼저 확인한 4가 남는다. 요구한 답은 2이므로 동률 조건이 반드시 필요하다.</p>
<p>반대로 단순히 <code>&gt;=</code>로 바꾸면 마지막에 확인한 동률 후보가 선택된다. 이것도 “가장 작은 값”이라는 조건과 다르다. 비교 연산자 하나를 바꾸는 것으로 문제의 규칙을 대신할 수는 없다.</p>
<h2 id="경계값과-입력-순서-검증">경계값과 입력 순서 검증</h2>
<pre><code class="language-python">assert smallest_mode([]) is None
assert smallest_mode([0]) == 0
assert smallest_mode([-2, -1, -2, -1]) == -2
assert smallest_mode([5, 3, 1]) == 1
assert smallest_mode([8, 8, 2]) == 8
assert smallest_mode([4, 2, 4, 2, 7]) == 2
assert smallest_mode([7, 2, 4, 2, 4]) == 2

numbers = [4, 2, 4, 2, 7]
before = numbers.copy()
smallest_mode(numbers)
assert numbers == before</code></pre>
<p>이 문제의 정답은 값별 횟수와 대소 관계로 결정된다. 같은 원소들의 순서만 바꾸어도 정답은 같아야 한다. 0이나 음수를 포함한 사례는 초기값을 임의의 정수로 두었을 때 생기는 오류를 확인하는 데 유용하다.</p>
<h2 id="복잡도">복잡도</h2>
<p>입력 길이를 n, 서로 다른 정수 수를 k라고 하면 빈도표 생성은 평균 O(n), 후보 탐색은 O(k)다. 딕셔너리 연산을 평균 O(1)로 보는 조건에서 전체 평균 시간은 O(n), 추가 공간은 O(k)다. 최빈값 하나만 필요하므로 모든 후보를 정렬할 필요는 없다.</p>
<h3 id="함께-보기">함께 보기</h3>
<ul>
<li><a href="https://velog.io/@8113jsl-ai/python-dictionary-frequency-count">딕셔너리로 빈도표 만들기</a></li>
<li><a href="https://docs.python.org/3.14/library/stdtypes.html#boolean-operations-and-or-not">Python 공식 문서 — 불리언 연산</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] sort()와 sorted() — 원본 변경과 정렬 기준 구분하기]]></title>
            <link>https://velog.io/@8113jsl-ai/python-sort-sorted-key-stability</link>
            <guid>https://velog.io/@8113jsl-ai/python-sort-sorted-key-stability</guid>
            <pubDate>Sun, 27 Sep 2026 03:38:29 GMT</pubDate>
            <description><![CDATA[<h2 id="정렬-결과뿐-아니라-원본도-확인하기">정렬 결과뿐 아니라 원본도 확인하기</h2>
<p>정렬 코드를 읽을 때는 결과의 순서와 원본 변경 여부를 따로 확인해야 한다. <code>list.sort()</code>는 원본 리스트를 정렬하고 <code>None</code>을 반환한다. <code>sorted()</code>는 정렬된 새 리스트를 반환한다.</p>
<pre><code class="language-python">numbers = [4, 1, 3]
ordered = sorted(numbers)
print(numbers)
print(ordered)

result = numbers.sort()
print(numbers)
print(result)</code></pre>
<pre><code class="language-text">[4, 1, 3]
[1, 3, 4]
[1, 3, 4]
None</code></pre>
<p><code>numbers = numbers.sort()</code>로 작성하면 정렬된 리스트가 아니라 <code>None</code>이 변수에 저장된다. 원본을 유지해야 하는 경우에는 <code>sorted(numbers)</code>가 의도를 명확하게 드러낸다.</p>
<h2 id="key는-비교할-기준을-만든다">key는 비교할 기준을 만든다</h2>
<p>다음 데이터는 <code>(이름, 점수)</code> 형식이다. 점수 내림차순으로 정렬하고, 점수가 같으면 이름 오름차순으로 정렬한다.</p>
<pre><code class="language-python">students = [(&quot;Min&quot;, 90), (&quot;Ara&quot;, 90), (&quot;Jun&quot;, 80)]
ranked = sorted(students, key=lambda row: (-row[1], row[0]))
print(ranked)
print(students)</code></pre>
<pre><code class="language-text">[(&#39;Ara&#39;, 90), (&#39;Min&#39;, 90), (&#39;Jun&#39;, 80)]
[(&#39;Min&#39;, 90), (&#39;Ara&#39;, 90), (&#39;Jun&#39;, 80)]</code></pre>
<p>비교 키는 차례로 <code>(-90, &#39;Min&#39;)</code>, <code>(-90, &#39;Ara&#39;)</code>, <code>(-80, &#39;Jun&#39;)</code>이다. 튜플의 첫 항목을 먼저 비교하고 같으면 다음 항목을 비교한다. 점수에 음수를 붙여 점수만 내림차순으로 만들었다. 이 방법은 여기처럼 수치형 점수에 적용한다.</p>
<p>전체 키에 <code>reverse=True</code>를 적용하면 이름까지 내림차순이 된다. 여러 기준의 방향이 다를 때는 각 기준을 분리해서 생각해야 한다.</p>
<h2 id="안정-정렬은-동점자의-원래-순서를-보존한다">안정 정렬은 동점자의 원래 순서를 보존한다</h2>
<pre><code class="language-python">students = [(&quot;Min&quot;, 90), (&quot;Ara&quot;, 90), (&quot;Jun&quot;, 80)]
print(sorted(students, key=lambda row: row[1], reverse=True))</code></pre>
<pre><code class="language-text">[(&#39;Min&#39;, 90), (&#39;Ara&#39;, 90), (&#39;Jun&#39;, 80)]</code></pre>
<p>점수만 비교했으므로 같은 90점인 Min과 Ara는 입력 순서를 유지한다. 이것이 안정 정렬이다. 다만 안정 정렬이 이름순까지 보장하는 것은 아니다. 이름순이 요구사항이라면 앞의 예제처럼 두 번째 비교 기준을 명시해야 한다.</p>
<h2 id="새-리스트와-깊은-복사는-다르다">새 리스트와 깊은 복사는 다르다</h2>
<p><code>sorted()</code>가 새 리스트를 만든다고 해서 내부의 가변 객체까지 모두 복제하는 것은 아니다. 이번 예제의 원소는 튜플이지만, 원소가 딕셔너리나 리스트라면 정렬 전후의 리스트가 같은 내부 객체를 참조할 수 있다. 원본의 순서 보존과 내부 데이터의 독립성은 별개의 조건이다.</p>
<h2 id="정리">정리</h2>
<p>정렬 전에 원본 변경 허용 여부, 첫 번째 기준, 동률일 때의 기준을 정한다. <code>sort()</code>와 <code>sorted()</code>의 선택은 첫 번째 조건에, <code>key</code>의 구성은 나머지 두 조건에 대응한다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://docs.python.org/3.14/howto/sorting.html">Python 공식 문서 — 정렬</a></li>
<li><a href="https://velog.io/@8113jsl-ai/python-list-copy-reference">리스트 복사와 참조</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 구현 전에 반환값 정하기 — 개수·존재 여부·위치의 차이]]></title>
            <link>https://velog.io/@8113jsl-ai/til-return-value-contract</link>
            <guid>https://velog.io/@8113jsl-ai/til-return-value-contract</guid>
            <pubDate>Thu, 24 Sep 2026 14:33:42 GMT</pubDate>
            <description><![CDATA[<h2 id="같은-입력이라도-질문이-다르면-반환값이-달라진다">같은 입력이라도 질문이 다르면 반환값이 달라진다</h2>
<p><code>[3, 1, 3]</code>에서 3을 찾는다고 해도 요구사항은 여러 가지다. 있는지만 확인할 수도 있고, 개수나 첫 위치가 필요할 수도 있다. 반복문부터 작성하면 이 차이를 놓치기 쉽다.</p>
<p>이번 정리의 기준은 “무엇을 반환해야 다음 코드가 올바르게 사용할 수 있는가”다.</p>
<table>
<thead>
<tr>
<th>질문</th>
<th>반환값</th>
<th>찾지 못한 경우</th>
</tr>
</thead>
<tbody><tr>
<td>3이 있는가</td>
<td><code>True</code></td>
<td><code>False</code></td>
</tr>
<tr>
<td>3이 몇 개인가</td>
<td><code>2</code></td>
<td><code>0</code></td>
</tr>
<tr>
<td>첫 3은 어디에 있는가</td>
<td><code>0</code></td>
<td><code>None</code></td>
</tr>
<tr>
<td>각 값은 몇 개인가</td>
<td><code>{3: 2, 1: 1}</code></td>
<td>빈 입력이면 <code>{}</code></td>
</tr>
</tbody></table>
<p>첫 위치의 실패 값은 이 글에서 <code>None</code>으로 정했다. 문제에서 <code>-1</code> 등 다른 형식을 지정했다면 그 계약을 따라야 한다.</p>
<h2 id="존재-여부와-개수는-같은-일이-아니다">존재 여부와 개수는 같은 일이 아니다</h2>
<pre><code class="language-python">def contains_value(values, target):
    for value in values:
        if value == target:
            return True
    return False

def count_value(values, target):
    count = 0
    for value in values:
        if value == target:
            count += 1
    return count

print(contains_value([3, 1, 3], 3))
print(count_value([3, 1, 3], 3))</code></pre>
<pre><code class="language-text">True
2</code></pre>
<p>존재 여부는 한 번 발견하면 답이 확정된다. 개수는 뒤에 같은 값이 더 있을 수 있으므로 끝까지 확인해야 한다. 두 함수의 반환 시점이 다른 이유는 문법이 아니라 질문의 차이다.</p>
<h2 id="위치-0을-실패로-오해하지-않기">위치 0을 실패로 오해하지 않기</h2>
<pre><code class="language-python">def first_index(values, target):
    for index, value in enumerate(values):
        if value == target:
            return index
    return None

index = first_index([3, 1, 3], 3)
print(index)
print(index is not None)
print(bool(index))</code></pre>
<pre><code class="language-text">0
True
False</code></pre>
<p>인덱스 0은 정상 결과지만 불리언으로 바꾸면 거짓이다. <code>if index:</code>는 발견 여부와 같은 조건이 아니다. 실패 값을 <code>None</code>으로 정했다면 호출하는 쪽에서도 <code>is not None</code>으로 그 계약을 지킨다.</p>
<h2 id="구현-전에-남길-짧은-메모">구현 전에 남길 짧은 메모</h2>
<ol>
<li>입력의 자료형과 허용 범위를 적는다.</li>
<li>반환값의 자료형과 의미를 한 문장으로 정한다.</li>
<li>빈 입력, 미발견, 여러 번 발견한 경우의 값을 정한다.</li>
<li>원본을 변경해도 되는지 결정한다.</li>
</ol>
<p>예를 들어 <code>first_index</code>의 계약은 “리스트를 변경하지 않고 첫 일치 위치를 반환하며, 일치가 없으면 None을 반환한다”로 적을 수 있다. 이 한 문장이 있어야 테스트의 기대값도 흔들리지 않는다.</p>
<h2 id="sql과-연결해서-생각하기">SQL과 연결해서 생각하기</h2>
<p>주문 여부, 주문 건수, 주문 상세는 서로 다른 질문이다. 각각 EXISTS, 집계, JOIN을 검토하는 것처럼 Python에서도 질문에 맞게 반환 형식을 선택한다. 다만 EXISTS의 실제 실행 방식은 DB 옵티마이저가 결정하므로 Python 반복문과 물리적으로 똑같이 동작한다고 설명하지는 않는다.</p>
<h2 id="다음-문제에-적용할-기준">다음 문제에 적용할 기준</h2>
<p>다음 풀이에서는 구현 전에 정상 입력과 빈 입력의 기대 반환값부터 적는다. 결과가 0인지, False인지, None인지 구분하는 작은 절차가 조기 반환과 실패 처리의 위치를 결정하는 기준이 된다.</p>
<h3 id="함께-보기">함께 보기</h3>
<ul>
<li><a href="https://velog.io/@8113jsl-ai/python-first-match-early-return">첫 번째 일치 위치 찾기 — 반복문과 조기 반환</a></li>
<li><a href="https://docs.python.org/3.14/library/stdtypes.html#truth-value-testing">Python 공식 문서 — 참·거짓 판정</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>