<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>nano_o.log</title>
        <link>https://velog.io/</link>
        <description>즐거운 토마토</description>
        <lastBuildDate>Tue, 06 Oct 2026 14:06:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>nano_o.log</title>
            <url>https://velog.velcdn.com/images/nano_o/profile/3691e4c3-7e79-451f-b45b-d5bd6d2ed10b/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. nano_o.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/nano_o" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Python 코딩테스트 치트시트]]></title>
            <link>https://velog.io/@nano_o/Python-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%B9%98%ED%8A%B8%EC%8B%9C%ED%8A%B8</link>
            <guid>https://velog.io/@nano_o/Python-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%B9%98%ED%8A%B8%EC%8B%9C%ED%8A%B8</guid>
            <pubDate>Tue, 06 Oct 2026 14:06:38 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>코딩테스트에서 손에서 바로 나와야 하는 Python 문법/표준 라이브러리/패턴만 모았다. (Python 3.10 기준)</p>
</blockquote>
<hr>
<h2 id="1-기본-템플릿과-입출력">1. 기본 템플릿과 입출력</h2>
<p><code>input()</code>은 느리다... 입력이 1만 줄 넘으면 <code>sys.stdin.readline</code>으로 바꾸고, 재귀를 쓰면 <code>setrecursionlimit</code>을 올린다.</p>
<pre><code class="language-python">import sys
from collections import deque, defaultdict, Counter
from itertools import permutations, combinations, product, accumulate
from heapq import heappush, heappop, heapify
from bisect import bisect_left, bisect_right
from functools import lru_cache, cmp_to_key
import math

input = sys.stdin.readline
sys.setrecursionlimit(10**6)

n, m = map(int, input().split())
arr = list(map(int, input().split()))
grid = [list(map(int, input().split())) for _ in range(n)]
s = input().rstrip()          # readline은 개행 포함 → 문자열은 반드시 rstrip

# 출력 많으면 모아서 한 번에
out = []
for x in arr:
    out.append(str(x))
print(&quot;\n&quot;.join(out))
# 또는 sys.stdout.write(&quot;\n&quot;.join(out) + &quot;\n&quot;)</code></pre>
<table>
<thead>
<tr>
<th>상황</th>
<th>쓰는 것</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>한 줄 정수 여러 개</td>
<td><code>map(int, input().split())</code></td>
<td>변수 개수 모르면 <code>list(...)</code></td>
</tr>
<tr>
<td>문자열 그리드</td>
<td><code>[input().rstrip() for _ in range(n)]</code></td>
<td>각 행이 str, <code>grid[i][j]</code>로 접근</td>
</tr>
<tr>
<td>전체 입력 한 번에</td>
<td><code>data = sys.stdin.read().split()</code> → 인덱스로 꺼내기</td>
<td>가장 빠름</td>
</tr>
<tr>
<td>테스트케이스 반복</td>
<td><code>for _ in range(int(input())):</code></td>
<td></td>
</tr>
<tr>
<td>숫자 ↔ 문자열</td>
<td><code>int(s)</code>, <code>str(n)</code>, <code>float(s)</code></td>
<td></td>
</tr>
<tr>
<td>진법 변환</td>
<td><code>bin(n)[2:]</code>, <code>oct</code>, <code>hex</code>, <code>int(&quot;1011&quot;, 2)</code>, <code>format(n, &#39;b&#39;)</code></td>
<td></td>
</tr>
<tr>
<td>포맷 출력</td>
<td><code>f&quot;{x:.2f}&quot;</code>, <code>f&quot;{n:05d}&quot;</code>, <code>f&quot;{s:&gt;10}&quot;</code></td>
<td></td>
</tr>
<tr>
<td>구분자 출력</td>
<td><code>print(*arr)</code> (공백), <code>print(*arr, sep=&quot;\n&quot;)</code></td>
<td></td>
</tr>
<tr>
<td>끝 문자 지정</td>
<td><code>print(x, end=&quot; &quot;)</code></td>
<td></td>
</tr>
</tbody></table>
<p>과제형(클래스 설계 등)이면 입력 파싱 대신 <code>if __name__ == &quot;__main__&quot;:</code> 아래에 테스트 데이터를 만들어 직접 호출하고 <code>print</code>로 보여준다.</p>
<hr>
<h2 id="2-문자열">2. 문자열</h2>
<p>Python 문자열도 불변이다. 반복 <code>+=</code>는 느리니 리스트에 모아 <code>&quot;&quot;.join()</code>.</p>
<table>
<thead>
<tr>
<th>할 일</th>
<th>코드</th>
</tr>
</thead>
<tbody><tr>
<td>길이·인덱스·슬라이스</td>
<td><code>len(s)</code>, <code>s[i]</code>, <code>s[a:b]</code> (b 미포함), <code>s[::-1]</code> (뒤집기), <code>s[::2]</code></td>
</tr>
<tr>
<td>분리·합치기</td>
<td><code>s.split()</code> (공백 전부), <code>s.split(&quot;,&quot;)</code>, <code>&quot;,&quot;.join(list)</code></td>
</tr>
<tr>
<td>포함·위치·개수</td>
<td><code>&quot;ab&quot; in s</code>, <code>s.find(&quot;ab&quot;)</code> (없으면 -1), <code>s.index(&quot;ab&quot;)</code> (없으면 에러), <code>s.count(&quot;a&quot;)</code></td>
</tr>
<tr>
<td>바꾸기</td>
<td><code>s.replace(&quot;a&quot;, &quot;b&quot;)</code>, <code>s.replace(&quot;a&quot;, &quot;b&quot;, 1)</code> (1회)</td>
</tr>
<tr>
<td>대소문자</td>
<td><code>s.upper()</code>, <code>s.lower()</code>, <code>s.swapcase()</code>, <code>s.capitalize()</code>, <code>s.title()</code></td>
</tr>
<tr>
<td>공백 제거</td>
<td><code>s.strip()</code>, <code>s.lstrip()</code>, <code>s.rstrip()</code></td>
</tr>
<tr>
<td>시작·끝</td>
<td><code>s.startswith(&quot;ab&quot;)</code>, <code>s.endswith((&quot;a&quot;, &quot;b&quot;))</code> (튜플 가능)</td>
</tr>
<tr>
<td>판별</td>
<td><code>s.isdigit()</code>, <code>s.isalpha()</code>, <code>s.isalnum()</code>, <code>s.isupper()</code>, <code>s.islower()</code>, <code>s.isspace()</code></td>
</tr>
<tr>
<td>문자 ↔ 코드</td>
<td><code>ord(&#39;a&#39;)</code> → 97, <code>chr(97)</code> → &#39;a&#39;, <code>ord(c) - ord(&#39;a&#39;)</code> (알파벳 인덱스)</td>
</tr>
<tr>
<td>채우기</td>
<td><code>s.zfill(5)</code>, <code>s.rjust(5, &quot;0&quot;)</code>, <code>s.ljust(5)</code>, <code>s.center(5)</code></td>
</tr>
<tr>
<td>정렬</td>
<td><code>&quot;&quot;.join(sorted(s))</code>, <code>sorted(s, reverse=True)</code></td>
</tr>
<tr>
<td>반복</td>
<td><code>&quot;ab&quot; * 3</code></td>
</tr>
<tr>
<td>문자 교체 테이블</td>
<td><code>s.translate(str.maketrans(&quot;abc&quot;, &quot;xyz&quot;))</code></td>
</tr>
<tr>
<td>팰린드롬</td>
<td><code>s == s[::-1]</code></td>
</tr>
<tr>
<td>아스키 연산</td>
<td><code>chr(ord(c) + 1)</code></td>
</tr>
</tbody></table>
<pre><code class="language-python"># 문자 빈도
from collections import Counter
cnt = Counter(&quot;banana&quot;)          # {&#39;a&#39;: 3, &#39;n&#39;: 2, &#39;b&#39;: 1}
cnt.most_common(2)               # [(&#39;a&#39;, 3), (&#39;n&#39;, 2)]

# 정규식은 간단한 것만
import re
re.sub(r&quot;[^a-z0-9]&quot;, &quot;&quot;, s)      # 영소문자·숫자만 남기기
re.findall(r&quot;\d+&quot;, s)            # 숫자 덩어리 전부
re.split(r&quot;[,;]&quot;, s)</code></pre>
<hr>
<h2 id="3-리스트·튜플·슬라이싱">3. 리스트·튜플·슬라이싱</h2>
<p>리스트는 Java의 ArrayList, 튜플은 불변 리스트(해시 가능 → dict 키, set 원소로 쓸 수 있음).</p>
<pre><code class="language-python">a = [0] * n                        # 1차원 초기화
grid = [[0] * m for _ in range(n)] # 2차원 — [[0]*m]*n 은 같은 행 참조라 금지
visited = [[False] * m for _ in range(n)]

a.append(x);  a.insert(i, x);  a.pop();  a.pop(i);  a.remove(x)   # remove는 값, O(n)
a.index(x);  a.count(x);  a.reverse();  a.sort();  a.extend(b)
len(a);  sum(a);  max(a);  min(a);  sorted(a);  reversed(a)
a[-1]                              # 마지막
a[a.index(max(a))]                 # 최댓값 위치는 a.index(max(a))
b = a[:]  /  a.copy()  /  list(a)  # 얕은 복사
import copy; c = copy.deepcopy(grid)   # 2차원 복사

# 슬라이스
a[1:4];  a[:3];  a[3:];  a[::-1];  a[::2]
a[2:5] = [9, 9]                    # 구간 교체
del a[2:5]

# 컴프리헨션
sq = [x * x for x in a if x % 2 == 0]
flat = [x for row in grid for x in row]
transposed = list(zip(*grid))      # 전치 (튜플 행)
rotated = [list(r) for r in zip(*grid[::-1])]   # 시계방향 90도 회전

# 자주 쓰는 내장
for i, x in enumerate(a, start=1): ...
for x, y in zip(a, b): ...
any(x &gt; 0 for x in a);  all(x &gt; 0 for x in a)
list(map(str, a));  list(filter(None, a))
max(a, key=len);  max(d, key=d.get)   # 키 지정 최대</code></pre>
<p><code>a.sort()</code>는 제자리(None 반환), <code>sorted(a)</code>는 새 리스트. 리스트의 <code>in</code>은 O(n)이니 포함 확인이 많으면 set으로.</p>
<hr>
<h2 id="4-딕셔너리·집합·collections">4. 딕셔너리·집합·collections</h2>
<p>카운팅·그룹핑은 <code>defaultdict</code>와 <code>Counter</code>로 끝낸다. <code>dict.get(k)</code>는 없으면 None, <code>d[k]</code>는 KeyError.</p>
<pre><code class="language-python"># dict
d = {}
d[k] = v;  d.get(k, 0);  k in d;  del d[k];  d.pop(k, None)
d.keys();  d.values();  d.items()
for k, v in d.items(): ...
sorted(d.items(), key=lambda kv: -kv[1])       # 값 내림차순
dict(sorted(d.items()))                        # 키 정렬된 새 dict
d.setdefault(k, []).append(v)                  # 그룹핑 (defaultdict 없이)
# dict는 3.7+ 삽입 순서 유지 → LinkedHashMap 역할

# defaultdict / Counter
from collections import defaultdict, Counter
cnt = defaultdict(int);   cnt[k] += 1
grp = defaultdict(list);  grp[k].append(v)
c = Counter(arr)                               # 빈도
c.most_common(3);  c[x];  c.total()  (3.10+)
Counter(a) - Counter(b);  Counter(a) &amp; Counter(b)   # 차집합·교집합 (빈도 기준)

# set
s = set();  s.add(x);  s.discard(x) (없어도 OK);  s.remove(x) (없으면 에러);  x in s
a | b;  a &amp; b;  a - b;  a ^ b                  # 합·교·차·대칭차
len(set(arr))                                  # 중복 제거 개수
frozenset(...)                                 # 해시 가능한 set (set의 set 만들 때)

# deque (큐·스택·슬라이딩)
from collections import deque
q = deque([1, 2])
q.append(x);  q.appendleft(x);  q.pop();  q.popleft();  q[0];  q[-1]
q.rotate(1)                                    # 오른쪽으로 한 칸
deque(maxlen=k)                                # 고정 길이 윈도우
# 리스트 pop(0)은 O(n) — 큐는 반드시 deque

# heapq (최소 힙)
import heapq
h = [];  heapq.heappush(h, x);  heapq.heappop(h);  h[0]
heapq.heapify(arr)                             # 리스트를 제자리 힙으로 O(n)
heapq.heappush(h, -x)                          # 최대 힙은 부호 뒤집기
heapq.heappush(h, (priority, data))            # 튜플은 첫 원소 기준
heapq.nlargest(3, arr);  heapq.nsmallest(3, arr, key=...)

# 정렬 유지 삽입 / 이분탐색
from bisect import bisect_left, bisect_right, insort
bisect_left(a, x)    # x 이상인 첫 인덱스 (lower bound)
bisect_right(a, x)   # x 초과인 첫 인덱스 (upper bound) → 개수 = right - left
insort(a, x)         # 정렬 유지 삽입 O(n)</code></pre>
<table>
<thead>
<tr>
<th>자료구조</th>
<th>언제</th>
<th>비용</th>
</tr>
</thead>
<tbody><tr>
<td>list</td>
<td>순서 목록, 인덱스 접근, 스택</td>
<td>끝 append/pop O(1), 앞 삽입/삭제 O(n), in O(n)</td>
</tr>
<tr>
<td>dict / set</td>
<td>카운팅, 중복 체크, 조회</td>
<td>O(1)</td>
</tr>
<tr>
<td>deque</td>
<td>큐, BFS, 양끝 조작</td>
<td>O(1)</td>
</tr>
<tr>
<td>heapq</td>
<td>최소/최대 반복 추출, 다익스트라</td>
<td>O(log n)</td>
</tr>
<tr>
<td>Counter / defaultdict</td>
<td>빈도, 그룹핑</td>
<td>O(1)</td>
</tr>
<tr>
<td>bisect + 정렬 리스트</td>
<td>범위 검색 (TreeMap 대용)</td>
<td>검색 O(log n), 삽입 O(n)</td>
</tr>
</tbody></table>
<p>Python엔 TreeMap이 없다. 정렬 상태 유지 + 범위 검색이 필요하면 정렬 리스트 + <code>bisect</code>, 또는 힙으로 우회.</p>
<hr>
<h2 id="5-정렬과-key">5. 정렬과 key</h2>
<p><code>sort</code>/<code>sorted</code>는 안정 정렬(TimSort). 다중 조건은 <code>key</code>에 튜플, 내림차순은 <code>reverse=True</code> 또는 숫자면 부호 반전.</p>
<pre><code class="language-python">a.sort()                                  # 오름차순 제자리
sorted(a, reverse=True)                   # 내림차순 새 리스트
sorted(words, key=len)                    # 길이 순
sorted(words, key=lambda w: (len(w), w))  # 길이 순, 같으면 사전순
sorted(people, key=lambda p: (-p[1], p[0]))   # 점수 내림, 이름 오름
sorted(d.items(), key=lambda kv: (-kv[1], kv[0]))

# 문자열 내림 + 숫자 오름처럼 부호 반전이 안 되는 경우 → cmp_to_key
from functools import cmp_to_key
def cmp(x, y):
    if x[0] != y[0]: return -1 if x[0] &gt; y[0] else 1      # 문자열 내림
    return x[1] - y[1]                                    # 숫자 오름
sorted(a, key=cmp_to_key(cmp))

# 가장 큰 수 만들기 (문자열 결합 비교)
sorted(nums, key=cmp_to_key(lambda a, b: 1 if a + b &lt; b + a else -1))

# 객체 정렬
sorted(songs, key=lambda s: (-s.plays, s.title))
from operator import itemgetter, attrgetter
sorted(rows, key=itemgetter(1, 0));  sorted(songs, key=attrgetter(&quot;plays&quot;))</code></pre>
<p><code>key</code> 함수는 원소당 한 번만 호출돼서 <code>cmp_to_key</code>보다 빠르다. 가능하면 튜플 key로 해결하고 cmp는 정말 필요할 때만.</p>
<hr>
<h2 id="6-itertools·functools·math">6. itertools·functools·math</h2>
<p>조합·순열은 직접 짜지 말고 <code>itertools</code>. 메모이제이션은 <code>lru_cache</code> 한 줄.</p>
<pre><code class="language-python">from itertools import permutations, combinations, product, combinations_with_replacement, accumulate, groupby, chain

list(permutations([1,2,3], 2))     # 순열 (순서 있음) nP2
list(combinations([1,2,3], 2))     # 조합 (순서 없음) nC2
list(product([0,1], repeat=3))     # 중복 순열 / 데카르트 곱 → 비트마스크 대용
list(combinations_with_replacement([1,2,3], 2))   # 중복 조합
list(accumulate([1,2,3]))          # 누적합 [1,3,6]
list(accumulate(a, max))           # 누적 최댓값
list(chain(a, b))                  # 이어붙이기
for k, g in groupby(sorted(a)): print(k, len(list(g)))   # 연속 동일값 묶기 (정렬 먼저)

from functools import lru_cache, reduce
@lru_cache(maxsize=None)
def fib(n):
    return n if n &lt; 2 else fib(n-1) + fib(n-2)
# 3.9+ 는 @cache 도 가능. 인자는 해시 가능해야 함 (리스트 → 튜플)
reduce(lambda x, y: x * y, a, 1)

import math
math.gcd(a, b);  math.lcm(a, b) (3.9+);  math.gcd(*arr) (3.9+)
math.factorial(n);  math.comb(n, r);  math.perm(n, r)
math.ceil(x);  math.floor(x);  math.sqrt(x);  math.isqrt(n) (정수 제곱근)
math.inf;  -math.inf;  float(&quot;inf&quot;)
math.log(x, base);  math.log2(x);  math.hypot(dx, dy);  math.dist(p, q)
divmod(a, b)       # (몫, 나머지)
pow(a, b, mod)     # 모듈러 거듭제곱 O(log b)
abs(x);  round(x, 2)   # round는 은행가 반올림 — 0.5는 짝수로</code></pre>
<hr>
<h2 id="7-자주-나오는-알고리즘-패턴">7. 자주 나오는 알고리즘 패턴</h2>
<h3 id="완전탐색">완전탐색</h3>
<pre><code class="language-python"># 비트마스크 부분집합
for mask in range(1 &lt;&lt; n):
    subset = [a[i] for i in range(n) if mask &amp; (1 &lt;&lt; i)]

# 백트래킹 (N-Queen 틀)
def dfs(depth, path):
    if depth == n:
        result.append(path[:])
        return
    for i in range(n):
        if not used[i]:
            used[i] = True;  path.append(i)
            dfs(depth + 1, path)
            path.pop();  used[i] = False</code></pre>
<h3 id="투포인터--슬라이딩-윈도우">투포인터 / 슬라이딩 윈도우</h3>
<pre><code class="language-python">left = total = 0;  best = float(&quot;inf&quot;)
for right in range(n):
    total += a[right]
    while total &gt;= target:
        best = min(best, right - left + 1)
        total -= a[left];  left += 1</code></pre>
<h3 id="누적합">누적합</h3>
<pre><code class="language-python">pre = [0] + list(accumulate(a))     # pre[i] = a[0..i-1] 합
# [l, r] 구간 합 = pre[r+1] - pre[l]

# 2차원
P = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n):
    for j in range(m):
        P[i+1][j+1] = grid[i][j] + P[i][j+1] + P[i+1][j] - P[i][j]</code></pre>
<h3 id="bfs-그리드-최단거리">BFS (그리드 최단거리)</h3>
<pre><code class="language-python">from collections import deque
dr, dc = [-1, 1, 0, 0], [0, 0, -1, 1]
dist = [[-1] * m for _ in range(n)]
q = deque([(sr, sc)]);  dist[sr][sc] = 0
while q:
    r, c = q.popleft()
    for d in range(4):
        nr, nc = r + dr[d], c + dc[d]
        if not (0 &lt;= nr &lt; n and 0 &lt;= nc &lt; m): continue
        if grid[nr][nc] == 1 or dist[nr][nc] != -1: continue
        dist[nr][nc] = dist[r][c] + 1
        q.append((nr, nc))</code></pre>
<h3 id="dfs-인접-리스트-재귀-→-스택">DFS (인접 리스트, 재귀 → 스택)</h3>
<pre><code class="language-python">adj = [[] for _ in range(n)]
adj[a].append(b);  adj[b].append(a)

visited = [False] * n
def dfs(u):
    visited[u] = True
    for v in adj[u]:
        if not visited[v]: dfs(v)

# 재귀 깊이 걱정되면 스택으로
stack = [start];  visited[start] = True
while stack:
    u = stack.pop()
    for v in adj[u]:
        if not visited[v]:
            visited[v] = True;  stack.append(v)</code></pre>
<h3 id="다익스트라">다익스트라</h3>
<pre><code class="language-python">import heapq
INF = float(&quot;inf&quot;)
dist = [INF] * n;  dist[s] = 0
pq = [(0, s)]
while pq:
    d, u = heapq.heappop(pq)
    if d &gt; dist[u]: continue
    for v, w in adj[u]:
        if dist[u] + w &lt; dist[v]:
            dist[v] = dist[u] + w
            heapq.heappush(pq, (dist[v], v))</code></pre>
<h3 id="이분탐색--매개변수-탐색">이분탐색 / 매개변수 탐색</h3>
<pre><code class="language-python">from bisect import bisect_left
idx = bisect_left(a, target)          # 정렬된 a에서 target 이상 첫 위치

# &quot;X가 가능한가&quot;가 단조일 때 최대 X
lo, hi = 1, max_x
while lo &lt; hi:
    mid = (lo + hi + 1) // 2
    if ok(mid): lo = mid
    else: hi = mid - 1
# 최소 X 는 mid = (lo+hi)//2, ok면 hi = mid, 아니면 lo = mid+1</code></pre>
<h3 id="유니온-파인드">유니온 파인드</h3>
<pre><code class="language-python">parent = list(range(n))
def find(x):
    while parent[x] != x:
        parent[x] = parent[parent[x]];  x = parent[x]
    return x
def union(a, b):
    parent[find(a)] = find(b)</code></pre>
<h3 id="dp-기본-틀">DP 기본 틀</h3>
<pre><code class="language-python"># 1차원
dp = [0] * (n + 1)
for i in range(1, n + 1):
    dp[i] = max(dp[i-1], ...)

# 2차원 (LCS, 격자 경로)
dp = [[0] * (m + 1) for _ in range(n + 1)]

# 탑다운
@lru_cache(None)
def solve(i, j): ...

# 배낭
for w, v in items:
    for cap in range(W, w - 1, -1):      # 0/1은 역순
        dp[cap] = max(dp[cap], dp[cap - w] + v)</code></pre>
<hr>
<h2 id="8-자주-하는-실수와-함정">8. 자주 하는 실수와 함정</h2>
<table>
<thead>
<tr>
<th>함정</th>
<th>틀린 예</th>
<th>맞는 예</th>
</tr>
</thead>
<tbody><tr>
<td>2차원 초기화</td>
<td><code>[[0]*m]*n</code> → 모든 행이 같은 객체</td>
<td><code>[[0]*m for _ in range(n)]</code></td>
</tr>
<tr>
<td>readline 개행</td>
<td><code>s = input()</code> 후 <code>len(s)</code>에 개행 포함</td>
<td><code>input().rstrip()</code></td>
</tr>
<tr>
<td>리스트 복사</td>
<td><code>b = a</code> (같은 참조)</td>
<td><code>b = a[:]</code>, 2차원은 <code>deepcopy</code></td>
</tr>
<tr>
<td>순회 중 삭제</td>
<td><code>for x in a: a.remove(x)</code></td>
<td><code>a = [x for x in a if cond]</code></td>
</tr>
<tr>
<td>큐를 list로</td>
<td><code>a.pop(0)</code> O(n)</td>
<td><code>deque.popleft()</code></td>
</tr>
<tr>
<td><code>sort</code> 반환값</td>
<td><code>a = a.sort()</code> → None</td>
<td><code>a.sort()</code> 또는 <code>a = sorted(a)</code></td>
</tr>
<tr>
<td>정수 나눗셈</td>
<td><code>7 / 2</code> → 3.5 (float)</td>
<td><code>7 // 2</code> → 3</td>
</tr>
<tr>
<td>음수 나눗셈</td>
<td><code>-7 // 2</code> → -4 (바닥)</td>
<td>0 방향 자르기는 <code>int(-7 / 2)</code> → -3</td>
</tr>
<tr>
<td>큰 수 나눗셈 float</td>
<td><code>10**18 / 3</code> 정밀도 손실</td>
<td><code>//</code> 유지, float 변환 피하기</td>
</tr>
<tr>
<td>재귀 깊이</td>
<td>기본 1000 → RecursionError</td>
<td><code>sys.setrecursionlimit(10**6)</code> + 깊이 10만 넘으면 스택으로</td>
</tr>
<tr>
<td>전역 변수 수정</td>
<td>함수 안 <code>cnt += 1</code> → UnboundLocalError</td>
<td><code>global cnt</code> 또는 리스트로 감싸기 <code>cnt[0] += 1</code></td>
</tr>
<tr>
<td>가변 기본 인자</td>
<td><code>def f(a, acc=[])</code></td>
<td><code>def f(a, acc=None): acc = acc or []</code></td>
</tr>
<tr>
<td>dict 키 없음</td>
<td><code>d[k]</code> KeyError</td>
<td><code>d.get(k, 0)</code> 또는 <code>defaultdict</code></td>
</tr>
<tr>
<td>set 원소로 리스트</td>
<td><code>{[1,2]}</code> TypeError</td>
<td><code>{(1,2)}</code> 튜플</td>
</tr>
<tr>
<td>lru_cache 인자</td>
<td>리스트 넘김</td>
<td>튜플/문자열로 변환</td>
</tr>
<tr>
<td><code>is</code> vs <code>==</code></td>
<td><code>x is 1000</code></td>
<td><code>x == 1000</code> (<code>is</code>는 None 비교에만)</td>
</tr>
<tr>
<td>문자열 결합 반복</td>
<td><code>s += c</code> 루프</td>
<td><code>parts.append(c)</code> 후 <code>&quot;&quot;.join(parts)</code></td>
</tr>
<tr>
<td><code>max([])</code></td>
<td>빈 리스트 → ValueError</td>
<td><code>max(a, default=0)</code></td>
</tr>
<tr>
<td>인덱스 음수</td>
<td><code>a[i-1]</code>에서 i=0 → 마지막 원소</td>
<td>경계 검사 먼저</td>
</tr>
<tr>
<td>부동소수 비교</td>
<td><code>0.1 + 0.2 == 0.3</code> False</td>
<td><code>abs(x - y) &lt; 1e-9</code> 또는 정수로 스케일</td>
</tr>
<tr>
<td>round</td>
<td><code>round(2.5)</code> → 2</td>
<td>올림 반올림은 <code>int(x + 0.5)</code></td>
</tr>
<tr>
<td>입력 속도</td>
<td><code>input()</code> 10만 번</td>
<td><code>sys.stdin.readline</code></td>
</tr>
<tr>
<td>출력 속도</td>
<td><code>print</code> 10만 번</td>
<td>리스트에 모아 <code>&quot;\n&quot;.join</code></td>
</tr>
<tr>
<td>시간 초과 일반</td>
<td>1초 ≈ 2천만 연산 기준으로 복잡도 계산 안 함</td>
<td>n=10^5면 O(n log n)까지, n=10^3이면 O(n²) OK</td>
</tr>
</tbody></table>
<hr>
<h2 id="9-속도-올리는-습관">9. 속도 올리는 습관</h2>
<p>Python은 느리다. 같은 O(n)이라도 쓰는 방법에 따라 3~10배 차이 난다.</p>
<ul>
<li><code>for</code> 안에서 <code>len(a)</code>, <code>a.b.c</code> 같은 속성 접근 반복하지 말고 변수로 빼기</li>
<li><code>in</code> 검사는 list 말고 set/dict</li>
<li><code>sum</code>, <code>max</code>, <code>min</code>, <code>sorted</code>, <code>&quot;&quot;.join</code>, 컴프리헨션은 C로 돌아서 명시적 루프보다 빠름</li>
<li>2차원 접근 <code>grid[i][j]</code> 반복이면 <code>row = grid[i]</code> 한 번 꺼내기</li>
<li>재귀 DP보다 반복 DP가 2~3배 빠름. 깊이 깊으면 반복으로</li>
<li>전역 변수보다 지역 변수가 빠름 → 메인 로직을 <code>def main():</code> 안에 넣고 호출</li>
<li>PyPy 선택 가능하면 PyPy (단, 재귀와 큰 dict에서 메모리 주의)</li>
<li>문자열 비교 많으면 <code>ord</code>로 정수화</li>
</ul>
<pre><code class="language-python">def main():
    input = sys.stdin.readline
    # ... 전체 로직 ...
    print(...)

if __name__ == &quot;__main__&quot;:
    main()</code></pre>
<hr>
<h2 id="10-과제형-테스트용-클래스-설계">10. 과제형 테스트용 클래스 설계</h2>
<p>오프라인 IDE 테스트에서 &quot;플레이리스트 관리 기능을 만들어라&quot; 같은 과제가 나오면 이 틀로 간다.</p>
<pre><code class="language-python">from dataclasses import dataclass, field
from typing import Optional

@dataclass
class Song:
    title: str
    artist: str
    duration: int          # 초
    plays: int = 0

class Playlist:
    def __init__(self, name: str):
        self.name = name
        self._songs: list[Song] = []
        self._titles: set[str] = set()     # 중복 방지 O(1)

    def add(self, song: Song) -&gt; bool:
        if song.title in self._titles:
            return False
        self._songs.append(song)
        self._titles.add(song.title)
        return True

    def remove(self, title: str) -&gt; Optional[Song]:
        for i, s in enumerate(self._songs):
            if s.title == title:
                self._titles.discard(title)
                return self._songs.pop(i)
        return None

    def by_artist(self, artist: str) -&gt; list[Song]:
        return [s for s in self._songs if s.artist == artist]

    def total_duration(self) -&gt; int:
        return sum(s.duration for s in self._songs)

    def top(self, n: int) -&gt; list[Song]:
        return sorted(self._songs, key=lambda s: -s.plays)[:n]

    def __len__(self): return len(self._songs)
    def __repr__(self): return f&quot;Playlist({self.name}, {len(self)} songs)&quot;


if __name__ == &quot;__main__&quot;:
    pl = Playlist(&quot;드라이브&quot;)
    pl.add(Song(&quot;A&quot;, &quot;IU&quot;, 210, 50))
    pl.add(Song(&quot;B&quot;, &quot;IU&quot;, 180, 120))
    print(pl.add(Song(&quot;A&quot;, &quot;IU&quot;, 210)))   # False — 중복
    print(pl.by_artist(&quot;IU&quot;))
    print(pl.total_duration())
    print(pl.top(1))</code></pre>
<p>면접에서 받을 질문과 한 줄 답: 
왜 set을 따로 뒀나 → 중복 체크를 O(1)로 하려고, 리스트 <code>in</code>은 O(n)
곡이 100만 개면 → 삭제가 O(n)이니 dict로 title→Song 매핑하고 순서가 필요하면 OrderedDict나 인덱스 관리
<code>dataclass</code> 쓴 이유 → <code>__init__</code>, <code>__repr__</code>, <code>__eq__</code> 자동 생성으로 보일러플레이트 제거
타입 힌트는 → 실행엔 영향 없지만 의도를 읽는 사람에게 보여준다.
변수명은 <code>a</code>, <code>tmp</code> 대신 <code>play_count</code>, <code>songs_by_artist</code>처럼
Python은 snake_case</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java 코딩테스트 치트시트]]></title>
            <link>https://velog.io/@nano_o/Java-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%B9%98%ED%8A%B8%EC%8B%9C%ED%8A%B8</link>
            <guid>https://velog.io/@nano_o/Java-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%B9%98%ED%8A%B8%EC%8B%9C%ED%8A%B8</guid>
            <pubDate>Sun, 04 Oct 2026 17:00:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>평소 Python으로 문제를 풀다가 Java로 코딩테스트를 봐야 할 때, 손에서 바로 나와야 하는 것들만 모았다. 알고리즘 이론보다 &quot;Java로 생각을 코드로 옮기는 속도&quot;에 초점을 맞췄다. (Java 17 기준)</p>
</blockquote>
<hr>
<h2 id="1-기본-템플릿과-입출력">1. 기본 템플릿과 입출력</h2>
<p>오프라인 IDE 테스트는 채점기가 없으므로 <code>main</code>에서 직접 함수를 호출해 결과를 출력하는 형태가 기본이다. 입력이 주어지는 경우엔 <code>BufferedReader</code>가 안전하다.</p>
<pre><code class="language-java">import java.util.*;
import java.io.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());

        int[] arr = Arrays.stream(br.readLine().split(&quot; &quot;))
                          .mapToInt(Integer::parseInt).toArray();

        StringBuilder sb = new StringBuilder();
        for (int x : arr) sb.append(x).append(&#39;\n&#39;);
        System.out.print(sb);
    }
}</code></pre>
<table>
<thead>
<tr>
<th>상황</th>
<th>쓰는 것</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>입력 적음, 빨리 짜기</td>
<td><code>Scanner sc = new Scanner(System.in);</code> <code>sc.nextInt()</code>, <code>sc.next()</code>, <code>sc.nextLine()</code></td>
<td><code>nextInt()</code> 뒤 <code>nextLine()</code>은 개행 하나 먹으니 한 번 더 호출</td>
</tr>
<tr>
<td>입력 10만 줄 이상</td>
<td><code>BufferedReader</code> + <code>StringTokenizer</code></td>
<td><code>readLine()</code>은 String 반환, 파싱 필요</td>
</tr>
<tr>
<td>출력 많음</td>
<td><code>StringBuilder</code>에 모아 마지막에 한 번 출력</td>
<td><code>System.out.println</code> 반복은 느림</td>
</tr>
<tr>
<td>한 줄 정수 여러 개</td>
<td><code>Arrays.stream(line.split(&quot; &quot;)).mapToInt(Integer::parseInt).toArray()</code></td>
<td></td>
</tr>
<tr>
<td>문자열 → 숫자</td>
<td><code>Integer.parseInt(s)</code>, <code>Long.parseLong(s)</code>, <code>Double.parseDouble(s)</code></td>
<td></td>
</tr>
<tr>
<td>숫자 → 문자열</td>
<td><code>String.valueOf(n)</code>, <code>Integer.toString(n)</code>, <code>&quot;&quot; + n</code></td>
<td></td>
</tr>
<tr>
<td>진법 변환</td>
<td><code>Integer.toBinaryString(n)</code>, <code>Integer.toString(n, 2)</code>, <code>Integer.parseInt(&quot;1011&quot;, 2)</code></td>
<td></td>
</tr>
<tr>
<td>포맷 출력</td>
<td><code>String.format(&quot;%.2f&quot;, d)</code>, <code>System.out.printf(&quot;%d %s%n&quot;, a, b)</code></td>
<td></td>
</tr>
</tbody></table>
<p>과제형(플레이리스트 만들기 등)이면 입력 파싱 대신 <code>main</code>에 테스트 데이터를 직접 만들어 호출하고, 결과를 <code>println</code>으로 보여주면 된다. 클래스 여러 개는 한 파일에 <code>class Song {}</code>처럼 <code>public</code> 없이 두면 된다.</p>
<hr>
<h2 id="2-string--stringbuilder--char">2. String / StringBuilder / char</h2>
<p>String은 불변이라 반복 <code>+=</code>는 O(n²)다. 문자열을 만들거나 바꿀 땐 무조건 <code>StringBuilder</code>.</p>
<table>
<thead>
<tr>
<th>할 일</th>
<th>코드</th>
</tr>
</thead>
<tbody><tr>
<td>길이</td>
<td><code>s.length()</code> (배열은 <code>arr.length</code>, 리스트는 <code>list.size()</code>)</td>
</tr>
<tr>
<td>i번째 문자</td>
<td><code>s.charAt(i)</code> → <code>char</code></td>
</tr>
<tr>
<td>부분 문자열</td>
<td><code>s.substring(from, to)</code> (to 미포함), <code>s.substring(from)</code></td>
</tr>
<tr>
<td>문자 배열로</td>
<td><code>s.toCharArray()</code> / 다시 문자열로 <code>new String(chars)</code></td>
</tr>
<tr>
<td>분리</td>
<td><code>s.split(&quot; &quot;)</code>, <code>s.split(&quot;,&quot;)</code> — 정규식이라 <code>\\.</code>, `\</td>
</tr>
<tr>
<td>합치기</td>
<td><code>String.join(&quot;,&quot;, list)</code>, <code>String.join(&quot; &quot;, arr)</code></td>
</tr>
<tr>
<td>포함·위치</td>
<td><code>s.contains(&quot;ab&quot;)</code>, <code>s.indexOf(&quot;ab&quot;)</code> (없으면 -1), <code>s.startsWith</code>, <code>s.endsWith</code></td>
</tr>
<tr>
<td>바꾸기</td>
<td><code>s.replace(&quot;a&quot;, &quot;b&quot;)</code> (전부), <code>s.replaceAll(&quot;[0-9]&quot;, &quot;&quot;)</code> (정규식)</td>
</tr>
<tr>
<td>대소문자</td>
<td><code>s.toUpperCase()</code>, <code>s.toLowerCase()</code></td>
</tr>
<tr>
<td>공백 제거</td>
<td><code>s.trim()</code>, <code>s.strip()</code></td>
</tr>
<tr>
<td>비교</td>
<td><code>s.equals(t)</code> (절대 <code>==</code> 금지), <code>s.equalsIgnoreCase(t)</code>, <code>s.compareTo(t)</code> (사전순, 음수/0/양수)</td>
</tr>
<tr>
<td>비었는지</td>
<td><code>s.isEmpty()</code>, <code>s.isBlank()</code></td>
</tr>
<tr>
<td>반복</td>
<td><code>&quot;ab&quot;.repeat(3)</code></td>
</tr>
<tr>
<td>문자 판별</td>
<td><code>Character.isDigit(c)</code>, <code>isLetter(c)</code>, <code>isUpperCase(c)</code>, <code>isAlphabetic(c)</code></td>
</tr>
<tr>
<td>문자 ↔ 숫자</td>
<td><code>c - &#39;0&#39;</code> (숫자 문자→int), <code>(char)(n + &#39;0&#39;)</code>, <code>c - &#39;a&#39;</code> (알파벳 인덱스)</td>
</tr>
<tr>
<td>문자 변환</td>
<td><code>Character.toUpperCase(c)</code>, <code>Character.toLowerCase(c)</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">StringBuilder sb = new StringBuilder();
sb.append(&quot;abc&quot;).append(1).append(&#39;x&#39;);
sb.insert(0, &quot;z&quot;);        // 앞에 삽입
sb.deleteCharAt(sb.length() - 1);
sb.setCharAt(0, &#39;q&#39;);
sb.reverse();             // 뒤집기 — 팰린드롬 판별에 바로 쓰임
sb.setLength(0);          // 비우기
String result = sb.toString();</code></pre>
<p>문자열 뒤집기는 <code>new StringBuilder(s).reverse().toString()</code>. 문자 정렬은 <code>char[] c = s.toCharArray(); Arrays.sort(c); new String(c)</code>. 아스키 연산은 <code>char</code>가 정수로 자동 승격되니 <code>(char)(c + 1)</code>처럼 캐스팅 필요.</p>
<hr>
<h2 id="3-배열과-arrays-유틸">3. 배열과 Arrays 유틸</h2>
<p>크기가 고정이면 배열, 늘어나면 <code>ArrayList</code>. <code>int[]</code>와 <code>Integer[]</code>는 다른 타입이라 Comparator 정렬은 <code>Integer[]</code>에만 된다.</p>
<pre><code class="language-java">int[] a = new int[n];                 // 0으로 초기화
int[] b = {3, 1, 2};
int[][] grid = new int[r][c];
boolean[][] visited = new boolean[r][c];
Arrays.fill(a, -1);                   // 전체 채우기
for (int[] row : grid) Arrays.fill(row, Integer.MAX_VALUE);  // 2차원은 행별로

Arrays.sort(a);                       // 오름차순 (기본형은 역순 직접 불가)
Integer[] boxed = {3, 1, 2};
Arrays.sort(boxed, Collections.reverseOrder());
Arrays.sort(a, from, to);             // 구간 정렬

int[] copy = Arrays.copyOf(a, a.length);
int[] part = Arrays.copyOfRange(a, 1, 4);   // [1,4)
int[] c2 = a.clone();

Arrays.toString(a);                   // 디버그 출력 &quot;[1, 2, 3]&quot;
Arrays.deepToString(grid);            // 2차원
Arrays.equals(a, b);
Arrays.stream(a).sum();  .max().getAsInt();  .min().getAsInt();
Arrays.asList(boxed);                 // 고정 크기 List (add 불가)
List&lt;Integer&gt; list = new ArrayList&lt;&gt;(Arrays.asList(boxed));   // 수정 가능
List&lt;Integer&gt; li = Arrays.stream(a).boxed().collect(Collectors.toList());   // int[] → List
int[] back = li.stream().mapToInt(Integer::intValue).toArray();            // List → int[]
int idx = Arrays.binarySearch(a, key);  // 정렬된 배열만, 없으면 음수</code></pre>
<p>2차원 배열 정렬은 <code>Arrays.sort(arr, (x, y) -&gt; x[0] - y[0])</code> (int[][] 은 객체 배열이라 가능). 방향 배열은 <code>int[] dr = {-1, 1, 0, 0}; int[] dc = {0, 0, -1, 1};</code>로 두고 범위 검사 <code>if (nr &lt; 0 || nr &gt;= R || nc &lt; 0 || nc &gt;= C) continue;</code>를 반드시 먼저.</p>
<hr>
<h2 id="4-컬렉션-핵심">4. 컬렉션 핵심</h2>
<p>과제형 문제의 8할은 <code>ArrayList</code> + <code>HashMap</code>으로 끝난다. 선언은 인터페이스 타입으로, 제네릭은 래퍼 타입(<code>Integer</code>, <code>Long</code>, <code>Character</code>)만 가능.</p>
<h3 id="list">List</h3>
<pre><code class="language-java">List&lt;Integer&gt; list = new ArrayList&lt;&gt;();
list.add(x);  list.add(0, x);  list.get(i);  list.set(i, x);
list.remove(i);                 // 인덱스로 삭제
list.remove(Integer.valueOf(x)); // 값으로 삭제 (int면 인덱스로 오해함)
list.size();  list.isEmpty();  list.contains(x);  list.indexOf(x);
Collections.sort(list);  list.sort(null);  list.sort(Comparator.reverseOrder());
Collections.reverse(list);  Collections.max(list);  Collections.min(list);
Collections.swap(list, i, j);
list.subList(from, to);         // 뷰(복사 아님)
new ArrayList&lt;&gt;(list);          // 복사
List.of(1, 2, 3);               // 불변</code></pre>
<h3 id="map">Map</h3>
<pre><code class="language-java">Map&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
map.put(k, v);
map.get(k);                     // 없으면 null → int에 넣으면 NPE
map.getOrDefault(k, 0);
map.put(k, map.getOrDefault(k, 0) + 1);   // 카운팅 기본
map.merge(k, 1, Integer::sum);            // 카운팅 한 줄
map.computeIfAbsent(k, x -&gt; new ArrayList&lt;&gt;()).add(v);   // 그룹핑 한 줄
map.containsKey(k);  map.containsValue(v);  map.remove(k);  map.size();
for (Map.Entry&lt;String, Integer&gt; e : map.entrySet()) { e.getKey(); e.getValue(); }
for (String k : map.keySet()) {}
for (int v : map.values()) {}

TreeMap&lt;Integer, String&gt; tm = new TreeMap&lt;&gt;();   // 키 정렬 유지
tm.firstKey();  tm.lastKey();  tm.floorKey(x);  tm.ceilingKey(x);  tm.headMap(x);  tm.tailMap(x);
LinkedHashMap&lt;&gt;                                  // 삽입 순서 유지</code></pre>
<h3 id="set">Set</h3>
<pre><code class="language-java">Set&lt;Integer&gt; set = new HashSet&lt;&gt;();
set.add(x);  set.contains(x);  set.remove(x);  set.size();
new HashSet&lt;&gt;(list).size();     // 중복 제거 개수
TreeSet&lt;Integer&gt; ts = new TreeSet&lt;&gt;();   // 정렬 유지
ts.first();  ts.last();  ts.floor(x);  ts.ceiling(x);  ts.pollFirst();</code></pre>
<h3 id="deque-스택·큐-둘-다">Deque (스택·큐 둘 다)</h3>
<pre><code class="language-java">Deque&lt;Integer&gt; dq = new ArrayDeque&lt;&gt;();
// 큐: offer / poll / peek         (뒤에 넣고 앞에서 뺀다)
// 스택: push / pop / peek        (앞에 넣고 앞에서 뺀다)
dq.offerFirst(x);  dq.offerLast(x);  dq.pollFirst();  dq.pollLast();  dq.peekFirst();  dq.peekLast();
dq.isEmpty();
// Stack 클래스는 느리고 권장 안 함. ArrayDeque는 null 불가.</code></pre>
<h3 id="priorityqueue">PriorityQueue</h3>
<pre><code class="language-java">PriorityQueue&lt;Integer&gt; pq = new PriorityQueue&lt;&gt;();                          // 최소 힙
PriorityQueue&lt;Integer&gt; maxPq = new PriorityQueue&lt;&gt;(Collections.reverseOrder());
PriorityQueue&lt;int[]&gt; pq2 = new PriorityQueue&lt;&gt;((a, b) -&gt; a[1] - b[1]);      // 배열 우선순위
pq.offer(x);  pq.poll();  pq.peek();  pq.size();  pq.isEmpty();</code></pre>
<table>
<thead>
<tr>
<th>자료구조</th>
<th>언제</th>
<th>주요 연산 비용</th>
</tr>
</thead>
<tbody><tr>
<td>ArrayList</td>
<td>순서 있는 목록, 인덱스 접근</td>
<td>get O(1), 중간 삽입/삭제 O(n)</td>
</tr>
<tr>
<td>HashMap / HashSet</td>
<td>카운팅, 중복 체크, 키→값 조회</td>
<td>O(1)</td>
</tr>
<tr>
<td>TreeMap / TreeSet</td>
<td>정렬 상태 유지, 범위 검색</td>
<td>O(log n)</td>
</tr>
<tr>
<td>LinkedHashMap</td>
<td>삽입 순서 유지하는 Map (LRU 등)</td>
<td>O(1)</td>
</tr>
<tr>
<td>ArrayDeque</td>
<td>큐, 스택, BFS</td>
<td>O(1)</td>
</tr>
<tr>
<td>PriorityQueue</td>
<td>항상 최소/최대 꺼내기, 다익스트라</td>
<td>O(log n)</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-정렬과-comparator">5. 정렬과 Comparator</h2>
<p>람다 <code>(a, b) -&gt; a - b</code>는 오름차순, <code>b - a</code>는 내림차순. 값이 클 수 있으면 뺄셈 대신 <code>Integer.compare(a, b)</code>를 써야 오버플로우를 피한다.</p>
<pre><code class="language-java">// 기본
Collections.sort(list);
list.sort(Comparator.reverseOrder());
Arrays.sort(arr2d, (a, b) -&gt; a[0] - b[0]);

// 다중 조건: 점수 내림차순, 같으면 이름 오름차순
list.sort((a, b) -&gt; {
    if (a.score != b.score) return Integer.compare(b.score, a.score);
    return a.name.compareTo(b.name);
});

// 같은 것을 Comparator 체이닝으로 (면접에서 더 좋아 보임)
list.sort(Comparator.comparingInt((Song s) -&gt; s.score).reversed()
                    .thenComparing(s -&gt; s.name));

// 문자열 길이 순, 같으면 사전순
words.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));

// Map을 값 기준으로 정렬해 상위 N개
List&lt;Map.Entry&lt;String, Integer&gt;&gt; entries = new ArrayList&lt;&gt;(map.entrySet());
entries.sort((a, b) -&gt; b.getValue() - a.getValue());</code></pre>
<p>객체가 기본 정렬 기준을 가지게 하려면 <code>Comparable</code> 구현:</p>
<pre><code class="language-java">class Song implements Comparable&lt;Song&gt; {
    String title; int plays;
    Song(String title, int plays) { this.title = title; this.plays = plays; }
    @Override
    public int compareTo(Song o) { return Integer.compare(o.plays, this.plays); }  // 재생수 내림차순
    @Override
    public String toString() { return title + &quot;(&quot; + plays + &quot;)&quot;; }
}</code></pre>
<p><code>Collections.sort</code>와 <code>Arrays.sort(객체)</code>는 안정 정렬(TimSort)이라 같은 키는 원래 순서 유지. <code>Arrays.sort(int[])</code>는 듀얼 피벗 퀵소트로 최악 O(n²)가 있으나 실무 테스트에선 신경 안 써도 됨.</p>
<hr>
<h2 id="6-stream-핵심-패턴">6. Stream 핵심 패턴</h2>
<p>과제형에서 &quot;필터링해서 정렬해서 상위 N개&quot; 같은 요구는 Stream 한 줄이 가장 읽기 좋다. 다만 성능 질문이 나오면 &quot;반복문과 동일한 O(n), 가독성 때문에 썼다&quot;고 답하면 된다.</p>
<pre><code class="language-java">import java.util.stream.*;

// 필터 + 변환 + 수집
List&lt;String&gt; titles = songs.stream()
    .filter(s -&gt; s.plays &gt; 100)
    .map(s -&gt; s.title)
    .collect(Collectors.toList());      // Java 16+ 면 .toList()

// 정렬 + 상위 N
List&lt;Song&gt; top3 = songs.stream()
    .sorted(Comparator.comparingInt((Song s) -&gt; s.plays).reversed())
    .limit(3)
    .collect(Collectors.toList());

// 그룹핑: 아티스트별 곡 목록 / 개수 / 합계
Map&lt;String, List&lt;Song&gt;&gt; byArtist = songs.stream().collect(Collectors.groupingBy(s -&gt; s.artist));
Map&lt;String, Long&gt; countByArtist = songs.stream().collect(Collectors.groupingBy(s -&gt; s.artist, Collectors.counting()));
Map&lt;String, Integer&gt; playsByArtist = songs.stream().collect(Collectors.groupingBy(s -&gt; s.artist, Collectors.summingInt(s -&gt; s.plays)));

// 집계
int total = songs.stream().mapToInt(s -&gt; s.plays).sum();
Optional&lt;Song&gt; best = songs.stream().max(Comparator.comparingInt(s -&gt; s.plays));
double avg = songs.stream().mapToInt(s -&gt; s.plays).average().orElse(0);
boolean any = songs.stream().anyMatch(s -&gt; s.plays == 0);   // allMatch, noneMatch
long cnt = songs.stream().filter(s -&gt; s.plays &gt; 0).count();

// 중복 제거, 문자열 합치기
List&lt;String&gt; artists = songs.stream().map(s -&gt; s.artist).distinct().collect(Collectors.toList());
String joined = titles.stream().collect(Collectors.joining(&quot;, &quot;));

// 범위 반복, 문자열 문자 순회
IntStream.range(0, n).forEach(i -&gt; ...);          // 0..n-1
IntStream.rangeClosed(1, n).sum();
s.chars().filter(Character::isDigit).count();

// List → Map (키 중복 시 터짐, 세 번째 인자로 병합 규칙)
Map&lt;String, Song&gt; byTitle = songs.stream().collect(Collectors.toMap(s -&gt; s.title, s -&gt; s, (a, b) -&gt; a));</code></pre>
<p><code>Optional</code>은 <code>.orElse(기본값)</code>, <code>.orElseThrow()</code>, <code>.isPresent()</code>, <code>.get()</code>로 꺼낸다. <code>Collectors.toList()</code>로 만든 리스트는 수정 가능, <code>.toList()</code>(Java 16+)는 불변이라 add 하면 예외.</p>
<hr>
<h2 id="7-자주-나오는-알고리즘-패턴">7. 자주 나오는 알고리즘 패턴</h2>
<p>1시간 오프라인 테스트에서 실제로 나오는 건 이 여섯 개가 거의 전부다. 각각 손에서 바로 나와야 한다.</p>
<h3 id="완전탐색-순열·조합·부분집합">완전탐색 (순열·조합·부분집합)</h3>
<pre><code class="language-java">// 조합: n개 중 r개 고르기
static void comb(int[] arr, int start, int r, List&lt;Integer&gt; cur, List&lt;List&lt;Integer&gt;&gt; out) {
    if (cur.size() == r) { out.add(new ArrayList&lt;&gt;(cur)); return; }
    for (int i = start; i &lt; arr.length; i++) {
        cur.add(arr[i]);
        comb(arr, i + 1, r, cur, out);
        cur.remove(cur.size() - 1);
    }
}
// 순열은 start 대신 boolean[] used 로, 부분집합은 비트마스크 for (int mask = 0; mask &lt; (1 &lt;&lt; n); mask++)</code></pre>
<h3 id="투포인터--슬라이딩-윈도우">투포인터 / 슬라이딩 윈도우</h3>
<pre><code class="language-java">// 합이 target 이상인 최소 길이 구간
int left = 0, sum = 0, best = Integer.MAX_VALUE;
for (int right = 0; right &lt; n; right++) {
    sum += arr[right];
    while (sum &gt;= target) {
        best = Math.min(best, right - left + 1);
        sum -= arr[left++];
    }
}</code></pre>
<h3 id="누적합">누적합</h3>
<pre><code class="language-java">int[] pre = new int[n + 1];
for (int i = 0; i &lt; n; i++) pre[i + 1] = pre[i] + arr[i];
// [l, r] 구간 합 = pre[r + 1] - pre[l]</code></pre>
<h3 id="bfs-그리드-최단거리">BFS (그리드 최단거리)</h3>
<pre><code class="language-java">int[] dr = {-1, 1, 0, 0}, dc = {0, 0, -1, 1};
int[][] dist = new int[R][C];
for (int[] row : dist) Arrays.fill(row, -1);
Deque&lt;int[]&gt; q = new ArrayDeque&lt;&gt;();
q.offer(new int[]{sr, sc}); dist[sr][sc] = 0;
while (!q.isEmpty()) {
    int[] cur = q.poll();
    for (int d = 0; d &lt; 4; d++) {
        int nr = cur[0] + dr[d], nc = cur[1] + dc[d];
        if (nr &lt; 0 || nr &gt;= R || nc &lt; 0 || nc &gt;= C) continue;
        if (grid[nr][nc] == 1 || dist[nr][nc] != -1) continue;
        dist[nr][nc] = dist[cur[0]][cur[1]] + 1;
        q.offer(new int[]{nr, nc});
    }
}</code></pre>
<h3 id="dfs-인접-리스트-연결-요소">DFS (인접 리스트, 연결 요소)</h3>
<pre><code class="language-java">List&lt;List&lt;Integer&gt;&gt; adj = new ArrayList&lt;&gt;();
for (int i = 0; i &lt; n; i++) adj.add(new ArrayList&lt;&gt;());
adj.get(a).add(b); adj.get(b).add(a);

boolean[] visited = new boolean[n];
static void dfs(int u) {
    visited[u] = true;
    for (int v : adj.get(u)) if (!visited[v]) dfs(v);
}
// 재귀 깊이 1만 넘으면 스택으로 바꿀 것 (Java 기본 스택은 작음)</code></pre>
<h3 id="이분탐색">이분탐색</h3>
<pre><code class="language-java">// 정렬된 배열에서 target 이상인 첫 위치 (lower bound)
int lo = 0, hi = n;
while (lo &lt; hi) {
    int mid = (lo + hi) &gt;&gt;&gt; 1;
    if (arr[mid] &lt; target) lo = mid + 1; else hi = mid;
}
// 매개변수 탐색: &quot;X가 가능한가&quot;가 단조이면 같은 틀로 최대/최소 X를 찾는다</code></pre>
<h3 id="유니온-파인드-그룹-묶기">유니온 파인드 (그룹 묶기)</h3>
<pre><code class="language-java">int[] parent;
static int find(int x) { return parent[x] == x ? x : (parent[x] = find(parent[x])); }
static void union(int a, int b) { parent[find(a)] = find(b); }</code></pre>
<h3 id="유틸-함수">유틸 함수</h3>
<pre><code class="language-java">Math.max(a, b);  Math.min(a, b);  Math.abs(x);  Math.pow(2, 10);  Math.sqrt(x);  Math.floorMod(a, m);
long gcd(long a, long b) { return b == 0 ? a : gcd(b, a % b); }   // lcm = a / gcd * b
Integer.MAX_VALUE;  Integer.MIN_VALUE;  Long.MAX_VALUE;
Integer.bitCount(n);  1 &lt;&lt; k;  (mask &amp; (1 &lt;&lt; k)) != 0;</code></pre>
<hr>
<h2 id="8-python-유저가-java에서-자주-하는-실수">8. Python 유저가 Java에서 자주 하는 실수</h2>
<p>이 표의 절반은 컴파일 에러로 바로 잡히지만, 나머지 절반은 조용히 틀린 답을 낸다. 특히 <code>==</code>, 정수 나눗셈, 오버플로우는 테스트 끝나기 전에 한 번씩 훑어볼 것.</p>
<table>
<thead>
<tr>
<th>함정</th>
<th>틀린 예</th>
<th>맞는 예</th>
</tr>
</thead>
<tbody><tr>
<td>문자열 비교</td>
<td><code>s == &quot;abc&quot;</code></td>
<td><code>s.equals(&quot;abc&quot;)</code></td>
</tr>
<tr>
<td>Integer 비교 (128 이상이면 틀림)</td>
<td><code>list.get(i) == list.get(j)</code></td>
<td><code>list.get(i).equals(list.get(j))</code> 또는 <code>intValue()</code></td>
</tr>
<tr>
<td>정수 나눗셈</td>
<td><code>7 / 2</code> → 3 (Python 3.5 기대)</td>
<td><code>7 / 2.0</code> 또는 <code>(double) a / b</code></td>
</tr>
<tr>
<td>음수 나머지</td>
<td><code>-7 % 3</code> → -1</td>
<td><code>Math.floorMod(-7, 3)</code> → 2</td>
</tr>
<tr>
<td>int 오버플로우 (약 21억)</td>
<td><code>int sum = a * b</code></td>
<td><code>long sum = (long) a * b</code> — 곱하기 전에 캐스팅</td>
</tr>
<tr>
<td>오버플로우 Comparator</td>
<td><code>(a, b) -&gt; a - b</code> (큰 값)</td>
<td><code>Integer.compare(a, b)</code></td>
</tr>
<tr>
<td>char + 숫자</td>
<td><code>&#39;a&#39; + 1</code> → 98 (int)</td>
<td><code>(char)(&#39;a&#39; + 1)</code> → &#39;b&#39;</td>
</tr>
<tr>
<td>배열 출력</td>
<td><code>System.out.println(arr)</code> → 주소</td>
<td><code>Arrays.toString(arr)</code></td>
</tr>
<tr>
<td>기본형 배열 역순 정렬</td>
<td><code>Arrays.sort(intArr, reverseOrder())</code> 컴파일 에러</td>
<td><code>Integer[]</code>로 바꾸거나 정렬 후 뒤집기</td>
</tr>
<tr>
<td>List.remove 오해</td>
<td><code>list.remove(3)</code> → 인덱스 3 삭제</td>
<td>값 삭제는 <code>list.remove(Integer.valueOf(3))</code></td>
</tr>
<tr>
<td>순회 중 삭제</td>
<td>for-each 안에서 <code>list.remove()</code> → ConcurrentModificationException</td>
<td><code>removeIf(x -&gt; ...)</code> 또는 Iterator</td>
</tr>
<tr>
<td>Map.get null</td>
<td><code>int v = map.get(k)</code> → NPE</td>
<td><code>map.getOrDefault(k, 0)</code></td>
</tr>
<tr>
<td>제네릭에 기본형</td>
<td><code>List&lt;int&gt;</code></td>
<td><code>List&lt;Integer&gt;</code></td>
</tr>
<tr>
<td>스트림 재사용</td>
<td>같은 stream 두 번 소비</td>
<td>매번 <code>.stream()</code> 새로</td>
</tr>
<tr>
<td>스택 오버플로우</td>
<td>재귀 깊이 수만</td>
<td>반복 + ArrayDeque로 전환</td>
</tr>
<tr>
<td>객체 복사</td>
<td><code>List&lt;Integer&gt; b = a</code> (같은 참조)</td>
<td><code>new ArrayList&lt;&gt;(a)</code></td>
</tr>
<tr>
<td>리스트 안 리스트 복사</td>
<td><code>out.add(cur)</code> (백트래킹에서 다 같은 객체)</td>
<td><code>out.add(new ArrayList&lt;&gt;(cur))</code></td>
</tr>
<tr>
<td>substring 범위</td>
<td><code>s.substring(i, j)</code> j 포함으로 착각</td>
<td>j 미포함 (Python 슬라이스와 동일)</td>
</tr>
<tr>
<td>리스트 마지막</td>
<td><code>list.get(-1)</code></td>
<td><code>list.get(list.size() - 1)</code></td>
</tr>
<tr>
<td>자료구조 다시 보기</td>
<td>적은 배열에서만 테스트</td>
<td>빈 입력, 한 개 입력, 중복, 최대 크기 확인</td>
</tr>
</tbody></table>
<p>문법 상 자주 막히는 것: 삼항은 <code>a ? b : c</code>, 향상된 for문은 <code>for (int x : arr)</code>, <code>static</code> 메서드에서는 인스턴스 필드 접근 불가(헬퍼 메서드를 모두 <code>static</code>으로 통일하거나 객체를 만들어 호출), 예외를 던지는 메서드 쓰면 <code>throws IOException</code> 또는 try-catch. 프로그래머스 스타일은 <code>class Solution { public int solution(int[] arr) {...} }</code>이니 그 형태도 익숙해지기.</p>
<hr>
<h2 id="9-intellij-필수-단축키와-낯선-환경-대비">9. IntelliJ 필수 단축키와 낯선 환경 대비</h2>
<p>회사 장비는 맥북일 수도 윈도우일 수도 있다. 이 열 개만 양쪽으로 외우면 된다. 마우스로 해도 되니 기억 안 나면 그냥 클릭.</p>
<table>
<thead>
<tr>
<th>할 일</th>
<th>Mac</th>
<th>Windows</th>
</tr>
</thead>
<tbody><tr>
<td>실행</td>
<td><code>Ctrl + R</code></td>
<td><code>Shift + F10</code></td>
</tr>
<tr>
<td>현재 파일 main 실행</td>
<td><code>Ctrl + Shift + R</code></td>
<td><code>Ctrl + Shift + F10</code></td>
</tr>
<tr>
<td>자동완성</td>
<td><code>Ctrl + Space</code></td>
<td><code>Ctrl + Space</code></td>
</tr>
<tr>
<td>import 자동 추가</td>
<td><code>Opt + Enter</code> (빨간 줄에서)</td>
<td><code>Alt + Enter</code></td>
</tr>
<tr>
<td>모든 import 정리</td>
<td><code>Ctrl + Opt + O</code></td>
<td><code>Ctrl + Alt + O</code></td>
</tr>
<tr>
<td>코드 포맷</td>
<td><code>Cmd + Opt + L</code></td>
<td><code>Ctrl + Alt + L</code></td>
</tr>
<tr>
<td>줄 복제</td>
<td><code>Cmd + D</code></td>
<td><code>Ctrl + D</code></td>
</tr>
<tr>
<td>줄 삭제</td>
<td><code>Cmd + Backspace</code></td>
<td><code>Ctrl + Y</code></td>
</tr>
<tr>
<td>줄 이동</td>
<td><code>Opt + Shift + ↑/↓</code></td>
<td><code>Alt + Shift + ↑/↓</code></td>
</tr>
<tr>
<td>주석 토글</td>
<td><code>Cmd + /</code></td>
<td><code>Ctrl + /</code></td>
</tr>
<tr>
<td>이름 일괄 변경</td>
<td><code>Shift + F6</code></td>
<td><code>Shift + F6</code></td>
</tr>
<tr>
<td>메서드 추출</td>
<td><code>Cmd + Opt + M</code></td>
<td><code>Ctrl + Alt + M</code></td>
</tr>
<tr>
<td>정의로 이동</td>
<td><code>Cmd + B</code></td>
<td><code>Ctrl + B</code></td>
</tr>
<tr>
<td>파라미터 힌트</td>
<td><code>Cmd + P</code></td>
<td><code>Ctrl + P</code></td>
</tr>
<tr>
<td>어디서나 검색</td>
<td><code>Shift Shift</code></td>
<td><code>Shift Shift</code></td>
</tr>
<tr>
<td>에러 위치로</td>
<td><code>F2</code></td>
<td><code>F2</code></td>
</tr>
</tbody></table>
<p><strong>라이브 템플릿 (타이핑 후 Tab)</strong>: <code>main</code> → main 메서드, <code>sout</code> → println, <code>fori</code> → for (int i = 0; ...), <code>iter</code> → 향상된 for, <code>psvm</code> → main, <code>ifn</code> → if null, <code>.var</code> (식 뒤에) → 변수 추출, <code>.for</code> → 반복문 감싸기.</p>
<p><strong>시작 직후 할 일 (2분)</strong></p>
<ol>
<li>새 프로젝트가 아니라 이미 열려 있는지, Java 버전이 몇인지 확인 (<code>.toList()</code>, <code>var</code>, <code>record</code>는 16+ / 10+ / 16+)</li>
<li><code>Main</code> 클래스에 <code>sout(&quot;hi&quot;)</code> 하고 실행해서 콘솔 나오는지 확인</li>
<li>한글 입력 전환이 어떻게 되는지 확인 (윈도우면 한/영 키, 맥이면 Caps Lock 또는 Ctrl+Space)</li>
<li>자동완성이 안 뜨면 <code>Ctrl + Space</code> 수동 호출, 그래도 안 되면 외운 대로 친다</li>
</ol>
<p>테스트 중 전략: 먼저 클래스와 메서드 시그니처만 빈 몸으로 다 만들어 놓고 설명 주석 한 줄씩 달기 → 핵심 로직 구현 → <code>main</code>에서 테스트 데이터로 호출해 출력 → 엣지 케이스 추가. 시간이 끝나도 뼈대가 다 있으면 면접에서 설명할 게 남는다.</p>
<hr>
<h2 id="10-면접에서-왜-이-자료구조를-썼나-한-줄-답변">10. 면접에서 &quot;왜 이 자료구조를 썼나&quot; 한 줄 답변</h2>
<p>코드를 놓고 바로 면접이 이어지므로, 선택마다 이유를 한 문장으로 말할 수 있어야 한다. 답의 형식은 항상 &quot;필요한 연산 + 그 연산에서 이 구조가 빠른 이유 + 대안을 안 고른 이유&quot;.</p>
<table>
<thead>
<tr>
<th>질문</th>
<th>답</th>
</tr>
</thead>
<tbody><tr>
<td>왜 HashMap?</td>
<td>키로 값을 찾는 게 핵심이고 평균 O(1)이라. 순서나 정렬이 필요했다면 TreeMap이나 LinkedHashMap을 썼을 겁니다.</td>
</tr>
<tr>
<td>왜 HashSet?</td>
<td>중복 제거와 포함 여부 확인만 필요하고 값은 필요 없어서. List.contains는 O(n)이라 안 썼습니다.</td>
</tr>
<tr>
<td>왜 ArrayList?</td>
<td>인덱스 접근과 마지막에 추가가 대부분이라. 중간 삽입/삭제가 많았다면 LinkedList나 다른 구조를 고려했을 겁니다.</td>
</tr>
<tr>
<td>왜 ArrayDeque? Stack 클래스는?</td>
<td>Stack은 Vector 기반이라 동기화 오버헤드가 있고 공식적으로도 Deque 사용을 권장합니다.</td>
</tr>
<tr>
<td>왜 PriorityQueue?</td>
<td>매번 최소/최대만 꺼내면 되니 전체 정렬 O(n log n) 대신 삽입·삭제 O(log n)으로 충분해서.</td>
</tr>
<tr>
<td>왜 TreeMap?</td>
<td>키 정렬 상태를 유지하면서 범위 검색(floor/ceiling)이 필요해서. 조회만 하면 HashMap이 더 빠릅니다.</td>
</tr>
<tr>
<td>왜 StringBuilder?</td>
<td>String은 불변이라 반복 결합이 O(n²)가 되어서.</td>
</tr>
<tr>
<td>왜 Stream? / 왜 반복문?</td>
<td>Stream은 의도가 읽히고 복잡도는 동일. 다만 디버깅이나 이른 종료(break)가 필요한 곳은 반복문을 썼습니다.</td>
</tr>
<tr>
<td>시간 복잡도는?</td>
<td>입력 n에 대해 정렬 O(n log n)이 지배적이고 나머지는 O(n)입니다. (자기 코드의 가장 비싼 연산 하나를 집어 말하기)</td>
</tr>
<tr>
<td>데이터가 100만 개면?</td>
<td>메모리는 객체당 약 바이트 계산해서 말하고, 병목 연산을 짚고, 인덱스나 페이징으로 풀겠다고.</td>
</tr>
<tr>
<td>동시에 수정하면?</td>
<td>HashMap은 스레드 안전하지 않으니 ConcurrentHashMap이나 동기화, 실제 서비스면 DB 트랜잭션으로.</td>
</tr>
<tr>
<td>예외 처리는?</td>
<td>입력 검증은 경계에서 한 번, 내부는 신뢰. null은 Optional이나 기본값으로.</td>
</tr>
<tr>
<td>테스트는 어떻게?</td>
<td>빈 입력, 한 개, 중복, 경계값을 main에서 확인했고, 실무면 JUnit으로 분리합니다.</td>
</tr>
<tr>
<td>다 못 풀었는데?</td>
<td>여기까지 구현했고, 남은 부분은 이런 방법으로 할 계획이었다고 구체적으로. 변명 말고 설계를 말하기.</td>
</tr>
</tbody></table>
<p>하나 더: 변수명은 <code>a</code>, <code>tmp</code> 대신 <code>playCount</code>, <code>songsByArtist</code>처럼 의미 있게. 코드 스타일을 본다고 했으니 이게 점수에 직접 들어간다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java vs Python] 오버로딩/오버라이딩 한 장 정리]]></title>
            <link>https://velog.io/@nano_o/Java-vs-Python-%EC%98%A4%EB%B2%84%EB%A1%9C%EB%94%A9%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@nano_o/Java-vs-Python-%EC%98%A4%EB%B2%84%EB%A1%9C%EB%94%A9%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 04 Oct 2026 16:44:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>알고리즘 Cheat Sheet</strong> 시리즈는 코딩 테스트를 풀다가 &quot;이거 자바에선 뭐였지?&quot;, &quot;파이썬은 어떻게 했더라?&quot; 싶을 때 바로 펼쳐 보려고 만든 개인 참고용 정리입니다.
Java와 Python을 나란히 놓고, 문법 차이 때문에 실수하기 쉬운 부분만 짧게 정리합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/nano_o/post/a275c07a-1317-442c-ae29-3a2033da41cf/image.png" alt=""></p>
<p>이번 주제는 <strong>오버로딩</strong>과 <strong>오버라이딩</strong>입니다.
이름이 비슷해서 헷갈리지만 전혀 다른 개념이고, 면접 단골 질문이기도 합니다.
코테에서는 직접 만든 클래스를 <strong>정렬하거나 <code>HashMap</code> 키로 쓸 때</strong> 오버라이딩이 꼭 필요합니다.</p>
<hr>
<h2 id="1-한눈에-비교">1. 한눈에 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>오버로딩 (Overloading)</th>
<th>오버라이딩 (Overriding)</th>
</tr>
</thead>
<tbody><tr>
<td>한 줄 정의</td>
<td><strong>같은 이름, 다른 매개변수</strong>로 메서드를 여러 개 생성</td>
<td><strong>부모의 메서드를 자식이 다시 정의</strong></td>
</tr>
<tr>
<td>관계</td>
<td>같은 클래스 안</td>
<td>상속 관계 (부모 ↔ 자식)</td>
</tr>
<tr>
<td>메서드 이름</td>
<td>같다</td>
<td>같다</td>
</tr>
<tr>
<td>매개변수</td>
<td><strong>달라야 한다</strong> (개수 · 타입 · 순서)</td>
<td><strong>같아야 한다</strong></td>
</tr>
<tr>
<td>반환 타입</td>
<td>상관없다 (반환 타입만 다르면 안 됨)</td>
<td>같거나 하위 타입</td>
</tr>
<tr>
<td>결정 시점</td>
<td>컴파일할 때</td>
<td>실행할 때 (실제 객체 기준)</td>
</tr>
<tr>
<td>자바</td>
<td>⭕</td>
<td>⭕</td>
</tr>
<tr>
<td>파이썬</td>
<td>❌ <strong>지원하지 않는다</strong></td>
<td>⭕</td>
</tr>
</tbody></table>
<blockquote>
<p><strong>오버로딩은 &quot;새로 추가(load)&quot;</strong>, <strong>오버라이딩은 &quot;덮어쓰기(ride over)&quot;</strong> 로 외우자!</p>
</blockquote>
<hr>
<h2 id="2-오버로딩">2. 오버로딩</h2>
<h3 id="자바-매개변수가-다르면-같은-이름을-쓸-수-있다">자바: 매개변수가 다르면 같은 이름을 쓸 수 있다</h3>
<pre><code class="language-java">class Calc {
    int add(int a, int b)          { return a + b; }
    int add(int a, int b, int c)   { return a + b + c; }      // 개수가 다름
    double add(double a, double b) { return a + b; }          // 타입이 다름
}

Calc c = new Calc();
c.add(1, 2);        // 3      → 첫 번째
c.add(1, 2, 3);     // 6      → 두 번째
c.add(1.5, 2.5);    // 4.0    → 세 번째</code></pre>
<p>우리가 늘 쓰는 <code>Math.max(int, int)</code>, <code>Math.max(long, long)</code>, <code>Math.max(double, double)</code>과 <code>System.out.println()</code>도 오버로딩이다.</p>
<pre><code class="language-java">int add(int a, int b)  { ... }
long add(int a, int b) { ... }   // ❌ 반환 타입만 다른 것은 오버로딩이 아니다 (컴파일 에러)</code></pre>
<h3 id="파이썬-같은-이름으로-다시-정의하면-덮어쓴다">파이썬: 같은 이름으로 다시 정의하면 덮어쓴다</h3>
<pre><code class="language-python">def add(a, b):
    return a + b

def add(a, b, c):          # 앞의 add를 덮어쓴다
    return a + b + c

add(1, 2)                  # ❌ TypeError: add() missing 1 required positional argument: &#39;c&#39;</code></pre>
<p>파이썬은 대신 <strong>기본값 인자</strong>와 <strong>가변 인자</strong>로 같은 효과를 낸다.</p>
<pre><code class="language-python">def add(a, b, c=0):        # 기본값 인자
    return a + b + c

add(1, 2)                  # 3
add(1, 2, 3)               # 6

def add_all(*nums):        # 가변 인자 (몇 개든 받는다)
    return sum(nums)

add_all(1, 2, 3, 4)        # 10</code></pre>
<h3 id="생성자-오버로딩">생성자 오버로딩</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>인자 개수에 따라 다르게</td>
<td>생성자를 여러 개 정의</td>
<td><code>__init__</code>에 기본값 인자</td>
</tr>
<tr>
<td>다른 생성자 호출</td>
<td><code>this(...)</code></td>
<td>-</td>
</tr>
</tbody></table>
<pre><code class="language-java">class Point {
    int x, y;
    Point()             { this(0, 0); }        // 다른 생성자를 호출
    Point(int x, int y) { this.x = x; this.y = y; }
}</code></pre>
<pre><code class="language-python">class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y</code></pre>
<hr>
<h2 id="3-오버라이딩">3. 오버라이딩</h2>
<h3 id="상속-문법부터">상속 문법부터</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>상속</td>
<td><code>class Dog extends Animal</code></td>
<td><code>class Dog(Animal):</code></td>
</tr>
<tr>
<td>부모 생성자 호출</td>
<td><code>super(name);</code></td>
<td><code>super().__init__(name)</code></td>
</tr>
<tr>
<td>부모 메서드 호출</td>
<td><code>super.speak();</code></td>
<td><code>super().speak()</code></td>
</tr>
<tr>
<td>오버라이딩 표시</td>
<td><code>@Override</code></td>
<td>없음 (같은 이름으로 정의하면 끝)</td>
</tr>
<tr>
<td>다중 상속</td>
<td>❌ (인터페이스는 여러 개 가능)</td>
<td>⭕</td>
</tr>
</tbody></table>
<pre><code class="language-java">class Animal {
    String name;
    Animal(String name) { this.name = name; }

    void speak() { System.out.println(&quot;...&quot;); }
}

class Dog extends Animal {
    Dog(String name) { super(name); }

    @Override
    void speak() {                       // 부모의 speak를 다시 정의
        super.speak();                   // 부모 것도 부르고 싶을 때
        System.out.println(&quot;멍멍&quot;);
    }
}

Animal a = new Dog(&quot;바둑이&quot;);
a.speak();   // &quot;...&quot; 다음에 &quot;멍멍&quot;  ← 변수 타입(Animal)이 아니라 실제 객체(Dog) 기준</code></pre>
<pre><code class="language-python">class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(&quot;...&quot;)

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name)

    def speak(self):                     # 같은 이름으로 정의하면 오버라이딩
        super().speak()
        print(&quot;멍멍&quot;)

Dog(&quot;바둑이&quot;).speak()   # &quot;...&quot; 다음에 &quot;멍멍&quot;</code></pre>
<h3 id="자바-오버라이딩-규칙">자바 오버라이딩 규칙</h3>
<ul>
<li>메서드 이름과 매개변수가 <strong>부모와 똑같아야</strong> 한다.</li>
<li>접근 범위를 <strong>더 좁힐 수 없다</strong> (<code>public</code> → <code>private</code> 불가).</li>
<li><strong><code>static</code>, <code>final</code>, <code>private</code> 메서드는 오버라이딩할 수 없다.</strong></li>
</ul>
<hr>
<h2 id="4-코테에서-오버라이딩하는-메서드">4. 코테에서 오버라이딩하는 메서드</h2>
<p>직접 만든 클래스를 <strong>출력, 비교, 해시, 정렬</strong>에 쓰려면 아래 메서드를 다시 정의해야 한다.</p>
<table>
<thead>
<tr>
<th>용도</th>
<th>자바</th>
<th>파이썬</th>
<th>안 하면 생기는 일</th>
</tr>
</thead>
<tbody><tr>
<td>출력</td>
<td><code>toString()</code></td>
<td><code>__repr__</code> / <code>__str__</code></td>
<td><code>Point@1b6d3586</code> 같은 주소가 출력된다</td>
</tr>
<tr>
<td>값이 같은지</td>
<td><code>equals(Object o)</code></td>
<td><code>__eq__(self, other)</code></td>
<td>값이 같아도 다른 객체로 본다</td>
</tr>
<tr>
<td>해시 (<code>HashMap</code> · <code>HashSet</code>)</td>
<td><code>hashCode()</code></td>
<td><code>__hash__(self)</code></td>
<td>값이 같은 키를 못 찾는다</td>
</tr>
<tr>
<td>정렬 · 우선순위 큐</td>
<td><code>compareTo()</code> (<code>Comparable</code>)</td>
<td><code>__lt__(self, other)</code></td>
<td>정렬할 때 에러가 난다</td>
</tr>
</tbody></table>
<pre><code class="language-java">class Point implements Comparable&lt;Point&gt; {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public String toString() {
        return &quot;(&quot; + x + &quot;, &quot; + y + &quot;)&quot;;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return x == p.x &amp;&amp; y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public int compareTo(Point o) {           // x 오름차순, 같으면 y 오름차순
        if (x != o.x) return Integer.compare(x, o.x);
        return Integer.compare(y, o.y);
    }
}

Set&lt;Point&gt; set = new HashSet&lt;&gt;();
set.add(new Point(1, 2));
set.contains(new Point(1, 2));   // true  (equals + hashCode 덕분)

PriorityQueue&lt;Point&gt; pq = new PriorityQueue&lt;&gt;();   // compareTo 기준으로 꺼낸다</code></pre>
<pre><code class="language-python">class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f&quot;({self.x}, {self.y})&quot;

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))

    def __lt__(self, other):                 # x 오름차순, 같으면 y 오름차순
        return (self.x, self.y) &lt; (other.x, other.y)

s = {Point(1, 2)}
Point(1, 2) in s                 # True

sorted([Point(2, 1), Point(1, 5)])   # [(1, 5), (2, 1)]</code></pre>
<h3 id="더-짧게-쓰는-방법-사실-이건-외우기-좀-힘들지도">더 짧게 쓰는 방법 (사실 이건 외우기 좀 힘들지도)</h3>
<table>
<thead>
<tr>
<th>방법</th>
<th>코드</th>
<th>자동으로 만들어 주는 것</th>
</tr>
</thead>
<tbody><tr>
<td>자바 <code>record</code> (16+)</td>
<td><code>record Point(int x, int y) {}</code></td>
<td><code>equals</code>, <code>hashCode</code>, <code>toString</code></td>
</tr>
<tr>
<td>파이썬 <code>dataclass</code></td>
<td><code>@dataclass(frozen=True, order=True)</code></td>
<td><code>__eq__</code>, <code>__hash__</code>, <code>__lt__</code>, <code>__repr__</code></td>
</tr>
<tr>
<td>파이썬 튜플</td>
<td><code>(x, y)</code></td>
<td>전부 (코테에서는 이게 제일 간단)</td>
</tr>
</tbody></table>
<pre><code class="language-python">from dataclasses import dataclass

@dataclass(frozen=True, order=True)
class Point:
    x: int
    y: int</code></pre>
<hr>
<h2 id="5-체크포인트">5. 체크포인트</h2>
<h3 id="equalspoint-p는-오버라이딩이-아니라-오버로딩이다"><code>equals(Point p)</code>는 오버라이딩이 아니라 오버로딩이다</h3>
<pre><code class="language-java">public boolean equals(Point p) {      // ❌ 매개변수 타입이 Object가 아니다
    return x == p.x &amp;&amp; y == p.y;
}</code></pre>
<p>부모(<code>Object</code>)의 <code>equals(Object o)</code>와 <strong>매개변수 타입이 달라서</strong> 오버라이딩이 아니라 <strong>오버로딩</strong>이 된다.
<code>HashSet</code>은 <code>equals(Object)</code>를 부르기 때문에 이 메서드는 쓰이지 않고, 값이 같아도 못 찾는다.</p>
<blockquote>
<p><strong><code>@Override</code>를 항상 붙이자!</strong> 위 코드에 <code>@Override</code>를 붙이면 컴파일 에러로 바로 알려 준다.</p>
</blockquote>
<h3 id="그-밖에">그 밖에</h3>
<ul>
<li><p><strong><code>equals</code>를 재정의하면 <code>hashCode</code>도 반드시 함께...</strong> <code>equals</code>만 하면 <code>HashSet</code> / <code>HashMap</code>에서 값이 같은 객체를 못 찾는다.</p>
</li>
<li><p><strong>파이썬에서 <code>__eq__</code>만 정의하면 해시가 불가능해진다.</strong> <code>TypeError: unhashable type</code>이 나므로 <code>__hash__</code>도 같이 정의한다.</p>
</li>
<li><p><strong>파이썬 리스트를 출력하면 <code>__str__</code>이 아니라 <code>__repr__</code>을 쓴다.</strong> 하나만 정의할 거면 <code>__repr__</code>을 정의하자.</p>
</li>
<li><p><strong>자바 <code>static</code> 메서드는 오버라이딩되지 않는다.</strong> 자식에 같은 이름으로 만들면 가려질(hiding) 뿐, 변수 타입 기준으로 호출된다.</p>
</li>
<li><p><strong>파이썬 기본값 인자에 리스트를 쓰지 말자.</strong> 기본값은 함수 정의 시 <strong>한 번만</strong> 만들어져서 호출끼리 공유된다.</p>
<pre><code class="language-python">def add(x, lst=[]):          # ❌
    lst.append(x)
    return lst

add(1)   # [1]
add(2)   # [1, 2]  ← 이전 호출의 리스트가 남아 있다

def add(x, lst=None):        # ⭕
    if lst is None:
        lst = []
    lst.append(x)
    return lst</code></pre>
</li>
</ul>
<hr>
<h2 id="💡-이것만-기억하자">💡 이것만 기억하자</h2>
<ol>
<li><strong>오버로딩: 같은 이름, 다른 매개변수</strong> </li>
</ol>
<p><strong>오버라이딩: 부모 메서드를 자식이 재정의</strong>
2. <strong>파이썬은 오버로딩이 없다.</strong> 같은 이름으로 다시 정의하면 덮어쓴다. 기본값 인자와 <code>*args</code>로 대신한다.
3. <strong>부모 호출: 
자바: <code>super.메서드()</code>
파이썬: <code>super().메서드()</code></strong>
4. <strong>직접 만든 클래스를 <code>HashSet</code> / <code>HashMap</code>에 넣으려면 <code>equals</code> + <code>hashCode</code></strong> (파이썬은 <code>__eq__</code> + <code>__hash__</code>)
5. <strong>정렬/우선순위 큐에 넣으려면 <code>compareTo</code></strong> (파이썬은 <code>__lt__</code>)
6. <strong>자바는 <code>@Override</code>를 항상 붙이자!</strong> <code>equals(Point p)</code> 같은 실수를 컴파일러가 잡아 준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java vs Python] 클래스/static 한 장 정리]]></title>
            <link>https://velog.io/@nano_o/Java-vs-Python-%ED%81%B4%EB%9E%98%EC%8A%A4static-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@nano_o/Java-vs-Python-%ED%81%B4%EB%9E%98%EC%8A%A4static-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 04 Oct 2026 16:04:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>알고리즘 Cheat Sheet</strong> 시리즈는 코딩 테스트를 풀다가 &quot;이거 자바에선 뭐였지?&quot;, &quot;파이썬은 어떻게 했더라?&quot; 싶을 때 바로 펼쳐 보려고 만든 개인 참고용 정리입니다.
Java와 Python을 나란히 놓고, 문법 차이 때문에 실수하기 쉬운 부분만 짧게 정리합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/nano_o/post/d1468e15-4751-4b52-914a-8ccda4744e3b/image.png" alt=""></p>
<p>이번 주제는 <strong>클래스와 static</strong>입니다.
자바로 코테를 풀다 보면 <code>static</code>을 일단 붙이고 보자는 식으로 쓰게 되는데, <strong>왜 붙여야 하는지</strong>와 <strong>파이썬에서는 무엇에 해당하는지</strong>를 정리해 봅시다.</p>
<hr>
<h2 id="1-클래스-기본">1. 클래스 기본</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>생성자</td>
<td>클래스 이름과 같은 메서드</td>
<td><code>__init__(self, ...)</code></td>
</tr>
<tr>
<td>자기 자신 참조</td>
<td><code>this</code> (생략 가능)</td>
<td><code>self</code> (<strong>생략 불가</strong>)</td>
</tr>
<tr>
<td>객체 생성</td>
<td><code>new Point(1, 2)</code></td>
<td><code>Point(1, 2)</code></td>
</tr>
<tr>
<td>필드 선언</td>
<td>클래스 본문에 타입과 함께</td>
<td><code>__init__</code> 안에서 <code>self.x = ...</code></td>
</tr>
<tr>
<td>메서드 첫 번째 매개변수</td>
<td>없음</td>
<td><strong><code>self</code>를 직접 적는다</strong></td>
</tr>
<tr>
<td>접근 제어</td>
<td><code>private</code>, <code>public</code> 등</td>
<td>없음 (<code>_이름</code>은 관례일 뿐)</td>
</tr>
</tbody></table>
<pre><code class="language-java">class Point {
    int x, y;                    // 필드 선언

    Point(int x, int y) {        // 생성자
        this.x = x;
        this.y = y;
    }

    int sum() {                  // 인스턴스 메서드
        return x + y;            // this.x에서 this 생략 가능
    }
}

Point p = new Point(1, 2);
p.sum();   // 3</code></pre>
<pre><code class="language-python">class Point:
    def __init__(self, x, y):    # 생성자
        self.x = x               # 여기서 필드가 만들어진다
        self.y = y

    def sum(self):               # 인스턴스 메서드
        return self.x + self.y   # self 생략 불가

p = Point(1, 2)
p.sum()   # 3</code></pre>
<blockquote>
<p>코테에서 좌표나 노드처럼 <strong>값 몇 개를 묶기만 할 때</strong>는 자바는 <code>int[]</code>나 클래스, 파이썬은 <strong>튜플</strong> <code>(x, y)</code>이 가장 간단하다.</p>
</blockquote>
<hr>
<h2 id="2-static-객체가-아니라-클래스에-속한-것">2. static: 객체가 아니라 클래스에 속한 것</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>인스턴스 멤버</th>
<th>static 멤버</th>
</tr>
</thead>
<tbody><tr>
<td>소속</td>
<td>객체마다 따로</td>
<td><strong>클래스에 하나</strong> (모든 객체가 공유)</td>
</tr>
<tr>
<td>사용 조건</td>
<td>객체를 만들어야 쓸 수 있다</td>
<td><strong>객체 없이</strong> 쓸 수 있다</td>
</tr>
<tr>
<td>호출</td>
<td><code>p.sum()</code></td>
<td><code>Math.max(a, b)</code></td>
</tr>
</tbody></table>
<h3 id="자바와-파이썬-대응표">자바와 파이썬 대응표</h3>
<table>
<thead>
<tr>
<th>개념</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>공유 변수</td>
<td><code>static int count;</code></td>
<td>클래스 본문에 <code>count = 0</code> (클래스 변수)</td>
</tr>
<tr>
<td>객체 없이 부르는 메서드</td>
<td><code>static int add(int a, int b)</code></td>
<td><code>@staticmethod</code></td>
</tr>
<tr>
<td>클래스 자체를 받는 메서드</td>
<td>-</td>
<td><code>@classmethod</code> (<code>cls</code>)</td>
</tr>
<tr>
<td>상수</td>
<td><code>static final int MOD = 1_000_000_007;</code></td>
<td><code>MOD = 10**9 + 7</code> (대문자는 관례)</td>
</tr>
<tr>
<td>접근</td>
<td><code>클래스이름.count</code></td>
<td><code>클래스이름.count</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">class Counter {
    static int count = 0;        // 모든 객체가 공유
    int id;                      // 객체마다 따로

    Counter() {
        count++;
        id = count;
    }

    static int total() {         // 객체 없이 호출 가능
        return count;
    }
}

new Counter(); new Counter();
Counter.total();   // 2</code></pre>
<pre><code class="language-python">class Counter:
    count = 0                    # 클래스 변수 (모든 객체가 공유)

    def __init__(self):
        Counter.count += 1       # 클래스 이름으로 접근
        self.id = Counter.count  # 인스턴스 변수 (객체마다 따로)

    @staticmethod
    def total():                 # self 없음, 객체 없이 호출 가능
        return Counter.count

Counter(); Counter()
Counter.total()   # 2</code></pre>
<hr>
<h2 id="3-코테에서-static이-필요한-이유">3. 코테에서 static이 필요한 이유</h2>
<h3 id="main이-static이기-때문"><code>main</code>이 static이기 때문!</h3>
<p>자바 프로그램은 <strong>객체를 만들기 전에</strong> <code>main</code>부터 실행된다. 그래서 <code>main</code>은 <code>static</code>이고, static 메서드 안에서는 <strong>static 멤버만 바로 쓸 수 있다.</strong></p>
<pre><code class="language-java">public class Main {
    int n;                                  // 인스턴스 변수

    void dfs(int x) { }                     // 인스턴스 메서드

    public static void main(String[] args) {
        n = 5;        // ❌ non-static variable n cannot be referenced from a static context
        dfs(0);       // ❌ non-static method dfs(int) cannot be referenced from a static context
    }
}</code></pre>
<p>해결 방법은 두 가지다.</p>
<pre><code class="language-java">// 방법 1: 전부 static으로 (백준 스타일, 가장 흔함)
static int n;
static void dfs(int x) { }

// 방법 2: 객체를 만들어서 호출
Main m = new Main();
m.dfs(0);</code></pre>
<table>
<thead>
<tr>
<th>플랫폼</th>
<th>시작점</th>
<th>static 필요 여부</th>
</tr>
</thead>
<tbody><tr>
<td>백준</td>
<td><code>public static void main</code></td>
<td>보조 메서드와 공유 변수에 <strong>static 필요</strong></td>
</tr>
<tr>
<td>프로그래머스</td>
<td><code>class Solution</code>의 <code>solution()</code> (인스턴스 메서드)</td>
<td><strong>필요 없음</strong></td>
</tr>
</tbody></table>
<h3 id="dfs-·-bfs용-공유-변수">DFS · BFS용 공유 변수</h3>
<p>재귀 함수 여러 번의 호출이 <strong>같은 배열과 답을 공유</strong>해야 할 때, 자바는 static 필드, 파이썬은 전역 변수를 쓴다.</p>
<pre><code class="language-java">public class Main {
    static int n;
    static boolean[] visited;
    static List&lt;List&lt;Integer&gt;&gt; graph = new ArrayList&lt;&gt;();
    static int answer = 0;

    static void dfs(int cur) {
        visited[cur] = true;
        answer++;
        for (int next : graph.get(cur)) {
            if (!visited[next]) dfs(next);
        }
    }
}</code></pre>
<pre><code class="language-python">import sys
sys.setrecursionlimit(10**6)      # 파이썬 재귀 깊이 제한(기본 1000) 늘리기, 분할정복 시 필수다...

n = 5
visited = [False] * (n + 1)
graph = [[] for _ in range(n + 1)]
answer = 0

def dfs(cur):
    global answer                 # 전역 변수에 &quot;대입&quot;하려면 global 선언
    visited[cur] = True           # 리스트 원소 수정은 global 없이 가능
    answer += 1
    for nxt in graph[cur]:
        if not visited[nxt]:
            dfs(nxt)</code></pre>
<h3 id="파이썬-global-vs-nonlocal">파이썬 <code>global</code> vs <code>nonlocal</code></h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>필요한 선언</th>
</tr>
</thead>
<tbody><tr>
<td>전역 변수 <strong>읽기</strong></td>
<td>없음</td>
</tr>
<tr>
<td>전역 리스트 · 딕셔너리의 <strong>내용 수정</strong> (<code>visited[i] = True</code>, <code>d[k] = v</code>)</td>
<td>없음</td>
</tr>
<tr>
<td>전역 변수에 <strong>새 값 대입</strong> (<code>answer += 1</code>, <code>answer = 0</code>)</td>
<td><code>global answer</code></td>
</tr>
<tr>
<td>함수 안의 함수에서 바깥 함수 변수에 대입</td>
<td><code>nonlocal answer</code></td>
</tr>
</tbody></table>
<pre><code class="language-python">def solution(n):
    answer = 0

    def dfs(x):
        nonlocal answer           # solution의 answer를 바꾸겠다는 뜻
        answer += 1

    dfs(0)
    return answer</code></pre>
<hr>
<h2 id="4-체크포인트">4. 체크포인트</h2>
<h3 id="자바">자바</h3>
<ul>
<li><strong>static 메서드 안에서는 <code>this</code>와 인스턴스 멤버를 쓸 수 없다.</strong> 객체가 없을 수도 있기 때문이다.</li>
<li><strong>프로그래머스에서 static 필드는 초기화를 직접 하자.</strong> 테스트 케이스가 같은 클래스로 여러 번 실행되면 <strong>이전 값이 남아 있을 수 있다.</strong> <code>solution()</code> 시작 부분에서 <code>answer = 0;</code>처럼 다시 초기화한다.</li>
<li><strong><code>static final</code> 상수는 대문자로!</strong> <code>static final int MOD = 1_000_000_007;</code></li>
</ul>
<h3 id="파이썬">파이썬</h3>
<ul>
<li><p><strong><code>self</code>를 빼먹으면 에러가 난다...</strong></p>
<pre><code class="language-python">class A:
    def hello():          # self 누락
        print(&quot;hi&quot;)

A().hello()               # TypeError: A.hello() takes 0 positional arguments but 1 was given</code></pre>
</li>
<li><p><strong><code>self.count += 1</code>은 클래스 변수를 바꾸지 않는다.</strong> 그 객체에 <strong>새 인스턴스 변수</strong>를 만들 뿐이다. 클래스 변수는 <code>클래스이름.count += 1</code>로 바꾼다.</p>
<pre><code class="language-python">class Counter:
    count = 0
    def inc(self):
        self.count += 1   # 인스턴스 변수 count가 새로 생긴다

a = Counter(); a.inc()
print(a.count, Counter.count)   # 1 0</code></pre>
</li>
<li><p><strong>클래스 변수에 리스트를 두면 모든 객체가 공유한다.</strong> 객체마다 따로 가져야 하면 <code>__init__</code> 안에서 만든다.</p>
<pre><code class="language-python">class Bag:
    items = []                    # ❌ 모든 Bag이 같은 리스트를 쓴다
    def __init__(self):
        self.items = []           # ⭕ 객체마다 새 리스트</code></pre>
</li>
<li><p><strong><code>global</code> 없이 대입하면 지역 변수가 새로 생긴다.</strong> <code>answer += 1</code>은 <code>UnboundLocalError</code>가 난다.</p>
</li>
</ul>
<hr>
<h2 id="💡-이것만-기억하자">💡 이것만 기억하자</h2>
<ol>
<li><strong>static은 &quot;객체 없이 쓸 수 있고, 모두가 공유하는 것&quot;</strong> 파이썬에서는 클래스 변수와 <code>@staticmethod</code></li>
<li><strong><code>main</code>이 static이라서 보조 메서드와 공유 변수도 static</strong>, 프로그래머스 <code>solution()</code>은 인스턴스 메서드라 필요 없다.</li>
<li><strong>파이썬 메서드는 첫 매개변수로 <code>self</code>를 적고, 필드는 항상 <code>self.</code>을 붙인다.</strong></li>
<li><strong>파이썬 전역 변수: 읽기와 내용 수정은 그냥, 대입은 <code>global</code></strong> (함수 안의 함수는 <code>nonlocal</code>)</li>
<li><strong>파이썬 클래스 변수는 <code>클래스이름.변수</code>로 바꾼다.</strong> <code>self.변수 += 1</code>은 다른 변수를 만든다.</li>
<li><strong>파이썬 DFS는 <code>sys.setrecursionlimit</code>을 잊지 말자.</strong></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java vs Python] 람다 한 장 정리]]></title>
            <link>https://velog.io/@nano_o/Java-vs-Python-%EB%9E%8C%EB%8B%A4-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@nano_o/Java-vs-Python-%EB%9E%8C%EB%8B%A4-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 03 Oct 2026 07:58:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>알고리즘 Cheat Sheet</strong> 시리즈는 코딩 테스트를 풀다가 &quot;이거 자바에선 뭐였지?&quot;, &quot;파이썬은 어떻게 했더라?&quot; 싶을 때 바로 펼쳐 보려고 만든 개인 참고용 정리입니다.
Java와 Python을 나란히 놓고, 문법 차이 때문에 실수하기 쉬운 부분만 짧게 정리합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/nano_o/post/835fa08f-f3f8-4cae-aed0-97d9a4bedacb/image.png" alt=""></p>
<p>이번 주제는 <strong>람다</strong>입니다!
람다는 쉽게 말하면 <strong>이름 없는 짧은 함수</strong>입니다. 코테에서는 주로 <strong>정렬 기준</strong>, <strong>변환(map)/거르기(filter)</strong>, <strong>입력 파싱</strong>에 씁니다.</p>
<hr>
<h2 id="1-기본-문법">1. 기본 문법</h2>
<table>
<thead>
<tr>
<th>형태</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>인자 1개</td>
<td><code>x -&gt; x * 2</code></td>
<td><code>lambda x: x * 2</code></td>
</tr>
<tr>
<td>인자 2개</td>
<td><code>(a, b) -&gt; a + b</code></td>
<td><code>lambda a, b: a + b</code></td>
</tr>
<tr>
<td>인자 없음</td>
<td><code>() -&gt; 42</code></td>
<td><code>lambda: 42</code></td>
</tr>
<tr>
<td>여러 줄</td>
<td><code>(a, b) -&gt; { int s = a + b; return s * 2; }</code></td>
<td>불가능 (식 하나만)</td>
</tr>
<tr>
<td>조건 분기</td>
<td><code>x -&gt; x % 2 == 0 ? &quot;짝&quot; : &quot;홀&quot;</code></td>
<td><code>lambda x: &quot;짝&quot; if x % 2 == 0 else &quot;홀&quot;</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">// 자바 람다는 반드시 &quot;함수형 인터페이스&quot; 타입에 담아야 한다
Function&lt;Integer, Integer&gt; doubleIt = x -&gt; x * 2;
BinaryOperator&lt;Integer&gt; add = (a, b) -&gt; a + b;

doubleIt.apply(3);   // 6
add.apply(2, 3);     // 5</code></pre>
<pre><code class="language-python">double_it = lambda x: x * 2
add = lambda a, b: a + b

double_it(3)   # 6
add(2, 3)      # 5</code></pre>
<blockquote>
<p>파이썬 람다는 <strong>식(expression) 하나만</strong> 쓸 수 있다. <code>if</code>문, <code>for</code>문, 대입이 필요하면 <code>def</code>로 함수를 만든다.
자바 람다는 <code>{ }</code> 블록을 쓰면 여러 줄도 가능하지만, 이때는 <code>return</code>을 직접 써야 한다.</p>
</blockquote>
<h3 id="자바-함수형-인터페이스">자바 함수형 인터페이스</h3>
<p>자바 람다는 혼자 존재할 수 없고, <strong>어떤 인터페이스의 구현</strong>으로 쓰인다. 자주 보는 것만 알아두면 된다.</p>
<table>
<thead>
<tr>
<th>인터페이스</th>
<th>모양</th>
<th>호출</th>
<th>주로 쓰는 곳</th>
</tr>
</thead>
<tbody><tr>
<td><code>Comparator&lt;T&gt;</code></td>
<td><code>(a, b) -&gt; int</code></td>
<td><code>compare(a, b)</code></td>
<td>정렬</td>
</tr>
<tr>
<td><code>Function&lt;T, R&gt;</code></td>
<td><code>x -&gt; 결과</code></td>
<td><code>apply(x)</code></td>
<td>변환 (<code>map</code>)</td>
</tr>
<tr>
<td><code>Predicate&lt;T&gt;</code></td>
<td><code>x -&gt; boolean</code></td>
<td><code>test(x)</code></td>
<td>조건 (<code>filter</code>, <code>removeIf</code>)</td>
</tr>
<tr>
<td><code>Consumer&lt;T&gt;</code></td>
<td><code>x -&gt; { }</code> (반환 없음)</td>
<td><code>accept(x)</code></td>
<td><code>forEach</code></td>
</tr>
<tr>
<td><code>Supplier&lt;T&gt;</code></td>
<td><code>() -&gt; 값</code></td>
<td><code>get()</code></td>
<td><code>computeIfAbsent</code>의 새 객체 생성 등</td>
</tr>
<tr>
<td><code>BinaryOperator&lt;T&gt;</code></td>
<td><code>(a, b) -&gt; 같은 타입</code></td>
<td><code>apply(a, b)</code></td>
<td><code>reduce</code>, <code>merge</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="2-메서드-참조-vs-함수-그대로-넘기기">2. 메서드 참조 vs 함수 그대로 넘기기</h2>
<p>람다가 <strong>이미 있는 함수를 호출만 할 때</strong>는 더 짧게 쓸 수 있다.</p>
<table>
<thead>
<tr>
<th>하고 싶은 일</th>
<th>자바 람다</th>
<th>자바 메서드 참조</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>문자열 → 정수</td>
<td><code>s -&gt; Integer.parseInt(s)</code></td>
<td><code>Integer::parseInt</code></td>
<td><code>int</code></td>
</tr>
<tr>
<td>길이</td>
<td><code>s -&gt; s.length()</code></td>
<td><code>String::length</code></td>
<td><code>len</code></td>
</tr>
<tr>
<td>두 수 더하기</td>
<td><code>(a, b) -&gt; a + b</code></td>
<td><code>Integer::sum</code></td>
<td><code>operator.add</code></td>
</tr>
<tr>
<td>출력</td>
<td><code>x -&gt; System.out.println(x)</code></td>
<td><code>System.out::println</code></td>
<td><code>print</code></td>
</tr>
<tr>
<td>새 리스트 생성</td>
<td><code>() -&gt; new ArrayList&lt;&gt;()</code></td>
<td><code>ArrayList::new</code></td>
<td><code>list</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">map.merge(key, 1, Integer::sum);
words.sort(Comparator.comparing(String::length));</code></pre>
<pre><code class="language-python">words.sort(key=len)                 # 함수 이름을 그대로 넘긴다
nums = list(map(int, [&quot;1&quot;, &quot;2&quot;]))   # int 함수를 그대로 넘긴다</code></pre>
<blockquote>
<p>파이썬은 함수 자체가 값이라서 <code>key=len</code>처럼 <strong>괄호 없이 이름만</strong> 넘기면 된다. <code>key=len()</code>이라고 쓰면 에러 발생!</p>
</blockquote>
<hr>
<h2 id="3-정렬-기준-복습">3. 정렬 기준 (복습)</h2>
<table>
<thead>
<tr>
<th>기준</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>오름차순</td>
<td><code>(a, b) -&gt; Integer.compare(a, b)</code></td>
<td>기본값</td>
</tr>
<tr>
<td>내림차순</td>
<td><code>(a, b) -&gt; Integer.compare(b, a)</code></td>
<td><code>reverse=True</code></td>
</tr>
<tr>
<td>길이순</td>
<td><code>Comparator.comparing(String::length)</code></td>
<td><code>key=len</code></td>
</tr>
<tr>
<td>길이 내림차순</td>
<td><code>Comparator.comparing(String::length).reversed()</code></td>
<td><code>key=len, reverse=True</code></td>
</tr>
<tr>
<td>1순위 → 2순위</td>
<td><code>.thenComparing(...)</code></td>
<td><code>key=lambda x: (x[0], x[1])</code></td>
</tr>
</tbody></table>
<ul>
<li><strong>자바는 &quot;두 값을 받아 비교&quot;</strong>, <strong>파이썬은 &quot;값 하나를 받아 비교할 기준을 반환&quot;</strong> 하는 방식이다.</li>
<li>자바 <code>Comparator.comparing(...)</code>이 파이썬 <code>key=</code>와 같은 방식이라, 익숙해지면 둘을 똑같이 생각할 수 있다.</li>
</ul>
<pre><code class="language-java">// 파이썬 key=lambda x: (x[0], -x[1]) 과 같은 정렬
Arrays.sort(arr, Comparator.comparingInt((int[] x) -&gt; x[0])
                           .thenComparing((int[] x) -&gt; x[1], Comparator.reverseOrder()));</code></pre>
<blockquote>
<p>자세한 정렬은 <a href="https://velog.io/@nano_o/Java-vs-Python-%ED%98%95%EB%B3%80%ED%99%98%EB%B9%84%EA%B5%90%EC%A0%95%EB%A0%AC-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC">#2 형변환/비교/정렬</a> 편 참고</p>
</blockquote>
<hr>
<h2 id="4-map-·-filter-·-reduce">4. map · filter · reduce</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바 (스트림)</th>
<th>파이썬 (함수)</th>
<th>파이썬 (컴프리헨션)</th>
</tr>
</thead>
<tbody><tr>
<td>모두 변환</td>
<td><code>.map(x -&gt; x * 2)</code></td>
<td><code>map(lambda x: x * 2, arr)</code></td>
<td><code>[x * 2 for x in arr]</code></td>
</tr>
<tr>
<td>조건 거르기</td>
<td><code>.filter(x -&gt; x &gt; 0)</code></td>
<td><code>filter(lambda x: x &gt; 0, arr)</code></td>
<td><code>[x for x in arr if x &gt; 0]</code></td>
</tr>
<tr>
<td>하나로 합치기</td>
<td><code>.reduce(0, Integer::sum)</code></td>
<td><code>reduce(lambda a, b: a + b, arr, 0)</code></td>
<td><code>sum(arr)</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">int[] arr = {1, -2, 3, -4, 5};

int[] result = Arrays.stream(arr)
                     .filter(x -&gt; x &gt; 0)     // 1, 3, 5
                     .map(x -&gt; x * 2)        // 2, 6, 10
                     .toArray();

List&lt;String&gt; words = List.of(&quot;apple&quot;, &quot;kiwi&quot;, &quot;banana&quot;);
List&lt;Integer&gt; lengths = words.stream()
                             .map(String::length)
                             .collect(Collectors.toList());   // [5, 4, 6]</code></pre>
<pre><code class="language-python">arr = [1, -2, 3, -4, 5]

result = [x * 2 for x in arr if x &gt; 0]              # [2, 6, 10]  ← 추천
result = list(map(lambda x: x * 2,
                  filter(lambda x: x &gt; 0, arr)))    # 같은 결과

from functools import reduce
total = reduce(lambda a, b: a + b, arr, 0)          # 3</code></pre>
<blockquote>
<p>파이썬에서는 <code>map</code> + <code>lambda</code> 조합보다 <strong>리스트 컴프리헨션</strong>이 더 읽기 쉽고 흔하다.
단, <code>map(int, ...)</code>처럼 <strong>이미 있는 함수를 넘길 때는 <code>map</code>이 더 짧다.</strong></p>
</blockquote>
<hr>
<h2 id="5-코테에서-자주-쓰는-한-줄">5. 코테에서 자주 쓰는 한 줄</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>입력 한 줄 → 정수 배열</td>
<td><code>Arrays.stream(br.readLine().split(&quot; &quot;)).mapToInt(Integer::parseInt).toArray()</code></td>
<td><code>list(map(int, input().split()))</code></td>
</tr>
<tr>
<td>가장 긴 문자열</td>
<td><code>Collections.max(words, Comparator.comparing(String::length))</code></td>
<td><code>max(words, key=len)</code></td>
</tr>
<tr>
<td>값이 가장 큰 키</td>
<td><code>Collections.max(map.entrySet(), Map.Entry.comparingByValue()).getKey()</code></td>
<td><code>max(d, key=d.get)</code></td>
</tr>
<tr>
<td>조건에 맞는 개수</td>
<td><code>list.stream().filter(x -&gt; x &gt; 0).count()</code></td>
<td><code>sum(1 for x in lst if x &gt; 0)</code></td>
</tr>
<tr>
<td>조건에 맞는 원소 삭제</td>
<td><code>list.removeIf(x -&gt; x &lt; 0)</code></td>
<td><code>lst = [x for x in lst if x &gt;= 0]</code></td>
</tr>
<tr>
<td>모두 변환 (원본 수정)</td>
<td><code>list.replaceAll(x -&gt; x * 2)</code></td>
<td><code>lst = [x * 2 for x in lst]</code></td>
</tr>
<tr>
<td>맵 순회</td>
<td><code>map.forEach((k, v) -&gt; ...)</code></td>
<td><code>for k, v in d.items():</code></td>
</tr>
<tr>
<td>하나라도 / 모두</td>
<td><code>.anyMatch(x -&gt; x &gt; 0)</code> / <code>.allMatch(...)</code></td>
<td><code>any(x &gt; 0 for x in lst)</code> / <code>all(...)</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="6-체크포인트">6. 체크포인트</h2>
<h3 id="자바-람다-안에서-바깥-지역변수를-바꿀-수-없다">[자바] 람다 안에서 바깥 지역변수를 바꿀 수 없다</h3>
<p>람다가 쓰는 바깥 지역변수는 <strong>사실상 final</strong>(한 번도 바뀌지 않는 값)이어야 한다.</p>
<pre><code class="language-java">int count = 0;
list.forEach(x -&gt; count++);        // ❌ 컴파일 에러

int[] count = {0};
list.forEach(x -&gt; count[0]++);     // ⭕ 배열 원소는 바꿀 수 있다 (꼼수)

int cnt = 0;
for (int x : list) cnt++;          // ⭕ 그냥 for문이 제일 깔끔</code></pre>
<h3 id="파이썬-반복문-안에서-만든-람다는-마지막-값을-본다">[파이썬] 반복문 안에서 만든 람다는 마지막 값을 본다</h3>
<pre><code class="language-python">funcs = [lambda: i for i in range(3)]
[f() for f in funcs]               # [2, 2, 2]  ← 0, 1, 2가 아님!

funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]               # [0, 1, 2]  ← 기본값으로 현재 i를 고정</code></pre>
<p>람다는 <code>i</code>의 <strong>값이 아니라 변수 자체</strong>를 기억하기 때문에, 호출 시점의 <code>i</code>(마지막 값 2)를 읽는다.</p>
<h3 id="그-밖에">그 밖에</h3>
<ul>
<li><strong>파이썬 <code>map</code> / <code>filter</code>는 결과를 바로 만들지 않는다.</strong> 이터레이터라서 <code>list()</code>로 감싸야 리스트가 되고, <strong>한 번 순회하면 다시 쓸 수 없다.</strong></li>
<li><strong>자바 스트림도 한 번만 쓸 수 있다.</strong> 다시 쓰면 <code>IllegalStateException</code>이 난다.</li>
<li><strong>자바 스트림은 <code>for</code>문보다 느리다.</strong> 반복 횟수가 많은 핵심 로직에서는 <code>for</code>문을 쓰고, 스트림은 입력 파싱처럼 한 번만 실행되는 곳에 쓰자.</li>
<li><strong>자바 <code>int[]</code> 스트림은 <code>IntStream</code>이다.</strong> <code>mapToInt</code>, <code>boxed()</code>로 <code>Stream&lt;Integer&gt;</code>와 오가야 할 때가 있다 <a href="https://velog.io/@nano_o/Java-vs-Python-%EB%B0%B0%EC%97%B4%EB%A6%AC%EC%8A%A4%ED%8A%B8%EB%AC%B8%EC%9E%90%EC%97%B4-%EA%B8%B0%EC%B4%88-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC">#4 참고</a></li>
<li><strong>자바 람다 + <code>.reversed()</code>는 타입 추론이 안 될 때가 있다.</strong> <code>Comparator.comparing((String s) -&gt; s.length()).reversed()</code>처럼 매개변수 타입을 적어 준다.</li>
</ul>
<hr>
<h2 id="💡-이것만-기억하자">💡 이것만 기억하자</h2>
<ol>
<li><strong>문법: 
자바: <code>(a, b) -&gt; 식</code>
파이썬: <code>lambda a, b: 식</code></strong>, 파이썬은 식 하나만</li>
<li><strong>이미 있는 함수는 그대로 넘기자.</strong> </li>
</ol>
<p><strong>자바:</strong> <code>Integer::parseInt</code>
<strong>파이썬:</strong> <code>int</code>, <code>len</code>
3. <strong>정렬: 자바는 두 값을 비교</strong>
<strong>파이썬:</strong> 기준 값을 반환(<strong><code>key=</code></strong>)
<strong>자바:</strong> <strong><code>Comparator.comparing</code></strong>이 파이썬 <strong><code>key=</code></strong>와 같은 방식
4. <strong>변환/거르기:
자바: 스트림 <code>map</code> / <code>filter</code>
파이썬: 리스트 컴프리헨션</strong>
5. <strong>자바 람다 안에서 바깥 변수 수정 불가!</strong> 카운트는 <code>for</code>문으로
6. <strong>파이썬 반복문 안 람다는 <code>lambda i=i:</code>로 값을 고정</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java vs Python] HashMap/dict /Set 한 장 정리]]></title>
            <link>https://velog.io/@nano_o/Java-vs-Python-HashMapdict-Set-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC-odnjsgz4</link>
            <guid>https://velog.io/@nano_o/Java-vs-Python-HashMapdict-Set-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC-odnjsgz4</guid>
            <pubDate>Sat, 03 Oct 2026 07:47:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>알고리즘 Cheat Sheet</strong> 시리즈는 코딩 테스트를 풀다가 &quot;이거 자바에선 뭐였지?&quot;, &quot;파이썬은 어떻게 했더라?&quot; 싶을 때 바로 펼쳐 보려고 만든 개인 참고용 정리입니다.
Java와 Python을 나란히 놓고, 문법 차이 때문에 실수하기 쉬운 부분만 짧게 정리합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/nano_o/post/ddb9a602-7b88-4f65-8d64-4fd658f95dfe/image.png" alt=""></p>
<p>이번 주제는 <strong>HashMap · dict · Set</strong>입니다.
개수 세기, 중복 제거, 빠른 존재 여부 확인까지 코테에서 가장 많이 쓰는 자료구조입니다. 웬만한 문제는 풀 수 있어요!
조회·추가·포함 확인이 <strong>평균 O(1)</strong> 이라서, 리스트로 <code>in</code> / <code>contains</code>를 반복하다 시간 초과가 나면 가장 먼저 떠올려야 합니다.</p>
<hr>
<h2 id="0-선언">0. 선언</h2>
<pre><code class="language-java">import java.util.*;

Map&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
Set&lt;Integer&gt; set = new HashSet&lt;&gt;();</code></pre>
<pre><code class="language-python">from collections import Counter, defaultdict

d = {}          # 빈 딕셔너리
s = set()       # 빈 셋  ← {}는 딕셔너리</code></pre>
<table>
<thead>
<tr>
<th>종류</th>
<th>자바</th>
<th>파이썬</th>
<th>순서</th>
</tr>
</thead>
<tbody><tr>
<td>기본 맵</td>
<td><code>HashMap</code></td>
<td><code>dict</code></td>
<td>자바: 보장 안 됨 / 파이썬: <strong>넣은 순서 유지</strong></td>
</tr>
<tr>
<td>넣은 순서 유지</td>
<td><code>LinkedHashMap</code></td>
<td><code>dict</code></td>
<td>넣은 순서</td>
</tr>
<tr>
<td>키 정렬 유지</td>
<td><code>TreeMap</code></td>
<td>- (<code>sorted(d)</code>로 정렬)</td>
<td>키 오름차순</td>
</tr>
<tr>
<td>기본 셋</td>
<td><code>HashSet</code></td>
<td><code>set</code></td>
<td>보장 안 됨</td>
</tr>
<tr>
<td>정렬된 셋</td>
<td><code>TreeSet</code></td>
<td>- (<code>sorted(s)</code>로 정렬)</td>
<td>오름차순</td>
</tr>
</tbody></table>
<hr>
<h2 id="1-hashmap-·-dict-기본-사용">1. HashMap · dict 기본 사용</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>넣기 · 수정</td>
<td><code>map.put(k, v)</code></td>
<td><code>d[k] = v</code></td>
</tr>
<tr>
<td>가져오기</td>
<td><code>map.get(k)</code></td>
<td><code>d[k]</code></td>
</tr>
<tr>
<td>없으면 기본값</td>
<td><code>map.getOrDefault(k, 0)</code></td>
<td><code>d.get(k, 0)</code></td>
</tr>
<tr>
<td>키가 있는지</td>
<td><code>map.containsKey(k)</code></td>
<td><code>k in d</code></td>
</tr>
<tr>
<td>값이 있는지</td>
<td><code>map.containsValue(v)</code></td>
<td><code>v in d.values()</code></td>
</tr>
<tr>
<td>삭제</td>
<td><code>map.remove(k)</code></td>
<td><code>del d[k]</code> / <code>d.pop(k)</code></td>
</tr>
<tr>
<td>없을 때만 넣기</td>
<td><code>map.putIfAbsent(k, v)</code></td>
<td><code>d.setdefault(k, v)</code></td>
</tr>
<tr>
<td>크기</td>
<td><code>map.size()</code></td>
<td><code>len(d)</code></td>
</tr>
<tr>
<td>비었는지</td>
<td><code>map.isEmpty()</code></td>
<td><code>not d</code></td>
</tr>
</tbody></table>
<h3 id="없는-키를-조회하면">없는 키를 조회하면</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>조회</td>
<td><code>map.get(k)</code> → <strong><code>null</code></strong></td>
<td><code>d[k]</code> → <strong><code>KeyError</code></strong></td>
</tr>
<tr>
<td>안전하게 조회</td>
<td><code>map.getOrDefault(k, 0)</code></td>
<td><code>d.get(k, 0)</code></td>
</tr>
<tr>
<td>삭제</td>
<td><code>map.remove(k)</code> → <code>null</code> (에러 없음)</td>
<td><code>del d[k]</code> → <strong><code>KeyError</code></strong><br><code>d.pop(k, None)</code> → 안전</td>
</tr>
</tbody></table>
<pre><code class="language-java">Map&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
int n = map.get(&quot;a&quot;);        // ❌ NullPointerException (null을 int로 언박싱)
int m = map.getOrDefault(&quot;a&quot;, 0);   // ⭕ 0</code></pre>
<pre><code class="language-python">d = {}
n = d[&quot;a&quot;]           # ❌ KeyError
m = d.get(&quot;a&quot;, 0)    # ⭕ 0</code></pre>
<blockquote>
<p>자바 <code>map.get()</code>은 에러 없이 <code>null</code>을 주지만, <strong><code>int</code> 변수에 담거나 <code>+ 1</code>을 하는 순간 <code>NullPointerException</code></strong> 이 난다.</p>
</blockquote>
<hr>
<h2 id="2-개수-세기-가장-많이-쓰는-패턴">2. 개수 세기 (가장 많이 쓰는 패턴)</h2>
<p><code>[&quot;a&quot;, &quot;b&quot;, &quot;a&quot;, &quot;c&quot;, &quot;a&quot;]</code> → <code>{a: 3, b: 1, c: 1}</code></p>
<pre><code class="language-java">Map&lt;String, Integer&gt; count = new HashMap&lt;&gt;();

for (String x : arr) {
    count.put(x, count.getOrDefault(x, 0) + 1);   // 방법 1
    // count.merge(x, 1, Integer::sum);           // 방법 2 (한 줄)
}</code></pre>
<pre><code class="language-python">count = {}
for x in arr:
    count[x] = count.get(x, 0) + 1       # 방법 1

from collections import Counter
count = Counter(arr)                      # 방법 2 (한 줄)
count.most_common(2)                      # [(&#39;a&#39;, 3), (&#39;b&#39;, 1)]  빈도 상위 2개</code></pre>
<table>
<thead>
<tr>
<th>방법</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>기본</td>
<td><code>put(x, getOrDefault(x, 0) + 1)</code></td>
<td><code>d[x] = d.get(x, 0) + 1</code></td>
</tr>
<tr>
<td>한 줄</td>
<td><code>merge(x, 1, Integer::sum)</code></td>
<td><code>Counter(arr)</code></td>
</tr>
<tr>
<td>빈도순 정렬</td>
<td>아래 &quot;값 기준 정렬&quot; 참고</td>
<td><code>Counter(arr).most_common()</code></td>
</tr>
</tbody></table>
<h3 id="그룹-묶기-키-하나에-여러-값">그룹 묶기 (키 하나에 여러 값)</h3>
<pre><code class="language-java">Map&lt;String, List&lt;String&gt;&gt; group = new HashMap&lt;&gt;();
group.computeIfAbsent(key, k -&gt; new ArrayList&lt;&gt;()).add(value);</code></pre>
<pre><code class="language-python">group = defaultdict(list)
group[key].append(value)     # 키가 없으면 빈 리스트를 자동으로 만든다</code></pre>
<blockquote>
<p><code>defaultdict(int)</code>는 없는 키를 <code>0</code>으로, <code>defaultdict(list)</code>는 <code>[]</code>로 자동 생성한다.</p>
</blockquote>
<hr>
<h2 id="3-순회">3. 순회</h2>
<table>
<thead>
<tr>
<th>순회 대상</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>키</td>
<td><code>for (String k : map.keySet())</code></td>
<td><code>for k in d:</code></td>
</tr>
<tr>
<td>값</td>
<td><code>for (int v : map.values())</code></td>
<td><code>for v in d.values():</code></td>
</tr>
<tr>
<td>키 + 값</td>
<td><code>for (Map.Entry&lt;String, Integer&gt; e : map.entrySet())</code></td>
<td><code>for k, v in d.items():</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">for (Map.Entry&lt;String, Integer&gt; e : map.entrySet()) {
    String key = e.getKey();
    int value = e.getValue();
}</code></pre>
<pre><code class="language-python">for key, value in d.items():
    print(key, value)</code></pre>
<blockquote>
<p>파이썬 <code>d.items()</code>는 <code>enumerate</code>처럼 <strong>두 값을 한 번에 풀어서</strong> 받는다. 자바는 <code>Entry</code>에서 <code>getKey()</code>, <code>getValue()</code>로 꺼낸다.</p>
</blockquote>
<h3 id="정렬해서-순회">정렬해서 순회</h3>
<table>
<thead>
<tr>
<th>기준</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>키 오름차순</td>
<td><code>new TreeMap&lt;&gt;(map)</code></td>
<td><code>sorted(d.items())</code></td>
</tr>
<tr>
<td>값 오름차순</td>
<td>아래 코드</td>
<td><code>sorted(d.items(), key=lambda x: x[1])</code></td>
</tr>
<tr>
<td>값 내림차순</td>
<td>아래 코드</td>
<td><code>sorted(d.items(), key=lambda x: -x[1])</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">List&lt;Map.Entry&lt;String, Integer&gt;&gt; entries = new ArrayList&lt;&gt;(map.entrySet());
entries.sort((a, b) -&gt; Integer.compare(b.getValue(), a.getValue()));   // 값 내림차순</code></pre>
<h3 id="순회-체크포인트">순회 체크포인트</h3>
<ul>
<li><strong>순회하면서 삭제하면 에러가 난다.</strong> 자바는 <code>ConcurrentModificationException</code>, 파이썬은 <code>RuntimeError</code>.
자바는 <code>map.entrySet().removeIf(e -&gt; e.getValue() == 0)</code>, 파이썬은 <code>for k in list(d):</code>처럼 키 목록을 복사해서 순회한다.</li>
</ul>
<hr>
<h2 id="4-set-중복-없는-집합">4. Set: 중복 없는 집합</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>추가</td>
<td><code>set.add(x)</code> → <code>boolean</code></td>
<td><code>s.add(x)</code> → <code>None</code></td>
</tr>
<tr>
<td>삭제</td>
<td><code>set.remove(x)</code></td>
<td><code>s.remove(x)</code> (없으면 <code>KeyError</code>)<br><code>s.discard(x)</code> (없어도 OK)</td>
</tr>
<tr>
<td>포함 여부</td>
<td><code>set.contains(x)</code></td>
<td><code>x in s</code></td>
</tr>
<tr>
<td>크기</td>
<td><code>set.size()</code></td>
<td><code>len(s)</code></td>
</tr>
<tr>
<td>리스트 → 셋</td>
<td><code>new HashSet&lt;&gt;(list)</code></td>
<td><code>set(lst)</code></td>
</tr>
<tr>
<td>셋 → 리스트</td>
<td><code>new ArrayList&lt;&gt;(set)</code></td>
<td><code>list(s)</code></td>
</tr>
</tbody></table>
<h3 id="중복-제거">중복 제거</h3>
<pre><code class="language-java">List&lt;Integer&gt; list = List.of(3, 1, 3, 2, 1);
Set&lt;Integer&gt; set = new HashSet&lt;&gt;(list);            // [1, 2, 3] (순서 보장 X)
Set&lt;Integer&gt; kept = new LinkedHashSet&lt;&gt;(list);     // [3, 1, 2] (넣은 순서 유지)</code></pre>
<pre><code class="language-python">lst = [3, 1, 3, 2, 1]
s = set(lst)                    # {1, 2, 3} (순서 보장 X)
kept = list(dict.fromkeys(lst)) # [3, 1, 2] (넣은 순서 유지)</code></pre>
<h3 id="이미-본-값인지-확인">이미 본 값인지 확인</h3>
<p>자바 <code>add()</code>는 <strong>새로 추가되면 <code>true</code>, 이미 있으면 <code>false</code></strong> 를 반환한다. 중복 체크와 추가를 한 번에 할 수 있다.</p>
<pre><code class="language-java">Set&lt;Integer&gt; seen = new HashSet&lt;&gt;();
for (int x : arr) {
    if (!seen.add(x)) {
        System.out.println(x + &quot; 중복!&quot;);
    }
}</code></pre>
<pre><code class="language-python">seen = set()
for x in arr:
    if x in seen:
        print(x, &quot;중복!&quot;)
    seen.add(x)</code></pre>
<h3 id="집합-연산">집합 연산</h3>
<table>
<thead>
<tr>
<th>연산</th>
<th>자바 (원본 <code>a</code>가 바뀜)</th>
<th>파이썬 (새 셋 반환)</th>
</tr>
</thead>
<tbody><tr>
<td>합집합</td>
<td><code>a.addAll(b)</code></td>
<td><code>a | b</code></td>
</tr>
<tr>
<td>교집합</td>
<td><code>a.retainAll(b)</code></td>
<td><code>a &amp; b</code></td>
</tr>
<tr>
<td>차집합</td>
<td><code>a.removeAll(b)</code></td>
<td><code>a - b</code></td>
</tr>
<tr>
<td>부분집합인지</td>
<td><code>a.containsAll(b)</code> (b ⊂ a)</td>
<td><code>b &lt;= a</code></td>
</tr>
</tbody></table>
<blockquote>
<p>자바 집합 연산은 <strong>원본을 바꾼다.</strong> 원본을 남기려면 <code>Set&lt;Integer&gt; c = new HashSet&lt;&gt;(a); c.retainAll(b);</code>처럼 복사 후 연산한다.</p>
</blockquote>
<hr>
<h2 id="5-키로-쓸-수-있는-것">5. 키로 쓸 수 있는 것</h2>
<table>
<thead>
<tr>
<th>키</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>숫자 · 문자열</td>
<td>⭕</td>
<td>⭕</td>
</tr>
<tr>
<td>좌표 <code>(x, y)</code></td>
<td><code>x + &quot;,&quot; + y</code> 문자열 / <code>List.of(x, y)</code></td>
<td>튜플 <code>(x, y)</code> ⭕</td>
</tr>
<tr>
<td>배열 · 리스트</td>
<td><code>int[]</code> ❌ (주소로 비교됨)</td>
<td><code>list</code> ❌ (<code>TypeError: unhashable</code>)</td>
</tr>
</tbody></table>
<pre><code class="language-java">Set&lt;String&gt; visited = new HashSet&lt;&gt;();
visited.add(x + &quot;,&quot; + y);

// int[]를 키로 쓰면 값이 같아도 다른 키로 취급된다
Set&lt;int[]&gt; bad = new HashSet&lt;&gt;();
bad.add(new int[]{1, 2});
bad.contains(new int[]{1, 2});   // false</code></pre>
<pre><code class="language-python">visited = set()
visited.add((x, y))       # 튜플은 OK
visited.add([x, y])       # ❌ TypeError</code></pre>
<hr>
<h2 id="💡-이것만-기억하자">💡 이것만 기억하자</h2>
<ol>
<li><strong>없는 키 조회: 자바 <code>null</code>, 파이썬 <code>KeyError</code></strong>
둘 다 <code>getOrDefault</code> / <code>get(k, 0)</code>으로 안전하게 쓰자.</li>
<li><strong>개수 세기: 자바 <code>getOrDefault + 1</code> 또는 <code>merge</code>, 파이썬 <code>get + 1</code> 또는 <code>Counter</code></strong></li>
<li><strong>키 + 값 순회: 자바 <code>entrySet()</code>, 파이썬 <code>items()</code></strong></li>
<li><strong>파이썬 빈 셋은 <code>set()</code>,</strong> <code>{}</code>는 딕셔너리다.</li>
<li><strong>자바 <code>set.add()</code>는 중복이면 <code>false</code></strong> 중복 체크를 한 줄로!</li>
<li><strong>좌표를 키로 쓸 때: 자바는 문자열, 파이썬은 튜플</strong> 
배열과 리스트는 키가 될 수 없다.</li>
<li><strong>순서: <code>HashMap</code> / <code>HashSet</code>은 순서 보장 X, 파이썬 <code>dict</code>는 넣은 순서 유지</strong></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java vs Python] 배열/리스트/문자열 기초 한 장 정리]]></title>
            <link>https://velog.io/@nano_o/Java-vs-Python-%EB%B0%B0%EC%97%B4%EB%A6%AC%EC%8A%A4%ED%8A%B8%EB%AC%B8%EC%9E%90%EC%97%B4-%EA%B8%B0%EC%B4%88-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@nano_o/Java-vs-Python-%EB%B0%B0%EC%97%B4%EB%A6%AC%EC%8A%A4%ED%8A%B8%EB%AC%B8%EC%9E%90%EC%97%B4-%EA%B8%B0%EC%B4%88-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 30 Sep 2026 16:19:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>알고리즘 Cheat Sheet</strong> 시리즈는 코딩 테스트를 풀다가 &quot;이거 자바에선 뭐였지?&quot;, &quot;파이썬은 어떻게 했더라?&quot; 싶을 때 바로 펼쳐 보려고 만든 개인 참고용 정리입니다.
Java와 Python을 나란히 놓고, 문법 차이 때문에 실수하기 쉬운 부분만 짧게 정리합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/nano_o/post/b4b30df0-c73a-44b9-a4c7-754542abb523/image.png" alt=""></p>
<p>이번 주제는 <strong>배열 · 리스트 · 문자열 기초</strong>입니다.
길이 구하기, 순회, 복사, 정렬처럼 매 문제마다 쓰는 기본기인데, 자바는 <strong>대상마다 문법이 다르고</strong> 파이썬은 <strong>거의 하나로 통일</strong>되어 있어서 자주 헷갈립니다.</p>
<hr>
<h2 id="1-길이-·-크기">1. 길이 · 크기</h2>
<table>
<thead>
<tr>
<th>대상</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>배열</td>
<td><code>arr.length</code></td>
<td><code>len(arr)</code></td>
</tr>
<tr>
<td>문자열</td>
<td><code>s.length()</code></td>
<td><code>len(s)</code></td>
</tr>
<tr>
<td><code>StringBuilder</code></td>
<td><code>sb.length()</code></td>
<td>-</td>
</tr>
<tr>
<td>리스트</td>
<td><code>list.size()</code></td>
<td><code>len(lst)</code></td>
</tr>
<tr>
<td>맵 · 셋</td>
<td><code>map.size()</code>, <code>set.size()</code></td>
<td><code>len(d)</code>, <code>len(s)</code></td>
</tr>
<tr>
<td>2차원 배열 행 · 열</td>
<td><code>grid.length</code>, <code>grid[0].length</code></td>
<td><code>len(grid)</code>, <code>len(grid[0])</code></td>
</tr>
</tbody></table>
<blockquote>
<p>자바는 <strong>배열은 필드(<code>length</code>), 문자열은 메서드(<code>length()</code>), 컬렉션은 <code>size()</code></strong> 로 외우자!
파이썬은 무엇이든 <strong><code>len()</code></strong> 하나면 된다.</p>
</blockquote>
<pre><code class="language-java">int[] arr = {1, 2, 3};
String s = &quot;abc&quot;;
List&lt;Integer&gt; list = List.of(1, 2, 3);

arr.length;    // 3  (괄호 없음)
s.length();    // 3  (괄호 있음)
list.size();   // 3</code></pre>
<pre><code class="language-python">len([1, 2, 3])  # 3
len(&quot;abc&quot;)      # 3</code></pre>
<hr>
<h2 id="2-선언-·-초기화">2. 선언 · 초기화</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>크기 n, 0으로 초기화</td>
<td><code>int[] arr = new int[n];</code></td>
<td><code>arr = [0] * n</code></td>
</tr>
<tr>
<td>값으로 초기화</td>
<td><code>int[] arr = {1, 2, 3};</code></td>
<td><code>arr = [1, 2, 3]</code></td>
</tr>
<tr>
<td>특정 값으로 채우기</td>
<td><code>Arrays.fill(arr, -1);</code></td>
<td><code>arr = [-1] * n</code></td>
</tr>
<tr>
<td>가변 리스트</td>
<td><code>List&lt;Integer&gt; list = new ArrayList&lt;&gt;();</code></td>
<td><code>lst = []</code></td>
</tr>
<tr>
<td>2차원 n × m</td>
<td><code>int[][] grid = new int[n][m];</code></td>
<td><code>grid = [[0] * m for _ in range(n)]</code></td>
</tr>
</tbody></table>
<h3 id="선언-체크포인트">선언 체크포인트</h3>
<ul>
<li><p><strong>파이썬 2차원 리스트를 <code>[[0] * m] * n</code>으로 만들면 안 된다.</strong>
모든 행이 <strong>같은 리스트를 가리켜서</strong> 한 칸을 바꾸면 모든 행이 같이 바뀐다.</p>
<pre><code class="language-python">grid = [[0] * 3] * 2
grid[0][0] = 1
print(grid)   # [[1, 0, 0], [1, 0, 0]]  ← 둘 다 바뀜</code></pre>
</li>
<li><p><strong>자바 <code>Arrays.asList()</code>와 <code>List.of()</code>는 크기를 바꿀 수 없다.</strong> <code>add</code>하면 예외가 난다. 추가하려면 <code>new ArrayList&lt;&gt;(Arrays.asList(...))</code>로 감싼다.</p>
</li>
<li><p><strong>자바 <code>int[]</code>는 <code>Arrays.asList()</code>로 리스트가 되지 않는다.</strong> <code>List&lt;int[]&gt;</code>가 되어 버린다. 박싱이 필요하다.</p>
<pre><code class="language-java">List&lt;Integer&gt; list = Arrays.stream(arr).boxed().collect(Collectors.toList());   // int[] → List
int[] back = list.stream().mapToInt(Integer::intValue).toArray();                // List → int[]</code></pre>
</li>
</ul>
<hr>
<h2 id="3-순회">3. 순회</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>값만</td>
<td><code>for (int x : arr)</code></td>
<td><code>for x in arr:</code></td>
</tr>
<tr>
<td>인덱스만</td>
<td><code>for (int i = 0; i &lt; arr.length; i++)</code></td>
<td><code>for i in range(len(arr)):</code></td>
</tr>
<tr>
<td>인덱스 + 값</td>
<td>인덱스로 순회 후 <code>arr[i]</code></td>
<td><code>for i, x in enumerate(arr):</code></td>
</tr>
<tr>
<td>인덱스를 1부터</td>
<td><code>i + 1</code>로 계산</td>
<td><code>enumerate(arr, start=1)</code></td>
</tr>
<tr>
<td>거꾸로</td>
<td><code>for (int i = n - 1; i &gt;= 0; i--)</code></td>
<td><code>for i in range(n - 1, -1, -1):</code> / <code>reversed(arr)</code></td>
</tr>
<tr>
<td>두 배열 동시에</td>
<td>인덱스로 순회</td>
<td><code>for a, b in zip(x, y):</code></td>
</tr>
<tr>
<td>문자열 한 글자씩</td>
<td><code>for (char c : s.toCharArray())</code></td>
<td><code>for c in s:</code></td>
</tr>
</tbody></table>
<h3 id="enumerate">enumerate</h3>
<p>파이썬에서 <strong>인덱스와 값이 둘 다 필요할 때</strong> 쓰는 함수다. 자바에는 없으니 인덱스 <code>for</code>문을 쓴다.</p>
<pre><code class="language-python">fruits = [&quot;apple&quot;, &quot;banana&quot;, &quot;cherry&quot;]

for i, fruit in enumerate(fruits):
    print(i, fruit)        # 0 apple / 1 banana / 2 cherry

for i, fruit in enumerate(fruits, start=1):
    print(i, fruit)        # 1 apple / 2 banana / 3 cherry</code></pre>
<pre><code class="language-java">String[] fruits = {&quot;apple&quot;, &quot;banana&quot;, &quot;cherry&quot;};

for (int i = 0; i &lt; fruits.length; i++) {
    System.out.println(i + &quot; &quot; + fruits[i]);
}</code></pre>
<blockquote>
<p>파이썬에서 <code>for i in range(len(arr)):</code> 안에서 <code>arr[i]</code>를 쓰고 있다면 <code>enumerate</code>로 바꾸는 게 더 파이썬답다.</p>
</blockquote>
<hr>
<h2 id="4-문자열-다루기">4. 문자열 다루기</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>i번째 문자</td>
<td><code>s.charAt(i)</code></td>
<td><code>s[i]</code></td>
</tr>
<tr>
<td>마지막 문자</td>
<td><code>s.charAt(s.length() - 1)</code></td>
<td><code>s[-1]</code></td>
</tr>
<tr>
<td>부분 문자열 <code>[a, b)</code></td>
<td><code>s.substring(a, b)</code></td>
<td><code>s[a:b]</code></td>
</tr>
<tr>
<td>뒤집기</td>
<td><code>new StringBuilder(s).reverse().toString()</code></td>
<td><code>s[::-1]</code></td>
</tr>
<tr>
<td>공백으로 나누기</td>
<td><code>s.split(&quot; &quot;)</code></td>
<td><code>s.split()</code></td>
</tr>
<tr>
<td>합치기</td>
<td><code>String.join(&quot;,&quot;, list)</code></td>
<td><code>&quot;,&quot;.join(lst)</code></td>
</tr>
<tr>
<td>문자 배열로</td>
<td><code>s.toCharArray()</code></td>
<td><code>list(s)</code></td>
</tr>
<tr>
<td>포함 여부</td>
<td><code>s.contains(&quot;ab&quot;)</code></td>
<td><code>&quot;ab&quot; in s</code></td>
</tr>
</tbody></table>
<h3 id="문자열-체크포인트">문자열 체크포인트</h3>
<ul>
<li><strong>두 언어 모두 문자열은 불변(immutable)이다.</strong> <code>s.charAt(0) = &#39;x&#39;</code>, <code>s[0] = &#39;x&#39;</code> 모두 불가능하다. 문자 배열(리스트)로 바꿔서 수정한 뒤 다시 합친다.</li>
</ul>
<pre><code class="language-java">  String s = &quot;hello&quot;;
  char[] cs = s.toCharArray();   // [&#39;h&#39;, &#39;e&#39;, &#39;l&#39;, &#39;l&#39;, &#39;o&#39;]
  cs[0] = &#39;j&#39;;                   // 배열은 수정 가능
  s = new String(cs);            // &quot;jello&quot;

  // StringBuilder도 가능
  StringBuilder sb = new StringBuilder(s);
  sb.setCharAt(0, &#39;y&#39;);
  s = sb.toString();             // &quot;yello&quot;</code></pre>
<pre><code class="language-python">  s = &quot;hello&quot;
  chars = list(s)                # [&#39;h&#39;, &#39;e&#39;, &#39;l&#39;, &#39;l&#39;, &#39;o&#39;]
  chars[0] = &quot;j&quot;                 # 리스트는 수정 가능
  s = &quot;&quot;.join(chars)             # &quot;jello&quot;

  # 한 글자만 바꿀 거면 슬라이싱으로 새로 만들어도 된다
  s = &quot;y&quot; + s[1:]                # &quot;yello&quot;</code></pre>
<ul>
<li><p><strong>반복문에서 문자열을 <code>+</code>로 이어 붙이지 말자.</strong> 매번 새 문자열이 만들어져서 느리다.</p>
<pre><code class="language-java">StringBuilder sb = new StringBuilder();
for (int x : arr) sb.append(x).append(&quot; &quot;);
String result = sb.toString();</code></pre>
<pre><code class="language-python">result = &quot; &quot;.join(map(str, arr))</code></pre>
</li>
<li><p><strong><code>split</code> 동작이 다르다.</strong> 파이썬 <code>split()</code>(인자 없음)은 연속된 공백도 하나로 보고 앞뒤 공백을 무시한다. 자바 <code>split(&quot; &quot;)</code>는 공백 하나 기준이라 빈 문자열이 섞일 수 있다. 자바에서 연속 공백을 처리하려면 <code>s.trim().split(&quot;\\s+&quot;)</code>를 쓴다.</p>
</li>
</ul>
<hr>
<h2 id="5-복사">5. 복사</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>같은 객체 가리키기 (복사 아님)</td>
<td><code>int[] b = a;</code></td>
<td><code>b = a</code></td>
</tr>
<tr>
<td>1차원 복사</td>
<td><code>a.clone()</code> / <code>Arrays.copyOf(a, a.length)</code></td>
<td><code>a[:]</code> / <code>a.copy()</code> / <code>list(a)</code></td>
</tr>
<tr>
<td>리스트 복사</td>
<td><code>new ArrayList&lt;&gt;(list)</code></td>
<td><code>lst.copy()</code></td>
</tr>
<tr>
<td>2차원 깊은 복사</td>
<td>행마다 <code>clone()</code></td>
<td><code>[row[:] for row in a]</code> / <code>copy.deepcopy(a)</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">int[][] copy = new int[n][];
for (int i = 0; i &lt; n; i++) copy[i] = grid[i].clone();</code></pre>
<blockquote>
<p><code>b = a</code>는 두 언어 모두 <strong>복사가 아니라 같은 배열을 가리키는 것</strong>이다. <code>b</code>를 바꾸면 <code>a</code>도 바뀐다.
2차원 배열에 <code>clone()</code> / <code>a[:]</code>만 쓰면 <strong>행은 여전히 공유</strong>된다(얕은 복사).</p>
</blockquote>
<hr>
<h2 id="6-정렬-원본을-바꿀까-복사본을-만들까">6. 정렬: 원본을 바꿀까, 복사본을 만들까</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>원본 정렬 (배열)</td>
<td><code>Arrays.sort(arr)</code></td>
<td>-</td>
</tr>
<tr>
<td>원본 정렬 (리스트)</td>
<td><code>Collections.sort(list)</code> / <code>list.sort(null)</code></td>
<td><code>lst.sort()</code></td>
</tr>
<tr>
<td>정렬된 복사본</td>
<td><code>int[] c = arr.clone(); Arrays.sort(c);</code></td>
<td><code>sorted(lst)</code></td>
</tr>
<tr>
<td>리스트 정렬된 복사본</td>
<td><code>list.stream().sorted().collect(Collectors.toList())</code></td>
<td><code>sorted(lst)</code></td>
</tr>
<tr>
<td>문자열 정렬</td>
<td><code>char[] cs = s.toCharArray(); Arrays.sort(cs); new String(cs)</code></td>
<td><code>&quot;&quot;.join(sorted(s))</code></td>
</tr>
</tbody></table>
<h3 id="sort-vs-sorted"><code>sort()</code> vs <code>sorted()</code></h3>
<pre><code class="language-python">nums = [3, 1, 2]

a = sorted(nums)   # a = [1, 2, 3], nums = [3, 1, 2]  ← 원본 유지
nums.sort()        # nums = [1, 2, 3]                 ← 원본 변경</code></pre>
<ul>
<li><strong><code>lst.sort()</code></strong>: 리스트 <strong>원본을 정렬</strong>하고 <strong><code>None</code>을 반환</strong>한다. 리스트에만 있는 메서드다.</li>
<li><strong><code>sorted(x)</code></strong>: 원본은 그대로 두고 <strong>정렬된 새 리스트를 반환</strong>한다. 문자열, 튜플, 딕셔너리 등 <strong>순회 가능한 모든 것</strong>에 쓸 수 있다.</li>
</ul>
<pre><code class="language-python">nums = nums.sort()      # (X) nums가 None이 된다
nums = sorted(nums)     # (O)

sorted(&quot;dcba&quot;)          # [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;]
sorted({&quot;b&quot;: 1, &quot;a&quot;: 2})  # [&#39;a&#39;, &#39;b&#39;]  (딕셔너리는 키만)</code></pre>
<p>자바의 <code>Arrays.sort()</code>, <code>Collections.sort()</code>는 <strong>항상 원본을 바꾼다.</strong> 원본을 남겨야 하면 먼저 복사하고 정렬한다.</p>
<hr>
<h2 id="💡-이것만-기억하자">💡 이것만 기억하자</h2>
<ol>
<li><strong>길이: 자바 배열 <code>length</code>, 문자열 <code>length()</code>, 컬렉션 <code>size()</code>.</strong> 파이썬은 전부 <code>len()</code></li>
<li><strong>파이썬 2차원 리스트는 <code>[[0] * m for _ in range(n)]</code>.</strong> <code>[[0] * m] * n</code>은 행이 공유된다.</li>
<li><strong>인덱스와 값이 둘 다 필요하면 파이썬은 <code>enumerate</code>.</strong> 자바는 인덱스 <code>for</code>문</li>
<li><strong><code>b = a</code>는 복사가 아니다.</strong> 1차원은 <code>clone()</code> / <code>a[:]</code>, 2차원은 행마다 복사</li>
<li><strong>파이썬 <code>sort()</code>는 원본 변경 + <code>None</code> 반환, <code>sorted()</code>는 새 리스트 반환.</strong> 자바 정렬은 항상 원본 변경</li>
<li><strong>문자열 이어 붙이기는 <code>StringBuilder</code> / <code>join</code></strong></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java vs Python] Math 함수 한 장 정리]]></title>
            <link>https://velog.io/@nano_o/Java-vs-Python-Math-%ED%95%A8%EC%88%98-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@nano_o/Java-vs-Python-Math-%ED%95%A8%EC%88%98-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 30 Sep 2026 16:07:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>알고리즘 Cheat Sheet</strong> 시리즈는 코딩 테스트를 풀다가 &quot;이거 자바에선 뭐였지?&quot;, &quot;파이썬은 어떻게 했더라?&quot; 싶을 때 바로 펼쳐 보려고 만든 개인 참고용 정리입니다.
Java와 Python을 나란히 놓고, 문법 차이 때문에 실수하기 쉬운 부분만 짧게 정리합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/nano_o/post/54cc257f-a503-4264-be67-43b93dd9975f/image.png" alt=""></p>
<p>이번 주제는 <strong>Math</strong>입니다.
자바는 대부분 <code>Math</code> 클래스에 모여 있고, 파이썬은 <strong>내장 함수</strong>와 <strong><code>math</code> 모듈</strong>에 나뉘어 있습니다.
함수 이름은 비슷하지만 <strong>반환 타입</strong>과 <strong>반올림 방식</strong>이 달라서 실수하기 쉽습니다.</p>
<pre><code class="language-python">import math   # 파이썬은 math 모듈을 import 해야 한다 (abs, max, min, pow, round는 내장)</code></pre>
<hr>
<h2 id="1-기본-연산">1. 기본 연산</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바</th>
<th>파이썬</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>절댓값</td>
<td><code>Math.abs(x)</code></td>
<td><code>abs(x)</code></td>
<td></td>
</tr>
<tr>
<td>최댓값</td>
<td><code>Math.max(a, b)</code></td>
<td><code>max(a, b, c...)</code> / <code>max(lst)</code></td>
<td>자바는 <strong>인자 2개만</strong></td>
</tr>
<tr>
<td>최솟값</td>
<td><code>Math.min(a, b)</code></td>
<td><code>min(a, b, c...)</code> / <code>min(lst)</code></td>
<td>자바는 <strong>인자 2개만</strong></td>
</tr>
<tr>
<td>거듭제곱</td>
<td><code>Math.pow(a, b)</code> → <code>double</code></td>
<td><code>a ** b</code> / <code>pow(a, b)</code> → <code>int</code></td>
<td></td>
</tr>
<tr>
<td>제곱근</td>
<td><code>Math.sqrt(x)</code> → <code>double</code></td>
<td><code>math.sqrt(x)</code> → <code>float</code></td>
<td></td>
</tr>
<tr>
<td>정수 제곱근</td>
<td><code>(int) Math.sqrt(x)</code></td>
<td><code>math.isqrt(x)</code> → <code>int</code></td>
<td>파이썬 3.8+</td>
</tr>
<tr>
<td>세제곱근</td>
<td><code>Math.cbrt(x)</code></td>
<td><code>math.cbrt(x)</code></td>
<td>파이썬 3.11+</td>
</tr>
</tbody></table>
<pre><code class="language-java">int big = Math.max(a, Math.max(b, c));     // 3개 이상은 중첩
int p = (int) Math.pow(2, 10);             // 1024, double이라 형변환 필요</code></pre>
<pre><code class="language-python">big = max(a, b, c)
p = 2 ** 10                                # 1024, 결과가 바로 int</code></pre>
<hr>
<h2 id="2-반올림올림내림">2. 반올림/올림/내림</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>올림</td>
<td><code>Math.ceil(x)</code> → <code>double</code></td>
<td><code>math.ceil(x)</code> → <code>int</code></td>
</tr>
<tr>
<td>내림</td>
<td><code>Math.floor(x)</code> → <code>double</code></td>
<td><code>math.floor(x)</code> → <code>int</code></td>
</tr>
<tr>
<td>반올림</td>
<td><code>Math.round(x)</code> → <code>long</code></td>
<td><code>round(x)</code> → <code>int</code></td>
</tr>
<tr>
<td>소수점 버림</td>
<td><code>(int) x</code></td>
<td><code>int(x)</code> / <code>math.trunc(x)</code></td>
</tr>
<tr>
<td>소수 n째 자리 반올림</td>
<td><code>Math.round(x * 100) / 100.0</code></td>
<td><code>round(x, 2)</code></td>
</tr>
<tr>
<td>소수 n째 자리까지 출력</td>
<td><code>String.format(&quot;%.2f&quot;, x)</code></td>
<td><code>f&quot;{x:.2f}&quot;</code></td>
</tr>
</tbody></table>
<h3 id="반올림-결과가-다른-거-주의">반올림 결과가 다른 거 주의!!</h3>
<table>
<thead>
<tr>
<th>값</th>
<th>자바 <code>Math.round</code></th>
<th>파이썬 <code>round</code></th>
</tr>
</thead>
<tbody><tr>
<td><code>2.5</code></td>
<td><code>3</code></td>
<td><strong><code>2</code></strong></td>
</tr>
<tr>
<td><code>3.5</code></td>
<td><code>4</code></td>
<td><code>4</code></td>
</tr>
<tr>
<td><code>-2.5</code></td>
<td><strong><code>-2</code></strong></td>
<td><code>-2</code></td>
</tr>
</tbody></table>
<ul>
<li>자바 <code>Math.round(x)</code>는 <code>floor(x + 0.5)</code> 방식이다. 그래서 <code>-2.5</code>는 <code>-2</code>가 된다.</li>
<li>파이썬 <code>round()</code>는 <strong>은행가 반올림</strong>이라 <code>.5</code>일 때 짝수 쪽으로 간다.</li>
<li>사사오입이 필요하면 파이썬은 <code>math.floor(x + 0.5)</code>를 쓰거나 <code>decimal</code> 모듈의 <code>ROUND_HALF_UP</code>을 쓴다.</li>
</ul>
<h3 id="정수-올림-나눗셈-코테-단골">정수 올림 나눗셈 (코테 단골)</h3>
<p><code>a / b</code>를 올림하고 싶을 때, 실수로 바꾸지 않고 정수만으로 계산하는 방법이다.</p>
<pre><code class="language-java">int q = (a + b - 1) / b;        // a, b가 양수일 때</code></pre>
<pre><code class="language-python">q = (a + b - 1) // b            # a, b가 양수일 때
q = -(-a // b)                  # 음수에서도 동작</code></pre>
<hr>
<h2 id="3-몫과-나머지">3. 몫과 나머지</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>몫 (0 방향 버림)</td>
<td><code>a / b</code></td>
<td><code>int(a / b)</code></td>
</tr>
<tr>
<td>몫 (내림)</td>
<td><code>Math.floorDiv(a, b)</code></td>
<td><code>a // b</code></td>
</tr>
<tr>
<td>나머지 (부호가 <code>a</code>를 따름)</td>
<td><code>a % b</code></td>
<td><code>math.fmod(a, b)</code></td>
</tr>
<tr>
<td>나머지 (부호가 <code>b</code>를 따름)</td>
<td><code>Math.floorMod(a, b)</code></td>
<td><code>a % b</code></td>
</tr>
<tr>
<td>몫과 나머지 한 번에</td>
<td>-</td>
<td><code>divmod(a, b)</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">-7 / 2;                  // -3
-7 % 2;                  // -1
Math.floorDiv(-7, 2);    // -4  (파이썬 // 와 같음)
Math.floorMod(-7, 2);    // 1   (파이썬 % 와 같음)</code></pre>
<blockquote>
<p>음수 인덱스를 순환시킬 때(<code>(i - 1) % n</code>) 자바는 음수가 나올 수 있다. <strong><code>Math.floorMod(i - 1, n)</code></strong> 또는 <code>((i - 1) % n + n) % n</code>을 쓰자.</p>
</blockquote>
<hr>
<h2 id="4-큰-수-·-오버플로우">4. 큰 수 · 오버플로우</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>정수 최댓값</td>
<td><code>Integer.MAX_VALUE</code> (약 21억)<br><code>Long.MAX_VALUE</code> (약 922경)</td>
<td>제한 없음</td>
</tr>
<tr>
<td>무한대</td>
<td><code>Integer.MAX_VALUE</code> 등으로 대체<br><code>Double.POSITIVE_INFINITY</code></td>
<td><code>float(&#39;inf&#39;)</code> / <code>math.inf</code></td>
</tr>
<tr>
<td>오버플로우 감지</td>
<td><code>Math.addExact(a, b)</code><br><code>Math.multiplyExact(a, b)</code></td>
<td>필요 없음</td>
</tr>
<tr>
<td>아주 큰 수</td>
<td><code>BigInteger</code></td>
<td>그냥 <code>int</code></td>
</tr>
</tbody></table>
<h3 id="거듭제곱-나머지-모듈러-거듭제곱">거듭제곱 나머지 (모듈러 거듭제곱)</h3>
<p><code>a^b % mod</code>처럼 결과가 큰 거듭제곱은 <strong><code>Math.pow</code>를 쓰면 안 된다.</strong> <code>double</code>이라 정밀도가 깨진다.</p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/10a1b663-c062-4b2b-8aaf-4a8e13bdca15/image.png" alt=""></p>
<pre><code class="language-java">// 빠른 거듭제곱 (분할 정복)
static long modPow(long a, long b, long mod) {
    long result = 1;
    a %= mod;
    while (b &gt; 0) {
        if ((b &amp; 1) == 1) result = result * a % mod;
        a = a * a % mod;
        b &gt;&gt;= 1;
    }
    return result;
}</code></pre>
<pre><code class="language-python">pow(a, b, mod)   # 내장 함수가 세 번째 인자로 모듈러를 지원한다</code></pre>
<hr>
<h2 id="5-정수론-·-조합">5. 정수론 · 조합</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>최대공약수</td>
<td>직접 구현 / <code>BigInteger.gcd</code></td>
<td><code>math.gcd(a, b)</code></td>
</tr>
<tr>
<td>최소공배수</td>
<td><code>a / gcd(a, b) * b</code></td>
<td><code>math.lcm(a, b)</code> (3.9+)</td>
</tr>
<tr>
<td>팩토리얼</td>
<td>직접 구현</td>
<td><code>math.factorial(n)</code></td>
</tr>
<tr>
<td>조합 nCr</td>
<td>직접 구현</td>
<td><code>math.comb(n, r)</code> (3.8+)</td>
</tr>
<tr>
<td>순열 nPr</td>
<td>직접 구현</td>
<td><code>math.perm(n, r)</code> (3.8+)</td>
</tr>
</tbody></table>
<pre><code class="language-java">static int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

static int lcm(int a, int b) {
    return a / gcd(a, b) * b;   // 곱하기 먼저 하면 오버플로우 위험
}</code></pre>
<pre><code class="language-python">math.gcd(12, 18)    # 6
math.lcm(12, 18)    # 36
math.comb(5, 2)     # 10</code></pre>
<hr>
<h2 id="6-로그-·-기타">6. 로그 · 기타</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>자연로그</td>
<td><code>Math.log(x)</code></td>
<td><code>math.log(x)</code></td>
</tr>
<tr>
<td>상용로그</td>
<td><code>Math.log10(x)</code></td>
<td><code>math.log10(x)</code></td>
</tr>
<tr>
<td>밑이 2인 로그</td>
<td><code>Math.log(x) / Math.log(2)</code></td>
<td><code>math.log2(x)</code></td>
</tr>
<tr>
<td>임의의 밑</td>
<td><code>Math.log(x) / Math.log(b)</code></td>
<td><code>math.log(x, b)</code></td>
</tr>
<tr>
<td>원주율</td>
<td><code>Math.PI</code></td>
<td><code>math.pi</code></td>
</tr>
<tr>
<td>부호</td>
<td><code>Math.signum(x)</code></td>
<td><code>(x &gt; 0) - (x &lt; 0)</code></td>
</tr>
<tr>
<td>난수 <code>[0, 1)</code></td>
<td><code>Math.random()</code></td>
<td><code>random.random()</code></td>
</tr>
<tr>
<td>정수 난수 <code>[a, b]</code></td>
<td><code>(int) (Math.random() * (b - a + 1)) + a</code></td>
<td><code>random.randint(a, b)</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="7-배열-집계">7. 배열 집계</h2>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>합계</td>
<td><code>Arrays.stream(arr).sum()</code></td>
<td><code>sum(arr)</code></td>
</tr>
<tr>
<td>최댓값</td>
<td><code>Arrays.stream(arr).max().getAsInt()</code></td>
<td><code>max(arr)</code></td>
</tr>
<tr>
<td>최솟값</td>
<td><code>Arrays.stream(arr).min().getAsInt()</code></td>
<td><code>min(arr)</code></td>
</tr>
<tr>
<td>평균</td>
<td><code>Arrays.stream(arr).average().getAsDouble()</code></td>
<td><code>sum(arr) / len(arr)</code></td>
</tr>
</tbody></table>
<blockquote>
<p>자바 스트림은 편하지만 반복 호출하면 느리다. 반복문 안에서는 <code>for</code>문으로 직접 계산하는 게 안전하다.</p>
</blockquote>
<hr>
<h2 id="💡-이것만-기억하자">💡 이것만 기억하자</h2>
<ol>
<li><strong>자바 <code>Math</code>는 대부분 <code>double</code>을 반환한다.</strong> <code>pow</code>, <code>sqrt</code>, <code>ceil</code>, <code>floor</code>는 <code>(int)</code>로 형변환하고, <code>round</code>는 <code>long</code>이다. 파이썬 <code>math.ceil</code> / <code>math.floor</code>는 바로 <code>int</code>다.</li>
<li><strong>반올림이 다르다!</strong> <code>2.5</code>는 자바 <code>3</code>, 파이썬 <code>2</code>(은행가 반올림)</li>
<li><strong>음수 몫과 나머지.</strong> 파이썬 <code>//</code>, <code>%</code>와 같게 하려면 자바는 <code>Math.floorDiv</code>, <code>Math.floorMod</code>를 쓴다.</li>
<li><strong>큰 거듭제곱에 <code>Math.pow</code> 금지.</strong> 자바는 빠른 거듭제곱을 직접 구현하고, 파이썬은 <code>pow(a, b, mod)</code>를 쓴다.</li>
<li><strong>자바는 오버플로우를 조심!</strong> 곱셈은 <code>long</code>으로, 최소공배수는 나누기를 먼저!</li>
<li><strong>gcd, 조합, 팩토리얼은 파이썬만 내장.</strong> 자바는 직접 구현한다.</li>
<li>사실 나도 아직 안 써본 게 많다...언젠가는 쓰겠지 싶어서 정리했다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java vs Python] 형변환/비교/정렬 한 장 정리]]></title>
            <link>https://velog.io/@nano_o/Java-vs-Python-%ED%98%95%EB%B3%80%ED%99%98%EB%B9%84%EA%B5%90%EC%A0%95%EB%A0%AC-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@nano_o/Java-vs-Python-%ED%98%95%EB%B3%80%ED%99%98%EB%B9%84%EA%B5%90%EC%A0%95%EB%A0%AC-%ED%95%9C-%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 30 Sep 2026 15:43:17 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>알고리즘 Cheat Sheet</strong> 시리즈는 코딩 테스트를 풀다가 &quot;이거 자바에선 뭐였지?&quot;, &quot;파이썬은 어떻게 했더라?&quot; 싶을 때 바로 펼쳐 보려고 만든 개인 참고용 정리입니다.
Java와 Python을 나란히 놓고, 문법 차이 때문에 실수하기 쉬운 부분만 짧게 정리합니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/nano_o/post/c226452c-aac0-48aa-8f22-31cc89c99c47/image.png" alt=""></p>
<p>이번 주제는 <strong>형변환, 비교, 정렬</strong>입니다.
입력을 파싱하고, 값을 비교하고, 원하는 기준으로 정렬하는 건 거의 모든 문제에서 반복되는 작업이므로 <strong>꼭!</strong> 숙지할 필요가 있습니다.</p>
<hr>
<h2 id="1-형변환">1. 형변환</h2>
<h3 id="문자열-↔-숫자">문자열 ↔ 숫자</h3>
<table>
<thead>
<tr>
<th>변환</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>문자열 → 정수</td>
<td><code>Integer.parseInt(&quot;123&quot;)</code></td>
<td><code>int(&quot;123&quot;)</code></td>
</tr>
<tr>
<td>문자열 → 실수</td>
<td><code>Double.parseDouble(&quot;3.14&quot;)</code></td>
<td><code>float(&quot;3.14&quot;)</code></td>
</tr>
<tr>
<td>정수 → 문자열</td>
<td><code>String.valueOf(123)</code> / <code>Integer.toString(123)</code></td>
<td><code>str(123)</code></td>
</tr>
<tr>
<td>실수 → 문자열</td>
<td><code>String.valueOf(3.14)</code> / <code>Double.toString(3.14)</code></td>
<td><code>str(3.14)</code></td>
</tr>
<tr>
<td>정수 → 실수</td>
<td><code>double d = 123;</code> (자동) / <code>(double) i</code></td>
<td><code>float(123)</code></td>
</tr>
<tr>
<td>실수 → 정수</td>
<td><code>(int) 3.14</code> → <code>3</code></td>
<td><code>int(3.14)</code> → <code>3</code></td>
</tr>
</tbody></table>
<pre><code class="language-java">int i = Integer.parseInt(&quot;123&quot;);
double d = Double.parseDouble(&quot;3.14&quot;);
String s = String.valueOf(123);
int t = (int) 3.99;   // 3 (소수점 버림)</code></pre>
<pre><code class="language-python">i = int(&quot;123&quot;)
d = float(&quot;3.14&quot;)
s = str(123)
t = int(3.99)          # 3 (소수점 버림)</code></pre>
<h3 id="문자-↔-숫자-코테-단골">문자 ↔ 숫자 (코테 단골)</h3>
<table>
<thead>
<tr>
<th>변환</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>숫자 문자 → 정수</td>
<td><code>&#39;7&#39; - &#39;0&#39;</code> → <code>7</code></td>
<td><code>int(&#39;7&#39;)</code> → <code>7</code></td>
</tr>
<tr>
<td>정수 → 숫자 문자</td>
<td><code>(char) (7 + &#39;0&#39;)</code> → <code>&#39;7&#39;</code></td>
<td><code>str(7)</code> → <code>&#39;7&#39;</code></td>
</tr>
<tr>
<td>문자 → 아스키 코드</td>
<td><code>(int) &#39;a&#39;</code> → <code>97</code></td>
<td><code>ord(&#39;a&#39;)</code> → <code>97</code></td>
</tr>
<tr>
<td>아스키 코드 → 문자</td>
<td><code>(char) 97</code> → <code>&#39;a&#39;</code></td>
<td><code>chr(97)</code> → <code>&#39;a&#39;</code></td>
</tr>
<tr>
<td>알파벳 인덱스</td>
<td><code>c - &#39;a&#39;</code></td>
<td><code>ord(c) - ord(&#39;a&#39;)</code></td>
</tr>
</tbody></table>
<h3 id="형변환-체크포인트">형변환 체크포인트</h3>
<ul>
<li><strong>소수 문자열을 바로 정수로 바꾸면 에러가 난다.</strong>
자바 <code>Integer.parseInt(&quot;3.14&quot;)</code> → <code>NumberFormatException</code>, 파이썬 <code>int(&quot;3.14&quot;)</code> → <code>ValueError</code>.
실수로 바꾼 뒤 정수로 변환해야 한다: <code>(int) Double.parseDouble(&quot;3.14&quot;)</code>, <code>int(float(&quot;3.14&quot;))</code>.</li>
<li><strong><code>(int)</code>, <code>int()</code>는 반올림이 아니라 0 방향 버림이다.</strong> <code>(int) -3.7</code>과 <code>int(-3.7)</code>은 둘 다 <code>-3</code>이다.
내림이 필요하면 <code>Math.floor()</code> / <code>math.floor()</code>, 반올림은 <code>Math.round()</code> / <code>round()</code>를 쓴다.</li>
<li><strong>파이썬 <code>round()</code>는 사사오입이 아니다.</strong> 은행가 반올림이라 <code>round(2.5)</code>는 <code>2</code>, <code>round(3.5)</code>는 <code>4</code>다.</li>
<li><strong>나눗셈 결과 타입이 다르다.</strong></li>
</ul>
<table>
<thead>
<tr>
<th>연산</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td><code>7 / 2</code></td>
<td><code>3</code> (정수끼리면 정수)</td>
<td><code>3.5</code> (항상 실수)</td>
</tr>
<tr>
<td>정수 몫</td>
<td><code>7 / 2</code> → <code>3</code></td>
<td><code>7 // 2</code> → <code>3</code></td>
</tr>
<tr>
<td>음수 몫</td>
<td><code>-7 / 2</code> → <code>-3</code> (0 방향)</td>
<td><code>-7 // 2</code> → <code>-4</code> (내림)</td>
</tr>
<tr>
<td>음수 나머지</td>
<td><code>-7 % 2</code> → <code>-1</code></td>
<td><code>-7 % 2</code> → <code>1</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="2-비교">2. 비교</h2>
<table>
<thead>
<tr>
<th>비교 상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>기본형 비교</td>
<td><code>Integer.compare(x, y)</code><br><code>Character.compare(c1, c2)</code><br><code>Double.compare(d1, d2)</code></td>
<td><code>x &lt; y</code>, <code>x == y</code> 연산자 그대로</td>
</tr>
<tr>
<td>객체 기본 기준</td>
<td><code>x.compareTo(y)</code></td>
<td><code>x &lt; y</code> (문자열은 사전순)</td>
</tr>
<tr>
<td>문자열 같은지</td>
<td><code>s1.equals(s2)</code></td>
<td><code>s1 == s2</code></td>
</tr>
</tbody></table>
<p><code>compare</code> / <code>compareTo</code>의 반환값은 <strong>음수(앞이 작음), 0(같음), 양수(앞이 큼)</strong> 이다.</p>
<h3 id="비교-체크포인트">비교 체크포인트</h3>
<ul>
<li><strong>자바에서 문자열은 <code>==</code>로 비교하면 안 된다.</strong> <code>==</code>는 값이 아니라 참조(주소)를 비교한다. 반드시 <code>equals()</code>를 쓴다.</li>
<li><strong><code>Integer</code> 객체도 <code>==</code>로 비교하면 안 된다.</strong> <code>-128 ~ 127</code> 범위만 캐시되어 있어서 그 밖의 값은 같은 숫자여도 <code>false</code>가 나올 수 있다. <code>equals()</code>나 <code>intValue()</code>로 비교한다.</li>
<li><strong>비교 함수에 <code>a - b</code>를 쓰지 말자.</strong> 정수는 오버플로우가 날 수 있고(<code>Integer.MIN_VALUE - 1</code>), 실수는 <code>int</code>로 잘리면서 오차가 생긴다. <code>Integer.compare(a, b)</code>를 쓴다.</li>
</ul>
<hr>
<h2 id="3-정렬">3. 정렬</h2>
<h3 id="기본-정렬">기본 정렬</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>배열 오름차순</td>
<td><code>Arrays.sort(arr)</code></td>
<td><code>arr.sort()</code></td>
</tr>
<tr>
<td>리스트 오름차순</td>
<td><code>Collections.sort(list)</code> / <code>list.sort(null)</code></td>
<td><code>lst.sort()</code></td>
</tr>
<tr>
<td>새 리스트로 정렬</td>
<td><code>stream().sorted()</code></td>
<td><code>sorted(lst)</code></td>
</tr>
<tr>
<td>내림차순</td>
<td><code>Arrays.sort(arr, Collections.reverseOrder())</code></td>
<td><code>arr.sort(reverse=True)</code></td>
</tr>
</tbody></table>
<h3 id="기준을-정해서-정렬">기준을 정해서 정렬</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바 (람다)</th>
<th>파이썬 (<code>key</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>오름차순</td>
<td><code>(a, b) -&gt; a.compareTo(b)</code></td>
<td>기본값</td>
</tr>
<tr>
<td>내림차순</td>
<td><code>(a, b) -&gt; b.compareTo(a)</code></td>
<td><code>reverse=True</code></td>
</tr>
<tr>
<td>길이순</td>
<td><code>(a, b) -&gt; Integer.compare(a.length(), b.length())</code></td>
<td><code>key=len</code></td>
</tr>
<tr>
<td>첫 원소 기준</td>
<td><code>(a, b) -&gt; Integer.compare(a[0], b[0])</code></td>
<td><code>key=lambda x: x[0]</code></td>
</tr>
<tr>
<td>1순위 오름, 2순위 내림</td>
<td>아래 코드 참고</td>
<td><code>key=lambda x: (x[0], -x[1])</code></td>
</tr>
</tbody></table>
<blockquote>
<p>자바는 <strong><code>(a, b)</code> 순서면 오름차순, <code>(b, a)</code>로 뒤집으면 내림차순</strong>으로 외우면 된다.</p>
</blockquote>
<pre><code class="language-java">// 2차원 배열: 첫 번째 값 오름차순, 같으면 두 번째 값 내림차순
Arrays.sort(arr, (a, b) -&gt; {
    if (a[0] != b[0]) return Integer.compare(a[0], b[0]);
    return Integer.compare(b[1], a[1]);
});

// Comparator 체이닝으로도 가능
list.sort(Comparator.comparing((int[] a) -&gt; a[0])
                    .thenComparing(a -&gt; a[1], Comparator.reverseOrder()));</code></pre>
<pre><code class="language-python">arr.sort(key=lambda x: (x[0], -x[1]))</code></pre>
<p>파이썬은 튜플을 <code>key</code>로 주면 앞 원소부터 차례로 비교한다. 숫자 기준 내림차순은 <code>-</code>만 붙이면 된다.</p>
<h3 id="특수-정렬-이어-붙였을-때-가장-큰-수">특수 정렬: 이어 붙였을 때 가장 큰 수</h3>
<p><code>[&quot;3&quot;, &quot;30&quot;, &quot;34&quot;, &quot;5&quot;, &quot;9&quot;]</code> → <code>&quot;9534330&quot;</code> 처럼 <strong>두 값을 붙여 보고 더 큰 쪽을 앞에</strong> 두는 정렬이다.</p>
<pre><code class="language-java">String[] strs = {&quot;3&quot;, &quot;30&quot;, &quot;34&quot;, &quot;5&quot;, &quot;9&quot;};
Arrays.sort(strs, (a, b) -&gt; (b + a).compareTo(a + b));
String answer = String.join(&quot;&quot;, strs);   // &quot;9534330&quot;</code></pre>
<pre><code class="language-python">from functools import cmp_to_key

strs = [&quot;3&quot;, &quot;30&quot;, &quot;34&quot;, &quot;5&quot;, &quot;9&quot;]
strs.sort(key=cmp_to_key(lambda a, b: int(b + a) - int(a + b)))
answer = &quot;&quot;.join(strs)                   # &quot;9534330&quot;</code></pre>
<p>파이썬은 <code>key</code>만 받기 때문에, 두 값을 직접 비교하는 함수는 <code>cmp_to_key</code>로 감싸야 한다.</p>
<h3 id="정렬-체크포인트">정렬 체크포인트</h3>
<ul>
<li><p><strong>자바 <code>int[]</code>는 람다나 <code>reverseOrder()</code>로 정렬할 수 없다.</strong> 비교자를 받는 <code>Arrays.sort</code>는 <code>Integer[]</code>, <code>String[]</code>, <code>int[][]</code> 같은 객체 배열에만 쓸 수 있다.
<code>int[]</code>를 내림차순으로 정렬하려면 박싱하거나, 오름차순 정렬 후 뒤에서부터 읽는다.</p>
<pre><code class="language-java">Integer[] boxed = Arrays.stream(arr).boxed().toArray(Integer[]::new);
Arrays.sort(boxed, Collections.reverseOrder());</code></pre>
</li>
<li><p><strong>파이썬 <code>list.sort()</code>는 <code>None</code>을 반환한다.</strong> <code>arr = arr.sort()</code>라고 쓰면 <code>arr</code>이 <code>None</code>이 된다. 새 리스트가 필요하면 <code>sorted()</code>를 쓴다.</p>
</li>
<li><p><strong>두 언어 모두 객체 정렬은 안정 정렬이다.</strong> 기준 값이 같으면 원래 순서가 유지되므로, 정렬을 여러 번 나눠서 해도 된다.</p>
</li>
</ul>
<hr>
<h2 id="💡-이것만-기억하자">💡 이것만 기억하자</h2>
<ol>
<li><strong>소수 문자열 → 정수는 두 단계.</strong> 실수로 바꾼 다음 정수로. <code>(int)</code>와 <code>int()</code>는 0 방향 버림이다.</li>
<li><strong>나눗셈이 다르다.</strong> 자바 <code>/</code>는 정수끼리면 정수, 파이썬 <code>/</code>는 항상 실수. 음수 몫은 자바 <code>-3</code>, 파이썬 <code>//</code>는 <code>-4</code>.</li>
<li><strong>자바 문자열·<code>Integer</code> 비교는 <code>equals()</code>.</strong> <code>==</code>는 주소 비교다.</li>
<li><strong>비교 함수에서 <code>a - b</code> 대신 <code>Integer.compare(a, b)</code>.</strong></li>
<li><strong>정렬 기준: 자바는 <code>(a, b)</code> 오름 / <code>(b, a)</code> 내림, 파이썬은 <code>key=</code>와 튜플, <code>-</code> 붙여서 내림.</strong></li>
<li><strong>자바 <code>int[]</code>는 비교자 정렬 불가.</strong> <code>Integer[]</code>로 박싱하자.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java vs Python] 스택/큐 구현 (Deque 기준)]]></title>
            <link>https://velog.io/@nano_o/Java-vs-Python-%EC%8A%A4%ED%83%9D%ED%81%90-%EA%B5%AC%ED%98%84-Deque-%EA%B8%B0%EC%A4%80</link>
            <guid>https://velog.io/@nano_o/Java-vs-Python-%EC%8A%A4%ED%83%9D%ED%81%90-%EA%B5%AC%ED%98%84-Deque-%EA%B8%B0%EC%A4%80</guid>
            <pubDate>Wed, 30 Sep 2026 15:21:09 GMT</pubDate>
            <description><![CDATA[<p>코딩 테스트에서 스택과 큐는 <strong>자바는 <code>ArrayDeque</code>, 파이썬은 <code>collections.deque</code></strong> 하나로 모두 해결할 수 있다.
헷갈리는 건 딱 두 가지다. <strong>꺼내는 방향</strong>과 <strong>비어 있을 때의 동작</strong></p>
<hr>
<h2 id="0-선언">0. 선언</h2>
<pre><code class="language-java">// Java
import java.util.ArrayDeque;
import java.util.Deque;

Deque&lt;Integer&gt; stack = new ArrayDeque&lt;&gt;();
Deque&lt;Integer&gt; queue = new ArrayDeque&lt;&gt;();   // Queue&lt;Integer&gt; queue = new ArrayDeque&lt;&gt;(); 도 가능</code></pre>
<pre><code class="language-python"># Python
from collections import deque

stack = deque()          # 스택은 그냥 list를 써도 된다: stack = []
queue = deque([1, 2, 3]) # 리스트/튜플 등 iterable로 바로 초기화 가능</code></pre>
<h3 id="선언-시-체크포인트">선언 시 체크포인트</h3>
<ul>
<li><code>Deque</code>는 <strong>인터페이스</strong>라서 <code>new Deque&lt;&gt;()</code>는 불가능하다. 구현체 <code>ArrayDeque</code> 또는 <code>LinkedList</code>를 쓴다.</li>
<li>코테에서는 <strong><code>ArrayDeque</code></strong> 를 쓰자. <code>LinkedList</code>보다 빠르고 메모리 효율이 좋다. 레거시 <code>Stack</code> 클래스도 쓸 이유가 없다.</li>
<li><code>ArrayDeque</code>에는 <strong><code>null</code>을 넣을 수 없다</strong> (<code>NullPointerException</code>).</li>
<li>파이썬 <code>deque</code>는 타입을 명시하지 않으므로 숫자·문자열·튜플 등을 섞어 넣을 수 있다.</li>
<li>파이썬에서 큐를 <code>list</code>로 만들고 <code>pop(0)</code>을 쓰면 <strong>O(N)</strong> 이다. 큐는 반드시 <code>deque</code>의 <code>popleft()</code>(O(1))를 쓴다.</li>
</ul>
<hr>
<h2 id="1-스택-stack-lifo-후입선출">1. 스택 (Stack, LIFO: 후입선출)</h2>
<blockquote>
<p>자바는 <strong>맨 앞(왼쪽)</strong>, 파이썬은 <strong>맨 뒤(오른쪽)</strong> 이 입구이자 출구</p>
</blockquote>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바 (<code>Deque</code>)</th>
<th>파이썬 (<code>deque</code>)</th>
<th>조작 위치</th>
</tr>
</thead>
<tbody><tr>
<td>넣기</td>
<td><code>dq.push(x)</code></td>
<td><code>dq.append(x)</code></td>
<td>자바: 맨 앞 / 파이썬: 맨 뒤</td>
</tr>
<tr>
<td>꺼내기</td>
<td><code>dq.pop()</code></td>
<td><code>dq.pop()</code></td>
<td>자바: 맨 앞 / 파이썬: 맨 뒤</td>
</tr>
<tr>
<td>최상단 확인</td>
<td><code>dq.peek()</code> / <code>dq.peekFirst()</code></td>
<td><code>dq[-1]</code></td>
<td>자바: 맨 앞 / 파이썬: 맨 뒤</td>
</tr>
<tr>
<td>비었는지 확인</td>
<td><code>dq.isEmpty()</code></td>
<td><code>not dq</code></td>
<td>-</td>
</tr>
<tr>
<td>크기</td>
<td><code>dq.size()</code></td>
<td><code>len(dq)</code></td>
<td>-</td>
</tr>
</tbody></table>
<pre><code class="language-java">Deque&lt;Integer&gt; stack = new ArrayDeque&lt;&gt;();
stack.push(1); stack.push(2); stack.push(3);   // [3, 2, 1]
stack.peek();  // 3
stack.pop();   // 3</code></pre>
<pre><code class="language-python">stack = deque()
stack.append(1); stack.append(2); stack.append(3)  # [1, 2, 3]
stack[-1]      # 3
stack.pop()    # 3</code></pre>
<p>두 코드 모두 <strong>마지막에 넣은 3이 먼저 나온다.</strong> 내부 저장 순서만 반대일 뿐 동작은 같다.</p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/30035a7d-4a17-47ae-bdd1-f5cd7003539c/image.png" alt=""></p>
<hr>
<h2 id="2-큐-queue-fifo-선입선출">2. 큐 (Queue, FIFO: 선입선출)</h2>
<blockquote>
<p>두 언어 모두 <strong>맨 뒤로 넣고, 맨 앞에서 꺼낸다.</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th>기능</th>
<th>자바 (<code>Deque</code>)</th>
<th>파이썬 (<code>deque</code>)</th>
<th>조작 위치</th>
</tr>
</thead>
<tbody><tr>
<td>넣기</td>
<td><code>dq.offer(x)</code> / <code>dq.add(x)</code></td>
<td><code>dq.append(x)</code></td>
<td>두 언어 모두 맨 뒤</td>
</tr>
<tr>
<td>꺼내기</td>
<td><code>dq.poll()</code> / <code>dq.pollFirst()</code></td>
<td><code>dq.popleft()</code></td>
<td>두 언어 모두 맨 앞</td>
</tr>
<tr>
<td>맨 앞 확인</td>
<td><code>dq.peek()</code> / <code>dq.peekFirst()</code></td>
<td><code>dq[0]</code></td>
<td>두 언어 모두 맨 앞</td>
</tr>
<tr>
<td>비었는지 확인</td>
<td><code>dq.isEmpty()</code></td>
<td><code>not dq</code></td>
<td>-</td>
</tr>
<tr>
<td>크기</td>
<td><code>dq.size()</code></td>
<td><code>len(dq)</code></td>
<td>-</td>
</tr>
</tbody></table>
<pre><code class="language-java">Deque&lt;Integer&gt; queue = new ArrayDeque&lt;&gt;();
queue.offer(1); queue.offer(2); queue.offer(3);  // [1, 2, 3]
queue.peek();  // 1
queue.poll();  // 1</code></pre>
<pre><code class="language-python">queue = deque()
queue.append(1); queue.append(2); queue.append(3)  # [1, 2, 3]
queue[0]         # 1
queue.popleft()  # 1</code></pre>
<p><img src="https://velog.velcdn.com/images/nano_o/post/0b3e4028-c8f4-41ff-9064-e34db0d469c9/image.png" alt=""></p>
<hr>
<h2 id="3-비어-있을-때-동작-에러-처리">3. 비어 있을 때 동작 (에러 처리)</h2>
<p>가장 많이 실수하는 부분이다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>자바</th>
<th>파이썬</th>
</tr>
</thead>
<tbody><tr>
<td>꺼내기</td>
<td><code>poll()</code> → <strong><code>null</code> 반환</strong><br><code>pop()</code>, <code>remove()</code> → <strong><code>NoSuchElementException</code></strong></td>
<td><code>pop()</code>, <code>popleft()</code> → <strong><code>IndexError</code></strong></td>
</tr>
<tr>
<td>확인</td>
<td><code>peek()</code> → <strong><code>null</code> 반환</strong><br><code>element()</code>, <code>getFirst()</code> → <strong>예외</strong></td>
<td><code>dq[0]</code>, <code>dq[-1]</code> → <strong><code>IndexError</code></strong></td>
</tr>
</tbody></table>
<ul>
<li><strong>자바</strong>: <code>poll()</code> / <code>peek()</code> 계열은 <code>null</code>을 돌려주므로 비교적 안전하다. 단, <strong>스택의 <code>pop()</code>은 예외를 던진다.</strong> 꺼내기 전에 <code>isEmpty()</code>로 확인하는 습관을 들이자.</li>
<li><strong>파이썬</strong>: 비어 있으면 무조건 예외가 난다. 실행 전에 <code>if dq:</code> 로 확인한다.</li>
</ul>
<pre><code class="language-java">while (!stack.isEmpty()) {
    int top = stack.pop();
}</code></pre>
<pre><code class="language-python">while dq:
    top = dq.pop()</code></pre>
<hr>
<h2 id="4-한눈에-보는-전체-치트시트">4. 한눈에 보는 전체 치트시트</h2>
<table>
<thead>
<tr>
<th>용도</th>
<th>동작</th>
<th>자바 (<code>ArrayDeque</code>)</th>
<th>파이썬 (<code>deque</code>)</th>
</tr>
</thead>
<tbody><tr>
<td>스택</td>
<td>넣기</td>
<td><code>push(x)</code></td>
<td><code>append(x)</code></td>
</tr>
<tr>
<td>스택</td>
<td>꺼내기</td>
<td><code>pop()</code></td>
<td><code>pop()</code></td>
</tr>
<tr>
<td>스택</td>
<td>확인</td>
<td><code>peek()</code></td>
<td><code>dq[-1]</code></td>
</tr>
<tr>
<td>큐</td>
<td>넣기</td>
<td><code>offer(x)</code></td>
<td><code>append(x)</code></td>
</tr>
<tr>
<td>큐</td>
<td>꺼내기</td>
<td><code>poll()</code></td>
<td><code>popleft()</code></td>
</tr>
<tr>
<td>큐</td>
<td>확인</td>
<td><code>peek()</code></td>
<td><code>dq[0]</code></td>
</tr>
<tr>
<td>덱</td>
<td>앞에 넣기</td>
<td><code>offerFirst(x)</code></td>
<td><code>appendleft(x)</code></td>
</tr>
<tr>
<td>덱</td>
<td>뒤에서 꺼내기</td>
<td><code>pollLast()</code></td>
<td><code>pop()</code></td>
</tr>
<tr>
<td>덱</td>
<td>맨 뒤 확인</td>
<td><code>peekLast()</code></td>
<td><code>dq[-1]</code></td>
</tr>
<tr>
<td>공통</td>
<td>비었는지</td>
<td><code>isEmpty()</code></td>
<td><code>not dq</code></td>
</tr>
<tr>
<td>공통</td>
<td>크기</td>
<td><code>size()</code></td>
<td><code>len(dq)</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="💡-이것만-기억하자">💡 이것만 기억하자</h2>
<ol>
<li><strong><code>pop()</code>의 방향이 다르다.</strong>
자바 <code>pop()</code>은 <strong>맨 앞(왼쪽)</strong>, 파이썬 <code>pop()</code>은 <strong>맨 뒤(오른쪽)</strong> 에서 꺼낸다.</li>
<li><strong>큐는 둘 다 뒤로 넣고 앞에서 꺼낸다.</strong>
자바 <code>offer</code> / <code>poll</code>, 파이썬 <code>append</code> / <code>popleft</code>.</li>
<li><strong>비어 있을 때가 다르다.</strong>
자바 <code>poll()</code> / <code>peek()</code>은 <code>null</code>, 파이썬은 <code>IndexError</code>. 자바 <code>pop()</code>도 예외가 나니 둘 다 <strong>꺼내기 전에 비었는지 확인</strong>하자.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[TRPG 동아리 디스코드 서버를 처음부터 설계했다 (4) — 오라클 무료 티어, 그리고 되돌린 기능들]]></title>
            <link>https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-4-%EC%98%A4%EB%9D%BC%ED%81%B4-%EB%AC%B4%EB%A3%8C-%ED%8B%B0%EC%96%B4-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%90%98%EB%8F%8C%EB%A6%B0-%EA%B8%B0%EB%8A%A5%EB%93%A4</link>
            <guid>https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-4-%EC%98%A4%EB%9D%BC%ED%81%B4-%EB%AC%B4%EB%A3%8C-%ED%8B%B0%EC%96%B4-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%90%98%EB%8F%8C%EB%A6%B0-%EA%B8%B0%EB%8A%A5%EB%93%A4</guid>
            <pubDate>Fri, 28 Aug 2026 09:20:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>1편 — <a href="https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-1-%ED%8F%AC%EB%9F%BC-%EC%B1%84%EB%84%90">서버 설계: 포럼 채널</a>
2편 — <a href="https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-2-%EB%AA%85%EB%A0%B9%EC%96%B4%EB%8A%94-%EB%8B%A8%EC%88%9C%ED%95%98%EA%B2%8C">봇 설계: 명령어는 단순하게</a>
3편 — <a href="https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-3-%EC%97%90%EB%9F%AC%EB%8F%84-%EC%95%88-%EB%82%98%EB%8A%94-%EB%B2%84%EA%B7%B8%EB%93%A4">삽질 모음: 에러도 안 나는 버그들</a>
4편 — 배포와 회고 ← 지금 글</p>
</blockquote>
<p>여태 로컬에서 돌려서 봇은 배포가 아직...인 상태였다. 스펙 정하기도 꽤 걸렸는데, 오늘은 배포 이야기를 해보고자 한다.</p>
<h2 id="왜-오라클-무료-티어인가">왜 오라클 무료 티어인가</h2>
<p>선택지를 놓고 봤다.</p>
<table>
<thead>
<tr>
<th></th>
<th>비용</th>
<th>문제</th>
</tr>
</thead>
<tbody><tr>
<td>Fly.io / Railway</td>
<td>소액이지만 유료 전환</td>
<td>동아리 봇에 매달 과금은 부담</td>
</tr>
<tr>
<td>라즈베리파이</td>
<td>전기값만</td>
<td>집에 놀고 있는 게 없었다</td>
</tr>
<tr>
<td><strong>오라클 클라우드 무료 티어</strong></td>
<td><strong>0원, 기한 없음</strong></td>
<td>가입이 까다롭다</td>
</tr>
</tbody></table>
<p>오라클 Always Free는 기한이 없다. 30일 크레딧이 아니라 계속 무료인 인스턴스를 준다. 램 512MB면 충분한 봇에는 과분한 사양이다.</p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/b56af909-f71f-4530-bc95-0cbfee8c66db/image.png" alt=""></p>
<p>대신 가입할 때 <strong>결제 카드 등록</strong>이 필요하다(무료 티어만 쓰면 청구되지는 않는다). 그리고 지역·주소 입력이 꽤 깐깐하다. 리전은 한 번 정하면 못 바꾸니 가까운 곳으로 했다. <del>(한국이 없어서 도쿄로 했다. 회원가입만 30분 걸렸다. 그리고 로그인 할 때마다 전용 앱으로 인증도 해야 한다. 매우 귀찮)</del></p>
<h2 id="1-인스턴스-만들기--고난의-시작">[1] 인스턴스 만들기 — 고난의 시작</h2>
<h3 id="문제-①-out-of-capacity">문제 ① Out of capacity</h3>
<p>Always Free로 받을 수 있는 사양은 둘이다.</p>
<ul>
<li><code>VM.Standard.A1.Flex</code> — ARM(Ampere), 최대 4코어 24GB</li>
<li><code>VM.Standard.E2.1.Micro</code> — AMD, 1코어 1GB</li>
</ul>
<p>당연히 A1을 고르려고 했다. 스펙이 압도적으로 좋다.</p>
<pre><code>API Error: Out of capacity for shape VM.Standard.A1.Flex
in availability domain AD-1</code></pre><p><strong>A1은 인기가 많아서 사용이 불가했다.ㅠㅠ 그래서 그냥 E2.1.Micro로 갔다.</strong> 1코어 1GB인데, Node 프로세스 하나에 SQLite 한 파일 돌리는 데는 차고 넘친다. <del>(실제로 지금 메모리 95MB 쓴다. 야 호 ~)</del></p>
<h3 id="문제-②-공용-ip-토글">문제 ② 공용 IP 토글</h3>
<p>인스턴스 생성 화면의 네트워킹 항목에서 &quot;공용 IPv4 주소 할당&quot; 토글이 <strong>비활성화</strong>돼 있었다. <del>(근데 진짜 클릭이 막혀 있었다...)</del></p>
<p>아래에 이런 안내가 있었다.</p>
<blockquote>
<p>There are additional options available when you use the Networking pages...</p>
</blockquote>
<p>원인은 VCN 설정이었다. <strong>공용 IP를 붙이려면 서브넷이 퍼블릭 서브넷이어야 한다.</strong> 기존에 잡힌 서브넷이 프라이빗이면 토글 자체가 잠긴다.</p>
<p>해결은 네트워킹 항목에서:</p>
<ol>
<li><strong>Create new virtual cloud network</strong> 선택</li>
<li><strong>Create new public subnet</strong> 선택</li>
<li>CIDR은 기본값 <code>10.0.0.0/24</code> 그대로</li>
</ol>
<p>분명 하라는대로 했는데, 토글 버튼 클릭이 막혀 있었다. <del>젠장!</del> 그래서 일단 인스턴스를 먼저 만들어버렸다. 만들고 나니 당연하게도 공용 IP가 <code>-</code> 로 비어 있었다.</p>
<p><strong>다행히 나중에 붙일 수 있다!</strong></p>
<pre><code>인스턴스 → Resources → Attached VNICs → (VNIC 클릭)
→ IPv4 Addresses → (주소 옆 ⋮) → Edit
→ Public IP Type: Ephemeral IP 선택 → Update</code></pre><p>Ephemeral IP는 인스턴스를 재부팅해도 유지되고, 인스턴스를 완전히 종료(terminate)해야 사라진다. 고정 IP가 필요하면 Reserved IP를 쓰면 되는데, 봇 하나 돌리는 데는 Ephemeral로 충분했다. <strong>뭐 어찌저찌 완료!!</strong></p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/54f466e6-c0d5-40d5-9496-4cedced46e32/image.png" alt=""></p>
<h2 id="2-ssh-키">[2] SSH 키</h2>
<p>인스턴스를 만들 때 SSH 공개키를 넣어야 한다. 미리 만들어두는 게 편하다.</p>
<pre><code class="language-bash">ssh-keygen -t ed25519 -f ~/.ssh/oracle -C &quot;oracle-seogi&quot;
cat ~/.ssh/oracle.pub    # ← 이 내용을 붙여넣는다</code></pre>
<p><code>~/.ssh/oracle</code> (확장자 없는 쪽)이 <strong>개인키</strong>다. 이건 절대 어디에도 올리면 안 된다. 깃허브에도, 블로그에도...</p>
<p>접속:</p>
<pre><code class="language-bash">ssh -i ~/.ssh/oracle ubuntu@&lt;공용IP&gt;</code></pre>
<p>Ubuntu 이미지의 기본 사용자는 <code>ubuntu</code> 다. <code>root</code> 로는 직접 못 들어간다.</p>
<h2 id="3-서버-세팅">[3] 서버 세팅</h2>
<pre><code class="language-bash">sudo apt update &amp;&amp; sudo apt upgrade -y

# Node 22
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

# better-sqlite3 를 컴파일하려면 필요
sudo apt install -y build-essential python3</code></pre>
<p>마지막 줄이 없으면 <code>npm install</code> 이 중간에 동작하지 않는다. <code>better-sqlite3</code> 는 네이티브 모듈이라 설치할 때 C++ 컴파일이 돌고, 그러려면 <code>build-essential</code> 과 <code>python3</code>(node-gyp)이 있어야 한다.</p>
<p>미리 깔아두는 게 낫다. 에러 메시지가 <code>gyp ERR!</code> 로 시작하는 긴 로그라서, 처음 보면 원인을 찾는 데 시간이 든다.</p>
<h2 id="4-파일-옮기기">[4] 파일 옮기기</h2>
<p>rsync로 보냈다.</p>
<pre><code class="language-bash">rsync -avz --delete \
  -e &quot;ssh -i ~/.ssh/oracle&quot; \
  --exclude &#39;node_modules&#39; \
  --exclude &#39;data/&#39; \
  --exclude &#39;.DS_Store&#39; \
  ~/Downloads/discord-trpg-setup/seogi/ \
  ubuntu@&lt;공용IP&gt;:~/seogi/</code></pre>
<p>제외 항목마다 이유가 있다.</p>
<ul>
<li><strong><code>node_modules</code></strong> — 네이티브 모듈이 있어서 맥에서 빌드한 걸 리눅스로 보내면 안 된다. 서버에서 <code>npm install</code> 을 다시 해야 한다.</li>
<li><strong><code>data/</code></strong> — 여기에 SQLite 파일이 있다. <code>--delete</code> 를 쓰고 있으므로 이걸 빼지 않으면 <strong>서버의 실제 데이터가 로컬 버전으로 덮인다.</strong></li>
<li><strong><code>.DS_Store</code></strong> — <del>맥 부스러기...</del></li>
</ul>
<p><code>--delete</code> 는 로컬에서 지운 파일을 서버에서도 지운다. 편한데, 제외 목록을 잘못 쓰면 데이터가 날아간다. 이 조합은 한 번 확인하고 쓰는 게 좋다.</p>
<p>그리고 <code>.env</code> 와 <code>service-account.json</code> 은 rsync로 보내되, <strong>git에는 절대 올리지 않는다.</strong> <code>.gitignore</code> 확인.</p>
<pre><code class="language-bash">ssh -i ~/.ssh/oracle ubuntu@&lt;공용IP&gt;
cd ~/seogi
npm install          # 여기서 better-sqlite3 컴파일이 돈다 (1~2분)
npm run deploy       # 슬래시 명령어 등록
node src/index.js    # 일단 손으로 띄워서 확인</code></pre>
<p><code>▶ Mr. Clerk#1003 로 로그인했습니다.</code> 가 뜨면 성공이다.</p>
<h2 id="5-pm2-사용하기">[5] pm2 사용하기</h2>
<p><code>node src/index.js</code> 로 띄우면 SSH를 끊는 순간 또 문제가 생긴다. 그리고 봇이 crash하면 그대로 끝이다.</p>
<pre><code class="language-bash">sudo npm install -g pm2

cd ~/seogi
pm2 start src/index.js --name seogi
pm2 save                # 지금 목록을 저장
pm2 startup             # 부팅 시 자동 시작 설정</code></pre>
<p><strong><code>pm2 startup</code> 을 치면 이런 걸 출력하는데,</strong></p>
<pre><code>sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu</code></pre><p><strong>이걸 복사해서 한 번 실행해야 한다.</strong> pm2가 대신 실행해주지 않고 명령어만 보여준다. 한 번 실행하면 systemd 서비스가 등록되고, 그 뒤로는 잊어도 된다.</p>
<p>순서가 하나 있다. <code>pm2 save</code> 로 목록을 저장한 다음에 <code>startup</code> 을 해야 한다. 반대로 하면 재부팅 후 빈 목록으로 뜬다.</p>
<pre><code class="language-bash">pm2 status</code></pre>
<p>위의 명령어로 상태를 확인하면, 아래와 같이 정보가 나온다.</p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/e4cc3aaa-7ff0-475f-b8f0-ac808837f4a6/image.png" alt=""></p>
<p>1GB 인스턴스에 95MB라니~ 여유가 많다!</p>
<p>자주 쓰는 것들:</p>
<pre><code class="language-bash">pm2 logs seogi          # 실시간 로그
pm2 logs seogi --lines 100
pm2 restart seogi
pm2 monit               # CPU·메모리 실시간</code></pre>
<h2 id="6-배포를-한-줄로">[6] 배포를 한 줄로</h2>
<p>코드를 고칠 때마다 rsync 명령어를 다시 치는 건 금방 귀찮아진다. 그래서 스크립트로 만들었다. <del>(클로드 최고)</del></p>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

HOST=&quot;ubuntu@&lt;공용IP&gt;&quot;
KEY=&quot;$HOME/.ssh/oracle&quot;
SRC=&quot;$(cd &quot;$(dirname &quot;$0&quot;)&quot; &amp;&amp; pwd)/seogi/&quot;

rsync -avz --delete -e &quot;ssh -i $KEY&quot; \
  --exclude &#39;node_modules&#39; --exclude &#39;data/&#39; \
  --exclude &#39;_to_delete&#39; --exclude &#39;.DS_Store&#39; \
  &quot;$SRC&quot; &quot;$HOST:~/seogi/&quot;

# 옵션 처리
for arg in &quot;$@&quot;; do
  case &quot;$arg&quot; in
    --deps) ssh -i &quot;$KEY&quot; &quot;$HOST&quot; &quot;cd ~/seogi &amp;&amp; npm install&quot; ;;
    --cmds) ssh -i &quot;$KEY&quot; &quot;$HOST&quot; &quot;cd ~/seogi &amp;&amp; npm run deploy&quot; ;;
  esac
done

ssh -i &quot;$KEY&quot; &quot;$HOST&quot; &quot;pm2 restart seogi &amp;&amp; pm2 status&quot;</code></pre>
<pre><code class="language-bash">./deploy.sh            # 코드만
./deploy.sh --deps     # 패키지가 바뀐 경우
./deploy.sh --cmds     # 슬래시 명령어가 바뀐 경우</code></pre>
<p>백업도 하나 만들었다. 사실상 지켜야 할 건 SQLite 파일 하나다.</p>
<pre><code class="language-bash">DEST=&quot;backups/$(date +%Y%m%d-%H%M)&quot;
mkdir -p &quot;$DEST&quot;
scp -i &quot;$KEY&quot; &quot;$HOST:~/seogi/data/seogi.db&quot; &quot;$DEST/&quot;
scp -i &quot;$KEY&quot; &quot;$HOST:~/seogi/config.json&quot; &quot;$DEST/&quot;</code></pre>
<h2 id="7-유지보수-체크리스트">[7] 유지보수 체크리스트</h2>
<p>한 달에 한 번쯤 하면 되는 것들:</p>
<pre><code class="language-bash">./backup.sh                              # DB 백업
sudo apt update &amp;&amp; sudo apt upgrade -y   # 서버 업데이트
sudo reboot                              # 커널이 올라갔으면</code></pre>
<p>pm2가 자동 시작으로 등록돼 있으니 재부팅해도 봇은 알아서 살아난다.</p>
<p>그리고 <strong>오라클 콘솔에 가끔 로그인해야 한다.</strong> 무료 티어는 장기간 미사용 시 리소스가 회수될 수 있다고 명시돼 있다...</p>
<hr>
<h1 id="회고">회고</h1>
<h2 id="만들었다가-되돌린-것들">만들었다가 되돌린 것들</h2>
<p>이번 프로젝트에서 제일 배운 게 이쪽이다. 기능을 넣는 것보다 <strong>뺀 결정들</strong>이 결과를 더 좋게 만들었다.</p>
<h3 id="①-캐릭터-자동완성">① 캐릭터 자동완성</h3>
<p>&quot;장기 캠페인이면 예전 캐릭터를 다시 쓸 텐데, 등록할 때 이미 있는 애인지 물어보면 좋겠다~&quot;</p>
<ul>
<li>과거 세션의 캐릭터 목록을 선택 메뉴로</li>
<li>→ 25개 한도에 걸림 (디스코드 선택 메뉴 제한)</li>
<li>→ 세션별로 먼저 고르고 그 안에서 캐릭터 고르는 2단계로</li>
<li>→ 구글 시트에 있는 캐릭터도 검색되게 자동완성 추가</li>
<li>→ 시트를 10분마다 읽어서 메모리에 캐싱</li>
</ul>
<p>기능은 다 동작했지만 문제가 있었다.</p>
<blockquote>
<p>2단계 선택은 클릭이 늘어난 만큼 더 헷갈렸고, 자동완성으로 불러온 값이 빈칸으로 오는 경우가 있었다. <strong>한 번 치고 마는 입력을 편하게 하려다 매번 하는 입력을 불편하게 만든 것</strong>이다.</p>
</blockquote>
<p>전부 걷어내고 [캐릭터 등록] 버튼 → 모달 두 칸으로 돌아갔다.</p>
<p>여기서 하나의 반성 포인트가 있다. 걷어낼 때 <code>services/sheets.js</code> 의 시트 인덱스 코드(<code>refreshSheetIndex</code>, <code>getSheetIndex</code>, <code>startSheetIndexJob</code>)를 <strong>호출만 끊고 함수는 안 지웠다.</strong> 지금은 아무도 안 부르는 시체 같은 코드만 남아 있다... (언젠가 정리하겠다.)</p>
<h3 id="②-참가자-한정-아카이브">② 참가자 한정 아카이브</h3>
<p>세션 기록을 참가자에게만 보이게 하는 옵션이다. 구현하려면 기록을 포럼 포스트가 아니라 비공개 스레드에 넣어야 했다.</p>
<blockquote>
<p><strong>기록보관소를 만드는 이유는 나중에 찾아보기 위해서인데, 접근 제한을 붙이면 찾기가 어려워져서 목적과 수단이 충돌해 버렸다.</strong></p>
</blockquote>
<p>그래서 기록은 항상 공개하되, 종료할 때 KP가 <strong>비밀을 어디까지 남길지</strong>를 고르게 했다.</p>
<p>이쪽이 훨씬 간단하고, 결과적으로 더 잘 맞는다.</p>
<h3 id="③-스레드-보관-→-삭제">③ 스레드 보관 → 삭제</h3>
<p>세션이 끝나면 스레드를 보관(archive)하는 게 기본값이었다. 그런데 세션 하나에 스레드가 5~6개씩 생긴다. 몇 달이면 목록이 스레드로 가득 찬다.</p>
<blockquote>
<p>로그와 이미지가 기록 글에 다 들어가니 원본 스레드가 남아 있을 이유가 없지 않나? (지금 생각해보면 당연하기도)</p>
</blockquote>
<p>기본값을 삭제로 바꾸면서 안전장치를 뒀다. 종료 패널에서 언제든 &#39;보관&#39;으로 바꿀 수 있고, 삭제를 고르면 확인 문구가 하나 더 뜬다. <strong>되돌릴 수 없는 동작은 기본값이더라도 눈에 잘 보이게 설계했다.</strong></p>
<p>그리고 기존 DB의 기본값도 바꿔야 해서 2편에서 얘기한 <code>PRAGMA user_version</code> 일회성 마이그레이션이 여기서 나왔다.</p>
<hr>
<h2 id="잘한-결정-셋">잘한 결정 셋</h2>
<p><strong>① [customId에 상태를 다 넣은 것]</strong> 처음에 이걸 안 했으면 배포할 때마다 신경 쓸 일이 생겼을 것이다.</p>
<p><strong>② [상태 전이를 한 파일에 몰아넣은 것]</strong> 기능을 추가할 때마다 &quot;이건 session.js에 있어야 하나?&quot;를 묻게 되는데, 이 질문 자체가 설계를 지켜줬다. 여러 파일에서 status를 건드리기 시작했으면 지금쯤 엉켰을 것 같다.</p>
<p><strong>③ [스크립트로 서버를 만든 것]</strong> 이게 제일 컸다. 구조를 바꾸는 비용이 싸지니까 <strong>바꿔볼 마음이 생긴다.</strong> 손으로 만들었으면 <code>#시나리오</code> 포럼 신설이나 태그 정리 같은 개편은 &quot;귀찮으니 그냥 두자&quot;로 끝났을 것이다.</p>
<h2 id="아직-안-만든-것">아직 안 만든 것</h2>
<p><del>없어도 서버는 완전히 돌아간다.</del></p>
<ul>
<li><code>/핸드아웃</code> — KP가 특정 PC의 비밀 스레드로 자료 전송</li>
<li><code>/비밀배포</code> — 비밀 카드 N장을 섞어 각자 1장씩 (인세인용)</li>
<li><code>/전적</code> — 플레이 횟수, KP 횟수, 룰별 통계. 데이터는 이미 DB에 다 쌓여서 쉽게 만들 수 있다.</li>
</ul>
<h2 id="마지막으로--화려함보다는-편리함이-우선이다">마지막으로 — 화려함보다는 편리함이 우선이다!</h2>
<p>블로그에서는 전반적으로 기술적인 얘기를 다루고 있지만, 사실 <strong>제일 중요한 것은 기능보다 이 서비스를 사용하는 사용자이다.</strong></p>
<p>사실 제일 열심히 만든 기능이 제일 먼저 폐기됐다. <del>(캐릭터 자동완성)</del> 25개 한도를 우회하려고 2단계 선택을 설계하고, 구글 시트를 캐싱하고, 검색까지 붙였다. 만들고 나서 좀 뿌듯했던 것 같은데, 몇 번 써보고 없앴다.</p>
<p>반대로 제일 반응이 좋았던 건 제목 자동으로 만들어주는 거랑 하루 전에 DM 한 통 가는 거였다. 코드로 치면 별거 아니지만...</p>
<p>이 둘 차이가 뭐였나 생각해 보면, 하나는 내가 만들고 싶었던 거고 하나는 쓰는 사람이 필요한 기능이었다.</p>
<p>결국 서버의 라이프 사이클이 유지되는 건 봇과 화려한 기능이 아니라 함께 해주는 사람들 덕분인 것 같다. <del>앞으로도 함께 해줘!!</del></p>
<hr>
<p>읽어주셔서 감사합니다. <strong>언제나 GM과 PL의 행복한 플레이를 응원합니다!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TRPG 동아리 디스코드 서버를 처음부터 설계했다 (3) — 에러도 안 나는 버그들]]></title>
            <link>https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-3-%EC%97%90%EB%9F%AC%EB%8F%84-%EC%95%88-%EB%82%98%EB%8A%94-%EB%B2%84%EA%B7%B8%EB%93%A4</link>
            <guid>https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-3-%EC%97%90%EB%9F%AC%EB%8F%84-%EC%95%88-%EB%82%98%EB%8A%94-%EB%B2%84%EA%B7%B8%EB%93%A4</guid>
            <pubDate>Fri, 28 Aug 2026 09:05:25 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>1편 — <a href="https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-1-%ED%8F%AC%EB%9F%BC-%EC%B1%84%EB%84%90">서버 설계: 포럼 채널</a>
2편 — <a href="https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-2-%EB%AA%85%EB%A0%B9%EC%96%B4%EB%8A%94-%EB%8B%A8%EC%88%9C%ED%95%98%EA%B2%8C">봇 설계: 명령어는 단순하게</a>
3편 — 에러도 안 나는 버그들 ← 지금 글
4편 — 배포와 회고: 오라클 무료 티어, 그리고 되돌린 기능들</p>
</blockquote>
<p>이번 편은 만들면서 실제로 막혔던 것들이다. 공통점이 하나 있는데, <strong>대부분 에러가 안 났다. ㅠㅠ</strong> 스택 트레이스가 찍히는 버그는 오히려 쉽다. 어려운 건 아무 일도 안 일어나거나, 조용히 틀린 결과가 나오는 쪽이다...</p>
<hr>
<h2 id="1-텅-빈-세션-로그">[1] 텅 빈 세션 로그</h2>
<h3 id="증상">증상</h3>
<p>첫 테스트 세션을 마치고 [세션 종료]를 눌렀다. <code>#세션-기록</code> 에 포스트는 잘 만들어졌다. 로그 파일도 첨부됐다. 그런데 열어보니 이랬다.</p>
<pre><code class="language-markdown"># [CoC] 테스트 세션

- 룰: CoC
- KP: 오일러
- 참가: 뉴턴, 보일

---

## 플레이 로그

**뉴턴**
**보일**
**뉴턴**</code></pre>
<p>이름만 있고 <strong>내용이 없다!!!</strong> 메시지 개수와 순서도 맞지만 본문만 빈 문자열로 저장되었다. 에러는 딱히 뜨지 않았고, 종료도 정상적으로 끝났다고 나왔다.</p>
<h3 id="원인">원인</h3>
<p><code>GatewayIntentBits.MessageContent</code> 를 코드에는 선언해뒀다.</p>
<pre><code class="language-js">const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMembers,
    GatewayIntentBits.MessageContent,   // ← 있음
  ],
});</code></pre>
<p>문제는 <strong>디스코드 개발자 포털에서도 따로 켜야 한다</strong>는 것이었다.</p>
<p><code>MESSAGE CONTENT INTENT</code> 는 특권 인텐트(Privileged Intent)라서 포털 → Bot 탭에서 토글을 켜야 실제로 부여된다. 코드 선언만으로는 안 된다.</p>
<p>그리고 안 켜져 있을 때는 너무한 게 <strong><code>message.content</code> 가 빈 문자열로 온다.</strong> 예외를 던지는 것도 아니고, 권한 오류가 오는 것도 아니라 그냥 빈 값이다.</p>
<h3 id="해결">해결</h3>
<p>포털에서 토글을 켰다.</p>
<p><strong>특권 인텐트는 코드와 포털 양쪽에 있어야 한다.</strong> 그리고 없을 때 조용히 빈 값이 오므로, 남의 메시지 본문을 읽는 봇이라면 이걸 제일 먼저 확인해야 한다.</p>
<hr>
<h2 id="2-중복-기록">[2] 중복 기록</h2>
<h3 id="증상-1">증상</h3>
<p>인텐트를 켜고 다시 테스트했다. 이번엔 내용이 들어왔는데, 로그가 이상했다.</p>
<p>비밀 스레드 내용이 <strong>두 번씩</strong> 나왔다. (1. 플레이 로그, 2. 비밀)
거기에 봇이 쓴 안내 메시지(&quot;## 🔓 비밀 공개&quot; 같은 것)까지 로그에 섞여 있었다.</p>
<h3 id="원인-1">원인</h3>
<p>종료 처리 순서를 이렇게 짰던 게 문제였다.</p>
<pre><code class="language-js">// 처음 (틀림)
await revealSecrets(main, secrets);        // 1. 비밀을 전체 스레드에 공개
const mainMsgs = await fetchAll(main);     // 2. 전체 스레드 로그 추출</code></pre>
<p>비밀을 전체 스레드에 <strong>먼저 풀고 나서</strong> 그 스레드의 로그를 떴다. 그러니 방금 푼 비밀이 전체 스레드 메시지에 포함돼서 로그에 들어가고, 그 뒤에 &#39;비밀&#39; 절에서 한 번 더 들어간다.</p>
<p>봇 메시지가 섞인 것도 같은 이유다. 봇이 쓴 &quot;🔓 비밀 공개&quot; 헤더가 로그 추출 시점에는 이미 스레드에 존재했다.</p>
<h3 id="해결-1">해결</h3>
<p>순서를 뒤집고, 봇 자신의 메시지를 걸러냈다.</p>
<pre><code class="language-js">const selfId = client.user.id;

/* 1. 로그를 먼저 뜬다 */
let mainMsgs = [];
if (main) mainMsgs = await fetchAll(main);

const secretSets = [];
for (const p of joined) {
  if (!p.secret_thread_id) continue;
  const t = await client.channels.fetch(p.secret_thread_id);
  secretSets.push({ userId: p.user_id, msgs: await fetchAll(t) });
}

/* 2. 첨부 수집 — 봇 메시지 제외 */
const attachments = [
  ...collectAttachments(mainMsgs.filter((m) =&gt; m.author.id !== selfId), &#39;main&#39;),
  ...secretSets.flatMap((x) =&gt;
    collectAttachments(x.msgs.filter((m) =&gt; m.author.id !== selfId), &#39;secret&#39;)),
].sort((a, b) =&gt; a.at - b.at);

/* 3. 이제 공개한다 */
if (main &amp;&amp; optSecret === &#39;reveal&#39;) { ... }</code></pre>
<p>코드에 주석으로 이유를 박아뒀다. 나중에 누가(혹은 내가) 리팩토링하면서 순서를 바꿀 수 있으니까...</p>
<pre><code class="language-js">/**
 * 순서가 중요
 *   1) 로그를 먼저 뜬다 (비밀을 뿌린 뒤에 뜨면 중복)
 *   2) 기록 글을 만들고
 *   3) 이미지를 기록 글에 다시 업로드
 */</code></pre>
<p><strong>부수효과가 있는 작업과 읽기 작업이 섞이면 순서가 의미를 갖는다.</strong>
<del>(당연한 말인데, 기능이 여덟 단계쯤 되면 눈에 잘 안 들어온다.)</del></p>
<hr>
<h2 id="3-discordapierror10008-unknown-message">[3] <code>DiscordAPIError[10008] Unknown Message</code></h2>
<h3 id="증상-2">증상</h3>
<p>스레드 처리 기본값을 &#39;삭제&#39;로 바꾸고 나서 생긴 문제다. 세션 종료가 정상적으로 다 끝나는데, 마지막에 이게 뜬다.</p>
<pre><code>DiscordAPIError[10008]: Unknown Message</code></pre><p>기록 글도 잘 만들어졌고, 시트에도 들어갔고, 스레드도 지워졌는데 말이다.</p>
<h3 id="원인-2">원인</h3>
<p>종료 확인 패널이 <strong>지워질 스레드 안에</strong> 떠 있었다.</p>
<p>흐름을 다시 보면:</p>
<ol>
<li>KP가 전체 스레드에서 [세션 종료]를 누른다</li>
<li>확인 패널이 나만 보이는(ephemeral) 메시지로 그 스레드에 뜬다</li>
<li>[이대로 종료]를 누른다</li>
<li>봇이 일을 하고... <strong>그 스레드를 지운다</strong></li>
<li>봇이 <code>interaction.editReply(&#39;종료했습니다&#39;)</code> 를 부른다</li>
<li>그 메시지가 있던 스레드가 없다 → <code>Unknown Message</code></li>
</ol>
<p>나만 보이는 메시지는 <strong>호스트 채널에 종속</strong>된다. 채널이 사라지면 같이 사라진다.</p>
<h3 id="해결-2">해결</h3>
<p>결과 보고를 DM으로 떨어지게 했다.</p>
<pre><code class="language-js">/**
 * 확인 패널은 세션 스레드 안에 떠 있는데, 스레드 삭제를 고르면 그 메시지가 함께 사라짐
 * 그럼 editReply 가 Unknown Message 로 실패하므로 DM 으로
 */
async function report(interaction, content) {
  try {
    await interaction.editReply({ content, components: [] });
  } catch {
    try {
      await interaction.user.send(content);
    } catch {
      console.log(&#39;[finish]&#39;, content);
    }
  }
}</code></pre>
<p><code>editReply</code> → 실패하면 DM → 그것도 실패하면(DM 차단) 콘솔 방식으로 3단 폴백이다.</p>
<hr>
<h2 id="4-실패한-밀담-추가">[4] 실패한 <code>/밀담 추가</code></h2>
<h3 id="증상-3">증상</h3>
<p><code>/밀담 추가</code> 로 사람을 넣으면 &quot;추가했습니다&quot; 는 뜨는데 그 사람이 스레드에 없는 문제가 있었다. 다른 하위 명령(만들기, 제거, 이름)은 다 정상적으로 동작하는데 말이다.</p>
<h3 id="원인--두-개였다">원인 — 두 개였다</h3>
<p><strong>첫째, 에러를 코드가 삼키고 있었다.</strong></p>
<pre><code class="language-js">const ok = await thread.members.add(uid).catch(() =&gt; false);   // ← 범인</code></pre>
<p><code>.catch(() =&gt; false)</code> 로 뭉개놓으니 왜 실패했는지 알 방법이 없었다. 실패를 실패로만 알고 이유를 모르는 상태...</p>
<p>이걸 풀었더니 진짜 원인이 나왔다.</p>
<p><strong>둘째, 잘못 설계한 부분이 등장했다.</strong> 추가하려던 사람이 그 채널을 볼 권한이 없었다! 디스코드는 <strong>채널을 못 보는 사람은 그 채널의 스레드에도 못 넣는다.</strong> 당연한 규칙인데 에러 코드(50001 Missing Access)만 보고는 알기 어려웠다... 힝</p>
<h3 id="해결-3">해결</h3>
<p>에러를 사람 말로 풀어서 보여주게 했다.</p>
<pre><code class="language-js">const results = [];
for (const uid of picked) {
  try {
    await thread.members.add(uid);
    results.push(`✓ &lt;@${uid}&gt;`);
  } catch (e) {
    const why =
      e.code === 50001 ? &#39;그 사람이 이 채널을 볼 권한이 없습니다&#39;
    : e.code === 50013 ? &#39;봇 권한이 부족합니다&#39;
    : e.message;
    results.push(`✗ &lt;@${uid}&gt; — ${why}`);
  }
}</code></pre>
<p>보관된 스레드에 추가하려 할 때도 실패해서, 자동으로 깨우는 것도 넣었다.</p>
<pre><code class="language-js">if (thread.archived) await thread.setArchived(false).catch(() =&gt; {});</code></pre>
<p><strong>교훈은 에러의 출처를 알아내는 것도 정말 중요하다는 것이다.</strong> <code>.catch(() =&gt; false)</code> 를 쓸 때는 그 자리에서 왜 실패했는지 알 필요가 정말 없는지 한 번 생각하는 게 좋다.</p>
<hr>
<h2 id="5-깨지는-이미지">[5] 깨지는 이미지</h2>
<h3 id="증상-4">증상</h3>
<p>이건 증상이 발생하기 전에 발견했다. 로그에 이미지를 링크로 넣는 코드를 짜다가 디스코드 첨부 URL을 보고 이상해서 뜯어봤다.</p>
<pre><code>https://cdn.discordapp.com/attachments/.../scene.png?ex=...&amp;is=...&amp;hm=...</code></pre><p><code>ex</code>, <code>is</code>, <code>hm</code>. 만료(expire), 발급(issued), 서명(hmac)이다.</p>
<h3 id="원인-3">원인</h3>
<p>디스코드 CDN 첨부 URL에는 <strong>서명된 만료 시간이 붙는다.</strong> 대략 하루면 만료된다. 그리고 원본 메시지가 있는 채널이나 스레드가 삭제되면 파일 자체가 사라진다.</p>
<p>기록보관소를 만드는 게 목적인데, 로그에 링크만 적어두면 <strong>며칠 뒤에 전부 깨진 이미지가 된다.</strong> 심지어 우리는 종료 시 스레드를 지우는 게 기본값이라 고민이 많았다.</p>
<h3 id="해결-4">해결</h3>
<p>파일을 내려받아서 <strong>기록 글에 다시 올린다.</strong></p>
<pre><code class="language-js">export async function rehostAttachments(thread, attachments) {
  const files = [];
  for (const a of attachments) {
    if (a.size &gt; 9 * 1024 * 1024) { skipped.push(a); continue; }   // 9MB 한도
    const buf = await download(a.url);
    files.push({ attachment: buf, name: `${String(a.no).padStart(3, &#39;0&#39;)}_${a.name}` });
    if (files.length &gt;= 100) break;
  }
  // 10개씩 나눠서 전송 (메시지당 첨부 10개 한도)
}</code></pre>
<p>그리고 로그 본문에는 번호로 짝을 맞춘다.</p>
<pre><code class="language-markdown">## 이미지

아래 번호는 이 기록 글에 함께 올려둔 파일과 짝이 맞습니다.

- `001` scene.png — 김도현
- `002` map.jpg — 이서연 · 🔒 비밀</code></pre>
<p>한도가 있긴 하다. 파일 하나 <strong>9MB</strong>(무료 서버 8MB 제한을 감안한 값), 한 메시지에 첨부 <strong>10개</strong></p>
<hr>
<h2 id="6-carl-bot-역할-버튼-노작동">[6] Carl-bot 역할 버튼 노작동</h2>
<h3 id="증상-5">증상</h3>
<p><code>#역할-알림설정</code> 에 룰별 알림 구독 버튼을 만들려고 Carl-bot 대시보드에서 Reaction Roles를 설정했다. 저장했다. 채널에 아무것도 안 생긴다.</p>
<p>대시보드에는 저장됐다고 나온다. 에러 메시지는 없다.</p>
<h3 id="원인-4">원인</h3>
<p><code>#역할-알림설정</code> 은 <strong>읽기 전용 채널</strong>이었다. <code>@everyone</code> 의 <code>SendMessages</code> 를 막아뒀다.</p>
<p>그런데 봇도 <code>@everyone</code> 에 속한다. <strong>글쓰기를 막으면 봇도 같이 막힌다.</strong></p>
<p>사람이 못 쓰게 하려던 건데 봇까지 막힌 것이고, 봇 입장에서는 메시지 전송이 거부된 것이라 대시보드에서는 알 방법이 없다.</p>
<h3 id="해결-5">해결</h3>
<p>봇 역할에만 예외를 여는 스크립트를 만들었다. 봇을 초대하면 자동으로 생기는 역할은 <code>managed</code> 플래그가 붙어 있어서 골라내기 쉽다.</p>
<pre><code class="language-js">const botRoles = guild.roles.cache.filter((r) =&gt; r.managed);

// @everyone 의 글쓰기가 막혀 있는 텍스트 채널만
const locked = guild.channels.cache.filter((c) =&gt; {
  if (c.type !== ChannelType.GuildText) return false;
  const ow = c.permissionOverwrites.cache.get(guild.roles.everyone.id);
  return ow?.deny.has(PermissionFlagsBits.SendMessages);
});

for (const ch of locked.values()) {
  for (const role of botRoles.values()) {
    await ch.permissionOverwrites.edit(role, {
      ViewChannel: true, SendMessages: true, EmbedLinks: true,
      AttachFiles: true, ReadMessageHistory: true, AddReactions: true,
      ManageMessages: true, CreatePublicThreads: true,
    });
  }
  await new Promise((r) =&gt; setTimeout(r, 400));   // 레이트 리밋 회피
}</code></pre>
<p>사람은 여전히 못 쓰고 봇만 예외다. 봇을 새로 추가할 때마다 한 번씩 돌리면 된다.</p>
<p><strong>읽기 전용 채널을 만들면 봇 예외를 같이 생각해야 한다.</strong> 이건 <code>#공지</code>, <code>#세션-일정</code> 에도 똑같이 해당됐다.</p>
<hr>
<h2 id="7-미디어-채널의-부재">[7] 미디어 채널의 부재</h2>
<h3 id="증상-6">증상</h3>
<p><code>#캐릭터-갤러리</code> 를 미디어 채널로 만들려고 했다. 미디어 채널은 포럼과 비슷한데 이미지 위주로 최적화된 채널 타입이다.</p>
<pre><code>✗ 실패: Cannot execute action on this channel type</code></pre><p><code>DiscordAPIError[50024]</code>.</p>
<h3 id="원인-5">원인</h3>
<p><code>ChannelType.GuildMedia</code> 는 discord.js에 정의돼 있고 API 문서에도 있는데, <strong>이 서버에서는 만들 수 없는 타입</strong>이었다. 미디어 채널은 서버 조건이나 롤아웃 상태에 따라 가용 여부가 갈린다.</p>
<p>에러 메시지가 &quot;이 채널 타입에는 이 작업을 할 수 없다&quot;라서 처음엔 코드 문제인 줄 알고 파라미터를 계속 바꿔봤다. 그게 아니라 <strong>애초에 만들 수 없는 것</strong>이었다.</p>
<h3 id="해결-6">해결</h3>
<p>포기하고 <strong>포럼 채널 + 갤러리 뷰</strong>로 갔다.</p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/1aa2560d-9c2f-4505-84ce-b069e541f412/image.png" alt=""></p>
<pre><code class="language-js">const gallery = await guild.channels.create({
  name: &#39;캐릭터-갤러리&#39;,
  type: ChannelType.GuildForum,
  defaultForumLayout: ForumLayoutType.GalleryView,   // ← 격자 썸네일
  defaultSortOrder: SortOrderType.LatestActivity,
});</code></pre>
<p>결과적으로 보기에는 거의 같다. 격자로 썸네일이 깔리고 태그도 쓸 수 있다. 오히려 태그를 쓸 수 있어서 나은 면도 있었다.</p>
<p><strong>API 문서에 있다고 이 서버에서 되는 건 아니다.</strong> 채널 타입, 기능 플래그 같은 건 서버 상태에 따라 갈린다.</p>
<hr>
<h2 id="8-봇-역할의-한계">[8] 봇 역할의 한계</h2>
<p>이건 겪기 전에 알고 대비한 건데, 그래도 적어둘 만하다.</p>
<p><strong>봇은 자기 역할보다 아래에 있는 역할만 만들고, 지우고, 부여할 수 있다.</strong></p>
<p><code>@세션-42</code> 같은 임시 역할을 만드는 봇인데, 봇 역할이 역할 목록에서 아래쪽에 있으면 역할 생성이나 부여가 <strong>조용히 실패한다.</strong> 파티 확정을 눌렀는데 스레드는 생기고 역할만 안 붙는 상태가 된다.</p>
<p>봇을 초대하면 그 역할은 기본적으로 목록 아래쪽에 생긴다. 그래서 서버 설정 → 역할에서 <strong>드래그로 올려줘야 한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/0d5a336f-9f41-4b04-bb22-91a845e28b5a/image.png" alt=""></p>
<p>파티 확정 코드에 실패를 수집하는 부분을 둔 것도 이 때문이다.</p>
<pre><code class="language-js">const report = { role: null, main: null, secrets: [], failed: [] };

for (const uid of memberIds) {
  try {
    const m = await guild.members.fetch(uid);
    await m.roles.add(role, &#39;세션 참가자&#39;);
  } catch (e) {
    report.failed.push(`역할 부여 실패: &lt;@${uid}&gt; (${e.message})`);
  }
}</code></pre>
<hr>
<h2 id="정리">[정리]</h2>
<p>여덟 개를 놓고 보니 패턴이 보인다.</p>
<table>
<thead>
<tr>
<th>패턴</th>
<th>사례</th>
</tr>
</thead>
<tbody><tr>
<td>권한이 없으면 빈 값이 온다</td>
<td>MESSAGE CONTENT 인텐트</td>
</tr>
<tr>
<td>권한이 없으면 아무 일도 안 일어난다</td>
<td>읽기 전용 채널의 봇, 역할 순서</td>
</tr>
<tr>
<td>코드가 에러를 삼킨다</td>
<td><code>.catch(() =&gt; false)</code></td>
</tr>
<tr>
<td>순서가 의미를 갖는데 안 보인다</td>
<td>로그 추출 vs 비밀 공개</td>
</tr>
<tr>
<td>외부 리소스에 유효기간이 있다</td>
<td>첨부 URL 만료</td>
</tr>
<tr>
<td>문서에 있어도 여기서는 안 된다</td>
<td>미디어 채널</td>
</tr>
</tbody></table>
<p>디스코드 API를 다루면서 제일 도움이 됐던 습관은 <strong>&quot;권한 문제일 수 있다&quot;를 항상 먼저 의심하는 것</strong>이었다. 코드가 맞는데 아무 일도 안 일어나면 열에 아홉은 권한이었다.</p>
<p>그리고 <code>.catch(() =&gt; {})</code> 를 쓸 때 잠깐 멈추고 생각을 해보자. 실패를 무시해도 되는 자리와 무시하면 안 되는 자리를 구분해야 한다.</p>
<hr>
<p>다음 편은 이걸 클라우드에 올린 얘기다. 오라클 무료 티어에서 원하는 사양을 찾고, 공용 IP 토글을 설정해야 한다. 그리고 만들었다가 되돌린 기능들에 대한 회고도 넣었다.</p>
<p>→ <strong>4편 — 배포와 회고: 오라클 무료 티어, 그리고 되돌린 기능들</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TRPG 동아리 디스코드 서버를 처음부터 설계했다 (2) — 명령어는 단순하게]]></title>
            <link>https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-2-%EB%AA%85%EB%A0%B9%EC%96%B4%EB%8A%94-%EB%8B%A8%EC%88%9C%ED%95%98%EA%B2%8C</link>
            <guid>https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-2-%EB%AA%85%EB%A0%B9%EC%96%B4%EB%8A%94-%EB%8B%A8%EC%88%9C%ED%95%98%EA%B2%8C</guid>
            <pubDate>Fri, 28 Aug 2026 08:57:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>1편 — <a href="https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-1-%ED%8F%AC%EB%9F%BC-%EC%B1%84%EB%84%90">서버 설계: 포럼 채널</a>
2편 — 봇 설계: 명령어는 단순하게 ← 지금 글
3편 — 삽질 모음: 에러도 안 나는 버그들
4편 — 배포와 회고: 오라클 무료 티어, 그리고 되돌린 기능들</p>
</blockquote>
<p>1편에서 서버 구조를 짰다. 이제 원칙 04 — <strong>정리는 사람이 하면 반드시 밀린다</strong> — 를 실제로 지킬 차례다. <strong>봇을 만들자!</strong></p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/e8f83ac1-dd41-4d3d-bd05-5c8869168219/image.png" alt=""></p>
<p>봇 이름은 <strong>서기(書記)</strong> 로 정했다. 하는 일이 결국 기록하고, 나눠주고, 보관하는 것이기 때문이다. (이래놓고 보여지는 이름은 <strong>Mr. Clerk</strong>로 했다...)</p>
<h2 id="기능보다-급한-것">[기능보다 급한 것]</h2>
<p>기능 목록부터 뽑고 싶은 걸 꾹 참고, 먼저 정한 건 이거였다.</p>
<blockquote>
<p><strong>KP가 외워야 하는 명령어는 <code>/모집</code> 하나여야 한다.</strong></p>
</blockquote>
<p>이유는 단순하다. 봇을 만든 나만 쓰는 명령어가 아니기 때문이다. 몇 개의 처리를 제외한 나머지는 전부 버튼으로 처리하기로 했다. 실제 사이클은 이렇게 됐다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>사람이 하는 것</th>
</tr>
</thead>
<tbody><tr>
<td>모집</td>
<td><code>/모집</code> + 모달 5칸</td>
</tr>
<tr>
<td>참가</td>
<td>플레이어가 [참가] 버튼</td>
</tr>
<tr>
<td>확정</td>
<td>KP가 [파티 확정] 버튼</td>
</tr>
<tr>
<td>시작</td>
<td>KP가 [세션 시작] 버튼</td>
</tr>
<tr>
<td>종료</td>
<td>KP가 [세션 종료] 버튼 → 옵션 고르고 한 번 더</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/nano_o/post/2921de19-43ba-43f7-bcd4-8cb4df3caa2e/image.png" alt=""></p>
<p>명령어 하나와 클릭 네 번으로 세션 모집이 가능하다. 나머지 명령어(<code>/세션수정</code> <code>/세션취소</code> <code>/글찾기</code> <code>/밀담</code> <code>/이번주</code>)는 <strong>몰라도 세션이 돌아갈 수 있다.</strong> <del>(근데 그렇게 많은 명령어도 아닌데, 외워줬으면 하는 바람은 있다.)</del></p>
<br>

<h2 id="상태-몰빵-상태를-한-파일에-몰아넣기">[상태 몰빵] 상태를 한 파일에 몰아넣기</h2>
<p>봇의 거의 모든 버그는 <strong>상태 전이가 여기저기 흩어지는 것</strong>에서 나온다고 생각했다. <code>/모집</code> 파일에서도 상태를 바꾸고, 버튼 핸들러에서도 바꾸고, 잡(job)에서도 바꾸면, 세 곳이 서로 다른 가정을 하기 시작한다.</p>
<p>그래서 규칙을 하나 세웠다. <strong>상태 전이는 <code>services/session.js</code> 한 파일에만 둔다.</strong> 명령어와 버튼은 그 함수를 부르기만 한다.</p>
<pre><code>recruiting  ──[파티 확정]──▶  confirmed  ──[시작]──▶  playing  ──[종료]──▶  done
     │
     └──[/세션취소]──▶ cancelled</code></pre><p><strong>각 상태에서 무슨 일이 일어나는지</strong>:</p>
<p><strong>recruiting</strong> — 포럼 포스트가 있고, 태그는 🟢 모집중 + 룰 + 시간대를 자동으로 설정한다. 아래에 [참가] [취소] 버튼이 살아 있다. 정원이 차면 [파티 확정] 버튼이 나타난다.</p>
<p><strong>confirmed</strong> — 여기가 제일 많은 일을 한다.</p>
<pre><code class="language-js">// 1. 역할
const role = await guild.roles.create({ name: `세션-${s.id}`, mentionable: true });
for (const uid of memberIds) {
  const m = await guild.members.fetch(uid);
  await m.roles.add(role);
}

// 2. 전체 스레드
const main = await progress.threads.create({
  name: `[${s.rule}] ${s.title} — 전체`,
  type: ChannelType.PrivateThread,
  invitable: false,           // 참가자가 남을 못 데려오게
  autoArchiveDuration: 10080,
});

// 3. 사람마다 비밀 스레드
for (const p of joined) {
  const t = await progress.threads.create({
    name: `[${s.rule}] ${s.title} — ${member.displayName} 🔒`,
    type: ChannelType.PrivateThread,
  });
  await t.members.add(p.user_id);   // 본인
  await t.members.add(s.kp_id);     // KP
}</code></pre>
<p>버튼 한 번에 역할 하나, 스레드 N+1개, 리마인드 예약 2건이 생긴다. 손으로 하면 5분씩 걸리던 일이다.</p>
<p><code>invitable: false</code> 가 작은 포인트다. 기본값은 참가자가 다른 사람을 스레드에 초대할 수 있는데, 세션 스레드에서는 그러면 안 된다.</p>
<p><strong>playing</strong> — 태그가 🎬로 바뀐다. 이곳에서 오간 대화가 나중에 로그가 된다.</p>
<p><strong>done</strong> — 이게 제일 복잡해서 따로 설명할 예정이다.</p>
<br>

<h2 id="핸들러-설정">[핸들러 설정]</h2>
<p>여기가 이 봇에서 제일 중요한 설계 결정이었다고 생각한다. 그런데 왜 중요한지는 디스코드 버튼이 어떻게 생겨먹었는지를 먼저 알아야 한다.</p>
<p><strong>버튼은 메시지에 붙어 있는 데이터다.</strong> 그리고 메시지는 디스코드 서버에 영구히 남는다. 내 봇이 켜져 있든 꺼져 있든, 3개월 전 모집 글의 [참가] 버튼은 계속 거기 있고 계속 눌러진다.</p>
<p>버튼이 눌리면 디스코드가 내 봇에게 이벤트를 보내는데, 알려주는 건 딱 이거다.</p>
<blockquote>
<p>누가 <code>custom_id</code> 가 <code>xxx</code> 인 버튼을 눌렀다</p>
</blockquote>
<p><strong>문자열 하나가 전부다.</strong> 그걸 보고 뭘 할지는 봇이 알아서 정해야 하는 것이다.</p>
<h3 id="처음에-떠오른-방식">처음에 떠오른 방식</h3>
<p>버튼을 만들 때 &quot;이거 눌리면 이 함수 실행해&quot; 를 같이 등록해두는 게 자연스럽다.</p>
<pre><code class="language-js">const handlers = new Map();          // ← 그냥 자바스크립트 변수

// 모집 글을 만들 때
const id = crypto.randomUUID();
handlers.set(id, () =&gt; {
  session.participants.push(userId);   // session 을 클로저로 붙잡고 있다
});
button.setCustomId(id);

// 눌렸을 때
const fn = handlers.get(interaction.customId);
fn();</code></pre>
<p>동작은 하지만, <strong>문제가... 봇이 켜진 동안만 동작한다는 것이다.</strong></p>
<p>문제는 <code>handlers</code> 가 <strong>프로세스 메모리에 있는 변수</strong>라는 것이다. 배포하거나, 크래시하거나, 서버가 재부팅되면 프로세스가 죽고 이 Map은 통째로 사라진다.</p>
<p>그런데 <strong>디스코드에 있는 메시지는 안 사라진다.</strong> 버튼도 그대로 있고, 여전히 눌러진다는 문제가 있다.</p>
<h3 id="해법">해법</h3>
<p>카페로 치면 이렇다. 메모리에 콜백을 두는 건 주문을 종이에 안 적고 직원이 머릿속으로만 기억하는 것이다. 그 직원이 퇴근하면 다음 직원은 3번 손님이 뭘 시켰는지 모른다.</p>
<p>대신 <strong>진동벨에 주문번호를 새겨서</strong> 주면 된다. 손님이 벨을 들고 오면 번호를 보고 주문서를 찾으면 되고, 직원이 몇 번 바뀌든 상관없다.</p>
<p><code>custom_id</code> 는 최대 100자 문자열이고, <strong>디스코드가 메시지와 함께 보관했다가 누를 때마다 그대로 돌려준다.</strong> 이 아이가 진동벨이다. <del>지이잉</del></p>
<pre><code class="language-js">new ButtonBuilder()
  .setCustomId(`sess:join:${s.id}`)     // &quot;참가 버튼 / 42번 세션&quot;
  .setLabel(full ? &#39;대기 등록&#39; : &#39;참가&#39;)
  .setStyle(full ? ButtonStyle.Secondary : ButtonStyle.Success)</code></pre>
<p>눌렸을 때는 기억에 의존하지 않고 <strong>매번 새로 조회한다.</strong></p>
<pre><code class="language-js">const [, action, idRaw] = interaction.customId.split(&#39;:&#39;);
const s = getSession(Number(idRaw));   // SQLite = 디스크 = 재시작해도 살아있음</code></pre>
<p>봇은 버튼에 대해 아무것도 기억하지 않는다. 문자열을 읽고 DB를 조회할 뿐이라 <strong>어느 프로세스가 처리하든 결과가 같다.</strong> 그래서 몇 번을 재시작하든, 몇 달 전 글의 버튼이든 그대로 동작한다.</p>
<p>같은 이유로 라우팅도 접두사로만 한다.</p>
<pre><code class="language-js">if (i.isButton() &amp;&amp; i.customId.startsWith(&#39;sess:&#39;))  return sessionButtons.handle(i);
if (i.customId.startsWith(&#39;fin:&#39;))                    return finishPanel.handle(i);
if (i.isUserSelectMenu() &amp;&amp; i.customId.startsWith(&#39;mildam:&#39;)) return 밀담.handleSelect(i);</code></pre>
<br>

<h2 id="3초-규칙">[3초 규칙]</h2>
<p>디스코드는 상호작용에 <strong>3초 안에</strong> 응답하지 않으면 실패로 처리한다. 그런데 위에서 본 파티 확정은 스레드를 5개 만든다. 3초를 훌쩍 넘는다.</p>
<p>그래서 비용이 큰 일을 하기 전에 무조건 먼저 호출한다.</p>
<pre><code class="language-js">await interaction.deferReply({ flags: MessageFlags.Ephemeral });
// ← 여기서부터는 시간을 써도 된다
await confirmParty(client, sessionId);
await interaction.editReply(&#39;파티를 확정했습니다.&#39;);</code></pre>
<p>(<code>deferReply</code> 를 부르면 &quot;생각 중...&quot; 상태로 바뀌고 15분까지 시간을 벌 수 있다.)</p>
<h2 id="타임존-계산">[타임존 계산]</h2>
<p>일정을 다루는 봇이라 타임존에 시간이 좀 걸릴 줄 알았는데, 디스코드가 해결책을 갖고 있었다.</p>
<p><strong>DB에는 유닉스 초(UTC)로만 저장하고, 출력은 디스코드 타임스탬프 형식으로 던진다.</strong></p>
<pre><code class="language-js">export const stamp = (unix, style = &#39;F&#39;) =&gt; `&lt;t:${unix}:${style}&gt;`;</code></pre>
<p><code>&lt;t:1788000000:F&gt;</code> 를 메시지에 넣으면 디스코드가 <strong>보는 사람의 시간대로 알아서 표시</strong>한다. <code>&lt;t:...:R&gt;</code> 을 쓰면 &quot;3일 후&quot; 같은 상대 시간으로 나온다.</p>
<p>이거 하나로 타임존 버그가 통째로 사라졌다. 서버가 도쿄 리전에 있든 UTC로 돌든 상관없어진다.</p>
<p>입력 쪽은 반대로 아주 빡빡하게 잡았다. 한국어 자연어 파싱(&quot;다음 주 토요일 8시&quot;)을 하고 싶었지만 참았다.</p>
<pre><code class="language-js">/** &#39;YYYY-MM-DD HH:mm&#39; (한국 시간) → 유닉스 초. 형식이 틀리면 null */
export function parseKST(text) { ... }</code></pre>
<p>형식이 틀리면 그냥 거절하고 예시를 보여준다. 자연어 파싱은 대체로 맞지만, 가끔 <strong>은밀하게 틀린 날짜로 세션을 잡는다.</strong> <del>(너무해!)</del></p>
<h2 id="일정-리마인드-settimeout-쓰지-말자">[일정 리마인드] setTimeout 쓰지 말자</h2>
<p>세션 24시간 전, 1시간 전에 참가자에게 DM을 보내야 한다.</p>
<p>가장 쉬운 구현은 이거다.</p>
<pre><code class="language-js">setTimeout(() =&gt; sendReminder(session), msUntil24hBefore);   // ← 하지 말 것</code></pre>
<p>이러면 봇이 <strong>한 번만 재시작해도 예약이 전부 증발한다.</strong> 배포 한 번에 다음 주 세션 리마인드가 다 사라지는 것이고, 심지어 아무 에러도 안 난다.</p>
<p>그래서 DB에 적어두고 1분마다 확인하는 방식으로 했다.</p>
<pre><code class="language-sql">CREATE TABLE reminders (
  session_id  INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
  kind        TEXT    NOT NULL,   -- 24h | 1h
  fire_at     INTEGER NOT NULL,
  sent_at     INTEGER,
  PRIMARY KEY (session_id, kind)
);</code></pre>
<pre><code class="language-js">const tick = async () =&gt; {
  const due = db.prepare(
    &#39;SELECT * FROM reminders WHERE sent_at IS NULL AND fire_at &lt;= ?&#39;).all(now());
  for (const row of due) {
    await fire(client, row);
    db.prepare(&#39;UPDATE reminders SET sent_at = ? WHERE session_id = ? AND kind = ?&#39;)
      .run(now(), row.session_id, row.kind);
  }
};

tick();                     // 재시작 직후 밀린 것부터 처리
setInterval(tick, 60_000);</code></pre>
<p>소규모 서버에서 1분에 한 번 SQLite 조회는 부하가 생기지 않아서 다행이다. 그리고 <strong>재시작 직후에 <code>tick()</code> 을 한 번 부르는 것</strong>이 중요하다. 봇이 꺼져 있는 동안 지나간 예약을 그때 처리한다.</p>
<h2 id="db-sqlite--한-파일">[DB] SQLite = 한 파일</h2>
<p>동시 접속이 몇 명 안 되는 서버라서 굳이 Postgres를 띄울 이유가 없다. <code>better-sqlite3</code> 는 동기 API라 오히려 코드가 단순해진다.</p>
<pre><code class="language-sql">CREATE TABLE sessions (
  id                INTEGER PRIMARY KEY AUTOINCREMENT,
  rule              TEXT    NOT NULL,
  title             TEXT    NOT NULL,
  kp_id             TEXT    NOT NULL,
  capacity          INTEGER NOT NULL,
  scheduled_at      INTEGER NOT NULL,   -- unix seconds, UTC
  status            TEXT    NOT NULL DEFAULT &#39;recruiting&#39;,
  forum_post_id     TEXT,
  main_thread_id    TEXT,
  role_id           TEXT,
  archive_post_id   TEXT,
  created_at        INTEGER NOT NULL
);

CREATE TABLE participants (
  session_id        INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
  user_id           TEXT    NOT NULL,
  state             TEXT    NOT NULL,   -- joined | waitlist | cancelled
  pc_name           TEXT,
  sheet_url         TEXT,
  secret_thread_id  TEXT,
  PRIMARY KEY (session_id, user_id)
);</code></pre>
<p><strong>캐릭터 테이블은 만들지 않았다.</strong> 1편에서 정한 대로 캐릭터 인덱스는 스프레드시트가 맡는다. 봇은 <code>pc_name</code> 과 <code>sheet_url</code> 만 들고 있다가 세션이 끝날 때 시트에 정보를 기입한다.</p>
<h3 id="마이그레이션을-가볍게">마이그레이션을 가볍게</h3>
<p>기능을 추가하면서 컬럼이 늘었는데, 이미 돌아가는 DB가 있으니 스키마를 갈아엎을 수는 없다. 그래서 실행할 때 조용히 붙이게 했다.</p>
<pre><code class="language-js">for (const [col, def] of [
  [&#39;opt_secret&#39;,  &quot;TEXT DEFAULT &#39;reveal&#39;&quot;],
  [&#39;opt_threads&#39;, &quot;TEXT DEFAULT &#39;delete&#39;&quot;],
]) {
  const has = db.prepare(&#39;PRAGMA table_info(sessions)&#39;).all().some((c) =&gt; c.name === col);
  if (!has) db.exec(`ALTER TABLE sessions ADD COLUMN ${col} ${def}`);
}</code></pre>
<p>한 번만 돌아야 하는 데이터 변환은 <code>PRAGMA user_version</code> 으로 막는다.</p>
<pre><code class="language-js">if (db.pragma(&#39;user_version&#39;, { simple: true }) &lt; 1) {
  db.exec(&quot;UPDATE sessions SET opt_threads = &#39;delete&#39; WHERE opt_threads = &#39;archive&#39;&quot;);
  db.pragma(&#39;user_version = 1&#39;);
}</code></pre>
<h2 id="하드코딩-금지-id는-코드에-넣지-말자">[하드코딩 금지] ID는 코드에 넣지 말자</h2>
<p>채널 ID, 포럼 태그 ID, 역할 ID를 코드에 하드코딩하면 채널 하나 새로 만드는 순간 전부 고쳐야 한다. 그래서 <code>config.json</code> 으로 뺐다.</p>
<pre><code class="language-json">{
  &quot;channels&quot;: { &quot;모집&quot;: &quot;...&quot;, &quot;진행&quot;: &quot;...&quot;, &quot;일정&quot;: &quot;...&quot;, &quot;기록&quot;: &quot;...&quot; },
  &quot;tags&quot;:     { &quot;모집중&quot;: &quot;...&quot;, &quot;마감&quot;: &quot;...&quot;, &quot;CoC&quot;: &quot;...&quot;, &quot;평일밤&quot;: &quot;...&quot; },
  &quot;roles&quot;:    { &quot;kp&quot;: &quot;...&quot;, &quot;알림&quot;: { &quot;CoC&quot;: &quot;...&quot;, &quot;인세인&quot;: &quot;...&quot; } },
  &quot;rules&quot;:    [&quot;CoC&quot;, &quot;인세인&quot;, &quot;D&amp;D 5e&quot;, &quot;더블크로스&quot;, &quot;파라노이아&quot;, &quot;기타룰&quot;]
}</code></pre>
<p>그리고 이 파일을 <strong>서버를 읽어서 자동 생성</strong>하는 스크립트를 만들었다. <del>(클로드야 고맙다!)</del> 채널 이름과 태그 이름으로 찾아서 ID를 채운다. 구조를 바꿨으면 <code>npm run config</code> 한 번이면 끝이다.</p>
<p>여기에 하나 더 붙였는데, 이게 꽤 도움이 됐다. <strong>기동할 때 config에 적힌 ID가 실제로 존재하는지 검증한다.</strong></p>
<pre><code class="language-js">const REQUIRED = [&#39;모집&#39;, &#39;진행&#39;];      // 없으면 봇이 못 돈다
const OPTIONAL = [&#39;일정&#39;, &#39;기록&#39;, &#39;다이스&#39;];</code></pre>
<p>필수 채널이 없으면 콘솔에 이름을 찍고 즉시 종료한다.</p>
<h2 id="종료-순서의-중요성">[종료] 순서의 중요성</h2>
<p>세션 종료가 이 봇에서 가장 복잡한 부분이다. [세션 종료]를 누르면 바로 끝나지 않고 <strong>확인 패널</strong>이 먼저 뜬다.</p>
<p>두 가지를 고른다.</p>
<table>
<thead>
<tr>
<th>고르는 것</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>비밀 처리</td>
<td>전체 스레드에 공개 + 기록에도 (기본) / 기록에만 / 어디에도 안 남김</td>
</tr>
<tr>
<td>스레드 처리</td>
<td>삭제 (기본) / 보관</td>
</tr>
</tbody></table>
<p>비밀 스레드가 하나도 없으면 비밀 메뉴는 자동으로 잠긴다. 고른 값은 DB에 저장돼서 패널을 다시 열어도 유지된다.</p>
<p>[이대로 종료]를 누르면 이 순서로 실행된다.</p>
<ol>
<li><strong>로그를 먼저 뜬다</strong> — 전체 + 비밀 스레드 메시지 전부 (봇 자신의 메시지는 제외)</li>
<li>첨부를 시간순으로 모아 번호를 매긴다</li>
<li><strong>비밀을 공개한다</strong> (reveal일 때만)</li>
<li>로그 파일을 마크다운으로 조립</li>
<li><code>#세션-기록</code> 에 포스트 생성 (룰 태그 승계)</li>
<li><strong>이미지를 기록 글에 다시 올린다</strong></li>
<li>마스터 시트에 참가자 수만큼 행 추가</li>
<li>역할 삭제 → 스레드 정리 → 모집 글 [✅ 완결] + 보관</li>
</ol>
<p><strong>1번과 3번의 순서가 핵심이다.</strong> 처음엔 반대로 짰다가 기록이 통째로 중복됐다. (이 얘기는 3편에서 자세히 다룬다...)</p>
<p>6번도 그냥 지나가면 안 되는 부분이다. 디스코드 첨부 URL에는 만료 서명이 붙어 있어서 하루면 만료된다. 링크만 적어두면 <strong>기록보관소가 며칠 뒤에 전부 깨진다.</strong> 그래서 파일을 내려받아 기록 글에 다시 올리고, 로그 본문에는 번호로 짝을 맞춘다.</p>
<pre><code>## 이미지

아래 번호는 이 기록 글에 함께 올려둔 파일과 짝이 맞습니다.

- `001` scene.png — 김도현
- `002` map.jpg — 이서연 · 🔒 비밀</code></pre><p>7번 시트 연동은 <code>try/catch</code> 로 감쌌다. <strong>시트 API가 실패해도 세션 종료는 진행돼야 한다.</strong> 구글 API 때문에 기록이 통째로 날아가면 안 되므로, 실패하면 경고만 남긴다.</p>
<h2 id="부가-기능">[부가 기능]</h2>
<p>세션 사이클 외의 기능이지만 만들길 잘했다~ 싶은 게 두 개 있다.</p>
<h3 id="글찾기--포럼에-작성자-필터가-없다"><code>/글찾기</code> — 포럼에 작성자 필터가 없다</h3>
<p>&quot;특정 작성자가 올린 시나리오 글만 보고 싶다&quot;는 요구사항 ➡ <strong>충격적이게도 디스코드 포럼 검색에는 작성자 필터가 없다.</strong></p>
<p>태그로 만드는 건 현실적으로 불가능해서, 봇의 명령어를 추가했다. 보관된 글까지 훑어야 하니 지금 생각해 보면... 봇이 하는 게 맞긴 하다.</p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/ff9ae5e5-8c5e-480e-84c5-15006c6fbb77/image.png" alt=""></p>
<pre><code class="language-js">const active = await forum.threads.fetchActive();
let before;
for (let page = 0; page &lt; 10; page++) {          // 최대 1000개
  const arch = await forum.threads.fetchArchived({ type: &#39;public&#39;, limit: 100, before });
  if (arch.threads.size === 0) break;
  before = arch.threads.last()?.archivedAt;
  if (!arch.hasMore) break;
}
const mine = found.filter((t) =&gt; t.ownerId === user.id);</code></pre>
<p>처음엔 채널 목록을 하드코딩했다가, 채널을 하나 추가하니 바로 어긋났다. 지금은 <strong>서버의 포럼 채널을 그때그때 읽어서 자동완성</strong>으로 띄운다.</p>
<pre><code class="language-js">export async function autocomplete(interaction) {
  const q = (interaction.options.getFocused() ?? &#39;&#39;).toLowerCase();
  const list = forumChannels(interaction.guild)
    .filter((c) =&gt; !q || c.name.toLowerCase().includes(q))
    .slice(0, 25)
    .map((c) =&gt; ({ name: `#${c.name}`, value: c.name }));
  await interaction.respond(list);
}</code></pre>
<h3 id="밀담--아무-때나-여는-임시-스레드"><code>/밀담</code> — 아무 때나 여는 임시 스레드</h3>
<p>원래는 세션 중 PC끼리 몰래 이야기하는 용도로 구상했는데, 만들다 보니 &quot;세션이 아니어도 아무나 쓸 수 있으면 좋겠다&quot;로 범위가 넓어졌다.</p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/015ef05d-ab0b-4252-8960-4069ad2afe10/image.png" alt=""></p>
<p>사람 고르는 건 <code>UserSelectMenuBuilder</code> 를 쓴다. 텍스트로 멘션을 받는 것보다 훨씬 낫다.</p>
<pre><code class="language-js">const menu = new UserSelectMenuBuilder()
  .setCustomId(`mildam:create:${s?.id ?? 0}:${withoutKp ? &#39;1&#39; : &#39;0&#39;}:${parent.id}`)
  .setPlaceholder(&#39;사람 고르기&#39;)
  .setMinValues(1)
  .setMaxValues(10);</code></pre>
<p>customId에 상태를 전부 밀어넣는 패턴이 여기서도 쓰인다. 세션 id, KP 제외 여부, 부모 채널 id까지 다 문자열에 담는다.</p>
<p>세션 스레드 안에서 실행하면 그 세션에 묶이고 <strong>KP가 자동으로 들어간다.</strong> PC끼리만 몰래 하면 KP가 상황을 놓치기 때문이다. 정 빼고 싶으면 <code>kp제외</code> 옵션이 있는데, 그때는 경고를 띄운다.</p>
<hr>
<p>여기까지가 설계다. 명령어 하나 원칙, 상태 기계 한 파일, customId에 상태 인코딩, 타임존은 디스코드에 위임, 리마인드는 DB 폴링...</p>
<p>그런데 설계가 맞아도 <strong>에러 메시지도 없이 조용히 꼬이는 것들</strong>이 있었다.
다음 편은 그 얘기다.</p>
<p>→ <strong>3편 — 삽질 모음: 에러도 안 나는 버그들</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TRPG 동아리 디스코드 서버를 처음부터 설계했다 (1) — 포럼 채널]]></title>
            <link>https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-1-%ED%8F%AC%EB%9F%BC-%EC%B1%84%EB%84%90</link>
            <guid>https://velog.io/@nano_o/TRPG-%EB%8F%99%EC%95%84%EB%A6%AC-%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%84%A4%EA%B3%84%ED%96%88%EB%8B%A4-1-%ED%8F%AC%EB%9F%BC-%EC%B1%84%EB%84%90</guid>
            <pubDate>Fri, 28 Aug 2026 08:27:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>TRPG 동아리용 디스코드 서버를 설계하고, 세션 진행 전체를 자동화하는 봇을 만들어 클라우드에 올린 과정을 4편으로 나눠 보았다.</p>
<p>1편 — 서버 설계: 포럼 채널 ← 지금 글
2편 — 봇 설계: 명령어는 단순하게
3편 — 삽질 모음: 에러도 안 나는 버그들
4편 — 배포와 회고: 오라클 무료 티어, 그리고 되돌린 기능들</p>
</blockquote>
<br>

<h2 id="문제점-파티가-매번-바뀐다">[문제점] 파티가 매번 바뀐다!</h2>
<p><del>(기존 디스코드 서버가 너무 더러워서)</del> TRPG 동아리 서버를 만들기로 했다. 규모는 작고, 룰은 다섯 개를 돌아가면서 하고, 세션은 종종 음성으로 한다. 여기까지는 흔한 조건인데, 하나가 걸렸다...</p>
<p><strong>바로 파티가 매번 바뀐다는 것이다.</strong></p>
<p>장기 캠페인을 고정 멤버로 도는 서버가 아니라, 단편 위주로 그때그때 사람을 모아서 한 판 돌리고 흩어지는 구조다. 이 부분이 서버 설계의 거의 결정에 영향을 미쳤다.</p>
<p>처음에 자연스럽게 떠올린 건 이런 구조였다.</p>
<pre><code>📁 CoC
  └ #coc-공지  #coc-자료  #coc-세션1  #coc-세션2
📁 인세인
  └ #인세인-공지  #인세인-자료  ...</code></pre><p>세션마다 채널을 하나 파고, 룰마다 카테고리를 파는 것이 처음엔 깔끔해 보였다.</p>
<p>그런데... 대충 계산해보니 답이 없는 구조라는 걸 알 수 있었다. 한 달에 세션 두 번만 해도 1년이면 채널 24개다. 솔직히 끝난 세션 채널은 아무도 안 들어가는데 사이드바만 차지한다. (그리고 반년에 한 번 돌리는 룰은 카테고리 자체가 짐이 되는 느낌이 들었다.)</p>
<br>

<h2 id="해결법-포럼-채널">[해결법] 포럼 채널</h2>
<p>레퍼런스로 다른 디스코드 서버 스크린샷을 보다가 게시판처럼 생긴 화면을 발견했다. 처음엔 봇으로 만든 건 줄 알았는데, 알고 보니 디스코드 기본 기능인 <strong>포럼 채널(Forum Channel)</strong> 이었다.</p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/06a52dd7-8530-413f-b353-7cb060c7068d/image.png" alt=""></p>
<p><strong>포럼 채널의 특징</strong>:</p>
<ul>
<li>포스트 하나하나가 <strong>그대로 스레드</strong>다</li>
<li>포스트마다 <strong>태그</strong>를 붙이고, 태그로 필터링할 수 있다</li>
<li>갤러리 뷰로 두면 썸네일이 격자로 깔린다</li>
<li>오래된 포스트는 자동으로 보관된다 (사라지지는 않는다)</li>
</ul>
<p>여기서 이 서버의 설계 원칙이 하나로 정리됐다!</p>
<blockquote>
<p><strong>파티가 매번 바뀌는 소규모 서버는 &#39;채널을 파는 서버&#39;가 아니라 &#39;포스트를 파는 서버&#39;가 되어야 한다.</strong></p>
</blockquote>
<p>세션마다 채널을 파면 반년 뒤 안 쓰는 채널 20개가 남는다. 세션마다 <strong>포스트</strong>를 파면 그건 그대로 기록보관소가 된다. 같은 행위인데 결과물이 정반대다.</p>
<table>
<thead>
<tr>
<th>원칙</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>01</td>
<td>파티당 채널 금지, 세션당 포스트</td>
</tr>
<tr>
<td>02</td>
<td>룰은 채널이 아니라 <strong>태그</strong>로 나눈다</td>
</tr>
<tr>
<td>03</td>
<td>캐릭터 인덱스는 포럼이 아니라 <strong>스프레드시트</strong>가 맡는다</td>
</tr>
<tr>
<td>04</td>
<td>정리는 사람이 하면 귀찮다... → 봇을 시키자!</td>
</tr>
</tbody></table>
<p>3번은 부연 설명이 필요하다. 단편의 경우, 캐릭터가 시나리오와 함께 태어나고 시나리오와 함께 끝난다. <code>#캐릭터-시트</code> 포럼을 파서 캐릭터마다 포스트를 만들면, 한 번 쓰고 버려지는 포스트가 계속 쌓인다. 그래서 캐릭터는 구글 스프레드시트 한 장에 행으로 쌓기로 했다. (이건 나중에 봇이 자동으로 채우게 설계했다.)</p>
<br>

<h2 id="최종-채널-구조">[최종 채널 구조]</h2>
<img src="https://velog.velcdn.com/images/nano_o/post/f6af1bed-3734-4147-983c-3a807f20f90e/image.png" width="300" />

<p>카테고리 6개로 끝났다. 처음 설계안보다 확실히 <strong>줄었다.</strong></p>
<pre><code>📌 시작
  #공지            
  #규칙과-매너      
  #역할-알림설정

🎲 세션
  #세션-모집 [포럼]
  #세션-일정 [봇 전용]         
  #세션-진행 [확정된 세션마다 비공개 스레드가 열림]

📚 자료실
  #자료실 [포럼]  룰 요약·핸드아웃·BGM·맵
  #시나리오 [포럼]

🏛 기록보관소
  #세션-기록 [포럼]  완결 세션 로그. 봇이 자동 생성
  #캐릭터-갤러리 [포럼]
  #명장면 [스타보드 자동 수집]

💬 라운지
  #잡담  #trpg-소식  #다이스  #봇-테스트

🔊 음성
  ➕ 세션방 만들기 [들어가면 방이 생기고, 다 나가면 사라짐]
  🔒 밀담방
  대기실  잡담방</code></pre><p>주요 채널 다섯 개가 포럼이다. 그리고 여기서 설계상 제일 고민했던 지점이 하나 있다.</p>
<h3 id="왜-세션-진행만-포럼이-아닌가">왜 <code>#세션-진행</code>만 포럼이 아닌가</h3>
<p>확정된 세션의 참가자들만 쓰는 공간이 필요했다. 핸드아웃을 주고받고, 비밀 정보를 주고 받으며 정보를 확인하는 곳 말이다.</p>
<p>처음엔 이것도 포럼으로 만들려고 했다. 그런데 문제가 생겼다!</p>
<p><strong>디스코드는 &quot;포럼 포스트 하나만 특정 인원에게 보이게&quot; 하는 걸 지원하지 않는다.</strong> 포스트 권한은 부모 채널 권한을 그대로 따라간다. 채널이 보이면 모든 포스트가 보이고, 안 보이면 다 안 보인다. 포스트 단위 권한이라는 개념 자체가 없는 것이다. ㅠㅠ</p>
<p>반면 <strong>비공개 스레드(Private Thread)</strong> 는 참가자를 한 명씩 넣는 방식이다. 세션마다 딱 맞는 공간이 된다.</p>
<p>그런데 비공개 스레드는 <strong>일반 텍스트 채널에서만 만들 수 있었다.</strong> 포럼 안에서는 못 만든다.</p>
<p>그래서 <code>#세션-진행</code> 만 텍스트 채널로 남겼다. 나중에 만들 봇의 <code>/밀담</code> 명령어가 텍스트 채널에서만 동작하는 것도 같은 제약 때문이다.</p>
<br>

<h2 id="태그를-예산처럼-배분하기">태그를 &#39;예산&#39;처럼 배분하기</h2>
<p>포럼 채널의 태그에는 두 가지 한도가 있다.</p>
<ul>
<li><strong>채널당 최대 20개</strong></li>
<li><strong>포스트당 최대 5개</strong>
<img src="https://velog.velcdn.com/images/nano_o/post/7d7fe53b-5286-452b-82ed-755a286c8ef1/image.png" alt=""></li>
</ul>
<p>20개는 생각보다 금방 찬다. 아무 생각 없이 만들다 보면 룰 6개 + 상태 4개만 해도 절반이 날아간다. 그래서 태그를 예산처럼 미리 배분해두는 게 이 설계의 핵심이었다.</p>
<p><code>#세션-모집</code> 은 20칸을 꽉 채워 썼다.</p>
<table>
<thead>
<tr>
<th>그룹</th>
<th>개수</th>
<th>태그</th>
</tr>
</thead>
<tbody><tr>
<td>상태</td>
<td>4</td>
<td>🟢 모집중 / 🔴 마감 / 🎬 진행중 / ✅ 완결</td>
</tr>
<tr>
<td>룰</td>
<td>6</td>
<td>CoC / 인세인 / D&amp;D 5e / 더블크로스 / 파라노이아 / 기타룰</td>
</tr>
<tr>
<td>분량</td>
<td>3</td>
<td>원샷 / 단편 / 캠페인</td>
</tr>
<tr>
<td>시간대</td>
<td>4</td>
<td>평일밤 / 주말낮 / 주말밤 / 유동</td>
</tr>
<tr>
<td>대상</td>
<td>3</td>
<td>초보환영 / 경험자 / 고어·호러주의</td>
</tr>
</tbody></table>
<p><strong>상태 4개와 시간대 4개는 봇이 자동으로 붙인다.</strong> 상태는 세션이 진행되면서 교체되고, 시간대는 입력된 일시에서 계산한다. 사람이 직접 고르는 건 &#39;대상&#39; 3개뿐이다.</p>
<br>

<h2 id="역할-설계-임시-역할이라는-트릭">[역할 설계] &#39;임시 역할&#39;이라는 트릭</h2>
<p>파티가 매번 바뀌는 서버에서 권한을 수동으로 관리하면 무조건 사고가 난다는 확신에 기반했다. <del>&quot;아! 저번 사람들 역할 빼는 거 깜빡했다..&quot;</del></p>
<p>해법은 <strong>세션마다 태어나고 세션이 끝나면 사라지는 임시 역할</strong>이다.</p>
<pre><code>@운영진          채널·역할·봇 관리
@KP / GM        세션 개설
@플레이어         온보딩 통과 후 기본 역할
@알림-CoC ...    룰별 알림 구독 (버튼으로 자율)
@세션-{ID}       ← 봇이 만들고 봇이 지우도록 설계</code></pre><p><code>@세션-42</code> 같은 역할이 파티 확정 순간에 생겨서 참가자에게 붙고, 세션이 끝나면 사라진다. 그 세션의 공간 접근권이 이 역할 하나로 결정된다. 사람이 손댈 일이 없다.</p>
<h3 id="발견한-권한-구멍">발견한 권한 구멍</h3>
<p>설계하다가 하나 발견했는데, 이건 꽤 중요하다.</p>
<p><strong>디스코드에서 &#39;스레드 관리(Manage Threads)&#39; 권한이 있으면, 초대받지 않은 비공개 스레드도 열어볼 수 있다.</strong></p>
<p><code>@운영진</code> 과 <code>@KP / GM</code> 역할에 관례적으로 이 권한을 주기 쉬운데, 그러면 KP가 여러 명이 됐을 때 <strong>서로의 세션 비밀 스레드가 전부 보인다.</strong> 인세인 비밀이나 CoC 개별 핸드아웃이 다른 KP에게 노출된다는 뜻이다.</p>
<p>그래서 <code>#세션-진행</code> 채널에서만 그 권한을 뺐다.</p>
<pre><code class="language-js">await jinhaeng.permissionOverwrites.edit(kp, {
  ViewChannel: true,
  SendMessages: true,
  CreatePrivateThreads: true,   // 만들 수는 있고
  CreatePublicThreads: true,
  ManageThreads: false,          // 남의 것은 못 보게
});</code></pre>
<p>스레드를 <strong>만들 수는 있지만 남의 것을 열어볼 수는 없는</strong> 상태. 정리는 봇이 하니까 사람에게 관리 권한이 필요 없다.</p>
<p>다만 정직하게 고백하자면, <strong>서버 소유자는 권한과 무관하게 모든 비공개 스레드를 볼 수 있다.</strong> 디스코드 사양이라 못 막는다. 하하!</p>
<br>

<h2 id="채널-제작은-클로드와-함께">채널 제작은 클로드와 함께</h2>
<p>카테고리 6개, 채널 15개, 포럼 태그 60여 개, 역할 10개. 상상만 해도 오래 걸리기에, 클로드와 함께 작업했다. 그래서 discord.js로 구축 스크립트를 짰다.</p>
<pre><code class="language-js">const guild = await client.guilds.fetch(process.env.GUILD_ID);

const forum = await guild.channels.create({
  name: &#39;세션-모집&#39;,
  type: ChannelType.GuildForum,
  parent: category.id,
  defaultForumLayout: ForumLayoutType.ListView,
  defaultSortOrder: SortOrderType.LatestActivity,
  availableTags: TAGS.map((t) =&gt; ({
    name: t.name,
    moderated: false,
    ...(t.emoji ? { emoji: { id: null, name: t.emoji } } : {}),
  })),
});</code></pre>
<p><code>--dry-run</code> 플래그를 붙이면 디스코드에 붙지 않고 무엇을 만들지만 출력하게 해뒀다. 이게 생각보다 유용했다. 태그 개수가 20을 넘는지, 이름에 오타가 없는지를 API 호출 없이 확인할 수 있다.</p>
<p>스크립트로 만들면 좋은 점이 하나 더 있다. <strong>구조를 바꾸는 게 편해진다.</strong> 나중에 <code>#시나리오</code> 포럼을 새로 만들고 <code>#자료실</code> 에서 태그 하나를 빼는 개편을 했는데, 이것도 스크립트(<code>revamp.js</code>)로 처리했다. 손으로 했으면 &quot;귀찮으니 그냥 두자&quot; 로 끝났을 일이다.</p>
<h3 id="다만-코드로-안-되는-것들도-있다">다만 코드로 안 되는 것들도 있다</h3>
<p>전부 자동화되지는 않았다. ** 서버 설정 화면에서 직접 설정해야 했던 것들**:</p>
<ul>
<li><strong>커뮤니티 활성화</strong> — 이걸 켜야 온보딩과 공지 채널을 쓸 수 있다</li>
<li><strong>온보딩</strong> — 규칙 동의 + 관심 룰 선택</li>
<li><strong>온보딩 &#39;기본 채널&#39;</strong> — 가장 헷갈렸던 부분이라 따로 쓴다</li>
<li><strong>역할 순서</strong> — 봇 역할을 <code>@세션-*</code> 위로 드래그</li>
</ul>
<p>세 번째가 유독 헷갈렸다. 보통 온보딩의 <strong>기본 채널</strong> 목록에 등록된 채널만 신입에게 기본으로 보인다. 사용자가 &#39;모든 채널 보기&#39;를 꺼두고 있으면 <strong>이 목록이 곧 그 사람이 보는 서버 전부</strong>다.</p>
<p>이걸 모르고 있다가, GM 역할 계정으로 서버를 보니 카테고리가 이상하게 갈라져 보이고 음성 채널이 안 보이는 현상을 겪었다. 원인은 그 계정의 &#39;모든 채널 보기&#39; 가 꺼져 있었고 기본 채널 목록에 음성 채널이 빠져 있던 것이다.</p>
<p>이건 API로 채울 수 있어서 스크립트를 하나 더 만들었다.</p>
<pre><code class="language-js">const onboarding = await guild.fetchOnboarding();
await guild.editOnboarding({
  defaultChannels: [...allChannelIds],   // 음성 채널까지 전부
  mode: onboarding.mode,
  enabled: true,
});</code></pre>
<hr>
<p>여기까지가 서버 구조다. 채널을 파지 않고 포스트를 파는 것, 태그를 예산처럼 배분하는 것, 임시 역할로 권한을 자동화하는 것...</p>
<p>다음 편은 그 봇을 어떻게 설계했는지다. 사실 제일 중요했던 결정은 기능이 아니라 <strong>&quot;KP가 외워야 하는 명령어를 몇 개로 할 것인가&quot;</strong> 였다.</p>
<p>→ <strong>2편 — 봇 설계: 명령어를 외우게 하지 않기</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[정보처리기사 실기]_1장 요구사항 확인_소프트웨어 개발 방법론]]></title>
            <link>https://velog.io/@nano_o/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B01%EC%9E%A5-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%ED%99%95%EC%9D%B8%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EA%B0%9C%EB%B0%9C-%EB%B0%A9%EB%B2%95%EB%A1%A0</link>
            <guid>https://velog.io/@nano_o/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B01%EC%9E%A5-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%ED%99%95%EC%9D%B8%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EA%B0%9C%EB%B0%9C-%EB%B0%A9%EB%B2%95%EB%A1%A0</guid>
            <pubDate>Mon, 30 Mar 2026 12:40:45 GMT</pubDate>
            <description><![CDATA[<h2 id="소프트웨어-개발-방법론">소프트웨어 개발 방법론</h2>
<p>: 소프트웨어 개발, 유지보수 등에 필요한 수행 방법과 각종 기법 및 도구를 체계적으로 정리하여 표준화한 것</p>
<ul>
<li>주요 방법론<ul>
<li>구조적 방법론
: 사용자 요구사항 파악하여 문서화하는 처리 중심의 방법론</li>
<li>정보공학 방법론
: 정형화된 기법들을 상호 연관성 있게 통합 및 적용하는 자료 중심의 방법론</li>
<li>객체지향 방법론
: 객체들을 조립해서 소프트웨어 구현하는 방법론</li>
<li>컴포넌트 기반 방법론
: 컴포넌트를 조합하여 새로운 애플리케이션을 만드는 방법론</li>
<li>제품 개열 방법론
: 제품에 적용하고 싶은 공통된 기능을 정의하여 개발하는 방법론</li>
<li>애자일 방법론</li>
</ul>
</li>
</ul>
<h3 id="소프트웨어-재사용software-reuse">소프트웨어 재사용(Software Reuse)</h3>
<p>: 이미 개발되어 인정받은 소프트웨어를 다른 소프트웨어 개발이나 유지에 사용하는 것 / 품질과 생산성 향상</p>
<ul>
<li>방법<ul>
<li>합성 중심(Composition-Based): 블록을 만들어서 끼워 맞춰 소프트웨어를 완성, <strong>블록 구성 방법</strong></li>
<li>생성 중심(Generation-Based): 추상화 형태로 써진 명세를 구체화하여 프로그램 완성, <strong>패턴 구성 방법</strong></li>
</ul>
</li>
</ul>
<h3 id="소프트웨어-재공학software-reengineering">소프트웨어 재공학(Software Reengineering)</h3>
<p>: 기존 시스템을 이용하여 보다 나은 시스템을 구축하고, 새로운 기능을 추가하여 소프트웨어 성능을 향상시키는 것</p>
<ul>
<li>이점: 품질 향상, 생산성 증가, 수명 연장, 오류 감소</li>
</ul>
<h3 id="casecomputer-aided-software-engineering">CASE(Computer Aided Software Engineering)</h3>
<p>: 소프트웨어 개발 과정 전체 또는 일부를 컴퓨터와 전용 소프트웨어 도구를 사용하여 자동화하는 것 = 자동화 도구</p>
<hr>
<h2 id="비용-산정-기법">비용 산정 기법</h2>
<h3 id="하향식">하향식</h3>
<p>: 과거의 유사한 경험을 바탕으로 전문 지식이 많은 개발자들이 참여한 회의를 통해 비용을 산정하는 비과학적인 방법</p>
<ul>
<li>기법<ul>
<li>전문가 감정 기법</li>
<li>델파이 기법(주관적 편견 보완)</li>
</ul>
</li>
</ul>
<h3 id="상향식">상향식</h3>
<p>: 세부적인 작업 단위별로 비용을 산정한 후 집계하여 전체 비용을 산정하는 방법</p>
<ul>
<li>기법<ul>
<li><strong>LOC(원시 코드 라인 수) 기법</strong>: 비관치(가장 많이 측정), 낙관치(가장 적게), 기대치(측정된 모든 코드 라인 수)
<img src="https://velog.velcdn.com/images/nano_o/post/54efbde8-ca59-434b-8fdb-abdb0a0f9ec7/image.png" alt=""></li>
<li><strong>개발 단계별 인월수(Effort Per Task) 기법</strong>: LOC 기법 보완</li>
<li>수학적 산정 기법</li>
</ul>
</li>
</ul>
<hr>
<h2 id="수학적-산정-기법">수학적 산정 기법</h2>
<p>: 상향식 비용 산정 기법, 경험적 추정 모형, 실험적 추정 모형 / 개발 비용 산정의 자동화를 목표</p>
<ul>
<li><p>기법</p>
</li>
<li><p>COCOMO 모형</p>
</li>
<li><p>Putnam 모형
: 소프트웨어 생명 주기의 전 과정 동안에 사용될 노력의 분포를 예상하는 모형, 생명 주기 예측 모형 / Rayleigh-Norden 곡선의 노력 분포도를 기초</p>
</li>
<li><p>기능 점수(FP) 모형
: 소프트웨어의 기능을 증대시키는 요인별로 가중치 부여</p>
</li>
<li><p>비용 산정 자동화 추정 도구</p>
<ul>
<li><strong>SLIM</strong>: Rayleigh-Norden 곡선과 Putnam 예측 모델을 기초로 하여 개발된 자동화 추정 도구</li>
<li>ESTIMACS</li>
</ul>
</li>
</ul>
<h2 id="cocomoconstructive-cost-model-모형">COCOMO(Constructive COst MOdel) 모형</h2>
<p>: 원시 프로그램 규모인 LOC(원시 코드 라인 수)에 의한 비용 산정 기법 / 비용 산정 결과는 프로젝트를 완성하는 데 필요한 노력으로 나타남 / 보헴이 제안</p>
<ul>
<li>개발 유형 : <strong>&#39;조반내&#39;</strong><ul>
<li><strong>조직형(Organic Mode)</strong></li>
<li><strong>반분리형(Semi-Detached Mode)</strong></li>
<li><strong>내장형(Embedded Mode)</strong></li>
</ul>
</li>
<li>모형 종류<ul>
<li>기본형 COCOMO</li>
<li>중간형 COCOMO</li>
<li>발전형 COCOMO</li>
</ul>
</li>
</ul>
<hr>
<h2 id="프로젝트-일정-계획">프로젝트 일정 계획</h2>
<p>: 프로젝트의 프로세스를 이루는 소작업을 파악하고 예측된 노력을 각 소작업에 분배하여 소작업의 순서와 일정을 정하는 것</p>
<ul>
<li>기능: WBS, PERT/CPM, 간트 차트</li>
</ul>
<hr>
<h2 id="소프트웨어-개발-방법론-1">소프트웨어 개발 방법론</h2>
<ul>
<li>프로젝트 관리: <strong>&#39;일비인위품&#39;</strong>
일정, 비용, 인력, 위험, 품질 관리</li>
</ul>
<hr>
<h2 id="소프트웨어-개발-표준">소프트웨어 개발 표준</h2>
<p>: 소프트웨어 개발 단계에서 수행하는 품질 관리에 사용되는 국제 표준</p>
<h3 id="isoiec-12207">ISO/IEC 12207</h3>
<p>: ISO에서 만든 표준 소프트웨어 생명 주기 프로세스</p>
<ul>
<li>기본 생명 주기 프로세스</li>
<li>지원 생명 주기 프로세스</li>
<li>조직 생명 주기 프로세스</li>
</ul>
<h3 id="cmmi">CMMI</h3>
<p>: 소프트웨어 개발 조직의 업무 능력 및 조직의 성숙도를 평가하는 모델 / SEI 개발</p>
<ul>
<li>프로세스 성숙도: <strong>&#39;초관정,정관최&#39;</strong>
초기, 관리, 정의, 정량적 관리, 최적화</li>
</ul>
<h3 id="spice--isoiec-15504">SPICE = ISO/IEC 15504</h3>
<p>: 소프트웨어의 품질 및 생산성 향상을 위한 소프트웨어 프로세스를 평가 및 개선</p>
<ul>
<li>수행 능력 단계: 불완전, 수행, 관리, 확립, 예측, 최적화</li>
</ul>
<hr>
<h2 id="소프트웨어-개발-방법론-테일러링">소프트웨어 개발 방법론 테일러링</h2>
<p>: 소프트웨어 개발 방법론의 절차, 사용기법 등을 수정 및 보완하는 작업</p>
<ul>
<li>내부적 기준: 목표 환경, 요구사항, 프로젝트 규모, 보유 기술</li>
<li>외부적 기준: 법적 제약사항, 표준 품질 기준</li>
</ul>
<hr>
<h2 id="소프트웨어-개발-프레임워크">소프트웨어 개발 프레임워크</h2>
<p>: 소프트웨어 개발에 공통적으로 사용되는 구성 요소와 아키텍처를 일반화하여 여러 가지 기능들을 제공해주는 반제품 형태의 소프트웨어 시스템</p>
<h3 id="스프링-프레임워크spring-framework">스프링 프레임워크(Spring Framework)</h3>
<p>: 자바 플랫폼을 위한 오픈 소스 경량형 애플리케이션 프레임워크</p>
<h3 id="전자정부-프레임워크">전자정부 프레임워크</h3>
<p>: 대한민국의 공공부문 정보화 사업 시, 정보 시스템의 구축을 지원하기 위해 필요한 기능 및 아키텍처를 제공하는 프레임워크</p>
<h3 id="닷넷-프레임워크net-framework">닷넷 프레임워크(.NET Framework)</h3>
<p>: Windows 프로그램의 개발 및 실행 환경을 제공하는 프레임워크 / CLR(공용 언어 런타임) 가상머신 상에서 작동</p>
<h3 id="소프트웨어-개발-프레임워크의-특성">소프트웨어 개발 프레임워크의 특성</h3>
<ul>
<li><strong>모듈화</strong>
: 캡슐화로 모듈화 강화 --&gt; 유지보수 용이
: 설계 및 구현 변경에 따른 영향 최소화 --&gt; 소프트웨어 품질 향상</li>
<li><strong>재사용성</strong>
: 재사용 가능한 모듈 제공 --&gt; 예산 절감, 생산성 향상, 품질 보증</li>
<li><strong>확장성</strong>
: 다형성을 통한 인터페이스 확장 가능 --&gt; 다양한 형태와 기능을 가진 애플리케이션 개발 가능</li>
<li><strong>제어의 역흐름</strong>
: 관리하고 통제해야 하는 객체들의 제어를 프레임워크에 넘김 --&gt; 생산성 향상</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[정보처리기사 실기]_1장 요구사항 확인_UML]]></title>
            <link>https://velog.io/@nano_o/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B01%EC%9E%A5-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%ED%99%95%EC%9D%B8UML</link>
            <guid>https://velog.io/@nano_o/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B01%EC%9E%A5-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%ED%99%95%EC%9D%B8UML</guid>
            <pubDate>Mon, 30 Mar 2026 09:25:09 GMT</pubDate>
            <description><![CDATA[<h2 id="umlunified-modeling-language">UML(Unified Modeling Language)</h2>
<p>: 시스템 개발 과정에서 의사소통이 원활하게 이루어지도록 표준화한 대표적인 객체지향 모델링 언어</p>
<ul>
<li>구성 요소: <strong>&#39;사관다&#39;</strong><ul>
<li><strong>사물(Things)</strong></li>
<li><strong>관계(Relationships)</strong></li>
<li><strong>다이어그램(Diagram)</strong></li>
</ul>
</li>
</ul>
<hr>
<h2 id="사물things">사물(Things)</h2>
<p>: 다이어그램 안에서 관계가 형성될 수 있는 대상</p>
<ul>
<li>종류: <strong>&#39;구행그주&#39;</strong></li>
<li><strong>구조 사물(Structural Things)</strong>
: 시스템의 개념적 물리적 요소 --&gt; 클래스, 유스케이스, 컴포넌트, 인터페이스, 노드</li>
<li><strong>행동 사물(Behavioral Things)</strong>
: 시간과 공간에 따른 요소들의 행위 표현 --&gt; 상호 작용, 상태 머신</li>
<li><strong>그룹 사물(Grouping Things)</strong>
: 요소들을 그룹으로 묶어서 표현 --&gt; 패키지</li>
<li><strong>주해 사물(Annotation Things)</strong>
: 부가적인 설명이나 제약조건 등을 표현 --&gt; 노트</li>
</ul>
<hr>
<h2 id="관계relationships">관계(Relationships)</h2>
<p>: 사물과 사물 사이의 연관성을 표현하는 것</p>
<ul>
<li>종류: <strong>&#39;연집포일의실&#39;</strong>
연관, 집합, 포함, 일반화, 의존, 실체화 관계</li>
</ul>
<h3 id="연관association-관계">연관(Association) 관계</h3>
<p>: 2개 이상의 사물이 서로 관련</p>
<ul>
<li>실선 방향성은 화살표, 양뱡향의 경우 생략 가능, 다중도는 선 위에 표기
<img src="https://velog.velcdn.com/images/nano_o/post/502f1e49-e389-4bfe-a19c-9da400e97bef/image.png" alt=""></li>
</ul>
<h3 id="집합aggregation-관계">집합(Aggregation) 관계</h3>
<p>: 하나의 사물이 다른 사물에 포함되어 있는 관계</p>
<ul>
<li>전체(Whole)와 부분(Part)는 <strong>서로 독립적</strong>
<img src="https://velog.velcdn.com/images/nano_o/post/3f4282c9-21e0-4a46-a7eb-377ec32fd4d9/image.png" alt=""></li>
</ul>
<h3 id="포함composition-관계">포함(Composition) 관계</h3>
<p>: 집합 관계의 특수한 형태, 포함하는 사물의 변화가 포함되는 사물에게 영향을 미치는 관계</p>
<ul>
<li>전체와 부분은 서로 독립될 수 없고, 생명주기를 함께 함
<img src="https://velog.velcdn.com/images/nano_o/post/bc386cfd-3657-43da-9171-9367134410fc/image.png" alt=""></li>
</ul>
<h3 id="일반화generalization-관계">일반화(Generalization) 관계</h3>
<p>: 하나의 사물이 다른 사물에 비해 더 일반적이거나 구체적인 관계</p>
<ul>
<li>구제적(하위), 일반적(상위)
<img src="https://velog.velcdn.com/images/nano_o/post/06e1d001-6380-47e5-8699-7277698eb99c/image.png" alt=""></li>
</ul>
<h3 id="의존dependency-관계">의존(Dependency) 관계</h3>
<p>: 서로 연관은 있으나, 필요에 의해 서로에게 영향을 주는 짧은 시간 동안만 연관을 유지하는 관계</p>
<ul>
<li>소유 관계는 아니지만 사물의 변화가 다른 사물에도 영향을 미침
<img src="https://velog.velcdn.com/images/nano_o/post/13abb4dc-5a6f-4180-af5a-feff2815e031/image.png" alt=""></li>
</ul>
<h3 id="실체화realization-관계">실체화(Realization) 관계</h3>
<p>: 할 수 있거나 해야 하는 기능으로, 서로를 그룹화 할 수 있는 관계</p>
<ul>
<li>사물에서 기능으로 연결
<img src="https://velog.velcdn.com/images/nano_o/post/005b2a60-9973-4a33-844d-9ad4004d5bd8/image.png" alt=""></li>
</ul>
<p><img src="https://velog.velcdn.com/images/nano_o/post/f7d9fbf2-d663-4420-9467-c7b22eb852f6/image.png" alt=""></p>
<hr>
<h2 id="다이어그램diagram">다이어그램(Diagram)</h2>
<p>: 사물과 관계를 도형으로 표현한 것 / 여러 관점에서 시스템을 가시화한 뷰(View)를 제공함으로써 의사소통에 도움을 줌</p>
<ul>
<li><strong>정적 모델링</strong>: <strong>구조적 다이어그램</strong></li>
<li><strong>동적 모델링</strong>: <strong>행위 다이어그램</strong></li>
</ul>
<h3 id="구조적structural-다이어그램-종류-클객컴배복패">구조적(Structural) 다이어그램 종류: &#39;클객컴배복패&#39;</h3>
<ul>
<li>클래스 다이어그램: 클래스 속성</li>
<li>객체 다이어그램: 인스턴스와 럼바우</li>
<li>컴포넌트 다이어그램: 실제 구현 모듈인 컴포넌트, 구현 단계</li>
<li>배치 다이어그램: 물리적 요소 위치, 구현 단계</li>
<li>복합체 구조 다이어그램: 복합 구조</li>
<li>패키지 다이어그램: 그룹화</li>
</ul>
<h3 id="행위behavioral-다이어그램-종류-유순커상활상타">행위(Behavioral) 다이어그램 종류: &#39;유순커상활상타&#39;</h3>
<ul>
<li>유스케이스 다이어그램: 사용자 + 사용 사례</li>
<li>순차 다이어그램: 메시지</li>
<li>커뮤니케이션 다이어그램: 연관 관계</li>
<li>상태 다이어그램: 상태 변화, 럼바우, 동적 모델링</li>
<li>활동 다이어그램: 수행, 처리 로직</li>
<li>상호작용 개요 다이어그램: 제어</li>
<li>타이밍 다이어그램: 시간 제약</li>
</ul>
<h3 id="스테레오-타입stereotype">스테레오 타입(Stereotype)</h3>
<p>: UML에서 표현하는 기본 기능 외에 추가적인 기능을 표현하는 것</p>
<ul>
<li>길러멧(Guilemet): ≪ ≫ 사이에 형태 기술<ul>
<li>≪include≫: 포함 관계</li>
<li>≪extends≫: 확장 관계</li>
<li>≪interface≫: 인터페이스 정의</li>
<li>≪exception≫: 예외 정의</li>
<li>≪constructor≫: 생성자 역할</li>
</ul>
</li>
</ul>
<hr>
<h2 id="유스케이스use-case-다이어그램">유스케이스(Use Case) 다이어그램</h2>
<p>: 개발될 시스템을 이용해 수행할 수 있는 기능을 사용자의 관점에서 표현한 것</p>
<ul>
<li>기능 모델링: 개발될 시스템이 갖춰야 할 기능을 정리한 후, 사용자와 공유하기 위해 그림으로 표현하는 것</li>
<li>종류: 유스케이스 다이어그램, 액티비티 다이어그램</li>
<li>구성 요소: 시스템, 액터, 유스케이스, 관계</li>
</ul>
<h2 id="활동activity-다이어그램">활동(Activity) 다이어그램</h2>
<p>: 사용자의 관점에서 시스템이 수행하는 기능을 처리 흐름에 따라 순서대로 표현한 것</p>
<ul>
<li>구성 요소: 액션(단일 작업), 액티비티(액션으로 분리 가능), (시작, 종료, 조건, 병합, 포크, 조인) 노드, 스윔레인</li>
</ul>
<h2 id="클래스class-다이어그램">클래스(Class) 다이어그램</h2>
<p>: 클래스와 클래스가 가지는 속성, 클래스 사이의 관계를 표현한 것</p>
<ul>
<li>정적 모델링: 사용자가 요구한 기능을 구현하는데 필요한 자료들의 논리적인 구조를 표현한 것</li>
<li>구성 요소: 클래스, 제약조건, 관계</li>
<li>연관 클래스: 연관 관계에 있는 두 클래스에 추가적으로 표현해야 할 속성이나 오퍼레이션이 있는 경우 생성하는 클래스</li>
</ul>
<h2 id="순차sequence-다이어그램">순차(Sequence) 다이어그램</h2>
<p>: 시스템이나 객체들이 메시지를 주고 받으며 상호 작용하는 과정을 그림으로 표현한 것</p>
<ul>
<li>동적 모델링: 시스템의 내부 구성 요소들의 상태 변화 과정과 과정에서 발생하는 상호 작용을 표현한 것</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[정보처리기사 실기]_1장 요구사항 확인_요구사항 정의]]></title>
            <link>https://velog.io/@nano_o/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B01%EC%9E%A5-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%ED%99%95%EC%9D%B8%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%EC%A0%95%EC%9D%98</link>
            <guid>https://velog.io/@nano_o/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B01%EC%9E%A5-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%ED%99%95%EC%9D%B8%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%EC%A0%95%EC%9D%98</guid>
            <pubDate>Sun, 29 Mar 2026 05:52:56 GMT</pubDate>
            <description><![CDATA[<h2 id="요구사항requirement-설명--제약조건">요구사항(Requirement): 설명 + 제약조건</h2>
<p>: 소프트웨어가 어떤 문제를 해결하기 위해 제공하는 서비스에 대한 <strong>설명</strong>과 운영되는데 필요한 <strong>제약조건</strong> / 기준과 근거 제공 / 이해관계자들 간의 의사소통을 원활</p>
<ul>
<li>요구사항의 유형: <strong>&#39;기비사시&#39;</strong><ul>
<li><strong>기능 요구사항(Functional Requirements)</strong>
: 시스템의 기능이나 수행과 관련된 요구사항</li>
<li><strong>비기능 요구사항(Non-functional requirements)</strong>
: 품질이나 제약사항과 관련된 요구사항 (품질 요구사항: 가용성, 정합성, 상호 환성, 대응성, 이식성, 확장성, 보안성 등)</li>
<li><strong>사용자 요구사항(User requirements)</strong>
: 사용자 관점에서 본 시스템이 제공해야 할 요구사항</li>
<li><strong>시스템 요구사항(System requirements)</strong>
: 개발자 관점에서 본 시스템 전체가 사용자와 다른 시스템에 제공해야 할 요구사항 
= 소프트웨어 요구사항</li>
</ul>
</li>
</ul>
<hr>
<h2 id="요구사항-개발-프로세스-도분명확">요구사항 개발 프로세스: &#39;도분명확&#39;</h2>
<p>: 개발 대상에 대한 요구사항을 체계적으로 도출하고 분석한 후 명세서에 정리한 다음 확인 및 검증하는 일련의 구조화된 활동 = 요구 공학의 한 요소 / 진행되기 전에 <strong>타당성 조사(Feasibility Study)</strong>가 선행 </p>
<p><strong>도출(Elicitation) --&gt; 분석(Analysis) --&gt; 명세(Specification) --&gt; 확인(Validation)</strong></p>
<h3 id="1-요구사항-도출requirement-elicitation-요구사항-수집">1. 요구사항 도출(Requirement Elicitation, 요구사항 수집)</h3>
<p>: 시스템 개발에 관련된 사람들이 서로 의견을 교환하여 <strong>요구사항을 식별하고 이해하는 과정</strong> / 이해관계자(Stakeholder)가 식별 --&gt; 소프르웨어 개발 생명 주기(SDLC) 동안 지속적으로 반복</p>
<ul>
<li>주요 기법: 청취와 인터뷰, 설문, 브레인스토밍, 워크샵, 프로토타이핑, 유스케이스</li>
</ul>
<h3 id="2-요구사항-분석requirement-analysis">2. 요구사항 분석(Requirement Analysis)</h3>
<p>: 요구사항 중 명확하지 않거나 <strong>이해되지 않는 부분을 발견</strong>하고 이를 <strong>걸러내기 위한 과정</strong> / 서로 상충되는 요구사항이 있으면 이를 중재하는 과정</p>
<ul>
<li>대표 도구: <strong>자료 흐름도(DFD), 자료 사전(DD)</strong></li>
</ul>
<h3 id="3-요구사항-명세requirement-specification">3. 요구사항 명세(Requirement Specification)</h3>
<p>: 분석된 요구사항을 바탕으로 <strong>모델을 작성하고 문서화</strong>하는 것 / 기능은 전부, 비기능은 필요한 것만 기술</p>
<ul>
<li>소단위 명세서(Mini-Spec)</li>
</ul>
<h3 id="4-요구사항-확인requirement-validation-요구사항-검증">4. 요구사항 확인(Requirement Validation, 요구사항 검증)</h3>
<p>: 요구사항 명세서가 <strong>정확하고 완전하게 작성되었는지 검토하는 활동</strong></p>
<ul>
<li>관리 도구를 이용하여 <strong>형상 관리(SCM; Software Configuration Management)</strong>를 수행</li>
</ul>
<h3 id="요구공학requirements-engineering">요구공학(Requirements Engineering)</h3>
<p>: <strong>요구사항을 정의하고, 분석 및 관리하는 프로세스를 연구하는 학문</strong>
--&gt; 요구사항 변경의 원인과 처리 방법을 이해하고, 프로세스 품질을 개선하여 소프트웨어 프로젝트 실패를 최소화하는 것을 목표</p>
<h3 id="요구사항-명세-기법">요구사항 명세 기법</h3>
<ul>
<li>정형 명세 기법</li>
<li>비정형 명세 기법</li>
</ul>
<hr>
<h2 id="요구사항-분석">요구사항 분석</h2>
<p>: SW 개발의 실질적인 첫 단계, 개발 대상에 대한 사용자의 요구사항을 이해하고 문서화하는 활동 (타당성 조사, 제약 및 목표 설정)</p>
<h3 id="구조적-분석-기법">구조적 분석 기법</h3>
<p>: 자료 흐름과 처리 중심의 요구사항 분석 기법 / 하향식 방법으로 시스템 세분화 / 분석의 중복 배제 가능</p>
<ul>
<li>주요 도구: 자료 흐름도(DFD), 자료 사전(DD), 소단위 명세서(Mini-Spec), 개체 관계도(ERD), 상태 전이도(STD), 제어 명세서</li>
</ul>
<h3 id="자료-흐름도dfd-data-flow-diagram">자료 흐름도(DFD; Data Flow Diagram)</h3>
<p>: 자료의 흐름 및 변환 과정과 기능을 도형 중심으로 기술하는 방법
= 자료 흐름 그래프, 버블 차트</p>
<ul>
<li>기본 기호 (Yourdon/DeMacro 표기법)
<img src="https://velog.velcdn.com/images/nano_o/post/4f5bf79f-696a-4702-a286-f392bcf350ff/image.png" alt=""></li>
</ul>
<h3 id="자료-사전dd-data-dictionary">자료 사전(DD; Data Dictionary)</h3>
<p>: 자료 흐름도에 있는 자료를 더 자세히 정의하고 기록한 것 --&gt; 메타 데이터(Meta Data) = 데이터를 위한 데이터</p>
<ul>
<li>표기 기호
<img src="https://velog.velcdn.com/images/nano_o/post/133eee5c-73b7-4348-9fa7-7dc63fdde82c/image.png" alt=""></li>
</ul>
<hr>
<h2 id="요구사항-분석-case와-hipo">요구사항 분석 CASE와 HIPO</h2>
<h3 id="요구사항-분석용-case자동화-도구">요구사항 분석용 CASE(자동화 도구)</h3>
<p>: 요구사항을 자동으로 분석하고, 요구사항 분석 명세서를 기술하도록 개발된 도구</p>
<ul>
<li>대표 CASE<ul>
<li><strong>SADT</strong>: SoftTech 사에서 개발</li>
<li><strong>SREM = RSL/REVS</strong> : TRW 사에서 개발, 실시간 처리용</li>
<li><strong>PSL/PSA</strong>: 미시간 대학에서 개발</li>
<li><strong>TAGS</strong>: 자동 접근 방법, 통합 자동화</li>
</ul>
</li>
</ul>
<h3 id="hipohierarchy-input-process-output">HIPO(Hierarchy Input Process Output)</h3>
<p>: 시스템 실행 과정인 입력, 처리, 출력의 기능을 표현한 것 / 하향식 소프트웨어 개발을 위한 문서화 도구</p>
<ul>
<li>HIPO Chart
: 시스템의 기능을 여러 개의 고유 모듈로 분할하여 이들 간의 인터페이스를 계층 구조로 표현한 것<ul>
<li>가시적 도표</li>
<li>총체적 도표</li>
<li>세부적 도표</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[정보처리기사 실기]_1장 요구사항 확인_소프트웨어 생명 주기]]></title>
            <link>https://velog.io/@nano_o/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B01%EC%9E%A5-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%ED%99%95%EC%9D%B8%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0</link>
            <guid>https://velog.io/@nano_o/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B01%EC%9E%A5-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%ED%99%95%EC%9D%B8%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0</guid>
            <pubDate>Sun, 29 Mar 2026 04:43:16 GMT</pubDate>
            <description><![CDATA[<h2 id="소프트웨어-생명-주기software-life-cycle">소프트웨어 생명 주기(Software Life Cycle)</h2>
<p>: 소프트웨어를 개발하기 위한 설계, 운용, 유지보수 등의 과정을 각 단계별로 나눈 것 --&gt; 단계별 주요 활동 + 산출물</p>
<h3 id="1-폭포수-모형waterfall-model">1. 폭포수 모형(Waterfall Model)</h3>
<p>: 이전 단계로 돌아갈 수 없다는 전제, 각 단계를 확실히 매듭짓고 그 결과를 철저하게 검토하여 승인 과정을 거친 후에 다음 단계를 진행하는 개발 방법론</p>
<ul>
<li>고전적 생명 주기 모형</li>
<li>단계 끝에 다음 단계를 수행하기 위한 결과물이 명확하게 산출되어야 함</li>
</ul>
<h3 id="2-프로토타입-모형prototype-model-원형-모형">2. 프로토타입 모형(Prototype Model, 원형 모형)</h3>
<p>: 사용자의 요구사항을 파악하기 위해 실제 개발될 소프트웨어에 대한 견본품(prototype)을 만들어 최종 결과물을 예측하는 모형</p>
<h3 id="3-나선형-모형spiral-model-점진적-모형">3. 나선형 모형(Spiral Model, 점진적 모형)</h3>
<p>: 나선을 따라 돌듯이 여러 번의 소프트웨어 개발 과정을 거쳐 점진적으로 완벽한 최종 소프트웨어를 개발하는 모형</p>
<ul>
<li>4가지 주요 활동: <strong>&#39;계분개평&#39;</strong></li>
<li><em>계*</em>획 수립 --&gt; 위험 <strong>분</strong>석 --&gt; <strong>개</strong>발 및 검증 --&gt; 고객 <strong>평</strong>가 (다시 계획 수립으로 돌아감)</li>
<li>보헴(Boehm)이 제안</li>
<li>누락되거나 추가된 요구사항을 첨가할 수 있음</li>
<li>유지보수 과정이 필요 없음</li>
</ul>
<h3 id="4-애자일-모형agile-model---시험에서는-모형-빼고-애자일agile로-쓰는-듯">4. 애자일 모형(Agile Model) -&gt; 시험에서는 모형 빼고, 애자일(Agile)로 쓰는 듯</h3>
<p>: 애자일은 &#39;민첩한&#39;, &#39;기민한&#39;이라는 의미, 고객의 요구사항 변화에 유연하게 대응할 수 있도록 일정한 주기를 반복하면서 개발하는 모형</p>
<ul>
<li>고객과의 소통에 초점을 맞춘 방법론을 통칭</li>
<li>폭포수 모형과 대조적</li>
<li>대표적인 개발 모형
: <strong>스크럼(Scrum)</strong>, <strong>XP(eXtreme Programming)</strong>, 기능 중심 개발(FDD; Feature Driven Development), 칸반(Kanban), Lean</li>
<li><strong>4가지 핵심 가치</strong><ul>
<li>방대한 문서보다는 실행되는 SW에 더 가치를 둔다</li>
<li>프로세스와 도구보다는 개인과 상호작용에 더 가치를 둔다</li>
<li>방대한 문서보다는 실행되는 SW에 더 가치를 둔다</li>
<li>계약 협상보다는 고객과의 협업에 더 가치를 둔다</li>
<li>계획을 따르기 보다는 변화에 반응하는 것에 더 가치를 둔다</li>
</ul>
</li>
</ul>
<h3 id="5-소프트웨어-공학se-software-engineering">5. 소프트웨어 공학(SE; Software Engineering)</h3>
<p>: 소프트웨어의 위기를 극복하기 위한 방안으로 연구된 학문 / 여러 가지 방법론과 도구, 관리 기법들을 통하여 소프트웨어의 품질과 생산성 향상을 목적</p>
<ul>
<li><strong>소프트웨어 공학의 기본 원칙</strong><ul>
<li><strong>현대적인 프로그래밍 기술</strong>을 계속적으로 <strong>적용</strong>해야 한다</li>
<li>개발된 소프트웨어의 <strong>품질이 유지</strong>되도록 <strong>지속적으로 검증</strong>해야 한다</li>
<li>소프트웨어 개발 관련 사항 및 결과에 대한 <strong>명확한 기록을 유지</strong>해야 한다</li>
</ul>
</li>
</ul>
<hr>
<h2 id="스크럼scrum-기법">스크럼(Scrum) 기법</h2>
<p>: <strong>팀이 중심이 되어 개발의 효율성을 높이는 기법</strong> / 팀원 스스로가 스크럼 팀을 구성하고 개발 작업에 관한 모든 것을 스스로 해결할 수 있어야 함</p>
<h3 id="스크럼-팀">스크럼 팀</h3>
<ul>
<li><strong>제품 책임자(PO; Product Owner)</strong>
: 요구사항이 담긴 백로그(Backlog)를 작성하는 주체, 이해관계자들 중 개발될 제품에 대한 이해도가 높고, 요구사항을 책임지고 의사를 결정할 사람으로 선정<ul>
<li>백로그: 제품 개발에 필요한 요구사항을 모두 모아 우선순위를 부여한 목록</li>
</ul>
</li>
<li><strong>스크럼 마스터(SM; Scrum Master)</strong>
: 스크럼 팀이 스크럼을 잘 수행하도록 가이드 역할</li>
<li><strong>개발팀(DT; Development Team)</strong>
: 제품 책임자와 스크럼 마스터를 제외한 모든 팀원으로 제품 개발 수행</li>
</ul>
<h3 id="스크럼-개발-프로세스-계진회검회">스크럼 개발 프로세스: &#39;계진회검회&#39;</h3>
<p>계획 --&gt; 진행(스프린트) --&gt; 회의 --&gt; 검토 --&gt; 회고</p>
<ul>
<li><strong>스프린트 계획 회의(Sprint Planning Meeting)</strong>: 제품 백로그 중 이번 스프린트에서 수행할 작업을 대상으로 단기 일정을 수립하는 회의</li>
<li><strong>스프린트(Sprint)</strong>: 실제 개발 작업 진행, 보통 2~4주</li>
<li><strong>일일 스크럼 회의(Daily Scrum Meeting)</strong>: 모든 팀원 매일 약 15분 동안 진행 상황 점검, 남은 작업 시간은 소멸 차트(Burn-down Chart)에 표시</li>
<li><strong>스프린트 검토 회의(Review)</strong>: 부분 또는 전체 완성 제품이 요구사항에 잘 부합하는지 테스팅</li>
<li><strong>스프린트 회고(Sprint Retrospective)</strong>: 규칙 준수 여부 및 개선점 확인 후 기록</li>
</ul>
<hr>
<h2 id="xpextreme-programming-기법">XP(eXtreme Programming) 기법</h2>
<p>: 수시로 발생하는 고객의 요구사항에 유연하게 대응하기 위해 고객 참여와 개발 과정의 반복을 극대화하여 개발 생산성을 향상시키는 방법</p>
<ul>
<li>짧고 반복적인 개발 주기, 단순한 설계, 고객의 적극적인 참여를 통해 소프트웨어를 빠르게 개발하는 것이 목적</li>
<li>릴리즈 기간을 짧게 반복하며 고객의 요구사항 반영에 대한 가시성을 높임</li>
<li>XP의 핵심 가치: <strong>&#39;의단용존피&#39;</strong><ul>
<li>의사소통(Communication)</li>
<li>단순성(Simplicity)</li>
<li>용기(Courage)</li>
<li>존중(Respect)</li>
<li>피드백(Feedback)</li>
</ul>
</li>
</ul>
<h3 id="xp-개발-프로세스-계진검출">XP 개발 프로세스: &#39;계진검출&#39;</h3>
<p>계획 --&gt; 진행(이터레이션) --&gt; 검사 --&gt; 출시(릴리즈)</p>
<ul>
<li>릴리즈 계획 수립(Release Planning)</li>
<li>이터레이션(Iteration)</li>
<li>승인 검사(Acceptance Test, 인수 테스트)</li>
<li>소규모 릴리즈(Small Release)</li>
</ul>
<h3 id="xp의-주요-실천-방법practice">XP의 주요 실천 방법(Practice)</h3>
<ul>
<li>Pair Programming(짝 프로그래밍)</li>
<li>Collective Ownership(공동 코드 소유)</li>
<li>Test-Driven Development(테스트 주도 개발)</li>
<li>Whole Team(전체 팀)</li>
<li>Continuous Integration(계속적인 통합)</li>
<li>Refactoring(리팩토링): 프로그램 기능 변경 없이 시스템을 재구성</li>
<li>Small Releases(소규모 릴리즈)</li>
</ul>
<hr>
<h2 id="개발-기술-환경-파악">개발 기술 환경 파악</h2>
<ul>
<li><p><strong>운영체제(OS; Operating System)</strong>
: 컴퓨터 시스템의 자원을 효율적으로 관리 / 사용자에게 편리한 환경을 제공하는 소프트웨어
고려사항: 가용성, 성능, 기술 지원, 주변 기기, 구축 비용</p>
</li>
<li><p><strong>데이터베이스 관리 시스템(DBMS; DataBase Management System)</strong>
: 사용자와 데이터베이스 사이에서 사용자의 요구에 따라 정보를 생성, 데이터베이스를 관리해 주는 소프트웨어 / 기존 파일 시스템이 갖는 데이터의 종속성과 중복성의 문제
고려사항: 가용성, 성능, 기술 지원, 상호 호환성, 구축 비용</p>
</li>
<li><p><strong>웹 어플리케이션 서버(WAS; Web Application Server)</strong>
: 사용자의 요구에 따라 변하는 동적인 콘텐츠를 처리하기 위해 사용되는 미들웨어 / 라이브러리 제공
고려사항: 가용성, 성능, 기술 지원, 구축 비용</p>
</li>
<li><p><strong>오픈 소스(Open Source)</strong>
: 누구나 별다은 제한 없이 사용할 수 있도록 소스 코드를 공개한 소프트웨어 / 오픈 소스 라이선스를 만족
고려사항: 라이선스의 종류, 사용자 수, 기술의 지속 가능성</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Pixels]_Procreate로 픽셀 아트하기]]></title>
            <link>https://velog.io/@nano_o/PixelsProcreate%EB%A1%9C-%ED%94%BD%EC%85%80-%EC%95%84%ED%8A%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@nano_o/PixelsProcreate%EB%A1%9C-%ED%94%BD%EC%85%80-%EC%95%84%ED%8A%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 23 Mar 2026 16:36:44 GMT</pubDate>
            <description><![CDATA[<p>요즘 픽셀 아트 생성기가 워낙 잘 되어 있지만, <strong>직접</strong> 그리고 싶을 때가 있다. (특히 에셋 만들 때......)</p>
<p>평소에는 템플릿을 가져올 때도 있고, 아이콘 리소스가 많은 사이트도 자주 이용한다. 그러나 손맛이 필요할 때, 나는 <strong>Procreate</strong>를 이용하는 편이다.</p>
<p>그래서 오늘은 <strong>픽셀 아트 그리는 방법</strong>을 준비했다.</p>
<hr>
<h2 id="step-1_브러시-제작">[Step 1]_브러시 제작</h2>
<p>우선 <del>어떤 귀인께서 올려주신</del> <a href="https://www.youtube.com/watch?v=_6KmocVyEjo">유튜브 링크</a>를 참고하여 픽셀을 그릴 수 있는 브러시를 만들어야 한다. 참고로 귀찮은 사람은 <strong><a href="https://drive.google.com/file/d/1uUlixBGwXhQB0ZgQyXqmMYC36ZYR4U_W/view?usp=sharing">브러시 공유 구글 드라이브</a></strong>에 들어가서 내가 만들어 둔 브러시를 사용하기 바란다.</p>
<p>다운로드 한 다음, 프로크리에이트로 내보내기하면 된다. 참고로 메뉴 들어가서 <strong>Imported</strong>에 들어가면 다운받은 것을 확인할 수 있다. 이름은 Pixel이다.
<img src="https://velog.velcdn.com/images/nano_o/post/022b6e9d-81b9-4335-8460-f8bc46c939e4/image.png" width="400"/></p>
<hr>
<h2 id="step-2_new-canvas">[Step 2]_New Canvas</h2>
<p> 커스텀에 들어가서 그리기 쉬운 사이즈로 설정해야 한다. 작게 그리고 싶으면 <code>10 x 10</code>이나<code>20 x 20</code> 정도를 추천한다. 크게 그리고 싶으면 50이나 100 이상도 좋다.</p>
<p> 일단 나는 <strong>10 px</strong>로 했다.
<img src="https://velog.velcdn.com/images/nano_o/post/0c6f0737-b527-43d2-a282-1e35d107e186/image.png" width="500"/></p>
<hr>
<h2 id="step-3_drawing-guide">[Step 3]_Drawing Guide</h2>
<p> 그리기 전에 <strong>Actions</strong>에 들어가서 <strong>Drawing Guide</strong>를 켜준다. 그러면 아래에 편집하기 옵션이 뜨는데 눌러준다.
<img src="https://velog.velcdn.com/images/nano_o/post/6b8cdcfe-8072-4289-aa75-0226eff87315/image.png" width="500"/></p>
<p><strong>그리드 사이즈</strong>는 <strong>0</strong>이 되게 만들어 준다. 그럼 <strong>None</strong>으로 뜰 것이다. 브러시를 가장 작은 사이즈로 했을 때, 한 번의 터치마다 한 칸이 채워진다. (만약 캔버스의 크기를 크게 생성했다면 라인의 크기를 적당히 조절해야 한다.)</p>
<p>상단의 <strong>색상 바</strong>를 통해서 가이드 라인의 색상을 지정할 수 있다.
<img src="https://velog.velcdn.com/images/nano_o/post/fd919934-86ee-4421-8084-4c76fbe6fc3c/image.png" width="500"/></p>
<hr>
<h2 id="step-4_resize-settings">[Step 4]_Resize Settings</h2>
<p> Procreate의 사용법은 이미 알고 있으리라 생각한다.
 (보통 그림은 <strong>레이어</strong>를 나눠서 그린다. 이번 그림은 단순해서 한 레이어에 색칠까지 다 했다.) 
 <img src="https://velog.velcdn.com/images/nano_o/post/7fb0d655-3f1a-4c0f-8f28-8ea9b2d9f0f4/image.png" width="500"/></p>
<p> 그림을 알아서 잘 그렸다면 위처럼 완성된 이미지가 있을 것이다. 그럼 다시 <strong>Actions</strong>에 들어가서 이번에는 <strong>사이즈 조절</strong>을 해준다. 이걸 안하면 완성된 이미지를 내보내기할 때 해상도가 처참하게 박살나는 것을 볼 수 있다. <del>궁금하면 그대로 내보내기 추천!</del>
 <img src="https://velog.velcdn.com/images/nano_o/post/484c85c2-3178-4998-a209-cac12f03c0fa/image.png" width="500"/></p>
<p> 내가 필요한 사이즈로 조절해준다. 일반적인 앱 아이콘은 <code>1024 x 1024</code>가 권장 사이즈이다.
 <img src="https://velog.velcdn.com/images/nano_o/post/70bb05dd-f2ff-475b-aed2-ecb9d8ae2af2/image.png" width="500"/></p>
<hr>
<h2 id="step-5_fit-to-canvas">[Step 5]_Fit to Canvas</h2>
<p>사이즈 조절이 끝난 직후의 그림은... 되게 웃긴 상태일 것이다. <del>코딱지만하다.</del>
 <img src="https://velog.velcdn.com/images/nano_o/post/37d5e88d-852b-4c5b-9779-398857986532/image.png" width="500"/></p>
<p><strong>여기서부터 중요하다!</strong> 마우스 버튼 같은 선택 도구, <strong>Selections</strong>에 들어가서 <strong>유니폼</strong>하게 사이즈를 조절할 수 있도록 수정하고, 특히!! <strong>Interpolation(보간법)</strong>을 <strong>Nearest Neighbour(최근접 이웃)</strong>로 설정한다.</p>
<p>픽셀 아트의 경우에 꼭 필요한데, 평균이나 보간을 안 하기 때문에 색이 섞이지 않아서 <strong>이미지 리사이즈(확대) 시, 값 왜곡이 발생하지 않는다.</strong> 더불어 블러 효과가 생기지 않아서 <strong>경계가 또렷하게 유지</strong>된다는 장점이 있다.
 <img src="https://velog.velcdn.com/images/nano_o/post/2afee8e9-3af2-4ea5-b2bd-779afc97d53b/image.png" width="500"/></p>
<p>다 따라했으면 <strong>Fit to Canvas</strong>를 클릭한다.</p>
<hr>
<h2 id="step-6_share-layers">[Step 6]_Share Layers</h2>
<p><strong>Fit to Canvas</strong> 직후에는 흐려 보이지만, <strong>Selection</strong> 버튼 클릭을 해제하면 이미지가 잘 확대된 것을 확인할 수 있다.</p>
<div style="display: flex; justify-content: center; gap: 10px;">
 <img src="https://velog.velcdn.com/images/nano_o/post/ee9916f4-966f-4972-b9e7-c4cdae276e24/image.png" width="300"/>
  <img src="https://velog.velcdn.com/images/nano_o/post/7bc0e233-56b2-43ca-9735-90f97d8962e0/image.png" width="300"/>
</div>

<p>드디어 마지막!!
이제 <strong>내보내기</strong>만 남았다. <strong>레이어를 모두 보이게 표시하고, 배경색을 숨김 처리</strong>한다.
 <img src="https://velog.velcdn.com/images/nano_o/post/55e9b18d-1ffd-4bca-ac06-bb77bcc5a9d2/image.png" width="500"/></p>
<p>다시 <strong>Actions</strong>에 들어가서 Share Image 말고, <strong>Share Layers</strong>의 <strong>PNg Files</strong>로 내보내기 하면 각각의 레이어들이 한 번에 내보내진다.
 <img src="https://velog.velcdn.com/images/nano_o/post/8a131fc2-431c-44a2-a6ab-558e29a3976e/image.png" width="500"/></p>
<hr>
<h2 id="마치며">[마치며]</h2>
<p>픽셀 아트는 단순해 보이지만, 작은 점들이 모여 하나의 그림이 되는 과정에서 생각보다 많은 섬세함이 필요하다. 제한된 해상도 안에서 표현해야 하기 때문에 오히려 더 창의적으로 고민하게 되는 부분도 많았다.</p>
<p>지금 블로그에 예시로 그린 그림들은 앱 개발에 사용할 에셋들이다. 어떻게 사용될지는 추후 블로그에 올릴 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 Java]_신촌 통폐합 계획 (31423)]]></title>
            <link>https://velog.io/@nano_o/%EB%B0%B1%EC%A4%80-Java%EC%8B%A0%EC%B4%8C-%ED%86%B5%ED%8F%90%ED%95%A9-%EA%B3%84%ED%9A%8D-31423</link>
            <guid>https://velog.io/@nano_o/%EB%B0%B1%EC%A4%80-Java%EC%8B%A0%EC%B4%8C-%ED%86%B5%ED%8F%90%ED%95%A9-%EA%B3%84%ED%9A%8D-31423</guid>
            <pubDate>Wed, 18 Mar 2026 19:49:44 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-정보">문제 정보</h2>
<ul>
<li>출처 및 난이도: 백준 골드5</li>
<li>링크: <a href="https://www.acmicpc.net/problem/31423">https://www.acmicpc.net/problem/31423</a></li>
<li>분류: 문자열, 그래프 이론, DFS, <strong>연결 리스트</strong>, 자료구조</li>
</ul>
<hr>
<h2 id="문제-요약">문제 요약</h2>
<blockquote>
<ul>
<li>N개 대학교 이름이 주어질 때, N-1번의 합치기 연산 <code>(i, j)</code> 를 순서대로 수행</li>
<li>s_i 뒤에 s_j를 이어 붙이고 s_j를 빈 문자열로 만든 뒤, 최종 이름을 출력하는 문제.</li>
</ul>
</blockquote>
<hr>
<h2 id="풀이-접근">풀이 접근</h2>
<p><strong>시도 1 - <code>LinkedList&lt;String&gt;</code></strong></p>
<ul>
<li>직관적으로 연결 리스트 사용</li>
<li>합치기 연산마다 리스트 탐색이 필요해 <strong>시간 초과</strong></li>
</ul>
<p><strong>시도 2 - <code>ArrayList&lt;StringBuilder&gt;</code></strong></p>
<ul>
<li><code>list.get(i).append(list.get(j))</code>로 문자열 직접 합치기</li>
<li>N이 최대 100,000이고 이름 길이도 길어서 합칠 때마다 문자열 복사 발생 → O(N²) 시간 초과</li>
</ul>
<p><strong>최종 - <code>next[] + tail[] 배열로 연결 리스트 구현</code></strong></p>
<ol>
<li><code>next[i]</code>: i번 대학 다음에 연결된 대학 번호 (초기값 -1)</li>
<li><code>tail[i]</code>: i번 체인의 마지막 노드 번호 (초기값 자기 자신 i)</li>
<li>합치기 <code>(i, j)</code> 시:<ul>
<li><code>next[tail[i]] = j</code> → i 체인 끝에 j 체인 연결</li>
<li><code>tail[i] = tail[j]</code> → i 체인의 꼬리 갱신</li>
</ul>
</li>
<li>마지막 answer(=i)부터 next 따라가며 이름 출력</li>
</ol>
<hr>
<h2 id="연결-리스트">연결 리스트?</h2>
<p><strong>연결 리스트(Linked List)</strong>는 데이터를 저장할 때 하나의 데이터와 그 다음 데이터로의 위치를 함께 저장하여 <strong>논리적으로 연결(link)</strong>하는 방식으로 자료를 저장한다. </p>
<p>데이터는 논리적으로 연결되어 있으므로 배열과 달리 데이터의 삽입 삭제가 자유로워지면서 자연스럽게 전체 크기를 늘리고 줄이는 것 또한 가능해진다.</p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/27c1aeb2-9b42-4fb9-be2e-d1de47f99061/image.png" alt=""></p>
<p>보통 <code>node로 저장</code>을 하고, 그 안에 <code>데이터(data)</code>와 <code>다음 노드의 주소나 참조(next)</code>를 담는다. 즉, <strong>node: data + next</strong></p>
<br>

<p>Java에서는 LinkedList 클래스를 기본으로 제공한다.</p>
<pre><code class="language-java">import java.util.LinkedList;

LinkedList&lt;String&gt; list = new LinkedList&lt;&gt;();

// 삽입
list.add(&quot;A&quot;);          // 맨 뒤
list.addFirst(&quot;B&quot;);     // 맨 앞
list.addLast(&quot;C&quot;);      // 맨 뒤

// 조회
list.get(0);            // 인덱스로 접근
list.getFirst();        // 첫 번째 요소
list.getLast();         // 마지막 요소

// 삭제
list.remove(0);         // 인덱스로 삭제
list.removeFirst();     // 첫 번째 삭제
list.removeLast();      // 마지막 삭제

// 크기
list.size();</code></pre>
<p><strong>이중 연결 리스트(Doubly Linked List) 구조로 구현</strong>되어 있다. 심지어 List와 Deque 인터페이스를 모두 구현하므로, 리스트뿐 아니라 스택/큐/덱으로도 활용 가능하다.</p>
<hr>
<h2 id="핵심-아이디어">핵심 아이디어</h2>
<ul>
<li>실제로 문자열을 합치지 않고 <strong>포인터만 연결</strong> → 합치기 연산이 O(1)</li>
<li><code>tail[]</code> 배열이 핵심: 리스트 끝을 항상 O(1)에 알 수 있어서 이어붙이기가 빠름</li>
<li>출력할 때만 next를 따라가며 StringBuilder에 append → 전체 O(N)</li>
</ul>
<hr>
<h2 id="코드">코드</h2>
<h3 id="span-stylecolor-darkmagenta시도-1span"><span style="color: darkmagenta;">&lt;시도 1&gt;</span></h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;

class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        LinkedList&lt;String&gt; list = new LinkedList&lt;&gt;();
        // 이 문제는 인덱스 1부터 시작
        list.add(&quot;&quot;);

        int N = Integer.parseInt(br.readLine());
        for (int temp = 0; temp &lt; N; temp++) list.add(br.readLine());

        String s = &quot;&quot;;
        for (int temp = 0; temp &lt; N-1; temp++) {
            String[] nums = br.readLine().split(&quot; &quot;);
            int i = Integer.parseInt(nums[0]);
            int j = Integer.parseInt(nums[1]);
            list.set(i, list.get(i) + list.get(j));
            if (temp == N - 2) s = list.get(i);
        }

        System.out.println(s);
    }
}</code></pre>
<h3 id="span-stylecolor-darkmagenta시도-2span"><span style="color: darkmagenta;">&lt;시도 2&gt;</span></h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;

class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        ArrayList&lt;StringBuilder&gt; list = new ArrayList&lt;&gt;();
        // 이 문제는 인덱스 1부터 시작
        list.add(new StringBuilder());

        int N = Integer.parseInt(br.readLine());
        for (int temp = 0; temp &lt; N; temp++) list.add(new StringBuilder(br.readLine()));

        int answer = 0;
        for (int temp = 0; temp &lt; N-1; temp++) {
            String[] nums = br.readLine().split(&quot; &quot;);
            int i = Integer.parseInt(nums[0]);
            int j = Integer.parseInt(nums[1]);
            list.get(i).append(list.get(j));
            list.get(j).setLength(0);
            answer = i;
        }

        System.out.println(list.get(answer));
    }
}</code></pre>
<h3 id="span-stylecolor-darkmagenta최종span"><span style="color: darkmagenta;">&lt;최종&gt;</span></h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int N = Integer.parseInt(br.readLine());

        String[] unis = new String[N + 1];
        int[] next = new int[N + 1];
        int[] tail = new int[N + 1];

        for (int i = 1; i &lt;= N; i++) {
            unis[i] = br.readLine();
            next[i] = -1;
            tail[i] = i;
        }

        int answer = 0;
        for (int temp = 0; temp &lt; N - 1; temp++) {
            String[] nums = br.readLine().split(&quot; &quot;);
            int i = Integer.parseInt(nums[0]);
            int j = Integer.parseInt(nums[1]);

            next[tail[i]] = j;
            tail[i] = tail[j];
            answer = i;
        }
        StringBuilder sb = new StringBuilder();
        int next_num = answer;
        while (next_num != -1) {
            sb.append(unis[next_num]);
            next_num = next[next_num];
        }
        System.out.print(sb);
    }
}</code></pre>
<p><strong><span style="color: navy;">&lt;초기 배열 상태&gt;</span></strong>
<strong><code>unis</code>:</strong> 0 1 2 3 4 5
<strong><code>next</code>:</strong> -1 -1 -1 -1 -1 -1
<strong><code>tail</code>:</strong> 0 1 2 3 4 5</p>
<p><strong><span style="color: navy;">&lt;최종 배열 상태&gt;</span></strong>
<strong><code>unis</code>:</strong> 0 1 2 3 4 5
<strong><code>next</code>:</strong> -1 2 3 4 5 -1
<strong><code>tail</code>:</strong> 0 5 3 3 5 5  </p>
<hr>
<h2 id="배운-점--회고">배운 점 / 회고</h2>
<p><strong>드디어 풀었다! 복병 해결</strong></p>
<p><img src="https://velog.velcdn.com/images/nano_o/post/1acfd2b5-dc43-41b0-a32b-943ef61c1534/image.png" alt=""></p>
<ul>
<li><strong>문자열을 실제로 합치면 무조건 시간 초과</strong>가 발생한다.</li>
<li>핵심은 &quot;합치는 척만 하고 포인터만 연결&quot;하는 것. 실제 합치기는 출력할 때 딱 한 번만 하는 것이다.</li>
<li>LinkedList → StringBuilder → 포인터 배열 구현 순서로 최적화해가는 과정 자체가 좋은 공부였다고 생각한다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>