<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>woojin-archive.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Mon, 04 May 2026 08:07:35 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>woojin-archive.log</title>
            <url>https://velog.velcdn.com/images/woojin-archive/profile/585a0e8c-b3e9-4dba-a709-a4b35ec32746/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. woojin-archive.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/woojin-archive" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[이코테] 3강. DFS & BFS]]></title>
            <link>https://velog.io/@woojin-archive/%EC%9D%B4%EC%BD%94%ED%85%8C-3%EA%B0%95.-DFS-BFS</link>
            <guid>https://velog.io/@woojin-archive/%EC%9D%B4%EC%BD%94%ED%85%8C-3%EA%B0%95.-DFS-BFS</guid>
            <pubDate>Mon, 04 May 2026 08:07:35 GMT</pubDate>
            <description><![CDATA[<h1 id="3강-dfs--bfs">3강. DFS &amp; BFS</h1>
<h2 id="📌-탐색-알고리즘이란">📌 탐색 알고리즘이란?</h2>
<blockquote>
<p><strong>많은 양의 데이터 중에서 원하는 데이터를 찾는 과정</strong></p>
</blockquote>
<p>대표적인 그래프 탐색 알고리즘으로 <strong>DFS</strong>와 <strong>BFS</strong>가 있으며, 코딩 테스트에서 매우 자주 등장하므로 반드시 숙지해야 한다.</p>
<hr>
<h2 id="🗂️-자료구조-기본기">🗂️ 자료구조 기본기</h2>
<h3 id="스택-stack">스택 (Stack)</h3>
<ul>
<li><strong>선입후출 (LIFO)</strong> — 나중에 들어온 것이 먼저 나온다.</li>
<li>Python에서는 리스트의 <code>append()</code> / <code>pop()</code>으로 구현한다.</li>
</ul>
<pre><code class="language-python">stack = []

stack.append(5)
stack.append(2)
stack.append(3)
stack.append(7)
stack.pop()      # 7 제거
stack.append(1)
stack.append(4)
stack.pop()      # 4 제거

print(stack[::-1])  # 최상단 원소부터 출력: [1, 3, 2, 5]
print(stack)        # 최하단 원소부터 출력: [5, 2, 3, 1]</code></pre>
<h3 id="큐-queue">큐 (Queue)</h3>
<ul>
<li><strong>선입선출 (FIFO)</strong> — 먼저 들어온 것이 먼저 나온다.</li>
<li>Python에서는 <code>collections.deque</code>를 사용한다. (리스트보다 효율적)</li>
</ul>
<pre><code class="language-python">from collections import deque

queue = deque()

queue.append(5)
queue.append(2)
queue.append(3)
queue.append(7)
queue.popleft()  # 5 제거
queue.append(1)
queue.append(4)
queue.popleft()  # 2 제거

print(queue)         # 먼저 들어온 순서대로: deque([3, 7, 1, 4])
queue.reverse()
print(queue)         # 나중에 들어온 원소부터: deque([4, 1, 7, 3])</code></pre>
<hr>
<h2 id="🔄-재귀-함수">🔄 재귀 함수</h2>
<blockquote>
<p><strong>자기 자신을 다시 호출하는 함수</strong></p>
</blockquote>
<h3 id="종료-조건이-반드시-필요하다">종료 조건이 반드시 필요하다</h3>
<p>종료 조건 없이 재귀를 호출하면 최대 재귀 깊이를 초과해 에러가 발생한다.</p>
<pre><code class="language-python">def recursive_function(i):
    if i == 100:  # 종료 조건
        return
    print(i, &#39;번째 재귀함수에서&#39;, i+1, &#39;번째 재귀함수를 호출합니다.&#39;)
    recursive_function(i + 1)
    print(i, &#39;번째 재귀함수를 종료합니다.&#39;)

recursive_function(1)</code></pre>
<h3 id="팩토리얼-비교-예시">팩토리얼 비교 예시</h3>
<pre><code class="language-python"># 반복문으로 구현
def factorial_iterative(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

# 재귀로 구현
def factorial_recursive(n):
    if n &lt;= 1:
        return 1
    return n * factorial_recursive(n - 1)

print(&#39;반복적으로 구현:&#39;, factorial_iterative(5))  # 120
print(&#39;재귀적으로 구현:&#39;, factorial_recursive(5))  # 120</code></pre>
<h3 id="유클리드-호제법-최대공약수">유클리드 호제법 (최대공약수)</h3>
<blockquote>
<p>A를 B로 나눈 나머지를 R이라 할 때, <strong>GCD(A, B) = GCD(B, R)</strong></p>
</blockquote>
<pre><code class="language-python">def gcd(a, b):
    if a % b == 0:
        return b
    else:
        return gcd(b, a % b)

print(gcd(192, 162))  # 6</code></pre>
<h3 id="재귀-함수-유의사항">재귀 함수 유의사항</h3>
<ul>
<li>복잡한 알고리즘을 간결하게 작성할 수 있지만, 가독성이 떨어질 수 있다.</li>
<li>모든 재귀 함수는 반복문으로 동일하게 구현 가능하다.</li>
<li>함수 호출 시 스택 프레임에 쌓이기 때문에, <strong>스택이 필요한 경우 재귀로 대체</strong>하는 경우가 많다.</li>
</ul>
<hr>
<h2 id="🔍-dfs-깊이-우선-탐색">🔍 DFS (깊이 우선 탐색)</h2>
<blockquote>
<p><strong>그래프에서 깊은 부분을 우선적으로 탐색하는 알고리즘</strong></p>
</blockquote>
<h3 id="동작-과정">동작 과정</h3>
<ol>
<li>탐색 시작 노드를 스택에 삽입하고 방문 처리한다.</li>
<li>스택 최상단 노드에 방문하지 않은 인접 노드가 있으면 → 스택에 넣고 방문 처리</li>
<li>방문하지 않은 인접 노드가 없으면 → 스택에서 최상단 노드를 꺼낸다.</li>
<li>더 이상 반복할 수 없을 때까지 2~3을 반복한다.</li>
</ol>
<h3 id="핵심">핵심</h3>
<ul>
<li><strong>스택</strong> 또는 <strong>재귀 함수</strong>를 이용한다.</li>
<li>실제 구현에서는 재귀 함수가 더 간결하다.</li>
</ul>
<pre><code class="language-python">def dfs(graph, v, visited):
    visited[v] = True
    print(v, end=&#39; &#39;)

    for i in graph[v]:
        if not visited[i]:
            dfs(graph, i, visited)

graph = [
    [],
    [2, 3, 8],  # 1번 노드와 연결
    [1, 7],
    [1, 4, 5],
    [3, 5],
    [3, 4],
    [7],
    [2, 6, 8],
    [1, 7]
]

visited = [False] * 9

dfs(graph, 1, visited)
# 출력: 1 2 7 6 8 3 4 5</code></pre>
<hr>
<h2 id="🔍-bfs-너비-우선-탐색">🔍 BFS (너비 우선 탐색)</h2>
<blockquote>
<p><strong>그래프에서 가까운 노드부터 우선적으로 탐색하는 알고리즘</strong></p>
</blockquote>
<h3 id="동작-과정-1">동작 과정</h3>
<ol>
<li>탐색 시작 노드를 큐에 삽입하고 방문 처리한다.</li>
<li>큐에서 노드를 꺼낸 뒤, 인접한 방문하지 않은 노드를 모두 큐에 삽입하고 방문 처리한다.</li>
<li>더 이상 반복할 수 없을 때까지 2를 반복한다.</li>
</ol>
<h3 id="핵심-1">핵심</h3>
<ul>
<li><strong>큐</strong> 자료구조를 이용한다.</li>
<li><strong>최단 거리</strong> 문제에 유리하다. (모든 간선의 비용이 동일할 때)</li>
</ul>
<pre><code class="language-python">from collections import deque

def bfs(graph, start, visited):
    queue = deque([start])
    visited[start] = True

    while queue:
        v = queue.popleft()
        print(v, end=&#39; &#39;)

        for i in graph[v]:
            if not visited[i]:
                queue.append(i)
                visited[i] = True

graph = [
    [],
    [2, 3, 8],
    [1, 7],
    [1, 4, 5],
    [3, 5],
    [3, 4],
    [7],
    [2, 6, 8],
    [1, 7]
]

visited = [False] * 9

bfs(graph, 1, visited)
# 출력: 1 2 3 8 7 4 5 6</code></pre>
<hr>
<h2 id="🆚-dfs-vs-bfs-비교">🆚 DFS vs BFS 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>DFS</th>
<th>BFS</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>
<hr>
<h2 id="🧊-문제-1-음료수-얼려-먹기">🧊 문제 1. 음료수 얼려 먹기</h2>
<h3 id="문제">문제</h3>
<p>N×M 얼음 틀에서 <code>0</code>은 구멍, <code>1</code>은 칸막이다. 상하좌우로 연결된 <code>0</code>끼리 하나의 아이스크림이 된다. <strong>총 아이스크림의 개수</strong>를 구하라.</p>
<h3 id="아이디어-dfs">아이디어 (DFS)</h3>
<ol>
<li>모든 칸을 순서대로 확인하며 <code>0</code>인 칸에서 DFS를 수행한다.</li>
<li>DFS 중에 방문한 <code>0</code> 칸을 <code>1</code>로 바꿔 재방문을 방지한다.</li>
<li>DFS가 한 번 성공할 때마다 아이스크림 개수를 +1 한다.</li>
</ol>
<pre><code class="language-python">def dfs(x, y):
    # 범위를 벗어나면 종료
    if x &lt;= -1 or x &gt;= n or y &lt;= -1 or y &gt;= m:
        return False

    if graph[x][y] == 0:
        graph[x][y] = 1  # 방문 처리
        dfs(x - 1, y)
        dfs(x + 1, y)
        dfs(x, y - 1)
        dfs(x, y + 1)
        return True
    return False

n, m = map(int, input().split())

graph = []
for i in range(n):
    graph.append(list(map(int, input())))

result = 0
for i in range(n):
    for j in range(m):
        if dfs(i, j) == True:
            result += 1

print(result)

# 입력 예시:
# 4 5
# 00110
# 00011
# 11111
# 00000
# 출력: 3</code></pre>
<h3 id="핵심-포인트">핵심 포인트</h3>
<ul>
<li><code>graph[x][y] = 1</code>로 방문을 표시해 별도의 <code>visited</code> 배열 없이도 동작한다.</li>
<li>범위 체크를 먼저 하는 것이 핵심 — 범위 밖이면 <code>False</code>를 반환하고 종료한다.</li>
</ul>
<hr>
<h2 id="🌀-문제-2-미로-탈출">🌀 문제 2. 미로 탈출</h2>
<h3 id="문제-1">문제</h3>
<p>N×M 미로에서 <code>(1,1)</code>에서 출발해 <code>(N,M)</code>까지 이동할 때, <strong>최소 칸의 개수</strong>를 구하라. <code>0</code>은 괴물(이동 불가), <code>1</code>은 이동 가능한 칸이다.</p>
<h3 id="아이디어-bfs">아이디어 (BFS)</h3>
<ul>
<li>BFS는 가까운 노드부터 탐색하므로 <strong>최단 경로를 보장</strong>한다.</li>
<li>방문할 때마다 이전 칸의 거리 + 1을 기록한다.</li>
<li>별도의 거리 배열 없이 <strong>graph 값 자체를 거리로 갱신</strong>한다.</li>
</ul>
<pre><code class="language-python">from collections import deque

def bfs(x, y):
    queue = deque()
    queue.append((x, y))

    while queue:
        x, y = queue.popleft()

        for i in range(4):
            nx = x + dx[i]
            ny = y + dy[i]

            if nx &lt; 0 or nx &gt;= n or ny &lt; 0 or ny &gt;= m:
                continue
            if graph[nx][ny] == 0:  # 벽이면 무시
                continue
            if graph[nx][ny] == 1:  # 처음 방문하는 경우만 기록
                graph[nx][ny] = graph[x][y] + 1
                queue.append((nx, ny))

    return graph[n - 1][m - 1]

n, m = map(int, input().split())

graph = []
for i in range(n):
    graph.append(list(map(int, input())))

dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]

print(bfs(0, 0))

# 입력 예시:
# 3 3
# 110
# 010
# 011
# 출력: 5</code></pre>
<h3 id="핵심-포인트-1">핵심 포인트</h3>
<ul>
<li><code>graph[nx][ny] == 1</code> 조건으로 <strong>처음 방문하는 칸만</strong> 거리를 갱신한다.</li>
<li>이미 거리가 기록된 칸(값 &gt; 1)은 다시 큐에 넣지 않아 중복 탐색을 방지한다.</li>
<li><code>graph[n-1][m-1]</code>을 반환하면 자연스럽게 최단 거리가 나온다.</li>
</ul>
<hr>
<h2 id="📝-정리">📝 정리</h2>
<table>
<thead>
<tr>
<th>문제 유형</th>
<th>알고리즘</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>연결된 구역의 개수</td>
<td>DFS</td>
<td>연결 요소 탐색에 적합</td>
</tr>
<tr>
<td>최단 경로 / 최소 칸</td>
<td>BFS</td>
<td>가까운 노드부터 탐색해 최단 거리 보장</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[이코테] 2강. 그리디 & 구현]]></title>
            <link>https://velog.io/@woojin-archive/%EC%9D%B4%EC%BD%94%ED%85%8C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@woojin-archive/%EC%9D%B4%EC%BD%94%ED%85%8C-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Tue, 28 Apr 2026 08:34:52 GMT</pubDate>
            <description><![CDATA[<h1 id="2강-그리디--구현">2강. 그리디 &amp; 구현</h1>
<h2 id="📌-그리디-알고리즘이란">📌 그리디 알고리즘이란?</h2>
<blockquote>
<p><strong>현재 상황에서 지금 당장 좋은 것만 고르는 방법</strong></p>
</blockquote>
<ul>
<li>문제를 풀기 위한 최소한의 아이디어를 떠올리는 능력이 필요하다.</li>
<li><strong>정당성 분석이 중요</strong> — 단순히 가장 좋아 보이는 선택을 반복해도 최적의 해가 나오는지 검토해야 한다.</li>
<li>일반적인 상황에서는 최적의 해를 보장할 수 없는 경우가 많다.</li>
<li>단, 코딩 테스트에서는 탐욕법으로 얻은 해가 최적의 해가 되는 상황으로 출제된다.</li>
</ul>
<hr>
<h2 id="🪙-문제-1-거스름돈">🪙 문제 1. 거스름돈</h2>
<h3 id="아이디어">아이디어</h3>
<p>큰 단위의 화폐부터 최대한 사용하면 항상 최소 동전 개수를 보장한다.</p>
<pre><code class="language-python">n = 1260
count = 0

array = [500, 100, 50, 10]

for coin in array:
    count += n // coin  # 해당 화폐로 거슬러 줄 수 있는 동전의 개수
    n %= coin

print(count)</code></pre>
<h3 id="시간-복잡도">시간 복잡도</h3>
<ul>
<li>화폐의 종류가 K개일 때 <strong>O(K)</strong></li>
<li>거슬러줘야 하는 금액과 무관하고, <strong>동전의 종류 수에만 영향</strong>을 받는다.</li>
</ul>
<hr>
<h2 id="🔢-문제-2-1이-될-때까지">🔢 문제 2. 1이 될 때까지</h2>
<h3 id="문제">문제</h3>
<p>N과 K가 주어졌을 때, N에서 1을 빼거나 K로 나누는 연산을 반복해 <strong>1로 만드는 최소 연산 횟수</strong>를 구하라.</p>
<h3 id="아이디어-1">아이디어</h3>
<ul>
<li>K로 나누는 것이 1을 빼는 것보다 훨씬 빠르게 N을 줄인다.</li>
<li>따라서 <strong>최대한 많이 나누기를 수행</strong>하는 것이 최적이다.</li>
<li>K로 나누어 떨어지지 않을 때는 나누어 떨어지는 수까지 1씩 뺀다.</li>
</ul>
<h3 id="정당성">정당성</h3>
<p>K가 2 이상이기만 하면, K로 나누는 것이 1을 빼는 것보다 항상 빠르게 N을 줄인다. N은 항상 1에 도달하게 된다.</p>
<pre><code class="language-python">n, k = map(int, input().split())

result = 0

while True:
    # N이 K로 나누어 떨어지는 수가 될 때까지 빼기
    target = (n // k) * k  # K의 배수 중 N보다 작거나 같은 가장 큰 수
    result += (n - target)
    n = target

    # N이 K보다 작을 때 (더 이상 나눌 수 없을 때) 반복문 탈출
    if n &lt; k:
        break

    # K로 나누기
    result += 1
    n //= k

# 마지막으로 남은 수에 대하여 1씩 빼기
result += (n - 1)
print(result)</code></pre>
<h3 id="핵심-포인트">핵심 포인트</h3>
<p><code>target = (n // k) * k</code> → K로 나누어 떨어지는 가장 가까운 수를 한 번에 계산해, 1씩 빼는 횟수를 줄인다.</p>
<hr>
<h2 id="✖️-문제-3-곱하기-혹은-더하기">✖️ 문제 3. 곱하기 혹은 더하기</h2>
<h3 id="문제-1">문제</h3>
<p>숫자로 이루어진 문자열이 주어졌을 때, 각 숫자 사이에 <code>+</code> 또는 <code>×</code>를 넣어 <strong>결과값을 최대</strong>로 만들어라.</p>
<h3 id="아이디어-2">아이디어</h3>
<ul>
<li>대부분의 경우 <code>+</code>보다 <code>×</code>가 더 값을 크게 만든다. (ex. 5+6=11, 5×6=30)</li>
<li>단, 두 수 중 하나라도 <strong>0 또는 1이면 더하기가 유리</strong>하다.</li>
</ul>
<table>
<thead>
<tr>
<th>케이스</th>
<th>더하기</th>
<th>곱하기</th>
<th>선택</th>
</tr>
</thead>
<tbody><tr>
<td>5, 6</td>
<td>11</td>
<td>30</td>
<td>✅ 곱하기</td>
</tr>
<tr>
<td>0, 6</td>
<td>6</td>
<td>0</td>
<td>✅ 더하기</td>
</tr>
<tr>
<td>1, 6</td>
<td>7</td>
<td>6</td>
<td>✅ 더하기</td>
</tr>
</tbody></table>
<pre><code class="language-python">data = input()

result = int(data[0])

for i in range(1, len(data)):
    num = int(data[i])
    if num &lt;= 1 or result &lt;= 1:
        result += num
    else:
        result *= num

print(result)</code></pre>
<hr>
<h2 id="🧙-문제-4-모험가-길드">🧙 문제 4. 모험가 길드</h2>
<h3 id="문제-2">문제</h3>
<p>공포도가 X인 모험가는 반드시 <strong>X명 이상으로 구성된 그룹</strong>에 참여해야 한다. 최대 몇 개의 그룹을 만들 수 있는지 구하라.</p>
<h3 id="아이디어-3">아이디어</h3>
<ul>
<li>그룹 수를 <strong>최대화</strong>하려면 각 그룹을 최대한 <strong>작게</strong> 만들어야 한다.</li>
<li>오름차순 정렬 후 공포도가 낮은 모험가부터 확인한다.</li>
<li>현재 그룹 인원 수 ≥ 현재 모험가의 공포도가 되는 순간 그룹을 결성한다.</li>
</ul>
<pre><code class="language-python">n = int(input())
data = list(map(int, input().split()))
data.sort()

result = 0  # 총 그룹의 수
count = 0   # 현재 그룹에 포함된 모험가의 수

for i in data:
    count += 1
    if count &gt;= i:  # 현재 그룹 인원이 공포도 이상이면 그룹 결성
        result += 1
        count = 0

print(result)</code></pre>
<h3 id="핵심-포인트-1">핵심 포인트</h3>
<p>오름차순 정렬 덕분에 현재 <code>i</code>가 <strong>그룹 내 최대 공포도</strong>임이 보장된다. 따라서 <code>count &gt;= i</code> 조건 하나로 그룹 결성 가능 여부를 판단할 수 있다.</p>
<hr>
<h2 id="📌-구현implementation이란">📌 구현(Implementation)이란?</h2>
<blockquote>
<p><strong>풀이를 떠올리는 것은 쉽지만, 소스코드로 옮기기 어려운 문제</strong></p>
</blockquote>
<ul>
<li>코드가 지나치게 길어지는 문제</li>
<li>실수 연산 및 특정 소수점 자리 출력 문제</li>
<li>문자열을 특정 기준으로 끊어 처리하는 문제</li>
<li>적절한 라이브러리를 찾아 사용해야 하는 문제</li>
</ul>
<blockquote>
<p>💡 시뮬레이션 및 완전 탐색 문제에서는 <strong>2차원 방향 벡터</strong>가 자주 활용된다.</p>
</blockquote>
<pre><code># 상하좌우 방향 벡터
dx = [0, 0, -1, 1]   # 행 변화량 (L, R, U, D)
dy = [-1, 1, 0, 0]   # 열 변화량</code></pre><hr>
<h2 id="🗺️-문제-5-상하좌우">🗺️ 문제 5. 상하좌우</h2>
<h3 id="문제-3">문제</h3>
<p>N×N 격자에서 (1,1)부터 시작해 L/R/U/D 명령어를 따라 이동할 때, 격자를 벗어나는 이동은 무시한다. 최종 위치를 구하라.</p>
<h3 id="아이디어-4">아이디어</h3>
<p>요구사항대로 구현하는 시뮬레이션 문제. 방향 벡터를 리스트로 정의해 이동을 처리한다.</p>
<pre><code class="language-python">n = int(input())
x, y = 1, 1
plans = input().split()

dx = [0, 0, -1, 1]
dy = [-1, 1, 0, 0]
move_types = [&#39;L&#39;, &#39;R&#39;, &#39;U&#39;, &#39;D&#39;]

for plan in plans:
    for i in range(len(move_types)):
        if plan == move_types[i]:
            nx = x + dx[i]
            ny = y + dy[i]
    # 공간을 벗어나는 경우 무시
    if nx &lt; 1 or ny &lt; 1 or nx &gt; n or ny &gt; n:
        continue
    x, y = nx, ny

print(x, y)</code></pre>
<hr>
<h2 id="⏰-문제-6-시각">⏰ 문제 6. 시각</h2>
<h3 id="문제-4">문제</h3>
<p>00:00:00부터 N:59:59까지의 모든 시각 중 <strong>숫자 3이 하나라도 포함된 경우의 수</strong>를 구하라.</p>
<h3 id="아이디어-5">아이디어</h3>
<p>하루는 86,400초. 모든 경우의 수를 직접 탐색하는 <strong>완전 탐색</strong> 문제.</p>
<pre><code class="language-python">h = int(input())

count = 0
for i in range(h + 1):
    for j in range(60):
        for k in range(60):
            if &#39;3&#39; in str(i) + str(j) + str(k):
                count += 1

print(count)</code></pre>
<h3 id="핵심-포인트-2">핵심 포인트</h3>
<p><code>&#39;3&#39; in str(i) + str(j) + str(k)</code> → 시/분/초를 문자열로 합쳐서 &#39;3&#39;이 포함되는지 한 번에 확인한다.</p>
<hr>
<h2 id="♞-문제-7-왕실의-나이트">♞ 문제 7. 왕실의 나이트</h2>
<h3 id="문제-5">문제</h3>
<p>8×8 체스판에서 나이트의 위치가 주어졌을 때, <strong>이동 가능한 경우의 수</strong>를 구하라. 나이트는 L자로만 이동한다.</p>
<h3 id="아이디어-6">아이디어</h3>
<p>나이트의 8가지 이동 방향을 모두 시도하고, 체스판을 벗어나지 않는 경우만 카운트한다.</p>
<pre><code class="language-python">input_data = input()
row = int(input_data[1])
column = int(ord(input_data[0])) - int(ord(&#39;a&#39;)) + 1

# 나이트의 8가지 이동 방향
steps = [(-2,-1), (-1,-2), (1,-2), (2,-1),
         (2,1), (1,2), (-1,2), (-2,1)]

result = 0
for step in steps:
    next_row = row + step[0]
    next_column = column + step[1]
    if 1 &lt;= next_row &lt;= 8 and 1 &lt;= next_column &lt;= 8:
        result += 1

print(result)</code></pre>
<h3 id="핵심-포인트-3">핵심 포인트</h3>
<p><code>ord(input_data[0]) - ord(&#39;a&#39;) + 1</code> → 알파벳 열 좌표를 숫자로 변환한다. (a→1, b→2, ...)</p>
<hr>
<h2 id="🔤-문제-8-문자열-재정렬">🔤 문제 8. 문자열 재정렬</h2>
<h3 id="문제-6">문제</h3>
<p>알파벳 대문자와 숫자로 구성된 문자열이 주어졌을 때, <strong>알파벳은 오름차순 정렬</strong>, <strong>숫자는 모두 더해서</strong> 뒤에 붙여 출력하라.</p>
<ul>
<li>예시: <code>K1KA5CB7</code> → <code>ABCKK13</code></li>
</ul>
<h3 id="아이디어-7">아이디어</h3>
<p>문자를 하나씩 확인하며 알파벳과 숫자를 분리한 뒤, 알파벳은 정렬하고 숫자 합계를 뒤에 붙인다.</p>
<pre><code class="language-python">data = input()
result = []
value = 0

for x in data:
    if x.isalpha():
        result.append(x)
    else:
        value += int(x)

result.sort()

if value != 0:
    result.append(str(value))

print(&#39;&#39;.join(result))</code></pre>
<hr>
<h2 id="📝-정리">📝 정리</h2>
<table>
<thead>
<tr>
<th>유형</th>
<th>핵심 전략</th>
</tr>
</thead>
<tbody><tr>
<td>그리디</td>
<td>현재 최선의 선택 반복 + 정당성 검토 필수</td>
</tr>
<tr>
<td>시뮬레이션</td>
<td>요구사항 그대로 구현, 방향 벡터 활용</td>
</tr>
<tr>
<td>완전 탐색</td>
<td>가능한 모든 경우 탐색 (경우의 수가 적을 때)</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Capstone-8 주차] 회고록]]></title>
            <link>https://velog.io/@woojin-archive/GANADI-%ED%9A%8C%EA%B3%A0%EB%A1%9D</link>
            <guid>https://velog.io/@woojin-archive/GANADI-%ED%9A%8C%EA%B3%A0%EB%A1%9D</guid>
            <pubDate>Wed, 15 Apr 2026 04:35:04 GMT</pubDate>
            <description><![CDATA[<h1 id="첫-팀-프로젝트-백엔드-개발-회고---마이그레이션과-라우터-실수에서-배운-것들">첫 팀 프로젝트 백엔드 개발 회고 - 마이그레이션과 라우터 실수에서 배운 것들</h1>
<h2 id="들어가며">들어가며</h2>
<p>캡스톤디자인 프로젝트 GANADI(반려동물 안구질환 AI 스크리닝 플랫폼) 백엔드 개발에 처음으로 참여했다. 개발 자체도 처음이고 FastAPI도 처음이라 모르는 것투성이였는데, 첫날부터 팀원분께 버그를 잡아주시는 상황이 생겼다. 부끄럽지만 다음에 같은 실수를 반복하지 않기 위해 기록으로 남긴다.</p>
<hr>
<h2 id="오늘-한-작업">오늘 한 작업</h2>
<ul>
<li>로그인 응답에 <code>name</code>, <code>role</code> 필드 추가</li>
<li><code>GET /api/diagnosis/history</code> 전체 진단 이력 API 추가</li>
<li>알림 API 구현 (<code>GET</code>, <code>PATCH /read-all</code>, <code>PATCH /{id}</code>)</li>
</ul>
<hr>
<h2 id="실수-1---마이그레이션-파일이-비어있었다">실수 1 - 마이그레이션 파일이 비어있었다</h2>
<h3 id="무슨-일이-있었나">무슨 일이 있었나?</h3>
<p>알림 API를 구현하면서 <code>Notification</code> 모델을 추가하고 아래 명령어를 실행했다.</p>
<pre><code class="language-bash">alembic revision --autogenerate -m &quot;add notifications table&quot;
alembic upgrade head</code></pre>
<p>언뜻 보면 정상적으로 실행된 것처럼 보였다. 하지만 팀원분께서 확인해보니 <strong>실제로 DB에 notifications 테이블이 생성되지 않았다.</strong></p>
<h3 id="원인">원인</h3>
<p><code>alembic revision --autogenerate</code>는 <strong>실제 DB에 연결해서</strong> 현재 DB 상태와 모델 코드를 비교해 차이점을 감지하는 명령어다.</p>
<pre><code>현재 DB 상태: notifications 테이블 없음
모델 코드: Notification 클래스 있음
→ 차이 감지 → upgrade()에 create_table 코드 자동 생성</code></pre><p>근데 나는 <strong>MySQL이 꺼져있는 상태</strong>에서 실행했다. 그러다보니:</p>
<pre><code>MySQL 꺼져있음
→ alembic revision --autogenerate 실행
→ DB 연결 실패
→ DB랑 비교를 못함
→ upgrade() 함수가 비어있는 파일만 생성됨
→ alembic upgrade head 해도 아무것도 안됨</code></pre><h3 id="올바른-마이그레이션-파일">올바른 마이그레이션 파일</h3>
<pre><code class="language-python"># ❌ 내가 만든 파일 (비어있음)
def upgrade():
    pass

def downgrade():
    pass

# ✅ 올바른 파일
def upgrade():
    op.create_table(&#39;notifications&#39;,
        sa.Column(&#39;id&#39;, sa.Integer(), nullable=False),
        sa.Column(&#39;user_id&#39;, sa.Integer(), nullable=False),
        sa.Column(&#39;message&#39;, sa.String(500), nullable=False),
        sa.Column(&#39;type&#39;, sa.String(50), nullable=False),
        sa.Column(&#39;is_read&#39;, sa.Boolean(), nullable=False),
        sa.Column(&#39;created_at&#39;, sa.DateTime(), nullable=True),
    )

def downgrade():
    op.drop_table(&#39;notifications&#39;)</code></pre>
<h3 id="앞으로는-이렇게">앞으로는 이렇게</h3>
<pre><code>1. MySQL 먼저 켜기 ← 핵심!
2. alembic revision --autogenerate -m &quot;설명&quot;
3. 생성된 파일 열어서 upgrade() 안에 코드 있는지 확인 ← 필수!
4. alembic upgrade head</code></pre><p><strong>upgrade() 함수가 비어있으면 MySQL이 꺼져있었던 것!</strong></p>
<hr>
<h2 id="실수-2---라우터-순서를-잘못-선언했다">실수 2 - 라우터 순서를 잘못 선언했다</h2>
<h3 id="무슨-일이-있었나-1">무슨 일이 있었나?</h3>
<p>알림 전체 읽음 처리 API(<code>PATCH /read-all</code>)를 호출하면 <strong>422 에러</strong>가 발생했다.</p>
<h3 id="원인-1">원인</h3>
<p>FastAPI는 라우터를 <strong>위에서 아래로 순서대로</strong> 매칭한다.</p>
<pre><code class="language-python"># ❌ 내가 작성한 잘못된 순서
@router.patch(&quot;/{notification_id}&quot;)  # 변수 경로 먼저
@router.patch(&quot;/read-all&quot;)           # 고정 경로 나중에</code></pre>
<p><code>/read-all</code> 요청이 오면 FastAPI가 위에서부터 확인하는데, <code>read-all</code>이 <code>{notification_id}</code> 자리에 들어갈 수 있다고 판단해버린다. 그래서 <code>/read-all</code> 까지 못 가고 <code>/{notification_id}</code>에서 걸려 422 에러가 발생한 것이다.</p>
<pre><code class="language-python"># ✅ 올바른 순서
@router.patch(&quot;/read-all&quot;)           # 고정 경로 먼저
@router.patch(&quot;/{notification_id}&quot;)  # 변수 경로 나중에</code></pre>
<h3 id="규칙">규칙</h3>
<blockquote>
<p><strong>고정 경로는 항상 변수 경로보다 위에 선언해야 한다!</strong></p>
</blockquote>
<hr>
<h2 id="오늘-배운-것들">오늘 배운 것들</h2>
<h3 id="http-메서드">HTTP 메서드</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>용도</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>POST</td>
<td>새로운 데이터 생성</td>
<td>회원가입, 알림 생성</td>
</tr>
<tr>
<td>GET</td>
<td>데이터 조회</td>
<td>알림 목록 조회</td>
</tr>
<tr>
<td>PUT</td>
<td>데이터 전체 수정</td>
<td>반려동물 정보 전체 수정</td>
</tr>
<tr>
<td>PATCH</td>
<td>데이터 일부 수정</td>
<td>알림 읽음 처리 (is_read만 변경)</td>
</tr>
<tr>
<td>DELETE</td>
<td>데이터 삭제</td>
<td>반려동물 삭제</td>
</tr>
</tbody></table>
<h3 id="알림-읽음-처리에-patch를-쓰는-이유">알림 읽음 처리에 PATCH를 쓰는 이유</h3>
<p>읽음 처리는 <code>is_read</code> 필드 <strong>하나만</strong> False → True로 바꾸는 것이기 때문에 <strong>일부 수정</strong>인 PATCH가 적합하다. PUT을 쓰면 모든 필드를 다 보내야 하지만 PATCH는 변경할 필드만 보내면 된다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>개발도 처음, FastAPI도 처음인 상태에서 팀 프로젝트에 참여하다 보니 당연히 실수가 있을 수밖에 없다. 중요한 건 같은 실수를 반복하지 않는 것이라고 생각한다.</p>
<p>팀원분이 꼼꼼하게 버그를 잡아주셔서 오히려 덕분에 마이그레이션과 라우터 개념을 확실하게 이해할 수 있었다. 앞으로는 코드를 작성하고 나서 <strong>직접 눈으로 확인하는 습관</strong>을 들여야겠다.</p>
<blockquote>
<p>MySQL 먼저 켜고, 마이그레이션 파일 열어서 확인하고, 고정 경로는 변수 경로보다 위에!</p>
</blockquote>
<hr>
<p><em>캡스톤디자인 프로젝트 - 2026.04.14</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL 공부(1)]]></title>
            <link>https://velog.io/@woojin-archive/SQL-%EA%B3%B5%EB%B6%801</link>
            <guid>https://velog.io/@woojin-archive/SQL-%EA%B3%B5%EB%B6%801</guid>
            <pubDate>Sat, 04 Apr 2026 17:32:13 GMT</pubDate>
            <description><![CDATA[<h1 id="sql-연습문제-정리-q31">SQL 연습문제 정리 (Q3.1)</h1>
<blockquote>
<p>마당서점 데이터베이스를 이용한 SQL 기초 쿼리 연습</p>
</blockquote>
<hr>
<h2 id="📦-사용-테이블">📦 사용 테이블</h2>
<h3 id="book">Book</h3>
<table>
<thead>
<tr>
<th>bookid</th>
<th>bookname</th>
<th>publisher</th>
<th>price</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>축구의 역사</td>
<td>굿스포츠</td>
<td>7000</td>
</tr>
<tr>
<td>2</td>
<td>축구 아는 여자</td>
<td>나무수</td>
<td>13000</td>
</tr>
<tr>
<td>3</td>
<td>축구의 이해</td>
<td>대한미디어</td>
<td>22000</td>
</tr>
<tr>
<td>4</td>
<td>골프 바이블</td>
<td>대한미디어</td>
<td>35000</td>
</tr>
<tr>
<td>5</td>
<td>피겨 교본</td>
<td>굿스포츠</td>
<td>8000</td>
</tr>
<tr>
<td>6</td>
<td>배구 단계별기술</td>
<td>굿스포츠</td>
<td>6000</td>
</tr>
<tr>
<td>7</td>
<td>야구의 추억</td>
<td>이상미디어</td>
<td>20000</td>
</tr>
<tr>
<td>8</td>
<td>야구를 부탁해</td>
<td>이상미디어</td>
<td>13000</td>
</tr>
<tr>
<td>9</td>
<td>올림픽 이야기</td>
<td>삼성당</td>
<td>7500</td>
</tr>
<tr>
<td>10</td>
<td>Olympic Champions</td>
<td>Pearson</td>
<td>13000</td>
</tr>
</tbody></table>
<h3 id="orders">Orders</h3>
<table>
<thead>
<tr>
<th>orderid</th>
<th>custid</th>
<th>bookid</th>
<th>saleprice</th>
<th>orderdate</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>1</td>
<td>1</td>
<td>6000</td>
<td>2024-07-01</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>3</td>
<td>21000</td>
<td>2024-07-03</td>
</tr>
<tr>
<td>3</td>
<td>2</td>
<td>5</td>
<td>8000</td>
<td>2024-07-03</td>
</tr>
<tr>
<td>4</td>
<td>3</td>
<td>6</td>
<td>6000</td>
<td>2024-07-04</td>
</tr>
<tr>
<td>5</td>
<td>4</td>
<td>7</td>
<td>20000</td>
<td>2024-07-05</td>
</tr>
<tr>
<td>6</td>
<td>1</td>
<td>2</td>
<td>12000</td>
<td>2024-07-07</td>
</tr>
<tr>
<td>7</td>
<td>4</td>
<td>8</td>
<td>13000</td>
<td>2024-07-07</td>
</tr>
<tr>
<td>8</td>
<td>3</td>
<td>10</td>
<td>12000</td>
<td>2024-07-08</td>
</tr>
<tr>
<td>9</td>
<td>2</td>
<td>10</td>
<td>7000</td>
<td>2024-07-09</td>
</tr>
<tr>
<td>10</td>
<td>3</td>
<td>8</td>
<td>13000</td>
<td>2024-07-10</td>
</tr>
</tbody></table>
<h3 id="customer">Customer</h3>
<table>
<thead>
<tr>
<th>custid</th>
<th>name</th>
<th>address</th>
<th>phone</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>박지성</td>
<td>영국 맨체스타</td>
<td>000-5000-0001</td>
</tr>
<tr>
<td>2</td>
<td>김연아</td>
<td>대한민국 서울</td>
<td>000-6000-0001</td>
</tr>
<tr>
<td>3</td>
<td>김연경</td>
<td>대한민국 경기도</td>
<td>000-7000-0001</td>
</tr>
<tr>
<td>4</td>
<td>추신수</td>
<td>미국 클리블랜드</td>
<td>000-8000-0001</td>
</tr>
<tr>
<td>5</td>
<td>박세리</td>
<td>대한민국 대전</td>
<td>NULL</td>
</tr>
</tbody></table>
<hr>
<h2 id="01-고객-요구-질문">01 고객 요구 질문</h2>
<h3 id="11-도서번호가-1인-도서의-이름">1.1 도서번호가 1인 도서의 이름</h3>
<pre><code class="language-sql">SELECT bookname FROM book
WHERE bookid = 1;</code></pre>
<h3 id="12-가격이-20000원-이상인-도서의-이름">1.2 가격이 20,000원 이상인 도서의 이름</h3>
<pre><code class="language-sql">SELECT bookname FROM book
WHERE price &gt;= 20000;</code></pre>
<h3 id="13-박지성의-총구매액">1.3 박지성의 총구매액</h3>
<pre><code class="language-sql">SELECT SUM(saleprice) FROM orders
WHERE custid = 1;</code></pre>
<blockquote>
<p>박지성의 custid를 모를 경우 서브쿼리 사용</p>
<pre><code class="language-sql">SELECT SUM(saleprice) FROM orders
WHERE custid = (SELECT custid FROM customer WHERE name = &#39;박지성&#39;);</code></pre>
</blockquote>
<h3 id="14-박지성이-구매한-도서의-수">1.4 박지성이 구매한 도서의 수</h3>
<pre><code class="language-sql">SELECT COUNT(*) FROM orders
WHERE custid = 1;</code></pre>
<h3 id="15-박지성이-구매한-도서의-출판사-수">1.5 박지성이 구매한 도서의 출판사 수</h3>
<pre><code class="language-sql">SELECT COUNT(DISTINCT b.publisher)
FROM orders o JOIN book b ON o.bookid = b.bookid
WHERE o.custid IN (SELECT custid FROM customer WHERE name = &#39;박지성&#39;);</code></pre>
<blockquote>
<p><strong><code>=</code> vs <code>IN</code> 차이</strong></p>
<ul>
<li><code>=</code> : 서브쿼리 결과가 <strong>1개</strong>일 때만 사용 가능, 2개 이상이면 오류</li>
<li><code>IN</code> : 결과가 <strong>여러 개</strong>여도 정상 작동 → 동명이인 등을 고려하면 <code>IN</code>이 더 안전</li>
</ul>
</blockquote>
<h3 id="16-박지성이-구매한-도서의-이름-가격-정가와-판매가격의-차이">1.6 박지성이 구매한 도서의 이름, 가격, 정가와 판매가격의 차이</h3>
<pre><code class="language-sql">SELECT b.bookname, b.price, o.saleprice, (b.price - o.saleprice) AS difference
FROM orders o JOIN book b ON o.bookid = b.bookid
WHERE o.custid IN (SELECT custid FROM customer WHERE name = &#39;박지성&#39;);</code></pre>
<table>
<thead>
<tr>
<th>bookname</th>
<th>price</th>
<th>saleprice</th>
<th>difference</th>
</tr>
</thead>
<tbody><tr>
<td>축구의 역사</td>
<td>7000</td>
<td>6000</td>
<td>1000</td>
</tr>
<tr>
<td>축구의 이해</td>
<td>22000</td>
<td>21000</td>
<td>1000</td>
</tr>
<tr>
<td>축구 아는 여자</td>
<td>13000</td>
<td>12000</td>
<td>1000</td>
</tr>
</tbody></table>
<h3 id="17-박지성이-구매하지-않은-도서의-이름">1.7 박지성이 구매하지 않은 도서의 이름</h3>
<pre><code class="language-sql">SELECT bookname
FROM book
WHERE bookid NOT IN (
    SELECT bookid FROM orders
    WHERE custid IN (SELECT custid FROM customer WHERE name = &#39;박지성&#39;)
);</code></pre>
<blockquote>
<p>⚠️ <strong>주의</strong>: <code>FROM orders JOIN book</code>으로 시작하면 안 됨</p>
<ul>
<li>&quot;박지성이 구매하지 않은 <strong>도서</strong>&quot; → 기준이 <strong>전체 도서(book)</strong></li>
<li><code>FROM book</code>에서 시작해서 박지성의 주문 목록을 <code>NOT IN</code>으로 제외해야 함</li>
<li><code>FROM orders</code>로 시작하면 &quot;다른 사람들이 주문한 책&quot;이 나와버려 결과가 틀림</li>
</ul>
</blockquote>
<hr>
<h2 id="02-운영자·경영자-요구-질문">02 운영자·경영자 요구 질문</h2>
<h3 id="21-마당서점-도서의-총개수">2.1 마당서점 도서의 총개수</h3>
<pre><code class="language-sql">SELECT COUNT(*) FROM book;</code></pre>
<h3 id="22-마당서점에-도서를-출고하는-출판사의-총개수">2.2 마당서점에 도서를 출고하는 출판사의 총개수</h3>
<pre><code class="language-sql">SELECT COUNT(DISTINCT publisher) FROM book;</code></pre>
<blockquote>
<p><code>DISTINCT</code> 없이 쓰면 중복 출판사가 여러 번 카운트됨</p>
</blockquote>
<h3 id="23-모든-고객의-이름-주소">2.3 모든 고객의 이름, 주소</h3>
<pre><code class="language-sql">SELECT name, address FROM customer;</code></pre>
<h3 id="24-2024년-7월-4일7월-7일-사이에-주문받은-도서의-주문번호">2.4 2024년 7월 4일~7월 7일 사이에 주문받은 도서의 주문번호</h3>
<pre><code class="language-sql">SELECT orderid FROM orders
WHERE orderdate BETWEEN &#39;2024-07-04&#39; AND &#39;2024-07-07&#39;;</code></pre>
<blockquote>
<p>⚠️ <code>IN (&#39;2024-07-04&#39;, &#39;2024-07-07&#39;)</code>은 4일과 7일만 조회됨, 사이 날짜 누락</p>
</blockquote>
<h3 id="25-2024년-7월-4일7월-7일-사이에-주문받은-도서를-제외한-도서의-주문번호">2.5 2024년 7월 4일~7월 7일 사이에 주문받은 도서를 제외한 도서의 주문번호</h3>
<pre><code class="language-sql">SELECT orderid FROM orders
WHERE orderdate NOT BETWEEN &#39;2024-07-04&#39; AND &#39;2024-07-07&#39;;</code></pre>
<h3 id="26-성이-김-씨인-고객의-이름과-주소">2.6 성이 &#39;김&#39; 씨인 고객의 이름과 주소</h3>
<pre><code class="language-sql">SELECT name, address FROM customer
WHERE name LIKE &#39;김%&#39;;</code></pre>
<h3 id="27-성이-김-씨이고-이름이-아로-끝나는-고객의-이름과-주소">2.7 성이 &#39;김&#39; 씨이고 이름이 &#39;아&#39;로 끝나는 고객의 이름과 주소</h3>
<pre><code class="language-sql">SELECT name, address FROM customer
WHERE name LIKE &#39;김%아&#39;;</code></pre>
<blockquote>
<p>⚠️ <code>&#39;김_아&#39;</code>는 이름이 3글자(김X아)인 경우만 해당됨</p>
<ul>
<li><code>_</code> : 딱 <strong>1글자</strong>만 대체</li>
<li><code>%</code> : <strong>0글자 이상</strong> 대체 → 4글자 이상 이름도 포함</li>
</ul>
</blockquote>
<h3 id="28-주문하지-않은-고객의-이름-부속질의-사용">2.8 주문하지 않은 고객의 이름 (부속질의 사용)</h3>
<pre><code class="language-sql">SELECT name FROM customer
WHERE custid NOT IN (SELECT custid FROM orders);</code></pre>
<h3 id="29-주문-금액의-총액과-주문의-평균-금액">2.9 주문 금액의 총액과 주문의 평균 금액</h3>
<pre><code class="language-sql">SELECT SUM(saleprice) AS 총액, AVG(saleprice) AS 평균금액
FROM orders;</code></pre>
<h3 id="210-고객의-이름과-고객별-구매액">2.10 고객의 이름과 고객별 구매액</h3>
<pre><code class="language-sql">SELECT c.name, SUM(o.saleprice) AS 구매액
FROM customer c JOIN orders o ON c.custid = o.custid
GROUP BY c.custid, c.name;</code></pre>
<blockquote>
<p><strong>&quot;고객별&quot;</strong> 이라는 단어가 나오면 <code>GROUP BY</code> 사용</p>
<p><code>GROUP BY c.custid, c.name</code> 으로 쓰는 이유: 동명이인이 있을 경우 이름만으로 묶으면 합산될 수 있어서 custid를 함께 명시하는 것이 안전</p>
</blockquote>
<h3 id="211-고객의-이름과-고객이-구매한-도서-목록">2.11 고객의 이름과 고객이 구매한 도서 목록</h3>
<pre><code class="language-sql">SELECT c.name, b.bookname
FROM customer c
JOIN orders o ON c.custid = o.custid
JOIN book b ON o.bookid = b.bookid;</code></pre>
<blockquote>
<p>customer와 book은 직접 연결되는 컬럼이 없음
orders가 중간 테이블 역할 → JOIN을 두 번 해야 함</p>
<pre><code>customer --- orders --- book
   custid      custid
               bookid   bookid</code></pre></blockquote>
<h3 id="212-도서의-가격book과-판매가격orders의-차이가-가장-많은-주문">2.12 도서의 가격(Book)과 판매가격(Orders)의 차이가 가장 많은 주문</h3>
<pre><code class="language-sql">SELECT b.bookname, b.price, o.saleprice, (b.price - o.saleprice) AS difference
FROM book b JOIN orders o ON b.bookid = o.bookid
WHERE (b.price - o.saleprice) = (
    SELECT MAX(b.price - o.saleprice)
    FROM book b JOIN orders o ON b.bookid = o.bookid
);</code></pre>
<blockquote>
<p><code>MAX()</code>만 쓰면 최댓값 숫자만 나옴
해당 <strong>주문 정보 전체</strong>를 가져오려면 서브쿼리로 최댓값을 구한 뒤 <code>WHERE</code>로 해당 행을 찾아야 함</p>
</blockquote>
<h3 id="213-도서의-판매액-평균보다-자신의-구매액-평균이-더-높은-고객의-이름">2.13 도서의 판매액 평균보다 자신의 구매액 평균이 더 높은 고객의 이름</h3>
<pre><code class="language-sql">SELECT c.name
FROM customer c JOIN orders o ON c.custid = o.custid
GROUP BY c.custid, c.name
HAVING AVG(o.saleprice) &gt; (SELECT AVG(saleprice) FROM orders);</code></pre>
<blockquote>
<p><strong><code>WHERE</code> vs <code>HAVING</code></strong></p>
<table>
<thead>
<tr>
<th>구분</th>
<th>사용 위치</th>
</tr>
</thead>
<tbody><tr>
<td>일반 조건</td>
<td><code>WHERE</code></td>
</tr>
<tr>
<td>집계함수 조건</td>
<td><code>HAVING</code></td>
</tr>
</tbody></table>
<p><code>HAVING</code>은 항상 <code>GROUP BY</code> 뒤에 위치</p>
</blockquote>
<hr>
<h2 id="💡-핵심-개념-정리">💡 핵심 개념 정리</h2>
<h3 id="from-테이블-선택-기준">FROM 테이블 선택 기준</h3>
<table>
<thead>
<tr>
<th>질문 패턴</th>
<th>기준 테이블</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;~한 것들 중에서 집계&quot;</td>
<td>행위 테이블 (orders)</td>
</tr>
<tr>
<td>&quot;전체 중에서 ~한/안한 것&quot;</td>
<td>대상 테이블 (book, customer)</td>
</tr>
</tbody></table>
<p><strong>핵심</strong>: &quot;무엇을&quot; 조회하는지가 FROM/기준 테이블을 결정한다</p>
<h3 id="like-패턴">LIKE 패턴</h3>
<table>
<thead>
<tr>
<th>패턴</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>%</code></td>
<td>0글자 이상 임의의 문자</td>
</tr>
<tr>
<td><code>_</code></td>
<td>정확히 1글자</td>
</tr>
</tbody></table>
<h3 id="집계함수와-group-by">집계함수와 GROUP BY</h3>
<ul>
<li><code>COUNT(*)</code> : NULL 포함 전체 행 수</li>
<li><code>COUNT(컬럼)</code> : NULL 제외 행 수</li>
<li><code>COUNT(DISTINCT 컬럼)</code> : 중복 제외 고유값 수</li>
<li>집계함수 조건은 반드시 <code>HAVING</code> 사용</li>
</ul>
<h3 id="서브쿼리">서브쿼리</h3>
<pre><code class="language-sql">-- 단일 결과 → = 사용 가능
WHERE custid = (SELECT custid FROM customer WHERE name = &#39;박지성&#39;)

-- 복수 결과 가능성 → IN 사용 권장
WHERE custid IN (SELECT custid FROM customer WHERE name = &#39;박지성&#39;)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[이코테] 파이썬 문법 정리(2)]]></title>
            <link>https://velog.io/@woojin-archive/%EC%9D%B4%EC%BD%94%ED%85%8C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC2</link>
            <guid>https://velog.io/@woojin-archive/%EC%9D%B4%EC%BD%94%ED%85%8C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC2</guid>
            <pubDate>Thu, 02 Apr 2026 13:21:03 GMT</pubDate>
            <description><![CDATA[<h1 id="파이썬-문법-정리---입출력-함수-표준-라이브러리">파이썬 문법 정리 - 입출력, 함수, 표준 라이브러리</h1>
<h2 id="📥-입력-방법">📥 입력 방법</h2>
<h3 id="기본-입력---map">기본 입력 - map()</h3>
<pre><code class="language-python"># 공백으로 구분된 여러 데이터 입력받기
data = list(map(int, input().split()))

# 개수가 적은 경우 변수에 바로 할당
a, b, c = map(int, input().split())</code></pre>
<h3 id="빠른-입력---sysstdinreadline">빠른 입력 - sys.stdin.readline()</h3>
<p>입력 데이터가 많을 때 <code>input()</code> 대신 사용. 엔터가 줄 바꿈 기호로 포함되므로 <code>rstrip()</code> 필수!</p>
<pre><code class="language-python">import sys

data = sys.stdin.readline().rstrip()
print(data)</code></pre>
<hr>
<h2 id="🖨️-출력-방법">🖨️ 출력 방법</h2>
<h3 id="f-string-python-36">f-string (Python 3.6+)</h3>
<p>문자열 앞에 <code>f</code>를 붙이고 중괄호 안에 변수명을 넣어 간편하게 출력</p>
<pre><code class="language-python">answer = 7
print(f&quot;정답은 {answer}입니다.&quot;)</code></pre>
<hr>
<h2 id="🔧-유용한-문법">🔧 유용한 문법</h2>
<h3 id="in--not-in-연산자">in / not in 연산자</h3>
<p>리스트, 튜플, 문자열, 딕셔너리 모두에서 사용 가능</p>
<pre><code class="language-python">a = [1, 2, 3]
print(3 in a)      # True
print(5 not in a)  # True</code></pre>
<h3 id="조건문-간소화-삼항-연산자">조건문 간소화 (삼항 연산자)</h3>
<pre><code class="language-python">score = 85
result = &quot;Success&quot; if score &gt;= 80 else &quot;Fail&quot;
print(result)  # Success</code></pre>
<h3 id="global-키워드">global 키워드</h3>
<p>함수 내부에서 외부 변수를 직접 참조할 때 사용</p>
<pre><code class="language-python">a = 0

def func():
    global a
    a += 1

for i in range(10):
    func()

print(a)  # 10</code></pre>
<h3 id="여러-개의-반환값">여러 개의 반환값</h3>
<p>파이썬 함수는 여러 값을 동시에 반환할 수 있다 (튜플로 반환됨)</p>
<pre><code class="language-python">def operator(a, b):
    return a + b, a - b, a * b, a / b

a, b, c, d = operator(7, 3)
print(a, b, c, d)  # 10 4 21 2.333...</code></pre>
<hr>
<h2 id="⚡-람다-표현식-lambda">⚡ 람다 표현식 (Lambda)</h2>
<p>함수를 한 줄로 간결하게 표현</p>
<pre><code class="language-python"># 일반 함수
def add(a, b):
    return a + b

# 람다 표현식
print((lambda a, b: a + b)(3, 7))  # 10</code></pre>
<h3 id="sorted와-함께-사용">sorted()와 함께 사용</h3>
<pre><code class="language-python">array = [(&#39;홍길동&#39;, 50), (&#39;이순신&#39;, 32), (&#39;아무개&#39;, 74)]

# 두 번째 원소(점수)를 기준으로 정렬
print(sorted(array, key=lambda x: x[1]))
# [(&#39;이순신&#39;, 32), (&#39;홍길동&#39;, 50), (&#39;아무개&#39;, 74)]</code></pre>
<h3 id="map과-함께-여러-리스트에-적용">map()과 함께 여러 리스트에 적용</h3>
<pre><code class="language-python">list1 = [1, 2, 3, 4, 5]
list2 = [6, 7, 8, 9, 10]

result = list(map(lambda a, b: a + b, list1, list2))
print(result)  # [7, 9, 11, 13, 15]</code></pre>
<hr>
<h2 id="📚-자주-쓰는-내장-함수">📚 자주 쓰는 내장 함수</h2>
<pre><code class="language-python"># sum() - 합계
print(sum([1, 2, 3, 4, 5]))  # 15

# min(), max() - 최솟값, 최댓값
print(min(7, 3, 5, 2))  # 2
print(max(7, 3, 5, 2))  # 7

# eval() - 문자열 수식 계산
print(eval(&quot;(3+5)*7&quot;))  # 56

# sorted() - 정렬 (원본 변경 없음)
print(sorted([9, 1, 8, 5, 4]))              # [1, 4, 5, 8, 9]
print(sorted([9, 1, 8, 5, 4], reverse=True))  # [9, 8, 5, 4, 1]

# sorted() with key
array = [(&#39;홍길동&#39;, 35), (&#39;이순신&#39;, 75), (&#39;아무개&#39;, 50)]
result = sorted(array, key=lambda x: x[1], reverse=True)
print(result)
# [(&#39;이순신&#39;, 75), (&#39;아무개&#39;, 50), (&#39;홍길동&#39;, 35)]</code></pre>
<hr>
<h2 id="🗂️-실전-표준-라이브러리">🗂️ 실전 표준 라이브러리</h2>
<table>
<thead>
<tr>
<th>라이브러리</th>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td><code>itertools</code></td>
<td>순열, 조합 등 반복 데이터 처리</td>
</tr>
<tr>
<td><code>heapq</code></td>
<td>힙(우선순위 큐) 구현</td>
</tr>
<tr>
<td><code>bisect</code></td>
<td>이진 탐색</td>
</tr>
<tr>
<td><code>collections</code></td>
<td>deque, Counter 등 유용한 자료구조</td>
</tr>
<tr>
<td><code>math</code></td>
<td>팩토리얼, 제곱근, GCD, 삼각함수, 상수 등</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔀-순열과-조합-itertools">🔀 순열과 조합 (itertools)</h2>
<table>
<thead>
<tr>
<th>종류</th>
<th>공식</th>
<th>함수</th>
</tr>
</thead>
<tbody><tr>
<td>순열</td>
<td>nPr</td>
<td><code>permutations(data, r)</code></td>
</tr>
<tr>
<td>조합</td>
<td>nCr</td>
<td><code>combinations(data, r)</code></td>
</tr>
<tr>
<td>중복 순열</td>
<td>n^r</td>
<td><code>product(data, repeat=r)</code></td>
</tr>
<tr>
<td>중복 조합</td>
<td>nHr</td>
<td><code>combinations_with_replacement(data, r)</code></td>
</tr>
</tbody></table>
<h3 id="순열-permutations">순열 (permutations)</h3>
<pre><code class="language-python">from itertools import permutations

data = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;]
result = list(permutations(data, 3))
print(result)
# [(&#39;A&#39;,&#39;B&#39;,&#39;C&#39;), (&#39;A&#39;,&#39;C&#39;,&#39;B&#39;), (&#39;B&#39;,&#39;A&#39;,&#39;C&#39;),
#  (&#39;B&#39;,&#39;C&#39;,&#39;A&#39;), (&#39;C&#39;,&#39;A&#39;,&#39;B&#39;), (&#39;C&#39;,&#39;B&#39;,&#39;A&#39;)]</code></pre>
<h3 id="조합-combinations">조합 (combinations)</h3>
<pre><code class="language-python">from itertools import combinations

data = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;]
result = list(combinations(data, 2))
print(result)
# [(&#39;A&#39;, &#39;B&#39;), (&#39;A&#39;, &#39;C&#39;), (&#39;B&#39;, &#39;C&#39;)]</code></pre>
<h3 id="중복-순열-product">중복 순열 (product)</h3>
<pre><code class="language-python">from itertools import product

data = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;]
result = list(product(data, repeat=2))
print(result)
# [(&#39;A&#39;,&#39;A&#39;), (&#39;A&#39;,&#39;B&#39;), (&#39;A&#39;,&#39;C&#39;),
#  (&#39;B&#39;,&#39;A&#39;), (&#39;B&#39;,&#39;B&#39;), (&#39;B&#39;,&#39;C&#39;),
#  (&#39;C&#39;,&#39;A&#39;), (&#39;C&#39;,&#39;B&#39;), (&#39;C&#39;,&#39;C&#39;)]</code></pre>
<h3 id="중복-조합-combinations_with_replacement">중복 조합 (combinations_with_replacement)</h3>
<pre><code class="language-python">from itertools import combinations_with_replacement

data = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;]
result = list(combinations_with_replacement(data, 2))
print(result)
# [(&#39;A&#39;,&#39;A&#39;), (&#39;A&#39;,&#39;B&#39;), (&#39;A&#39;,&#39;C&#39;),
#  (&#39;B&#39;,&#39;B&#39;), (&#39;B&#39;,&#39;C&#39;), (&#39;C&#39;,&#39;C&#39;)]</code></pre>
<hr>
<h2 id="🧮-collections---counter">🧮 collections - Counter</h2>
<p>리스트 내 원소의 등장 횟수를 자동으로 카운팅</p>
<pre><code class="language-python">from collections import Counter

counter = Counter([&#39;red&#39;, &#39;blue&#39;, &#39;red&#39;, &#39;green&#39;, &#39;blue&#39;, &#39;blue&#39;])

print(counter[&#39;blue&#39;])   # 3
print(counter[&#39;green&#39;])  # 1
print(dict(counter))     # {&#39;red&#39;: 2, &#39;blue&#39;: 3, &#39;green&#39;: 1}</code></pre>
<hr>
<h2 id="➗-math---최대공약수--최소공배수">➗ math - 최대공약수 &amp; 최소공배수</h2>
<pre><code class="language-python">import math

def lcm(a, b):
    return a * b // math.gcd(a, b)

a, b = 21, 14

print(math.gcd(a, b))  # 최대공약수(GCD): 7
print(math.lcm(a, b))       # 최소공배수(LCM): 42</code></pre>
<blockquote>
<p>💡 Python 3.9부터는 <code>math.lcm(a, b)</code>로 바로 사용 가능!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[이코테] 파이썬 문법 정리]]></title>
            <link>https://velog.io/@woojin-archive/%EC%9D%B4%EC%BD%94%ED%85%8C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@woojin-archive/%EC%9D%B4%EC%BD%94%ED%85%8C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 02 Apr 2026 07:49:48 GMT</pubDate>
            <description><![CDATA[<h1 id="코딩-테스트-출제-경향-분석-및-파이썬-문법-정리">코딩 테스트 출제 경향 분석 및 파이썬 문법 정리</h1>
<h2 id="📌-알고리즘-설계-tip">📌 알고리즘 설계 Tip</h2>
<h3 id="언어별-연산-속도-기준">언어별 연산 속도 기준</h3>
<table>
<thead>
<tr>
<th>언어</th>
<th>1초당 연산 횟수</th>
</tr>
</thead>
<tbody><tr>
<td>C언어</td>
<td>약 1억 번</td>
</tr>
<tr>
<td>Python</td>
<td>약 2천만 번</td>
</tr>
<tr>
<td>PyPy</td>
<td>때때로 C언어보다 빠름</td>
</tr>
</tbody></table>
<blockquote>
<p>연산 횟수가 <strong>5억을 넘어가는 경우</strong>, C언어 기준 1<del>3초, Python 기준 5</del>15초 소요<br>코딩 테스트의 시간제한은 보통 <strong>1~5초</strong>. 명시되지 않은 경우 <strong>5초</strong>로 가정하자.</p>
</blockquote>
<hr>
<h3 id="n의-범위에-따른-시간-복잡도-기준-시간제한-1초">n의 범위에 따른 시간 복잡도 기준 (시간제한 1초)</h3>
<table>
<thead>
<tr>
<th>n의 범위</th>
<th>적합한 시간 복잡도</th>
</tr>
</thead>
<tbody><tr>
<td>500</td>
<td>O(n³)</td>
</tr>
<tr>
<td>2,000</td>
<td>O(n²)</td>
</tr>
<tr>
<td>100,000</td>
<td>O(n log n)</td>
</tr>
<tr>
<td>10,000,000</td>
<td>O(n)</td>
</tr>
</tbody></table>
<hr>
<h3 id="알고리즘-문제-해결-과정">알고리즘 문제 해결 과정</h3>
<ol>
<li>지문 읽기 및 컴퓨터적 사고</li>
<li>요구사항(복잡도) 분석</li>
<li>문제 해결을 위한 아이디어 찾기</li>
<li>소스코드 설계 및 코딩</li>
</ol>
<blockquote>
<p>💡 핵심 아이디어를 캐치하면 코드는 간결해진다. 문제에서 가장 먼저 <strong>시간제한</strong>을 확인하자!</p>
</blockquote>
<hr>
<h3 id="수행-시간-측정-코드">수행 시간 측정 코드</h3>
<pre><code class="language-python">import time

start_time = time.time()  # 측정 시작

# 프로그램 소스코드

end_time = time.time()    # 측정 종료
print(&quot;time:&quot;, end_time - start_time)  # 수행 시간 출력</code></pre>
<hr>
<h2 id="📦-파이썬-자료형">📦 파이썬 자료형</h2>
<h3 id="1-실수형-float">1. 실수형 (Float)</h3>
<p>실수형은 부동소수점 오차가 있으므로 비교 시 <strong><code>round()</code></strong> 를 사용하자.</p>
<pre><code class="language-python">a = 0.3 + 0.6
print(round(a, 4))  # 0.9

if round(a, 4) == 0.9:
    print(True)
else:
    print(False)</code></pre>
<hr>
<h3 id="2-리스트-list">2. 리스트 (List)</h3>
<h4 id="인덱싱--슬라이싱">인덱싱 &amp; 슬라이싱</h4>
<p>끝 인덱스는 실제보다 <strong>1 크게</strong> 설정한다.</p>
<pre><code class="language-python">a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(a[1:4])  # [2, 3, 4]</code></pre>
<h4 id="리스트-컴프리헨션">리스트 컴프리헨션</h4>
<pre><code class="language-python"># 0부터 9까지
array = [i for i in range(10)]

# 0~19 중 홀수만
array = [i for i in range(20) if i % 2 == 1]

# 1~9의 제곱값
array = [i * i for i in range(1, 10)]</code></pre>
<h4 id="2차원-리스트-초기화">2차원 리스트 초기화</h4>
<pre><code class="language-python"># ✅ 올바른 방법
array = [[0] * m for _ in range(n)]

# ❌ 잘못된 방법 (모든 행이 같은 객체를 참조함)
array = [[0] * m] * n</code></pre>
<h4 id="리스트-주요-메서드">리스트 주요 메서드</h4>
<table>
<thead>
<tr>
<th>함수명</th>
<th>사용법</th>
<th>설명</th>
<th>시간 복잡도</th>
</tr>
</thead>
<tbody><tr>
<td><code>append()</code></td>
<td><code>변수명.append(값)</code></td>
<td>원소 하나 삽입</td>
<td>O(1)</td>
</tr>
<tr>
<td><code>sort()</code></td>
<td><code>변수명.sort()</code> / <code>변수명.sort(reverse=True)</code></td>
<td>오름차순 / 내림차순 정렬</td>
<td>O(N log N)</td>
</tr>
<tr>
<td><code>reverse()</code></td>
<td><code>변수명.reverse()</code></td>
<td>원소 순서 뒤집기</td>
<td>O(N)</td>
</tr>
<tr>
<td><code>insert()</code></td>
<td><code>insert(인덱스, 값)</code></td>
<td>특정 위치에 삽입</td>
<td>O(N)</td>
</tr>
<tr>
<td><code>count()</code></td>
<td><code>변수명.count(값)</code></td>
<td>특정 값의 개수 반환</td>
<td>O(N)</td>
</tr>
<tr>
<td><code>remove()</code></td>
<td><code>변수명.remove(값)</code></td>
<td>특정 값 하나 제거</td>
<td>O(N)</td>
</tr>
</tbody></table>
<pre><code class="language-python">a = [1, 4, 3]

a.append(2)           # [1, 4, 3, 2]
a.sort()              # [1, 2, 3, 4]
a.sort(reverse=True)  # [4, 3, 2, 1]
a.reverse()           # [1, 2, 3, 4]
a.insert(2, 3)        # [1, 2, 3, 3, 4]
a.count(3)            # 2
a.remove(1)           # [2, 3, 3, 4]</code></pre>
<h4 id="특정-값-원소-모두-제거하기">특정 값 원소 모두 제거하기</h4>
<pre><code class="language-python">a = [1, 2, 3, 4, 5, 5, 5]
remove_set = {3, 5}

result = [i for i in a if i not in remove_set]
print(result)  # [1, 2, 4]</code></pre>
<hr>
<h3 id="3-문자열-string">3. 문자열 (String)</h3>
<ul>
<li><code>+</code> : 문자열 연결</li>
<li><code>*</code> : 문자열 반복</li>
<li>인덱싱/슬라이싱 가능, 단 <strong>Immutable</strong> (특정 인덱스 값 변경 불가)</li>
</ul>
<pre><code class="language-python">a = &quot;Hello&quot;
b = &quot;World&quot;
print(a + &quot; &quot; + b)  # Hello World

a = &quot;String&quot;
print(a * 3)        # StringStringString

a = &quot;ABCDEF&quot;
print(a[2:4])       # CD</code></pre>
<hr>
<h3 id="4-튜플-tuple">4. 튜플 (Tuple)</h3>
<p>리스트와 유사하지만 <strong>변경 불가(Immutable)</strong>, 소괄호 <code>()</code> 사용</p>
<pre><code class="language-python">a = (1, 2, 3, 4, 5, 6, 7, 8, 9)

print(a[3])    # 4
print(a[1:4])  # (2, 3, 4)
# a[2] = 7  ← 오류 발생!</code></pre>
<h4 id="튜플을-사용하면-좋은-경우">튜플을 사용하면 좋은 경우</h4>
<ul>
<li>서로 다른 성질의 데이터를 묶을 때 → ex. <code>(비용, 노드번호)</code> 형태로 최단경로 알고리즘에서 활용</li>
<li>해싱의 <strong>키(key)</strong> 로 사용해야 할 때 (리스트는 키로 사용 불가)</li>
<li>메모리를 더 효율적으로 사용해야 할 때</li>
</ul>
<hr>
<h3 id="5-사전-자료형-dictionary">5. 사전 자료형 (Dictionary)</h3>
<ul>
<li>키(key) - 값(value) 쌍으로 데이터 저장</li>
<li><strong>해시 테이블</strong> 기반 → 조회 및 수정 O(1)</li>
<li>키는 <strong>Immutable</strong> 자료형만 사용 가능</li>
</ul>
<pre><code class="language-python">data = dict()
data[&#39;사과&#39;] = &#39;Apple&#39;
data[&#39;바나나&#39;] = &#39;Banana&#39;
data[&#39;코코넛&#39;] = &#39;Coconut&#39;

print(data)  # {&#39;사과&#39;: &#39;Apple&#39;, &#39;바나나&#39;: &#39;Banana&#39;, &#39;코코넛&#39;: &#39;Coconut&#39;}

if &#39;사과&#39; in data:
    print(&quot;&#39;사과&#39;를 키로 가지는 데이터가 존재합니다.&quot;)</code></pre>
<pre><code class="language-python"># 키/값 리스트 추출
key_list = data.keys()
value_list = data.values()

for key in key_list:
    print(data[key])</code></pre>
<pre><code class="language-python"># 딕셔너리 선언 방법
b = {
    &#39;홍길동&#39;: 97,
    &#39;이순신&#39;: 98
}

print(b[&#39;이순신&#39;])           # 98
print(list(b.keys()))       # [&#39;홍길동&#39;, &#39;이순신&#39;]</code></pre>
<hr>
<h3 id="6-집합-자료형-set">6. 집합 자료형 (Set)</h3>
<ul>
<li><strong>중복 허용 X</strong>, <strong>순서 없음</strong></li>
<li>조회 및 수정 O(1)</li>
</ul>
<pre><code class="language-python">a = set([1, 2, 3, 4, 5])
b = set([3, 4, 5, 6, 7])

print(a | b)  # 합집합: {1, 2, 3, 4, 5, 6, 7}
print(a &amp; b)  # 교집합: {3, 4, 5}
print(a - b)  # 차집합: {1, 2}</code></pre>
<pre><code class="language-python">data = set([1, 2, 3])

data.add(4)         # 원소 하나 추가
data.update([5, 6]) # 원소 여러 개 추가
data.remove(3)      # 특정 원소 삭제</code></pre>
<hr>
<h2 id="🔑-자료형-핵심-정리">🔑 자료형 핵심 정리</h2>
<table>
<thead>
<tr>
<th>자료형</th>
<th>순서</th>
<th>인덱싱</th>
<th>중복</th>
<th>조회 시간</th>
</tr>
</thead>
<tbody><tr>
<td>리스트</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>O(N)</td>
</tr>
<tr>
<td>튜플</td>
<td>✅</td>
<td>✅</td>
<td>✅</td>
<td>O(N)</td>
</tr>
<tr>
<td>사전</td>
<td>❌</td>
<td>❌</td>
<td>키 중복 X</td>
<td>O(1)</td>
</tr>
<tr>
<td>집합</td>
<td>❌</td>
<td>❌</td>
<td>❌</td>
<td>O(1)</td>
</tr>
</tbody></table>
<blockquote>
<p>사전과 집합은 순서가 없어 인덱싱 불가. 대신 <strong>O(1)</strong> 의 빠른 조회 속도가 장점!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 5. 기능 고도화]]></title>
            <link>https://velog.io/@woojin-archive/FastAPI-5.-%EA%B8%B0%EB%8A%A5-%EA%B3%A0%EB%8F%84%ED%99%94</link>
            <guid>https://velog.io/@woojin-archive/FastAPI-5.-%EA%B8%B0%EB%8A%A5-%EA%B3%A0%EB%8F%84%ED%99%94</guid>
            <pubDate>Wed, 18 Mar 2026 08:02:31 GMT</pubDate>
            <description><![CDATA[<h1 id="fastapi-섹션-5-기능-고도화">FastAPI 섹션 5. 기능 고도화</h1>
<hr>
<h2 id="1-sql-join">1. SQL JOIN</h2>
<p>두 개의 테이블을 <strong>공통 컬럼을 기준으로 연결</strong>해서 조회하는 기능입니다.</p>
<h3 id="예시">예시</h3>
<p><strong>User 테이블:</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>username</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>qu3vipon</td>
</tr>
</tbody></table>
<p><strong>ToDo 테이블:</strong></p>
<table>
<thead>
<tr>
<th>id</th>
<th>user_id</th>
<th>contents</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>1</td>
<td>FastAPI Section 1</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>FastAPI Section 2</td>
</tr>
</tbody></table>
<p><strong>JOIN 쿼리:</strong></p>
<pre><code class="language-sql">SELECT u.username, t.contents
FROM user u
JOIN todo t ON u.id = t.user_id;</code></pre>
<p><strong>결과:</strong></p>
<table>
<thead>
<tr>
<th>username</th>
<th>contents</th>
</tr>
</thead>
<tbody><tr>
<td>qu3vipon</td>
<td>FastAPI Section 1</td>
</tr>
<tr>
<td>qu3vipon</td>
<td>FastAPI Section 2</td>
</tr>
</tbody></table>
<blockquote>
<p><code>user.id = todo.user_id</code> 라는 공통 컬럼을 기준으로 두 테이블을 연결합니다.</p>
</blockquote>
<hr>
<h2 id="2-user-모델링">2. User 모델링</h2>
<h3 id="orm-클래스-→-sql-변환-확인-python-콘솔">ORM 클래스 → SQL 변환 확인 (Python 콘솔)</h3>
<pre><code class="language-python">from sqlalchemy.schema import CreateTable
from database.orm import ToDo
from database.connection import engine

# ORM 클래스가 어떤 SQL CREATE TABLE로 변환되는지 확인
print(CreateTable(ToDo.__table__).compile(engine))</code></pre>
<p>출력 결과:</p>
<pre><code class="language-sql">CREATE TABLE todo (
    id INTEGER NOT NULL AUTO_INCREMENT,
    contents VARCHAR(256) NOT NULL,
    is_done BOOL NOT NULL,
    PRIMARY KEY (id)
)</code></pre>
<h3 id="databaseormpy--user-모델-추가"><code>database/orm.py</code> — User 모델 추가</h3>
<pre><code class="language-python">from sqlalchemy import Boolean, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, relationship
from schema.request import CreateToDoRequest

Base = declarative_base()

class ToDo(Base):
    __tablename__ = &quot;todo&quot;

    id = Column(Integer, primary_key=True, index=True)
    contents = Column(String(256), nullable=False)
    is_done = Column(Boolean, nullable=False)
    # ForeignKey: todo.user_id가 user.id를 참조함 (외래키 설정)
    user_id = Column(Integer, ForeignKey(&quot;user.id&quot;))

    def __repr__(self):
        return f&quot;ToDo(id={self.id}, contents={self.contents}, is_done={self.is_done})&quot;

    @classmethod
    def create(cls, request: CreateToDoRequest) -&gt; &quot;ToDo&quot;:
        return cls(
            contents=request.contents,
            is_done=request.is_done,
        )

    def done(self) -&gt; &quot;ToDo&quot;:
        self.is_done = True
        return self

    def undone(self) -&gt; &quot;ToDo&quot;:
        self.is_done = False
        return self


class User(Base):
    __tablename__ = &quot;user&quot;

    id = Column(Integer, primary_key=True, index=True)
    username = Column(String(256), nullable=False)
    password = Column(String(256), nullable=False)

    # relationship: User 객체에서 user.todos로 연관된 ToDo 목록에 바로 접근 가능
    # lazy=&quot;joined&quot;: Eager Loading 방식 (User 조회 시 ToDo도 함께 JOIN해서 가져옴)
    todos = relationship(&quot;ToDo&quot;, lazy=&quot;joined&quot;)

    @classmethod
    def create(cls, username: str, hashed_password: str) -&gt; &quot;User&quot;:
        return cls(
            username=username,
            password=hashed_password,
        )</code></pre>
<hr>
<h2 id="3-user-테이블-생성-mysql-ddl">3. User 테이블 생성 (MySQL DDL)</h2>
<pre><code class="language-sql">-- Docker MySQL 접속
-- docker exec -it todos bash
-- mysql -u root -p

USE todos;

-- User 테이블 생성
CREATE TABLE user (
    id INTEGER NOT NULL AUTO_INCREMENT,
    username VARCHAR(256) NOT NULL,
    password VARCHAR(256) NOT NULL,
    PRIMARY KEY (id)
);

-- todo 테이블에 user_id 컬럼 추가
ALTER TABLE todo ADD COLUMN user_id INTEGER;

-- 외래키 설정 (todo.user_id → user.id)
ALTER TABLE todo ADD FOREIGN KEY(user_id) REFERENCES user(id);

-- 샘플 유저 삽입
INSERT INTO user(username, password) VALUES (&quot;admin&quot;, &quot;password&quot;);

-- 기존 todo에 user_id 연결
UPDATE todo SET user_id=1 WHERE id=1;
UPDATE todo SET user_id=1 WHERE id=2;
UPDATE todo SET user_id=1 WHERE id=3;

-- JOIN 확인
SELECT u.username, t.contents, t.is_done
FROM todo t
JOIN user u ON t.user_id = u.id;</code></pre>
<hr>
<h2 id="4-lazy-loading-vs-eager-loading">4. Lazy Loading vs Eager Loading</h2>
<p>데이터를 언제 가져오는지에 대한 전략입니다.</p>
<h3 id="lazy-loading-지연-로딩">Lazy Loading (지연 로딩)</h3>
<p>연관된 데이터가 <strong>실제로 필요한 시점에</strong> 조회합니다.</p>
<ul>
<li><strong>장점:</strong> 첫 조회 속도가 빠름</li>
<li><strong>단점:</strong> N+1 문제 발생 가능</li>
</ul>
<p><strong>N+1 문제 예시:</strong></p>
<pre><code class="language-python"># todos를 조회하는 쿼리 1번 + 각 todo마다 user 조회 N번 = 총 N+1번 쿼리 실행
for todo in todos:
    print(todo.user.username)  # 이 시점마다 DB 조회 발생</code></pre>
<h3 id="eager-loading-즉시-로딩">Eager Loading (즉시 로딩)</h3>
<p>데이터를 조회할 때 <strong>처음부터 연관 객체를 JOIN해서</strong> 함께 가져옵니다.</p>
<ul>
<li><strong>장점:</strong> N+1 문제 방지, 데이터를 효율적으로 가져옴</li>
<li><strong>단점:</strong> 필요 없는 데이터까지 JOIN해서 가져올 수 있음</li>
</ul>
<pre><code class="language-python"># lazy=&quot;joined&quot; → Eager Loading 설정
todos = relationship(&quot;ToDo&quot;, lazy=&quot;joined&quot;)</code></pre>
<h3 id="orm-join-동작-확인-python-콘솔">ORM JOIN 동작 확인 (Python 콘솔)</h3>
<pre><code class="language-python">from database.connection import SessionFactory
from database.orm import User
from sqlalchemy import select

session = SessionFactory()
user = session.scalar(select(User))

# user.todos로 연관된 ToDo 목록 바로 접근 (이미 JOIN해서 가져왔으므로 추가 쿼리 없음)
user.todos
# [ToDo(id=1, ...), ToDo(id=2, ...), ToDo(id=3, ...)]</code></pre>
<hr>
<h2 id="5-회원가입-api">5. 회원가입 API</h2>
<h3 id="패키지-설치">패키지 설치</h3>
<pre><code class="language-bash">pip install bcrypt  # 비밀번호 해싱 라이브러리</code></pre>
<h3 id="bcrypt-동작-확인-python-콘솔">bcrypt 동작 확인 (Python 콘솔)</h3>
<pre><code class="language-python">import bcrypt

password = &quot;password&quot;
byte_password = password.encode(&quot;UTF-8&quot;)  # bytes로 변환

# 같은 비밀번호도 매번 다른 해시값이 생성됨 (salt 때문)
hash_1 = bcrypt.hashpw(byte_password, salt=bcrypt.gensalt())
hash_2 = bcrypt.hashpw(byte_password, salt=bcrypt.gensalt())

# 검증은 checkpw로 → 내부적으로 동일한 원문인지 확인
bcrypt.checkpw(byte_password, hash_1)  # True
bcrypt.checkpw(byte_password, hash_2)  # True</code></pre>
<blockquote>
<p>bcrypt는 같은 비밀번호도 매번 다른 해시값을 만들기 때문에 단순히 값을 비교하면 안 됩니다. <code>checkpw()</code>로만 검증해야 합니다.</p>
</blockquote>
<h3 id="schemarequestpy"><code>schema/request.py</code></h3>
<pre><code class="language-python">from pydantic import BaseModel

class CreateToDoRequest(BaseModel):
    contents: str
    is_done: bool

class SignUpRequest(BaseModel):
    username: str
    password: str</code></pre>
<h3 id="serviceuserpy"><code>service/user.py</code></h3>
<pre><code class="language-python">import bcrypt

class UserService:
    encoding: str = &quot;UTF-8&quot;

    def hash_password(self, plain_password: str) -&gt; str:
        # 문자열 → bytes 변환 후 해싱
        hashed_password: bytes = bcrypt.hashpw(
            plain_password.encode(self.encoding),
            salt=bcrypt.gensalt()  # 매번 랜덤한 salt 생성
        )
        # bytes → 문자열로 변환해서 반환 (DB에 저장하기 위해)
        return hashed_password.decode(self.encoding)</code></pre>
<h3 id="databaserepositorypy--userrepository-추가"><code>database/repository.py</code> — UserRepository 추가</h3>
<pre><code class="language-python">class UserRepository:
    def __init__(self, session: Session = Depends(get_db)):
        self.session = session

    def save_user(self, user: User) -&gt; User:
        self.session.add(instance=user)
        self.session.commit()           # DB 저장
        self.session.refresh(instance=user)  # 자동 생성된 id 반영
        return user</code></pre>
<h3 id="schemaresponsepy--userschema-추가"><code>schema/response.py</code> — UserSchema 추가</h3>
<pre><code class="language-python">class UserSchema(BaseModel):
    id: int
    username: str  # password는 보안상 응답에 포함하지 않음

    class Config:
        from_attributes = True</code></pre>
<h3 id="apiuserpy--회원가입-핸들러"><code>api/user.py</code> — 회원가입 핸들러</h3>
<pre><code class="language-python">from fastapi import APIRouter, Depends
from database.orm import User
from database.repository import UserRepository
from schema.response import UserSchema
from schema.request import SignUpRequest
from service.user import UserService

router = APIRouter(prefix=&quot;/users&quot;)

@router.post(&quot;/sign-up&quot;, status_code=201)
def user_sign_up_handler(
    request: SignUpRequest,
    user_service: UserService = Depends(),
    user_repo: UserRepository = Depends(),
):
    # 1. 요청에서 username, password 받기
    # 2. 비밀번호 해싱
    hashed_password: str = user_service.hash_password(
        plain_password=request.password
    )
    # 3. User 객체 생성 (id=None, DB 저장 전)
    user: User = User.create(
        username=request.username,
        hashed_password=hashed_password,
    )
    # 4. DB에 저장 (id=int, DB 저장 후)
    user: User = user_repo.save_user(user=user)
    # 5. id, username만 반환 (password 제외)
    return UserSchema.from_orm(user)</code></pre>
<h3 id="회원가입-테스트">회원가입 테스트</h3>
<pre><code class="language-python">from service.user import UserService
from database.orm import User
from database.repository import UserRepository

def test_user_sign_up(client, mocker):
    # hash_password를 mock으로 교체 → &quot;hashed&quot; 문자열 반환
    hash_password = mocker.patch.object(
        UserService,
        &quot;hash_password&quot;,
        return_value=&quot;hashed&quot;
    )
    # User.create를 mock으로 교체 → id=None인 User 반환
    user_create = mocker.patch.object(
        User,
        &quot;create&quot;,
        return_value=User(id=None, username=&quot;test&quot;, password=&quot;hashed&quot;)
    )
    # save_user를 mock으로 교체 → id=1인 User 반환 (DB 저장 후 상태)
    mocker.patch.object(
        UserRepository,
        &quot;save_user&quot;,
        return_value=User(id=1, username=&quot;test&quot;, password=&quot;hashed&quot;)
    )

    body = {&quot;username&quot;: &quot;test&quot;, &quot;password&quot;: &quot;plain&quot;}
    response = client.post(&quot;/users/sign-up&quot;, json=body)

    # hash_password가 plain 비밀번호로 호출됐는지 검증
    hash_password.assert_called_once_with(plain_password=&quot;plain&quot;)
    # User.create가 올바른 인자로 호출됐는지 검증
    user_create.assert_called_once_with(username=&quot;test&quot;, hashed_password=&quot;hashed&quot;)

    assert response.status_code == 201
    assert response.json() == {&quot;id&quot;: 1, &quot;username&quot;: &quot;test&quot;}</code></pre>
<hr>
<h2 id="6-로그인-api--jwt-인증">6. 로그인 API &amp; JWT 인증</h2>
<h3 id="jwt란">JWT란?</h3>
<p><strong>JWT(JSON Web Token)</strong> 는 사용자 인증에 사용되는 JSON 포맷의 토큰입니다.</p>
<p><strong>장점:</strong></p>
<ul>
<li>토큰 자체에 유저 정보가 담겨 있어 <strong>별도의 DB 조회 없이 인증 처리</strong> 가능</li>
<li>토큰 변조를 검증할 수 있어 <strong>내장 데이터를 신뢰</strong> 가능</li>
<li>토큰에 <strong>만료 시간</strong> 설정 가능</li>
</ul>
<h3 id="인증-절차">인증 절차</h3>
<pre><code>클라이언트                         서버
    │                               │
    │──── id &amp; password ──────────▶│
    │                          id&amp;password 검증
    │                          JWT 생성
    │◀─── JWT 반환(access_token) ───│
    │                               │
    │ JWT 저장 (localStorage 등)    │
    │                               │
    │──── API 요청 (헤더: JWT) ───▶│
    │                          JWT 검증
    │◀─── 응답 ─────────────────────│</code></pre><h3 id="패키지-설치-1">패키지 설치</h3>
<pre><code class="language-bash">pip install python-jose  # JWT 생성 및 검증 라이브러리</code></pre>
<h3 id="secret-key-생성">Secret Key 생성</h3>
<pre><code class="language-bash"># 터미널에서 랜덤한 비밀 키 생성
openssl rand -hex 32
# 출력 예: 8db9665caeab0ead6ebdf79150b7e4976a62e11e8cb15779fe06b64834b36dea</code></pre>
<blockquote>
<p>이 키는 JWT를 서명하고 검증하는 데 사용됩니다. <strong>절대 외부에 노출하면 안 됩니다.</strong></p>
</blockquote>
<h3 id="serviceuserpy--jwt-관련-메서드-추가"><code>service/user.py</code> — JWT 관련 메서드 추가</h3>
<pre><code class="language-python">import bcrypt
from datetime import datetime, timedelta
from jose import jwt

class UserService:
    encoding: str = &quot;UTF-8&quot;
    secret_key: str = &quot;8db9665caeab0ead6ebdf79150b7e4976a62e11e8cb15779fe06b64834b36dea&quot;
    jwt_algorithm: str = &quot;HS256&quot;  # 서명 알고리즘

    def hash_password(self, plain_password: str) -&gt; str:
        hashed_password: bytes = bcrypt.hashpw(
            plain_password.encode(self.encoding),
            salt=bcrypt.gensalt()
        )
        return hashed_password.decode(self.encoding)

    def verify_password(self, plain_password: str, hashed_password: str) -&gt; bool:
        # 입력한 비밀번호와 DB의 해시값을 비교
        return bcrypt.checkpw(
            plain_password.encode(self.encoding),
            hashed_password.encode(self.encoding)
        )

    def create_jwt(self, username: str) -&gt; str:
        return jwt.encode(
            {
                &quot;sub&quot;: username,       # 토큰의 주체 (유저 식별자)
                &quot;exp&quot;: datetime.now() + timedelta(days=1)  # 만료 시간: 1일
            },
            self.secret_key,
            algorithm=self.jwt_algorithm
        )

    def decode_jwt(self, access_token: str) -&gt; str:
        # 토큰을 복호화해서 payload(dict)를 꺼냄
        payload: dict = jwt.decode(
            access_token,
            self.secret_key,
            algorithms=[self.jwt_algorithm]
        )
        # sub 필드에서 username 반환
        return payload[&quot;sub&quot;]</code></pre>
<h3 id="databaserepositorypy--get_user_by_username-추가"><code>database/repository.py</code> — get_user_by_username 추가</h3>
<pre><code class="language-python">class UserRepository:
    def __init__(self, session: Session = Depends(get_db)):
        self.session = session

    def get_user_by_username(self, username: str) -&gt; User | None:
        # username으로 유저 조회
        return self.session.scalar(
            select(User).where(User.username == username)
        )

    def save_user(self, user: User) -&gt; User:
        self.session.add(instance=user)
        self.session.commit()
        self.session.refresh(instance=user)
        return user</code></pre>
<h3 id="schemaresponsepy--jwtresponse-추가"><code>schema/response.py</code> — JWTResponse 추가</h3>
<pre><code class="language-python">class JWTResponse(BaseModel):
    access_token: str  # 로그인 성공 시 반환할 JWT 토큰</code></pre>
<h3 id="apiuserpy--로그인-핸들러-추가"><code>api/user.py</code> — 로그인 핸들러 추가</h3>
<pre><code class="language-python">@router.post(&quot;/login-in&quot;)
def user_login_handler(
    request: LoginRequest,
    user_service: UserService = Depends(),
    user_repo: UserRepository = Depends(),
):
    # 1. username으로 DB에서 유저 조회
    user: User | None = user_repo.get_user_by_username(username=request.username)
    if not user:
        raise HTTPException(status_code=404, detail=&quot;User Not Found&quot;)

    # 2. 입력한 비밀번호와 DB의 해시 비밀번호 비교
    verified: bool = user_service.verify_password(
        plain_password=request.password,
        hashed_password=user.password,
    )
    if not verified:
        raise HTTPException(status_code=401, detail=&quot;Not Authorized&quot;)

    # 3. JWT 생성
    access_token: str = user_service.create_jwt(username=user.username)

    # 4. JWT 반환
    return JWTResponse(access_token=access_token)</code></pre>
<hr>
<h2 id="7-jwt-인증-적용">7. JWT 인증 적용</h2>
<p>로그인 이후 API 요청 시 JWT 토큰을 헤더에 담아 보내고, 서버에서 검증합니다.</p>
<h3 id="securitypy"><code>security.py</code></h3>
<pre><code class="language-python">from fastapi import HTTPException, Depends
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

def get_access_token(
    # HTTPBearer: Authorization 헤더에서 Bearer 토큰을 자동으로 파싱
    # auto_error=False: 토큰이 없어도 에러를 자동으로 내지 않고 None 반환
    auth_header: HTTPAuthorizationCredentials | None = Depends(HTTPBearer(auto_error=False))
) -&gt; str:
    if auth_header is None:
        raise HTTPException(
            status_code=401,
            detail=&quot;Not Authorized&quot;,
        )
    return auth_header.credentials  # Bearer 뒤의 실제 토큰 문자열 반환</code></pre>
<h3 id="apitodopy--get-전체-조회에-jwt-인증-적용"><code>api/todo.py</code> — GET 전체 조회에 JWT 인증 적용</h3>
<pre><code class="language-python">@router.get(&quot;&quot;, status_code=200)
def get_todos_handler(
    access_token: str = Depends(get_access_token),  # JWT 토큰 검증
    order: str | None = None,
    user_service: UserService = Depends(),
    user_repo: UserRepository = Depends(),
) -&gt; ToDoListSchema:
    # 토큰에서 username 추출
    username: str = user_service.decode_jwt(access_token=access_token)

    # username으로 유저 조회
    user: User | None = user_repo.get_user_by_username(username=username)
    if not user:
        raise HTTPException(status_code=404, detail=&quot;User Not Found&quot;)

    # Eager Loading으로 이미 가져온 todos 사용 (추가 DB 조회 없음)
    todos: List[ToDo] = user.todos

    if order and order == &quot;DESC&quot;:
        return ToDoListSchema(
            todos=[ToDoSchema.from_orm(todo) for todo in todos[::-1]]
        )
    return ToDoListSchema(
        todos=[ToDoSchema.from_orm(todo) for todo in todos]
    )</code></pre>
<h3 id="테스트-코드-변경--jwt-헤더-추가">테스트 코드 변경 — JWT 헤더 추가</h3>
<pre><code class="language-python">def test_get_todos(client, mocker):
    # 실제 JWT를 생성해서 테스트 헤더에 포함
    access_token: str = UserService().create_jwt(username=&quot;test&quot;)
    headers = {&quot;Authorization&quot;: f&quot;Bearer {access_token}&quot;}

    # 유저 객체에 todos를 직접 할당 (Eager Loading 시뮬레이션)
    user = User(id=1, username=&quot;test&quot;, password=&quot;hashed&quot;)
    user.todos = [
        ToDo(id=1, contents=&quot;FastAPI Section 0&quot;, is_done=True),
        ToDo(id=2, contents=&quot;FastAPI Section 1&quot;, is_done=False),
    ]

    mocker.patch.object(
        UserRepository,
        &quot;get_user_by_username&quot;,
        return_value=user
    )

    # 헤더에 JWT 포함해서 요청
    response = client.get(&quot;/todos&quot;, headers=headers)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
        ]
    }

    response = client.get(&quot;/todos?order=DESC&quot;, headers=headers)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
        ]
    }</code></pre>
<hr>
<h2 id="8-캐싱--otp">8. 캐싱 &amp; OTP</h2>
<h3 id="캐싱caching이란">캐싱(Caching)이란?</h3>
<p>데이터를 <strong>임시 저장소에 저장</strong>해서 빠르게 조회하는 기술입니다.</p>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>빠른 조회</td>
<td>DB보다 훨씬 빠름</td>
</tr>
<tr>
<td>영속성 없음</td>
<td>서버 재시작 시 데이터 소실</td>
</tr>
<tr>
<td>주요 활용</td>
<td>임시 데이터, 반복 조회 데이터, 빠른 응답이 필요한 데이터</td>
</tr>
</tbody></table>
<h3 id="otpone-time-password란">OTP(One-Time Password)란?</h3>
<p>일회용 비밀번호로, 캐싱의 대표적인 활용 사례입니다.</p>
<ul>
<li>이메일 인증 시 4자리 랜덤 숫자 생성</li>
<li>Redis에 <code>{email: otp}</code> 형태로 저장</li>
<li>3분 후 자동 만료</li>
</ul>
<h3 id="redis란">Redis란?</h3>
<p>캐싱에 자주 사용되는 <strong>key-value 데이터 스토어</strong> (NoSQL의 일종)</p>
<pre><code class="language-bash">pip install redis</code></pre>
<h3 id="redis-동작-확인-python-콘솔">Redis 동작 확인 (Python 콘솔)</h3>
<pre><code class="language-python">import redis

redis_client = redis.Redis(
    host=&quot;127.0.0.1&quot;, port=6379, db=0,
    encoding=&quot;UTF-8&quot;, decode_responses=True
)

redis_client.set(&quot;key&quot;, &quot;value&quot;)   # 저장
redis_client.get(&quot;key&quot;)            # 조회 → &#39;value&#39;
redis_client.expire(&quot;key&quot;, 10)     # 10초 후 만료 설정</code></pre>
<h3 id="cachepy"><code>cache.py</code></h3>
<pre><code class="language-python">import redis

redis_client = redis.Redis(
    host=&quot;127.0.0.1&quot;,
    port=6379,
    db=0,
    encoding=&quot;utf-8&quot;,
    decode_responses=True  # bytes가 아닌 str로 반환
)</code></pre>
<h3 id="serviceuserpy--otp-생성-메서드-추가"><code>service/user.py</code> — OTP 생성 메서드 추가</h3>
<pre><code class="language-python">import random

@staticmethod
def create_otp() -&gt; int:
    # 1000~9999 사이의 랜덤 4자리 숫자 반환
    return random.randint(1000, 9999)</code></pre>
<h3 id="schemarequestpy--otp-요청-모델-추가"><code>schema/request.py</code> — OTP 요청 모델 추가</h3>
<pre><code class="language-python">class CreateOTPRequest(BaseModel):
    email: str

class VerifyOTPRequest(BaseModel):
    email: str
    otp: int</code></pre>
<hr>
<h2 id="9-otp-생성--검증-api">9. OTP 생성 &amp; 검증 API</h2>
<h3 id="apiuserpy--otp-관련-핸들러"><code>api/user.py</code> — OTP 관련 핸들러</h3>
<pre><code class="language-python">from schema.request import CreateOTPRequest, VerifyOTPRequest
from cache import redis_client

# OTP 생성 API
# POST /users/email/otp
@router.post(&quot;/email/otp&quot;)
def create_otp_handler(
    request: CreateOTPRequest,
    _: str = Depends(get_access_token),   # 로그인한 유저만 접근 가능 (토큰 검증만 하고 값은 사용 안 함)
    user_service: UserService = Depends(),
):
    # 1. 4자리 OTP 생성
    otp: int = user_service.create_otp()

    # 2. Redis에 저장 (key: email, value: otp, 만료: 3분)
    redis_client.set(request.email, otp)
    redis_client.expire(request.email, 3 * 60)

    # 3. 실제 서비스에서는 이메일로 전송, 현재는 응답으로 반환
    return {&quot;otp&quot;: otp}


# OTP 검증 API
# POST /users/email/otp/verify
@router.post(&quot;/email/otp/verify&quot;)
def verify_otp_handler(
    request: VerifyOTPRequest,
    access_token: str = Depends(get_access_token),
    user_service: UserService = Depends(),
    user_repo: UserRepository = Depends(),
):
    # 1. Redis에서 해당 이메일의 OTP 조회
    otp: str | None = redis_client.get(request.email)

    if not otp:
        # Redis에 OTP가 없음 = 만료됐거나 요청하지 않은 이메일
        raise HTTPException(status_code=400, detail=&quot;Bad Request&quot;)

    if request.otp != int(otp):
        # 입력한 OTP가 저장된 OTP와 다름
        raise HTTPException(status_code=400, detail=&quot;Bad Request&quot;)

    # 2. 토큰에서 username 추출 후 유저 조회
    username: str = user_service.decode_jwt(access_token=access_token)
    user: User | None = user_repo.get_user_by_username(username)
    if not user:
        raise HTTPException(status_code=404, detail=&quot;User Not Found&quot;)

    # 3. 이메일 저장 (실제 구현은 생략)
    return UserSchema.from_orm(user)</code></pre>
<blockquote>
<p><code>_: str = Depends(get_access_token)</code> : 토큰 검증만 하고 값은 사용하지 않겠다는 의미로 변수명을 <code>_</code>로 씁니다.</p>
</blockquote>
<hr>
<h2 id="10-background-task-백그라운드-작업">10. Background Task (백그라운드 작업)</h2>
<p>이메일 전송처럼 <strong>시간이 오래 걸리는 작업</strong>을 응답 이후에 백그라운드에서 처리하는 기능입니다.</p>
<h3 id="일반-처리-vs-백그라운드-처리">일반 처리 vs 백그라운드 처리</h3>
<pre><code># 일반 처리 (응답이 느림)
요청 → OTP 검증 → 이메일 전송(10초) → 응답

# 백그라운드 처리 (응답이 빠름)
요청 → OTP 검증 → 응답
                 → 이메일 전송(10초, 백그라운드에서 별도 처리)</code></pre><h3 id="apiuserpy--background-task-적용"><code>api/user.py</code> — Background Task 적용</h3>
<pre><code class="language-python">from fastapi import BackgroundTasks

@router.post(&quot;/email/otp/verify&quot;)
def verify_otp_handler(
    request: VerifyOTPRequest,
    background_tasks: BackgroundTasks,   # FastAPI가 자동으로 주입해주는 백그라운드 작업 큐
    access_token: str = Depends(get_access_token),
    user_service: UserService = Depends(),
    user_repo: UserRepository = Depends(),
):
    otp: str | None = redis_client.get(request.email)
    if not otp:
        raise HTTPException(status_code=400, detail=&quot;Bad Request&quot;)
    if request.otp != int(otp):
        raise HTTPException(status_code=400, detail=&quot;Bad Request&quot;)

    username: str = user_service.decode_jwt(access_token=access_token)
    user: User | None = user_repo.get_user_by_username(username)
    if not user:
        raise HTTPException(status_code=404, detail=&quot;User Not Found&quot;)

    # 응답을 먼저 보내고, 이메일 전송은 백그라운드에서 처리
    background_tasks.add_task(
        user_service.send_email_to_user,  # 실행할 함수
        email=&quot;admin@fastapi.com&quot;         # 함수에 전달할 인자
    )

    return UserSchema.from_orm(user)</code></pre>
<hr>
<h2 id="11-전체-흐름-정리">11. 전체 흐름 정리</h2>
<pre><code>회원가입
클라이언트 → POST /users/sign-up (username, password)
           → 비밀번호 bcrypt 해싱
           → DB 저장
           → UserSchema 반환 (id, username)

로그인
클라이언트 → POST /users/login-in (username, password)
           → DB에서 유저 조회
           → bcrypt로 비밀번호 검증
           → JWT 생성 (유효기간 1일)
           → JWTResponse(access_token) 반환

인증이 필요한 API 요청
클라이언트 → GET /todos (헤더: Authorization: Bearer {token})
           → JWT 검증 (security.py)
           → 토큰에서 username 추출
           → DB에서 유저 조회
           → 유저의 todos 반환

OTP 이메일 인증
클라이언트 → POST /users/email/otp (email)
           → 4자리 OTP 생성
           → Redis 저장 (만료: 3분)
           → OTP 반환 (실제로는 이메일 전송)

           → POST /users/email/otp/verify (email, otp)
           → Redis에서 OTP 조회 및 검증
           → 이메일 저장
           → 이메일 전송 (Background Task)</code></pre><hr>
<h2 id="12-핵심-개념-요약">12. 핵심 개념 요약</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>ForeignKey</code></td>
<td>다른 테이블의 컬럼을 참조하는 외래키</td>
</tr>
<tr>
<td><code>relationship</code></td>
<td>ORM에서 연관 테이블 데이터를 객체로 접근 가능하게 함</td>
</tr>
<tr>
<td><code>lazy=&quot;joined&quot;</code></td>
<td>Eager Loading — 처음부터 JOIN해서 함께 조회</td>
</tr>
<tr>
<td><code>bcrypt</code></td>
<td>단방향 해싱 라이브러리 (비밀번호 저장용)</td>
</tr>
<tr>
<td><code>JWT</code></td>
<td>유저 정보를 담은 인증 토큰 (만료 시간 설정 가능)</td>
</tr>
<tr>
<td><code>HTTPBearer</code></td>
<td>FastAPI에서 Bearer 토큰을 헤더에서 자동 추출</td>
</tr>
<tr>
<td><code>Redis</code></td>
<td>key-value 기반 캐시 저장소</td>
</tr>
<tr>
<td><code>BackgroundTasks</code></td>
<td>응답 후 비동기로 실행되는 백그라운드 작업</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 4. 리팩터링]]></title>
            <link>https://velog.io/@woojin-archive/FastAPI-4.-%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81-hrksbgdu</link>
            <guid>https://velog.io/@woojin-archive/FastAPI-4.-%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81-hrksbgdu</guid>
            <pubDate>Sun, 15 Mar 2026 09:31:02 GMT</pubDate>
            <description><![CDATA[<h1 id="fastapi-섹션-4-리팩터링">FastAPI 섹션 4. 리팩터링</h1>
<hr>
<h2 id="1-fastapi-router">1. FastAPI Router</h2>
<p>API가 많아지면 <code>main.py</code> 하나에 모든 코드를 넣으면 관리하기 어렵습니다.
<strong>Router</strong>를 사용하면 API를 기능별로 파일을 분리해서 관리할 수 있습니다.</p>
<p><strong>파일 구조:</strong></p>
<pre><code>src/
├── api/
│   └── todo.py       ← Todo 관련 API를 여기로 분리
├── main.py           ← router만 등록하는 진입점</code></pre><h3 id="apitodopy"><code>api/todo.py</code></h3>
<pre><code class="language-python">from typing import List
from database.connection import get_db
from database.orm import ToDo
from database.repository import get_todos, get_todo_by_todo_id, create_todo, update_todo, delete_todo
from fastapi import Depends, HTTPException, Body, APIRouter
from schema.request import CreateToDoRequest
from schema.response import ToDoListSchema, ToDoSchema
from sqlalchemy.orm import Session

# prefix=&quot;/todos&quot; : 이 router의 모든 경로 앞에 /todos가 자동으로 붙음
# 덕분에 각 핸들러에서 &quot;/todos&quot;를 반복해서 쓰지 않아도 됨
router = APIRouter(prefix=&quot;/todos&quot;)

@router.get(&quot;&quot;, status_code=200)
def get_todos_handler(
    order: str | None = None,
    session: Session = Depends(get_db),
) -&gt; ToDoListSchema:
    todos: List[ToDo] = get_todos(session=session)
    if order and order == &quot;DESC&quot;:
        return ToDoListSchema(
            todos=[ToDoSchema.from_orm(todo) for todo in todos[::-1]]
        )
    return ToDoListSchema(
        todos=[ToDoSchema.from_orm(todo) for todo in todos]
    )

@router.get(&quot;/{todo_id}&quot;, status_code=200)
def get_todo_handler(
    todo_id: int,
    session: Session = Depends(get_db),
) -&gt; ToDoSchema:
    todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
    if todo:
        return ToDoSchema.from_orm(todo)
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)

@router.post(&quot;&quot;, status_code=201)
def create_todo_handler(
    request: CreateToDoRequest,
    session: Session = Depends(get_db),
) -&gt; ToDoSchema:
    todo: ToDo = ToDo.create(request=request)  # id=None (DB 저장 전)
    todo: ToDo = create_todo(session=session, todo=todo)  # id=int (DB 저장 후)
    return ToDoSchema.from_orm(todo)

@router.patch(&quot;/{todo_id}&quot;, status_code=200)
def update_todo_handler(
    todo_id: int,
    is_done: bool = Body(..., embed=True),
    session: Session = Depends(get_db),
):
    todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
    if todo:
        todo.done() if is_done else todo.undone()
        todo: ToDo = update_todo(session=session, todo=todo)
        return ToDoSchema.from_orm(todo)
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)

@router.delete(&quot;/{todo_id}&quot;, status_code=204)
def delete_todo_handler(
    todo_id: int,
    session: Session = Depends(get_db),
):
    todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
    if not todo:
        raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)
    delete_todo(session=session, todo_id=todo_id)</code></pre>
<h3 id="mainpy-router-등록"><code>main.py</code> (router 등록)</h3>
<pre><code class="language-python">from fastapi import FastAPI
from api.todo import router as todo_router

app = FastAPI()

# main.py에서는 router만 등록
app.include_router(todo_router)</code></pre>
<blockquote>
<p><strong>Before vs After:</strong></p>
<ul>
<li>Before: <code>@app.get(&quot;/todos&quot;)</code>, <code>@app.get(&quot;/todos/{todo_id}&quot;)</code> ... 모두 main.py에</li>
<li>After: <code>@router.get(&quot;&quot;)</code>, <code>@router.get(&quot;/{todo_id}&quot;)</code> ... api/todo.py로 분리</li>
</ul>
</blockquote>
<hr>
<h2 id="2-의존성-주입-dependency-injection">2. 의존성 주입 (Dependency Injection)</h2>
<h3 id="개념-이해">개념 이해</h3>
<p><strong>Coupling(결합)</strong> : 두 컴포넌트가 강하게 연결되어 있으면 하나를 바꿀 때 다른 것도 바꿔야 합니다.</p>
<p><strong>Dependency(의존성)</strong> : 한 컴포넌트가 올바르게 동작하기 위해 다른 요소에 의존하는 것입니다.</p>
<p><strong>Dependency Injection(의존성 주입)</strong> : 컴포넌트가 직접 의존성을 만들지 않고 <strong>외부에서 주입받는</strong> 방식입니다.</p>
<h3 id="예시-코드">예시 코드</h3>
<pre><code class="language-python">class Body:
    def __init__(self, color):
        self.color = color

class Car:
    def __init__(self, body):
        # Car가 직접 Body를 만들지 않고, 외부에서 주입받음
        self.body = body

    def drive(self):
        pass

# 외부에서 Body를 만들어 Car에 주입
red_car = Car(body=Body(color=&#39;red&#39;))
blue_car = Car(body=Body(color=&#39;blue&#39;))</code></pre>
<blockquote>
<p><code>Car</code>가 <code>Body</code>를 직접 생성하지 않기 때문에 색상이 바뀌어도 <code>Car</code> 코드를 수정할 필요가 없습니다.
FastAPI에서 <code>Depends()</code>가 바로 이 역할을 합니다.</p>
</blockquote>
<hr>
<h2 id="3-repository-pattern-레포지토리-패턴">3. Repository Pattern (레포지토리 패턴)</h2>
<h3 id="개념">개념</h3>
<p><strong>Repository Pattern</strong> 은 데이터를 다루는 부분을 비즈니스 로직과 분리하는 기술입니다.</p>
<ul>
<li>DB 조회/저장 같은 <strong>구체적인 구현을 캡슐화</strong>하여 숨깁니다.</li>
<li>비즈니스 로직(API 핸들러)은 데이터가 어디서 오는지 몰라도 됩니다.</li>
<li>나중에 DB를 바꿔도 Repository만 수정하면 됩니다.</li>
</ul>
<p><strong>Before (함수 방식):</strong></p>
<pre><code class="language-python"># 함수들이 흩어져 있고, session을 매번 직접 넘겨야 함
todos = get_todos(session=session)
todo = get_todo_by_todo_id(session=session, todo_id=todo_id)</code></pre>
<p><strong>After (Repository 클래스 방식):</strong></p>
<pre><code class="language-python"># 하나의 클래스로 묶여 있고, session 관리도 내부에서 처리
todos = todo_repo.get_todos()
todo = todo_repo.get_todo_by_todo_id(todo_id=todo_id)</code></pre>
<hr>
<h3 id="databaserepositorypy"><code>database/repository.py</code></h3>
<pre><code class="language-python">from typing import List
from sqlalchemy import select, delete
from sqlalchemy.orm import Session
from fastapi import Depends
from database.orm import ToDo
from database.connection import get_db

class ToDoRepository:
    def __init__(self, session: Session = Depends(get_db)):
        # FastAPI의 Depends로 세션을 자동 주입받음
        # 직접 세션을 생성하거나 닫을 필요 없음
        self.session = session

    def get_todos(self) -&gt; List[ToDo]:
        return list(self.session.scalars(select(ToDo)))

    def get_todo_by_todo_id(self, todo_id: int) -&gt; ToDo | None:
        return self.session.scalar(select(ToDo).where(ToDo.id == todo_id))

    def create_todo(self, todo: ToDo) -&gt; ToDo:
        self.session.add(instance=todo)
        self.session.commit()           # DB에 저장
        self.session.refresh(instance=todo)  # 저장 후 DB에서 최신 상태 반영 (id 등)
        return todo

    def update_todo(self, todo: ToDo) -&gt; ToDo:
        self.session.add(instance=todo)
        self.session.commit()
        self.session.refresh(instance=todo)
        return todo

    def delete_todo(self, todo_id: int) -&gt; None:
        self.session.execute(delete(ToDo).where(ToDo.id == todo_id))
        self.session.commit()</code></pre>
<hr>
<h3 id="apitodopy-repository-패턴-적용-후"><code>api/todo.py</code> (Repository 패턴 적용 후)</h3>
<pre><code class="language-python">from typing import List
from database.orm import ToDo
from database.repository import ToDoRepository
from fastapi import Depends, HTTPException, Body, APIRouter
from schema.request import CreateToDoRequest
from schema.response import ToDoListSchema, ToDoSchema

router = APIRouter(prefix=&quot;/todos&quot;)

@router.get(&quot;&quot;, status_code=200)
def get_todos_handler(
    order: str | None = None,
    todo_repo: ToDoRepository = Depends(),  # ToDoRepository 자동 생성 및 주입
) -&gt; ToDoListSchema:
    todos: List[ToDo] = todo_repo.get_todos()
    if order and order == &quot;DESC&quot;:
        return ToDoListSchema(
            todos=[ToDoSchema.from_orm(todo) for todo in todos[::-1]]
        )
    return ToDoListSchema(
        todos=[ToDoSchema.from_orm(todo) for todo in todos]
    )

@router.get(&quot;/{todo_id}&quot;, status_code=200)
def get_todo_handler(
    todo_id: int,
    todo_repo: ToDoRepository = Depends(),
) -&gt; ToDoSchema:
    todo: ToDo | None = todo_repo.get_todo_by_todo_id(todo_id=todo_id)
    if todo:
        return ToDoSchema.from_orm(todo)
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)

@router.post(&quot;&quot;, status_code=201)
def create_todo_handler(
    request: CreateToDoRequest,
    todo_repo: ToDoRepository = Depends(),
) -&gt; ToDoSchema:
    todo: ToDo = ToDo.create(request=request)       # id=None (DB 저장 전)
    todo: ToDo = todo_repo.create_todo(todo=todo)   # id=int  (DB 저장 후)
    return ToDoSchema.from_orm(todo)

@router.patch(&quot;/{todo_id}&quot;, status_code=200)
def update_todo_handler(
    todo_id: int,
    is_done: bool = Body(..., embed=True),
    todo_repo: ToDoRepository = Depends(),
):
    todo: ToDo | None = todo_repo.get_todo_by_todo_id(todo_id=todo_id)
    if todo:
        todo.done() if is_done else todo.undone()
        todo: ToDo = todo_repo.update_todo(todo=todo)
        return ToDoSchema.from_orm(todo)
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)

@router.delete(&quot;/{todo_id}&quot;, status_code=204)
def delete_todo_handler(
    todo_id: int,
    todo_repo: ToDoRepository = Depends(),
):
    todo: ToDo | None = todo_repo.get_todo_by_todo_id(todo_id=todo_id)
    if not todo:
        raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)
    todo_repo.delete_todo(todo_id=todo_id)</code></pre>
<blockquote>
<p><code>Depends()</code> 안에 아무것도 넣지 않으면 FastAPI가 타입 힌트(<code>ToDoRepository</code>)를 보고 자동으로 인스턴스를 생성하고 주입해줍니다.</p>
</blockquote>
<hr>
<h3 id="databaseconnectionpy"><code>database/connection.py</code></h3>
<pre><code class="language-python">from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = &quot;mysql+pymysql://root:todos@127.0.0.1:3306/todos&quot;
engine = create_engine(DATABASE_URL, echo=True)
SessionFactory = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db():
    session = SessionFactory()
    try:
        yield session   # 요청 처리 동안 세션 제공
    finally:
        session.close() # 요청이 끝나면 반드시 세션 닫기</code></pre>
<hr>
<h2 id="4-repository-패턴-적용-후-테스트-코드">4. Repository 패턴 적용 후 테스트 코드</h2>
<p>기존에는 <code>mocker.patch(&quot;main.get_todos&quot;, ...)</code> 처럼 함수 경로를 직접 지정했지만,
Repository 패턴 적용 후에는 <code>mocker.patch.object(ToDoRepository, &quot;메서드명&quot;, ...)</code> 으로 클래스의 메서드를 mock합니다.</p>
<h3 id="teststest_mainpy"><code>tests/test_main.py</code></h3>
<pre><code class="language-python">from database.orm import ToDo
from database.repository import ToDoRepository

def test_health_check(client):
    response = client.get(&quot;/&quot;)
    assert response.status_code == 200
    assert response.json() == {&quot;ping&quot;: &quot;pong&quot;}


def test_get_todos(client, mocker):
    # mocker.patch.object(클래스, &quot;메서드명&quot;, return_value=가짜데이터)
    # ToDoRepository의 get_todos 메서드를 가짜 데이터로 대체
    mocker.patch.object(ToDoRepository, &quot;get_todos&quot;, return_value=[
        ToDo(id=1, contents=&quot;FastAPI Section 0&quot;, is_done=True),
        ToDo(id=2, contents=&quot;FastAPI Section 1&quot;, is_done=False),
    ])

    # order=ASC
    response = client.get(&quot;/todos&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
        ]
    }

    # order=DESC (순서 반전)
    response = client.get(&quot;/todos?order=DESC&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
        ]
    }


def test_get_todo(client, mocker):
    # 200 성공
    mocker.patch.object(
        ToDoRepository,
        &quot;get_todo_by_todo_id&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )
    response = client.get(&quot;/todos/1&quot;)
    assert response.status_code == 200
    assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: True}

    # 404 실패 (존재하지 않는 todo)
    mocker.patch.object(
        ToDoRepository,
        &quot;get_todo_by_todo_id&quot;,
        return_value=None,
    )
    response = client.get(&quot;/todos/1&quot;)
    assert response.status_code == 404
    assert response.json() == {&quot;detail&quot;: &quot;ToDo Not Found&quot;}


def test_create_todo(client, mocker):
    # ToDo.create가 어떤 값으로 호출됐는지 추적 (실제로 실행은 됨)
    create_spy = mocker.spy(ToDo, &quot;create&quot;)

    # DB 저장 함수는 mock으로 대체
    mocker.patch.object(
        ToDoRepository,
        &quot;create_todo&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )

    body = {&quot;contents&quot;: &quot;test&quot;, &quot;is_done&quot;: False}
    response = client.post(&quot;/todos&quot;, json=body)

    # DB 저장 전 ToDo.create의 반환값 검증 (이 시점엔 id가 None이어야 함)
    assert create_spy.spy_return.id is None
    assert create_spy.spy_return.contents == &quot;test&quot;
    assert create_spy.spy_return.is_done is False

    # DB 저장 후 응답값 검증 (mock이 반환한 값 기준)
    assert response.status_code == 201
    assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: True}


def test_update_todo(client, mocker):
    # 200 성공
    mocker.patch.object(
        ToDoRepository,
        &quot;get_todo_by_todo_id&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )

    # ToDo.undone()이 실제로 호출됐는지 검증하기 위해 mock으로 교체
    undone = mocker.patch.object(ToDo, &quot;undone&quot;)

    mocker.patch.object(
        ToDoRepository,
        &quot;update_todo&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=False),
    )

    response = client.patch(&quot;/todos/1&quot;, json={&quot;is_done&quot;: False})

    # is_done=False이므로 undone()이 정확히 1번 호출됐어야 함
    undone.assert_called_once_with()
    assert response.status_code == 200
    assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: False}

    # 404 실패
    mocker.patch.object(
        ToDoRepository,
        &quot;get_todo_by_todo_id&quot;,
        return_value=None,
    )
    response = client.patch(&quot;/todos/1&quot;, json={&quot;is_done&quot;: True})
    assert response.status_code == 404
    assert response.json() == {&quot;detail&quot;: &quot;ToDo Not Found&quot;}


def test_delete_todo(client, mocker):
    # 204 성공
    mocker.patch.object(
        ToDoRepository,
        &quot;get_todo_by_todo_id&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )
    mocker.patch.object(ToDoRepository, &quot;delete_todo&quot;, return_value=None)

    response = client.delete(&quot;/todos/1&quot;)
    assert response.status_code == 204
    # 204 No Content: 응답 본문 없음 → json() 검증 없음

    # 404 실패
    mocker.patch.object(
        ToDoRepository,
        &quot;get_todo_by_todo_id&quot;,
        return_value=None,
    )
    response = client.delete(&quot;/todos/1&quot;)
    assert response.status_code == 404
    assert response.json() == {&quot;detail&quot;: &quot;ToDo Not Found&quot;}</code></pre>
<hr>
<h2 id="5-전체-흐름-정리">5. 전체 흐름 정리</h2>
<pre><code>클라이언트 요청
     ↓
main.py (router 등록)
     ↓
api/todo.py (APIRouter) ← 기능별 API 분리
     ↓
ToDoRepository (Depends로 자동 주입) ← DB 로직 캡슐화
     ↓
database/orm.py (ORM 모델)
     ↓
database/connection.py (DB 세션)
     ↓
MySQL</code></pre><hr>
<h2 id="6-핵심-개념-요약">6. 핵심 개념 요약</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>APIRouter</code></td>
<td>API를 기능별 파일로 분리하는 FastAPI 기능</td>
</tr>
<tr>
<td><code>prefix=&quot;/todos&quot;</code></td>
<td>router의 모든 경로 앞에 자동으로 붙는 공통 경로</td>
</tr>
<tr>
<td><code>Depends()</code></td>
<td>FastAPI가 자동으로 의존성을 생성하고 주입해주는 기능</td>
</tr>
<tr>
<td><code>Repository Pattern</code></td>
<td>DB 로직을 비즈니스 로직과 분리하는 설계 패턴</td>
</tr>
<tr>
<td><code>mocker.patch.object</code></td>
<td>클래스의 특정 메서드를 mock으로 교체</td>
</tr>
<tr>
<td><code>session.refresh()</code></td>
<td>DB 저장 후 자동 생성된 id 등 최신 상태를 객체에 반영</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 3. 테스트 코드]]></title>
            <link>https://velog.io/@woojin-archive/FastAPI-3.-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@woojin-archive/FastAPI-3.-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C</guid>
            <pubDate>Sun, 15 Mar 2026 07:53:42 GMT</pubDate>
            <description><![CDATA[<h1 id="fastapi-섹션-3-테스트-코드">FastAPI 섹션 3. 테스트 코드</h1>
<hr>
<h2 id="1-테스트-코드란">1. 테스트 코드란?</h2>
<p>시스템의 <strong>품질과 신뢰성을 검증</strong>하기 위한 코드입니다.</p>
<p><strong>장점:</strong></p>
<ul>
<li><code>코드 변경 → 기능 점검</code> 과정을 자동화하여 반복 작업을 줄이고 생산성 향상</li>
<li>시스템에 대한 안정성 확신 → 유연한 코드 변경 및 리팩터링 가능</li>
</ul>
<blockquote>
<p><strong>심화 키워드:</strong> Unit Test, Integration Test, Regression Test, TDD, BDD 등 다양한 테스트 종류와 방법론이 존재합니다.</p>
</blockquote>
<hr>
<h2 id="2-pytest란">2. PyTest란?</h2>
<p>테스트 코드 작성을 위한 Python 라이브러리입니다.</p>
<p><strong>기본 Unittest 대비 장점:</strong></p>
<ul>
<li>간결한 문법 (<code>assert</code> 문)</li>
<li>함수 단위 테스트 지원</li>
<li><strong>fixture</strong> 지원 (테스트 데이터 관리)</li>
</ul>
<hr>
<h2 id="3-pytest-세팅">3. PyTest 세팅</h2>
<pre><code class="language-bash">pip install pytest       # 테스트 라이브러리
pip install httpx        # TestClient가 내부적으로 사용하는 HTTP 클라이언트
pip install pytest-mock  # mocker 기능 사용 (Mocking)</code></pre>
<p><strong>파일 구조:</strong></p>
<pre><code>src/
└── test/          ← Python Package로 생성
    └── test_*.py  ← 파일명 반드시 test_ 로 시작해야 PyTest가 인식함</code></pre><hr>
<h2 id="4-fixture란">4. Fixture란?</h2>
<p>테스트마다 반복되는 <strong>공통 준비 작업을 함수로 분리</strong>하는 기능입니다.</p>
<p>예를 들어 <code>TestClient</code>를 매 테스트마다 만들지 않고, fixture로 한 번만 정의하고 재사용합니다.</p>
<pre><code class="language-python"># conftest.py
import pytest
from fastapi.testclient import TestClient
from main import app

@pytest.fixture
def client():
    # 이 함수가 반환하는 값이 테스트 함수의 인자로 자동 주입됨
    return TestClient(app=app)</code></pre>
<blockquote>
<p><code>@pytest.fixture</code> 로 정의한 함수는 테스트 함수의 <strong>인자 이름</strong>과 일치하면 자동으로 주입됩니다.</p>
</blockquote>
<hr>
<h2 id="5-mocking이란">5. Mocking이란?</h2>
<p>실제 DB나 외부 서비스에 의존하지 않고, <strong>가짜 데이터를 반환하도록 함수를 대체</strong>하는 기술입니다.</p>
<ul>
<li>테스트가 DB 상태에 영향을 받지 않아서 <strong>독립적이고 빠른 테스트</strong> 가능</li>
<li><code>mocker.patch(&quot;경로.함수명&quot;, return_value=가짜데이터)</code> 형태로 사용</li>
</ul>
<hr>
<h2 id="6-health-check-테스트">6. Health Check 테스트</h2>
<pre><code class="language-python">from database.orm import ToDo

def test_health_check(client):
    response = client.get(&quot;/&quot;)
    assert response.status_code == 200
    assert response.json() == {&quot;ping&quot;: &quot;pong&quot;}</code></pre>
<blockquote>
<p>서버가 정상적으로 실행되고 있는지 확인하는 가장 기본적인 테스트입니다.</p>
</blockquote>
<hr>
<h2 id="7-get-전체-조회-테스트">7. GET 전체 조회 테스트</h2>
<pre><code class="language-python">def test_get_todos(client, mocker):
    # mocker.patch로 실제 DB 조회 함수를 가짜 데이터로 대체
    # &quot;main.get_todos&quot; : main.py 안의 get_todos 함수를 mock으로 교체
    mocker.patch(&quot;main.get_todos&quot;, return_value=[
        ToDo(id=1, contents=&quot;FastAPI Section 0&quot;, is_done=True),
        ToDo(id=2, contents=&quot;FastAPI Section 1&quot;, is_done=False),
    ])

    # order=ASC (기본값)
    response = client.get(&quot;/todos&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
        ]
    }

    # order=DESC (순서 반전)
    response = client.get(&quot;/todos?order=DESC&quot;)
    assert response.status_code == 200
    assert response.json() == {
        &quot;todos&quot;: [
            {&quot;id&quot;: 2, &quot;contents&quot;: &quot;FastAPI Section 1&quot;, &quot;is_done&quot;: False},
            {&quot;id&quot;: 1, &quot;contents&quot;: &quot;FastAPI Section 0&quot;, &quot;is_done&quot;: True},
        ]
    }</code></pre>
<blockquote>
<p><code>mocker.patch</code>로 대체한 데이터를 기준으로 DESC는 단순히 순서를 뒤집은 결과를 검증합니다.</p>
</blockquote>
<hr>
<h2 id="8-get-단일-조회-테스트">8. GET 단일 조회 테스트</h2>
<pre><code class="language-python">def test_get_todo(client, mocker):
    # --- 200 성공 케이스 ---
    mocker.patch(
        &quot;main.get_todo_by_todo_id&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )
    response = client.get(&quot;/todos/1&quot;)
    assert response.status_code == 200
    assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: True}

    # --- 404 실패 케이스 ---
    # return_value=None : 해당 id의 ToDo가 없는 상황을 mock으로 재현
    mocker.patch(
        &quot;main.get_todo_by_todo_id&quot;,
        return_value=None,
    )
    response = client.get(&quot;/todos/1&quot;)
    assert response.status_code == 404
    assert response.json() == {&quot;detail&quot;: &quot;ToDo Not Found&quot;}</code></pre>
<blockquote>
<p>하나의 테스트 함수 안에서 <strong>성공(200)과 실패(404) 케이스를 모두 검증</strong>합니다.
<code>return_value=None</code>으로 설정하면 핸들러에서 HTTPException이 발생하는 경로를 테스트할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="9-post-생성-테스트">9. POST 생성 테스트</h2>
<pre><code class="language-python">def test_create_todo(client, mocker):
    # mocker.spy: 함수를 대체하지 않고 호출 결과를 추적(감시)하는 기능
    # ToDo.create 메서드가 어떤 값으로 호출됐는지 확인하기 위해 사용
    create_spy = mocker.spy(ToDo, &quot;create&quot;)

    # create_todo(DB 저장 함수)는 mock으로 대체 → 실제 DB 저장 없이 결과만 반환
    mocker.patch(
        &quot;main.create_todo&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )

    body = {
        &quot;contents&quot;: &quot;test&quot;,
        &quot;is_done&quot;: False,
    }
    response = client.post(&quot;/todos&quot;, json=body)

    # spy_return: ToDo.create가 반환한 객체 (DB 저장 전 상태)
    # DB 저장 전이라 id는 None이어야 함
    assert create_spy.spy_return.id is None
    assert create_spy.spy_return.contents == &quot;test&quot;
    assert create_spy.spy_return.is_done is False

    # DB 저장 후 응답은 mock이 반환한 값 기준
    assert response.status_code == 201
    assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: True}</code></pre>
<blockquote>
<p><strong><code>mocker.patch</code> vs <code>mocker.spy</code> 차이:</strong></p>
<ul>
<li><code>mocker.patch</code> : 함수를 <strong>완전히 가짜로 교체</strong> (실제 함수 실행 안 함)</li>
<li><code>mocker.spy</code> : 함수를 <strong>그대로 실행하되 호출 결과를 추적</strong> (spy_return으로 확인 가능)</li>
</ul>
</blockquote>
<hr>
<h2 id="10-patch-수정-테스트">10. PATCH 수정 테스트</h2>
<pre><code class="language-python">def test_update_todo(client, mocker):
    # --- 200 성공 케이스 ---
    mocker.patch(
        &quot;main.get_todo_by_todo_id&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )

    # mocker.patch.object: 특정 클래스의 메서드를 mock으로 교체
    # ToDo.undone() 이 실제로 호출됐는지 검증하기 위해 spy처럼 활용
    undone = mocker.patch.object(ToDo, &quot;undone&quot;)

    mocker.patch(
        &quot;main.update_todo&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=False),
    )

    response = client.patch(&quot;/todos/1&quot;, json={&quot;is_done&quot;: False})

    # is_done=False를 보냈으므로 undone()이 정확히 한 번 호출됐어야 함
    undone.assert_called_once_with()
    assert response.status_code == 200
    assert response.json() == {&quot;id&quot;: 1, &quot;contents&quot;: &quot;todo&quot;, &quot;is_done&quot;: False}

    # --- 404 실패 케이스 ---
    mocker.patch(
        &quot;main.get_todo_by_todo_id&quot;,
        return_value=None,
    )
    response = client.patch(&quot;/todos/1&quot;, json={&quot;is_done&quot;: True})
    assert response.status_code == 404
    assert response.json() == {&quot;detail&quot;: &quot;ToDo Not Found&quot;}</code></pre>
<blockquote>
<p><code>undone.assert_called_once_with()</code> : mock으로 교체된 <code>undone</code> 메서드가 <strong>정확히 1번 호출됐는지</strong> 검증합니다.
이를 통해 <code>is_done=False</code>일 때 <code>undone()</code>이 실행되는 로직을 테스트할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="11-delete-삭제-테스트">11. DELETE 삭제 테스트</h2>
<pre><code class="language-python">def test_delete_todo(client, mocker):
    # --- 204 성공 케이스 ---
    mocker.patch(
        &quot;main.get_todo_by_todo_id&quot;,
        return_value=ToDo(id=1, contents=&quot;todo&quot;, is_done=True),
    )
    # delete_todo는 반환값이 없으므로 return_value=None
    mocker.patch(&quot;main.delete_todo&quot;, return_value=None)

    response = client.delete(&quot;/todos/1&quot;)
    assert response.status_code == 204
    # 204 No Content: 성공했지만 응답 본문이 없으므로 json() 검증 없음

    # --- 404 실패 케이스 ---
    mocker.patch(
        &quot;main.get_todo_by_todo_id&quot;,
        return_value=None,
    )
    response = client.delete(&quot;/todos/1&quot;)
    assert response.status_code == 404
    assert response.json() == {&quot;detail&quot;: &quot;ToDo Not Found&quot;}</code></pre>
<blockquote>
<p>DELETE 성공 시 상태코드는 <code>204 No Content</code>로, 응답 본문이 없기 때문에 <code>response.json()</code> 검증을 하지 않습니다.</p>
</blockquote>
<hr>
<h2 id="12-전체-흐름-정리">12. 전체 흐름 정리</h2>
<pre><code>테스트 함수 실행
     ↓
fixture(client) 자동 주입 → TestClient 생성
     ↓
mocker.patch로 DB 함수를 가짜 데이터로 대체
     ↓
client.get/post/patch/delete 로 API 호출
     ↓
response.status_code / response.json() 검증
     ↓
assert 통과 → 테스트 성공 ✅ / 실패 → 테스트 실패 ❌</code></pre><hr>
<h2 id="13-핵심-개념-요약">13. 핵심 개념 요약</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>@pytest.fixture</code></td>
<td>테스트에서 공통으로 쓰는 객체를 한 번만 정의하고 재사용</td>
</tr>
<tr>
<td><code>mocker.patch</code></td>
<td>함수를 가짜로 완전히 교체 (실제 실행 안 함)</td>
</tr>
<tr>
<td><code>mocker.spy</code></td>
<td>함수를 실행하면서 결과를 추적</td>
</tr>
<tr>
<td><code>mocker.patch.object</code></td>
<td>특정 클래스의 메서드를 mock으로 교체</td>
</tr>
<tr>
<td><code>assert_called_once_with()</code></td>
<td>mock 함수가 정확히 1번 호출됐는지 검증</td>
</tr>
<tr>
<td><code>spy_return</code></td>
<td>spy로 추적한 함수의 반환값 확인</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI 2. 데이터베이스]]></title>
            <link>https://velog.io/@woojin-archive/FastAPI-2.-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@woojin-archive/FastAPI-2.-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Sat, 14 Mar 2026 07:56:54 GMT</pubDate>
            <description><![CDATA[<h1 id="fastapi-섹션-2-데이터베이스">FastAPI 섹션 2. 데이터베이스</h1>
<hr>
<h2 id="1-sqlalchemy란">1. SQLAlchemy란?</h2>
<p><strong>SQLAlchemy</strong>는 Python에서 관계형 데이터베이스를 편리하게 사용할 수 있도록 도와주는 라이브러리입니다.</p>
<p>주요 기능:</p>
<ul>
<li><strong>ORM</strong> (Object-Relational Mapping)</li>
<li><strong>Query</strong> (쿼리 생성)</li>
<li><strong>Transaction</strong> (트랜잭션 처리)</li>
<li><strong>Connection Pooling</strong> (DB 연결 관리)</li>
</ul>
<hr>
<h2 id="2-orm이란">2. ORM이란?</h2>
<p><strong>ORM(Object-Relational Mapping)</strong> 은 관계형 데이터베이스의 테이블을 Python 클래스(객체)처럼 다룰 수 있게 해주는 기술입니다.</p>
<table>
<thead>
<tr>
<th>데이터베이스 개념</th>
<th>Python 개념</th>
</tr>
</thead>
<tbody><tr>
<td>테이블 (Table)</td>
<td>클래스 (Class)</td>
</tr>
<tr>
<td>행/레코드 (Row)</td>
<td>객체 (Object)</td>
</tr>
<tr>
<td>컬럼 (Column)</td>
<td>속성 (Attribute)</td>
</tr>
</tbody></table>
<p><strong>예시:</strong></p>
<p>데이터베이스 테이블:</p>
<table>
<thead>
<tr>
<th>id</th>
<th>username</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>qu3vipon</td>
</tr>
</tbody></table>
<p>Python ORM 코드:</p>
<pre><code class="language-python">user = User(id=1, username=&#39;qu3vipon&#39;)</code></pre>
<blockquote>
<p>즉, SQL을 직접 쓰지 않고 Python 코드로 DB를 조작할 수 있게 됩니다.</p>
</blockquote>
<hr>
<h2 id="3-docker로-mysql-실행하기">3. Docker로 MySQL 실행하기</h2>
<p>로컬에 MySQL을 직접 설치하지 않고, Docker를 이용해 간편하게 실행합니다.</p>
<pre><code class="language-bash"># MySQL 컨테이너 실행
docker run -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=todos \
  -e MYSQL_DATABASE=todos \
  -d \
  -v todos:/db \
  --name todos \
  mysql:8.0

# 실행 중인 컨테이너 확인
docker ps

# 컨테이너 로그 확인
docker logs todos

# 볼륨 목록 확인
docker volume ls</code></pre>
<p><strong>명령어 옵션 설명:</strong></p>
<ul>
<li><code>-p 3306:3306</code> : 로컬 포트 3306 ↔ 컨테이너 포트 3306 연결</li>
<li><code>-e MYSQL_ROOT_PASSWORD=todos</code> : root 비밀번호 설정</li>
<li><code>-e MYSQL_DATABASE=todos</code> : 기본 DB 이름 설정</li>
<li><code>-d</code> : 백그라운드 실행</li>
<li><code>-v todos:/db</code> : 데이터 영속성을 위한 볼륨 마운트</li>
<li><code>--name todos</code> : 컨테이너 이름 지정</li>
</ul>
<hr>
<h2 id="4-mysql-접속--테이블-생성-ddl">4. MySQL 접속 &amp; 테이블 생성 (DDL)</h2>
<pre><code class="language-bash"># MySQL 컨테이너 내부 접속
docker exec -it todos bash

# MySQL 로그인
mysql -u root -p</code></pre>
<pre><code class="language-sql">-- 데이터베이스 목록 확인
SHOW databases;

-- todos DB 선택
USE todos;

-- todo 테이블 생성
CREATE TABLE todo(
    id INT NOT NULL AUTO_INCREMENT,
    contents VARCHAR(256) NOT NULL,
    is_done BOOLEAN NOT NULL,
    PRIMARY KEY (id)
);

-- 샘플 데이터 삽입
INSERT INTO todo (contents, is_done) VALUES (&quot;FastAPI Section 0&quot;, true);

-- 데이터 조회
SELECT * FROM todo;</code></pre>
<hr>
<h2 id="5-데이터베이스-연결-설정">5. 데이터베이스 연결 설정</h2>
<h3 id="패키지-설치">패키지 설치</h3>
<pre><code class="language-bash">pip install SQLAlchemy   # ORM 라이브러리
pip install pyMySQL      # Python ↔ MySQL 연결 드라이버
pip install cryptography # MySQL 인증/암호화 처리</code></pre>
<h3 id="databaseconnectionpy"><code>database/connection.py</code></h3>
<pre><code class="language-python">from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# DB 연결 주소 (형식: mysql+pymysql://유저:비밀번호@호스트:포트/DB명)
DATABASE_URL = &quot;mysql+pymysql://root:todos@127.0.0.1:3306/todos&quot;

# 엔진 생성 (실제 DB와 연결하는 핵심 객체, echo=True는 실행된 SQL을 콘솔에 출력)
engine = create_engine(DATABASE_URL, echo=True)

# 세션 팩토리 생성 (DB와 대화하는 세션을 만드는 공장)
# autocommit=False : 명시적으로 commit() 해야 DB에 반영됨
# autoflush=False  : 명시적으로 flush() 하기 전까지 쿼리를 DB에 보내지 않음
SessionFactory = sessionmaker(autocommit=False, autoflush=False, bind=engine)</code></pre>
<h3 id="연결-확인-python-콘솔">연결 확인 (Python 콘솔)</h3>
<pre><code class="language-python">&gt;&gt;&gt; from database.connection import SessionFactory
&gt;&gt;&gt; session = SessionFactory()
&gt;&gt;&gt; from sqlalchemy import select
&gt;&gt;&gt; session.scalar(select(1))  # 1이 반환되면 연결 성공!</code></pre>
<hr>
<h2 id="6-orm-모델링">6. ORM 모델링</h2>
<h3 id="databaseormpy"><code>database/orm.py</code></h3>
<pre><code class="language-python">from sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import declarative_base

Base = declarative_base()  # 모든 ORM 모델의 부모 클래스

class ToDo(Base):
    __tablename__ = &quot;todo&quot;  # 연결할 실제 DB 테이블 이름

    # 컬럼 정의 (DB 테이블의 각 컬럼과 1:1 매핑)
    id = Column(Integer, primary_key=True, index=True)
    contents = Column(String(256), nullable=False)
    is_done = Column(Boolean, nullable=False)

    def __repr__(self):
        # 객체를 출력할 때 보여줄 문자열 형식
        return f&quot;ToDo(id={self.id}, contents={self.contents}, is_done={self.is_done})&quot;</code></pre>
<h3 id="orm-모델-동작-확인-python-콘솔">ORM 모델 동작 확인 (Python 콘솔)</h3>
<pre><code class="language-python">&gt;&gt;&gt; from database.connection import SessionFactory
&gt;&gt;&gt; session = SessionFactory()
&gt;&gt;&gt; from sqlalchemy import select
&gt;&gt;&gt; from database.orm import ToDo

# 모든 ToDo 조회
&gt;&gt;&gt; todos = list(session.scalars(select(ToDo)))
&gt;&gt;&gt; todos

# 하나씩 출력
&gt;&gt;&gt; for todo in todos:
...     print(todo)</code></pre>
<hr>
<h2 id="7-scalar-vs-scalars-차이">7. <code>scalar</code> vs <code>scalars</code> 차이</h2>
<p>SQLAlchemy에서 헷갈리기 쉬운 두 메서드의 차이입니다.</p>
<pre><code class="language-python"># scalars() - 여러 행을 ORM 객체 리스트로 반환
session.scalars(select(ToDo))
# → [ToDo(id=1, ...), ToDo(id=2, ...), ...]

# scalar() - 단일 행 하나만 반환
session.scalar(select(ToDo).where(ToDo.id == 1))
# → ToDo(id=1, ...) 객체 하나</code></pre>
<blockquote>
<p>쿼리 결과는 원래 행/열의 2차원 테이블 형태인데, 첫 번째 열만 뽑아서 단순하게(scalar) 만들어줍니다.
덕분에 <code>Row</code> 객체가 아니라 바로 <code>ToDo</code> 객체로 받을 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="8-orm-적용--crud-api-구현">8. ORM 적용 — CRUD API 구현</h2>
<h3 id="사전-준비-의존성-주입-db-세션">사전 준비: 의존성 주입 (DB 세션)</h3>
<p>FastAPI의 <code>Depends</code>를 이용해 각 API 핸들러에 DB 세션을 자동으로 주입합니다.</p>
<pre><code class="language-python"># database/connection.py 에 추가
from sqlalchemy.orm import Session

def get_db():
    # 요청마다 새 세션을 열고, 요청이 끝나면 자동으로 닫아줌
    db = SessionFactory()
    try:
        yield db
    finally:
        db.close()</code></pre>
<hr>
<h3 id="response-스키마-정의">Response 스키마 정의</h3>
<p>API가 반환할 데이터 형식을 Pydantic으로 정의합니다.</p>
<pre><code class="language-python"># schema/response.py
from pydantic import BaseModel
from typing import List

class ToDoSchema(BaseModel):
    id: int
    contents: str
    is_done: bool

    class Config:
        from_attributes = True  # ORM 객체 → Pydantic 변환 허용 (구버전: orm_mode = True)

class ListToDoResponse(BaseModel):
    todos: List[ToDoSchema]</code></pre>
<hr>
<h3 id="get--전체-조회">GET — 전체 조회</h3>
<pre><code class="language-python"># database/repository.py
from typing import List
from sqlalchemy import select
from sqlalchemy.orm import Session
from database.orm import ToDo

def get_todos(session: Session) -&gt; List[ToDo]:
    return list(session.scalars(select(ToDo)))</code></pre>
<pre><code class="language-python"># main.py
@app.get(&quot;/todos&quot;, status_code=200)
def get_todos_handler(
    order: str | None = None,
    session: Session = Depends(get_db),  # DB 세션 자동 주입
) -&gt; ListToDoResponse:
    todos: List[ToDo] = get_todos(session=session)

    if order and order == &quot;DESC&quot;:
        return ListToDoResponse(
            todos=[ToDoSchema.from_orm(todo) for todo in todos[::-1]]
        )
    return ListToDoResponse(
        todos=[ToDoSchema.from_orm(todo) for todo in todos]
    )</code></pre>
<blockquote>
<p><code>ToDoSchema.from_orm(todo)</code> : ORM 객체(ToDo)를 Pydantic 스키마(ToDoSchema)로 변환합니다.</p>
</blockquote>
<hr>
<h3 id="get--단일-조회">GET — 단일 조회</h3>
<pre><code class="language-python"># database/repository.py 에 추가
def get_todo_by_todo_id(session: Session, todo_id: int) -&gt; ToDo | None:
    # where 조건으로 특정 id를 가진 행 하나만 반환
    return session.scalar(select(ToDo).where(ToDo.id == todo_id))</code></pre>
<pre><code class="language-python"># main.py
@app.get(&quot;/todos/{todo_id}&quot;, status_code=200)
def get_todo_handler(
    todo_id: int,
    session: Session = Depends(get_db),
) -&gt; ToDoSchema:
    todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
    if todo:
        return ToDoSchema.from_orm(todo)
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)</code></pre>
<hr>
<h3 id="post--생성">POST — 생성</h3>
<pre><code class="language-python"># schema/request.py
from pydantic import BaseModel

class CreateToDoRequest(BaseModel):
    contents: str
    is_done: bool
    # id는 DB가 AUTO_INCREMENT로 자동 생성하므로 요청에 포함하지 않음</code></pre>
<pre><code class="language-python"># database/repository.py 에 추가
def create_todo(session: Session, todo: ToDo) -&gt; ToDo:
    session.add(instance=todo)   # 세션에 객체 추가 (아직 DB에 저장 안 됨)
    session.commit()              # DB에 실제로 저장 (INSERT 실행)
    session.refresh(instance=todo)  # DB에서 최신 상태 다시 읽어오기 (자동 생성된 id 반영)
    return todo</code></pre>
<pre><code class="language-python"># main.py
@app.post(&quot;/todos&quot;, status_code=201)
def create_todo_handler(
    request: CreateToDoRequest,
    session: Session = Depends(get_db),
) -&gt; ToDoSchema:
    todo: ToDo = ToDo.create(request=request)  # ORM 객체 생성 (id=None 상태)
    todo: ToDo = create_todo(session=session, todo=todo)  # DB 저장 후 id 할당됨
    return ToDoSchema.from_orm(todo)</code></pre>
<blockquote>
<p><code>session.refresh()</code> 가 중요한 이유: DB에 저장 후 AUTO_INCREMENT로 생성된 <code>id</code> 값을 Python 객체에 반영하기 위해 필요합니다.</p>
</blockquote>
<hr>
<h3 id="patch--수정">PATCH — 수정</h3>
<pre><code class="language-python"># database/repository.py 에 추가
def update_todo(session: Session, todo: ToDo) -&gt; ToDo:
    session.add(instance=todo)   # 변경된 객체를 세션에 반영
    session.commit()              # DB에 UPDATE 실행
    session.refresh(instance=todo)  # 최신 상태 다시 읽기
    return todo</code></pre>
<pre><code class="language-python"># main.py
@app.patch(&quot;/todos/{todo_id}&quot;, status_code=200)
def update_todo_handler(
    todo_id: int,
    is_done: bool = Body(..., embed=True),
    session: Session = Depends(get_db),
):
    todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
    if todo:
        # is_done 값에 따라 done() 또는 undone() 메서드 호출
        todo.done() if is_done else todo.undone()
        todo: ToDo = update_todo(session=session, todo=todo)
        return ToDoSchema.from_orm(todo)
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)</code></pre>
<hr>
<h3 id="delete--삭제">DELETE — 삭제</h3>
<pre><code class="language-python"># database/repository.py 에 추가
from sqlalchemy import delete

def delete_todo(session: Session, todo_id: int) -&gt; None:
    # DELETE FROM todo WHERE id = todo_id 실행
    session.execute(delete(ToDo).where(ToDo.id == todo_id))
    session.commit()</code></pre>
<pre><code class="language-python"># main.py
@app.delete(&quot;/todos/{todo_id}&quot;, status_code=204)
def delete_todo_handler(
    todo_id: int,
    session: Session = Depends(get_db),
):
    todo: ToDo | None = get_todo_by_todo_id(session=session, todo_id=todo_id)
    if not todo:
        raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)
    delete_todo(session=session, todo_id=todo_id)
    # 204 No Content: 성공했지만 반환할 데이터 없음 → return 생략</code></pre>
<hr>
<h2 id="9-리팩토링-팁-pycharm--ide">9. 리팩토링 팁 (PyCharm / IDE)</h2>
<p>코드가 길어지면 파일을 분리해서 관리하는 게 중요합니다.</p>
<ul>
<li><strong>Move</strong> : 클래스/함수를 드래그 후 우클릭 → 다른 파일로 이동</li>
<li><strong>Rename</strong> : 클래스/변수 이름 변경 시 사용된 모든 곳에 자동 반영</li>
</ul>
<hr>
<h2 id="10-전체-흐름-정리">10. 전체 흐름 정리</h2>
<pre><code>클라이언트 요청
     ↓
FastAPI 라우터 (main.py)
     ↓
Repository 함수 (database/repository.py) ← DB 쿼리 로직 분리
     ↓
ORM 모델 (database/orm.py) ← 테이블 구조 정의
     ↓
SQLAlchemy Engine (database/connection.py) ← 실제 DB 연결
     ↓
MySQL (Docker)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI]]></title>
            <link>https://velog.io/@woojin-archive/FastAPI-q82kg0cl</link>
            <guid>https://velog.io/@woojin-archive/FastAPI-q82kg0cl</guid>
            <pubDate>Thu, 12 Mar 2026 12:12:35 GMT</pubDate>
            <description><![CDATA[<h1 id="fastapi-정리">FastAPI 정리</h1>
<h2 id="1-기본-crud-api">1. 기본 CRUD API</h2>
<blockquote>
<p>웹 서버 실행 후 코드를 변경하면, 웹 서버를 <strong>재시작</strong>해야 변경사항이 Swagger UI에 반영됩니다.</p>
<p><strong>FastAPI 서버 종료</strong>: <code>Ctrl + C</code></p>
</blockquote>
<hr>
<h3 id="전체-조회-get">전체 조회 (GET)</h3>
<pre><code class="language-python">@app.get(&quot;/todos&quot;)</code></pre>
<h3 id="단일-조회-get">단일 조회 (GET)</h3>
<pre><code class="language-python">@app.get(&quot;/todos/{todo_id}&quot;)</code></pre>
<h3 id="생성-post">생성 (POST)</h3>
<pre><code class="language-python">from pydantic import BaseModel

class CreateToDoRequest(BaseModel):
    id: int
    contents: str
    is_done: bool

@app.post(&quot;/todos&quot;)
def create_todo_handler(request: CreateToDoRequest):
    todo_data[request.id] = request.dict()
    return todo_data[request.id]</code></pre>
<h3 id="수정-patch">수정 (PATCH)</h3>
<pre><code class="language-python">from fastapi import FastAPI, Body

@app.patch(&quot;/todos/{todo_id}&quot;)
def update_todo_handler(
    todo_id: int,
    is_done: bool = Body(..., embed=True),
):
    todo = todo_data.get(todo_id)
    if todo:
        todo[&quot;is_done&quot;] = is_done
        return todo
    return {}</code></pre>
<h3 id="삭제-delete">삭제 (DELETE)</h3>
<pre><code class="language-python">@app.delete(&quot;/todos/{todo_id}&quot;)
def delete_todo_handler(todo_id: int):
    todo_data.pop(todo_id, None)
    return todo_data</code></pre>
<hr>
<h2 id="2-http-status-code">2. HTTP Status Code</h2>
<h3 id="2xx--성공">2xx — 성공</h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>이름</th>
<th>의미 및 발생 상황</th>
</tr>
</thead>
<tbody><tr>
<td>200</td>
<td>OK</td>
<td>요청이 성공적으로 처리됨 (가장 일반적인 성공 응답)</td>
</tr>
<tr>
<td>201</td>
<td>Created</td>
<td>요청이 성공하여 새로운 리소스가 생성됨 (회원가입, 포스트 작성 성공 등)</td>
</tr>
<tr>
<td>204</td>
<td>No Content</td>
<td>요청은 성공했으나 응답 본문에 보낼 데이터가 없음 (주로 DELETE 성공 시 사용)</td>
</tr>
</tbody></table>
<h3 id="4xx--클라이언트-오류">4xx — 클라이언트 오류</h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>이름</th>
<th>의미 및 발생 상황</th>
</tr>
</thead>
<tbody><tr>
<td>400</td>
<td>Bad Request</td>
<td>요청 자체가 잘못됨 (파라미터 누락, 데이터 형식 오류 등)</td>
</tr>
<tr>
<td>401</td>
<td>Unauthorized</td>
<td>인증되지 않음 (로그인이 필요하거나 토큰이 유효하지 않을 때)</td>
</tr>
<tr>
<td>403</td>
<td>Forbidden</td>
<td>권한이 없음 (로그인은 했지만 해당 페이지에 접근 불가능할 때)</td>
</tr>
<tr>
<td>404</td>
<td>Not Found</td>
<td>요청한 리소스를 찾을 수 없음 (잘못된 URL 경로 등)</td>
</tr>
</tbody></table>
<h3 id="5xx--서버-오류">5xx — 서버 오류</h3>
<table>
<thead>
<tr>
<th>코드</th>
<th>이름</th>
<th>의미 및 발생 상황</th>
</tr>
</thead>
<tbody><tr>
<td>500</td>
<td>Internal Server Error</td>
<td>서버 내부 로직 오류 (코드 내 예외 발생, 소위 &#39;서버 터짐&#39;)</td>
</tr>
<tr>
<td>502</td>
<td>Bad Gateway</td>
<td>게이트웨이가 잘못된 응답을 받음 (Nginx와 서버 간 연결 문제 등)</td>
</tr>
<tr>
<td>503</td>
<td>Service Unavailable</td>
<td>서버가 과부하되었거나 점검 중이라 현재 이용 불가</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-상태-코드--에러-처리-실습">3. 상태 코드 &amp; 에러 처리 실습</h2>
<h3 id="전체-조회--상태-코드-적용">전체 조회 — 상태 코드 적용</h3>
<pre><code class="language-python">@app.get(&quot;/todos&quot;, status_code=200)
def get_todos_handler(order: str | None = None):
    ret = list(todo_data.values())
    if order and order == &quot;DESC&quot;:
        return ret[::-1]
    return ret</code></pre>
<h3 id="단일-조회--에러-처리">단일 조회 — 에러 처리</h3>
<pre><code class="language-python">from fastapi import FastAPI, Body, HTTPException

@app.get(&quot;/todos/{todo_id}&quot;, status_code=200)
def get_todo_handler(todo_id: int):
    todo = todo_data.get(todo_id)
    if todo:
        return todo
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)</code></pre>
<h3 id="생성-post--상태-코드-적용">생성 (POST) — 상태 코드 적용</h3>
<pre><code class="language-python">@app.post(&quot;/todos&quot;, status_code=201)
def create_todo_handler(request: CreateToDoRequest):
    todo_data[request.id] = request.dict()
    return todo_data[request.id]</code></pre>
<h3 id="수정-patch--에러-처리">수정 (PATCH) — 에러 처리</h3>
<pre><code class="language-python">@app.patch(&quot;/todos/{todo_id}&quot;, status_code=200)
def update_todo_handler(
    todo_id: int,
    is_done: bool = Body(..., embed=True),
):
    todo = todo_data.get(todo_id)
    if todo:
        todo[&quot;is_done&quot;] = is_done
        return todo
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)</code></pre>
<h3 id="삭제-delete--에러-처리">삭제 (DELETE) — 에러 처리</h3>
<pre><code class="language-python">@app.delete(&quot;/todos/{todo_id}&quot;, status_code=204)
def delete_todo_handler(todo_id: int):
    todo = todo_data.pop(todo_id, None)
    if todo:
        return
    raise HTTPException(status_code=404, detail=&quot;ToDo Not Found&quot;)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL & Database (2)]]></title>
            <link>https://velog.io/@woojin-archive/SQL-Database-2</link>
            <guid>https://velog.io/@woojin-archive/SQL-Database-2</guid>
            <pubDate>Sun, 08 Mar 2026 08:20:57 GMT</pubDate>
            <description><![CDATA[<h1 id="sql-기초-정리-2🗄️">SQL 기초 정리 2🗄️</h1>
<blockquote>
<p>DDL, 정규화, JOIN, INSERT/UPDATE/DELETE, UNION, VIEW</p>
</blockquote>
<hr>
<h2 id="1-테이블과-컬럼-생성--ddl">1. 테이블과 컬럼 생성 — DDL</h2>
<p>DDL(Data Definition Language) : 데이터베이스 구조를 정의하는 SQL 문법</p>
<h3 id="데이터베이스-생성--삭제">데이터베이스 생성 / 삭제</h3>
<pre><code class="language-sql">CREATE DATABASE 데이터베이스명;
DROP DATABASE 데이터베이스명;</code></pre>
<h3 id="테이블-생성--삭제">테이블 생성 / 삭제</h3>
<pre><code class="language-sql">-- 테이블 생성
CREATE TABLE 데이터베이스명.테이블명 (
    id INT,
    이름 VARCHAR(100) DEFAULT &#39;홍길동&#39;,  -- 값 미입력 시 &#39;홍길동&#39;으로 저장
    나이 INT
);

-- 테이블 삭제
DROP TABLE 테이블명;</code></pre>
<h3 id="컬럼-추가--수정--삭제--alter-table">컬럼 추가 / 수정 / 삭제 — ALTER TABLE</h3>
<pre><code class="language-sql">-- 컬럼 추가
ALTER TABLE 테이블명 ADD 성별 VARCHAR(10);

-- 컬럼 타입 수정
-- ⚠️ 이미 데이터가 저장되어 있으면 타입 변경 불가
ALTER TABLE 테이블명 MODIFY COLUMN 나이 VARCHAR(100);

-- 컬럼 삭제
ALTER TABLE 테이블명 DROP COLUMN 컬럼명;</code></pre>
<hr>
<h2 id="2-제약-조건--constraints">2. 제약 조건 — Constraints</h2>
<p>컬럼에 잘못된 데이터가 들어오는 것을 방지하는 규칙</p>
<h3 id="not-null--null-입력-금지">NOT NULL — NULL 입력 금지</h3>
<pre><code class="language-sql">CREATE TABLE 테이블명 (
    id INT NOT NULL,
    이름 VARCHAR(100),
    나이 INT
);</code></pre>
<h3 id="unique--중복값-방지">UNIQUE — 중복값 방지</h3>
<pre><code class="language-sql">-- 단일 컬럼
CREATE TABLE 테이블명 (
    id INT UNIQUE,
    이름 VARCHAR(100)
);

-- 복합 컬럼 (두 컬럼의 조합이 유일해야 함)
CREATE TABLE 테이블명 (
    id INT,
    이름 VARCHAR(100),
    UNIQUE(컬럼1, 컬럼2)
);</code></pre>
<h3 id="check--입력값-조건-검사">CHECK — 입력값 조건 검사</h3>
<pre><code class="language-sql">CREATE TABLE 테이블명 (
    id INT,
    나이 INT CHECK(나이 &gt; 0)  -- 0 이하의 나이 입력 불가
);</code></pre>
<h3 id="primary-key--행-식별자">PRIMARY KEY — 행 식별자</h3>
<p><code>NOT NULL + UNIQUE</code> 를 자동으로 포함</p>
<pre><code class="language-sql">CREATE TABLE 테이블명 (
    id INT PRIMARY KEY,
    이름 VARCHAR(100)
);</code></pre>
<h3 id="auto_increment--자동-증가">AUTO_INCREMENT — 자동 증가</h3>
<p>값을 직접 입력하지 않아도 1씩 자동 증가하는 정수를 넣어줌<br>주로 PRIMARY KEY와 함께 사용</p>
<pre><code class="language-sql">CREATE TABLE 테이블명 (
    id INT AUTO_INCREMENT PRIMARY KEY,
    이름 VARCHAR(100)
);</code></pre>
<hr>
<h2 id="3-정규화--1nf-2nf-3nf">3. 정규화 — 1NF, 2NF, 3NF</h2>
<h3 id="제1정규화-1nf">제1정규화 (1NF)</h3>
<blockquote>
<p><strong>&quot;한 칸엔 하나의 데이터만&quot;</strong></p>
</blockquote>
<ul>
<li>하나의 셀에 여러 값이 들어있으면 분리해야 함</li>
<li><strong>제1정규형</strong> : 제1정규화가 완료된 테이블</li>
</ul>
<h3 id="제2정규화-2nf">제2정규화 (2NF)</h3>
<blockquote>
<p><strong>&quot;현재 테이블의 주제와 관련없는 컬럼을 다른 테이블로 빼는 작업&quot;</strong><br>= 복합 기본키의 <strong>일부에만 종속</strong>되는 컬럼 분리 (부분 함수 종속 제거)</p>
</blockquote>
<ul>
<li><strong>복합 기본키(Composite Primary Key)</strong> : 단일 컬럼으로 행을 식별할 수 없을 때 2개 이상의 컬럼을 합쳐 기본키로 사용</li>
<li>기본키 설정 시 현재 데이터뿐 아니라 <strong>미래에 들어올 수 있는 데이터까지 고려</strong>해야 함</li>
</ul>
<blockquote>
<p>💡 <strong>종속관계 판단 팁</strong><br>하나의 컬럼 값을 바꿨을 때 다른 컬럼도 따라서 바뀌어야 한다면 → 종속관계</p>
</blockquote>
<p><strong>제2정규형</strong> : 제2정규화가 완료된 테이블</p>
<h3 id="제3정규화-3nf">제3정규화 (3NF)</h3>
<blockquote>
<p><strong>&quot;기본키가 아닌 일반 컬럼에 종속되는 컬럼 분리&quot;</strong> (이행 함수 종속 제거)</p>
</blockquote>
<p>쉽게 이해하는 방법 : <strong>기본키가 아닌 잔챙이 컬럼에 종속되는 컬럼을 분리</strong></p>
<p><strong>제3정규형</strong> : 제3정규화가 완료된 테이블</p>
<hr>
<h2 id="4-foreign-key-외래키">4. Foreign Key (외래키)</h2>
<blockquote>
<p><strong>다른 테이블의 Primary Key를 참조하는 컬럼</strong></p>
</blockquote>
<p>테이블을 쪼갤 때 유의사항</p>
<ol>
<li>첫 컬럼은 항상 PRIMARY KEY 넣는 게 좋음</li>
<li>다른 테이블의 데이터를 사용할 때 해당 테이블의 PRIMARY KEY를 적기 (Foreign Key)</li>
</ol>
<h3 id="foreign-key-장점">Foreign Key 장점</h3>
<ul>
<li>DBeaver에서 화살표 클릭으로 연결된 테이블 행 바로 확인 가능</li>
<li>다른 테이블에서 참조 중인 데이터 <strong>실수로 삭제하는 것을 방지</strong></li>
</ul>
<blockquote>
<p>⚠️ 단, 삭제/수정이 번거로워지고 테이블 구조 변경도 까다로워져서 <strong>등록하지 않는 곳도 있음</strong></p>
</blockquote>
<h3 id="sql로-foreign-key-등록하기">SQL로 Foreign Key 등록하기</h3>
<pre><code class="language-sql">-- 테이블 생성 시 인라인으로 등록
CREATE TABLE 테이블명 (
    id INT PRIMARY KEY,
    프로그램 VARCHAR(100),
    강사id INT REFERENCES 다른테이블명(참조할컬럼명)
);

-- CONSTRAINT 문법으로 등록
CREATE TABLE 테이블명 (
    id INT,
    프로그램 VARCHAR(100),
    강사id INT,
    CONSTRAINT pk_name PRIMARY KEY (id),
    CONSTRAINT fk_name FOREIGN KEY (강사id) REFERENCES 다른테이블명(참조할컬럼명)
);

-- 이미 생성된 테이블에 추가
ALTER TABLE 테이블명 ADD
CONSTRAINT fk_name FOREIGN KEY (강사id) REFERENCES teacher(id);</code></pre>
<hr>
<h2 id="5-테이블-합쳐서-출력--join">5. 테이블 합쳐서 출력 — JOIN</h2>
<p>정규화로 쪼개놓은 테이블을 다시 합쳐서 출력할 때 사용</p>
<h3 id="inner-join">INNER JOIN</h3>
<p>두 테이블에서 <strong>조건이 일치하는 행만</strong> 출력</p>
<pre><code class="language-sql">SELECT * FROM program
INNER JOIN teacher
ON 강사id = teacher.id;

-- 3개 이상 테이블 붙이기
SELECT * FROM 테이블1
INNER JOIN 테이블2 ON 조건식
INNER JOIN 테이블3 ON 조건식;</code></pre>
<h3 id="cross-join">CROSS JOIN</h3>
<p>두 테이블의 <strong>모든 행의 조합</strong> 출력</p>
<pre><code class="language-sql">SELECT * FROM program CROSS JOIN teacher;</code></pre>
<h3 id="left-join--right-join">LEFT JOIN / RIGHT JOIN</h3>
<pre><code class="language-sql">-- LEFT JOIN : INNER JOIN 결과 + 왼쪽 테이블의 모든 행
-- RIGHT JOIN : INNER JOIN 결과 + 오른쪽 테이블의 모든 행
SELECT * FROM program
RIGHT JOIN teacher
ON 강사id = teacher.id;</code></pre>
<blockquote>
<p>💡 <strong>활용 팁</strong> : 접점이 없는 행(빵꾸)을 찾을 때 유용</p>
</blockquote>
<pre><code class="language-sql">-- teacher 테이블에 있지만 program 테이블에서 참조되지 않는 강사 찾기
SELECT * FROM program
RIGHT JOIN teacher
ON 강사id = teacher.id
WHERE 강사id IS NULL;</code></pre>
<h3 id="join-vs-union-차이">JOIN vs UNION 차이</h3>
<table>
<thead>
<tr>
<th></th>
<th>JOIN</th>
<th>UNION</th>
</tr>
</thead>
<tbody><tr>
<td>방향</td>
<td>컬럼 방향으로 합침 (좌우)</td>
<td>행 방향으로 합침 (위아래)</td>
</tr>
<tr>
<td>조건</td>
<td>ON으로 연결 조건 지정</td>
<td>컬럼 개수가 같아야 함</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-데이터-삽입--insert">6. 데이터 삽입 — INSERT</h2>
<pre><code class="language-sql">-- 특정 컬럼에만 삽입
INSERT INTO stock(id, 상품명, 가격)
VALUES (1, &#39;김치&#39;, 500);

-- 전체 컬럼에 삽입 (컬럼명 생략 가능)
INSERT INTO stock
VALUES (1, &#39;김치&#39;, 500);</code></pre>
<h3 id="응용--테이블-간-데이터-복사">응용 — 테이블 간 데이터 복사</h3>
<pre><code class="language-sql">-- 기존 테이블에 다른 테이블 데이터 복사
INSERT INTO stock
SELECT * FROM stock2;

-- 새 테이블 생성과 동시에 복사
CREATE TABLE 새테이블명 SELECT * FROM 기존테이블명;</code></pre>
<blockquote>
<p>💡 <strong>테이블 복사는 언제?</strong></p>
<ul>
<li>쿼리 결과를 잠깐 저장할 때</li>
<li>테이블에 작업하기 전 테스트용으로 먼저 확인할 때</li>
</ul>
</blockquote>
<h3 id="db-접속-계정별-권한-설정-mysql-workbench">DB 접속 계정별 권한 설정 (MySQL Workbench)</h3>
<p>삽입/수정/삭제는 위험한 작업이므로 계정별 권한을 제한할 수 있음</p>
<ol>
<li>MySQL Workbench 접속</li>
<li>상단 <code>Server</code> &gt; <code>Users and Privileges</code></li>
<li>ID, PW 생성</li>
<li><code>Administrative Roles</code>에서 권한 설정 (예: SELECT, INSERT만 체크)</li>
<li><code>Apply</code>로 계정 생성</li>
<li>해당 계정으로 접속하면 설정한 권한만 사용 가능</li>
</ol>
<hr>
<h2 id="7-데이터-수정--삭제--update--delete">7. 데이터 수정 / 삭제 — UPDATE / DELETE</h2>
<h3 id="update">UPDATE</h3>
<pre><code class="language-sql">UPDATE stock
SET 상품명 = &#39;사과&#39;
WHERE id = 2;

-- 여러 컬럼 동시 수정
UPDATE stock
SET 상품명 = &#39;사과&#39;, 가격 = 가격 + 100
WHERE id = 2;</code></pre>
<blockquote>
<p>⚠️ <strong>WHERE 빠뜨리면 모든 행이 수정됨 — 반드시 확인!</strong></p>
</blockquote>
<h3 id="delete">DELETE</h3>
<pre><code class="language-sql">DELETE FROM stock
WHERE id = 100;</code></pre>
<blockquote>
<p>⚠️ <strong>WHERE 빠뜨리면 모든 행이 삭제됨 — 반드시 확인!</strong><br>⚠️ 다른 테이블에서 Foreign Key로 참조 중인 행은 삭제 불가</p>
</blockquote>
<h3 id="join한-테이블-수정--삭제">JOIN한 테이블 수정 / 삭제</h3>
<pre><code class="language-sql">-- 수정
UPDATE a INNER JOIN b ON 조건식
SET 컬럼 = 값
WHERE 조건식;

-- 삭제 (a만, b만, 또는 a,b 둘 다 삭제 가능)
DELETE a FROM a
INNER JOIN b ON 조건식
WHERE 조건식;</code></pre>
<hr>
<h2 id="8-select-결과-합치기--union">8. SELECT 결과 합치기 — UNION</h2>
<pre><code class="language-sql">-- 중복 제거하며 합치기
SELECT * FROM stock
UNION
SELECT * FROM stock2;

-- 중복 허용하며 합치기
SELECT * FROM stock
UNION ALL
SELECT * FROM stock2;</code></pre>
<blockquote>
<p>⚠️ 컬럼 개수가 다르면 실행 안 됨</p>
</blockquote>
<hr>
<h2 id="9-가상-테이블--view">9. 가상 테이블 — VIEW</h2>
<p>자주 쓰는 SELECT 쿼리를 테이블처럼 저장해두고 재사용할 때 사용<br>실제 데이터를 저장하는 게 아닌 <strong>가상 테이블</strong></p>
<pre><code class="language-sql">CREATE VIEW 뷰이름 AS
SELECT 컬럼1, 컬럼2
FROM 테이블명
WHERE 조건식;

-- 이후 일반 테이블처럼 사용 가능
SELECT * FROM 뷰이름;</code></pre>
<blockquote>
<p>💡 <strong>VIEW를 쓰는 이유</strong></p>
<ul>
<li>자주 쓰는 복잡한 쿼리를 매번 다시 짜지 않아도 됨</li>
<li>실제 테이블로 저장하기엔 무겁고, 그냥 두기엔 매번 쿼리 짜기 번거로울 때 사용</li>
</ul>
</blockquote>
<hr>
<h2 id="📋-sql-문법-실행-순서-요약">📋 SQL 문법 실행 순서 요약</h2>
<pre><code>FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[SQL & Database]]></title>
            <link>https://velog.io/@woojin-archive/SQL-Database</link>
            <guid>https://velog.io/@woojin-archive/SQL-Database</guid>
            <pubDate>Sat, 07 Mar 2026 11:48:48 GMT</pubDate>
            <description><![CDATA[<h1 id="sql-기초-정리-1🗄️">SQL 기초 정리 1🗄️</h1>
<blockquote>
<p>MySQL 기준으로 작성된 SQL 기초 학습 노트입니다.</p>
</blockquote>
<hr>
<h2 id="1-데이터베이스-설치">1. 데이터베이스 설치</h2>
<ul>
<li><strong>DBeaver</strong> 사용 권장 — 깔끔하고 직관적이며, MySQL 외 다른 DBMS(PostgreSQL, Oracle 등)도 쉽게 연결 가능</li>
</ul>
<hr>
<h2 id="2-테이블-만들기--데이터-타입">2. 테이블 만들기 &amp; 데이터 타입</h2>
<ul>
<li><strong>database</strong> = 폴더 개념</li>
<li><strong>table</strong> = 파일 개념</li>
<li>테이블을 만들기 전에 어떤 <strong>컬럼(열)</strong>이 들어갈지 미리 설계해야 함<ul>
<li>예) 상품 테이블 → 가격, 이름, 카테고리 컬럼</li>
</ul>
</li>
</ul>
<h3 id="📌-주요-데이터-타입">📌 주요 데이터 타입</h3>
<table>
<thead>
<tr>
<th>분류</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>숫자</strong></td>
<td><code>INT</code></td>
<td>일반 정수</td>
</tr>
<tr>
<td></td>
<td><code>BIGINT</code></td>
<td>큰 정수</td>
</tr>
<tr>
<td></td>
<td><code>FLOAT</code></td>
<td>소수</td>
</tr>
<tr>
<td></td>
<td><code>DOUBLE</code></td>
<td>더 긴 소수</td>
</tr>
<tr>
<td></td>
<td><code>DECIMAL</code></td>
<td>오차 없이 소수 저장 (금액 등에 적합)</td>
</tr>
<tr>
<td><strong>문자</strong></td>
<td><code>VARCHAR(100)</code></td>
<td>일반 문자열 (가변 길이)</td>
</tr>
<tr>
<td></td>
<td><code>MEDIUMTEXT</code></td>
<td>긴 문자열</td>
</tr>
<tr>
<td></td>
<td><code>LONGTEXT</code></td>
<td>매우 긴 문자열</td>
</tr>
<tr>
<td><strong>날짜/시간</strong></td>
<td><code>DATETIME</code></td>
<td><code>YYYY-MM-DD HH:MM:SS</code> 형식</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-데이터-출력과-정렬--select-order-by">3. 데이터 출력과 정렬 — SELECT, ORDER BY</h2>
<h3 id="기본-출력">기본 출력</h3>
<pre><code class="language-sql">-- 전체 컬럼 출력
SELECT * FROM 테이블이름;

-- 데이터베이스명 명시
SELECT * FROM 데이터베이스이름.테이블이름;

-- 특정 컬럼만 출력
SELECT 컬럼1, 컬럼2 FROM 테이블이름;</code></pre>
<h3 id="정렬">정렬</h3>
<pre><code class="language-sql">-- 오름차순(기본값)
SELECT * FROM 테이블이름 ORDER BY 컬럼 ASC;

-- 내림차순
SELECT * FROM 테이블이름 ORDER BY 컬럼 DESC;

-- 다중 정렬 (첫 번째 기준 정렬 후, 같은 값 내에서 두 번째 기준으로 다시 정렬)
SELECT * FROM 테이블이름 ORDER BY 컬럼1 ASC, 컬럼2 DESC;</code></pre>
<hr>
<h2 id="4-데이터-필터링--where">4. 데이터 필터링 — WHERE</h2>
<h3 id="csv-파일을-테이블에-가져오는-방법-dbeaver">CSV 파일을 테이블에 가져오는 방법 (DBeaver)</h3>
<ol>
<li>기존 데이터 전체 선택 후 삭제 &amp; 저장</li>
<li>테이블 우클릭 → <strong>데이터 가져오기</strong></li>
<li><code>CSV에서 가져오기</code> 클릭 후 파일 업로드</li>
<li>한글 데이터가 있다면 인코딩을 <strong>EUC-KR</strong>로 변경</li>
<li>파일명과 동일한 이름의 테이블이 생성됨 (기존 동일 이름 테이블은 미리 삭제 권장)</li>
</ol>
<h3 id="기본-필터링">기본 필터링</h3>
<pre><code class="language-sql">SELECT * FROM product WHERE 컬럼 = &#39;값&#39;;

-- 비교 연산자: =, !=, &gt;, &lt;, &gt;=, &lt;=
SELECT * FROM product WHERE 상품명 &gt; &#39;가&#39;;  -- 문자 비교도 가능</code></pre>
<h3 id="between-범위-조건">BETWEEN (범위 조건)</h3>
<pre><code class="language-sql">SELECT * FROM product WHERE 가격 BETWEEN 5000 AND 8000;</code></pre>
<hr>
<h2 id="5-복합-조건--and-or-not-in">5. 복합 조건 — AND, OR, NOT, IN</h2>
<h3 id="and--or">AND / OR</h3>
<pre><code class="language-sql">SELECT * FROM product
WHERE 가격 = 5000 AND 카테고리 = &#39;가구&#39;;

-- 괄호로 우선순위 지정
SELECT * FROM product
WHERE 가격 = 5000 AND (카테고리 = &#39;가구&#39; OR 카테고리 = &#39;옷&#39;);</code></pre>
<h3 id="not-조건-제외">NOT (조건 제외)</h3>
<pre><code class="language-sql">SELECT * FROM product
WHERE NOT 카테고리 = &#39;가구&#39; AND NOT 가격 = 5000;</code></pre>
<h3 id="in-여러-값-조건-축약--단일-컬럼에만-사용-가능">IN (여러 값 조건 축약 — 단일 컬럼에만 사용 가능)</h3>
<pre><code class="language-sql">-- 아래 두 쿼리는 동일한 결과
SELECT * FROM product
WHERE 카테고리 = &#39;신발&#39; OR 카테고리 = &#39;가전&#39; OR 카테고리 = &#39;식품&#39;;

SELECT * FROM product
WHERE 카테고리 IN (&#39;신발&#39;, &#39;가전&#39;, &#39;식품&#39;);</code></pre>
<blockquote>
<p>💡 <strong>코드 잘 짜는 팁</strong>: SQL을 바로 짜지 말고, 원하는 조건을 먼저 한글로 정리한 뒤 SQL로 변환하세요.</p>
</blockquote>
<hr>
<h2 id="6-패턴-검색--like--_">6. 패턴 검색 — LIKE, %, _</h2>
<pre><code class="language-sql">-- 특정 단어 포함 검색
SELECT * FROM product WHERE 상품명 LIKE &#39;%소파%&#39;;

-- % : 임의의 문자열
-- _ : 임의의 문자 1개
-- __ : 임의의 문자 2개</code></pre>
<blockquote>
<p>⚠️ <strong>주의사항</strong></p>
<ul>
<li><code>%</code>를 남용하면 인덱스를 활용하지 못해 <strong>성능 이슈</strong> 발생 가능</li>
<li><code>LIKE</code>와 <code>%</code>는 주로 <code>VARCHAR</code> 컬럼에 사용</li>
<li><code>CHAR</code> 컬럼은 공백으로 패딩되어 저장되므로 예상과 다르게 작동할 수 있음</li>
</ul>
</blockquote>
<hr>
<h2 id="7-집계-함수--min-max-avg-sum-count">7. 집계 함수 — MIN, MAX, AVG, SUM, COUNT</h2>
<pre><code class="language-sql">SELECT MAX(결제횟수) FROM card;
SELECT MIN(결제횟수) FROM card;
SELECT AVG(결제횟수) FROM card;
SELECT SUM(결제횟수) FROM card;
SELECT COUNT(*) FROM card;

-- 여러 집계 동시에
SELECT MAX(결제횟수), MAX(사용금액) FROM card;

-- AS로 컬럼명 변경
SELECT MAX(사용금액) AS 최대사용금액 FROM card;

-- WHERE와 함께 일부 행만 집계
SELECT AVG(사용금액) FROM card WHERE 고객등급 = &#39;vip&#39;;</code></pre>
<h3 id="중복-제거--distinct">중복 제거 — DISTINCT</h3>
<pre><code class="language-sql">SELECT DISTINCT 연체횟수 FROM card;

-- 집계와 함께 사용
SELECT AVG(DISTINCT 연체횟수) FROM card;</code></pre>
<h3 id="order-by--limit으로-최대최소-구하기">ORDER BY + LIMIT으로 최대/최소 구하기</h3>
<pre><code class="language-sql">-- 사용금액 상위 1개 행 출력 (일부 상황에서 MAX()보다 빠를 수 있음)
SELECT 사용금액 FROM card ORDER BY 사용금액 DESC LIMIT 1;</code></pre>
<hr>
<h2 id="8-컬럼-연산--문자-함수">8. 컬럼 연산 &amp; 문자 함수</h2>
<h3 id="사칙연산">사칙연산</h3>
<pre><code class="language-sql">SELECT 사용금액 * 10 FROM card;
SELECT 사용금액 / 결제횟수 FROM card;  -- 컬럼끼리 연산도 가능</code></pre>
<h3 id="문자-함수">문자 함수</h3>
<pre><code class="language-sql">-- 문자 이어붙이기
SELECT CONCAT(고객명, &#39; is &#39;, 고객등급) FROM card;

-- 좌우 공백 제거
SELECT TRIM(&#39;  hello  &#39;) FROM card;

-- 문자 치환
SELECT REPLACE(고객등급, &#39;패&#39;, &#39;훼&#39;) FROM card;

-- 일부 문자열 추출 (시작위치, 길이)
SELECT SUBSTR(고객명, 3, 2) FROM card;

-- 일부 문자열 교체 (시작위치, 교체할길이, 바꿀단어)
SELECT INSERT(&#39;test@naver.com&#39;, 1, 4, &#39;hello&#39;) FROM card;
-- 결과: &#39;hello@naver.com&#39;</code></pre>
<hr>
<h2 id="9-숫자-함수">9. 숫자 함수</h2>
<table>
<thead>
<tr>
<th>함수</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>GREATEST()</code></td>
<td>여러 값 중 최댓값</td>
<td><code>GREATEST(5, 3, 2)</code> → 5</td>
</tr>
<tr>
<td><code>LEAST()</code></td>
<td>여러 값 중 최솟값</td>
<td><code>LEAST(5, 3, 2)</code> → 2</td>
</tr>
<tr>
<td><code>FLOOR()</code></td>
<td>소수 내림</td>
<td><code>FLOOR(10.9)</code> → 10</td>
</tr>
<tr>
<td><code>CEIL()</code></td>
<td>소수 올림</td>
<td><code>CEIL(10.1)</code> → 11</td>
</tr>
<tr>
<td><code>ROUND(n, d)</code></td>
<td>소수점 d자리 반올림</td>
<td><code>ROUND(10.777, 2)</code> → 10.78</td>
</tr>
<tr>
<td><code>TRUNCATE(n, d)</code></td>
<td>소수점 d자리 내림</td>
<td><code>TRUNCATE(10.777, 2)</code> → 10.77</td>
</tr>
<tr>
<td><code>POWER(a, b)</code></td>
<td>거듭제곱</td>
<td><code>POWER(4, 2)</code> → 16</td>
</tr>
<tr>
<td><code>ABS()</code></td>
<td>절댓값</td>
<td><code>ABS(-100)</code> → 100</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 <code>MAX()</code> / <code>MIN()</code>은 <strong>한 컬럼 안</strong>에서 최대·최소를 구하고,<br><code>GREATEST()</code> / <code>LEAST()</code>는 <strong>한 행 안의 여러 값</strong> 중에서 최대·최소를 구합니다.</p>
</blockquote>
<blockquote>
<p>💡 Oracle, PostgreSQL에서는 <code>TRUNCATE()</code> 대신 <code>TRUNC()</code>를 사용합니다.</p>
</blockquote>
<hr>
<h2 id="10-서브쿼리">10. 서브쿼리</h2>
<p>SELECT 안에 또 다른 SELECT를 넣는 문법입니다.</p>
<pre><code class="language-sql">-- 사용금액이 평균보다 높은 사람만 출력
SELECT * FROM card
WHERE 사용금액 &gt; (SELECT AVG(사용금액) FROM card);

-- SELECT 절에서 활용
SELECT 사용금액, (SELECT AVG(사용금액) FROM card) FROM card;

-- IN 절에서 활용
SELECT * FROM card
WHERE 고객명 IN (SELECT 이름 FROM blacklist);</code></pre>
<blockquote>
<p>⚠️ <strong>서브쿼리 주의사항</strong></p>
<ol>
<li>서브쿼리는 숫자·문자(데이터) 위치에 넣을 수 있음</li>
<li><strong>반드시 1개의 값만 반환</strong>하는 쿼리여야 함 (여러 행 반환 시 오류)</li>
<li>반드시 <strong>소괄호</strong>로 감싸야 함</li>
</ol>
</blockquote>
<hr>
<h2 id="11-그룹별-집계--group-by-having">11. 그룹별 집계 — GROUP BY, HAVING</h2>
<pre><code class="language-sql">-- 고객등급별 평균 사용금액
SELECT 고객등급, AVG(사용금액) FROM card GROUP BY 고객등급;

-- 고객등급별 최대 사용금액
SELECT 고객등급, MAX(사용금액) FROM card GROUP BY 고객등급;

-- 고객등급별 인원 수
SELECT 고객등급, COUNT(사용금액) FROM card GROUP BY 고객등급;</code></pre>
<blockquote>
<p>💡 <code>GROUP BY</code> 뒤에는 주로 <strong>카테고리성 컬럼</strong> 사용 (예: 고객등급, 지역, 성별)</p>
</blockquote>
<h3 id="where-vs-having">WHERE vs HAVING</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>WHERE</code></td>
<td><code>GROUP BY</code> 이전에 <strong>원본 행</strong> 필터링</td>
</tr>
<tr>
<td><code>HAVING</code></td>
<td><code>GROUP BY</code> 이후에 <strong>그룹 결과</strong> 필터링</td>
</tr>
</tbody></table>
<pre><code class="language-sql">-- HAVING으로 그룹 결과 필터링
SELECT 고객등급, MAX(사용금액) FROM card
GROUP BY 고객등급
HAVING 고객등급 = &#39;vip&#39; OR 고객등급 = &#39;패밀리&#39;;

-- WHERE + GROUP BY + HAVING 함께 사용
SELECT 고객등급, MAX(사용금액) FROM card
WHERE 연체횟수 = 0
GROUP BY 고객등급
HAVING 고객등급 = &#39;vip&#39; OR 고객등급 = &#39;패밀리&#39;;

-- AS로 작명한 컬럼으로도 HAVING 필터링 가능
SELECT 연체횟수, COUNT(*) AS 인원수 FROM card
GROUP BY 연체횟수
HAVING 인원수 != 1
ORDER BY 연체횟수;</code></pre>
<hr>
<h2 id="12-조건-분기--if-case">12. 조건 분기 — IF, CASE</h2>
<h3 id="if-양자택일">IF (양자택일)</h3>
<pre><code class="language-sql">-- 기본 구조: IF(조건식, 참일 때 값, 거짓일 때 값)
SELECT IF(1 + 2 = 4, &#39;aaa&#39;, &#39;bbb&#39;);  -- 결과: bbb

SELECT 사용금액, IF(사용금액 &gt; 200000, &#39;우수&#39;, &#39;거지&#39;) FROM card;</code></pre>
<h3 id="case-3가지-이상-분기">CASE (3가지 이상 분기)</h3>
<pre><code class="language-sql">SELECT 사용금액,
  CASE
    WHEN 사용금액 &gt;= 200000 THEN &#39;우수&#39;
    WHEN 사용금액 &gt;= 100000 THEN &#39;준수&#39;
    ELSE &#39;거지&#39;
  END AS 평가
FROM card;</code></pre>
<blockquote>
<p>💡 <code>WHEN</code> 조건은 <strong>위에서부터 순서대로 평가</strong>되므로, 범위가 큰 조건일수록 먼저 써주세요.</p>
</blockquote>
<h3 id="case--집계-함수-응용">CASE + 집계 함수 응용</h3>
<pre><code class="language-sql">-- 고객등급에 따라 가중치 점수 합산
SELECT SUM(
  CASE
    WHEN 고객등급 = &#39;vip&#39;  THEN 3
    WHEN 고객등급 = &#39;로열&#39; THEN 2
    ELSE 1
  END
) AS 총점수
FROM card;</code></pre>
<hr>
<h2 id="📋-sql-실행-순서-요약">📋 SQL 실행 순서 요약</h2>
<pre><code>FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[FastAPI]]></title>
            <link>https://velog.io/@woojin-archive/FastAPI</link>
            <guid>https://velog.io/@woojin-archive/FastAPI</guid>
            <pubDate>Fri, 06 Mar 2026 15:37:55 GMT</pubDate>
            <description><![CDATA[<h1 id="fastapi-기초-정리">FastAPI 기초 정리</h1>
<h2 id="fastapi란">FastAPI란?</h2>
<p>Python 기반의 API 생성 툴로, 프레임워크보다는 <strong>API 생성에 특화</strong>된 도구다.
Micro Service 방식으로 서버를 기능별로 분할해서 만든다.</p>
<h3 id="특징">특징</h3>
<ul>
<li>매우 빠른 속도</li>
<li>코드가 간결함</li>
<li>비동기 처리 지원 → 특정 코드가 오래 걸리면 일단 제끼고 다른 코드 실행 가능</li>
<li>요청이 많은 사이트에 강점</li>
</ul>
<h3 id="주요-기능">주요 기능</h3>
<ul>
<li>GET / POST 요청 처리</li>
<li>DB 입출력</li>
<li>회원 인증</li>
<li>데이터 Validation</li>
<li>웹소켓</li>
<li>async / await (비동기)</li>
<li>타입 힌트</li>
<li>API 문서 자동 생성</li>
</ul>
<hr>
<h2 id="환경-세팅">환경 세팅</h2>
<blockquote>
<p>가상환경은 <strong>폴더마다 따로</strong> 만들어야 한다. 폴더가 바뀔 때마다 아래 순서를 진행한다.</p>
</blockquote>
<pre><code class="language-bash"># 1. 폴더 만들고 이동
cd C:\work
mkdir board
cd board

# 2. 가상환경 생성
py -m venv .venv

# 3. 가상환경 활성화 (앞에 (.venv) 붙으면 성공)
.venv\Scripts\activate

# 4. 패키지 설치
pip install fastapi uvicorn

# 5. 서버 실행
uvicorn main:app --reload</code></pre>
<h3 id="팀-프로젝트에서는">팀 프로젝트에서는</h3>
<p>팀원이 <code>requirements.txt</code> 를 넘겨주면 한 번에 환경 세팅 가능하다.</p>
<pre><code class="language-bash">pip install -r requirements.txt</code></pre>
<hr>
<h3 id="src-폴더-생성">src 폴더 생성</h3>
<p>폴더에src디렉터리를 만들고 그 안에 .py파일들을 생성한다.
src폴더는 mark directory as에서 source root으로 바꿔준다.</p>
<h2 id="기본-서버-만들기">기본 서버 만들기</h2>
<pre><code class="language-python">from fastapi import FastAPI

app = FastAPI()</code></pre>
<h3 id="메인-페이지-접속-시-데이터-반환">메인 페이지 접속 시 데이터 반환</h3>
<pre><code class="language-python">@app.get(&quot;/&quot;)
def root():
    return &quot;hello&quot;</code></pre>
<h3 id="특정-경로로-데이터-반환">특정 경로로 데이터 반환</h3>
<pre><code class="language-python">@app.get(&quot;/data&quot;)
def get_data():
    return {&quot;hello&quot;: 1234}</code></pre>
<h3 id="메인-페이지에서-html-파일-전송">메인 페이지에서 HTML 파일 전송</h3>
<pre><code class="language-python">from fastapi.responses import FileResponse

@app.get(&quot;/&quot;)
def root():
    return FileResponse(&quot;index.html&quot;)</code></pre>
<h3 id="api-문서-자동-생성">API 문서 자동 생성</h3>
<p>서버 실행 후 아래 주소로 접속하면 API 문서를 자동으로 확인할 수 있다.</p>
<ul>
<li>Swagger UI: <code>http://localhost:8000/docs</code></li>
<li>ReDoc: <code>http://localhost:8000/redoc</code></li>
</ul>
<hr>
<h2 id="유저-데이터-받기-post">유저 데이터 받기 (POST)</h2>
<p>유저에게 데이터를 받을 때는 <strong>Pydantic</strong>으로 모델을 생성해야 한다.</p>
<h3 id="pydantic이란">Pydantic이란?</h3>
<p>유저가 데이터를 보낼 때 <strong>타입이 맞는지 자동으로 검사</strong>해주는 도구.
잘못된 데이터가 서버에 들어오는 걸 막아준다.
Spring의 <code>@Valid</code>, <code>@NotNull</code> 같은 Bean Validation과 같은 역할이다.</p>
<pre><code class="language-python">from pydantic import BaseModel

class Model(BaseModel):
    name: str   # 타입 지정
    phone: int

@app.post(&quot;/send&quot;)
def send_data(data: Model):  # /send로 보낸 데이터가 담김
    print(data)
    return &quot;전송완료&quot;</code></pre>
<hr>
<h2 id="비동기-처리-async--await">비동기 처리 (async / await)</h2>
<pre><code class="language-python">@app.get(&quot;/&quot;)
async def root():
    await 오래_걸리는_코드()  # 이 코드가 끝날 때까지 기다림
    return FileResponse(&quot;index.html&quot;)</code></pre>
<hr>
<h2 id="sqlalchemy-orm">SQLAlchemy (ORM)</h2>
<h3 id="orm이란">ORM이란?</h3>
<p>SQL을 직접 쓰지 않고 <strong>Python 코드로 DB를 다룰 수 있게 해주는 도구</strong>다.
Spring의 JPA / Hibernate와 똑같은 역할이다.</p>
<pre><code class="language-python"># SQL 직접 쓰면
SELECT * FROM posts WHERE id = 1

# SQLAlchemy 쓰면
db.query(Post).filter(Post.id == 1).first()</code></pre>
<h3 id="spring이랑-비교">Spring이랑 비교</h3>
<table>
<thead>
<tr>
<th>역할</th>
<th>Spring</th>
<th>FastAPI</th>
</tr>
</thead>
<tbody><tr>
<td>ORM</td>
<td>JPA / Hibernate</td>
<td>SQLAlchemy</td>
</tr>
<tr>
<td>테이블 정의</td>
<td><code>@Entity</code></td>
<td><code>class Post(Base)</code></td>
</tr>
<tr>
<td>쿼리</td>
<td><code>JpaRepository</code></td>
<td><code>db.query(Post)</code></td>
</tr>
</tbody></table>
<blockquote>
<p>JPA 써봤으면 개념은 이미 알고 있다. 문법만 다를 뿐이다.</p>
</blockquote>
<hr>
<h2 id="db-입출력">DB 입출력</h2>
<blockquote>
<p>추후 작성 예정</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[programmers] K번째 수]]></title>
            <link>https://velog.io/@woojin-archive/K%EB%B2%88%EC%A7%B8-%EC%88%98</link>
            <guid>https://velog.io/@woojin-archive/K%EB%B2%88%EC%A7%B8-%EC%88%98</guid>
            <pubDate>Thu, 05 Mar 2026 05:12:22 GMT</pubDate>
            <description><![CDATA[<h1 id="java-코테-필수-라이브러리-정리---arrays">[Java] 코테 필수 라이브러리 정리 - Arrays</h1>
<blockquote>
<p>Java Arrays 라이브러리 사용법</p>
</blockquote>
<h2 id="📌-arrays-클래스란">📌 Arrays 클래스란?</h2>
<p><code>java.util.Arrays</code>에 포함된 클래스로, 배열을 다루는 유틸리티 메서드들을 제공한다.
C에서 직접 구현하던 것들을 한 줄로 처리할 수 있다.</p>
<pre><code class="language-java">import java.util.Arrays;</code></pre>
<hr>
<h2 id="✂️-arrayscopyofrange--배열-자르기">✂️ Arrays.copyOfRange() — 배열 자르기</h2>
<p>배열의 일부를 잘라서 새 배열로 복사한다.</p>
<pre><code class="language-java">Arrays.copyOfRange(원본배열, 시작인덱스, 끝인덱스)</code></pre>
<blockquote>
<p>⚠️ 끝인덱스는 <strong>포함되지 않는다</strong> (exclusive)</p>
</blockquote>
<h3 id="예시">예시</h3>
<pre><code class="language-java">int[] arr = {1, 2, 3, 4, 5};

int[] result = Arrays.copyOfRange(arr, 1, 4);
// → {2, 3, 4}   (index 1 이상, index 4 미만)</code></pre>
<h3 id="코테-적용-포인트">코테 적용 포인트</h3>
<p>문제가 <strong>1-indexed</strong> 기준일 때, Java는 <strong>0-indexed</strong>라서 시작값에 <code>-1</code> 해줘야 한다.
끝값은 exclusive이기 때문에 <code>-1</code> 없이 그대로 넣으면 딱 맞게 잘린다.</p>
<pre><code class="language-java">// commands[i] = [2, 5, 3] 이면
int[] temp = Arrays.copyOfRange(array, commands[i][0]-1, commands[i][1]);
//                                     2-1 = 1부터       5 미만까지 → index 1~4</code></pre>
<hr>
<h2 id="🔃-arrayssort--배열-정렬">🔃 Arrays.sort() — 배열 정렬</h2>
<p>배열을 <strong>오름차순</strong>으로 정렬한다.</p>
<pre><code class="language-java">int[] arr = {3, 1, 4, 1, 5};
Arrays.sort(arr);
// → {1, 1, 3, 4, 5}</code></pre>
<h3 id="내림차순-정렬">내림차순 정렬</h3>
<p>기본 <code>int[]</code>로는 내림차순 직접 지정이 안 되고, <code>Integer[]</code>로 바꿔야 한다.</p>
<pre><code class="language-java">Integer[] arr = {3, 1, 4, 1, 5};
Arrays.sort(arr, (a, b) -&gt; b - a);
// → {5, 4, 3, 1, 1}</code></pre>
<h3 id="일부-범위만-정렬">일부 범위만 정렬</h3>
<pre><code class="language-java">int[] arr = {5, 3, 1, 4, 2};
Arrays.sort(arr, 1, 4);  // index 1 이상, 4 미만만 정렬
// → {5, 1, 3, 4, 2}</code></pre>
<hr>
<h2 id="🧩-실전-예제--프로그래머스-k번째-수">🧩 실전 예제 — 프로그래머스 &quot;K번째 수&quot;</h2>
<h3 id="문제-요약">문제 요약</h3>
<p>배열에서 <code>i</code>번째부터 <code>j</code>번째까지 자르고, 정렬 후 <code>k</code>번째 수를 구한다.</p>
<h3 id="풀이">풀이</h3>
<pre><code class="language-java">import java.util.Arrays;

class Solution {
    public int[] solution(int[] array, int[][] commands) {
        int[] answer = new int[commands.length];

        for (int i = 0; i &lt; commands.length; i++) {
            // 1. i번째 ~ j번째 자르기 (1-indexed → 0-indexed 변환)
            int[] temp = Arrays.copyOfRange(array, commands[i][0] - 1, commands[i][1]);

            // 2. 오름차순 정렬
            Arrays.sort(temp);

            // 3. k번째 수 저장 (1-indexed → 0-indexed 변환)
            answer[i] = temp[commands[i][2] - 1];
        }

        return answer;
    }
}</code></pre>
<h3 id="흐름-시각화">흐름 시각화</h3>
<pre><code>array    = [1, 5, 2, 6, 3, 7, 4]
command  = [2, 5, 3]

1. copyOfRange(array, 1, 5)  →  temp = [5, 2, 6, 3]
2. sort(temp)                →  temp = [2, 3, 5, 6]
3. temp[2]                   →  answer = 5</code></pre><hr>
<h2 id="📚-arrays-주요-메서드-요약">📚 Arrays 주요 메서드 요약</h2>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>Arrays.sort(arr)</code></td>
<td>오름차순 정렬</td>
</tr>
<tr>
<td><code>Arrays.sort(arr, from, to)</code></td>
<td>범위 지정 정렬</td>
</tr>
<tr>
<td><code>Arrays.sort(arr, (a,b) -&gt; b-a)</code></td>
<td>내림차순 정렬 (Integer[] 필요)</td>
</tr>
<tr>
<td><code>Arrays.copyOfRange(arr, from, to)</code></td>
<td>배열 범위 복사</td>
</tr>
<tr>
<td><code>Arrays.copyOf(arr, newLength)</code></td>
<td>배열 복사 (길이 지정)</td>
</tr>
<tr>
<td><code>Arrays.fill(arr, value)</code></td>
<td>배열 특정 값으로 초기화</td>
</tr>
<tr>
<td><code>Arrays.equals(arr1, arr2)</code></td>
<td>두 배열 동등 비교</td>
</tr>
<tr>
<td><code>Arrays.toString(arr)</code></td>
<td>배열 출력용 문자열 변환</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[깃허브 협업 가이드]]></title>
            <link>https://velog.io/@woojin-archive/%EA%B9%83%ED%97%88%EB%B8%8C-%ED%98%91%EC%97%85-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@woojin-archive/%EA%B9%83%ED%97%88%EB%B8%8C-%ED%98%91%EC%97%85-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Mon, 02 Mar 2026 07:39:14 GMT</pubDate>
            <description><![CDATA[<h1 id="🤝-깃허브-협업-가이드-shared-repository-방식">🤝 깃허브 협업 가이드: Shared Repository 방식</h1>
<p>팀 프로젝트(3~5인)에서 <code>Fork</code> 과정 없이 하나의 원본 저장소에서 직접 브랜치를 나누어 작업하는 효율적인 협업 모델입니다.</p>
<h2 id="1-초기-설정-방장-권한">1. 초기 설정 (방장 권한)</h2>
<p>방장(소유자)은 팀원들에게 저장소 수정 권한을 부여해야 합니다.</p>
<ol>
<li><strong>Collaborator 초대</strong>: <code>Settings</code> &gt; <code>Collaborators</code> &gt; <code>Add people</code>에서 팀원 계정 추가.</li>
<li><strong>브랜치 보호 규칙 설정</strong>: <code>Settings</code> &gt; <code>Branches</code> &gt; <code>Add branch protection rule</code><ul>
<li><strong>Branch name pattern</strong>: <code>main</code> 입력</li>
<li><strong>Check</strong>: <code>Require a pull request before merging</code> (직접 푸시 방지)</li>
<li><strong>Check</strong>: <code>Require approvals</code> (1명 이상의 승인 필요)</li>
<li><strong>Check</strong>: <code>Require review from Code Owners</code> (방장 승인 필수 시)</li>
</ul>
</li>
</ol>
<h3 id="코드-오너-설정-codeowners-파일-생성">코드 오너 설정 (CODEOWNERS 파일 생성)</h3>
<p>이 파일이 있어야 특정 인원의 승인을 강제할 수 있습니다.</p>
<ol>
<li>프로젝트 최상위 폴더(root)에 확장자 없이 <strong><code>CODEOWNERS</code></strong> 파일을 만듭니다.</li>
<li>파일 내용에 아래와 같이 입력합니다.</li>
</ol>
<p>Bash
<code># 모든 파일(*)에 대해 방장의 승인을 필수로 지정함</code>
<code>* @방장의_GitHub_ID</code></p>
<ol start="3">
<li>이 파일을 <code>main</code> 브랜치에 <code>push</code> 합니다.</li>
</ol>
<h2 id="2-표준-협업-워크플로우-팀원-공통">2. 표준 협업 워크플로우 (팀원 공통)</h2>
<h3 id="step-1-로컬-저장소-연결-clone">STEP 1. 로컬 저장소 연결 (Clone)</h3>
<p>팀원 모두가 방장의 원본 저장소 주소를 클론받습니다.</p>
<p>Bash</p>
<p><code>git clone https://github.com/방장계정/프로젝트명.git</code></p>
<h3 id="step-2-새로운-기능-브랜치-생성-branch">STEP 2. 새로운 기능 브랜치 생성 (Branch)</h3>
<p><code>main</code>에서 직접 작업하지 않고, 항상 새로운 가지(Branch)를 쳐서 나갑니다.</p>
<p>Bash</p>
<p><code>git checkout -b feature/login  # &#39;login&#39; 기능을 위한 새 브랜치 생성 및 이동</code></p>
<h3 id="step-3-작업-및-원격-푸시-push">STEP 3. 작업 및 원격 푸시 (Push)</h3>
<p>코드 수정 후 내 브랜치를 서버에 올립니다.</p>
<p>Bash</p>
<p><code>git add .
git commit -m &quot;로그인 기능 구현&quot;
git push origin feature/login</code></p>
<h3 id="step-4-풀-리퀘스트-생성-pull-request">STEP 4. 풀 리퀘스트 생성 (Pull Request)</h3>
<p>GitHub 웹사이트에서 <code>feature/login</code> → <code>main</code>으로 합쳐달라는 요청(PR)을 보냅니다. 이때 팀원이나 방장에게 리뷰를 요청합니다.</p>
<h3 id="step-5-코드-리뷰-및-승인-approve">STEP 5. 코드 리뷰 및 승인 (Approve)</h3>
<p>방장 혹은 리뷰어는 코드를 검토한 후 <strong>[Review changes]</strong> 버튼을 눌러 <strong>Approve</strong>를 제출합니다. (단순 댓글이 아닌 공식 승인 버튼 클릭 필수!)</p>
<h3 id="step-6-병합-및-브랜치-삭제-merge--delete">STEP 6. 병합 및 브랜치 삭제 (Merge &amp; Delete)</h3>
<p>승인이 완료되면 <strong>Merge</strong> 버튼을 눌러 <code>main</code>에 합칩니다. 합쳐진 후에는 서버와 로컬에서 사용한 브랜치를 삭제하여 정리합니다.</p>
<p>Bash</p>
<p>`# 원격 브랜치 삭제 (GitHub 웹에서 버튼 클릭 또는 명령어)
git push origin --delete feature/login</p>
<h1 id="로컬-브랜치-삭제">로컬 브랜치 삭제</h1>
<p>git checkout main
git pull origin main  # 최신화된 main 받아오기
git branch -d feature/login
git remote prune origin  # 유령 브랜치 정리`</p>
<hr>
<h2 id="3-실무-필수-팁-best-practices">3. 실무 필수 팁 (Best Practices)</h2>
<ul>
<li><strong><code>main</code>은 언제나 Ready</strong>: <code>main</code> 브랜치는 항상 실행 가능한 상태(Stable)를 유지해야 합니다.</li>
<li><strong>Pull Before Start</strong>: 새로운 작업을 시작하기 전에는 항상 <code>git pull origin main</code>을 받아 최신 상태를 유지하세요.</li>
<li><strong>브랜치 네이밍</strong>: <code>feature/기능명</code>, <code>bugfix/이슈명</code> 처럼 용도를 명확히 적으면 소스트리에서 폴더처럼 묶여 관리하기 편합니다.</li>
<li><strong>Conflict(충돌) 발생 시</strong>: 당황하지 말고 <code>pull</code>을 받아 인텔리제이의 Merge 도구를 활용해 코드를 합친 후 다시 <code>push</code> 하세요.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CLI 환경에서 브랜치 생성 및 조작 ]]></title>
            <link>https://velog.io/@woojin-archive/CLI-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%A1%B0%EC%9E%91</link>
            <guid>https://velog.io/@woojin-archive/CLI-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EB%B8%8C%EB%9E%9C%EC%B9%98-%EC%83%9D%EC%84%B1-%EB%B0%8F-%EC%A1%B0%EC%9E%91</guid>
            <pubDate>Sat, 28 Feb 2026 16:06:50 GMT</pubDate>
            <description><![CDATA[<h1 id="cli-환경에서-브랜치-생성-및-조작">CLI 환경에서 브랜치 생성 및 조작</h1>
<hr>
<h2 id="브랜치를-사용하는-경우">브랜치를 사용하는 경우</h2>
<ul>
<li>새로운 기능 추가 (브랜치를 생성해서 기능 추가 후 main 브랜치로 병합)</li>
<li>버그 수정 (브랜치를 생성해서 버그 수정 후 main 브랜치로 병합)</li>
<li>병합과 리베이스 테스트</li>
<li>이전 코드 개선</li>
<li>특정 커밋으로 돌아가고 싶을 때</li>
</ul>
<blockquote>
<p>💡 현재 연결된 원격 저장소 확인: <code>git remote -v</code></p>
</blockquote>
<hr>
<h2 id="브랜치-만들기">브랜치 만들기</h2>
<pre><code class="language-bash">git branch                        # 현재 브랜치 확인
git branch &lt;생성할 브랜치 이름&gt;     # 새로운 브랜치 생성
git log --oneline --graph --all --decorate  # 변경된 브랜치 확인</code></pre>
<blockquote>
<p>💡 <code>HEAD</code>는 현재 작업 중인 브랜치를 가리킨다.</p>
</blockquote>
<hr>
<h2 id="switch--브랜치-변경하기">switch : 브랜치 변경하기</h2>
<p><code>[main]</code> 브랜치에서 <code>[mybranch1]</code> 브랜치로 변경하고 새로운 커밋 생성 후 결과 확인</p>
<pre><code class="language-bash">git switch &lt;브랜치 이름&gt;           # 브랜치 변경
git branch                        # 현재 브랜치 확인
git log --oneline --graph --all --decorate  # HEAD 변경 확인

# 파일 수정 후
git status                        # 스테이지 상태 확인
git add .                         # 스테이지에 변경 사항 추가
git commit -m &quot;&lt;message&gt;&quot;         # 커밋
git log --oneline --graph --all --decorate  # 변경된 브랜치 확인</code></pre>
<hr>
<h2 id="fast-forward-merge--빨리-감기-병합">fast-forward merge : 빨리 감기 병합</h2>
<p><code>main</code>과 <code>mybranch1</code> 병합하기</p>
<pre><code class="language-bash">git switch main          # main 브랜치로 변경
git merge mybranch1      # 빨리 감기 병합
git log --oneline --graph --all --decorate  # 확인</code></pre>
<hr>
<h2 id="reset---hard--브랜치-되돌리기">reset --hard : 브랜치 되돌리기</h2>
<p>현재 브랜치를 지정한 커밋으로 옮긴다. <strong>작업 폴더의 내용도 함께 변경된다.</strong></p>
<pre><code class="language-bash">git reset --hard &lt;이동할 커밋 체크섬&gt;</code></pre>
<p>체크섬 대신 아래 약칭 사용 가능:</p>
<table>
<thead>
<tr>
<th>약칭</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>HEAD~</code></td>
<td>HEAD의 부모 커밋</td>
</tr>
<tr>
<td><code>HEAD~2</code></td>
<td>HEAD의 할아버지 커밋</td>
</tr>
<tr>
<td><code>HEAD^</code></td>
<td>HEAD의 부모 커밋</td>
</tr>
<tr>
<td><code>HEAD^2</code></td>
<td>HEAD의 두 번째 부모 커밋 (부모가 둘 이상일 때 사용)</td>
</tr>
</tbody></table>
<pre><code class="language-bash">git reset --hard HEAD~2                     # 두 단계 이전으로 되돌리기
git log --oneline --graph --all --decorate  # 로그 확인</code></pre>
<hr>
<h2 id="rebase--리베이스">rebase : 리베이스</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>merge</code></td>
<td>병합 (줄기를 그대로 유지)</td>
</tr>
<tr>
<td><code>rebase</code></td>
<td>재배치 (내 브랜치의 시작점을 대상 브랜치의 최신 커밋에 옮겨 붙임 → 일직선 히스토리)</td>
</tr>
</tbody></table>
<p><strong>리베이스 원리:</strong></p>
<ol>
<li>HEAD와 대상 브랜치의 공통 조상을 찾는다.</li>
<li>공통 조상 이후에 생성한 커밋들을 대상 브랜치 뒤로 재배치한다.</li>
</ol>
<blockquote>
<p>⚠️ <strong>주의:</strong> 이미 원격 저장소(GitHub)에 푸시한 브랜치는 절대 Rebase 하지 말 것!</p>
</blockquote>
<hr>
<h2 id="tag--배포-버전에-태그-달기">tag : 배포 버전에 태그 달기</h2>
<pre><code class="language-bash">git tag -a -m &quot;첫 번째 태그 생성&quot; v0.1        # 주석 있는 태그 작성
git log --oneline --graph --all --decorate    # 태그 생성 확인
git push origin v0.1                          # 태그 푸시</code></pre>
<hr>
<h2 id="3-way-병합">3-way 병합</h2>
<h3 id="긴급-버그-처리-시나리오">긴급 버그 처리 시나리오</h3>
<ol>
<li>(옵션) 오류가 없는 버전(주로 Tag가 있는 커밋)으로 롤백</li>
<li><code>[main]</code> 브랜치로부터 <code>[hotfix]</code> 브랜치 생성</li>
<li>빠르게 소스코드 수정 및 테스트 완료</li>
<li><code>[main]</code> 브랜치로 빨리 감기 병합 및 배포</li>
<li>개발 중인 브랜치에도 병합 (단, 충돌 발생 가능성 높음)</li>
</ol>
<h3 id="새로운-브랜치-및-커밋-생성-기능-개발">새로운 브랜치 및 커밋 생성 (기능 개발)</h3>
<pre><code class="language-bash">git switch main                  # main 브랜치로 변경
git switch -c feature1           # feature1 브랜치 생성 및 변경

# 파일 수정 후
git add .
git commit -m &quot;&lt;message&gt;&quot;
git log --oneline --graph --all --decorate</code></pre>
<h3 id="hotfix-브랜치-생성-및-main에-병합">hotfix 브랜치 생성 및 main에 병합</h3>
<pre><code class="language-bash">git switch -c hotfix main        # main에서 hotfix 브랜치 생성 후 변경

# 버그 파일 수정 후
git add .
git commit -m &quot;&lt;message&gt;&quot;
git log --oneline --graph --all --decorate</code></pre>
<pre><code>* db531ae (HEAD -&gt; hotfix) test3 버그수정
| * dc53cc3 (feature1) test3
|/
* 3ab375f (tag: v0.1, origin/main, origin/HEAD, main) test2
* 96abff0 test
* 73850bd 코딩테스트 기록 시작</code></pre><pre><code class="language-bash">git switch main
git merge hotfix     # 빨리 감기 병합</code></pre>
<pre><code>* db531ae (HEAD -&gt; main, hotfix) test3 버그수정
| * dc53cc3 (feature1) test3
|/
* 3ab375f (tag: v0.1, origin/main, origin/HEAD) test2
* 96abff0 test
* 73850bd 코딩테스트 기록 시작</code></pre><p>hotfix 커밋(버그 수정)은 현재 개발 중인 <code>[feature1]</code> 브랜치에도 반영해야 한다.
<code>[feature1]</code>과 <code>[main]</code>은 서로 다른 분기이므로 <strong>3-way 병합</strong>을 수행한다.</p>
<h3 id="병합-및-충돌-해결">병합 및 충돌 해결</h3>
<pre><code class="language-bash">git switch feature1    # feature1 브랜치로 변경
git merge main         # main 브랜치와 병합 시도 → 충돌 발생</code></pre>
<pre><code>Auto-merging src/programmers/test3.java
CONFLICT (add/add): Merge conflict in src/programmers/test3.java
Automatic merge failed; fix conflicts and then commit the result.</code></pre><pre><code class="language-bash">git status             # 충돌 파일 확인 (both added: ...)</code></pre>
<p>충돌 파일을 직접 열어 수동으로 수정한 뒤:</p>
<pre><code class="language-bash">cat &lt;파일&gt;             # 최종 변경 내용 확인
git add .
git status             # modified: src/programmers/test3.java
git commit             # 병합 커밋 생성
git log --oneline --graph --all --decorate</code></pre>
<pre><code>*   997db09 (HEAD -&gt; feature1) 병합
|\
| * db531ae (origin/main, origin/HEAD, main, hotfix) test3 버그수정
* | dc53cc3 test3
|/
* 3ab375f (tag: v0.1) test2
* 96abff0 test
* 73850bd 코딩테스트 기록 시작</code></pre><blockquote>
<p>3-way 병합은 병합 커밋이 생성되어 트리가 복잡해진다. 히스토리를 깔끔하게 유지하고 싶다면 <strong>리베이스</strong>를 사용한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[CLI 환경에서 Git 명령어]]></title>
            <link>https://velog.io/@woojin-archive/CLI-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-Git-%EB%AA%85%EB%A0%B9%EC%96%B4</link>
            <guid>https://velog.io/@woojin-archive/CLI-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-Git-%EB%AA%85%EB%A0%B9%EC%96%B4</guid>
            <pubDate>Sat, 28 Feb 2026 06:45:52 GMT</pubDate>
            <description><![CDATA[<h1 id="git-cli-환경에서-자주-사용하는-핵심-명령어-정리">[Git] CLI 환경에서 자주 사용하는 핵심 명령어 정리</h1>
<p>GUI(SourceTree 등) 환경을 넘어, 터미널(Git Bash)에서 직접 명령어를 입력하며 Git 저장소를 관리하는 필수 방법들을 정리했습니다.</p>
<hr>
<h2 id="1-터미널-기본-개념-및-이동">1. 터미널 기본 개념 및 이동</h2>
<ul>
<li><strong>Git Bash:</strong> 윈도우 환경에서 리눅스 스타일의 명령어를 사용할 수 있게 해주는 도구입니다.</li>
<li><strong>프롬프트($):</strong> 명령어를 입력할 수 있는 상태임을 나타냅니다. 앞에 표시되는 경로는 현재 작업 중인 폴더의 위치를 의미합니다.</li>
</ul>
<h3 id="자주-사용하는-기본-명령어">자주 사용하는 기본 명령어</h3>
<ul>
<li><code>pwd</code>: 현재 작업 중인 폴더의 전체 경로 확인</li>
<li><code>ls -a</code>: 현재 폴더의 파일 목록 확인 (숨김 파일 포함)</li>
<li><code>mkdir &lt;폴더명&gt;</code>: 새로운 폴더 생성</li>
<li><code>cd &lt;폴더명&gt;</code>: 해당 폴더로 이동 (<code>cd ..</code>은 상위 폴더로 이동, <code>cd ~</code>은 홈 폴더로 이동)</li>
</ul>
<hr>
<h2 id="2-로컬-저장소-생성-및-초기화">2. 로컬 저장소 생성 및 초기화</h2>
<p>주의: 바탕화면 자체를 저장소로 만들지 말고, 별도의 작업 폴더를 생성한 뒤 내부에서 실행하세요.</p>
<ul>
<li><strong><code>git init -b main</code></strong>: 현재 폴더를 Git 로컬 저장소로 초기화하며, 기본 브랜치 이름을 <code>main</code>으로 설정합니다.</li>
<li><strong><code>.git</code> 폴더:</strong> 이 명령을 실행하면 숨김 폴더인 <code>.git</code>이 생성됩니다. 여기가 Git의 모든 이력이 기록되는 <strong>로컬 저장소</strong>입니다.</li>
<li><strong>워킹 트리(Working Tree):</strong> 실제로 파일을 수정하고 작성하는 현재 작업 폴더를 의미합니다.</li>
</ul>
<hr>
<h2 id="3-스테이징과-커밋-add--commit">3. 스테이징과 커밋 (Add &amp; Commit)</h2>
<h3 id="파일-추적-및-스테이징">파일 추적 및 스테이징</h3>
<ul>
<li><strong><code>git status</code></strong>: 현재 워킹 트리의 상태(수정된 파일, 스테이지에 올라간 파일 등)를 확인합니다.</li>
<li><strong><code>git add &lt;파일명&gt;</code></strong>: 파일을 <strong>스테이지(Stage)</strong>에 올립니다. (커밋 대기 상태)</li>
<li><strong><code>git reset &lt;파일명&gt;</code></strong>: 스테이지에 올린 파일을 다시 내립니다. (언스테이징)</li>
</ul>
<h3 id="커밋-생성-및-이력-확인">커밋 생성 및 이력 확인</h3>
<ul>
<li><strong><code>git commit -m &quot;메시지&quot;</code></strong>: 스테이지에 올라온 파일들을 하나의 버전(커밋)으로 기록합니다.</li>
<li><strong><code>git log --oneline --graph --all --decorate</code></strong>: 커밋 히스토리를 한 줄씩, 그래프 형태로 보기 좋게 출력합니다.</li>
</ul>
<hr>
<h2 id="4-원격-저장소github-협업">4. 원격 저장소(GitHub) 협업</h2>
<h3 id="원격-저장소-등록-및-푸시push">원격 저장소 등록 및 푸시(Push)</h3>
<ul>
<li><strong><code>git remote add origin &lt;주소&gt;</code></strong>: 로컬 저장소에 <code>origin</code>이라는 이름으로 원격 저장소 주소를 등록합니다.</li>
<li><strong><code>git remote -v</code></strong>: 연결된 원격 저장소 목록과 주소를 확인합니다.</li>
<li><strong><code>git push -u origin main</code></strong>: 로컬의 <code>main</code> 브랜치 내용을 원격(<code>origin</code>)으로 업로드합니다. <code>-u</code> 옵션은 업스트림 설정을 통해 이후 <code>git push</code>만 입력해도 동작하게 만듭니다.</li>
</ul>
<h3 id="저장소-복제clone와-반영pull">저장소 복제(Clone)와 반영(Pull)</h3>
<ul>
<li><strong><code>git clone &lt;주소&gt; [폴더명]</code></strong>: 원격 저장소의 전체 내용을 복제하여 새로운 로컬 저장소를 만듭니다. (반드시 저장소를 만들 상위 디렉터리에서 실행하세요.)</li>
<li><strong><code>git pull</code></strong>: 원격 저장소의 새로운 변경 사항을 내려받아 현재 내 워킹 트리에 합칩니다.</li>
</ul>
<hr>
<h2 id="💡-작업-흐름-요약-workflow">💡 작업 흐름 요약 (Workflow)</h2>
<ol>
<li><strong><code>git add .</code></strong>: 변경된 모든 파일을 스테이지에 올림</li>
<li><strong><code>git commit -m &quot;feat: 기능 추가&quot;</code></strong>: 로컬에 버전 기록</li>
<li><strong><code>git push</code></strong>: 원격 저장소에 공유</li>
<li>(다른 환경에서) <strong><code>git pull</code></strong>: 최신 상태 업데이트</li>
</ol>
<p>#Git #GitBash #CLI #Github #버전관리 #백엔드공부</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 데이터로 데이터 영속성 구현(2)]]></title>
            <link>https://velog.io/@woojin-archive/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B5%AC%ED%98%842-ajttoie7</link>
            <guid>https://velog.io/@woojin-archive/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B5%AC%ED%98%842-ajttoie7</guid>
            <pubDate>Mon, 23 Feb 2026 13:39:34 GMT</pubDate>
            <description><![CDATA[<h1 id="13장의-입출금-시스템-구현-확장">13장의 입출금 시스템 구현 확장</h1>
<h2 id="개요">개요</h2>
<ol>
<li>발신인 계좌에서 지정된 금액을 인출한다.</li>
<li>수취인 계좌에 해당 금액을 입금한다.</li>
</ol>
<hr>
<h2 id="model">Model</h2>
<pre><code class="language-java">public class Account {

    @Id
    private long id;

    private String name;
    private BigDecimal amount;

    // getter, setter 생략
}</code></pre>
<hr>
<h2 id="repository">Repository</h2>
<pre><code class="language-java">public interface AccountRepository extends CrudRepository&lt;Account, Long&gt; {

    @Query(&quot;SELECT * FROM account WHERE name = :name&quot;)
    List&lt;Account&gt; findAccountsByName(String name);

    @Modifying
    @Query(&quot;UPDATE account SET amount = :amount WHERE id = :id&quot;)
    void changeAmount(long id, BigDecimal amount);
}</code></pre>
<hr>
<h2 id="service">Service</h2>
<ul>
<li><p>변동성 있는 서비스 메서드에 <code>@Transactional</code> 애너테이션을 붙여 애스펙트를 사용한다.</p>
</li>
<li><p>Spring Data에서 기본적으로 <code>findById()</code>, <code>findAll()</code> 등의 메서드를 제공한다.</p>
</li>
<li><p><code>Iterable</code>은 <code>List</code>의 상위 타입이다.</p>
<pre><code class="language-java">@Service
public class TransferService {

  private final AccountRepository accountRepository;

  public TransferService(AccountRepository accountRepository) {
      this.accountRepository = accountRepository;
  }

  @Transactional
  public void transferMoney(
          long idSender,
          long idReceiver,
          BigDecimal amount
  ) {
      Account sender = accountRepository.findById(idSender)
              .orElseThrow(() -&gt; new AccountNotFoundException());

      Account receiver = accountRepository.findById(idReceiver)
              .orElseThrow(() -&gt; new AccountNotFoundException());

      BigDecimal senderNewAmount = sender.getAmount().subtract(amount);
      BigDecimal receiverNewAmount = receiver.getAmount().add(amount);

      accountRepository.changeAmount(idSender, senderNewAmount);
      accountRepository.changeAmount(idReceiver, receiverNewAmount);
  }

  public Iterable&lt;Account&gt; getAllAccounts() {
      return accountRepository.findAll();
  }

  public List&lt;Account&gt; findAccountsByName(String name) {
      return accountRepository.findAccountsByName(name);
  }
}</code></pre>
</li>
</ul>
<hr>
<h2 id="exception">Exception</h2>
<pre><code class="language-java">public class AccountNotFoundException extends RuntimeException {
}</code></pre>
<hr>
<h2 id="controller">Controller</h2>
<ul>
<li><p>계좌 상세 정보를 반환할 때 비필수 매개변수(<code>required = false</code>)로 이름을 받아 조건부로 조회한다.</p>
<pre><code class="language-java">@RestController
public class AccountController {

  private final TransferService transferService;

  public AccountController(TransferService transferService) {
      this.transferService = transferService;
  }

  @PostMapping(&quot;/transfer&quot;)
  public void transferMoney(
          @RequestBody TransferRequest request
  ) {
      transferService.transferMoney(
              request.getSenderAccountId(),
              request.getReceiverAccountId(),
              request.getAmount()
      );
  }

  @GetMapping(&quot;/accounts&quot;)
  public Iterable&lt;Account&gt; getAllAccounts(
          @RequestParam(required = false) String name
  ) {
      if (name == null) return transferService.getAllAccounts(); // 모든 계좌 반환
      else return transferService.findAccountsByName(name);     // 이름으로 필터링
  }
}</code></pre>
</li>
</ul>
<hr>
<h2 id="dto">DTO</h2>
<pre><code class="language-java">public class TransferRequest {
    private long senderAccountId;
    private long receiverAccountId;
    private BigDecimal amount;

    // getter 생략
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 데이터로 데이터 영속성 구현(1)]]></title>
            <link>https://velog.io/@woojin-archive/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B5%AC%ED%98%841</link>
            <guid>https://velog.io/@woojin-archive/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B5%AC%ED%98%841</guid>
            <pubDate>Mon, 23 Feb 2026 13:23:42 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-14-스프링-데이터spring-data로-데이터-영속성-구현">[Spring] 14. 스프링 데이터(Spring Data)로 데이터 영속성 구현</h1>
<p>스프링 데이터는 반복되는 CRUD 코드를 획기적으로 줄여주고, 다양한 영속성 기술(RDBMS, NoSQL 등)을 공통된 방식으로 다룰 수 있게 해주는 강력한 라이브러리입니다.</p>
<hr>
<h2 id="1-스프링-데이터spring-data란">1. 스프링 데이터(Spring Data)란?</h2>
<ul>
<li><strong>정의:</strong> 영속성 기술에 맞는 구현을 제공하여, 최소한의 코드 작성만으로 리포지터리를 정의할 수 있게 돕는 도구입니다.</li>
<li><strong>다양한 선택지:</strong> JDBC를 통한 직접 연결뿐만 아니라 MongoDB, Neo4J 같은 NoSQL이나 하이버네이트(Hibernate) 같은 ORM 프레임워크와도 연동 가능합니다.</li>
<li><strong>구현 단순화 방법:</strong><ol>
<li><strong>공통 추상화:</strong> 다양한 기술에 대해 일관된 인터페이스 집합을 제공하여 사용법을 유사하게 유지합니다.</li>
<li><strong>자동 구현:</strong> 사용자가 인터페이스(추상화)만 정의하면, 스프링 데이터가 백그라운드에서 실제 구현체를 생성하여 제공합니다.</li>
</ol>
</li>
</ul>
<hr>
<h2 id="2-스프링-데이터의-작동-방식-및-주요-인터페이스">2. 스프링 데이터의 작동 방식 및 주요 인터페이스</h2>
<p>스프링 데이터는 앱의 영속성 기능을 정의하기 위해 확장(extends)해야 하는 공통 인터페이스 계약을 제공합니다.</p>
<ul>
<li><strong>주요 인터페이스:</strong><ul>
<li><strong>Repository:</strong> 마커 인터페이스로, 가장 기본적인 계층을 나타냅니다.</li>
<li><strong>CrudRepository:</strong> 기본적인 생성, 조회, 수정, 삭제(CRUD) 연산을 제공합니다.</li>
<li><strong>PagingAndSortingRepository:</strong> CRUD 기능에 정렬 및 페이지네이션 기능이 추가됩니다.</li>
<li><strong>JpaRepository:</strong> 스프링 데이터 JPA 모듈에서 제공하며, 하이버네이트 같은 ORM 전용 연산이 추가된 인터페이스입니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="3-스프링-데이터-jdbc-사용-및-모델링">3. 스프링 데이터 JDBC 사용 및 모델링</h2>
<h3 id="기본-규칙">기본 규칙</h3>
<ul>
<li><p><strong>@Id:</strong> 어떤 필드가 테이블의 기본 키(Primary Key)인지 표시합니다.</p>
</li>
<li><p><strong>제네릭 타입 설정:</strong> 리포지터리 인터페이스 확장 시 두 개의 제네릭 타입을 지정해야 합니다.</p>
<ol>
<li><p><strong>엔티티(Entity):</strong> 리포지터리가 관리할 모델 클래스.</p>
</li>
<li><p><strong>ID 타입:</strong> 기본 키(@Id) 필드의 데이터 타입.</p>
<pre><code class="language-java">public class Account {

@Id
private long id;

private String name;
private BigDecimal amount;
</code></pre>
</li>
</ol>
</li>
</ul>
<p>//getter setter 생략
}</p>
<pre><code>```java
public interface AccountRepository 
extends CrudRepository&lt;Account, Long//엔티티, 기본키 타입&gt; {
}</code></pre><hr>
<h2 id="4-쿼리-생성-전략-query-methods">4. 쿼리 생성 전략 (Query Methods)</h2>
<p>스프링 데이터는 SQL을 직접 작성하지 않아도 메서드 이름을 해석하여 자동으로 쿼리를 생성해 줍니다.</p>
<h3 id="메서드-이름-해석-규칙-예시-findaccountbynamestring-name">메서드 이름 해석 규칙 예시: <code>findAccountByName(String name)</code></h3>
<ol>
<li><p><strong>List<Account>:</strong> 결과가 0개 이상일 수 있음을 알리며 리스트로 매핑됩니다.</p>
</li>
<li><p><strong>find:</strong> <code>SELECT</code> 절로 변환됩니다.</p>
</li>
<li><p><strong>Accounts:</strong> 대상 테이블(<code>FROM account</code>)을 지정합니다.</p>
</li>
<li><p><strong>ByName:</strong> 조회 조건(<code>WHERE name = ?</code>)을 지정합니다.</p>
</li>
<li><p><strong>(String name):</strong> 매개변수 값이 쿼리의 바인딩 변수가 됩니다.</p>
<pre><code class="language-java">public interface AccountRepository extends CrudRepository&lt;Account, Long&gt; {

 List&lt;Account&gt; findAccountsByName(String name);
}</code></pre>
<h3 id="query-애너테이션-활용">@Query 애너테이션 활용</h3>
<p>메서드 이름이 지나치게 길어지거나 성능 최적화가 필요한 경우, <code>@Query</code>를 통해 SQL을 직접 명시할 수 있습니다.</p>
</li>
</ol>
<ul>
<li><p><strong>특징:</strong> 메서드 이름 명명 규칙에서 자유로워집니다.</p>
</li>
<li><p><strong>@Modifying:</strong> <code>UPDATE</code>, <code>INSERT</code>, <code>DELETE</code>와 같이 데이터를 변경하는 쿼리를 사용할 때는 반드시 이 애너테이션을 함께 추가해야 합니다.</p>
<pre><code class="language-java">public interface AccountRepository extends CrudRepository&lt;Account, Long&gt; {

  @Query(&quot;SELECT * FROM account WHERE name = :name&quot;)
  //쿼리의 매개변수 이름은 메서드 매개변수 이름과 동일해야 한다. 
  //콜론(:)과 매개변수 이름 사이에 공백이 없어야 한다.
  List&lt;Account&gt; findAccountsByName(String name);
}</code></pre>
<pre><code class="language-java">public interface AccountRepository extends CrudRepository&lt;Account, Long&gt; {

  @Query(&quot;SELECT * FROM account WHERE name = :name&quot;)
  List&lt;Account&gt; findAccountsByName(String name);

  @Modifying //데이터를 변경하는 연산을 정의하는 메서드에 @Modifying 애너테이션을 추가한다.
  @Query(&quot;UPDATE account SET amount = :amount WHERE id = :id&quot;)
  void changeAmount(long id, BigDecimal amount);
}</code></pre>
</li>
</ul>
<hr>
<h2 id="🎯-요약">🎯 요약</h2>
<ol>
<li><strong>스프링 데이터</strong>는 인터페이스만 정의하면 구현체를 자동으로 생성하여 개발 속도를 높여준다.</li>
<li><strong>CrudRepository</strong> 등을 확장하여 필요한 수준의 연산을 선택적으로 사용할 수 있다.</li>
<li><strong>쿼리 메서드</strong> 기능을 통해 메서드 이름만으로 SQL 쿼리를 자동 생성할 수 있다.</li>
<li>복잡한 쿼리는 <strong>@Query</strong>를 사용하며, 데이터 변경 시에는 <strong>@Modifying</strong>을 잊지 말아야 한다.</li>
</ol>
<p>#Spring #SpringData #SpringDataJDBC #Repository #CrudRepository #QueryMethod #백엔드공부</p>
]]></description>
        </item>
    </channel>
</rss>