<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>tasker_dev.log</title>
        <link>https://velog.io/</link>
        <description>ML Engineer 🧠 | AI 모델 개발과 최적화 경험을 기록하며 성장하는 개발자 🚀 The light that burns twice as bright burns half as long ✨</description>
        <lastBuildDate>Thu, 17 Sep 2026 03:15:06 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>tasker_dev.log</title>
            <url>https://velog.velcdn.com/images/tasker_dev/profile/b58a301c-c58a-42ba-aedc-4677f3cf189d/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. tasker_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/tasker_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[시스템 구조와 프로그램 실행 2]]></title>
            <link>https://velog.io/@tasker_dev/%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%A1%B0%EC%99%80-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EC%8B%A4%ED%96%89-2</link>
            <guid>https://velog.io/@tasker_dev/%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%A1%B0%EC%99%80-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EC%8B%A4%ED%96%89-2</guid>
            <pubDate>Thu, 17 Sep 2026 03:15:06 GMT</pubDate>
            <description><![CDATA[<h2 id="1-동기식-입출력과-비동기식-입출력">1. 동기식 입출력과 비동기식 입출력</h2>
<p>프로그램이 디스크 읽기를 요청했습니다. 디스크는 CPU보다 수만 배 느립니다. 요청한 프로그램은 그동안 무엇을 해야할까요. 답이 두 가지입니다.</p>
<ul>
<li><strong>동기식 입출력(synchronous I/O)</strong>: 입출력이 끝난 뒤에야 제어가 사용자 프로그램으로 넘어옵니다. 요청한 프로그램은 그동안 아무것도 못 합니다.</li>
<li><strong>비동기식 입출력(asynchronous I/O)</strong>: 요청만 해두고 제어가 즉시 돌아옵니다. 결과가 필요한 시점에 가서 확인합니다.</li>
</ul>
<p>차이는 <strong>완료된 후에 제어가 넘어가는지</strong>, 그 하나뿐입니다. 자주 오해하는 지점이 하나 있는데, 두 경우 모두 입출력 완료를 알리는 것은 인터럽트입니다. 비동기식이라고 CPU가 장치를 계속 들여다보는 것이 아닙니다.</p>
<p>한 가지 덧붙이면, 동기식이라 해서 CPU까지 노는 것은 아닙니다. 요청한 프로그램만 멈출 뿐, 운영체제는 그 사이 다른 프로그램에 CPU를 줍니다. </p>
<h3 id="작은-예제">작은 예제</h3>
<p>디스크 읽기 10ms, 읽기 결과와 무관한 계산 5ms가 있는 프로그램입니다.</p>
<ol>
<li>동기식: 읽기 요청 → 10ms 동안 멈춤 → 완료 인터럽트 → 계산 5ms → <strong>총 15ms</strong></li>
<li>비동기식: 읽기 요청 → 제어 즉시 복귀 → 계산 5ms 먼저 수행 → 10ms 시점에 완료 인터럽트 → <strong>총 10ms</strong></li>
</ol>
<p>읽기와 계산이 서로 독립적일 때 5ms를 겹쳐서 벌었습니다. 반대로 계산이 읽기 결과를 써야 한다면 비동기식으로 짜도 결국 기다려야 하므로 이득이 없습니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>동기식 입출력</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>인터럽트</td>
</tr>
<tr>
<td>유리한 상황</td>
<td>결과가 바로 필요할 때</td>
<td>결과와 무관한 일이 남아 있을 때</td>
</tr>
<tr>
<td>코드 복잡도</td>
<td>낮음</td>
<td>완료 확인 처리가 필요해 높음</td>
</tr>
</tbody></table>
<h2 id="2-memory-mapped-io">2. memory mapped I/O</h2>
<p>CPU는 device controller의 register와 local buffer에 어떻게 접근합니까. 방법이 둘입니다.</p>
<p>하나는 입출력 전용 명령을 따로 두는 것입니다. 다른 하나가 <strong>memory mapped I/O</strong>로, controller의 register와 buffer에 메모리 주소를 할당해 두는 방식입니다. CPU가 보기에는 그냥 메모리 주소이므로 평소 쓰던 메모리 접근 명령을 그대로 씁니다. 특정 주소에 값을 쓰면 그것이 장치에 대한 명령이 됩니다.</p>
<p>주의할 점은 그 주소가 메인 메모리의 어딘가가 아니라는 것입니다. 주소 공간의 일부를 장치에 떼어준 것입니다.</p>
<h2 id="3-저장장치-계층-구조">3. 저장장치 계층 구조</h2>
<p>저장장치를 왜 한 종류로 통일하지 못할까요. 빠른 것은 비싸고 작고, 싼 것은 느리고 크기 때문입니다. 그래서 여러 종류를 쌓아 씁니다.</p>
<p>레지스터 → 캐시 메모리 → 메인 메모리 → 마그네틱 디스크 → 옵티컬 디스크 → 마그네틱 테이프 순으로, 위로 갈수록 빠르고(speed) 단위 용량당 비싸며(cost) 용량이 작습니다.</p>
<p>메인 메모리와 마그네틱 디스크 사이에 중요한 경계가 하나 있습니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>primary (주기억장치)</th>
<th>secondary (보조기억장치)</th>
</tr>
</thead>
<tbody><tr>
<td>해당 장치</td>
<td>레지스터, 캐시 메모리, 메인 메모리</td>
<td>마그네틱 디스크, 옵티컬 디스크, 테이프</td>
</tr>
<tr>
<td>CPU 직접 접근</td>
<td>가능</td>
<td>불가능 (메모리로 올려야 함)</td>
</tr>
<tr>
<td>접근 단위</td>
<td>바이트</td>
<td>섹터 등 블록</td>
</tr>
<tr>
<td>휘발성(volatility)</td>
<td>휘발성</td>
<td>비휘발성</td>
</tr>
</tbody></table>
<p>CPU가 직접 접근할 수 있는지가 경계의 기준입니다.</p>
<h2 id="4-캐싱">4. 캐싱</h2>
<p>계층을 쌓으면 자연스럽게 따라오는 전략이 <strong>캐싱(caching)</strong>입니다. 아래 계층의 데이터 중 자주 쓰는 것을 위쪽 빠른 공간에 복사해 두고 재사용하는 것입니다.</p>
<p>캐싱은 특정 부품의 이름이 아니라 계층 사이에서 반복되는 패턴입니다. 레지스터는 메인 메모리에 대한 캐시이고, 캐시 메모리도 메인 메모리에 대한 캐시이며, 메인 메모리는 디스크에 대한 캐시로 동작합니다. 빠른 공간은 항상 좁으므로 무엇을 남기고 무엇을 내보낼지 정해야 합니다.</p>
<h2 id="5-프로그램이-실행될-때-벌어지는-일">5. 프로그램이 실행될 때 벌어지는 일</h2>
<p>실행 파일은 평소 파일 시스템에 얌전히 있습니다. 실행하는 순간 그 프로그램만의 주소 공간, 즉 <strong>가상 메모리(virtual memory)</strong>가 만들어집니다. 주소 공간은 code, data, stack으로 나뉩니다.</p>
<p>이때 주소 공간 전체를 물리 메모리에 올리지는 않습니다. 당장 필요한 부분만 올리고 나머지는 디스크의 <strong>swap area</strong>에 둡니다. swap area도 디스크지만 파일 시스템과 목적이 다릅니다. 파일 시스템은 전원이 꺼져도 내용을 보존하려는 공간이고, swap area는 메모리가 좁아서 잠시 밀어둔 공간이라 프로그램이 끝나면 의미가 없습니다.</p>
<p>커널도 프로그램이므로 똑같이 주소 공간을 가집니다. 커널 주소 공간 역시 code, data, stack으로 구성되며, 항상 물리 메모리에 올라가 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/tasker_dev/post/bfecc2b6-fdae-435d-8a51-8dc081c9e8ab/image.png" alt=""></p>
<h2 id="6-세-가지-함수">6. 세 가지 함수</h2>
<p>프로그램이 호출하는 함수는 어디에 있느냐에 따라 성격이 다릅니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>위치</th>
<th>호출 방식</th>
</tr>
</thead>
<tbody><tr>
<td>사용자 정의 함수</td>
<td>내 프로세스의 code 영역</td>
<td>같은 주소 공간 안에서 점프</td>
</tr>
<tr>
<td>라이브러리 함수</td>
<td>내 프로세스의 code 영역</td>
<td>같은 주소 공간 안에서 점프</td>
</tr>
<tr>
<td>커널 함수</td>
<td>커널 주소 공간</td>
<td>모드 전환을 거쳐 커널로 진입</td>
</tr>
</tbody></table>
<p>라이브러리 함수는 내가 짜지 않았을 뿐, 실행 파일에 포함되어 내 주소 공간에 들어옵니다. 반면 커널 함수는 남의 주소 공간에 있어 그냥 점프할 수 없습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL 심화 — 중첩 질의, 데이터 변경, 트리거와 주장]]></title>
            <link>https://velog.io/@tasker_dev/SQL-%EC%8B%AC%ED%99%94-%EC%A4%91%EC%B2%A9-%EC%A7%88%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%EA%B2%BD-%ED%8A%B8%EB%A6%AC%EA%B1%B0%EC%99%80-%EC%A3%BC%EC%9E%A5</link>
            <guid>https://velog.io/@tasker_dev/SQL-%EC%8B%AC%ED%99%94-%EC%A4%91%EC%B2%A9-%EC%A7%88%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%EA%B2%BD-%ED%8A%B8%EB%A6%AC%EA%B1%B0%EC%99%80-%EC%A3%BC%EC%9E%A5</guid>
            <pubDate>Thu, 17 Sep 2026 02:22:55 GMT</pubDate>
            <description><![CDATA[<h2 id="1-릴레이션-하나로-끝나지-않는-질문">1. 릴레이션 하나로 끝나지 않는 질문</h2>
<p>2편에서 정의한 테이블에 다음 데이터를 넣고 시작합니다.</p>
<pre><code class="language-sql">INSERT INTO department VALUES
  (&#39;CSE&#39;,&#39;컴퓨터공학&#39;,&#39;공학관&#39;), (&#39;MTH&#39;,&#39;수학&#39;,&#39;자연관&#39;), (&#39;BIZ&#39;,&#39;경영&#39;,&#39;경영관&#39;);
INSERT INTO student VALUES
  (1001,&#39;김민수&#39;,&#39;CSE&#39;,3), (1002,&#39;이서연&#39;,&#39;CSE&#39;,2),
  (1003,&#39;박지훈&#39;,&#39;MTH&#39;,4), (1004,&#39;최유나&#39;,NULL,1);
INSERT INTO course VALUES
  (&#39;C101&#39;,&#39;데이터베이스&#39;,3,&#39;CSE&#39;), (&#39;C102&#39;,&#39;운영체제&#39;,3,&#39;CSE&#39;),
  (&#39;M201&#39;,&#39;선형대수&#39;,2,&#39;MTH&#39;), (&#39;M202&#39;,&#39;해석학&#39;,4,&#39;MTH&#39;);
INSERT INTO enroll VALUES
  (1001,&#39;C101&#39;,&#39;2014-1&#39;,&#39;A&#39;), (1001,&#39;M201&#39;,&#39;2014-1&#39;,&#39;B&#39;),
  (1002,&#39;C101&#39;,&#39;2014-1&#39;,&#39;B&#39;), (1003,&#39;M202&#39;,&#39;2014-1&#39;,&#39;A&#39;);</code></pre>
<p>&quot;C101이나 M201을 들은 학생&quot;, &quot;같은 학과 학생 쌍&quot; 같은 질문은 결과 두 개를 붙이거나 릴레이션을 겹쳐야 답이 나옵니다.</p>
<p>집합 연산(<code>UNION</code>, <code>INTERSECT</code>, <code>EXCEPT</code>)은 합집합 호환성(union compatibility)이 전제입니다. 조인(join)은 두 개 이상의 릴레이션에서 연관된 튜플을 결합하고, 자체 조인(self join)은 한 릴레이션을 별칭 두 개로 나눠 자기 자신과 조인합니다.</p>
<pre><code class="language-sql">-- 합집합: C101 또는 M201 수강자 (결과: 1001, 1002)
SELECT sid FROM enroll WHERE cid = &#39;C101&#39;
UNION
SELECT sid FROM enroll WHERE cid = &#39;M201&#39;;

-- 자체 조인: 같은 학과 학생 쌍 (결과: 김민수-이서연)
SELECT s1.name, s2.name
FROM student s1 JOIN student s2
  ON s1.dept_id = s2.dept_id AND s1.sid &lt; s2.sid;</code></pre>
<p><code>s1.sid &lt; s2.sid</code> 조건이 자기 자신과의 쌍과 (A,B)/(B,A) 중복을 동시에 걸러냅니다. 관계대수로는 ρ로 릴레이션 이름을 바꿔 세타 조인한 것과 같습니다.</p>
<h2 id="2-중첩-질의">2. 중첩 질의</h2>
<p>조인만으로 &quot;학과 평균 학점보다 큰 과목&quot;을 쓰려면 중간 결과를 먼저 만들어야 합니다. 중첩 질의(nested query)는 외부 질의의 <code>WHERE</code> 절 안에 <code>SELECT-FROM-WHERE</code>를 다시 넣어 이를 해결합니다.</p>
<table>
<thead>
<tr>
<th>연산자</th>
<th>의미</th>
<th>부질의 결과가 비면</th>
</tr>
</thead>
<tbody><tr>
<td><code>IN</code></td>
<td>결과 집합에 포함되는가</td>
<td>거짓</td>
</tr>
<tr>
<td><code>&gt; ANY</code></td>
<td>하나라도 그보다 큰가</td>
<td>거짓</td>
</tr>
<tr>
<td><code>&gt;= ALL</code></td>
<td>전부 그 이하인가</td>
<td>참</td>
</tr>
<tr>
<td><code>EXISTS</code></td>
<td>튜플이 하나라도 있는가</td>
<td>거짓</td>
</tr>
</tbody></table>
<pre><code class="language-sql">-- ALL: 학점이 가장 큰 과목 (결과: 해석학)
SELECT title FROM course
WHERE credit &gt;= ALL (SELECT credit FROM course);</code></pre>
<p>상관 중첩 질의(correlated nested query)는 부질의가 외부 질의의 애트리뷰트를 참조하는 형태입니다. 개념적으로 외부 튜플마다 부질의가 다시 평가됩니다.</p>
<pre><code class="language-sql">-- 자기 학과 평균보다 학점이 큰 과목 (결과: 해석학)
SELECT c.title FROM course c
WHERE c.credit &gt; (SELECT AVG(c2.credit) FROM course c2
                  WHERE c2.dept_id = c.dept_id);

-- NOT EXISTS: 소속 학생이 없는 학과 (결과: 경영)
SELECT d.dept_name FROM department d
WHERE NOT EXISTS (SELECT 1 FROM student s WHERE s.dept_id = d.dept_id);</code></pre>
<p>마지막 질의는 관계대수의 π<sub>dept_id</sub>(department) − π<sub>dept_id</sub>(student)에 해당합니다.</p>
<h2 id="3-insert-delete-update">3. INSERT, DELETE, UPDATE</h2>
<p>조회만 되면 데이터베이스는 정적 파일과 다르지 않습니다. 변경 대상을 고를 때도 앞의 부질의를 그대로 씁니다.</p>
<pre><code class="language-sql">UPDATE student SET year = year + 1
WHERE sid IN (SELECT sid FROM enroll WHERE cid = &#39;C101&#39;);

DELETE FROM enroll
WHERE sid IN (SELECT sid FROM student WHERE dept_id = &#39;MTH&#39;);</code></pre>
<p>다른 테이블 값으로 갱신할 때 SQL Server와 PostgreSQL 모두 <code>UPDATE ... FROM</code> 확장을 제공하지만 문법이 서로 다르고 표준도 아닙니다. 이식성이 필요하면 위처럼 부질의로 씁니다.</p>
<h2 id="4-트리거와-eca-규칙">4. 트리거와 ECA 규칙</h2>
<p>&quot;한 학기 18학점 초과 불가&quot; 같은 규칙을 애플리케이션에만 두면 배치나 관리 도구로 들어온 <code>INSERT</code>는 그냥 통과합니다. 트리거(trigger)는 명시된 이벤트가 발생할 때마다 DBMS가 자동으로 수행하는, 사용자가 정의한 문입니다. 이벤트를 받아 조건을 검사하고 동작을 수행하므로 ECA 규칙(Event-Condition-Action rule)이라고도 부릅니다.</p>
<p><img src="https://velog.velcdn.com/images/tasker_dev/post/3f6eb68d-b9f8-4b55-a236-effb221c487d/image.png" alt=""></p>
<pre><code class="language-sql">-- SQL Server: 변경된 행은 inserted / deleted 가상 테이블로 받습니다
CREATE TRIGGER trg_credit_limit ON enroll
AFTER INSERT, UPDATE
AS
BEGIN
  IF EXISTS (
    SELECT 1 FROM (SELECT DISTINCT sid, semester FROM inserted) i
    WHERE (SELECT SUM(c.credit) FROM enroll e JOIN course c ON e.cid = c.cid
           WHERE e.sid = i.sid AND e.semester = i.semester) &gt; 18)
  BEGIN
    ROLLBACK TRANSACTION;
    THROW 50001, N&#39;학기당 18학점 초과&#39;, 1;
  END
END;</code></pre>
<p>PostgreSQL은 <code>RETURNS trigger</code> 함수를 먼저 만들고 <code>FOR EACH ROW</code>로 연결하며, 변경 행은 <code>NEW</code>/<code>OLD</code>로 참조합니다. SQL Server는 문장 단위로만 동작하고 <code>BEFORE</code> 시점이 없다는 점이 큰 차이입니다.</p>
<p>트리거의 동작이 다른 테이블을 바꿔 또 다른 트리거를 깨우는 연쇄 트리거도 가능합니다. SQL Server는 중첩을 32단계까지 허용하고, 그 안에서 무한 루프가 생기면 한도를 넘는 순간 트랜잭션 전체가 취소됩니다.</p>
<h2 id="5-주장assertion">5. 주장(assertion)</h2>
<p><code>CHECK</code>는 한 행 안에서 닫히는 조건에 적합합니다. 위의 학점 규칙은 enroll과 course 두 릴레이션에 걸쳐 있어 <code>CHECK</code>로 쓰기 어렵습니다. 주장(assertion)은 데이터베이스가 항상 만족해야 하는 조건을 특정 테이블에 매달지 않고 선언합니다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>범위</th>
<th>성격</th>
<th>지원</th>
</tr>
</thead>
<tbody><tr>
<td><code>CHECK</code></td>
<td>한 행</td>
<td>선언적</td>
<td>대부분 지원</td>
</tr>
<tr>
<td>주장</td>
<td>여러 릴레이션</td>
<td>선언적</td>
<td>표준에만 존재</td>
</tr>
<tr>
<td>트리거</td>
<td>제한 없음</td>
<td>절차적</td>
<td>대부분 지원</td>
</tr>
</tbody></table>
<p>읽기는 쉽지만 어떤 변경이 조건을 깨는지 DBMS가 스스로 판단해야 해서 검사 비용이 큽니다. PostgreSQL은 표준 기능 F521(Assertions)을 미지원으로 명시하고 <code>CREATE ASSERTION</code>에 미구현 오류를 냅니다. SQL Server에도 없습니다.</p>
<h2 id="6-내포된-sql">6. 내포된 SQL</h2>
<p>SQL은 집합 단위로 동작할 뿐 조건 분기나 반복, 출력은 표현하지 못합니다. 내포된 SQL(embedded SQL)은 C 같은 호스트 언어(host language) 코드 안에 SQL을 넣는 방식으로, <code>EXEC SQL</code> 문장을 전처리기가 라이브러리 호출로 바꿉니다. 호스트 변수는 콜론을 붙여 전달하고, 여러 행이 나오는 질의는 커서(cursor)로 한 행씩 가져옵니다.</p>
<pre><code class="language-c">EXEC SQL DECLARE cur CURSOR FOR
  SELECT sid, name FROM student WHERE dept_id = &#39;CSE&#39;;
EXEC SQL OPEN cur;
EXEC SQL FETCH NEXT FROM cur INTO :v_sid, :v_name;
EXEC SQL CLOSE cur;</code></pre>
<p>지금은 JDBC, psycopg 같은 드라이버와 ORM이 같은 자리를 차지합니다. &quot;값은 변수로 바인딩하고 결과 집합은 한 행씩 소비한다&quot;는 구조는 그대로입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL 개요]]></title>
            <link>https://velog.io/@tasker_dev/SQL-%EA%B0%9C%EC%9A%94</link>
            <guid>https://velog.io/@tasker_dev/SQL-%EA%B0%9C%EC%9A%94</guid>
            <pubDate>Tue, 01 Sep 2026 10:18:25 GMT</pubDate>
            <description><![CDATA[<h2 id="1-관계-대수만-있으면-무엇이-문제인가">1. 관계 대수만 있으면 무엇이 문제인가</h2>
<p>앞 편에서 본 관계 대수는 절차적입니다. 무엇을 어떤 순서로 계산할지 사람이 직접 적어야 하고, 산술 연산도 정렬도 갱신도 안 됩니다. 실제 데이터베이스를 쓰려면 그것들이 전부 필요합니다.</p>
<p>SQL은 1974년 IBM 산호세 연구소에서 System R이라는 관계 DBMS 시제품을 만들며 나왔습니다. 챔벌린(Chamberlin)과 보이스(Boyce)가 발표한 SEQUEL(Structured English Query Language)이 원형이고, 상표 문제로 이름이 SQL로 줄었습니다. 관계 대수와 관계 해석을 기반으로 집단 함수, 그룹화, 갱신 연산을 얹은 결과물입니다. 그래서 SQL은 비절차적(선언적) 언어이면서 자연어에 가까운 구문을 갖습니다.</p>
<p>구성요소는 셋입니다.</p>
<ul>
<li>데이터 정의어(DDL, Data Definition Language): <code>CREATE</code>, <code>ALTER</code>, <code>DROP</code></li>
<li>데이터 조작어(DML, Data Manipulation Language): <code>SELECT</code>, <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code></li>
<li>데이터 제어어(DCL, Data Control Language): <code>GRANT</code>, <code>REVOKE</code></li>
</ul>
<h2 id="2-테이블-정의와-데이터-타입">2. 테이블 정의와 데이터 타입</h2>
<pre><code class="language-sql">CREATE TABLE department (
  dept_id   CHAR(3)     NOT NULL,
  dept_name VARCHAR(30) NOT NULL,
  building  VARCHAR(20),
  CONSTRAINT pk_department PRIMARY KEY (dept_id)
);</code></pre>
<table>
<thead>
<tr>
<th>구분</th>
<th>CHAR(n)</th>
<th>VARCHAR(n)</th>
</tr>
</thead>
<tbody><tr>
<td>저장 길이</td>
<td>항상 n바이트 고정</td>
<td>실제 길이 + 길이 정보</td>
</tr>
<tr>
<td>남는 자리</td>
<td>공백으로 채움</td>
<td>채우지 않음</td>
</tr>
<tr>
<td>유리한 경우</td>
<td>길이가 거의 일정 (<code>dept_id</code>, 국가 코드)</td>
<td>길이 편차가 큼 (이름, 주소)</td>
</tr>
<tr>
<td>주의</td>
<td>비교 시 뒤쪽 공백 처리가 DBMS마다 다름</td>
<td>길이 정보만큼 오버헤드</td>
</tr>
</tbody></table>
<p>한글은 SQL Server에서 오래도록 <code>NCHAR</code>/<code>NVARCHAR</code>를 써야 했습니다. 2019부터 UTF-8 콜레이션이 생겨 <code>VARCHAR</code>로도 다룰 수 있습니다. PostgreSQL은 <code>VARCHAR</code>와 <code>TEXT</code> 사이에 성능 차이가 없어 길이 제한이 업무 규칙일 때만 <code>n</code>을 붙입니다.</p>
<p>조회 성능을 위한 인덱스는 <code>CREATE INDEX</code>로 만들지만, 어떤 열에 어떻게 걸지는 6편에서 다룹니다.</p>
<h2 id="3-무결성-제약조건">3. 무결성 제약조건</h2>
<p>제약조건이 없으면 존재하지 않는 학과 코드를 가진 학생 행이 조용히 쌓입니다. 애플리케이션 코드가 아무리 검증해도 배치 작업 하나가 우회하면 끝입니다. 참조 무결성(referential integrity)은 외래키 값이 반드시 부모 릴레이션에 존재하는 값이거나 NULL이어야 한다는 제약입니다.</p>
<pre><code class="language-sql">CREATE TABLE student (
  sid     INT         NOT NULL,
  name    VARCHAR(20) NOT NULL,
  dept_id CHAR(3),
  year    INT,
  CONSTRAINT pk_student PRIMARY KEY (sid),
  CONSTRAINT fk_student_dept FOREIGN KEY (dept_id)
    REFERENCES department(dept_id)
    ON DELETE SET NULL ON UPDATE CASCADE,
  CONSTRAINT ck_student_year CHECK (year BETWEEN 1 AND 4)
);</code></pre>
<p>부모 행이 삭제되거나 키가 바뀔 때의 동작은 네 가지입니다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>자식 행에 일어나는 일</th>
<th>조건</th>
</tr>
</thead>
<tbody><tr>
<td><code>NO ACTION</code></td>
<td>위반이면 연산을 거부</td>
<td>기본값</td>
</tr>
<tr>
<td><code>CASCADE</code></td>
<td>함께 삭제 또는 함께 변경</td>
<td>연쇄 삭제 범위 확인 필요</td>
</tr>
<tr>
<td><code>SET NULL</code></td>
<td>외래키를 NULL로</td>
<td>그 열이 NULL 허용이어야 함</td>
</tr>
<tr>
<td><code>SET DEFAULT</code></td>
<td>외래키를 기본값으로</td>
<td>기본값이 부모에 존재해야 함</td>
</tr>
</tbody></table>
<p>SQL Server는 이 네 가지를 지원하고, PostgreSQL은 <code>RESTRICT</code>를 더해 다섯 가지입니다. <code>NO ACTION</code>은 검사를 트랜잭션 끝으로 미룰 수 있고 <code>RESTRICT</code>는 즉시 막는다는 차이입니다.</p>
<p>제약조건에 <code>pk_</code>, <code>fk_</code>, <code>ck_</code> 같은 이름을 직접 붙이면 위배 시 오류 메시지에 그 이름이 찍힙니다. 이름을 생략하면 <code>FK__student__dept___1234ABCD</code> 같은 자동 생성 이름이 나와 어느 조건인지 찾는 데 시간이 걸립니다.</p>
<h2 id="4-select의-뼈대">4. SELECT의 뼈대</h2>
<p><code>SELECT</code>와 <code>FROM</code>만 필수이고 나머지는 선택입니다. 관계 대수와 이렇게 대응합니다.</p>
<pre><code class="language-sql">-- π_{name, year}(σ_{dept_id=&#39;CSE&#39;}(student))
SELECT s.name AS 학생명, s.year        -- 프로젝션, 별칭
FROM   student s                       -- 카티션 곱 / 조인
WHERE  s.dept_id = &#39;CSE&#39;;              -- 셀렉션</code></pre>
<p><code>*</code>는 모든 애트리뷰트를 뜻하는 와일드카드이고, <code>DISTINCT</code>는 중복을 제거해 관계 대수의 프로젝션과 같은 결과를 만듭니다. 별칭(alias)은 조인에서 같은 이름의 애트리뷰트를 구분할 때 필수입니다.</p>
<p><code>WHERE</code> 절 연산자의 우선순위는 산술 → 비교 → <code>NOT</code> → <code>AND</code> → <code>OR</code>입니다. <code>AND</code>가 <code>OR</code>보다 먼저 묶이므로, 섞어 쓸 때는 괄호를 넣는 편이 안전합니다.</p>
<pre><code class="language-sql">SELECT * FROM student WHERE year BETWEEN 2 AND 3;      -- 범위
SELECT * FROM student WHERE dept_id IN (&#39;CSE&#39;,&#39;MTH&#39;);  -- 리스트
SELECT * FROM student WHERE name LIKE &#39;김%&#39;;           -- 패턴
SELECT sid, credit * 1.0 AS point FROM course;         -- 산술 연산</code></pre>
<p><code>LIKE</code>에서 <code>%</code>는 0자 이상, <code>_</code>는 정확히 한 글자입니다.</p>
<h2 id="5-null은-값이-아니라-unknown">5. NULL은 값이 아니라 Unknown</h2>
<p>NULL은 &quot;모른다&quot;입니다. 값이 아니므로 <code>= NULL</code>은 참도 거짓도 아닌 unknown이 되고, 그 행은 결과에서 빠집니다. 반드시 <code>IS NULL</code> / <code>IS NOT NULL</code>을 씁니다.</p>
<p>집단 함수는 <code>COUNT(*)</code>를 제외하면 NULL을 무시합니다. 평균을 낼 때 NULL인 행이 분모에서 빠진다는 뜻이라, &quot;전체 평균&quot;을 기대하면 값이 어긋납니다.</p>
<p><code>ORDER BY</code>의 NULL 위치도 갈립니다. SQL Server는 NULL을 가장 작은 값으로 봐서 오름차순에서 앞에 두고, PostgreSQL은 기본이 뒤쪽이며 <code>NULLS FIRST</code>로 바꿀 수 있습니다.</p>
<h2 id="6-집계와-그룹화">6. 집계와 그룹화</h2>
<p>집단 함수는 <code>COUNT</code>, <code>SUM</code>, <code>AVG</code>, <code>MAX</code>, <code>MIN</code> 다섯 가지입니다. <code>GROUP BY</code>는 지정한 애트리뷰트 값이 같은 튜플들을 한 그룹으로 묶고, 집단 함수는 그룹마다 한 번씩 계산됩니다.</p>
<pre><code class="language-sql">SELECT dept_id, COUNT(*) AS cnt, AVG(year) AS avg_year
FROM   student
GROUP  BY dept_id
ORDER  BY cnt DESC;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[계층 구조와 HTTP]]></title>
            <link>https://velog.io/@tasker_dev/%EA%B3%84%EC%B8%B5-%EA%B5%AC%EC%A1%B0%EC%99%80-HTTP</link>
            <guid>https://velog.io/@tasker_dev/%EA%B3%84%EC%B8%B5-%EA%B5%AC%EC%A1%B0%EC%99%80-HTTP</guid>
            <pubDate>Tue, 01 Sep 2026 08:39:15 GMT</pubDate>
            <description><![CDATA[<h2 id="1-계층-구조-위에서부터-내려가는-지도">1. 계층 구조: 위에서부터 내려가는 지도</h2>
<p>인터넷의 계층은 다섯 개입니다.</p>
<table>
<thead>
<tr>
<th>계층</th>
<th>무엇을 책임지나</th>
<th>주소 단위</th>
</tr>
</thead>
<tbody><tr>
<td>application(응용)</td>
<td>메시지의 의미와 형식. 무엇을 주고받을지</td>
<td>-</td>
</tr>
<tr>
<td>transport(전송)</td>
<td>프로세스 대 프로세스 전달</td>
<td>port</td>
</tr>
<tr>
<td>network(네트워크)</td>
<td>호스트 대 호스트 전달, 경로 결정</td>
<td>IP address</td>
</tr>
<tr>
<td>link(링크)</td>
<td>인접한 노드 사이 한 구간 전달</td>
<td>-</td>
</tr>
<tr>
<td>physical(물리)</td>
<td>비트를 실제 신호로</td>
<td>-</td>
</tr>
</tbody></table>
<p>각 층은 아래 층에게 &quot;알아서 배달해 달라&quot;고 맡기고, 자기 층에서 필요한 정보를 header(헤더)에 적어 붙입니다. 그래서 데이터는 내려갈수록 헤더가 겹겹이 쌓이고, 받는 쪽에서는 반대로 한 겹씩 벗겨집니다. 이것이 encapsulation(캡슐화)입니다.</p>
<p><img src="https://velog.velcdn.com/images/tasker_dev/post/d2d4e596-1f7b-4a6f-b02e-1f627881d2be/image.png" alt=""></p>
<p>중요한 것은 중간의 router는 이 전부를 열어보지 않는다는 점입니다. 라우터는 network 계층까지만 봅니다. 그 위, 즉 transport와 application은 양 끝 호스트에만 존재합니다. 지난 편의 dumb core가 계층 그림으로 그려지면 이렇게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/tasker_dev/post/fed7cc02-7e76-4f20-aafe-6de8c273feb7/image.png" alt=""></p>
<h2 id="2-transport가-해주는-것은-data-integrity-하나뿐">2. transport가 해주는 것은 data integrity 하나뿐</h2>
<p>top-down의 규칙대로, 애플리케이션 계층에 들어가기 전에 아래가 무엇을 안 해주는지부터 확인합니다. 애플리케이션이 네트워크에 요구할 수 있는 것은 대략 네 가지입니다.</p>
<ul>
<li>data integrity(데이터 무결성): 보낸 것이 그대로 도착하는가</li>
<li>timing(지연): 정해진 시간 안에 도착하는가</li>
<li>throughput(처리율): 최소 대역폭이 보장되는가</li>
<li>security(보안): 중간에서 못 읽는가</li>
</ul>
<p>transport 계층이 제공하는 것은 사실상 data integrity 하나입니다. TCP를 써도 timing과 throughput은 보장되지 않고, 기본 TCP에는 암호화도 없습니다.</p>
<p>그래서 나머지 셋은 필요하면 애플리케이션이 알아서 해결해야 합니다. 화상 통화가 지연을 감수하고 UDP를 고르는 것도, HTTPS가 TLS를 얹는 것도 전부 &quot;아래가 안 해주니까 위에서 한다&quot;는 같은 이야기입니다. 보안은 13편에서 다룹니다.</p>
<h2 id="3-ip-address와-port-그리고-socket">3. IP address와 port, 그리고 socket</h2>
<p>network 계층은 IP address로 호스트까지만 배달합니다. 하지만 한 호스트에는 브라우저, 서버 프로세스, 백그라운드 데몬이 동시에 떠 있습니다. 그래서 &quot;어느 프로세스인가&quot;를 가리키는 번호가 필요하고, 그것이 port(포트)입니다.</p>
<ul>
<li>IP address: 어느 기계인가</li>
<li>port: 그 기계의 어느 프로세스인가</li>
</ul>
<p>socket(소켓)은 이 둘을 묶어 애플리케이션이 잡을 수 있게 만든 문입니다. 프로세스 입장에서 네트워크는 소켓이라는 문 하나로 축약됩니다. 문 밖에서 벌어지는 라우팅과 재전송은 몰라도 되고, 그저 문에 데이터를 밀어 넣고 꺼내면 됩니다. 그래서 소켓은 애플리케이션 계층과 transport 계층 사이의 인터페이스입니다. 소켓 프로그래밍의 구체적인 API는 다음 편에서 다룹니다.</p>
<h2 id="4-client와-server-누가-고정-주소를-갖는가">4. client와 server: 누가 고정 주소를 갖는가</h2>
<p>client-server architecture(클라이언트-서버 구조)에서 서버는 permanent IP address(고정 IP 주소)를 갖습니다. 항상 같은 자리에 있어야 클라이언트가 찾아올 수 있기 때문입니다. 반대로 클라이언트는 주소가 바뀌어도 상관없고, 실제로 자주 바뀝니다. 카페 와이파이에 붙을 때마다 주소가 달라져도 웹서핑에 아무 문제가 없는 이유입니다.</p>
<p>비대칭은 여기서 끝나지 않습니다. 클라이언트끼리는 직접 통신하지 않고, 항상 서버를 거칩니다.</p>
<h2 id="5-http-request와-response">5. HTTP: request와 response</h2>
<p>HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)는 웹의 애플리케이션 계층 프로토콜입니다. 구조는 지극히 단순합니다. 클라이언트가 request message(요청 메시지)를 보내면 서버가 response message(응답 메시지)를 돌려줍니다. 그게 전부입니다.</p>
<p>HTTP는 transport로 TCP를 씁니다. 즉 HTTP 자신은 데이터가 유실됐는지 순서가 뒤바뀌었는지 신경 쓰지 않습니다. 그건 아래가 해주기로 한 일이고, HTTP는 &quot;무엇을 달라고 말할 것인가&quot;에만 집중합니다. 계층을 나눈 이득이 그대로 드러나는 지점입니다.</p>
<h2 id="6-stateless라는-선택">6. stateless라는 선택</h2>
<p>HTTP는 stateless(무상태) 프로토콜입니다. 서버는 이전 요청을 기억하지 않습니다. 같은 사람이 열 번 요청해도 서버 입장에서는 매번 처음 보는 요청입니다.</p>
<p>불편해 보이지만 의도된 설계입니다. 상태를 들고 있으면 서버는 클라이언트마다 기록을 유지해야 하고, 그 기록이 꼬이면 복구가 어렵고, 서버를 여러 대로 늘리기도 까다로워집니다. 상태를 버린 대가로 HTTP 서버는 가볍고 잘 늘어납니다. 로그인 상태 같은 것은 쿠키나 토큰으로 애플리케이션이 따로 얹습니다. 여기서도 같은 패턴이 반복됩니다. 프로토콜이 안 해주면 위에서 한다.</p>
<h2 id="7-non-persistent-vs-persistent-http">7. non-persistent vs persistent HTTP</h2>
<p>TCP 연결을 객체마다 새로 맺느냐, 한 번 맺고 재사용하느냐의 차이입니다.</p>
<p><img src="https://velog.velcdn.com/images/tasker_dev/post/4024ae7a-a151-4939-b291-8bc7c5e5dc0e/image.png" alt=""></p>
<p>RTT(Round Trip Time, 왕복 시간)를 기준으로 세면 차이가 분명합니다. non-persistent는 객체 하나당 최소 2 RTT가 듭니다. 첫 RTT는 TCP 연결을 세우는 데, 둘째 RTT는 요청을 보내고 응답을 받는 데 쓰입니다. persistent는 연결을 한 번만 세워두므로 이후 객체는 1 RTT씩만 더 듭니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>non-persistent</th>
<th>persistent</th>
</tr>
</thead>
<tbody><tr>
<td>연결</td>
<td>객체마다 새로 맺고 끊음</td>
<td>한 번 맺어 재사용</td>
</tr>
<tr>
<td>비용</td>
<td>객체당 2 RTT</td>
<td>첫 연결 후 객체당 1 RTT</td>
</tr>
<tr>
<td>서버 자원</td>
<td>연결 생성·종료 반복 부담</td>
<td>유휴 연결을 붙들고 있음</td>
</tr>
<tr>
<td>기본값</td>
<td>HTTP/1.0</td>
<td>HTTP/1.1 이후</td>
</tr>
</tbody></table>
<p>이미지 10개짜리 페이지라면 차이는 20 RTT 대 11 RTT입니다. 그래서 persistent connection은 HTTP/1.1에서 기본값이 되었습니다. 이후로는 한 연결 위에서 여러 요청을 동시에 흘려보내는 방향으로 더 나아갔습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[시스템 구조와 프로그램 실행]]></title>
            <link>https://velog.io/@tasker_dev/%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%A1%B0%EC%99%80-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EC%8B%A4%ED%96%89</link>
            <guid>https://velog.io/@tasker_dev/%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%A1%B0%EC%99%80-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EC%8B%A4%ED%96%89</guid>
            <pubDate>Tue, 01 Sep 2026 07:17:38 GMT</pubDate>
            <description><![CDATA[<h2 id="1-컴퓨터-시스템은-세-덩어리입니다">1. 컴퓨터 시스템은 세 덩어리입니다</h2>
<p>컴퓨터는 CPU, 메모리(memory), 입출력 장치(I/O device) 세 덩어리로 나뉩니다.</p>
<p>여기서 중요한 전제가 하나 있습니다. <strong>메모리는 CPU의 작업 공간</strong>이라는 것입니다. CPU는 디스크에 있는 데이터를 직접 읽지 못합니다. 반드시 메모리에 올라온 것만 처리합니다. 프로그램을 실행한다는 말은 실행 파일을 메모리에 올리고, CPU가 메모리에서 명령을 하나씩 읽어 실행한다는 뜻입니다.</p>
<p><img src="https://velog.velcdn.com/images/tasker_dev/post/25a052f7-4a94-4646-b998-55465824a8c5/image.png" alt=""></p>
<p>나머지 부품은 전부 이 그림에서 생기는 문제를 하나씩 막기 위해 존재합니다.</p>
<h2 id="2-mode-bit---사용자-프로그램을-믿지-않기-위해">2. mode bit - 사용자 프로그램을 믿지 않기 위해</h2>
<p>mode bit이 없다고 해봅니다. 그러면 사용자 프로그램이 디스크에 직접 명령을 내려 남의 파일을 지울 수 있고, 다른 프로그램이 쓰는 메모리 영역을 마음대로 덮어쓸 수 있습니다. 프로그램 하나가 시스템 전체를 망가뜨립니다.</p>
<p>그래서 CPU 안에 비트 하나를 둡니다. 이것이 mode bit입니다.</p>
<ul>
<li><strong>1: 사용자 모드(user mode)</strong> - 사용자 프로그램 실행 중. 제한된 명령만 가능</li>
<li><strong>0: 모니터 모드(monitor mode)</strong> - 운영체제 코드 실행 중. 모든 명령 가능</li>
</ul>
<p>모니터 모드는 커널 모드(kernel mode), 시스템 모드(system mode)라고도 부릅니다. 입출력 명령처럼 mode bit이 0일 때만 실행할 수 있는 명령을 특권 명령(privileged instruction)이라고 합니다.</p>
<p>전환 규칙은 단순합니다. 인터럽트나 예외가 발생하면 하드웨어가 mode bit을 0으로 바꾸고 운영체제에 CPU를 넘깁니다. 운영체제가 사용자 프로그램에 CPU를 넘길 때는 1로 바꿔 놓고 넘깁니다. mode bit을 바꾸는 것 자체가 특권 명령이므로, 사용자 프로그램이 스스로 0으로 만들 수는 없습니다.</p>
<h2 id="3-timer---cpu를-영원히-뺏기지-않기-위해">3. timer - CPU를 영원히 뺏기지 않기 위해</h2>
<p>mode bit으로 권한은 막았지만 아직 문제가 남습니다. 사용자 프로그램이 <code>while(1);</code> 을 실행하면 CPU를 영원히 붙잡고 놓지 않습니다. 운영체제도 결국 프로그램이라 CPU를 잡고 있어야 일을 하는데, CPU를 뺏겼으니 개입할 방법이 없습니다.</p>
<p>timer는 이 상황을 막습니다. 정해진 시간이 지나면 timer가 인터럽트를 걸고, CPU는 강제로 운영체제에 넘어갑니다. 그때 운영체제가 다른 프로그램에 CPU를 줄지 결정합니다. 타이머 값을 설정하는 것도 특권 명령입니다.</p>
<p>정리하면 timer는 특정 프로그램의 CPU 독점을 막는 장치이며, 시분할(time sharing) 구현의 기반입니다. 실제로 어떤 기준으로 다음 프로그램을 고르는지는 CPU 스케줄링 편에서 다룹니다.</p>
<h2 id="4-device-controller와-local-buffer">4. device controller와 local buffer</h2>
<p>입출력 장치는 CPU보다 훨씬 느립니다. CPU가 디스크의 회전을 기다리며 서 있으면 그만큼 손해입니다.</p>
<p>그래서 장치마다 <strong>device controller</strong>를 붙입니다. 해당 장치를 관리하는 작은 개별 CPU라고 보면 됩니다. 여기에 두 종류의 저장 공간이 붙습니다.</p>
<ul>
<li><strong>register</strong>: 제어 정보와 상태를 담는 작은 공간</li>
<li><strong>local buffer</strong>: 실제 데이터가 오가는 임시 저장 공간</li>
</ul>
<p>CPU가 &quot;이 블록을 읽어라&quot;라고 지시하면 controller가 데이터를 읽어 local buffer에 채웁니다. 그동안 CPU는 다른 일을 합니다. 다 채워지면 controller가 인터럽트로 CPU에 알립니다.</p>
<p>이름이 비슷한 device driver와 혼동하기 쉽습니다. device driver는 운영체제 안의 소프트웨어이고, device controller는 장치에 붙은 하드웨어입니다.</p>
<h2 id="5-interrupt-line---cpu에게-말을-거는-통로">5. interrupt line - CPU에게 말을 거는 통로</h2>
<p>CPU는 명령 하나를 끝낼 때마다 interrupt line을 확인합니다. 신호가 들어와 있으면 하던 일을 멈추고 운영체제의 인터럽트 처리 루틴으로 넘어갑니다. 즉 CPU는 평소에 장치를 살피지 않고, 장치 쪽에서 필요할 때 신호를 올립니다.</p>
<p>인터럽트는 두 가지로 나뉩니다.</p>
<ul>
<li><strong>하드웨어 인터럽트</strong>: controller나 timer가 거는 것</li>
<li><strong>소프트웨어 인터럽트(trap)</strong>: 실행 중인 프로그램에서 발생하는 예외 상황이나 요청</li>
</ul>
<p>어느 쪽이든 CPU가 운영체제로 넘어간다는 결과는 같습니다. 여기서 무엇을 저장하고 어떻게 복귀하는지는 프로세스 편에서 다룹니다.</p>
<h2 id="6-dma-controller---인터럽트가-너무-잦아서">6. DMA controller - 인터럽트가 너무 잦아서</h2>
<p>인터럽트로 문제가 다 풀린 것 같지만 새 문제가 생깁니다. controller가 데이터를 바이트 단위로 옮기면서 매번 인터럽트를 걸면 CPU가 인터럽트 처리만 하다 끝납니다.</p>
<p>작은 예제로 확인해 봅니다. 4KB 블록 하나를 디스크에서 메모리로 읽어옵니다.</p>
<ol>
<li>DMA가 없을 때: controller가 1바이트씩 local buffer에 채우고, CPU가 그것을 메모리로 옮깁니다. 4096바이트이므로 <strong>인터럽트 4096번</strong>이 발생합니다.</li>
<li>DMA가 있을 때: CPU는 &quot;이 데이터를 메모리 이 주소에 넣어라&quot;라고 한 번 지시하고 빠집니다. DMA controller가 메모리에 직접 접근해 4096바이트를 채웁니다.</li>
<li>블록을 다 옮긴 뒤 DMA controller가 <strong>인터럽트 1번</strong>만 겁니다.</li>
</ol>
<p>CPU 개입이 4096번에서 1번으로 줄었습니다. 이것이 DMA(Direct Memory Access)의 목적입니다.</p>
<p>다만 이제 메모리에 접근하려는 주체가 CPU와 DMA controller 둘이 되었습니다. 동시에 접근하면 충돌하므로 <strong>memory controller</strong>가 순서를 정리합니다. 메모리 접근 요청을 받아 중재하고 순서대로 처리하는 역할입니다.</p>
<h2 id="7-부품별-정리표">7. 부품별 정리표</h2>
<table>
<thead>
<tr>
<th>부품</th>
<th>없으면 생기는 문제</th>
<th>하는 일</th>
<th>특권 여부</th>
</tr>
</thead>
<tbody><tr>
<td>mode bit</td>
<td>사용자 프로그램이 하드웨어를 직접 조작</td>
<td>사용자 모드(1) / 모니터 모드(0) 구분</td>
<td>변경이 특권 명령</td>
</tr>
<tr>
<td>timer</td>
<td>한 프로그램이 CPU 독점</td>
<td>일정 시간 후 인터럽트 발생</td>
<td>설정이 특권 명령</td>
</tr>
<tr>
<td>device controller</td>
<td>CPU가 느린 장치를 직접 기다림</td>
<td>장치 하나를 전담 관리</td>
<td>하드웨어</td>
</tr>
<tr>
<td>register</td>
<td>제어 정보를 둘 곳이 없음</td>
<td>명령과 상태 저장</td>
<td>하드웨어</td>
</tr>
<tr>
<td>local buffer</td>
<td>데이터를 모아둘 곳이 없음</td>
<td>장치 데이터 임시 저장</td>
<td>하드웨어</td>
</tr>
<tr>
<td>interrupt line</td>
<td>장치가 CPU에 알릴 방법이 없음</td>
<td>완료 신호 전달</td>
<td>하드웨어</td>
</tr>
<tr>
<td>DMA controller</td>
<td>바이트 단위 인터럽트 폭주</td>
<td>메모리 직접 접근, 블록당 인터럽트 1회</td>
<td>하드웨어</td>
</tr>
<tr>
<td>memory controller</td>
<td>CPU와 DMA의 메모리 접근 충돌</td>
<td>접근 순서 중재</td>
<td>하드웨어</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[복잡도: 환경에 흔들리지 않는 성능 잣대]]></title>
            <link>https://velog.io/@tasker_dev/%EB%B3%B5%EC%9E%A1%EB%8F%84-%ED%99%98%EA%B2%BD%EC%97%90-%ED%9D%94%EB%93%A4%EB%A6%AC%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%84%B1%EB%8A%A5-%EC%9E%A3%EB%8C%80</link>
            <guid>https://velog.io/@tasker_dev/%EB%B3%B5%EC%9E%A1%EB%8F%84-%ED%99%98%EA%B2%BD%EC%97%90-%ED%9D%94%EB%93%A4%EB%A6%AC%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%84%B1%EB%8A%A5-%EC%9E%A3%EB%8C%80</guid>
            <pubDate>Tue, 01 Sep 2026 06:25:23 GMT</pubDate>
            <description><![CDATA[<h2 id="1-실행-시간을-직접-재면-생기는-문제">1. 실행 시간을 직접 재면 생기는 문제</h2>
<p>같은 코드라도 어떤 하드웨어와 소프트웨어 위에서 돌리느냐에 따라 성능이 달라집니다. CPU 속도, 메모리, 운영체제, 인터프리터 구현이 전부 제각각이기 때문입니다.</p>
<p>그래서 초 단위 실측값은 알고리즘의 성능이라기보다 &quot;이 알고리즘을 이 환경에서 돌린 결과&quot;에 가깝습니다. 두 알고리즘 중 어느 쪽이 더 나은지 말하려면 환경을 걷어낸 기준이 필요합니다.</p>
<h2 id="2-가상-컴퓨터-위에서-시뮬레이션하기">2. 가상 컴퓨터 위에서 시뮬레이션하기</h2>
<p>해결책은 측정 대상을 실제 컴퓨터에서 가상의 컴퓨터로 옮기는 것입니다. 가상 컴퓨터, 가상 언어, 가상 코드를 정해 두고 그 위에서 시뮬레이션합니다.</p>
<p>가상 컴퓨터가 정의하는 것은 기본 연산입니다. 다음 연산들은 각각 1단위 시간에 처리된다고 봅니다.</p>
<ul>
<li>배정, 대입, 복사 연산</li>
<li>산술 연산</li>
<li>비교 연산</li>
<li>논리 연산</li>
<li>비트 연산</li>
</ul>
<pre><code>[ 가상 컴퓨터 ]   기본 연산 1개 = 1단위 시간
      |
[ 가상 언어  ]   기본 연산 + 비교 + 반복 + 함수
      |
[ 가상 코드  ]   입력 / 본문 / 출력
      |
     T(n)</code></pre><p>곱셈이 덧셈보다 실제로는 느리지만 둘 다 1단위로 셉니다. 개별 연산의 실제 비용은 환경마다 달라지는 값이라, 여기서 구분해 봐야 다시 환경에 끌려가기 때문입니다.</p>
<h2 id="3-가상-언어와-가상-코드">3. 가상 언어와 가상 코드</h2>
<p>가상 언어는 최소한 다음을 표현할 수 있어야 합니다.</p>
<ul>
<li>기본 연산</li>
<li>비교</li>
<li>반복</li>
<li>함수</li>
</ul>
<p>가상 코드는 이 가상 언어로 작성한 코드이며, 입력과 본문과 출력으로 구성됩니다. 실행 가능한 완성 코드일 필요는 없고 흐름이 드러나면 충분합니다.</p>
<pre><code class="language-python">def search(A, key):            # 입력: 배열 A, 찾을 값 key
    for i in range(len(A)):    # 반복
        if A[i] == key:        # 비교 연산
            return i           # 출력
    return -1</code></pre>
<h2 id="4-무한한-입력-중-무엇을-기준으로-세는가">4. 무한한 입력 중 무엇을 기준으로 세는가</h2>
<p>가상 코드의 기본 연산 횟수를 세면 단위 시간 기준의 비용이 나옵니다. 문제는 입력이 무한히 많다는 점입니다. 크기가 같은 입력이어도 값이 어떻게 놓여 있느냐에 따라 연산 횟수가 달라집니다.</p>
<pre><code>best case:  찾는 값이 맨 앞
  [ 5 ][ 3 ][ 9 ][ 1 ][ 7 ]
    ^  비교 1회

worst case: 찾는 값이 맨 뒤이거나 아예 없음
  [ 5 ][ 3 ][ 9 ][ 1 ][ 7 ]
    ^---&gt;---&gt;---&gt;---&gt;^  비교 5회</code></pre><p>기준으로 삼는 것은 가장 안 좋은 입력, 즉 worst case입니다. 최악의 입력에 대한 기본 연산 횟수를 재는 것을 worst case time complexity라고 합니다. 최악을 기준으로 잡으면 &quot;적어도 이보다 나쁘지는 않다&quot;는 보장을 얻을 수 있습니다.</p>
<p>이 값은 입력 크기 n에 대한 함수 <code>T(n)</code> 형태로 표현합니다. 위 예시는 T(n) = n입니다.</p>
<h2 id="5-tn에서-big-o로">5. T(n)에서 Big-O로</h2>
<p>T(n)이 n에 대해 선형으로 증가하는지 비선형으로 증가하는지에 따라 알고리즘의 성격이 갈립니다. n이 커질수록 결과를 좌우하는 것은 최고차항이고, 계수나 낮은 차수 항의 영향은 상대적으로 줄어듭니다.</p>
<p>그래서 최고차항만 남겨 간단히 표기하는데, 이것이 Big-O 표기법입니다.</p>
<pre><code>T(n) = 3n² + 20n + 100
       ~~~
       최고차항만 남긴다   -&gt;   O(n²)</code></pre><table>
<thead>
<tr>
<th>예시</th>
<th>평균</th>
<th>최악</th>
<th>Big-O</th>
</tr>
</thead>
<tbody><tr>
<td>배정문 한 줄</td>
<td>1</td>
<td>1</td>
<td>O(1)</td>
</tr>
<tr>
<td>배열 선형 탐색</td>
<td>n/2</td>
<td>n</td>
<td>O(n)</td>
</tr>
<tr>
<td>이중 반복문 전체 순회</td>
<td>n²</td>
<td>n²</td>
<td>O(n²)</td>
</tr>
</tbody></table>
<p>선형 탐색의 평균은 찾는 값이 배열에 존재한다고 가정한 값입니다. 이중 반복문처럼 평균과 최악이 같은 경우도 있습니다.</p>
<p>파이썬에서 <code>key in some_list</code>가 바로 이 선형 탐색이라 O(n)이고, <code>key in some_dict</code>는 평균 O(1)입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[관계 대수]]></title>
            <link>https://velog.io/@tasker_dev/%EA%B4%80%EA%B3%84-%EB%8C%80%EC%88%98</link>
            <guid>https://velog.io/@tasker_dev/%EA%B4%80%EA%B3%84-%EB%8C%80%EC%88%98</guid>
            <pubDate>Mon, 31 Aug 2026 09:32:04 GMT</pubDate>
            <description><![CDATA[<h2 id="1-이게-없으면-무엇이-문제인가">1. 이게 없으면 무엇이 문제인가</h2>
<p>SQL만 아는 상태에서는 &quot;같은 결과를 내는 두 쿼리의 속도가 왜 다른가&quot;를 설명할 수 없습니다. SQL은 원하는 결과만 적을 뿐 계산 순서를 적지 않기 때문입니다. 그 순서를 적는 언어가 관계 대수이고, DBMS는 SQL을 관계 대수 식으로 바꾼 뒤 그 식을 변형해 최적화합니다. 실행 계획에 찍히는 Filter, Hash Join, Nested Loop는 그 결과물입니다.</p>
<ul>
<li>관계 해석(relational calculus): 원하는 데이터만 명시하고 수행 방법은 명시하지 않는 선언적 언어</li>
<li>관계 대수(relational algebra): 어떻게 수행할지를 명시하는 절차적 언어</li>
</ul>
<p>SQL은 겉모습이 관계 해석에 가깝지만 이론적 기초는 관계 대수이고, 관계 대수는 DBMS의 내부 언어로도 쓰입니다.</p>
<h2 id="2-예제-스키마">2. 예제 스키마</h2>
<p><code>student</code></p>
<table>
<thead>
<tr>
<th>sid</th>
<th>name</th>
<th>dept_id</th>
<th>year</th>
</tr>
</thead>
<tbody><tr>
<td>1001</td>
<td>김민수</td>
<td>CSE</td>
<td>3</td>
</tr>
<tr>
<td>1002</td>
<td>이서연</td>
<td>CSE</td>
<td>2</td>
</tr>
<tr>
<td>1003</td>
<td>박지훈</td>
<td>MTH</td>
<td>4</td>
</tr>
</tbody></table>
<p><code>department</code></p>
<table>
<thead>
<tr>
<th>dept_id</th>
<th>dept_name</th>
<th>building</th>
</tr>
</thead>
<tbody><tr>
<td>CSE</td>
<td>컴퓨터공학</td>
<td>공학관</td>
</tr>
<tr>
<td>MTH</td>
<td>수학</td>
<td>자연관</td>
</tr>
</tbody></table>
<h2 id="3-필수-연산자-다섯-개">3. 필수 연산자 다섯 개</h2>
<p>이 다섯 개만 있으면 나머지는 전부 유도됩니다.</p>
<p><strong>셀렉션(selection)</strong> — 조건을 만족하는 튜플의 부분집합. 단항 연산자이고 결과의 차수(degree)는 입력과 같습니다.</p>
<pre><code class="language-sql">-- σ_{year=3}(student)
SELECT * FROM student WHERE year = 3;</code></pre>
<p><strong>프로젝션(projection)</strong> — 애트리뷰트의 부분집합.</p>
<pre><code class="language-sql">-- π_{dept_id, year}(student)
SELECT DISTINCT dept_id, year FROM student;</code></pre>
<p><strong>합집합(union) · 차집합(difference)</strong> — 두 릴레이션이 합병 가능(union compatible)해야 합니다. 차수가 같고 대응하는 애트리뷰트의 도메인이 같아야 한다는 뜻입니다.</p>
<pre><code class="language-sql">SELECT dept_id FROM student
UNION
SELECT dept_id FROM department;</code></pre>
<p><strong>카티션 곱(Cartesian product)</strong> — 모든 조합. 3행 × 2행이면 6행입니다.</p>
<pre><code class="language-sql">SELECT * FROM student CROSS JOIN department;</code></pre>
<h2 id="4-중복은-어디서-사라지는가">4. 중복은 어디서 사라지는가</h2>
<p>관계 대수의 릴레이션은 집합(set)이므로 <strong>프로젝션 결과에서 중복 튜플은 제거됩니다.</strong> 셀렉션은 원래 있던 튜플을 골라낼 뿐이라 중복이 생길 일이 없습니다. 반면 SQL의 테이블은 멀티셋(multiset)이라 <code>SELECT</code>가 중복을 그대로 남기고, <code>DISTINCT</code>를 붙여야 관계 대수의 프로젝션과 같아집니다.</p>
<h2 id="5-유도된-연산자">5. 유도된 연산자</h2>
<table>
<thead>
<tr>
<th>연산자</th>
<th>표기</th>
<th>의미</th>
<th>SQL</th>
</tr>
</thead>
<tbody><tr>
<td>교집합</td>
<td>R ∩ S</td>
<td>양쪽에 모두 있는 튜플</td>
<td><code>INTERSECT</code></td>
</tr>
<tr>
<td>세타 조인</td>
<td>R ⋈<sub>θ</sub> S</td>
<td>임의 조건 θ로 결합</td>
<td><code>JOIN ... ON a &lt; b</code></td>
</tr>
<tr>
<td>동등 조인</td>
<td>R ⋈<sub>A=B</sub> S</td>
<td>θ가 등호인 세타 조인</td>
<td><code>JOIN ... ON a = b</code></td>
</tr>
<tr>
<td>자연 조인</td>
<td>R ⋈ S</td>
<td>같은 이름 애트리뷰트로 동등 조인 후 중복 열 제거</td>
<td><code>NATURAL JOIN</code></td>
</tr>
<tr>
<td>세미 조인</td>
<td>R ⋉ S</td>
<td>짝이 있는 R의 튜플만, R의 열만 남김</td>
<td><code>WHERE EXISTS (...)</code></td>
</tr>
<tr>
<td>디비전</td>
<td>R ÷ S</td>
<td>S의 모든 튜플과 짝을 이루는 R의 튜플</td>
<td><code>NOT EXISTS</code> 이중 부정</td>
</tr>
</tbody></table>
<pre><code class="language-sql">-- 자연 조인: student ⋈ department (dept_id 기준)
SELECT s.name, d.dept_name
FROM student s JOIN department d ON s.dept_id = d.dept_id;

-- 세미 조인: 수강 기록이 있는 학생만
SELECT * FROM student s
WHERE EXISTS (SELECT 1 FROM enroll e WHERE e.sid = s.sid);

-- 디비전: 모든 과목을 수강한 학생
SELECT * FROM student s
WHERE NOT EXISTS (
  SELECT 1 FROM course c
  WHERE NOT EXISTS (
    SELECT 1 FROM enroll e WHERE e.sid = s.sid AND e.cid = c.cid));</code></pre>
<p><code>NATURAL JOIN</code>은 PostgreSQL에는 있지만 SQL Server에는 없습니다. 실습 DBMS가 SQL Server라면 위처럼 <code>ON</code>으로 조인 열을 직접 적어야 합니다. 어차피 자연 조인은 열 이름이 우연히 겹치면 조용히 결과가 바뀌므로 실무에서는 명시하는 쪽이 낫습니다.</p>
<h2 id="6-외부-조인">6. 외부 조인</h2>
<p>내부 조인은 짝이 없는 튜플을 버립니다. &quot;학과가 아직 배정되지 않은 학생&quot;을 세야 하는 순간 이게 문제가 됩니다. 외부 조인(outer join)은 짝 없는 쪽을 NULL로 채워 남깁니다. 왼쪽·오른쪽·완전 세 가지가 있습니다.</p>
<pre><code class="language-sql">SELECT s.name, d.dept_name
FROM student s LEFT OUTER JOIN department d ON s.dept_id = d.dept_id;</code></pre>
<p>SQL Server의 옛 <code>*=</code>, <code>=*</code> 표기는 2008에서 사실상 막혔고 2012부터는 호환성 수준을 낮춰도 쓸 수 없습니다. 표준 <code>LEFT/RIGHT/FULL OUTER JOIN</code>만 씁니다.</p>
<h2 id="7-관계-대수의-한계">7. 관계 대수의 한계</h2>
<p>순수 관계 대수로는 산술 연산, 정렬, 데이터베이스 갱신을 할 수 없습니다. 그래서 집단 함수(aggregate function), 그룹화, 외부 조인이 확장 연산자로 추가되었습니다. SQL의 <code>SUM</code>, <code>GROUP BY</code>, <code>ORDER BY</code>, <code>UPDATE</code>는 모두 순수 관계 대수 바깥에 있는 것들입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네트워크 기본]]></title>
            <link>https://velog.io/@tasker_dev/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B8%B0%EB%B3%B8</link>
            <guid>https://velog.io/@tasker_dev/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EA%B8%B0%EB%B3%B8</guid>
            <pubDate>Mon, 31 Aug 2026 07:59:43 GMT</pubDate>
            <description><![CDATA[<h2 id="1-우리는-edge에-있고-core에는-router가-있다">1. 우리는 edge에 있고, core에는 router가 있다</h2>
<p>인터넷은 크게 network edge(네트워크 엣지)와 network core(네트워크 코어)로 나뉩니다. 노트북, 휴대폰, 서버 같은 end system은 모두 edge에 있습니다. 우리가 배포하는 서비스도 여기 있습니다. core에는 router(라우터)가 있고, 하는 일은 하나입니다. 들어온 데이터를 목적지 방향으로 넘겨주는 것.</p>
<p>여기서 server(서버)의 정의도 담백해집니다. 24시간 켜져 있으면서 고정된 주소로 클라이언트의 요청을 기다리는 쪽이 서버입니다. 특별한 하드웨어가 아니라 &quot;먼저 말을 걸지 않고 기다리는 역할&quot;이 서버를 서버로 만듭니다.</p>
<p><img src="https://velog.velcdn.com/images/tasker_dev/post/55d60d03-c20a-4e87-a311-d4bc65e1959c/image.png" alt=""></p>
<h2 id="2-edge가-고를-수-있는-두-가지-서비스">2. edge가 고를 수 있는 두 가지 서비스</h2>
<p>edge에 있는 애플리케이션은 통신 방식을 고를 수 있습니다. connection-oriented service(연결 지향 서비스)인 TCP와, connectionless service(비연결형 서비스)인 UDP입니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>TCP</th>
<th>UDP</th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>연결을 맺고 byte stream을 순서대로 전달</td>
<td>데이터그램을 그냥 던짐</td>
</tr>
<tr>
<td>신뢰성</td>
<td>reliable, in-order 보장. 유실 시 재전송</td>
<td>보장 없음. 유실·순서 뒤바뀜 그대로</td>
</tr>
<tr>
<td>오버헤드</td>
<td>연결 설정, ACK, flow control(흐름 제어), congestion control(혼잡 제어)</td>
<td>거의 없음</td>
</tr>
<tr>
<td>쓰이는 곳</td>
<td>웹, 파일 전송, 메일</td>
<td>실시간 스트리밍, DNS 조회, 게임</td>
</tr>
</tbody></table>
<p>TCP가 제공하는 flow control과 congestion control이 각각 무엇을 막아주는지는 전송 계층 편에서 본격적으로 다룹니다. 지금은 &quot;TCP는 대가를 치르고 신뢰성을 사고, UDP는 대가를 치르지 않는 대신 아무것도 사지 않는다&quot; 정도로 충분합니다.</p>
<h2 id="3-packet과-protocol">3. packet과 protocol</h2>
<p>packet(패킷)은 보내려는 데이터를 일정 크기로 자른 뒤, 어디로 가야 하는지 적은 header(헤더)를 붙인 덩어리입니다. 100MB 파일을 통째로 보내지 않고 잘게 쪼개는 이유는 뒤에서 계속 나옵니다. 일부만 실패해도 그 조각만 다시 보내면 되고, 여러 사용자가 링크를 번갈아 쓸 수 있기 때문입니다.</p>
<p>protocol(프로토콜)은 그 header를 어떻게 쓰고 어떤 순서로 메시지를 주고받을지 정한 통신 규약입니다. 사람으로 치면 &quot;여보세요&quot;를 먼저 말하기로 한 약속입니다.</p>
<h2 id="4-circuit-switching-vs-packet-switching">4. circuit switching vs packet switching</h2>
<p>core가 데이터를 나르는 방식은 역사적으로 두 갈래였습니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>circuit switching</th>
<th>packet switching</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>
<tr>
<td>예</td>
<td>전통적인 전화망</td>
<td>인터넷</td>
</tr>
</tbody></table>
<p>인터넷은 packet switching을 택했습니다. 사용자의 트래픽은 대체로 bursty하기 때문입니다. 잠깐 몰아 쓰고 한참 쉬는 패턴에서 회선을 통째로 예약해 두는 것은 낭비입니다. 대신 대가가 따릅니다. 아무도 자리를 예약해 주지 않으므로, 붐비면 밀립니다.</p>
<h2 id="5-packet-switching이-치르는-대가-네-가지-delay">5. packet switching이 치르는 대가: 네 가지 delay</h2>
<p>패킷 하나가 노드를 통과할 때 겪는 지연은 네 가지로 나뉩니다. 전체 지연은 이 넷의 합입니다.</p>
<p><img src="https://velog.velcdn.com/images/tasker_dev/post/2296ee9c-c4a5-4cf9-ac33-f2e90d19f829/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>지연</th>
<th>무엇을 기다리나</th>
<th>계산</th>
<th>줄이는 방법</th>
</tr>
</thead>
<tbody><tr>
<td>nodal processing</td>
<td>헤더를 보고 어느 링크로 보낼지 결정</td>
<td>고정에 가까움</td>
<td>라우터 성능, forwarding 단순화</td>
</tr>
<tr>
<td>queueing</td>
<td>큐에 있는 앞 패킷들이 빠지기를 대기</td>
<td>공식 없음. traffic intensity에 의존</td>
<td>트래픽 분산, 링크 증설</td>
</tr>
<tr>
<td>transmission</td>
<td>패킷 전체를 링크에 밀어 올리는 시간</td>
<td>L / R (L=패킷 크기, R=링크 속도)</td>
<td>R 키우기, L 줄이기</td>
</tr>
<tr>
<td>propagation</td>
<td>비트가 물리적으로 링크를 건너는 시간</td>
<td>d / s (s는 매질 전파 속도)</td>
<td>거리 자체를 줄이기</td>
</tr>
</tbody></table>
<p>패킷 길이를 L 비트, 링크 전송률을 R bps라고 하면 transmission delay는 L/R입니다. 네 개 중 셋은 비교적 예측 가능한데, queueing delay만 다릅니다. 패킷 도착률 a, 패킷 길이 L, 링크 처리율 R일 때 La/R을 traffic intensity라고 하며, 이 값이 1에 가까워질수록 큐잉 지연이 급격히 커집니다. 1을 넘으면 큐는 끝없이 쌓입니다.</p>
<h2 id="6-loss와-dumb-core">6. loss와 dumb core</h2>
<p>그런데 실제 큐는 유한합니다. 그래서 무한히 밀리는 대신 다른 일이 벌어집니다. 큐가 가득 찬 상태에서 도착한 패킷은 라우터가 버립니다. 즉 인터넷에서 packet loss(패킷 손실)는 대부분 전선이 끊겨서가 아니라 큐가 넘쳐서 생깁니다.</p>
<p>그러면 잃어버린 패킷은 누가 되살릴까요. 라우터가 대신 챙겨줄 것 같지만 아닙니다. 라우터는 버리고 잊습니다. 재전송은 양 끝단, 즉 edge의 TCP가 알아서 감지하고 다시 보냅니다. 이것이 dumb core(멍청한 코어)라는 표현의 의미입니다. 복잡한 판단과 자원 관리는 가장자리에서 하고, 코어는 최대한 단순하게 두는 설계 원칙입니다.</p>
<p>이 결정 하나가 인터넷의 성격을 거의 전부 설명합니다. 코어가 단순하니 라우터는 빠르고 싸게 만들 수 있었고, 새로운 애플리케이션은 코어를 고치지 않고도 등장할 수 있었습니다. 대신 신뢰성이라는 숙제가 통째로 양 끝단에 떠넘겨졌습니다. 앞으로 볼 전송 계층 내용의 상당 부분은 이 떠넘겨진 숙제를 푸는 이야기입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[운영체제란 무엇인가]]></title>
            <link>https://velog.io/@tasker_dev/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@tasker_dev/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Mon, 31 Aug 2026 06:33:52 GMT</pubDate>
            <description><![CDATA[<h2 id="1-운영체제가-없으면-무엇이-문제인가">1. 운영체제가 없으면 무엇이 문제인가</h2>
<p>프로그램 하나를 실행한다고 해봅시다. 운영체제가 없다면 이 프로그램은 CPU를 언제부터 얼마나 쓸지, 메모리의 어느 주소에 올라갈지, 디스크의 어느 위치에 파일을 쓸지를 스스로 정해야 합니다. 프로그램이 둘 이상이 되면 문제가 커집니다. 서로 같은 메모리 영역을 덮어쓰거나, 한 프로그램이 CPU를 놓지 않으면 나머지는 영영 실행되지 못합니다.</p>
<p>그래서 하드웨어 바로 위에 한 겹을 둡니다. 운영체제(operating system)는 컴퓨터 하드웨어 바로 위에 설치되어 사용자 및 다른 모든 소프트웨어와 하드웨어를 연결하는 계층입니다.</p>
<pre><code>+--------------------------------------------------+
|  사용자(User)                                     |
+--------------------------------------------------+
|  응용 프로그램 / 시스템 유틸리티                     |
+--------------------------------------------------+
|  운영체제(OS)  -- 이 중 메모리 상주 부분이 커널       |
+--------------------------------------------------+
|  하드웨어 : CPU / 메모리 / 입출력 장치               |
+--------------------------------------------------+</code></pre><h2 id="2-커널과-넓은-의미의-운영체제">2. 커널과 넓은 의미의 운영체제</h2>
<p>운영체제 전체를 항상 메모리에 올려두면 정작 사용자 프로그램이 쓸 메모리가 줄어듭니다. 그래서 꼭 필요한 부분만 상주시키고 나머지는 필요할 때 올립니다. </p>
<ul>
<li>커널(kernel): 운영체제의 핵심 부분으로 메모리에 상주하는 부분. 좁은 의미의 운영체제입니다.</li>
<li>넓은 의미의 운영체제: 커널뿐 아니라 각종 주변 시스템 유틸리티까지 포함하는 개념입니다.</li>
</ul>
<p>셸, 파일 탐색기, 각종 명령어 도구는 유틸리티 쪽입니다. 실행할 때 메모리에 올라왔다가 끝나면 내려갑니다. 반면 커널은 전원이 켜져 있는 동안 계속 메모리에 남아 있습니다.</p>
<h2 id="3-목적-자원-관리와-환상-만들기">3. 목적: 자원 관리와 환상 만들기</h2>
<p>운영체제의 목적은 두 가지입니다. 하드웨어를 효과적으로 관리하는 것, 그리고 사용자가 컴퓨터 시스템을 편리하게 사용하도록 돕는 것입니다. 여기서 관리 대상인 자원(resource)은 CPU, 메모리, 입출력 장치 등을 말합니다. 핵심은 주어진 자원으로 최대한의 성능을 내는 것입니다.</p>
<p>편리함 쪽의 결론은 이 한 문장입니다. 동시에 접속한 사용자와 프로그램들이 각각 독자적인 컴퓨터에서 수행되는 것 같은 환상을 제공합니다. 앞으로 나올 프로세스, 가상 메모리, 파일 시스템이 전부 이 환상을 유지하기 위한 장치입니다.</p>
<p>CPU 1개에 프로그램 A, B, C가 올라가 있고 10ms씩 번갈아 준다고 하면 이렇게 됩니다.</p>
<pre><code>시간(ms)  0     10    20    30    40    50    60
CPU      |  A  |  B  |  C  |  A  |  B  |  C  |
사람 눈  ------ 셋이 동시에 도는 것처럼 보임 ------</code></pre><h2 id="4-운영체제의-분류">4. 운영체제의 분류</h2>
<table>
<thead>
<tr>
<th>기준</th>
<th>구분</th>
<th>설명</th>
<th>예</th>
</tr>
</thead>
<tbody><tr>
<td>동시 작업 여부</td>
<td>단일 작업</td>
<td>한 번에 하나의 작업만 처리</td>
<td>MS-DOS</td>
</tr>
<tr>
<td></td>
<td>다중 작업(multitasking)</td>
<td>두 개 이상을 동시에 처리</td>
<td>UNIX, Windows</td>
</tr>
<tr>
<td>사용자의 수</td>
<td>단일 사용자</td>
<td>한 사람만 시스템을 사용</td>
<td>MS-DOS, 개인용 Windows</td>
</tr>
<tr>
<td></td>
<td>다중 사용자</td>
<td>여러 사용자가 동시 접속, 계정별 보호 필요</td>
<td>UNIX</td>
</tr>
<tr>
<td>처리 방식</td>
<td>일괄 처리(batch)</td>
<td>작업을 모아 한꺼번에 처리, 응답이 늦음</td>
<td>초기 시스템</td>
</tr>
<tr>
<td></td>
<td>시분할(time sharing)</td>
<td>CPU 시간을 잘라 나눠 씀, 대화형</td>
<td>UNIX</td>
</tr>
<tr>
<td></td>
<td>실시간(real time)</td>
<td>정해진 시간 안에 처리를 보장</td>
<td>제어 시스템</td>
</tr>
</tbody></table>
<p>실시간 시스템은 다시 둘로 나뉩니다. 메모에는 이름만 적혀 있어 내용을 채웁니다. 경성 실시간 시스템(hard real time system)은 데드라인을 넘기면 그 자체로 실패인 쪽이고, 연성 실시간 시스템(soft real time system)은 늦으면 품질이 나빠질 뿐 치명적이지는 않은 쪽입니다.</p>
<h2 id="5-헷갈리는-용어-정리">5. 헷갈리는 용어 정리</h2>
<table>
<thead>
<tr>
<th>용어</th>
<th>무엇이 여러 개인가</th>
</tr>
</thead>
<tbody><tr>
<td>다중 프로그래밍(multiprogramming)</td>
<td>메모리에 여러 프로그램이 동시에 올라가 있음</td>
</tr>
<tr>
<td>시분할(time sharing)</td>
<td>CPU의 시간을 분할하여 나눠 씀</td>
</tr>
<tr>
<td>다중 작업(multitasking)</td>
<td>여러가지 일을 동시에 처리함</td>
</tr>
<tr>
<td>다중 처리기(multiprocessor)</td>
<td>하나의 컴퓨터에 CPU가 여러 개 붙어 있음</td>
</tr>
</tbody></table>
<p>같은 상황을 어느 자원 관점에서 부르느냐의 차이입니다. 여러 프로그램이 번갈아 도는 한 장면을 두고 메모리에 초점을 맞추면 multiprogramming, CPU 시간에 초점을 맞추면 time sharing이라고 부릅니다. multiprocessor만 성격이 다릅니다. 이건 관점이 아니라 하드웨어 구성 자체가 여러 개인 경우입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자료구조 시작하기]]></title>
            <link>https://velog.io/@tasker_dev/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tasker_dev/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 31 Aug 2026 05:06:34 GMT</pubDate>
            <description><![CDATA[<h1 id="자료구조와-알고리즘">자료구조와 알고리즘</h1>
<h2 id="1-저장만-해서는-아무것도-못-합니다">1. 저장만 해서는 아무것도 못 합니다</h2>
<p>프로그램이 다루는 것은 결국 자료(data)입니다. 자료는 어딘가에 놓여야 하고, 그 자리가 메모리입니다. 그런데 값을 메모리에 올려두기만 하면 할 수 있는 일이 없습니다. 값을 꺼내 오고, 바꾸고, 중간에 끼워 넣고, 지우고, 찾을 수 있어야 비로소 쓸모가 생깁니다.</p>
<p>읽기, 쓰기, 삽입, 삭제, 탐색. 이 연산들을 어떤 규칙으로 지원할지 정해 둔 형태가 자료구조입니다. 저장 공간 자체가 자료구조인 것이 아니라, 저장 공간에 연산을 얹은 결과가 자료구조입니다.</p>
<pre><code>memory
+-----+-----+-----+-----+
|  3  |  1  |  4  |  1  |
+-----+-----+-----+-----+
   0     1     2     3
   ^
   읽기 / 쓰기 / 삽입 / 삭제 / 탐색</code></pre><h2 id="2-가장-단순한-예-변수와-배열">2. 가장 단순한 예: 변수와 배열</h2>
<p>변수는 값 하나를 담는 가장 작은 자료구조입니다. 배열은 값을 순서대로 여러 개 담습니다. 배열이 지원하는 연산은 append, insert, pop 같은 것들입니다.</p>
<p>이 중 insert처럼 상태를 바꾸는 연산은 전후를 같이 봐야 이해가 됩니다.</p>
<pre><code>before   insert(1, 9)
+-----+-----+-----+
|  3  |  1  |  4  |
+-----+-----+-----+
   0     1     2

after
+-----+-----+-----+-----+
|  3  |  9  |  1  |  4  |
+-----+-----+-----+-----+
   0     1     2     3
         ^ 뒤 원소들이 한 칸씩 밀립니다</code></pre><p>밀어내는 비용이 왜 생기는지, 파이썬 list가 이를 어떻게 처리하는지는 배열 편에서 다룹니다.</p>
<h2 id="3-알고리즘은-절차입니다">3. 알고리즘은 절차입니다</h2>
<p>알고리즘은 입력 데이터를 받아 유한한 횟수의 연산을 반복해 정답을 출력하는 절차입니다. 여기서 중요한 단어는 <strong>유한</strong>입니다. 언젠가 반드시 끝나야 하고, 끝났을 때 답이 나와야 합니다.</p>
<p>자료구조가 &quot;무엇을 어떻게 담을 것인가&quot;라면, 알고리즘은 &quot;그 위에서 무엇을 어떤 순서로 할 것인가&quot;입니다.</p>
<h2 id="4-같은-문제를-푸는-두-알고리즘">4. 같은 문제를 푸는 두 알고리즘</h2>
<p>최대공약수를 구하는 문제로 보겠습니다. 두 수에서 큰 수에서 작은 수를 계속 빼 나가다가 두 수가 같아지면 그 값이 최대공약수입니다. 유클리드가 남긴 방법이고, while문 하나로 바로 옮길 수 있습니다.</p>
<pre><code class="language-python">def gcd_sub(a, b):
    while a != b:
        if a &gt; b:
            a = a - b
        else:
            b = b - a
    return a</code></pre>
<p>동작을 따라가 보면 이렇습니다.</p>
<pre><code>gcd_sub(24, 18)
(24, 18) -&gt; (6, 18) -&gt; (6, 12) -&gt; (6, 6) -&gt; 6</code></pre><p>문제는 두 수의 차이가 클 때입니다. <code>gcd_sub(1000000000, 1)</code>이면 1을 십억 번 가까이 빼야 합니다. 뺄셈 자체는 빠르지만 반복 횟수가 너무 많습니다.</p>
<p>여기서 관찰 하나가 나옵니다. 같은 수를 계속 빼는 것은 결국 나눗셈이고, 남는 값은 나머지입니다. 뺄셈을 여러 번 반복할 자리에 나머지 연산 한 번을 넣으면 됩니다.</p>
<pre><code class="language-python">def gcd_mod(a, b):
    while b != 0:
        a, b = b, a % b
    return a</code></pre>
<pre><code>gcd_mod(24, 18)
(24, 18) -&gt; (18, 6) -&gt; (6, 0) -&gt; 6</code></pre><p>같은 입력인데 반복이 세 번으로 줄었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[in을 쓰고 딕셔너리 키가 없는 경우를 KeyError로 처리하지 말고 get을 사용할 것]]></title>
            <link>https://velog.io/@tasker_dev/in%EC%9D%84-%EC%93%B0%EA%B3%A0-%EB%94%95%EC%85%94%EB%84%88%EB%A6%AC-%ED%82%A4%EA%B0%80-%EC%97%86%EB%8A%94-%EA%B2%BD%EC%9A%B0%EB%A5%BC-KeyError%EB%A1%9C-%EC%B2%98%EB%A6%AC%ED%95%98%EC%A7%80-%EB%A7%90%EA%B3%A0-get%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0-%EA%B2%83</link>
            <guid>https://velog.io/@tasker_dev/in%EC%9D%84-%EC%93%B0%EA%B3%A0-%EB%94%95%EC%85%94%EB%84%88%EB%A6%AC-%ED%82%A4%EA%B0%80-%EC%97%86%EB%8A%94-%EA%B2%BD%EC%9A%B0%EB%A5%BC-KeyError%EB%A1%9C-%EC%B2%98%EB%A6%AC%ED%95%98%EC%A7%80-%EB%A7%90%EA%B3%A0-get%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%A0-%EA%B2%83</guid>
            <pubDate>Fri, 14 Aug 2026 05:51:28 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p>딕셔너리로 하는 일의 대부분은 결국 세 가지입니다. 값에 접근하고, 대입하고, 삭제하는 것입니다. 이 중에서 유독 코드가 지저분해지는 지점이 &quot;키가 아직 없을 때&quot;의 처리입니다. 투표 집계, 태그별 그룹핑, 사용자별 이벤트 누적처럼 흔한 작업마다 같은 분기가 반복됩니다.</p>
<p>파이썬에는 이 상황을 다루는 방법이 네 가지 있습니다. <code>in</code> 식으로 미리 확인하는 방법, <code>KeyError</code>를 잡는 방법, <code>get</code> 메서드를 쓰는 방법, 그리고 <code>setdefault</code>를 쓰는 방법입니다. 네 가지가 다 동작하기 때문에 아무거나 골라도 될 것 같지만, 값의 타입과 생성 비용에 따라 명확한 우열이 생깁니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python">counters = {}
key = &#39;wheat&#39;

if key in counters:  # 해시 조회가 두 번 일어납니다
    count = counters[key]
else:
    count = 0

counters[key] = count + 1

votes = {}
name = &#39;brioche&#39;
who = &#39;Elmer&#39;

try:  # 예외 흐름으로 정상 로직을 표현하고 있습니다
    names = votes[name]
except KeyError:
    votes[name] = names = []

names.append(who)</code></pre>
<p><code>in</code> 방식은 키를 두 번 조회하고 분기가 세 줄로 늘어납니다. <code>KeyError</code> 방식은 예외를 정상적인 제어 흐름으로 쓰는 셈이라 의도가 잘 드러나지 않습니다. 둘 다 같은 패턴이 코드베이스 곳곳에 복사되기 시작하면 관리 비용이 빠르게 불어납니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<pre><code class="language-python">from collections import defaultdict

counters = {}
key = &#39;wheat&#39;

counters[key] = counters.get(key, 0) + 1  # 조회 한 번으로 끝납니다
print(counters)  # {&#39;wheat&#39;: 1}

votes = {}

if (names := votes.get(&#39;brioche&#39;)) is None:  # 대입식으로 중복 조회를 막습니다
    votes[&#39;brioche&#39;] = names = []

names.append(&#39;Elmer&#39;)

# 값 생성이 반복되는 구조라면 defaultdict가 더 깔끔합니다
grouped = defaultdict(list)
grouped[&#39;brioche&#39;].append(&#39;Elmer&#39;)</code></pre>
<p>카운터처럼 값이 정수나 문자열 같은 기본 타입이면 <code>get</code>에 디폴트를 넘기는 한 줄이 가장 짧고 빠릅니다. 값을 만드는 비용이 비싸거나 생성 도중 예외가 날 수 있다면 <code>get</code>과 대입식(walrus operator)을 함께 써서 필요할 때만 만들면 됩니다. <code>setdefault</code>는 키 존재 여부와 무관하게 디폴트 값을 매번 생성하므로, 그 패턴이 자주 보인다면 <code>defaultdict</code>로 넘어가는 편이 낫습니다. 가독성과 성능이 동시에 개선되는 선택입니다.</p>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li>키가 없는 경우를 다루는 방법은 <code>in</code>, <code>KeyError</code>, <code>get</code>, <code>setdefault</code> 네 가지입니다.</li>
<li><code>get</code>의 두 번째 인자는 키가 없을 때 돌려줄 디폴트 값입니다.</li>
<li>값이 기본 타입인 카운터류에는 <code>get</code>이 가장 짧고 빠릅니다.</li>
<li>값 생성 비용이 크거나 예외가 날 수 있으면 <code>get</code>과 대입식을 조합합니다.</li>
<li><code>setdefault</code>는 이름이 헷갈리고 디폴트 값을 항상 만들어 내므로 우선순위가 낮습니다.</li>
<li><code>setdefault</code>가 적합해 보이는 상황이라면 <code>defaultdict</code> 사용을 먼저 검토해야 합니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>로그나 이벤트 집계 코드에서 곧바로 쓰입니다. 요청 로그를 훑으며 상태 코드별 건수를 세는 작업은 <code>stats[code] = stats.get(code, 0) + 1</code> 한 줄이면 충분하고, 사용자별 이벤트를 리스트로 모으는 작업은 <code>defaultdict(list)</code>로 분기 자체를 없앨 수 있습니다.</p>
<p>외부 API 응답을 파싱할 때도 유용합니다. 선택적 필드가 많은 JSON에서 <code>payload.get(&#39;meta&#39;, {}).get(&#39;locale&#39;, &#39;ko&#39;)</code>처럼 체이닝하면 중첩된 <code>if</code> 없이 안전한 기본값을 확보할 수 있습니다. 다만 값 생성이 비싼 경우에는 <code>get</code>의 인자가 항상 평가된다는 점을 기억하고 대입식 쪽으로 방향을 잡아야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[딕셔너리 삽입 순서에 의존할 때는 조심할것]]></title>
            <link>https://velog.io/@tasker_dev/%EB%94%95%EC%85%94%EB%84%88%EB%A6%AC-%EC%82%BD%EC%9E%85-%EC%88%9C%EC%84%9C%EC%97%90-%EC%9D%98%EC%A1%B4%ED%95%A0-%EB%95%8C%EB%8A%94-%EC%A1%B0%EC%8B%AC%ED%95%A0%EA%B2%83</link>
            <guid>https://velog.io/@tasker_dev/%EB%94%95%EC%85%94%EB%84%88%EB%A6%AC-%EC%82%BD%EC%9E%85-%EC%88%9C%EC%84%9C%EC%97%90-%EC%9D%98%EC%A1%B4%ED%95%A0-%EB%95%8C%EB%8A%94-%EC%A1%B0%EC%8B%AC%ED%95%A0%EA%B2%83</guid>
            <pubDate>Fri, 14 Aug 2026 05:35:43 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p>파이썬 3.7부터 <code>dict</code>는 키를 삽입한 순서대로 순회한다는 것이 언어 명세로 보장됩니다. 덕분에 정렬 코드를 따로 쓰지 않고도 &quot;가장 먼저 등록된 항목&quot;을 첫 번째로 꺼내 쓰는 코드가 자연스럽게 자리 잡았습니다. 여기까지는 안심해도 좋습니다.</p>
<p>함정은 함수가 받는 인자가 항상 진짜 <code>dict</code>라는 보장이 없다는 데 있습니다. 파이썬은 <code>collections.abc.MutableMapping</code>을 상속하거나 <code>__getitem__</code>, <code>keys</code> 같은 메서드만 갖춰도 딕셔너리처럼 동작하는 객체를 쉽게 만들 수 있게 해 줍니다. 이런 사용자 정의 타입은 내부를 정렬된 트리로 구현했든 알파벳순으로 돌려주든 자유입니다. 겉보기에는 똑같이 <code>[]</code>로 접근되고 <code>for</code> 문에 들어가지만, 순서 보장은 어디에도 적혀 있지 않습니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python">from collections.abc import MutableMapping


class SortedDict(MutableMapping):  # dict가 아니지만 dict처럼 보입니다
    def __init__(self):
        self.data = {}

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, value):
        self.data[key] = value

    def __delitem__(self, key):
        del self.data[key]

    def __iter__(self):
        for key in sorted(self.data):  # 삽입 순서가 아니라 정렬 순서입니다
            yield key

    def __len__(self):
        return len(self.data)


def first_winner(votes):
    for name in votes:  # 삽입 순서를 암묵적으로 가정하고 있습니다
        return name


ballots = SortedDict()
ballots[&#39;otter&#39;] = 1281
ballots[&#39;bear&#39;] = 803

print(first_winner(ballots))  # &#39;bear&#39; — 먼저 넣은 것은 otter였습니다</code></pre>
<p><code>first_winner</code>는 잘못된 곳이 없어 보이지만, 순서 보장이 함수 밖의 타입에 달려 있습니다. 타입이 바뀌는 순간 예외도 경고도 없이 결과만 조용히 틀어지므로 추적이 대단히 어렵습니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<pre><code class="language-python">def first_winner(votes: dict[str, int]) -&gt; str:
    # 순서에 의존하지 않고 기준을 코드로 명시합니다
    return max(votes, key=votes.get)


def first_inserted(votes) -&gt; str:
    if not isinstance(votes, dict):  # 실행 시점에 타입을 확인합니다
        raise TypeError(&#39;dict 인스턴스가 필요합니다&#39;)

    return next(iter(votes))</code></pre>
<p>첫 번째 방식은 &quot;가장 먼저 넣은 것&quot;이라는 모호한 기준을 &quot;득표가 가장 많은 것&quot;이라는 명시적 기준으로 바꿔 순서 의존 자체를 없앱니다. 두 번째 방식은 순서가 정말 필요할 때 <code>isinstance</code>로 계약을 강제합니다. 타입 애너테이션(type annotation)에 <code>dict</code>를 적어 두면 mypy 같은 정적 분석기가 호출부에서 미리 잡아 주므로, 셋 중 무엇을 쓰든 안전성이 올라갑니다.</p>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li>파이썬 3.7부터 <code>dict</code>의 삽입 순서 보존은 언어가 보장하는 동작입니다.</li>
<li><code>dict</code>를 상속하지 않은 딕셔너리 유사 객체는 순서를 보존한다고 가정해서는 안 됩니다.</li>
<li>순서 의존 버그는 예외 없이 결과만 바꾸므로 발견이 늦어집니다.</li>
<li>가장 안전한 대안은 정렬이나 집계 기준을 코드에 명시해 순서 의존을 제거하는 것입니다.</li>
<li>순서가 꼭 필요하다면 <code>isinstance</code>로 검사하거나 타입 애너테이션과 정적 분석으로 계약을 강제합니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>설정 로딩 계층에서 자주 걸립니다. YAML 파서나 설정 라이브러리가 돌려주는 객체는 <code>dict</code>가 아니라 자체 매핑 클래스인 경우가 흔한데, &quot;먼저 선언된 핸들러부터 등록한다&quot; 같은 규칙을 순회 순서에 맡기면 라이브러리 버전이 올라갈 때 등록 순서가 뒤바뀔 수 있습니다. 우선순위 필드를 두고 명시적으로 정렬하는 편이 안전합니다.</p>
<p>ORM이나 데이터 검증 라이브러리의 결과를 다룰 때도 마찬가지입니다. <code>RowMapping</code>이나 Pydantic 모델의 <code>dict()</code> 반환값을 그대로 CSV 헤더 순서로 쓰기 전에, 컬럼 순서를 별도 리스트로 고정해 두면 스키마 변경에도 출력이 흔들리지 않습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이터레이터나 제너레이터를 다룰 때는 itertools를 사용할 것]]></title>
            <link>https://velog.io/@tasker_dev/%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%ED%84%B0%EB%82%98-%EC%A0%9C%EB%84%88%EB%A0%88%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%8B%A4%EB%A3%B0-%EB%95%8C%EB%8A%94-itertools%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%A0-%EA%B2%83</link>
            <guid>https://velog.io/@tasker_dev/%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%ED%84%B0%EB%82%98-%EC%A0%9C%EB%84%88%EB%A0%88%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%8B%A4%EB%A3%B0-%EB%95%8C%EB%8A%94-itertools%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%A0-%EA%B2%83</guid>
            <pubDate>Fri, 14 Aug 2026 05:24:31 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p>리스트 세 개를 순서대로 이어 붙이려고 이중 for 문을 쓰고, 인접한 두 원소를 비교하려고 <code>range(len(items) - 1)</code>을 꺼내고, 고정 크기로 잘라 배치 전송하려고 인덱스 계산을 손으로 하고 있는 경우가 있을 겁니다. 이런 코드는 대체로 짧지만 경계 조건에서 조용히 틀립니다. 마지막 그룹이 잘리거나, 인덱스가 하나 어긋나거나, 리스트로 전부 펼치는 바람에 메모리를 잡아먹는 식입니다.</p>
<p>이터레이터(iterator)를 조합하는 패턴은 놀라울 만큼 정형화되어 있습니다. <code>itertools</code>는 그 정형화된 패턴들을 이미 C 레벨로 구현해 둔 표준 모듈입니다. 이터레이션 코드가 복잡해진다고 느끼는 순간이 바로 이 모듈의 문서를 열어 볼 때입니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python">records = list(range(1, 8))
size = 3

batches = []
i = 0

while i &lt; len(records):  # 인덱스 산술을 직접 관리하고 있습니다
    batches.append(records[i:i + size])
    i += size

diffs = []

for i in range(len(records) - 1):  # 빈 리스트에서 경계가 위태롭습니다
    diffs.append(records[i + 1] - records[i])</code></pre>
<p>돌아가기는 하지만 입력을 전부 리스트로 물질화해야 하고, 인덱스 계산이 한 칸만 어긋나도 결과가 조용히 틀어집니다. 스트리밍 입력에는 아예 적용할 수 없습니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<pre><code class="language-python">from itertools import batched, pairwise, chain, islice

records = range(1, 8)

batches = list(batched(records, 3))  # 마지막 그룹은 알아서 짧게 처리됩니다
print(batches)  # [(1, 2, 3), (4, 5, 6), (7,)]

diffs = [b - a for a, b in pairwise(records)]  # 인접 쌍을 안전하게 순회합니다
print(diffs)  # [1, 1, 1, 1, 1, 1]

merged = chain([1, 2], [3, 4])  # 복사 없이 이어 붙입니다
print(list(islice(merged, 3)))  # [1, 2, 3]</code></pre>
<p>인덱스 산술이 사라지면서 의도가 함수 이름으로 드러나고, 경계 처리는 라이브러리가 책임집니다. 게으르게(lazy) 동작하므로 원본을 리스트로 펼치지 않아 메모리에도 유리합니다. 가독성과 성능을 한 번에 챙기는 개선입니다.</p>
<p>자주 쓰는 함수를 용도별로 묶으면 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>목적</th>
<th>함수</th>
</tr>
</thead>
<tbody><tr>
<td>이어 붙이기·반복</td>
<td><code>chain</code>, <code>repeat</code>, <code>cycle</code>, <code>tee</code></td>
</tr>
<tr>
<td>길이 맞추기·자르기</td>
<td><code>zip_longest</code>, <code>islice</code>, <code>batched</code>, <code>pairwise</code></td>
</tr>
<tr>
<td>조건으로 거르기</td>
<td><code>takewhile</code>, <code>dropwhile</code>, <code>filterfalse</code></td>
</tr>
<tr>
<td>누적·조합 만들기</td>
<td><code>accumulate</code>, <code>product</code>, <code>permutations</code>, <code>combinations</code></td>
</tr>
</tbody></table>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li><code>itertools</code>에는 이터레이터를 조합하고 소비하는 검증된 함수들이 모여 있습니다.</li>
<li>반복문이 복잡해진다고 느끼면 직접 짜기 전에 문서를 먼저 확인하는 것이 좋습니다.</li>
<li><code>zip_longest</code>는 짧은 쪽을 <code>fillvalue</code>로 채워 주므로 데이터가 잘려 나가지 않습니다.</li>
<li><code>takewhile</code>은 술어가 거짓이 되는 순간 멈추고, <code>dropwhile</code>은 그 지점까지 건너뜁니다.</li>
<li><code>tee</code>는 하나의 이터레이터를 여러 갈래로 나누지만 내부 버퍼를 쓰므로 소비 속도 차이에 주의해야 합니다.</li>
<li>대부분의 함수가 게으르게 동작하므로 결과를 보려면 <code>list</code>로 감싸야 합니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>외부 API로 대량 데이터를 밀어 넣을 때 <code>batched</code>가 곧바로 쓰입니다. 대부분의 벌크 엔드포인트는 요청당 최대 건수를 제한하는데, <code>for chunk in batched(rows, 500):</code> 한 줄이면 나머지가 500개로 딱 떨어지지 않아도 마지막 묶음까지 안전하게 전송됩니다.</p>
<p>시계열 지표 처리에서는 <code>pairwise</code>와 <code>accumulate</code>가 유용합니다. 센서 값의 구간별 증감을 구할 때 <code>pairwise</code>로 인접 쌍을 만들고, 누적 합계나 러닝 맥스가 필요하면 <code>accumulate</code>에 함수를 넘겨 한 줄로 해결할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[효율적인 쇼트 서킷 로직을 사용하려면 any나 all에게 이터레이터를 전달할 것]]></title>
            <link>https://velog.io/@tasker_dev/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EC%87%BC%ED%8A%B8-%EC%84%9C%ED%82%B7-%EB%A1%9C%EC%A7%81%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A0%A4%EB%A9%B4-any%EB%82%98-all%EC%97%90%EA%B2%8C-%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%A0%84%EB%8B%AC%ED%95%A0-%EA%B2%83</link>
            <guid>https://velog.io/@tasker_dev/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EC%87%BC%ED%8A%B8-%EC%84%9C%ED%82%B7-%EB%A1%9C%EC%A7%81%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A0%A4%EB%A9%B4-any%EB%82%98-all%EC%97%90%EA%B2%8C-%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%A0%84%EB%8B%AC%ED%95%A0-%EA%B2%83</guid>
            <pubDate>Fri, 14 Aug 2026 05:02:36 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p>십만 줄짜리 로그 파일을 훑으면서 &quot;손상된 줄이 하나라도 있는가&quot;를 판단하는 코드를 상상해 보시기 바랍니다. 두 번째 줄에서 이미 손상이 발견됐다면 나머지 구만 구천여 줄은 읽을 이유가 없습니다. <code>any</code>와 <code>all</code>은 정확히 그런 판단을 위해 만들어진 내장 함수입니다. <code>all</code>은 거짓인 원소를 만나는 순간, <code>any</code>는 참인 원소를 만나는 순간 즉시 멈춥니다.</p>
<p>문제는 우리가 무심코 찍는 대괄호 한 쌍입니다. <code>all([...])</code>처럼 리스트 컴프리헨션(list comprehension)을 넘기면, <code>all</code>이 호출되기도 전에 파이썬이 모든 원소를 평가해 리스트를 완성합니다. 쇼트 서킷이 작동할 기회 자체가 사라지는 셈입니다. 검사 함수가 단순 비교라면 체감이 없지만, 그 안에 파일 파싱이나 네트워크 호출이 섞여 있다면 이야기가 완전히 달라집니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python">def is_valid(row):
    print(f&#39;검사 수행: {row!r}&#39;)  # 실제 호출 횟수를 눈으로 확인합니다
    return row.strip() != &#39;&#39;


rows = [&#39;alpha&#39;, &#39;&#39;, &#39;beta&#39;, &#39;gamma&#39;]

# 대괄호 때문에 all이 실행되기 전에 리스트가 먼저 완성됩니다
result = all([is_valid(row) for row in rows])

print(result)  # False — 하지만 검사는 이미 4번 모두 끝났습니다</code></pre>
<p>두 번째 원소에서 결론이 났는데도 <code>is_valid</code>가 네 번 호출됩니다. 리스트 컴프리헨션은 전체를 메모리에 올린 뒤 그 완성품을 <code>all</code>에게 건네기 때문입니다. 원소가 백만 개라면 불필요한 연산과 메모리를 그대로 떠안게 됩니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<pre><code class="language-python">rows = [&#39;alpha&#39;, &#39;&#39;, &#39;beta&#39;, &#39;gamma&#39;]

# 대괄호를 빼면 제너레이터 표현식(generator expression)이 됩니다
result = all(is_valid(row) for row in rows)

print(result)  # False — 검사는 2번만 수행되었습니다

# 반대 방향 질문은 any로 표현하는 편이 읽기 편합니다
has_broken = any(not is_valid(row) for row in rows)

print(has_broken)  # True — 역시 2번에서 멈춥니다</code></pre>
<p>바뀐 것은 대괄호를 뺀 것뿐이지만, 이제 <code>all</code>은 원소를 하나씩 당겨 쓰다가 거짓을 만나면 그 자리에서 반환합니다. 남은 제너레이터(generator)의 이터레이터 참조는 버려지고 가비지 컬렉션되므로 끝까지 순회되지 않습니다. 불필요한 연산과 메모리를 동시에 아끼는, 성능 측면의 개선입니다. <code>any</code>와 <code>all</code> 중 무엇을 쓸지는 조건을 긍정형으로 쓰는 쪽이 덜 헷갈리는지를 기준으로 고르는 것이 좋습니다.</p>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li><code>all</code>은 거짓인 원소를, <code>any</code>는 참인 원소를 만나는 즉시 순회를 중단합니다.</li>
<li>쇼트 서킷으로 중단된 뒤 남은 이터레이터는 끝까지 소비되지 않고 가비지 컬렉션됩니다.</li>
<li>리스트 컴프리헨션을 넘기면 모든 원소가 미리 평가되어 쇼트 서킷 이점이 사라집니다.</li>
<li>인자가 하나뿐일 때는 대괄호만 빼면 곧바로 제너레이터 표현식이 됩니다.</li>
<li>검사 조건을 긍정형으로 표현할 수 있는 쪽을 골라 <code>any</code>와 <code>all</code> 중 하나를 선택합니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>FastAPI의 권한 검사 의존성이 좋은 예입니다. <code>if any(role in user.roles for role in required_roles):</code> 형태로 작성하면 일치하는 역할을 찾는 즉시 나머지 비교를 건너뜁니다. 권한 조회가 캐시 미스로 외부 저장소를 때리는 구조라면 절감 효과가 그대로 응답 시간으로 돌아옵니다.</p>
<p>데이터 파이프라인의 사전 검증 단계에서도 유용합니다. 업로드된 CSV를 처리하기 전에 <code>all(has_required_columns(chunk) for chunk in read_chunks(path))</code>로 확인하면, 첫 청크에서 스키마가 어긋나는 순간 파일 전체를 읽지 않고 빠르게 실패할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[컨테이너에 대해 이터레이션하는중이라면 결코 컨테이너의 내용을 변경하지 말 것]]></title>
            <link>https://velog.io/@tasker_dev/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%97%90-%EB%8C%80%ED%95%B4-%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%EC%85%98%ED%95%98%EB%8A%94%EC%A4%91%EC%9D%B4%EB%9D%BC%EB%A9%B4-%EA%B2%B0%EC%BD%94-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%9D%98-%EB%82%B4%EC%9A%A9%EC%9D%84-%EB%B3%80%EA%B2%BD%ED%95%98%EC%A7%80-%EB%A7%90-%EA%B2%83</link>
            <guid>https://velog.io/@tasker_dev/%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%97%90-%EB%8C%80%ED%95%B4-%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%EC%85%98%ED%95%98%EB%8A%94%EC%A4%91%EC%9D%B4%EB%9D%BC%EB%A9%B4-%EA%B2%B0%EC%BD%94-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%9D%98-%EB%82%B4%EC%9A%A9%EC%9D%84-%EB%B3%80%EA%B2%BD%ED%95%98%EC%A7%80-%EB%A7%90-%EA%B2%83</guid>
            <pubDate>Fri, 14 Aug 2026 04:54:10 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p>딕셔너리를 돌면서 조건에 맞는 항목에 파생 키를 하나씩 붙이는 코드를 짜다가 <code>RuntimeError: dictionary changed size during iteration</code>을 만난 적이 있을겁니다. 논리적으로는 아무 문제가 없어 보이는데 인터프리터가 갑자기 에러를 내뱉는 순간입니다.</p>
<p>더 곤란한 점은 이 규칙이 일관되지 않다는 것입니다. 크기를 바꾸면 예외가 터지지만, 기존 키의 <strong>값만</strong> 바꾸면 조용히 통과합니다. 리스트는 한술 더 떠서 예외조차 던지지 않고 원소를 슬쩍 건너뛴 결과를 돌려줍니다. 이터레이터(iterator)는 내부 인덱스나 버전 카운터로 위치를 추적하는데, 순회 도중 컨테이너가 흔들리면 그 기준점이 어긋나기 때문입니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python"># 이렇게 짜면 어떻게 될까?
counters = {&#39;apple&#39;: 3, &#39;banana&#39;: 1}

for key, count in counters.items():
    if count &gt; 1:
        counters[f&#39;{key}_big&#39;] = True  # RuntimeError 발생

numbers = [1, 2, 2, 3]

for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)  # 예외 없이 조용히 실패

print(numbers)  # [1, 2, 3] — 짝수 2가 하나 남아 있습니다</code></pre>
<p>딕셔너리는 예외로 알려 주기라도 하지만, 리스트는 원소를 지우는 순간 뒤쪽 항목이 앞으로 당겨지고 이터레이터의 인덱스는 그대로 전진하므로 한 칸을 통째로 건너뜁니다. 테스트 데이터가 짧으면 우연히 맞는 답이 나와서 배포 후에야 발각되는 유형의 버그입니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<pre><code class="language-python"># 이렇게 바꾸면
counters = {&#39;apple&#39;: 3, &#39;banana&#39;: 1}
updates = {}

for key, count in counters.items():  # 원본은 읽기만 합니다
    if count &gt; 1:
        updates[f&#39;{key}_big&#39;] = True

counters.update(updates)  # 순회가 끝난 뒤 한 번에 반영합니다

numbers = [1, 2, 2, 3]
numbers[:] = [n for n in numbers if n % 2 != 0]  # 새 리스트로 통째 교체합니다

print(numbers)  # [1, 3]</code></pre>
<p>읽기 단계와 쓰기 단계를 분리한 것이 핵심입니다. 순회 대상이 고정되므로 결과가 항상 예측 가능하고, 조건이 복잡해져도 로직을 그대로 확장할 수 있습니다. 안전성과 가독성을 동시에 챙기는 방식입니다. 다만 컨테이너가 아주 크다면 <code>list(d.items())</code>처럼 복사본을 뜨는 비용이 부담이 될 수 있으니, 이때는 변경분만 모으는 캐시 방식을 택하는 것이 좋습니다.</p>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li>순회 중에 컨테이너의 크기를 바꾸면 딕셔너리와 집합은 <code>RuntimeError</code>를 던집니다.</li>
<li>기존 키의 값만 바꾸는 경우에는 예외가 발생하지 않아 규칙이 일관되지 않게 느껴집니다.</li>
<li>리스트는 예외 없이 원소를 건너뛰므로 오히려 더 위험합니다.</li>
<li>가장 안전한 선택은 순회 도중 원본을 전혀 건드리지 않는 것입니다.</li>
<li>변경이 꼭 필요하다면 복사본을 순회하거나 변경분을 모아 두었다가 나중에 한 번에 적용합니다.</li>
<li>데이터 크기와 성능 제약에 따라 복사와 캐시 중 무엇이 유리한지 측정한 뒤 결정해야 합니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>세션이나 캐시 정리 로직이 대표적입니다. 만료된 토큰을 지우려고 <code>for key in sessions:</code> 안에서 <code>del sessions[key]</code>를 호출하면 곧장 터지므로, 만료 키를 리스트에 모아 두었다가 순회가 끝난 뒤 삭제하는 형태로 작성해야 합니다.</p>
<p>배치 파이프라인의 검증 단계에서도 유용합니다. 레코드를 훑으면서 유효하지 않은 항목을 걸러 낼 때, 원본 리스트에서 하나씩 <code>remove</code>하는 대신 컴프리헨션(comprehension)으로 통과분과 실패분을 각각 새 리스트로 만들면 누락 없이 두 갈래를 모두 확보할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인자에 대해 이터레이션할 때는 방어적으로 접근하기]]></title>
            <link>https://velog.io/@tasker_dev/%EC%9D%B8%EC%9E%90%EC%97%90-%EB%8C%80%ED%95%B4-%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%EC%85%98%ED%95%A0-%EB%95%8C%EB%8A%94-%EB%B0%A9%EC%96%B4%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%A0%91%EA%B7%BC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tasker_dev/%EC%9D%B8%EC%9E%90%EC%97%90-%EB%8C%80%ED%95%B4-%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%EC%85%98%ED%95%A0-%EB%95%8C%EB%8A%94-%EB%B0%A9%EC%96%B4%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%A0%91%EA%B7%BC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 13 Aug 2026 07:11:42 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p>리스트를 받아 잘 동작하던 함수가 제너레이터(generator)를 넘기는 순간 빈 결과를 뱉는 상황을 만나신 적 있을겁니다. 예외도 없고 로그도 조용한데 결과만 비어 있는 이 증상은, 함수가 인자를 두 번 이상 순회할 때 벌어집니다.</p>
<p>원인은 이터레이터(iterator)가 한 번만 소비되는 일회용 자원이기 때문입니다. 파이썬의 이터레이터 프로토콜에서 리스트 같은 컨테이너는 <code>__iter__</code>가 호출될 때마다 새 이터레이터를 만들어 주지만, 이터레이터 자신은 <code>__iter__</code>에서 자기 자신을 돌려줍니다. 그래서 이미 끝까지 소비된 이터레이터를 다시 순회하면 남은 값이 없어 즉시 종료됩니다. 문제는 이 상황에서 <code>StopIteration</code>이 예외로 드러나지 않고 &quot;원소가 없는 정상적인 순회&quot;로 처리된다는 점입니다. 조용히 잘못되는 코드가 가장 나쁜 코드입니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python">def normalize(numbers):
    total = sum(numbers)  # 여기서 이터레이터를 끝까지 소비합니다
    result = []

    for value in numbers:  # 남은 값이 없어 한 번도 돌지 않습니다
        result.append(100 * value / total)

    return result


def read_visits(data_path):
    with open(data_path) as f:
        for line in f:
            yield int(line)


visits = [15, 35, 80]
print(normalize(visits))  # [11.53..., 26.92..., 61.53...]

it = read_visits(&#39;my_numbers.txt&#39;)
print(normalize(it))  # [] - 예외 없이 빈 리스트가 나옵니다</code></pre>
<p>리스트로 테스트할 때는 완벽하게 동작하다가, 파일이 커져 제너레이터로 바꾸는 순간 무너집니다. 함수 시그니처만 봐서는 &quot;이 인자를 두 번 읽습니다&quot;라는 사실을 알 방법이 없다는 점이 특히 곤란합니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<p>호출할 때마다 새 이터레이터를 만들어 주는 컨테이너를 직접 정의하는 것이 가장 깔끔한 해법입니다. <code>__iter__</code>를 제너레이터로 구현하면 클래스 하나로 끝납니다.</p>
<pre><code class="language-python">class ReadVisits:
    def __init__(self, data_path):
        self.data_path = data_path

    def __iter__(self):
        # 순회할 때마다 새 제너레이터가 만들어집니다
        with open(self.data_path) as f:
            for line in f:
                yield int(line)


visits = ReadVisits(&#39;my_numbers.txt&#39;)
print(normalize(visits))  # 몇 번을 호출해도 같은 결과가 나옵니다</code></pre>
<p>인자를 리스트로 통째로 복사하는 방법도 있지만, 그러면 애초에 제너레이터를 쓴 이유인 메모리 절약이 사라집니다. 반면 이 방식은 파일을 두 번 읽는 대신 메모리 사용량을 낮게 유지합니다.</p>
<p>여기에 더해 잘못된 입력을 일찍 걷어내는 방어 코드를 두면 좋습니다.</p>
<pre><code class="language-python">from collections.abc import Iterator


def normalize_defensive(numbers):
    if isinstance(numbers, Iterator):  # iter(numbers) is numbers 검사와 같은 의미입니다
        raise TypeError(&#39;컨테이너를 넘겨야 합니다&#39;)

    total = sum(numbers)

    return [100 * value / total for value in numbers]</code></pre>
<p>빈 리스트를 반환하며 조용히 실패하는 대신, 호출한 쪽에서 즉시 <code>TypeError</code>를 받게 됩니다. 버그를 발견하는 지점이 사고가 난 곳으로 앞당겨진다는 점에서 안전성이 크게 올라갑니다.</p>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li>인자를 두 번 이상 순회하는 함수는 이터레이터를 받으면 조용히 잘못된 결과를 냅니다.</li>
<li>이터레이터는 소진되어도 예외를 던지지 않고 빈 순회처럼 끝나므로 발견이 늦어집니다.</li>
<li><code>__iter__</code>를 제너레이터로 구현한 컨테이너 클래스를 만들면 매번 새 이터레이터를 얻습니다.</li>
<li>어떤 값이 이터레이터인지는 <code>iter(x) is x</code>로 확인하거나 <code>collections.abc.Iterator</code>로 검사합니다.</li>
<li>방어 코드를 넣어 이터레이터가 들어오면 <code>TypeError</code>를 던지는 편이 안전합니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>대용량 로그나 CSV를 여러 번 훑어야 하는 배치 작업에서 바로 쓰입니다. 전체 합계를 구한 뒤 각 행의 비율을 계산하는 집계 작업이 대표적이며, 파일 경로를 감싼 컨테이너 클래스를 하나 만들어 두면 메모리를 아끼면서도 순회 횟수 제약에서 자유로워집니다.</p>
<p>공용 유틸리티 함수를 작성할 때도 방어 검사를 습관화하면 좋습니다. 다른 팀원이 무심코 제너레이터 식을 넘겼을 때 빈 결과 대신 명확한 예외를 받게 되므로, 원인을 추적하는 시간이 몇 시간에서 몇 초로 줄어듭니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[루프 종료후 for 루프 변수를 사용하지 않기]]></title>
            <link>https://velog.io/@tasker_dev/%EB%A3%A8%ED%94%84-%EC%A2%85%EB%A3%8C%ED%9B%84-for-%EB%A3%A8%ED%94%84-%EB%B3%80%EC%88%98%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EA%B8%B0</link>
            <guid>https://velog.io/@tasker_dev/%EB%A3%A8%ED%94%84-%EC%A2%85%EB%A3%8C%ED%9B%84-for-%EB%A3%A8%ED%94%84-%EB%B3%80%EC%88%98%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EA%B8%B0</guid>
            <pubDate>Thu, 13 Aug 2026 06:51:45 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p>파이썬의 <code>for</code> 루프 변수는 루프 안에서만 사는 존재가 아닙니다. 루프가 끝난 뒤에도 마지막 값을 그대로 품은 채 바깥 스코프에 남아 있습니다. C나 자바에서 블록 안에 선언한 변수가 블록을 벗어나면 사라지는 것과는 정반대의 동작입니다.</p>
<p>이 성질을 알고 나면 유혹이 따라옵니다. 조건에 맞는 값을 찾을 때까지 루프를 돌리고 <code>break</code>로 빠져나온 뒤, 루프 변수를 그대로 결과처럼 쓰는 코드가 자연스럽게 나옵니다. 별도의 변수를 선언하지 않아도 되니 짧아 보이기까지 합니다. 하지만 이 코드는 두 가지 상황에서 무너집니다. 순회할 대상이 비어 있으면 변수 자체가 만들어지지 않아 <code>NameError</code>가 나고, 조건에 맞는 값이 하나도 없으면 마지막 원소가 마치 정답인 것처럼 반환됩니다. 앞의 경우는 그래도 요란하게 터지니 다행이지만, 뒤의 경우는 조용히 틀린 값을 흘려보냅니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python">def find_first_negative(numbers):
    for number in numbers:
        if number &lt; 0:
            break

    return number  # 루프 변수를 결과로 재활용합니다


print(find_first_negative([1, 3, -5, 7]))  # -5  (여기까지는 잘 동작합니다)
print(find_first_negative([1, 3, 5, 7]))   # 7   (음수가 없는데 7을 반환합니다)
print(find_first_negative([]))             # UnboundLocalError</code></pre>
<p>정상 경로에서만 맞고 나머지 두 경로에서는 각각 다른 방식으로 실패합니다. 특히 두 번째 호출은 예외도 없이 잘못된 값을 반환하기 때문에, 문제가 드러나는 시점은 이 함수를 호출한 곳이 아니라 한참 뒤의 어딘가입니다. 테스트 데이터에 음수가 늘 들어 있었다면 배포 후에야 발견될 종류의 버그입니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<pre><code class="language-python">def find_first_negative(numbers):
    for number in numbers:
        if number &lt; 0:
            return number  # 찾은 즉시 반환하므로 루프 밖에서 쓸 일이 없습니다

    return None  # 못 찾았을 때의 값을 명시합니다


print(find_first_negative([1, 3, -5, 7]))  # -5
print(find_first_negative([1, 3, 5, 7]))   # None
print(find_first_negative([]))             # None</code></pre>
<p>세 경로가 모두 명확해졌습니다. &quot;못 찾았다&quot;는 상태를 <code>None</code>으로 못 박아 두었기 때문에 호출하는 쪽에서도 처리해야 할 경우의 수가 눈에 보입니다. 함수로 빼기 어려운 상황이라면 결과를 담을 변수를 루프 앞에서 미리 초기화하는 방식으로 같은 안전성을 얻을 수 있습니다.</p>
<p>참고로 이 누수는 루프에서만 일어납니다. 컴프리헨션(comprehension)과 제너레이터 식(generator expression)은 자기만의 스코프를 가지기 때문에 변수가 바깥으로 새지 않습니다.</p>
<pre><code class="language-python">for i in range(3):
    pass

print(i)  # 2  - 루프 변수는 밖에 남습니다

squares = [j * j for j in range(3)]
print(j)  # NameError - 컴프리헨션 변수는 남지 않습니다</code></pre>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li><code>for</code> 루프 변수는 루프가 끝난 뒤에도 마지막 값을 유지한 채 바깥 스코프에 남습니다.</li>
<li>순회 대상이 비어 있으면 변수가 아예 만들어지지 않아 실행 시점에 예외가 발생합니다.</li>
<li><code>break</code> 없이 루프가 끝난 경우와 조건을 만족한 경우를 루프 변수만으로는 구분할 수 없습니다.</li>
<li>탐색 결과가 필요하다면 함수로 감싸 즉시 반환하거나 별도의 변수를 미리 초기화합니다.</li>
<li>컴프리헨션과 제너레이터 식의 변수는 자체 스코프를 가지므로 바깥으로 새지 않습니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>설정 파일에서 조건에 맞는 첫 항목을 고르는 코드가 대표적인 위험 지대입니다. 여러 데이터베이스 후보 중 살아 있는 첫 연결을 선택하는 로직 같은 것이 여기에 해당하며, <code>select_healthy_host()</code>처럼 이름을 붙여 함수로 빼고 실패 시 <code>None</code>을 반환하도록 만들면 호출부에서 예외 처리를 빠뜨릴 여지가 줄어듭니다.</p>
<p>리뷰 관점에서도 확인하기 쉬운 신호입니다. 루프 블록이 끝난 뒤 들여쓰기가 풀린 자리에서 루프 변수 이름이 다시 등장한다면 일단 의심해 볼 만합니다. 그 자리에 &quot;리스트가 비어 있으면 어떻게 됩니까?&quot;라는 질문 하나만 던져도 대부분 문제가 드러납니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[for나 while 루프 뒤에 else 블록 사용하지 않기]]></title>
            <link>https://velog.io/@tasker_dev/for%EB%82%98-while-%EB%A3%A8%ED%94%84-%EB%92%A4%EC%97%90-else-%EB%B8%94%EB%A1%9D-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EA%B8%B0</link>
            <guid>https://velog.io/@tasker_dev/for%EB%82%98-while-%EB%A3%A8%ED%94%84-%EB%92%A4%EC%97%90-else-%EB%B8%94%EB%A1%9D-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EA%B8%B0</guid>
            <pubDate>Thu, 13 Aug 2026 06:41:56 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p>파이썬은 <code>for</code>와 <code>while</code> 루프 바로 뒤에 <code>else</code> 블록을 붙일 수 있게 허용합니다. 다른 언어에서 보기 힘든 문법이라 처음 발견하면 뭔가 대단한 기능처럼 느껴지지만, 실제 동작을 확인하고 나면 대부분 고개를 갸웃하게 됩니다.</p>
<p>혼란의 원인은 <code>else</code>라는 단어가 파이썬 안에서 이미 두 가지 의미로 쓰이고 있기 때문입니다. <code>if/else</code>에서는 &quot;앞 조건이 거짓일 때&quot;, <code>try/except/else</code>에서는 &quot;예외가 없었을 때&quot; 실행됩니다. 앞의 경험을 그대로 가져오면 루프 뒤의 <code>else</code>는 &quot;루프가 한 번도 안 돌았을 때&quot;로 읽히는데, 실제로는 정반대에 가깝습니다. 루프가 끝까지 돌면서 <code>break</code>를 만나지 않은 경우에 실행됩니다. 문법 하나를 이해하려고 매번 문서를 다시 열어야 한다면, 그 문법은 팀 코드에 들어올 자격이 없습니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python">for i in range(3):
    print(&#39;루프&#39;, i)
else:
    print(&#39;else 블록 실행&#39;)  # break가 없었으므로 실행됩니다

# 더 헷갈리는 경우입니다
for x in []:
    print(&#39;절대 실행되지 않습니다&#39;)
else:
    print(&#39;그런데 이 줄은 실행됩니다&#39;)  # 빈 시퀀스여도 else는 돕니다

while False:
    print(&#39;여기도 실행되지 않습니다&#39;)
else:
    print(&#39;이 줄도 실행됩니다&#39;)  # 조건이 처음부터 거짓이어도 마찬가지입니다</code></pre>
<p>루프가 한 번도 돌지 않았는데 <code>else</code>가 실행된다는 사실이 직관과 정면으로 충돌합니다. 실제 규칙은 &quot;<code>break</code>로 빠져나오지 않았다면 실행된다&quot;이지만, 이 규칙을 코드만 보고 떠올릴 수 있는 사람은 많지 않습니다. 읽는 사람이 규칙을 외워야만 이해되는 코드는 유지보수 비용을 그대로 남에게 떠넘기는 것과 같습니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<pre><code class="language-python">def coprime(a, b):
    &quot;&quot;&quot;두 수가 서로소인지 판별합니다.&quot;&quot;&quot;
    for i in range(2, min(a, b) + 1):
        if a % i == 0 and b % i == 0:
            return False  # 조건을 만난 즉시 결과를 확정합니다

    return True  # 끝까지 돌았다면 서로소입니다


print(coprime(4, 9))   # True
print(coprime(3, 6))   # False</code></pre>
<p>같은 로직을 <code>for/else</code>로 쓰면 루프와 <code>else</code> 사이의 숨은 규칙을 알아야 읽히지만, 헬퍼 함수로 감싸면 <code>return</code> 위치만 보고도 흐름이 즉시 파악됩니다. 결과를 변수에 담아야 하는 상황이라면 플래그 변수를 쓰는 방식도 있습니다.</p>
<pre><code class="language-python">def coprime_alternate(a, b):
    is_coprime = True

    for i in range(2, min(a, b) + 1):
        if a % i == 0 and b % i == 0:
            is_coprime = False
            break

    return is_coprime</code></pre>
<p>두 방식 모두 줄 수는 비슷하지만, 읽는 사람이 파이썬의 예외적인 문법 규칙을 기억할 필요가 없다는 점에서 확실히 낫습니다. 가독성은 짧게 쓰는 것이 아니라 오해할 여지를 줄이는 것으로 얻어집니다.</p>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li>루프 뒤의 <code>else</code> 블록은 <code>break</code>를 만나지 않고 루프가 끝났을 때 실행됩니다.</li>
<li>빈 시퀀스를 순회하거나 조건이 처음부터 거짓이어도 <code>else</code>는 실행됩니다.</li>
<li><code>if/else</code>, <code>try/except/else</code>와 의미가 달라 오해를 부르기 쉽습니다.</li>
<li>같은 동작이 필요하다면 헬퍼 함수의 이른 <code>return</code>이나 플래그 변수로 표현합니다.</li>
<li>표현력이 약간 줄더라도 읽는 사람이 규칙을 외우지 않아도 되는 쪽을 선택합니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>이 Better Way는 &quot;쓸 곳&quot;보다 &quot;걷어낼 곳&quot;을 찾는 항목에 가깝습니다. 목록에서 조건에 맞는 항목을 찾고 없으면 기본값을 쓰는 탐색 코드가 대표적인 후보입니다. 재고에서 사용 가능한 창고를 고르는 로직 같은 것이 여기에 해당하며, <code>find_available_warehouse()</code>처럼 이름 붙은 함수로 빼내고 못 찾았을 때 <code>None</code>을 반환하게 하면 호출부가 훨씬 명확해집니다.</p>
<p>리뷰어 입장에서도 유용한 신호입니다. 코드에서 <code>for ... else</code>를 발견하면 그 자리에 이름이 필요한 함수가 숨어 있을 가능성이 높습니다. 문법을 지적하기보다 &quot;이 루프 전체가 무엇을 판정합니까?&quot;라고 물어보면, 대개 함수 이름이 자연스럽게 나옵니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[여러 이터레이터에 대해 나란히 루프를 수행하려면 zip을 사용하자]]></title>
            <link>https://velog.io/@tasker_dev/%EC%97%AC%EB%9F%AC-%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%ED%84%B0%EC%97%90-%EB%8C%80%ED%95%B4-%EB%82%98%EB%9E%80%ED%9E%88-%EB%A3%A8%ED%94%84%EB%A5%BC-%EC%88%98%ED%96%89%ED%95%98%EB%A0%A4%EB%A9%B4-zip%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90</link>
            <guid>https://velog.io/@tasker_dev/%EC%97%AC%EB%9F%AC-%EC%9D%B4%ED%84%B0%EB%A0%88%EC%9D%B4%ED%84%B0%EC%97%90-%EB%8C%80%ED%95%B4-%EB%82%98%EB%9E%80%ED%9E%88-%EB%A3%A8%ED%94%84%EB%A5%BC-%EC%88%98%ED%96%89%ED%95%98%EB%A0%A4%EB%A9%B4-zip%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90</guid>
            <pubDate>Thu, 13 Aug 2026 06:32:48 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p>실무에서 다루는 리스트는 혼자 다니는 법이 거의 없습니다. 사용자 이름 리스트가 있으면 그 사용자의 점수 리스트가 따로 있고, 컴프리헨션(comprehension)으로 원본 리스트를 가공해 파생 리스트를 하나 더 만드는 일도 흔합니다. 이렇게 만들어진 두 리스트는 &quot;같은 인덱스가 같은 대상을 가리킨다&quot;는 암묵적인 약속으로 묶여 있습니다.</p>
<p>문제는 이 약속을 지키는 책임이 전부 개발자에게 있다는 점입니다. 두 리스트를 인덱스로 오가며 순회하는 코드는 <code>names[i]</code>와 <code>counts[i]</code>가 계속 등장해 시선을 분산시키고, 어느 한쪽에만 원소를 추가하는 순간 짝이 어긋나기 시작합니다. 더 곤란한 것은 이런 어긋남이 예외가 아니라 잘못된 결과로 나타난다는 사실입니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python">names = [&#39;세실리아&#39;, &#39;리제&#39;, &#39;마리엔&#39;]
counts = [len(n) for n in names]

longest_name = None
max_count = 0

for i in range(len(names)):
    count = counts[i]      # 인덱스로 다른 리스트를 참조합니다
    if count &gt; max_count:
        longest_name = names[i]  # 여기서도 또 한 번 참조합니다
        max_count = count

print(longest_name)  # 세실리아</code></pre>
<p><code>enumerate</code>로 바꿔도 <code>counts[i]</code> 한 줄은 그대로 남습니다. 루프의 본질은 &quot;이름과 길이를 짝지어 보는 것&quot;인데, 코드는 여전히 인덱스를 중심으로 굴러가고 있습니다. 리스트가 셋으로 늘어나면 이 참조 줄도 함께 늘어납니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<pre><code class="language-python">names = [&#39;세실리아&#39;, &#39;리제&#39;, &#39;마리엔&#39;]
counts = [len(n) for n in names]

longest_name = None
max_count = 0

for name, count in zip(names, counts):  # 짝을 지어 튜플로 넘겨줍니다
    if count &gt; max_count:
        longest_name = name
        max_count = count

print(longest_name)  # 세실리아</code></pre>
<p>인덱스 변수가 사라지면서 루프 본문이 실제로 하려던 일만 남았습니다. <code>zip</code>은 지연 계산 제너레이터(generator)라 원본을 복사하지 않고 각 이터레이터(iterator)에서 값을 하나씩만 꺼내 쓰므로, 아주 긴 입력에도 메모리 부담 없이 쓸 수 있습니다.</p>
<p>한 가지 함정은 있습니다. <code>zip</code>은 넘겨받은 이터레이터 중 가장 짧은 쪽이 끝나면 그 자리에서 조용히 멈춥니다.</p>
<pre><code class="language-python">from itertools import zip_longest

names = [&#39;세실리아&#39;, &#39;리제&#39;, &#39;마리엔&#39;]
counts = [len(n) for n in names]

names.append(&#39;로절린드&#39;)  # counts는 갱신하지 않았습니다

for name, _ in zip(names, counts):
    print(name)  # 로절린드가 경고 없이 빠집니다

for name, count in zip_longest(names, counts):
    print(f&#39;{name}: {count}&#39;)  # 로절린드: None</code></pre>
<p>길이가 같다고 확신할 수 있을 때만 <code>zip</code>을 쓰고, 그렇지 않다면 빠진 자리를 <code>None</code>으로 채워 주는 <code>zip_longest</code>를 쓰는 것이 안전합니다.</p>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li><code>zip</code>은 여러 이터레이터를 묶어 원소들의 튜플을 만들어 주는 내장 함수입니다.</li>
<li>지연 계산 제너레이터이므로 아주 긴 입력에도 메모리를 크게 쓰지 않습니다.</li>
<li>길이가 다른 이터레이터를 넘기면 가장 짧은 쪽에 맞춰 아무 경고 없이 끝납니다.</li>
<li>길이가 다를 수 있는 상황에서는 <code>itertools.zip_longest</code>를 사용해야 합니다.</li>
<li>인덱스로 다른 리스트를 참조하는 루프를 보면 <code>zip</code>으로 바꿀 수 있는지 살펴봅니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>머신러닝 코드에서 예측값과 정답 레이블을 비교할 때 가장 자주 만나게 됩니다. <code>for pred, label in zip(predictions, labels)</code> 형태로 두면 인덱스 실수로 한 칸씩 밀려 정확도가 이상하게 나오는 사고를 막을 수 있습니다. 다만 배치 크기가 맞는지는 루프 전에 <code>assert len(predictions) == len(labels)</code>로 못 박아 두는 편이 좋습니다.</p>
<p>CSV 헤더와 데이터 행을 딕셔너리로 합칠 때도 유용합니다. <code>dict(zip(header, row))</code> 한 줄이면 컬럼 순서가 바뀌어도 대응되는 코드가 되고, 컬럼 개수가 다른 불량 행을 잡아내야 한다면 <code>zip_longest</code>로 바꿔 <code>None</code>이 섞인 행만 걸러 내는 검증 로직을 만들 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[range보다는 enumerate를 사용하기]]></title>
            <link>https://velog.io/@tasker_dev/range%EB%B3%B4%EB%8B%A4%EB%8A%94-enumerate%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@tasker_dev/range%EB%B3%B4%EB%8B%A4%EB%8A%94-enumerate%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 13 Aug 2026 06:22:05 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-이게-왜-문제인가">🤔 이게 왜 문제인가</h2>
<p><code>range</code>는 정수 집합을 순회할 때 쓰라고 만든 도구입니다. 난수를 여덟 번 뽑거나 0부터 9까지 더하는 코드라면 <code>range</code>가 정확히 제자리를 찾은 셈입니다.</p>
<p>문제는 이 도구가 리스트 순회에까지 끌려 나올 때 생깁니다. &quot;인덱스도 같이 필요하다&quot;는 이유로 <code>range(len(some_list))</code>를 쓰는 순간, 코드는 갑자기 세 가지 일을 동시에 하게 됩니다. 리스트의 길이를 재고, 그 길이만큼 정수를 만들고, 그 정수로 다시 원소를 꺼냅니다. 정작 하고 싶었던 일은 &quot;원소를 하나씩 보는 것&quot; 하나뿐인데 말입니다. 다른 언어에서 넘어온 분들이 가장 오래 끌고 오는 습관이기도 합니다.</p>
<h2 id="💣-흔한-실수">💣 흔한 실수</h2>
<pre><code class="language-python">flavor_list = [&#39;바닐라&#39;, &#39;초콜릿&#39;, &#39;피칸&#39;, &#39;딸기&#39;]

for i in range(len(flavor_list)):
    flavor = flavor_list[i]  # 인덱스로 다시 꺼내는 한 줄이 매번 따라붙습니다
    print(f&#39;{i + 1}: {flavor}&#39;)</code></pre>
<p>읽는 사람은 <code>flavor_list[i]</code>를 보고 나서야 이 루프가 리스트 순회라는 사실을 알게 됩니다. 인덱스 자체는 출력에만 쓰이는데 루프 구조 전체가 인덱스를 중심으로 짜여 있는 셈입니다. 원소를 꺼내는 줄 하나가 더 필요하다는 점도 군더더기입니다.</p>
<h2 id="✅-파이썬다운-방법">✅ 파이썬다운 방법</h2>
<pre><code class="language-python">flavor_list = [&#39;바닐라&#39;, &#39;초콜릿&#39;, &#39;피칸&#39;, &#39;딸기&#39;]

for i, flavor in enumerate(flavor_list):
    print(f&#39;{i + 1}: {flavor}&#39;)

# 시작값을 두 번째 인자로 넘기면 +1 보정도 사라집니다
for i, flavor in enumerate(flavor_list, 1):
    print(f&#39;{i}: {flavor}&#39;)</code></pre>
<p>한 줄이 줄어든 것보다 중요한 것은 의도가 드러난다는 점입니다. <code>enumerate</code>가 보이는 순간 &quot;인덱스와 값을 함께 쓰는 루프&quot;라는 사실이 즉시 읽힙니다. 시작값 인자를 활용하면 <code>i + 1</code> 같은 보정이 사라져 실수의 여지도 함께 줄어듭니다.</p>
<p>성능 면에서도 손해가 없습니다. <code>enumerate</code>는 원본을 통째로 복사하지 않고 지연 계산 제너레이터(generator)로 감싸서, 요청받을 때마다 인덱스와 값의 쌍을 하나씩 만들어 냅니다.</p>
<pre><code class="language-python">it = enumerate(flavor_list)

print(next(it))  # (0, &#39;바닐라&#39;)
print(next(it))  # (1, &#39;초콜릿&#39;)</code></pre>
<p>덕분에 길이를 미리 알 수 없는 이터레이터(iterator)나 파일 객체에도 그대로 쓸 수 있습니다. <code>range(len(...))</code>은 <code>len</code>을 지원하는 대상에만 쓸 수 있으니, 적용 범위 자체가 다릅니다.</p>
<h2 id="📎-기억할-것">📎 기억할 것</h2>
<ul>
<li><code>enumerate</code>를 쓰면 인덱스와 원소를 한 번에 받아 루프가 간결해집니다.</li>
<li><code>range</code> 위에서 인덱스로 원소를 다시 꺼내는 패턴은 대부분 <code>enumerate</code>로 바꿀 수 있습니다.</li>
<li>두 번째 인자로 시작값을 넘길 수 있어 1부터 세는 출력에 편리합니다.</li>
<li><code>enumerate</code>는 지연 계산 제너레이터이므로 메모리를 추가로 크게 쓰지 않습니다.</li>
<li>길이를 모르는 이터레이터에도 사용할 수 있다는 점이 <code>range</code>와의 결정적 차이입니다.</li>
</ul>
<h2 id="🛠-실무에서-어디-쓸까">🛠 실무에서 어디 쓸까</h2>
<p>대용량 로그 파일을 한 줄씩 처리하면서 오류 위치를 알려 줘야 하는 스크립트에서 특히 잘 맞습니다. <code>for line_no, line in enumerate(f, 1)</code>로 열어 두면 파일 전체를 메모리에 올리지 않고도 사람이 읽는 방식 그대로의 줄 번호를 예외 메시지에 담을 수 있습니다.</p>
<p>배치 작업의 진행률 로깅에도 유용합니다. <code>for i, item in enumerate(items, 1)</code> 뒤에 <code>if i % 1000 == 0</code> 조건을 두면 별도 카운터 변수를 만들고 증가시키는 코드 없이 진행 상황을 남길 수 있습니다. 루프 밖에서 관리하는 상태가 하나 줄어드는 것만으로도 실수가 눈에 띄게 줄어듭니다.</p>
]]></description>
        </item>
    </channel>
</rss>