<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>allluck777</title>
    <link>https://allluck777.tistory.com/</link>
    <description>allluck777</description>
    <language>ko</language>
    <pubDate>Mon, 6 Apr 2026 07:56:28 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>allluck777</managingEditor>
    <image>
      <title>allluck777</title>
      <url>https://tistory1.daumcdn.net/tistory/8492995/attach/34c65d372a64443eba3bad733f26bbb3</url>
      <link>https://allluck777.tistory.com</link>
    </image>
    <item>
      <title>[멋사 클라우드 5기] Day 37 &amp;amp; 38 - Linux (3)</title>
      <link>https://allluck777.tistory.com/41</link>
      <description>&lt;h1&gt;Chapter 18. 환경변수 &amp;amp; 셸 설정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;환경변수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경변수(Environment Variable)는 운영체제가 프로세스에게 전달하는 &lt;b&gt;이름=값&lt;/b&gt; 형태의 설정 정보이다. 프로세스는 환경변수를 통해 자신이 실행되는 환경에 대한 정보를 얻는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경변수는 리눅스뿐만 아니라 모든 운영체제에 존재하는 개념이며, &lt;b&gt;애플리케이션 설정을 코드와 분리&lt;/b&gt;하는 핵심 메커니즘이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;셸 변수 vs 환경변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘은 자주 혼동되지만 범위가 다르다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;셸 변수&lt;/b&gt;: 현재 셸 안에서만 유효하다. 자식 프로세스에 전달되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경변수&lt;/b&gt;: &lt;code&gt;export&lt;/code&gt;로 내보내면 &lt;b&gt;자식 프로세스에도 상속&lt;/b&gt;된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 셸 변수 (현재 셸에서만 유효)
MY_VAR=&quot;hello&quot;
echo $MY_VAR           # hello
bash -c 'echo $MY_VAR' # (빈 출력 &amp;mdash; 자식 셸에 전달되지 않음)

# 환경변수 (자식 프로세스에도 전달)
export MY_VAR=&quot;hello&quot;
echo $MY_VAR           # hello
bash -c 'echo $MY_VAR' # hello (자식 셸에서도 접근 가능)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;환경변수 확인 및 설정&lt;/h2&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 모든 환경변수 출력
env
printenv

# 특정 환경변수 확인
echo $HOME
echo $PATH
echo $USER
printenv HOME

# 현재 셸의 모든 변수 (환경변수 + 셸 변수 + 함수)
set&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 및 해제&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 환경변수 설정 (현재 셸 + 자식 프로세스)
export DB_HOST=&quot;localhost&quot;
export DB_PORT=5432

# 한 줄로 설정과 export 동시에
export APP_ENV=&quot;production&quot;

# 변수 해제 (삭제)
unset DB_HOST

# 특정 명령어에만 일시적으로 환경변수 설정 ★
DB_HOST=remotedb APP_ENV=staging ./myapp
# myapp은 DB_HOST=remotedb로 실행되지만, 현재 셸에는 영향 없음&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 시스템 환경변수&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;변수&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 사용자의 홈 디렉터리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home/devops&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 사용자 이름&lt;/td&gt;
&lt;td&gt;&lt;code&gt;devops&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SHELL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 사용자의 기본 셸&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/bin/bash&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실행 파일 검색 경로&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/usr/local/bin:/usr/bin:/bin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PWD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 작업 디렉터리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home/devops/projects&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LANG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;시스템 로캘(언어) 설정&lt;/td&gt;
&lt;td&gt;&lt;code&gt;en_US.UTF-8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TERM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;터미널 종류&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xterm-256color&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EDITOR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기본 텍스트 편집기&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vim&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOSTNAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;호스트명&lt;/td&gt;
&lt;td&gt;&lt;code&gt;web-server-01&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 사용자 ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;PATH&lt;/code&gt; &amp;mdash; 가장 중요한 환경변수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PATH&lt;/code&gt;는 셸이 명령어를 찾을 때 &lt;b&gt;검색하는 디렉터리 목록&lt;/b&gt;이다.&lt;br /&gt;&lt;code&gt;:&lt;/code&gt;으로 구분된 디렉터리 경로의 나열이며, &lt;b&gt;왼쪽부터 순서대로&lt;/b&gt; 검색한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# ls를 실행하면 셸이 다음 순서로 검색:
# 1. /usr/local/sbin/ls &amp;rarr; 없음
# 2. /usr/local/bin/ls &amp;rarr; 없음
# 3. /usr/sbin/ls &amp;rarr; 없음
# 4. /usr/bin/ls &amp;rarr; 있음! &amp;rarr; 실행&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PATH에 경로 추가&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# PATH 끝에 추가 (기존 PATH 뒤에)
export PATH=&quot;$PATH:/opt/myapp/bin&quot;

# PATH 앞에 추가 (우선순위 높게)
export PATH=&quot;/opt/myapp/bin:$PATH&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;앞에 추가 vs 뒤에 추가:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞에 추가하면 시스템 기본 명령어보다 &lt;b&gt;우선&lt;/b&gt; 실행된다&lt;/li&gt;
&lt;li&gt;뒤에 추가하면 시스템 기본 명령어를 그대로 사용하되, 새 경로는 &lt;b&gt;보조&lt;/b&gt;로 사용된다&lt;/li&gt;
&lt;li&gt;보안상 일반적으로 &lt;b&gt;뒤에 추가&lt;/b&gt;하는 것이 안전하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;command not found&quot; 문제 해결&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 1. 명령어가 어디에 있는지 확인
which python3
type python3

# 2. 명령어가 설치되어 있는지 확인
find / -name &quot;python3&quot; -type f 2&amp;gt;/dev/null

# 3. 찾은 경로가 PATH에 있는지 확인
echo $PATH | tr ':' '\n' | grep &quot;/usr/local/bin&quot;

# 4. 없으면 PATH에 추가
export PATH=&quot;$PATH:/found/path&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;히스토리 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bash는 실행한 명령어를 &lt;b&gt;&lt;code&gt;~/.bash_history&lt;/code&gt;&lt;/b&gt; 파일에 저장한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 히스토리 확인
history
history 20          # 최근 20개

# 히스토리에서 검색
history | grep &quot;docker&quot;

# 이전 명령어 재실행
!!                  # 직전 명령어 재실행
!123                # 히스토리 번호 123 재실행
!docker             # &quot;docker&quot;로 시작하는 가장 최근 명령어 재실행

# 역방향 검색 (가장 유용!) ★★★
# Ctrl + R &amp;rarr; 검색어 입력 &amp;rarr; 매치되면 Enter로 실행
# Ctrl + R을 반복 누르면 이전 매치로 이동&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보안: 히스토리에 민감한 정보 남기지 않기&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 방법 1: 명령어 앞에 공백을 넣으면 히스토리에 기록되지 않음 (HISTCONTROL=ignorespace 필요)
 export DB_PASSWORD=&quot;secret123&quot;     # 맨 앞에 공백

# 방법 2: 환경변수 파일에서 읽기
source /etc/myapp/env              # 파일에서 로드

# 방법 3: 현재 세션의 히스토리 삭제
history -c &amp;amp;&amp;amp; history -w&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 19. 사용자 &amp;amp; 그룹 관리&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리눅스의 사용자 체계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 크게 세 가지로 분류된다&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;UID 범위&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;root&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;시스템 최고 관리자. 모든 권한을 가진다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시스템 사용자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1~999&lt;/td&gt;
&lt;td&gt;서비스(데몬)를 실행하기 위한 계정. 직접 로그인하지 않는다. nginx, mysql, www-data 등.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;일반 사용자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1000~&lt;/td&gt;
&lt;td&gt;사람이 로그인하여 사용하는 계정.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UID(User ID) 범위는 배포판마다 약간 다를 수 있지만, 1000부터 일반 사용자가 시작되는 것은 대부분 공통이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 정보가 저장되는 파일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;/etc/passwd&lt;/code&gt;&lt;/b&gt; &amp;mdash; 사용자 계정 정보&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
devops:x:1000:1000:DevOps User:/home/devops:/bin/bash
nginx:x:33:33:nginx,,,:/nonexistent:/usr/sbin/nologin&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;devops : x : 1000 : 1000 : DevOps User : /home/devops : /bin/bash
  │      │    │      │         │              │             │
  │      │    │      │         │              │             └─ 로그인 셸
  │      │    │      │         │              └─ 홈 디렉터리
  │      │    │      │         └─ 코멘트 (이름, 설명)
  │      │    │      └─ GID (기본 그룹 ID)
  │      │    └─ UID (사용자 ID)
  │      └─ 비밀번호 (x = /etc/shadow에 저장)
  └─ 사용자명&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;/etc/shadow&lt;/code&gt;&lt;/b&gt; &amp;mdash; 비밀번호 해시 (root만 읽기 가능)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ sudo cat /etc/shadow
devops:$6$rounds=656000$salt...$hash...:19400:0:99999:7:::&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;사용자명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;비밀번호 해시 (&lt;code&gt;!&lt;/code&gt; 또는 &lt;code&gt;*&lt;/code&gt;면 로그인 불가)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;마지막 비밀번호 변경일 (1970-01-01부터의 일수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;최소 변경 간격 (일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;최대 유효 기간 (일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;만료 경고 기간 (일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;비활성화 기간 (일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;계정 만료일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;/etc/group&lt;/code&gt;&lt;/b&gt; &amp;mdash; 그룹 정보&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ cat /etc/group
devteam:x:1001:devops,deploy,alice
docker:x:999:devops&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;devteam : x : 1001 : devops,deploy,alice
   │      │    │          │
   │      │    │          └─ 그룹 멤버 목록
   │      │    └─ GID (그룹 ID)
   │      └─ 그룹 비밀번호 (거의 사용하지 않음)
   └─ 그룹명&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자 관리 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;useradd&lt;/code&gt; &amp;mdash; 사용자 생성&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 기본 사용자 생성
sudo useradd devops

# 실무에서 사용하는 전형적인 옵션 조합 ★
sudo useradd -m -s /bin/bash -c &quot;DevOps Engineer&quot; devops
#  -m : 홈 디렉터리 자동 생성 (/home/devops)
#  -s : 로그인 셸 지정
#  -c : 코멘트 (사용자 설명)

# 비밀번호 설정
sudo passwd devops

# 특정 UID/GID 지정
sudo useradd -m -u 1500 -g devteam -s /bin/bash deploy

# 시스템 사용자 생성 (서비스용, 로그인 불가)
sudo useradd -r -s /usr/sbin/nologin -d /nonexistent myservice
#  -r : 시스템 사용자 (UID &amp;lt; 1000)
#  -s /usr/sbin/nologin : 로그인 차단
#  -d /nonexistent : 홈 디렉터리 없음

# 사용자 생성 + sudo 그룹에 추가 (Ubuntu)
sudo useradd -m -s /bin/bash -G sudo newadmin
sudo passwd newadmin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;useradd&lt;/code&gt; vs &lt;code&gt;adduser&lt;/code&gt;:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;useradd&lt;/code&gt;: 저수준 명령어. 옵션을 직접 지정해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;adduser&lt;/code&gt;: Debian/Ubuntu에서 제공하는 대화형 래퍼 스크립트. 비밀번호 설정, 홈 디렉터리 생성 등을 자동으로 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;usermod&lt;/code&gt; &amp;mdash; 사용자 수정&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 사용자를 추가 그룹에 추가 ★★★
sudo usermod -aG docker devops
#  -a : append (기존 그룹 유지하면서 추가)
#  -G : 보조 그룹 지정

# ⚠️ -a를 빼먹으면 기존 보조 그룹이 모두 제거된다!
sudo usermod -G docker devops    # 위험: docker만 남고 나머지 그룹 제거

# 로그인 셸 변경
sudo usermod -s /bin/zsh devops

# 사용자명 변경
sudo usermod -l newname oldname

# 홈 디렉터리 변경 (기존 파일도 이동)
sudo usermod -d /new/home -m devops

# 계정 잠금 / 해제
sudo usermod -L devops    # Lock (비밀번호 앞에 ! 추가)
sudo usermod -U devops    # Unlock&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그룹 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 사용자는 기본 그룹(primary group)과 0개 이상의 보조 그룹(supplementary groups)을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 그룹&lt;/b&gt;: 사용자가 파일을 생성할 때 적용되는 그룹. &lt;code&gt;/etc/passwd&lt;/code&gt;의 GID 필드.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보조 그룹&lt;/b&gt;: 추가적인 권한을 부여하기 위한 그룹. &lt;code&gt;/etc/group&lt;/code&gt;에 멤버로 등록.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;# 현재 사용자의 그룹 정보 확인
id
# uid=1000(devops) gid=1000(devops) groups=1000(devops),27(sudo),999(docker)

# 특정 사용자의 그룹 확인
id deploy
groups deploy&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그룹 관리 명령어&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 그룹 생성
sudo groupadd devteam
sudo groupadd -g 2000 devteam    # GID 지정

# 그룹에 사용자 추가 ★
sudo usermod -aG devteam devops
sudo usermod -aG devteam alice
sudo usermod -aG devteam deploy

# 그룹에서 사용자 제거
sudo gpasswd -d alice devteam

# 그룹 삭제
sudo groupdel devteam

# 그룹명 변경
sudo groupmod -n newname oldname&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그룹 변경 즉시 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;usermod -aG&lt;/code&gt;로 그룹을 추가해도 &lt;b&gt;현재 세션에는 즉시 반영되지 않는다&lt;/b&gt;. 로그아웃/로그인을 해야 적용된다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;# 그룹 추가 후 확인
sudo usermod -aG docker devops
groups     # 아직 docker가 안 보일 수 있음

# 방법 1: 로그아웃 후 재로그인 (가장 확실)
# 방법 2: newgrp으로 임시 적용
newgrp docker
groups     # 이제 docker가 보임&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그룹 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker 그룹&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker를 &lt;code&gt;sudo&lt;/code&gt; 없이 사용하려면 사용자를 &lt;code&gt;docker&lt;/code&gt; 그룹에 추가해야 한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;sudo usermod -aG docker devops
# 로그아웃 후 재로그인
docker ps    # sudo 없이 실행 가능&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sudo 그룹&lt;/h3&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;# Ubuntu: sudo 그룹에 추가
sudo usermod -aG sudo newadmin

# RHEL/Fedora: wheel 그룹에 추가
sudo usermod -aG wheel newadmin&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 전용 사용자 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션은 &lt;b&gt;전용 사용자&lt;/b&gt;로 실행하는 것이 보안 best practice이다.&lt;br /&gt;root로 실행하면 취약점 발생 시 시스템 전체가 위험해진다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 서비스 전용 사용자 생성
sudo useradd -r -s /usr/sbin/nologin -d /opt/myapp -c &quot;MyApp service&quot; myapp

# 애플리케이션 디렉터리 소유자 설정
sudo chown -R myapp:myapp /opt/myapp

# systemd 서비스에서 해당 사용자로 실행
# [Service]
# User=myapp
# Group=myapp&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자 전환&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;su&lt;/code&gt; &amp;mdash; Switch User&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# root로 전환 (root 비밀번호 필요)
su -

# 특정 사용자로 전환
su - deploy
# deploy 사용자의 비밀번호 입력

# 현재 환경 유지하면서 전환 (- 없이)
su deploy
# 환경변수, 작업 디렉터리가 변경되지 않음

# 명령어 하나만 다른 사용자로 실행
su - deploy -c &quot;whoami&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;su -&lt;/code&gt; vs &lt;code&gt;su&lt;/code&gt; (하이픈의 차이):&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;su -&lt;/code&gt; (또는 &lt;code&gt;su -l&lt;/code&gt;, &lt;code&gt;su --login&lt;/code&gt;): 대상 사용자의 &lt;b&gt;로그인 셸을 완전히 재현&lt;/b&gt;한다. 환경변수, HOME, PATH 등이 대상 사용자의 것으로 설정된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;su&lt;/code&gt; (하이픈 없이): 사용자만 전환하고, &lt;b&gt;현재 환경을 유지&lt;/b&gt;한다. PATH가 원래 사용자의 것이라서 혼란이 생길 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;su -&lt;/code&gt;를 사용&lt;/b&gt;하는 것이 권장된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;sudo su -&lt;/code&gt; vs &lt;code&gt;sudo -i&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 둘 다 root 셸로 전환 (sudo 비밀번호 사용)
sudo su -
sudo -i

# 차이점은 미미하지만, sudo -i가 더 직접적이고 감사 로그에 더 잘 남는다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자 정보 확인 명령어&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 현재 사용자 확인
whoami
# devops

# 현재 로그인한 모든 사용자
who
# devops   pts/0    2025-03-23 10:00 (192.168.1.100)
# deploy   pts/1    2025-03-23 11:30 (192.168.1.101)

# 더 자세한 정보
w
# USER     TTY      FROM             LOGIN@   IDLE   WHAT
# devops   pts/0    192.168.1.100    10:00    0.00s  vim config.yaml
# deploy   pts/1    192.168.1.101    11:30    2:15   bash

# 사용자/그룹 ID 정보
id
id deploy

# 마지막 로그인 기록
last | head -20
last devops

# 마지막 로그인 실패 기록
sudo lastb | head -20&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시나리오&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새 팀원 온보딩&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
USERNAME=$1
FULLNAME=$2

# 사용자 생성
sudo useradd -m -s /bin/bash -c &quot;$FULLNAME&quot; $USERNAME

# 임시 비밀번호 설정 + 다음 로그인 시 변경 강제
echo &quot;${USERNAME}:TempPass123!&quot; | sudo chpasswd
sudo chage -d 0 $USERNAME

# 필요한 그룹에 추가
sudo usermod -aG sudo $USERNAME
sudo usermod -aG docker $USERNAME
sudo usermod -aG devteam $USERNAME

# SSH 키 디렉터리 설정
sudo mkdir -p /home/$USERNAME/.ssh
sudo chmod 700 /home/$USERNAME/.ssh
sudo chown -R $USERNAME:$USERNAME /home/$USERNAME/.ssh

echo &quot;User $USERNAME created successfully&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;퇴사자 계정 처리&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
USERNAME=$1

# 1. 계정 잠금
sudo usermod -L $USERNAME

# 2. 실행 중인 프로세스 종료
sudo pkill -u $USERNAME

# 3. crontab 제거
sudo crontab -r -u $USERNAME

# 4. 홈 디렉터리 백업
sudo tar -czf /backup/departed/${USERNAME}-$(date +%Y%m%d).tar.gz /home/$USERNAME

# 5. (확인 후) 계정 삭제
# sudo userdel -r $USERNAME

echo &quot;Account $USERNAME has been locked and backed up&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 20. 패키지 관리&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패키지 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지(Package)란 소프트웨어를 설치하기 위해 필요한 파일들(바이너리, 라이브러리, 설정 파일, 문서 등)을 하나로 묶은 것이다. 패키지 매니저는 이 패키지를 설치, 업데이트, 삭제하는 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 매니저가 없다면 소프트웨어를 설치할 때마다 소스 코드를 다운로드하고, 의존성 라이브러리를 직접 찾아 설치하고, 컴파일해야 한다. 패키지 매니저가 이 모든 과정을 자동화한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 매니저의 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;의존성 자동 해결&lt;/b&gt;: A 패키지가 B 라이브러리를 필요로 하면 B를 자동 설치&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버전 관리&lt;/b&gt;: 패키지의 설치, 업그레이드, 다운그레이드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저장소 관리&lt;/b&gt;: 어디서 패키지를 다운로드할지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무결성 검증&lt;/b&gt;: 패키지의 서명을 확인하여 변조 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포판별 패키지 시스템&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;배포판 계열&lt;/th&gt;
&lt;th&gt;패키지 형식&lt;/th&gt;
&lt;th&gt;저수준 도구&lt;/th&gt;
&lt;th&gt;고수준 도구&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Debian/Ubuntu&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.deb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dpkg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apt&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RHEL/Fedora&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.rpm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rpm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;dnf&lt;/code&gt;&lt;/b&gt; (이전: &lt;code&gt;yum&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alpine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.apk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;apk&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저수준 vs 고수준&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;저수준(dpkg, rpm)&lt;/b&gt;: 개별 패키지 파일을 직접 처리. 의존성을 자동 해결하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고수준(apt, dnf)&lt;/b&gt;: 저장소에서 패키지를 검색하고, 의존성을 자동으로 해결한다. &lt;b&gt;실무에서는 거의 항상 고수준 도구를 사용한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;APT &amp;mdash; Debian/Ubuntu 패키지 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 목록 업데이트&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 저장소에서 최신 패키지 목록을 가져온다 (설치는 아님)
sudo apt update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;apt update&lt;/code&gt;는 무엇을 하는가&lt;/b&gt;: &lt;code&gt;/etc/apt/sources.list&lt;/code&gt;에 정의된 저장소 서버에 접속하여 &quot;어떤 패키지가 어떤 버전으로 이용 가능한지&quot; 목록을 다운로드한다. 패키지를 설치/업그레이드하기 전에 &lt;b&gt;반드시 먼저 실행&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 설치/삭제/업그레이드&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 패키지 설치
sudo apt install nginx
sudo apt install nginx curl vim     # 여러 패키지 동시

# 확인 없이 설치 (-y)
sudo apt install -y nginx

# 패키지 삭제 (설정 파일은 유지)
sudo apt remove nginx

# 패키지 + 설정 파일까지 완전 삭제
sudo apt purge nginx

# 패키지 재설치
sudo apt reinstall nginx

# 모든 패키지 업그레이드
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y

# 의존성 변경이 필요한 업그레이드까지 포함 (커널 등)
sudo apt full-upgrade

# 불필요한 의존성 패키지 자동 제거
sudo apt autoremove&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 검색 및 정보 확인&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 패키지 검색
apt search nginx
apt search &quot;web server&quot;

# 패키지 상세 정보
apt show nginx

# 설치된 패키지 목록
apt list --installed
apt list --installed | grep nginx

# 업그레이드 가능한 패키지 목록
apt list --upgradable

# 패키지가 설치한 파일 목록
dpkg -L nginx

# 특정 파일이 어느 패키지에 포함되어 있는지
dpkg -S /usr/sbin/nginx
# nginx-core: /usr/sbin/nginx&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패키지 버전 고정 (Version Pinning)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 업그레이드로 인해 호환성이 깨지는 것을 방지하려면 특정 패키지의 버전을 고정할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;APT (Debian/Ubuntu)&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 패키지 버전 고정 (hold)
sudo apt-mark hold nginx

# 고정 해제
sudo apt-mark unhold nginx

# 고정된 패키지 목록 확인
apt-mark showhold

# 특정 버전 설치
sudo apt install nginx=1.18.0-0ubuntu1&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 원칙&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;항상 고수준 도구(apt, dnf)를 우선 사용&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apt install&lt;/code&gt; 전에 반드시 &lt;code&gt;apt update&lt;/code&gt;를 먼저 실행한다&lt;/li&gt;
&lt;li&gt;프로덕션 서버에서는 &lt;b&gt;버전 고정&lt;/b&gt;을 고려한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동 보안 업데이트&lt;/b&gt;를 활성화한다&lt;/li&gt;
&lt;li&gt;소스 설치는 최후의 수단으로만 사용한다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 21. 네트워킹&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IP 주소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IP 주소&lt;/b&gt;는 네트워크에서 장치를 식별하는 주소이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IPv4&lt;/b&gt;: &lt;code&gt;192.168.1.100&lt;/code&gt; &amp;mdash; 32비트, 약 43억 개. 현재 주류.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IPv6&lt;/b&gt;: &lt;code&gt;2001:db8::1&lt;/code&gt; &amp;mdash; 128비트, 사실상 무한. 점진적 전환 중.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;루프백(Loopback)&lt;/b&gt;: &lt;code&gt;127.0.0.1&lt;/code&gt; (localhost) &amp;mdash; 자기 자신을 가리키는 특수 주소.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사설 IP&lt;/b&gt;: 내부 네트워크에서만 사용. 인터넷에서 직접 접근 불가.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;10.0.0.0/8&lt;/code&gt;, &lt;code&gt;172.16.0.0/12&lt;/code&gt;, &lt;code&gt;192.168.0.0/16&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공인 IP&lt;/b&gt;: 인터넷에서 직접 접근 가능한 주소.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트 (Port)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 주소가 &lt;b&gt;건물 주소&lt;/b&gt;라면, 포트는 &lt;b&gt;방 번호&lt;/b&gt;이다. 하나의 IP에서 여러 서비스를 구분하는 번호(0~65535)이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;포트&lt;/th&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3306&lt;/td&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5432&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6379&lt;/td&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8080&lt;/td&gt;
&lt;td&gt;대체 HTTP (개발용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;27017&lt;/td&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Well-known 포트 (0~1023): 시스템/표준 서비스용. 바인딩에 root 권한 필요.&lt;/li&gt;
&lt;li&gt;등록된 포트&lt;span style=&quot;text-align: start;&quot;&gt; (1024~49151): 일반 애플리케이션용.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;동적 포트&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;(49152~65535): 임시 연결에 자동 할당.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DNS (Domain Name System)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 읽기 쉬운 &lt;b&gt;도메인 이름&lt;/b&gt;(google.com)을 IP 주소(142.250.196.110)로 변환하는 시스템이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;브라우저: &quot;google.com에 접속하고 싶어&quot;
    &amp;darr; DNS 쿼리
DNS 서버: &quot;google.com은 142.250.196.110이야&quot;
    &amp;darr;
브라우저: 142.250.196.110에 연결&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# DNS 설정 파일
cat /etc/resolv.conf
# nameserver 8.8.8.8
# nameserver 8.8.4.4

# 로컬 호스트 이름 매핑 (DNS보다 우선)
cat /etc/hosts
# 127.0.0.1   localhost
# 192.168.1.10 db-server
# 192.168.1.20 cache-server&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TCP vs UDP&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특성&lt;/th&gt;
&lt;th&gt;TCP&lt;/th&gt;
&lt;th&gt;UDP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;연결 방식&lt;/td&gt;
&lt;td&gt;연결 지향 (3-way handshake)&lt;/td&gt;
&lt;td&gt;비연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;신뢰성&lt;/td&gt;
&lt;td&gt;데이터 전달 보장, 순서 보장&lt;/td&gt;
&lt;td&gt;보장 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;상대적으로 느림&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;td&gt;HTTP, SSH, DB 연결&lt;/td&gt;
&lt;td&gt;DNS, 스트리밍, 게임&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CIDR 표기법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;192.168.1.0/24&lt;/code&gt;와 같은 표기를 CIDR(Classless Inter-Domain Routing)이라 한다. &lt;code&gt;/24&lt;/code&gt;는 네트워크 부분의 비트 수이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;192.168.1.0/24
&amp;rarr; 네트워크: 192.168.1.0
&amp;rarr; 호스트 범위: 192.168.1.1 ~ 192.168.1.254
&amp;rarr; 사용 가능 IP: 254개

10.0.0.0/16
&amp;rarr; 호스트 범위: 10.0.0.1 ~ 10.0.255.254
&amp;rarr; 사용 가능 IP: 65,534개

/32 &amp;rarr; 단일 IP (호스트 1개)
/0  &amp;rarr; 모든 IP&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;네트워크 인터페이스 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;ip&lt;/code&gt; &amp;mdash; 현대적 네트워크 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ifconfig&lt;/code&gt;는 더 이상 권장되지 않으며(deprecated), &lt;b&gt;&lt;code&gt;ip&lt;/code&gt;&lt;/b&gt; 명령어가 표준이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 모든 인터페이스와 IP 주소 확인 ★★★
ip addr show
ip a          # 축약

# 특정 인터페이스만
ip addr show eth0

# 인터페이스 활성화/비활성화
sudo ip link set eth0 up
sudo ip link set eth0 down

# IP 주소 추가/삭제
sudo ip addr add 192.168.1.100/24 dev eth0
sudo ip addr del 192.168.1.100/24 dev eth0

# 라우팅 테이블 확인 ★
ip route show
ip r
# default via 10.0.0.1 dev eth0     &amp;larr; 기본 게이트웨이
# 10.0.0.0/24 dev eth0 proto kernel  &amp;larr; 로컬 네트워크&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;연결 진단 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;ping&lt;/code&gt; &amp;mdash; 기본 연결 확인&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 호스트 연결 확인
ping google.com
ping 8.8.8.8

# 횟수 지정 (-c)
ping -c 4 google.com

# 타임아웃 지정 (-W: 초)
ping -c 1 -W 3 10.0.0.1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ping&lt;/code&gt;이 실패하면: 네트워크 자체가 끊어졌거나, 방화벽이 ICMP를 차단하고 있거나, DNS가 동작하지 않는 경우이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;curl&lt;/code&gt; &amp;mdash; HTTP 요청 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서비스의 응답을 확인하는 데 가장 많이 사용한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 기본 GET 요청
curl http://localhost:8080

# 응답 헤더 포함 (-i)
curl -i http://localhost:8080

# 상태 코드만 확인 (-o /dev/null -s -w)
curl -o /dev/null -s -w &quot;%{http_code}\n&quot; http://localhost:8080
# 200

# 상세 연결 과정 확인 (-v: verbose) ★
curl -v https://example.com

# POST 요청
curl -X POST -H &quot;Content-Type: application/json&quot; -d '{&quot;key&quot;:&quot;value&quot;}' http://localhost:8080/api

# 연결 타임아웃 설정
curl --connect-timeout 5 --max-time 10 http://example.com

# SSL 인증서 무시 (개발용)
curl -k https://self-signed.example.com&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;dig&lt;/code&gt; / &lt;code&gt;nslookup&lt;/code&gt; &amp;mdash; DNS 조회&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# DNS 조회
dig google.com
dig google.com +short       # IP만 간결하게
# 142.250.196.110

# 특정 DNS 서버로 조회
dig @8.8.8.8 google.com

# 역방향 DNS (IP &amp;rarr; 도메인)
dig -x 8.8.8.8

# 레코드 타입 지정
dig google.com MX           # 메일 서버
dig google.com NS           # 네임 서버
dig google.com CNAME        # 별칭
dig google.com TXT          # TXT 레코드

# nslookup (간단한 조회)
nslookup google.com
nslookup google.com 8.8.8.8&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;traceroute&lt;/code&gt; / &lt;code&gt;tracepath&lt;/code&gt; &amp;mdash; 경로 추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷이 목적지까지 &lt;b&gt;어떤 경로&lt;/b&gt;를 거치는지 추적한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 경로 추적
traceroute google.com

# tracepath (traceroute 대안, 별도 설치 불필요한 경우 많음)
tracepath google.com&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;포트 &amp;amp; 연결 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;ss&lt;/code&gt; &amp;mdash; 소켓 통계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;netstat&lt;/code&gt;의 현대적 대체 도구이다. 열린 포트, 연결 상태를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 리스닝 중인 TCP 포트 확인 ★★★
ss -tlnp
#  -t: TCP
#  -l: LISTEN 상태만
#  -n: 포트 번호를 숫자로 표시 (이름 변환 안 함)
#  -p: 프로세스 정보 표시

# State    Recv-Q Send-Q  Local Address:Port  Peer Address:Port  Process
# LISTEN   0      511     0.0.0.0:80          0.0.0.0:*          users:((&quot;nginx&quot;,pid=2345))
# LISTEN   0      128     0.0.0.0:22          0.0.0.0:*          users:((&quot;sshd&quot;,pid=1234))

# UDP 포트 확인
ss -ulnp

# 모든 TCP 연결 (LISTEN + ESTABLISHED + ...)
ss -tanp

# ESTABLISHED 연결만
ss -tanp state established

# 특정 포트로 필터링
ss -tlnp | grep :80
ss -tlnp sport = :443&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;netstat&lt;/code&gt; &amp;mdash; 전통적 네트워크 통계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ss&lt;/code&gt;와 유사하지만, 일부 환경에서는 여전히 &lt;code&gt;netstat&lt;/code&gt;이 기본 설치되어 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 설치 (없는 경우)
sudo apt install net-tools

# 리스닝 중인 TCP 포트
netstat -tlnp

# 모든 연결
netstat -tanp&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 전송&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;wget&lt;/code&gt; &amp;mdash; 파일 다운로드&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 파일 다운로드
wget https://example.com/file.tar.gz

# 다른 이름으로 저장
wget -O output.tar.gz https://example.com/file.tar.gz

# 백그라운드 다운로드
wget -b https://example.com/large-file.iso

# 재시도 횟수
wget --tries=3 https://example.com/file.tar.gz

# 이어받기 (중단된 다운로드 재개)
wget -c https://example.com/large-file.iso&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;curl&lt;/code&gt; vs &lt;code&gt;wget&lt;/code&gt;&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;curl&lt;/th&gt;
&lt;th&gt;wget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;파일 다운로드&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl -O url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wget url&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API 호출&lt;/td&gt;
&lt;td&gt;✓ (기본 용도)&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 메서드 지원&lt;/td&gt;
&lt;td&gt;GET, POST, PUT, DELETE...&lt;/td&gt;
&lt;td&gt;GET만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재귀적 다운로드&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓ (&lt;code&gt;wget -r&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이어받기&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl -C -&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wget -c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로: &lt;b&gt;API 테스트는 &lt;code&gt;curl&lt;/code&gt;&lt;/b&gt;, &lt;b&gt;파일 다운로드는 &lt;code&gt;wget&lt;/code&gt;&lt;/b&gt;.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;네트워크 트러블슈팅 체계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;서비스에 접속이 안 된다&quot;는 보고가 왔을 때의 체계적 진단 순서:&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 1. 네트워크 인터페이스 확인 &amp;mdash; IP가 할당되어 있는가?
ip addr show

# 2. 기본 게이트웨이 확인
ip route show

# 3. 외부 연결 확인 &amp;mdash; 인터넷에 나갈 수 있는가?
ping -c 2 8.8.8.8

# 4. DNS 확인 &amp;mdash; 도메인이 해석되는가?
dig example.com +short
# 실패하면: /etc/resolv.conf 확인, DNS 서버 변경

# 5. 특정 호스트+포트 연결 확인
curl -v http://target-host:8080
# 또는
ss -tlnp | grep :8080    # (대상 서버에서) 서비스가 리스닝 중인가?

# 6. 방화벽 확인
sudo ufw status           # 또는 firewall-cmd --list-all
# 클라우드: 보안 그룹 규칙 확인

# 7. 서비스 상태 확인
systemctl status nginx
journalctl -u nginx -n 20&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;가장 많이 쓰는 형태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip addr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IP/인터페이스 확인&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip a&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip route&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;라우팅 테이블&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip r&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기본 연결 확인&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ping -c 4 host&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;curl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP 요청/API 테스트&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl -v&lt;/code&gt;, &lt;code&gt;curl -s -o /dev/null -w &quot;%{http_code}&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DNS 조회&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dig domain +short&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;포트/연결 확인&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;ss -tlnp&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;traceroute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;경로 추적&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 다운로드&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wget -O file url&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Chapter 22. SSH &amp;amp; 원격 접속&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSH&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH(Secure Shell)는 네트워크를 통해 다른 컴퓨터에 &lt;b&gt;안전하게 접속&lt;/b&gt;하기 위한 프로토콜이다. 모든 통신이 &lt;b&gt;암호화&lt;/b&gt;되므로, 비밀번호나 명령어가 네트워크에서 평문으로 노출되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 이전에는 &lt;code&gt;telnet&lt;/code&gt;, &lt;code&gt;rsh&lt;/code&gt; 같은 프로토콜을 사용했는데, 이들은 데이터를 암호화하지 않아서 도청에 취약했다. SSH는 이 문제를 완전히 해결했으며, 현재 서버 원격 접속의 &lt;b&gt;사실상 유일한 표준&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH의 용도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원격 서버에 &lt;b&gt;로그인&lt;/b&gt;하여 명령어 실행&lt;/li&gt;
&lt;li&gt;서버 간 &lt;b&gt;파일 전송&lt;/b&gt; (scp, sftp, rsync)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;포트 포워딩&lt;/b&gt; (터널링)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Git&lt;/b&gt; 원격 저장소 접근&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동화&lt;/b&gt; 스크립트에서 원격 명령어 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH의 동작 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 연결은 두 단계로 이루어진다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 서버 인증&lt;/b&gt;: 클라이언트가 &quot;이 서버가 진짜 내가 접속하려는 서버인가&quot;를 확인한다. 서버의 공개 키(host key)를 이전에 저장해둔 것(&lt;code&gt;~/.ssh/known_hosts&lt;/code&gt;)과 비교한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 사용자 인증&lt;/b&gt;: 서버가 &quot;이 사용자가 접속 권한이 있는가&quot;를 확인한다. 비밀번호 또는 키 기반 인증을 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키 기반 인증 (Key-Based Authentication)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 인증의 문제점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무차별 대입 공격에 취약&lt;/li&gt;
&lt;li&gt;여러 서버에 비밀번호를 기억하기 어려움&lt;/li&gt;
&lt;li&gt;자동화 스크립트에 비밀번호를 하드코딩하면 보안 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키 기반 인증은:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수학적으로 매우 강력한 보안 (2048/4096비트 RSA, Ed25519)&lt;/li&gt;
&lt;li&gt;비밀번호 없이 자동화 가능&lt;/li&gt;
&lt;li&gt;AWS, GCP, Azure 등 클라우드 서비스의 &lt;b&gt;기본 인증 방식&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공개 키 / 개인 키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 키는 한 쌍(pair)으로 생성된다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개인 키(Private Key)&lt;/b&gt;: 자신만 가지고 있는 비밀 키. &lt;b&gt;절대 공유하면 안 된다.&lt;/b&gt; &lt;code&gt;~/.ssh/id_rsa&lt;/code&gt; 또는 &lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공개 키(Public Key)&lt;/b&gt;: 서버에 등록하는 키. 공유해도 안전하다. &lt;code&gt;~/.ssh/id_rsa.pub&lt;/code&gt; 또는 &lt;code&gt;~/.ssh/id_ed25519.pub&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;인증 흐름:
1. 사용자가 서버에 접속 시도
2. 서버가 랜덤 챌린지를 보냄
3. 클라이언트가 개인 키로 챌린지에 서명하여 응답
4. 서버가 등록된 공개 키로 서명을 검증
5. 검증 성공 &amp;rarr; 접속 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유하자면: 공개 키는 &lt;b&gt;자물쇠&lt;/b&gt;, 개인 키는 &lt;b&gt;열쇠&lt;/b&gt;이다. 자물쇠(공개 키)는 서버에 설치하고, 열쇠(개인 키)는 자기만 가지고 다닌다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH 키 생성&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Ed25519 키 생성 (현재 추천되는 알고리즘) ★★★
ssh-keygen -t ed25519 -C &quot;devops@example.com&quot;
# -t: 알고리즘 타입
# -C: 코멘트 (보통 이메일, 식별용)

# 실행하면 다음을 물어본다:
# 1. 키 저장 경로 (기본: ~/.ssh/id_ed25519) &amp;rarr; Enter
# 2. 패스프레이즈(비밀번호) &amp;rarr; 보안을 위해 설정 권장, 자동화 시 빈 값

# RSA 키 생성 (레거시 시스템 호환 필요 시)
ssh-keygen -t rsa -b 4096 -C &quot;devops@example.com&quot;
# -b 4096: 키 길이 (최소 2048, 4096 권장)

# 결과:
# ~/.ssh/id_ed25519       &amp;larr; 개인 키 (절대 유출 금지)
# ~/.ssh/id_ed25519.pub   &amp;larr; 공개 키 (서버에 등록)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버에 공개 키 등록&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 방법 1: ssh-copy-id (가장 간편) ★
ssh-copy-id user@remote-server
# 비밀번호를 한 번 입력하면 공개 키가 서버의 ~/.ssh/authorized_keys에 추가된다

# 방법 2: 수동 등록
cat ~/.ssh/id_ed25519.pub | ssh user@remote-server &quot;mkdir -p ~/.ssh &amp;amp;&amp;amp; cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&quot;

# 방법 3: 클라우드 (AWS EC2)
# 인스턴스 생성 시 키 페어를 지정하면 자동으로 등록된다
# 또는 EC2 Instance Connect, Session Manager 사용&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;키 파일 권한 (필수!)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH는 키 파일의 권한이 너무 열려있으면 &lt;b&gt;접속을 거부&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;chmod 700 ~/.ssh/
chmod 600 ~/.ssh/id_ed25519         # 개인 키
chmod 644 ~/.ssh/id_ed25519.pub     # 공개 키
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/config&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSH 접속&lt;/h2&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# 기본 접속
ssh user@hostname
ssh user@192.168.1.100

# 포트 지정 (기본: 22)
ssh -p 2222 user@hostname

# 특정 키 파일 지정
ssh -i ~/.ssh/my-key.pem ubuntu@ec2-xx-xx-xx-xx.compute-1.amazonaws.com

# 원격 서버에서 명령어 하나만 실행하고 종료
ssh user@server &quot;uptime&quot;
ssh user@server &quot;df -h &amp;amp;&amp;amp; free -h&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 접속 시 fingerprint 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 접속하는 서버에는 다음 메시지가 뜬다:&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;The authenticity of host 'server (192.168.1.100)' can't be established.
ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;yes&lt;/code&gt;를 입력하면 서버의 공개 키가 &lt;code&gt;~/.ssh/known_hosts&lt;/code&gt;에 저장된다. 이후 접속 시에는 이 키를 비교하여 서버 위조(중간자 공격)를 방지한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSH 설정 파일 (&lt;code&gt;~/.ssh/config&lt;/code&gt;)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 서버에 접속할 때 매번 긴 명령어를 입력하는 대신, &lt;b&gt;설정 파일에 별칭을 등록&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# ~/.ssh/config
Host web-prod
    HostName 10.0.1.100
    User deploy
    Port 22
    IdentityFile ~/.ssh/prod-key.pem

Host web-staging
    HostName 10.0.2.100
    User deploy
    IdentityFile ~/.ssh/staging-key.pem

Host db-prod
    HostName 10.0.1.200
    User admin
    IdentityFile ~/.ssh/prod-key.pem

# 패턴 매칭 &amp;mdash; 모든 호스트에 적용
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
    AddKeysToAgent yes&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 설정 후에는 별칭으로 간단히 접속
ssh web-prod       # ssh -i ~/.ssh/prod-key.pem deploy@10.0.1.100 과 동일
ssh web-staging
ssh db-prod&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 전송&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;scp&lt;/code&gt; &amp;mdash; 파일 복사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Secure Copy&lt;/b&gt;의 약자이다. SSH를 통해 파일을 복사한다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# 로컬 &amp;rarr; 원격
scp file.txt user@server:/home/user/

# 원격 &amp;rarr; 로컬
scp user@server:/var/log/app.log ./

# 디렉터리 복사 (-r)
scp -r ./project/ user@server:/opt/

# 특정 포트
scp -P 2222 file.txt user@server:/tmp/

# SSH 설정 파일의 별칭 사용
scp file.txt web-prod:/opt/deploy/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 23. 셸 스크립트 기초&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;셸 스크립트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셸 스크립트(Shell Script)는 셸 명령어들을 파일에 순서대로 작성하여 &lt;b&gt;자동으로 실행&lt;/b&gt;되게 하는 프로그램이다. 지금까지 배운 모든 명령어(&lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;systemctl&lt;/code&gt; 등)를 조합하여 복잡한 작업을 자동화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셸 스크립트는 다음과 같은 곳에 사용된다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 초기 설정 자동화&lt;/li&gt;
&lt;li&gt;배포(deploy) 스크립트&lt;/li&gt;
&lt;li&gt;백업 스크립트&lt;/li&gt;
&lt;li&gt;모니터링 / 헬스체크&lt;/li&gt;
&lt;li&gt;CI/CD 파이프라인의 빌드/테스트 단계&lt;/li&gt;
&lt;li&gt;크론 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 번째 스크립트&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
# 첫 번째 셸 스크립트

echo &quot;Hello, DevOps!&quot;
echo &quot;Current user: $(whoami)&quot;
echo &quot;Date: $(date)&quot;
echo &quot;Hostname: $(hostname)&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 실행 권한 부여
chmod +x hello.sh

# 실행
./hello.sh&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Shebang (&lt;code&gt;#!&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트 첫 줄의 &lt;code&gt;#!/bin/bash&lt;/code&gt;를 Shebang(셔뱅)이라고 한다. 이 스크립트를 &lt;b&gt;어떤 인터프리터로 실행할지&lt;/b&gt; 지정한다.&lt;/p&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;#!/bin/bash         # bash로 실행
#!/bin/sh           # POSIX sh로 실행 (더 이식성이 높음)
#!/usr/bin/env bash # PATH에서 bash를 찾아 실행 (이식성 좋음)
#!/usr/bin/python3  # Python 스크립트도 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;#!/bin/bash&lt;/code&gt; vs &lt;code&gt;#!/bin/sh&lt;/code&gt;&lt;/b&gt;: &lt;code&gt;bash&lt;/code&gt;는 &lt;code&gt;sh&lt;/code&gt;의 상위 호환이지만, bash 전용 기능(배열, &lt;code&gt;[[ ]]&lt;/code&gt; 등)은 &lt;code&gt;sh&lt;/code&gt;에서 동작하지 않는다. 이식성이 중요하면 &lt;code&gt;#!/bin/sh&lt;/code&gt;, bash 기능이 필요하면 &lt;code&gt;#!/bin/bash&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변수 선언과 사용&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash

# 변수 선언 (= 양옆에 공백 금지!)
NAME=&quot;DevOps&quot;
PORT=8080
LOG_DIR=&quot;/var/log/myapp&quot;

# 변수 사용 ($변수명 또는 ${변수명})
echo &quot;Hello, $NAME&quot;
echo &quot;Server running on port ${PORT}&quot;

# 중괄호가 필요한 경우 &amp;mdash; 변수명 뒤에 문자가 붙을 때
FILE=&quot;${NAME}_config.yaml&quot;    # DevOps_config.yaml
# $NAME_config로 쓰면 NAME_config라는 변수를 찾게 된다&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특수 변수&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;변수&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스크립트 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;...&lt;/td&gt;
&lt;td&gt;1번째, 2번째... 인자(argument)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$#&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;인자의 개수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$@&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모든 인자 (개별 문자열로)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모든 인자 (하나의 문자열로)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;직전 명령어의 &lt;b&gt;종료 코드&lt;/b&gt; ★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 스크립트의 PID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$!&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;마지막 백그라운드 프로세스의 PID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
echo &quot;Script: $0&quot;
echo &quot;First arg: $1&quot;
echo &quot;Second arg: $2&quot;
echo &quot;Total args: $#&quot;
echo &quot;All args: $@&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;./script.sh hello world
# Script: ./script.sh
# First arg: hello
# Second arg: world
# Total args: 2
# All args: hello world&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;종료 코드 (Exit Code)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 명령어는 종료 시 종료 코드(exit code)를 반환한다. &lt;code&gt;0&lt;/code&gt;은 성공, &lt;code&gt;0이 아닌 값&lt;/code&gt;은 실패이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 종료 코드 확인
ls /etc/passwd
echo $?    # 0 (성공)

ls /nonexistent
echo $?    # 2 (실패 &amp;mdash; 파일 없음)

# 스크립트에서 종료 코드 지정
exit 0     # 성공
exit 1     # 실패&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;명령어 치환 (Command Substitution)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어의 &lt;b&gt;출력 결과를 변수에 저장&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;# $( ) 형태 (추천)
CURRENT_DATE=$(date +%Y%m%d)
FILE_COUNT=$(find /var/log -name &quot;*.log&quot; | wc -l)
MY_IP=$(hostname -I | awk '{print $1}')

# 백틱 형태 (레거시, 중첩 시 불편)
CURRENT_DATE=`date +%Y%m%d`

echo &quot;Date: $CURRENT_DATE&quot;
echo &quot;Log files: $FILE_COUNT&quot;
echo &quot;IP: $MY_IP&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조건문&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;if&lt;/code&gt; 문&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash

if [ condition ]; then
    # 조건이 참일 때
elif [ condition2 ]; then
    # 조건2가 참일 때
else
    # 모두 거짓일 때
fi&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건 표현식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파일 테스트:&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표현식&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-f file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일이 존재하고 일반 파일인가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-d dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;디렉터리가 존재하는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-e path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;경로가 존재하는가 (파일/디렉터리 무관)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-r file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;읽기 가능한가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-w file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;쓰기 가능한가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-x file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실행 가능한가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-s file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일이 비어있지 않은가 (크기 &amp;gt; 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문자열 비교:&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표현식&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot;$a&quot; = &quot;$b&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;같은가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot;$a&quot; != &quot;$b&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다른가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-z &quot;$a&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;빈 문자열인가 (zero length)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-n &quot;$a&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;비어있지 않은가 (non-zero length)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;숫자 비교:&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표현식&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$a -eq $b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;같은가 (equal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$a -ne $b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다른가 (not equal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$a -gt $b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;큰가 (greater than)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$a -lt $b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작은가 (less than)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$a -ge $b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;크거나 같은가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$a -le $b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작거나 같은가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash

# 파일 존재 확인
CONFIG=&quot;/etc/myapp/config.yaml&quot;
if [ -f &quot;$CONFIG&quot; ]; then
    echo &quot;Config found: $CONFIG&quot;
else
    echo &quot;ERROR: Config not found!&quot;
    exit 1
fi

# 인자 확인
if [ $# -eq 0 ]; then
    echo &quot;Usage: $0 &amp;lt;environment&amp;gt;&quot;
    echo &quot;  environments: dev, staging, prod&quot;
    exit 1
fi

# 종료 코드로 성공/실패 판단
if systemctl is-active --quiet nginx; then
    echo &quot;Nginx is running&quot;
else
    echo &quot;Nginx is NOT running&quot;
    sudo systemctl start nginx
fi

# 디스크 사용률 체크
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ &quot;$USAGE&quot; -gt 90 ]; then
    echo &quot;WARNING: Disk usage is ${USAGE}%&quot;
fi&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;[[ ]]&lt;/code&gt; vs &lt;code&gt;[ ]&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[[ ]]&lt;/code&gt;는 bash 전용 확장이며, &lt;code&gt;[ ]&lt;/code&gt;보다 강력하다:&lt;/p&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;# [[ ]]의 장점:
# 1. 패턴 매칭
[[ &quot;$string&quot; == *.log ]]

# 2. 정규표현식
[[ &quot;$string&quot; =~ ^[0-9]+$ ]]

# 3. &amp;amp;&amp;amp; / || 사용 가능
[[ -f &quot;$file&quot; &amp;amp;&amp;amp; -r &quot;$file&quot; ]]

# [ ]에서는 -a / -o를 사용해야 한다 (비추천)
[ -f &quot;$file&quot; -a -r &quot;$file&quot; ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추천&lt;/b&gt;: bash 스크립트(&lt;code&gt;#!/bin/bash&lt;/code&gt;)에서는 &lt;code&gt;[[ ]]&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;반복문&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;for&lt;/code&gt; 문&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 리스트 순회
for server in web-01 web-02 web-03; do
    echo &quot;Checking $server...&quot;
    ssh &quot;$server&quot; &quot;uptime&quot;
done

# 범위 (Brace Expansion)
for i in {1..10}; do
    echo &quot;Iteration $i&quot;
done

# C 스타일
for ((i=0; i&amp;lt;5; i++)); do
    echo &quot;Count: $i&quot;
done

# 명령어 결과 순회
for file in /var/log/*.log; do
    echo &quot;$(wc -l &amp;lt; &quot;$file&quot;) lines in $file&quot;
done

# 파일의 각 줄 처리
while IFS= read -r line; do
    echo &quot;Processing: $line&quot;
done &amp;lt; servers.txt&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;while&lt;/code&gt; 문&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 기본 while
count=0
while [ $count -lt 5 ]; do
    echo &quot;Count: $count&quot;
    ((count++))
done

# 서비스가 시작될 때까지 대기 ★
echo &quot;Waiting for service to start...&quot;
while ! curl -s http://localhost:8080/health &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; do
    echo &quot;  Not ready yet, retrying in 3s...&quot;
    sleep 3
done
echo &quot;Service is up!&quot;

# 파일 읽기 (줄 단위)
while IFS= read -r line; do
    echo &quot;$line&quot;
done &amp;lt; /etc/hosts&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;until&lt;/code&gt; 문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;while&lt;/code&gt;의 반대 &amp;mdash; 조건이 &lt;b&gt;참이 될 때까지&lt;/b&gt; 반복한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;until systemctl is-active --quiet nginx; do
    echo &quot;Nginx not running, starting...&quot;
    sudo systemctl start nginx
    sleep 2
done
echo &quot;Nginx is running&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;함수&lt;/h2&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash

# 함수 정의
log() {
    echo &quot;[$(date '+%Y-%m-%d %H:%M:%S')] $1&quot;
}

check_service() {
    local service_name=$1    # local: 함수 내 지역 변수
    if systemctl is-active --quiet &quot;$service_name&quot;; then
        log &quot;$service_name is running&quot;
        return 0
    else
        log &quot;ERROR: $service_name is not running&quot;
        return 1
    fi
}

# 함수 호출
log &quot;Starting health check&quot;
check_service nginx
check_service postgresql

# 반환값 활용
if check_service redis; then
    log &quot;Redis OK&quot;
else
    log &quot;Redis FAILED &amp;mdash; attempting restart&quot;
    sudo systemctl restart redis
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;local&lt;/code&gt; 키워드&lt;/b&gt;: 함수 내에서 선언한 변수가 함수 밖에 영향을 주지 않게 한다. 사용하지 않으면 전역 변수가 되어 예기치 않은 버그가 발생할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;set&lt;/code&gt; 옵션 &amp;mdash; 안전한 스크립트의 시작&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
set -euo pipefail&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;효과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set -e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;명령어가 실패(종료 코드 &amp;ne; 0)하면 스크립트 &lt;b&gt;즉시 종료&lt;/b&gt; ★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set -u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;정의되지 않은 변수 사용 시 에러로 종료 ★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;set -o pipefail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파이프라인에서 중간 명령어가 실패해도 감지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;# set -e 없이: 에러가 발생해도 다음 줄이 실행된다 (위험!)
rm -rf /opt/myapp/
cp -r /tmp/deploy/* /opt/myapp/   # rm이 실패해도 실행됨

# set -e 있으면: rm이 실패하면 스크립트가 즉시 멈춘다 (안전!)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;trap&lt;/code&gt; &amp;mdash; 종료 시 정리 작업&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
set -euo pipefail

# 스크립트 종료 시 (정상/비정상 모두) 실행할 정리 코드
cleanup() {
    echo &quot;Cleaning up temporary files...&quot;
    rm -rf &quot;$TEMP_DIR&quot;
}
trap cleanup EXIT

TEMP_DIR=$(mktemp -d)
echo &quot;Working in $TEMP_DIR&quot;

# 작업 수행 (에러 발생해도 cleanup이 자동 호출된다)
cp important_file.txt &quot;$TEMP_DIR/&quot;
# ... 추가 작업 ...&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스크립트 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포 스크립트&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
set -euo pipefail

# --- 설정 ---
APP_NAME=&quot;myapp&quot;
DEPLOY_DIR=&quot;/opt/${APP_NAME}&quot;
RELEASE_DIR=&quot;${DEPLOY_DIR}/releases&quot;
CURRENT_LINK=&quot;${DEPLOY_DIR}/current&quot;
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
NEW_RELEASE=&quot;${RELEASE_DIR}/${TIMESTAMP}&quot;

log() { echo &quot;[$(date '+%H:%M:%S')] $1&quot;; }

# --- 인자 확인 ---
ARTIFACT=${1:?&quot;Usage: $0 &amp;lt;artifact-path&amp;gt;&quot;}
[ -f &quot;$ARTIFACT&quot; ] || { log &quot;ERROR: $ARTIFACT not found&quot;; exit 1; }

# --- 배포 ---
log &quot;Starting deployment...&quot;

mkdir -p &quot;$NEW_RELEASE&quot;
tar -xzf &quot;$ARTIFACT&quot; -C &quot;$NEW_RELEASE&quot;

log &quot;Switching symlink...&quot;
ln -sfn &quot;$NEW_RELEASE&quot; &quot;$CURRENT_LINK&quot;

log &quot;Restarting service...&quot;
sudo systemctl restart &quot;$APP_NAME&quot;

# 이전 릴리스 정리 (최근 5개만 유지)
ls -dt ${RELEASE_DIR}/*/ | tail -n +6 | xargs rm -rf

log &quot;Deployment completed: $TIMESTAMP&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 헬스체크&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
set -euo pipefail

ALERT_EMAIL=&quot;ops@example.com&quot;
CHECKS_FAILED=0

check() {
    local name=$1
    local cmd=$2
    if eval &quot;$cmd&quot; &amp;gt; /dev/null 2&amp;gt;&amp;amp;1; then
        echo &quot;  ✓ $name&quot;
    else
        echo &quot;  ✗ $name FAILED&quot;
        ((CHECKS_FAILED++))
    fi
}

echo &quot;=== Health Check: $(hostname) ===&quot;
echo &quot;Time: $(date)&quot;
echo &quot;&quot;

# 서비스 체크
echo &quot;[Services]&quot;
check &quot;Nginx&quot; &quot;systemctl is-active --quiet nginx&quot;
check &quot;PostgreSQL&quot; &quot;systemctl is-active --quiet postgresql&quot;
check &quot;Redis&quot; &quot;systemctl is-active --quiet redis&quot;

# 리소스 체크
echo &quot;[Resources]&quot;
DISK=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
MEM=$(free | awk '/Mem:/ {printf &quot;%.0f&quot;, $3/$2*100}')
check &quot;Disk &amp;lt; 90%&quot; &quot;[ $DISK -lt 90 ]&quot;
check &quot;Memory &amp;lt; 90%&quot; &quot;[ $MEM -lt 90 ]&quot;

# 결과
echo &quot;&quot;
if [ $CHECKS_FAILED -gt 0 ]; then
    echo &quot;RESULT: $CHECKS_FAILED check(s) FAILED&quot;
    exit 1
else
    echo &quot;RESULT: All checks passed&quot;
    exit 0
fi&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;셸 스크립트 작성 모범 사례&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;항상 &lt;code&gt;set -euo pipefail&lt;/code&gt;로 시작&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변수는 항상 따옴표로 감싼다&lt;/b&gt;: &lt;code&gt;&quot;$VAR&quot;&lt;/code&gt; (공백이 포함된 값 보호)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Shebang을 명시&lt;/b&gt;한다: &lt;code&gt;#!/bin/bash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함수로 구조화&lt;/b&gt;한다: 재사용 가능하고 읽기 쉽다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 메시지는 stderr로&lt;/b&gt;: &lt;code&gt;echo &quot;ERROR&quot; &amp;gt;&amp;amp;2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;임시 파일은 &lt;code&gt;mktemp&lt;/code&gt;으로 생성&lt;/b&gt;하고, &lt;code&gt;trap&lt;/code&gt;으로 정리한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하드코딩 대신 변수/인자&lt;/b&gt;를 사용한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주석을 충분히 작성&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;복잡한 로직은 &lt;b&gt;Python 등 다른 언어를 고려&lt;/b&gt;한다&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Learning Log</category>
      <category>Linux</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/41</guid>
      <comments>https://allluck777.tistory.com/41#entry41comment</comments>
      <pubDate>Wed, 25 Mar 2026 09:07:28 +0900</pubDate>
    </item>
    <item>
      <title>[멋사 클라우드 5기] Day 35 &amp;amp; 36 - Linux (2)</title>
      <link>https://allluck777.tistory.com/40</link>
      <description>&lt;h1&gt;Chapter 10. 텍스트 정렬, 집계 &amp;amp; 비교&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;grep, sed, awk&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt; 는 단독으로도 유용하지만, 파이프(&lt;code&gt;|&lt;/code&gt;)로 연결하여 &lt;b&gt;데이터 분석 파이프라인&lt;/b&gt;을 구성할 때 더 많은 작업을 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;# 로그에서 IP별 접속 횟수 상위 10개
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;sort&lt;/code&gt; &amp;mdash; 텍스트 정렬&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sort&lt;/code&gt;는 입력을 &lt;b&gt;줄 단위로 정렬&lt;/b&gt;한다. 기본은 알파벳(사전) 순서이다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 알파벳순 정렬
sort names.txt

# 역순 정렬 (-r: reverse)
sort -r names.txt

# 숫자순 정렬 (-n: numeric) ★
sort -n numbers.txt
# 숫자 정렬 없이 하면: 1, 10, 100, 2, 20 (문자열 비교)
# -n을 붙이면: 1, 2, 10, 20, 100 (숫자 비교)

# 사람이 읽기 쉬운 크기 단위로 정렬 (-h: human-numeric)
du -sh */ | sort -rh
# 2.1G, 500M, 128K 순서로 정렬&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특정 필드 기준 정렬&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 특정 필드 기준으로 정렬 (-k: key, -t: delimiter)
# /etc/passwd를 UID(3번째 필드) 기준으로 숫자 정렬
sort -t: -k3 -n /etc/passwd

# CSV에서 3번째 열 기준 숫자 역순 정렬
sort -t',' -k3 -rn data.csv

# 여러 키로 정렬 (1차: 2번째 필드, 2차: 3번째 필드 숫자)
sort -t',' -k2,2 -k3,3n data.csv&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유용한 옵션&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 중복 줄 제거하면서 정렬 (-u: unique)
sort -u names.txt

# 대소문자 무시 정렬 (-f: fold case)
sort -f mixed_case.txt

# 결과를 원본 파일에 덮어쓰기 (-o)
sort -n numbers.txt -o numbers.txt
# 주의: sort file &amp;gt; file 은 안된다 (파일이 비어버림). -o를 사용해야 안전&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;uniq&lt;/code&gt; &amp;mdash; 중복 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;uniq&lt;/code&gt;는 &lt;b&gt;인접한 중복 줄&lt;/b&gt;을 처리한다. 핵심 주의사항: &lt;code&gt;uniq&lt;/code&gt;는 &lt;b&gt;연속된&lt;/b&gt; 중복만 감지하므로, 반드시 &lt;b&gt;&lt;code&gt;sort&lt;/code&gt;와 함께 사용&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 잘못된 사용 &amp;mdash; 비연속 중복은 제거되지 않음
echo -e &quot;a\nb\na&quot; | uniq
# a
# b
# a    &amp;larr; 여전히 남아있음

# 올바른 사용 &amp;mdash; sort로 먼저 정렬
echo -e &quot;a\nb\na&quot; | sort | uniq
# a
# b&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 옵션&lt;/h3&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;# 중복 제거 (기본 동작)
sort data.txt | uniq

# 중복 횟수와 함께 출력 (-c: count) ★★★
sort data.txt | uniq -c
#       3 apple
#       1 banana
#       5 cherry

# 중복된 줄만 출력 (-d: duplicated)
sort data.txt | uniq -d

# 중복되지 않은 줄만 출력 (-u: unique)
sort data.txt | uniq -u

# 대소문자 무시 (-i)
sort data.txt | uniq -ci&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 패턴: &lt;code&gt;sort | uniq -c | sort -rn&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 패턴 분해:
# sort         &amp;rarr; 동일한 값을 인접하게 모음
# uniq -c      &amp;rarr; 중복 횟수를 센다
# sort -rn     &amp;rarr; 횟수 기준 내림차순 정렬

# 실무 예시: Nginx 로그에서 가장 많이 접속한 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10

# 실무 예시: 가장 많이 발생한 에러 메시지
grep &quot;ERROR&quot; app.log | awk -F'ERROR' '{print $2}' | sort | uniq -c | sort -rn | head -10

# 실무 예시: HTTP 상태 코드 분포
awk '{print $9}' access.log | sort | uniq -c | sort -rn
#   45231 200
#    3214 304
#     567 404
#      23 500&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;wc&lt;/code&gt; &amp;mdash; 텍스트 통계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Word Count&lt;/b&gt;의 약자이다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# 파이프와 조합 &amp;mdash; 결과의 줄 수 세기
grep &quot;ERROR&quot; app.log | wc -l          # 에러 발생 횟수
ps aux | wc -l                         # 실행 중인 프로세스 수
find /tmp -type f | wc -l             # /tmp의 파일 수
docker ps | wc -l                      # 실행 중인 컨테이너 수 (+1 헤더)

# 여러 파일의 줄 수 비교
wc -l /var/log/*.log&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;cut&lt;/code&gt; &amp;mdash; 필드 추출&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;cut&lt;/code&gt; vs &lt;code&gt;awk&lt;/code&gt; &amp;mdash; 언제 무엇을 쓰는가&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;추천&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단순히 N번째 필드 추출&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cut&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;더 간결하고 빠르다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;구분자가 여러 공백일 때&lt;/td&gt;
&lt;td&gt;&lt;code&gt;awk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cut&lt;/code&gt;은 연속 공백을 하나로 처리하지 못한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조건부 필드 추출&lt;/td&gt;
&lt;td&gt;&lt;code&gt;awk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;조건 처리 기능이 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;필드 계산이 필요할 때&lt;/td&gt;
&lt;td&gt;&lt;code&gt;awk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;프로그래밍 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# cut: 구분자가 명확한 경우 (CSV, /etc/passwd 등)
cut -d: -f1,3 /etc/passwd

# awk: 공백으로 구분된 출력 (ps, df 등)
ps aux | awk '{print $1, $11}'      # 사용자, 명령어
df -h | awk 'NR&amp;gt;1 {print $5, $6}'  # 사용률, 마운트포인트&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;tr&lt;/code&gt; &amp;mdash; 문자 변환 (보충)&lt;/h2&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Windows 줄바꿈(\r\n)을 Unix 줄바꿈(\n)으로 변환
tr -d '\r' &amp;lt; windows_file.txt &amp;gt; unix_file.txt

# 여러 구분자를 통일
echo &quot;a;b,c:d&quot; | tr ';,:' '\t\t\t'
# a    b    c    d

# 알파벳 외 문자 모두 제거
echo &quot;Hello, World! 123&quot; | tr -cd 'a-zA-Z\n'
# HelloWorld

# 연속된 빈 줄을 하나로 압축
cat messy.txt | tr -s '\n'&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;diff&lt;/code&gt; &amp;mdash; 파일 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 파일의 &lt;b&gt;차이점&lt;/b&gt;을 줄 단위로 비교하여 보여준다. 설정 파일 변경 전후 비교, 코드 리뷰, 패치 생성 등에 사용한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 기본 비교
diff file1.txt file2.txt
# 3c3         &amp;larr; 3번째 줄이 변경(changed)됨
# &amp;lt; old line  &amp;larr; file1의 내용
# ---
# &amp;gt; new line  &amp;larr; file2의 내용

# Unified 형식 (-u) ★★★ &amp;mdash; 가장 읽기 쉬움 (git diff와 동일한 형식)
diff -u file1.txt file2.txt
# --- file1.txt
# +++ file2.txt
# @@ -1,5 +1,5 @@
#  unchanged line
# -deleted line
# +added line
#  unchanged line&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Chapter 11. 파이프 &amp;amp; 리다이렉션&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프와 리다이렉션은 UNIX 철학의 핵심인 &quot;작은 도구를 조합하여 복잡한 작업을 수행한다&quot;를 실현하는 메커니즘이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리다이렉션 (Redirection)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리다이렉션은 &lt;b&gt;표준 스트림의 방향을 바꾸는 것&lt;/b&gt;이다. 기본적으로 stdout은 화면에 출력되고 stdin은 키보드에서 입력받지만, 이를 파일이나 다른 스트림으로 변경할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출력 리다이렉션 (&lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;)&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# stdout을 파일로 저장 (덮어쓰기)
echo &quot;Hello&quot; &amp;gt; output.txt

# stdout을 파일에 추가 (append)
echo &quot;World&quot; &amp;gt;&amp;gt; output.txt

# 명령어 결과를 파일로 저장
ls -la /var/log &amp;gt; file_list.txt
df -h &amp;gt; disk_status.txt
ps aux &amp;gt; process_list.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; vs &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; &amp;mdash; 중요한 차이:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; : 파일이 이미 존재하면 &lt;b&gt;내용을 완전히 덮어쓴다&lt;/b&gt;. 파일이 없으면 새로 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; : 파일이 이미 존재하면 기존 내용 &lt;b&gt;뒤에 추가&lt;/b&gt;한다. 파일이 없으면 새로 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# 파일 비우기 (truncate)
&amp;gt; /var/log/large_app.log    # 파일 내용을 0바이트로 만듦 (파일은 유지)
# 이것은 rm 후 재생성보다 안전하다. 파일을 열고 있는 프로세스에 영향이 적다.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 리다이렉션 (&lt;code&gt;2&amp;gt;&lt;/code&gt;, &lt;code&gt;2&amp;gt;&amp;gt;&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stderr(파일 디스크립터 2)를 별도로 제어할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 에러만 파일로 저장
find / -name &quot;*.conf&quot; 2&amp;gt; errors.txt

# 에러를 버리기 (화면에 표시하지 않기) ★★★
find / -name &quot;*.conf&quot; 2&amp;gt;/dev/null

# 에러를 별도 파일에 추가
command 2&amp;gt;&amp;gt; error_log.txt&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 stdout과 stderr를 동시에 제어&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# stdout은 파일로, stderr는 다른 파일로
command &amp;gt; output.txt 2&amp;gt; errors.txt

# stdout과 stderr를 같은 파일로 ★★★
command &amp;gt; all_output.txt 2&amp;gt;&amp;amp;1
# 해석: stdout(1)을 all_output.txt로 &amp;rarr; stderr(2)를 stdout(1)이 가리키는 곳으로

# 위와 동일한 축약 문법 (bash 4.0+)
command &amp;amp;&amp;gt; all_output.txt

# stdout과 stderr를 모두 버리기 ★★★
command &amp;gt; /dev/null 2&amp;gt;&amp;amp;1
# 또는
command &amp;amp;&amp;gt; /dev/null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt;의 의미:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;2&amp;gt;&lt;/code&gt; : stderr(fd 2)의 방향을 바꾼다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;1&lt;/code&gt; : fd 1(stdout)이 &lt;b&gt;현재 가리키고 있는 곳&lt;/b&gt;으로 보낸다&lt;/li&gt;
&lt;li&gt;따라서 &lt;code&gt;&amp;gt; file 2&amp;gt;&amp;amp;1&lt;/code&gt;은 &quot;stdout을 file로 보내고, stderr도 같은 곳(file)으로 보내라&quot;는 뜻이다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 순서가 중요하다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;&amp;gt; file 2&amp;gt;&amp;amp;1&lt;/code&gt; &amp;rarr; ✓ 올바름 (stdout이 먼저 file로 향하고, stderr가 따라감)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2&amp;gt;&amp;amp;1 &amp;gt; file&lt;/code&gt; &amp;rarr; ✗ 의도와 다름 (stderr가 원래 stdout=화면으로 향하고, stdout만 file로 감)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;입력 리다이렉션 (&lt;code&gt;&amp;lt;&lt;/code&gt;)&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 파일을 stdin으로 전달
sort &amp;lt; unsorted.txt

# 실무에서는 파이프가 더 흔하지만, 일부 명령어에서 사용
mysql -u root -p database &amp;lt; schema.sql    # SQL 파일 실행
wc -l &amp;lt; data.txt                           # 파일명 없이 줄 수만 출력&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Here Document (&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 줄의 텍스트를 stdin으로 전달한다. 스크립트에서 파일을 생성하거나, 대화형 명령어에 입력을 전달할 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 파일 생성
cat &amp;lt;&amp;lt; EOF &amp;gt; /etc/myapp/config.yaml
server:
  host: 0.0.0.0
  port: 8080
database:
  host: ${DB_HOST}
  port: 5432
EOF

# 변수 확장을 막으려면 구분자를 따옴표로 감싼다
cat &amp;lt;&amp;lt; 'EOF' &amp;gt; script_template.sh
echo &quot;Current user: $USER&quot;
echo &quot;Home: $HOME&quot;
EOF
# 위 경우 $USER, $HOME이 확장되지 않고 그대로 기록된다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파이프 (Pipe, &lt;code&gt;|&lt;/code&gt;)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프(&lt;code&gt;|&lt;/code&gt;)는 앞 명령어의 &lt;b&gt;stdout을 뒤 명령어의 stdin으로 연결&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;명령어A  |  명령어B  |  명령어C
stdout ──&amp;rarr; stdin   stdout ──&amp;rarr; stdin   stdout &amp;rarr; 화면&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 기본 예시
cat /etc/passwd | grep &quot;bash&quot; | wc -l
# 1) cat이 파일 내용을 stdout으로 출력
# 2) grep이 stdin에서 &quot;bash&quot; 포함 줄을 필터링
# 3) wc -l이 stdin에서 줄 수를 센다&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파이프는 stdout만 전달한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프는 &lt;b&gt;stdout만&lt;/b&gt; 전달한다. stderr는 파이프를 통과하지 않고 화면에 그대로 출력된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# stderr는 파이프를 통과하지 않는다
find / -name &quot;*.conf&quot; | grep nginx
# Permission denied 에러 메시지(stderr)는 여전히 화면에 출력된다

# stderr도 파이프로 전달하려면
find / -name &quot;*.conf&quot; 2&amp;gt;&amp;amp;1 | grep nginx
# stderr를 stdout으로 합친 후 파이프 전달&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 파이프 패턴&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# 1. 로그 분석: 에러 추출 &amp;rarr; 빈도 분석
grep &quot;ERROR&quot; app.log | awk '{print $4}' | sort | uniq -c | sort -rn

# 2. 프로세스 찾기
ps aux | grep nginx | grep -v grep

# 3. 디스크 사용량 정렬
du -sh /var/*/ 2&amp;gt;/dev/null | sort -rh | head -10

# 4. 파일 내용을 클립보드에 복사 (xclip 설치 필요)
cat config.yaml | xclip -selection clipboard

# 5. JSON 응답 정리 (jq 설치 필요)
curl -s https://api.example.com/data | jq '.items[] | .name'

# 6. 실시간 로그 필터링
tail -f /var/log/syslog | grep --line-buffered &quot;error&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;tee&lt;/code&gt; &amp;mdash; 출력을 파일과 화면 동시에&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tee&lt;/code&gt;는 stdin을 받아서 &lt;b&gt;파일과 stdout 양쪽에 동시 출력&lt;/b&gt;하는 명령어이다. 이름은 T자 모양 배관(pipe fitting)에서 유래했다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;           ┌──&amp;rarr; 파일
stdin ──&amp;rarr; tee
           └──&amp;rarr; stdout (다음 파이프 또는 화면)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 화면에 출력하면서 파일에도 저장
ls -la | tee file_list.txt

# 파일에 추가(append)하면서 화면에도 출력
echo &quot;new entry&quot; | tee -a log.txt

# 여러 파일에 동시 저장
echo &quot;config&quot; | tee file1.txt file2.txt file3.txt

# sudo와 함께 사용 &amp;mdash; 권한이 필요한 파일에 쓰기 ★
echo &quot;new line&quot; | sudo tee -a /etc/hosts
# 주의: sudo echo &quot;...&quot; &amp;gt; /etc/hosts는 동작하지 않는다
# &amp;gt; 리다이렉션은 셸이 처리하므로 sudo의 권한이 적용되지 않기 때문이다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스 치환 (Process Substitution)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스 치환은 명령어의 &lt;b&gt;출력을 임시 파일처럼&lt;/b&gt; 다른 명령어에 전달하는 기법이다.&lt;br /&gt;&lt;code&gt;&amp;lt;(command)&lt;/code&gt; 형태로 사용한다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 두 명령어의 출력을 diff로 비교
diff &amp;lt;(ls dir1/) &amp;lt;(ls dir2/)

# 두 서버의 설정 파일 비교
diff &amp;lt;(ssh server1 cat /etc/nginx/nginx.conf) \
     &amp;lt;(ssh server2 cat /etc/nginx/nginx.conf)

# 정렬된 두 결과를 comm으로 비교
comm -12 &amp;lt;(sort file1.txt) &amp;lt;(sort file2.txt)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;diff&lt;/code&gt;나 &lt;code&gt;comm&lt;/code&gt; 같은 명령어는 &lt;b&gt;파일&lt;/b&gt;을 인자로 받는다. 파이프로는 하나의 stdin만 전달할 수 있으므로, 두 명령어의 출력을 동시에 전달할 수 없다. 프로세스 치환은 명령어의 출력을 &lt;b&gt;임시 파일 디스크립터&lt;/b&gt;로 만들어 이 문제를 해결한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;xargs&lt;/code&gt; vs 파이프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프는 데이터를 &lt;b&gt;stdin 스트림&lt;/b&gt;으로 전달하지만, 일부 명령어는 stdin이 아닌 &lt;b&gt;인자(argument)&lt;/b&gt;로 데이터를 받는다. 이때 &lt;code&gt;xargs&lt;/code&gt;가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# rm은 stdin으로 파일명을 받지 않는다
find /tmp -name &quot;*.tmp&quot; | rm       # ✗ 동작하지 않음
find /tmp -name &quot;*.tmp&quot; | xargs rm # ✓ xargs가 stdin을 인자로 변환&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Chapter 12. vim 편집기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 SSH로 접속했을 때, GUI 에디터는 없다. 사용할 수 있는 텍스트 편집기는 &lt;code&gt;vi&lt;/code&gt;(또는 개선판인 &lt;code&gt;vim&lt;/code&gt;)와 &lt;code&gt;nano&lt;/code&gt; 정도이다. &lt;code&gt;vim&lt;/code&gt;은 거의 &lt;b&gt;모든 리눅스 시스템에 기본 설치&lt;/b&gt;되어 있으며, &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;crontab -e&lt;/code&gt;, &lt;code&gt;visudo&lt;/code&gt; 등 많은 시스템 도구가 기본 편집기로 vim을 호출한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vi와 vim의 관계&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;vi&lt;/b&gt;: 1976년에 만들어진 원조 편집기이다. Bill Joy가 개발했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;vim&lt;/b&gt;: &lt;b&gt;Vi IMproved&lt;/b&gt;의 약자이다. vi를 기반으로 구문 강조, 다중 되돌리기, 플러그인 등 수많은 기능을 추가한 개선판이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 최신 리눅스 배포판에서 &lt;code&gt;vi&lt;/code&gt;를 실행하면 실제로는 &lt;code&gt;vim&lt;/code&gt;이 실행된다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# vim 설치 확인
vim --version | head -1

# 없으면 설치
sudo apt install vim       # Debian/Ubuntu
sudo dnf install vim       # RHEL/Fedora&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vim의 모드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vim이 다른 편집기와 다른 점은 &lt;b&gt;모드(mode)&lt;/b&gt; 개념이다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;                     i, a, o
         ┌──────── Normal ─────────┐
         │          Mode           │
         │    (명령어 입력 모드)       │
    Esc  │                         │  Esc
         │   :     ──&amp;rarr; Command     │
         │            Mode         │
         │         (저장,종료 등)     │
         │                         │
         └────────&amp;rarr; Insert ────────┘
                    Mode
              (텍스트 입력 모드)&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모드&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;진입 방법&lt;/th&gt;
&lt;th&gt;빠져나가는 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Normal&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;커서 이동, 복사, 삭제, 검색 등 &lt;b&gt;명령&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;vim 시작 시 기본&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Insert&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;실제 &lt;b&gt;텍스트 입력&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;i&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;o&lt;/code&gt; 등&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Command&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;저장, 종료, 치환 등 &lt;b&gt;Ex 명령어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Normal에서 &lt;code&gt;:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Enter&lt;/code&gt; 또는 &lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Visual&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;텍스트 &lt;b&gt;선택&lt;/b&gt; (블록 복사/삭제)&lt;/td&gt;
&lt;td&gt;Normal에서 &lt;code&gt;v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 어떤 모드인지 모르겠으면 &lt;b&gt;&lt;code&gt;Esc&lt;/code&gt;를 누른다&lt;/b&gt;. &lt;code&gt;Esc&lt;/code&gt;는 항상 Normal 모드로 돌아간다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 열기&lt;/h3&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;vim filename.txt           # 파일 열기 (없으면 새로 생성)
vim +10 filename.txt       # 10번째 줄에서 열기
vim +/error filename.txt   # &quot;error&quot;가 처음 나오는 줄에서 열기
vim -R filename.txt        # 읽기 전용으로 열기&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;저장 &amp;amp; 종료 (Command 모드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Normal 모드에서 &lt;code&gt;:&lt;/code&gt;를 누르면 Command 모드로 진입한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;저장 (Write)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;종료 (Quit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:wq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;저장 후 종료 ★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:q!&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;저장하지 않고 강제 종료 ★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:wq!&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;강제 저장 후 종료 (읽기 전용 파일도)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ZZ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:wq&lt;/code&gt;와 동일 (Normal 모드에서 바로 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:w newfile.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다른 이름으로 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;vim을 빠져나올 수 없다면?&lt;/b&gt;: &lt;code&gt;Esc&lt;/code&gt;를 누른 후 &lt;code&gt;:q!&lt;/code&gt;를 입력하면 무조건 나갈 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Insert 모드 진입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Normal 모드에서 다음 키를 누르면 Insert 모드로 전환된다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;키&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;i&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서 &lt;b&gt;앞에서&lt;/b&gt; 입력 시작 (가장 많이 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서 &lt;b&gt;뒤에서&lt;/b&gt; 입력 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;I&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄의 &lt;b&gt;맨 앞에서&lt;/b&gt; 입력 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄의 &lt;b&gt;맨 뒤에서&lt;/b&gt; 입력 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 줄 &lt;b&gt;아래에 새 줄&lt;/b&gt; 추가 후 입력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;O&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 줄 &lt;b&gt;위에 새 줄&lt;/b&gt; 추가 후 입력&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Normal 모드 &amp;mdash; 커서 이동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 이동&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;키&lt;/th&gt;
&lt;th&gt;이동&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;h&lt;/code&gt; / &lt;code&gt;l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;좌 / 우 (한 글자)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;j&lt;/code&gt; / &lt;code&gt;k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;아래 / 위 (한 줄)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다음 단어의 시작으로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이전 단어의 시작으로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 단어의 끝으로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄의 맨 처음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄의 맨 끝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄의 첫 번째 비공백 문자&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;큰 이동&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;키&lt;/th&gt;
&lt;th&gt;이동&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일의 맨 처음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일의 맨 끝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;10G&lt;/code&gt; 또는 &lt;code&gt;:10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10번째 줄로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl + f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;한 페이지 아래 (forward)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl + b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;한 페이지 위 (backward)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl + d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;반 페이지 아래&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl + u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;반 페이지 위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;H&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;화면 맨 위 (High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;화면 중간 (Middle)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;화면 맨 아래 (Low)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Normal 모드 &amp;mdash; 편집 명령&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삭제&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서 위치의 한 글자 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 줄 전체 삭제 ★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3dd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 줄부터 3줄 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 단어 끝까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d$&lt;/code&gt; 또는 &lt;code&gt;D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 줄 끝까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 줄 처음까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 파일 끝까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dgg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 파일 처음까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복사 &amp;amp; 붙여넣기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vim에서 복사는 &lt;b&gt;yank&lt;/b&gt;라고 부른다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 줄 복사 ★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3yy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 줄부터 3줄 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;단어 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서 아래/뒤에 붙여넣기 ★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;P&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서 위/앞에 붙여넣기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;되돌리기 &amp;amp; 다시 실행&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;되돌리기 (Undo) ★&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl + r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다시 실행 (Redo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;마지막 명령 반복&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검색 &amp;amp; 치환&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;/pattern    &amp;rarr; 아래 방향으로 검색
?pattern    &amp;rarr; 위 방향으로 검색
n           &amp;rarr; 다음 검색 결과로 이동
N           &amp;rarr; 이전 검색 결과로 이동
*           &amp;rarr; 커서 위의 단어를 아래 방향으로 검색
#           &amp;rarr; 커서 위의 단어를 위 방향으로 검색&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;치환 (Command 모드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sed&lt;/code&gt;와 거의 동일한 문법이다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# 현재 줄에서 치환 (첫 번째만)
:s/old/new/

# 현재 줄에서 모든 매치 치환
:s/old/new/g

# 파일 전체에서 치환 ★★★
:%s/old/new/g

# 파일 전체에서 치환 (확인하면서)
:%s/old/new/gc
# 각 매치마다 y(yes), n(no), a(all), q(quit)를 선택

# 특정 범위에서 치환 (10~20번째 줄)
:10,20s/old/new/g

# 대소문자 무시 치환
:%s/old/new/gi&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;~/.vimrc&lt;/code&gt; &amp;mdash; 영구 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vim을 열 때마다 설정을 반복하고 싶지 않다면 &lt;code&gt;~/.vimrc&lt;/code&gt; 파일에 설정을 저장한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# 실무에 적합한 최소 .vimrc
cat &amp;lt;&amp;lt; 'EOF' &amp;gt; ~/.vimrc
set number              &quot; 줄 번호 표시
syntax on               &quot; 구문 강조
set hlsearch            &quot; 검색 하이라이트
set incsearch           &quot; 입력하면서 검색
set tabstop=4           &quot; 탭 너비
set shiftwidth=4        &quot; 자동 들여쓰기 너비
set expandtab           &quot; 탭을 공백으로
set autoindent          &quot; 자동 들여쓰기
set showmatch           &quot; 괄호 매치 표시
set ruler               &quot; 커서 위치 표시
set encoding=utf-8      &quot; UTF-8 인코딩
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;키&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;텍스트 입력하고 싶다&lt;/td&gt;
&lt;td&gt;&lt;code&gt;i&lt;/code&gt; &amp;rarr; 입력 &amp;rarr; &lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;새 줄 추가하고 싶다&lt;/td&gt;
&lt;td&gt;&lt;code&gt;o&lt;/code&gt; &amp;rarr; 입력 &amp;rarr; &lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;저장하고 종료&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt; &amp;rarr; &lt;code&gt;:wq&lt;/code&gt; &amp;rarr; Enter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;저장 안 하고 종료&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Esc&lt;/code&gt; &amp;rarr; &lt;code&gt;:q!&lt;/code&gt; &amp;rarr; Enter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;줄 삭제&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dd&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;되돌리기&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검색&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/keyword&lt;/code&gt; &amp;rarr; &lt;code&gt;n&lt;/code&gt;(다음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전체 치환&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:%s/old/new/g&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;줄 번호 표시&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:set number&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모르겠으면&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;Esc&lt;/code&gt; 연타&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Chapter 13. 프로세스 관리&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로그램 vs 프로세스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램(Program)은 디스크에 저장된 &lt;b&gt;실행 파일&lt;/b&gt;이다. &lt;code&gt;/usr/bin/nginx&lt;/code&gt;, &lt;code&gt;/usr/bin/python3&lt;/code&gt; 같은 바이너리 파일이 프로그램이다. 프로그램은 정적(static)이다 &amp;mdash; 그냥 파일일 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스(Process)는 프로그램이 메모리에 로드되어 &lt;b&gt;실행 중인 인스턴스&lt;/b&gt;이다. 하나의 프로그램에서 여러 프로세스가 생성될 수 있다. 예를 들어 터미널 3개를 열면 &lt;code&gt;bash&lt;/code&gt; 프로세스가 3개 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스는 다음을 가진다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PID (Process ID)&lt;/b&gt;: 프로세스의 고유 식별 번호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PPID (Parent PID)&lt;/b&gt;: 자신을 생성한 부모 프로세스의 PID&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UID/GID&lt;/b&gt;: 프로세스를 실행한 사용자/그룹&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 공간&lt;/b&gt;: 코드, 데이터, 스택, 힙&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 디스크립터&lt;/b&gt;: 열린 파일, 소켓, 파이프 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경변수&lt;/b&gt;: 프로세스에 전달된 설정값&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PID 1 &amp;mdash; init/systemd&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 부팅 시 커널이 가장 먼저 실행하는 프로세스가 &lt;b&gt;PID 1&lt;/b&gt;이다.&lt;br /&gt;최신 배포판에서는 &lt;code&gt;systemd&lt;/code&gt;가 PID 1이다.&lt;br /&gt;모든 다른 프로세스는 PID 1의 자식(또는 자손)이다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# PID 1 확인
ps -p 1 -o comm=
# systemd

# 프로세스 트리 확인
pstree -p | head -20
# systemd(1)─┬─sshd(1234)───sshd(5678)───bash(5679)
#            ├─nginx(2345)─┬─nginx(2346)
#            │             └─nginx(2347)
#            └─dockerd(3456)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로세스의 생성 &amp;mdash; fork와 exec&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서 새 프로세스는 &lt;b&gt;fork-exec&lt;/b&gt; 메커니즘으로 생성된다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;fork()&lt;/b&gt;: 현재 프로세스(부모)의 복사본을 만든다 &amp;rarr; 자식 프로세스 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;exec()&lt;/b&gt;: 자식 프로세스의 메모리를 새로운 프로그램으로 교체한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셸에서 명령어를 실행하면 이 과정이 일어난다:&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;bash(PID 100)  &amp;rarr;  fork()  &amp;rarr;  bash(PID 200, 복사본)  &amp;rarr;  exec(&quot;ls&quot;)  &amp;rarr;  ls(PID 200)
    (부모)                       (자식)                                  (프로그램 교체)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좀비 프로세스와 고아 프로세스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;좀비 프로세스(Zombie)&lt;/b&gt;: 실행이 끝났지만 부모가 아직 종료 상태를 회수하지 않은 프로세스이다. &lt;code&gt;ps&lt;/code&gt;에서 상태가 &lt;code&gt;Z&lt;/code&gt;로 표시된다. 소량은 정상이지만, 대량 발생하면 PID가 고갈될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;고아 프로세스(Orphan)&lt;/b&gt;: 부모 프로세스가 먼저 종료된 자식 프로세스이다. PID 1(systemd)이 자동으로 입양(adopt)하여 관리한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 좀비 프로세스 찾기
ps aux | awk '$8 == &quot;Z&quot; {print}'&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스 확인 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;ps&lt;/code&gt; &amp;mdash; 프로세스 상태 스냅샷&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ps&lt;/code&gt;는 실행 중인 프로세스의 &lt;b&gt;현재 상태를 한 번 캡처&lt;/b&gt;하여 보여준다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 모든 프로세스 상세 정보 ★★★
ps aux
# USER       PID %CPU %MEM    VSZ   RSS TTY  STAT START   TIME COMMAND
# root         1  0.0  0.3 169484 12340 ?    Ss   Mar20   0:05 /usr/lib/systemd/systemd
# nginx     2345  0.1  0.5  45678 20480 ?    S    Mar20   1:23 nginx: worker process

# 특정 프로세스 검색 ★★★
ps aux | grep nginx
ps aux | grep -v grep | grep nginx    # grep 자체 제외

# 프로세스 트리 형태로 보기
ps auxf

# 특정 필드만 출력 (-o: format)
ps -eo pid,ppid,user,%cpu,%mem,comm --sort=-%mem | head -15

# 특정 사용자의 프로세스
ps -u nginx

# 특정 PID의 상세 정보
ps -p 1234 -f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;ps aux&lt;/code&gt; 출력 필드&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;USER&lt;/td&gt;
&lt;td&gt;프로세스 소유자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PID&lt;/td&gt;
&lt;td&gt;프로세스 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%CPU&lt;/td&gt;
&lt;td&gt;CPU 사용률&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%MEM&lt;/td&gt;
&lt;td&gt;메모리 사용률&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VSZ&lt;/td&gt;
&lt;td&gt;가상 메모리 크기 (KB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSS&lt;/td&gt;
&lt;td&gt;실제 사용 중인 물리 메모리 (KB) &amp;mdash; &lt;b&gt;Resident Set Size&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTY&lt;/td&gt;
&lt;td&gt;연결된 터미널 (&lt;code&gt;?&lt;/code&gt;면 터미널 없음 = 데몬)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STAT&lt;/td&gt;
&lt;td&gt;프로세스 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;START&lt;/td&gt;
&lt;td&gt;시작 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TIME&lt;/td&gt;
&lt;td&gt;CPU 사용 누적 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COMMAND&lt;/td&gt;
&lt;td&gt;실행 명령어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STAT(프로세스 상태) 코드:&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;R&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Running &amp;mdash; 실행 중 또는 실행 대기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sleeping &amp;mdash; 이벤트 대기 중 (대부분의 프로세스)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uninterruptible Sleep &amp;mdash; I/O 대기 (디스크 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stopped &amp;mdash; 중지됨 (Ctrl+Z 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zombie &amp;mdash; 종료되었지만 부모가 회수하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;pgrep&lt;/code&gt; &amp;mdash; 프로세스 이름으로 PID 찾기&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 프로세스 이름으로 PID 검색
pgrep nginx
# 2345
# 2346

# 상세 정보와 함께
pgrep -a nginx
# 2345 nginx: master process /usr/sbin/nginx
# 2346 nginx: worker process

# 특정 사용자의 프로세스만
pgrep -u www-data&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스 종료 &amp;mdash; 시그널(Signal)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시그널이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시그널은 프로세스에 보내는 비동기적 알림(notification)이다. &quot;이 일을 해라&quot;, &quot;종료해라&quot;, &quot;멈춰라&quot; 같은 메시지를 프로세스에 전달하는 메커니즘이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 시그널&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;번호&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;기본 동작&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGHUP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;종료&lt;/td&gt;
&lt;td&gt;터미널 연결이 끊어질 때. 데몬에서는 &lt;b&gt;설정 다시 읽기&lt;/b&gt;용으로 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGINT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;종료&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl + C&lt;/code&gt;에 해당. 사용자가 인터럽트 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGKILL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;강제 종료&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;프로세스가 무시/가로챌 수 없다. &lt;b&gt;최후의 수단&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGTERM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;종료&lt;/td&gt;
&lt;td&gt;&lt;b&gt;정상 종료 요청&lt;/b&gt; (기본 시그널). 프로세스가 정리 작업 후 종료 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGCONT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;재개&lt;/td&gt;
&lt;td&gt;중지된 프로세스를 재개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGSTOP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;중지&lt;/td&gt;
&lt;td&gt;프로세스 일시 중지. 무시/가로챌 수 없다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGTSTP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;중지&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl + Z&lt;/code&gt;에 해당&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SIGTERM vs SIGKILL&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SIGTERM (15)&lt;/b&gt;: &quot;깔끔하게 종료해주세요&quot;라는 &lt;b&gt;요청&lt;/b&gt;이다. 프로세스는 이 시그널을 받으면 열린 파일을 닫고, 임시 파일을 정리하고, 연결을 해제한 뒤 종료할 수 있다. 항상 먼저 시도해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SIGKILL (9)&lt;/b&gt;: &quot;지금 즉시 죽어라&quot;라는 &lt;b&gt;명령&lt;/b&gt;이다. 커널이 프로세스를 즉시 종료시킨다. 프로세스는 어떤 정리 작업도 할 수 없다. 데이터 손실, 파일 손상, 잠금 미해제 등의 문제가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;kill&lt;/code&gt; &amp;mdash; 시그널 보내기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;kill&lt;/code&gt;이라는 이름이지만, 실제로는 &lt;b&gt;시그널을 보내는&lt;/b&gt; 명령어이다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# SIGTERM 보내기 (기본, 정상 종료 요청) ★
kill 1234
kill -15 1234
kill -SIGTERM 1234
# 세 가지 모두 동일

# SIGKILL 보내기 (강제 종료, 최후의 수단)
kill -9 1234
kill -SIGKILL 1234

# 설정 다시 읽기 (SIGHUP) &amp;mdash; 데몬을 재시작하지 않고 설정 반영
kill -1 $(pgrep nginx)
kill -HUP $(pgrep nginx)

# 여러 프로세스에 시그널 보내기
kill 1234 1235 1236&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;pkill&lt;/code&gt; / &lt;code&gt;killall&lt;/code&gt; &amp;mdash; 이름으로 종료&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 프로세스 이름으로 종료
pkill nginx
pkill -9 nginx          # 강제 종료

# 특정 사용자의 프로세스만 종료
pkill -u testuser

# killall: 정확한 이름으로 종료
killall nginx
killall -9 hung_process&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;올바른 프로세스 종료 순서&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 1단계: SIGTERM으로 정상 종료 요청
kill 1234

# 2단계: 잠시 대기 (프로세스가 정리할 시간을 준다)
sleep 5

# 3단계: 아직 살아있는지 확인
ps -p 1234

# 4단계: 여전히 살아있으면 SIGKILL
kill -9 1234&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;포그라운드 &amp;amp; 백그라운드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;포그라운드(Foreground)&lt;/b&gt;: 터미널을 점유하고 있는 프로세스이다. 명령어를 실행하면 기본적으로 포그라운드에서 실행된다. 프로세스가 끝나거나 중지할 때까지 터미널에 다른 명령어를 입력할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;백그라운드(Background)&lt;/b&gt;: 터미널을 점유하지 않고 실행되는 프로세스이다. 명령어 끝에 &lt;code&gt;&amp;amp;&lt;/code&gt;를 붙이면 백그라운드로 실행된다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 백그라운드로 실행
./long_running_task.sh &amp;amp;
# [1] 12345    &amp;larr; 작업 번호 1, PID 12345

# 포그라운드 프로세스를 백그라운드로 전환
# Ctrl + Z  (SIGTSTP &amp;rarr; 일시 중지)
# bg        (백그라운드에서 재개)

# 백그라운드 프로세스를 포그라운드로 전환
fg
fg %1     # 작업 번호 지정

# 백그라운드 작업 목록 확인
jobs
# [1]+  Running    ./long_task.sh &amp;amp;
# [2]-  Stopped    vim file.txt&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;nohup&lt;/code&gt; &amp;mdash; 터미널 종료 후에도 실행 유지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널(SSH 세션)을 닫으면 해당 터미널에서 실행한 프로세스는 SIGHUP을 받아 종료된다. &lt;code&gt;nohup&lt;/code&gt;은 SIGHUP을 무시하여 &lt;b&gt;터미널을 닫아도 프로세스가 계속 실행&lt;/b&gt;되게 한다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# nohup + 백그라운드 실행
nohup ./long_running_task.sh &amp;amp;
# 출력은 nohup.out 파일에 저장된다

# 출력을 특정 파일로 지정
nohup ./task.sh &amp;gt; /var/log/task.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;lsof&lt;/code&gt; &amp;mdash; 열린 파일 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;List Open Files&lt;/b&gt;의 약자이다. &lt;code&gt;lsof&lt;/code&gt;는 파일뿐만 아니라 &lt;b&gt;네트워크 소켓, 파이프, 디바이스&lt;/b&gt; 등 프로세스가 열고 있는 모든 것을 보여준다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 특정 프로세스가 열고 있는 파일
lsof -p 1234

# 특정 파일을 사용 중인 프로세스
lsof /var/log/syslog

# 삭제되었지만 아직 열려있는 파일 (디스크 공간 미해제 원인)
lsof +L1

# 특정 포트를 사용 중인 프로세스 ★★★
lsof -i :80
lsof -i :8080

# 특정 사용자가 열고 있는 파일
lsof -u nginx

# 특정 디렉터리의 파일을 사용 중인 프로세스
lsof +D /var/log/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;핵심&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ps aux&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;프로세스 목록&lt;/td&gt;
&lt;td&gt;가장 기본적인 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pgrep&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이름으로 PID 검색&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pgrep -a&lt;/code&gt; (상세)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pstree&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;프로세스 트리&lt;/td&gt;
&lt;td&gt;부모-자식 관계 파악&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;시그널 보내기&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-15&lt;/code&gt;(정상) &amp;rarr; &lt;code&gt;-9&lt;/code&gt;(강제) 순서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pkill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이름으로 종료&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkill -u user&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs&lt;/code&gt; / &lt;code&gt;fg&lt;/code&gt; / &lt;code&gt;bg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작업 제어&lt;/td&gt;
&lt;td&gt;포그라운드/백그라운드 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nohup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;터미널 독립 실행&lt;/td&gt;
&lt;td&gt;SSH 끊어져도 생존&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lsof&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;열린 파일/포트 확인&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lsof -i :PORT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Chapter 14. 시스템 모니터링&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애가 발생하면 &quot;지금 서버에서 무슨 일이 일어나고 있는가&quot;를 빠르게 파악해야 한다.&lt;br /&gt;모니터링은 크게 네 가지 리소스를 추적한다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;리소스&lt;/th&gt;
&lt;th&gt;핵심 질문&lt;/th&gt;
&lt;th&gt;주요 도구&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;CPU&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;어떤 프로세스가 CPU를 많이 쓰는가?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;top&lt;/code&gt;, &lt;code&gt;htop&lt;/code&gt;, &lt;code&gt;mpstat&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;메모리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;메모리가 부족한가? 스왑이 발생하는가?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;free&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;vmstat&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;디스크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;I/O 병목이 있는가? 용량이 부족한가?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;iostat&lt;/code&gt;, &lt;code&gt;df&lt;/code&gt;, &lt;code&gt;iotop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;네트워크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;트래픽이 과도한가? 연결 문제가 있는가?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ss&lt;/code&gt;, &lt;code&gt;netstat&lt;/code&gt;, &lt;code&gt;iftop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;top&lt;/code&gt; &amp;mdash; 실시간 시스템 모니터링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;top&lt;/code&gt;은 리눅스의 작업 관리자(Task Manager)에 해당한다. CPU, 메모리 사용량과 프로세스 목록을 실시간으로 보여준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출력 해석&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;top - 14:30:00 up 15 days,  3:22,  2 users,  load average: 0.52, 0.38, 0.41
Tasks: 215 total,   1 running, 213 sleeping,   0 stopped,   1 zombie
%Cpu(s):  5.2 us,  1.8 sy,  0.0 ni, 92.1 id,  0.3 wa,  0.0 hi,  0.6 si,  0.0 st
MiB Mem :   7812.5 total,   1245.3 free,   3210.8 used,   3356.4 buff/cache
MiB Swap:   2048.0 total,   1792.0 free,    256.0 used.   4230.1 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 2345 nginx     20   0  456780  98760  12340 S   3.2  1.2   5:23.45 nginx
 3456 mysql     20   0 1234560 567890  45678 S   2.1  7.1  12:34.56 mysqld&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;헤더 영역 상세 해석:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Load Average&lt;/b&gt; (0.52, 0.38, 0.41):&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1분, 5분, 15분 평균 시스템 부하이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CPU 코어 수와 비교&lt;/b&gt;해야 의미가 있다. 4코어 시스템에서 load 4.0이면 100% 활용, 8.0이면 과부하&lt;/li&gt;
&lt;li&gt;load average가 코어 수를 초과하면 프로세스가 CPU를 기다리는 대기열(queue)이 존재한다는 뜻이다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# CPU 코어 수 확인
nproc
# 또는
grep -c processor /proc/cpuinfo&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;htop&lt;/code&gt; &amp;mdash; 향상된 대화형 모니터링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;top&lt;/code&gt;의 개선판이다. 색상, 그래프, 마우스 지원 등 사용성이 훨씬 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;htop&lt;/code&gt;의 장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU, 메모리 사용률을 &lt;b&gt;그래프 바&lt;/b&gt;로 시각화&lt;/li&gt;
&lt;li&gt;화살표 키와 마우스로 탐색 가능&lt;/li&gt;
&lt;li&gt;프로세스를 직접 선택하여 시그널 전송 가능 (F9)&lt;/li&gt;
&lt;li&gt;트리 보기 (F5)&lt;/li&gt;
&lt;li&gt;필터링 (F4) / 검색 (F3)&lt;/li&gt;
&lt;li&gt;설정 커스터마이징 (F2)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;uptime&lt;/code&gt; &amp;mdash; 시스템 가동 시간 &amp;amp; 부하&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ uptime
 14:30:00 up 15 days,  3:22,  2 users,  load average: 0.52, 0.38, 0.41&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간결하게 load average를 확인할 때 자주 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;iostat&lt;/code&gt; &amp;mdash; 디스크 I/O 모니터링&lt;/h2&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 설치 (sysstat 패키지)
sudo apt install sysstat

# 확장 통계, 2초 간격
$ iostat -x 2
Device     r/s     w/s   rkB/s   wkB/s  await  %util
sda       12.50   45.30  200.00  1024.00  2.50  15.20
nvme0n1    5.20   10.10  320.00   512.00  0.30   3.40&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장애 대응 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 문제가 발생했을 때 순서대로 확인하는 체계적 접근법&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 1. 전체 상황 파악 (30초 안에)
uptime              # load average 확인
free -h             # 메모리 상태
df -h               # 디스크 용량
top -bn1 | head -20 # CPU/프로세스 상위

# 2. CPU 문제 의심 시
top                 # CPU 점유율 높은 프로세스 확인 (P키)
mpstat -P ALL 1     # CPU 코어별 사용률

# 3. 메모리 문제 의심 시
free -h             # available 확인
vmstat 1 5          # si/so (스왑 활동) 확인
ps aux --sort=-%mem | head -10   # 메모리 점유 상위 프로세스
dmesg -T | grep -i oom          # OOM killer 동작 여부

# 4. 디스크 문제 의심 시
df -h               # 용량 확인
df -i               # inode 확인
iostat -x 1 5       # I/O 활용률 확인
iotop                # 프로세스별 I/O (별도 설치)

# 5. 네트워크 문제 의심 시
ss -tlnp            # 리슨 중인 포트
ping 8.8.8.8        # 외부 연결 확인
curl -v http://localhost  # 웹 서비스 응답 확인&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Chapter 15. 서비스 관리 &amp;amp; systemd 심화&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;systemd&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;init 시스템의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스가 부팅되면 커널은 PID 1 프로세스를 실행한다.&lt;br /&gt;이 PID 1 프로세스가 &lt;b&gt;init 시스템&lt;/b&gt;이며, 이후 모든 서비스(데몬)를 시작하고 관리하는 역할을 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;systemd의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;병렬 부팅&lt;/b&gt;: 서비스들을 동시에 시작하여 부팅 속도가 빠르다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성 관리&lt;/b&gt;: 서비스 간 의존 관계를 선언적으로 정의한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;소켓 기반 활성화&lt;/b&gt;: 소켓 요청이 들어올 때 서비스를 자동 시작한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cgroup 통합&lt;/b&gt;: 프로세스 그룹을 추적하여 자식 프로세스까지 관리한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저널(journal)&lt;/b&gt;: 통합 로그 시스템(journald)을 내장한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타이머&lt;/b&gt;: cron을 대체할 수 있는 스케줄링 기능을 제공한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데몬(Daemon)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데몬(Daemon)은 백그라운드에서 실행되는 프로세스로, 사용자와 직접 상호작용하지 않는다.&lt;br /&gt;웹 서버(nginx), 데이터베이스(mysql), SSH 서버(sshd) 등이 데몬이다.&lt;br /&gt;관례적으로 이름 끝에 &lt;code&gt;d&lt;/code&gt;를 붙인다 (sshd, httpd, dockerd 등).&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;systemctl&lt;/code&gt; &amp;mdash; 서비스 관리의 핵심 명령어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;systemctl&lt;/code&gt;은 systemd를 제어하는 &lt;b&gt;통합 명령어&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 서비스 상태 확인 ★★★
systemctl status nginx
# ● nginx.service - A high performance web server and a reverse proxy server
#      Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
#      Active: active (running) since Mon 2025-03-20 10:00:00 KST; 3 days ago
#    Main PID: 2345 (nginx)
#       Tasks: 5 (limit: 4915)
#      Memory: 12.3M
#      CGroup: /system.slice/nginx.service
#              ├─2345 nginx: master process /usr/sbin/nginx
#              ├─2346 nginx: worker process
#              └─2347 nginx: worker process
#
# Mar 20 10:00:00 web-server systemd[1]: Starting A high performance web server...
# Mar 20 10:00:00 web-server systemd[1]: Started A high performance web server...

# 간단히 활성 여부만 확인
systemctl is-active nginx
# active

# 부팅 시 자동 시작 설정 여부
systemctl is-enabled nginx
# enabled&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Loaded&lt;/td&gt;
&lt;td&gt;unit 파일 경로, enabled/disabled 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;현재 상태 &amp;mdash; active(running), inactive(dead), failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main PID&lt;/td&gt;
&lt;td&gt;메인 프로세스의 PID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tasks&lt;/td&gt;
&lt;td&gt;서비스가 사용 중인 스레드/프로세스 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;서비스가 사용 중인 메모리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CGroup&lt;/td&gt;
&lt;td&gt;서비스에 속한 프로세스 트리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 시작/중지/재시작&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 시작
sudo systemctl start nginx

# 중지
sudo systemctl stop nginx

# 재시작 (stop + start)
sudo systemctl restart nginx

# 설정만 다시 읽기 (중단 없이) ★
sudo systemctl reload nginx
# 모든 서비스가 reload를 지원하지는 않는다

# reload를 시도하고, 안 되면 restart
sudo systemctl reload-or-restart nginx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;restart&lt;/code&gt; vs &lt;code&gt;reload&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;restart&lt;/code&gt;: 프로세스를 완전히 종료 후 다시 시작한다. 순간적으로 서비스가 중단된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reload&lt;/code&gt;: 프로세스를 종료하지 않고 설정 파일만 다시 읽는다. 서비스 중단이 없다. Nginx, Apache, HAProxy 등이 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부팅 시 자동 시작 설정&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 부팅 시 자동 시작 활성화
sudo systemctl enable nginx

# 활성화 + 즉시 시작을 한 번에
sudo systemctl enable --now nginx

# 부팅 시 자동 시작 비활성화
sudo systemctl disable nginx&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 목록 조회&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# 모든 활성 서비스 목록
systemctl list-units --type=service

# 실행 중인 서비스만
systemctl list-units --type=service --state=running

# 실패한 서비스 확인 ★★★
systemctl --failed

# 모든 서비스 (enabled/disabled 포함)
systemctl list-unit-files --type=service&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;journalctl&lt;/code&gt; &amp;mdash; 로그 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd는 &lt;b&gt;journald&lt;/b&gt;라는 통합 로그 시스템을 내장한다. &lt;code&gt;journalctl&lt;/code&gt;은 이 저널 로그를 조회하는 명령어이다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# 전체 로그 (페이저로 열림)
journalctl

# 특정 서비스의 로그 ★★★
journalctl -u nginx
journalctl -u nginx --since &quot;1 hour ago&quot;

# 최근 N줄만 보기
journalctl -u nginx -n 50

# 실시간 로그 추적 (tail -f와 동일) ★★★
journalctl -u nginx -f

# 이번 부팅 이후 로그만
journalctl -b

# 시간 범위 지정
journalctl --since &quot;2025-03-23 14:00&quot; --until &quot;2025-03-23 15:00&quot;

# 특정 우선순위 이상만 (에러 이상)
journalctl -p err
# 우선순위: emerg(0), alert(1), crit(2), err(3), warning(4), notice(5), info(6), debug(7)

# 커널 메시지만 (dmesg와 유사)
journalctl -k

# 디스크 사용량 확인
journalctl --disk-usage

# 오래된 로그 정리
sudo journalctl --vacuum-time=7d     # 7일 이전 삭제
sudo journalctl --vacuum-size=500M   # 500MB 이하로 유지&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Unit 파일 &amp;mdash; systemd의 설정 단위&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Unit이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd가 관리하는 모든 리소스를 &lt;b&gt;Unit&lt;/b&gt;이라고 한다.&lt;br /&gt;서비스뿐만 아니라 마운트, 타이머, 소켓 등도 Unit이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Unit 타입&lt;/th&gt;
&lt;th&gt;확장자&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;데몬 프로세스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.timer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스케줄링 (cron 대안)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Socket&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.socket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;소켓 기반 활성화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mount&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.mount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일시스템 마운트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unit들의 그룹 (부팅 레벨과 유사)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일시스템 경로 감시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Service Unit 파일 구조&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 기존 서비스의 unit 파일 확인
systemctl cat nginx.service
systemctl cat cron.service&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[Unit]
Description=A high performance web server and a reverse proxy server
Documentation=man:nginx(8)
After=network.target remote-fs.target nss-lookup.target
Wants=network-online.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/nginx.pid
TimeoutStopSec=5
KillMode=mixed
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Unit] 섹션&lt;/b&gt; &amp;mdash; 서비스의 메타데이터와 의존성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Service] 섹션&lt;/b&gt; &amp;mdash; 서비스 실행 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Install] 섹션&lt;/b&gt; &amp;mdash; &lt;code&gt;enable&lt;/code&gt;/&lt;code&gt;disable&lt;/code&gt; 시 동작&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;가장 많이 쓰는 형태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서비스 상태 확인&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl status nginx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl start/stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서비스 시작/중지&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서비스 재시작&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl reload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;설정만 다시 읽기&lt;/td&gt;
&lt;td&gt;서비스 중단 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl enable --now&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;자동 시작 + 즉시 시작&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl --failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실패한 서비스 확인&lt;/td&gt;
&lt;td&gt;장애 대응 시 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl daemon-reload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unit 파일 변경 반영&lt;/td&gt;
&lt;td&gt;&lt;b&gt;unit 수정 후 필수&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl edit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;서비스 설정 오버라이드&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;journalctl -u X -f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실시간 서비스 로그&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;Chapter 16. 로그 관리&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그가 왜 중요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그는 시스템과 애플리케이션이 &lt;b&gt;&quot;무슨 일이 일어났는지&quot;를 기록한 텍스트&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션이 죽었다 &amp;rarr; 로그에서 에러 메시지 확인&lt;/li&gt;
&lt;li&gt;보안 침해가 의심된다 &amp;rarr; 인증 로그에서 비정상 접근 확인&lt;/li&gt;
&lt;li&gt;성능이 저하되었다 &amp;rarr; 로그에서 느린 쿼리, 타임아웃 패턴 확인&lt;/li&gt;
&lt;li&gt;배포 후 문제가 생겼다 &amp;rarr; 배포 시점 전후 로그 비교&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리눅스 로그 체계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 가지 로그 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 리눅스에는 두 가지 로그 시스템이 공존한다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① rsyslog (전통적 방식)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트 파일로 &lt;code&gt;/var/log/&lt;/code&gt;에 저장&lt;/li&gt;
&lt;li&gt;오래된 표준이지만 여전히 널리 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt; 등으로 직접 읽을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② journald (systemd 방식)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;바이너리 형식으로 저장 (직접 읽을 수 없음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;journalctl&lt;/code&gt; 명령어로 조회&lt;/li&gt;
&lt;li&gt;구조화된 메타데이터(서비스명, PID, 우선순위 등) 포함&lt;/li&gt;
&lt;li&gt;systemd 서비스의 stdout/stderr를 자동 수집&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 배포판에서 두 시스템이 &lt;b&gt;동시에 동작&lt;/b&gt;한다.&lt;br /&gt;journald가 수집한 로그를 rsyslog가 전달받아 &lt;code&gt;/var/log/&lt;/code&gt;에 텍스트 파일로 저장하는 구조이다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 로그 디렉터리 구조 확인
ls -la /var/log/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그 로테이션 (logrotate)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 파일은 시간이 지나면 계속 커진다. 방치하면 디스크를 가득 채운다. &lt;b&gt;logrotate&lt;/b&gt;는 로그 파일을 주기적으로 회전(rotate)시켜 관리하는 도구이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;access.log      &amp;larr; 현재 기록 중
access.log.1    &amp;larr; 어제 (로테이션 됨)
access.log.2.gz &amp;larr; 그저께 (압축됨)
access.log.3.gz &amp;larr; 3일 전
...
access.log.7.gz &amp;larr; 7일 전 (이후 삭제)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;logrotate 테스트 및 수동 실행&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 설정 문법 검증 (실제로 실행하지 않음)
sudo logrotate -d /etc/logrotate.d/nginx

# 강제 수동 실행
sudo logrotate -f /etc/logrotate.d/nginx

# 전체 logrotate 수동 실행
sudo logrotate -f /etc/logrotate.conf&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Chapter 17. 스케줄링&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 시간 또는 주기적으로 &lt;b&gt;명령어나 스크립트를 자동 실행&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매일 새벽 3시 데이터베이스 백업&lt;/li&gt;
&lt;li&gt;매 5분마다 서비스 헬스체크&lt;/li&gt;
&lt;li&gt;매월 1일 리포트 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서는 &lt;b&gt;cron&lt;/b&gt;(전통적)과 &lt;b&gt;systemd timer&lt;/b&gt;(현대적), 두 가지 방식으로 스케줄링할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;cron &amp;mdash; 전통적 작업 스케줄러&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cron&lt;/code&gt;은 유닉스 시대부터 사용되어 온 작업 스케줄러 데몬이다. 백그라운드에서 항상 실행되며, 설정된 시간이 되면 명령어를 자동 실행한다. 설정 파일을 crontab(cron table)이라고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;crontab 시간 형식&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;┌───────── 분 (0-59)
│ ┌─────── 시 (0-23)
│ │ ┌───── 일 (1-31)
│ │ │ ┌─── 월 (1-12)
│ │ │ │ ┌─ 요일 (0-7, 0과 7은 일요일)
│ │ │ │ │
* * * * * command&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문자&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모든 값&lt;/td&gt;
&lt;td&gt;&lt;code&gt;* * * * *&lt;/code&gt; &amp;rarr; 매분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;,&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;여러 값&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1,15 * * * *&lt;/code&gt; &amp;rarr; 1분, 15분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;범위&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1-5 * * * *&lt;/code&gt; &amp;rarr; 1~5분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;간격&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*/5 * * * *&lt;/code&gt; &amp;rarr; 5분마다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 스케줄 예시&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 매분 실행
* * * * * /path/to/script.sh

# 매시 정각
0 * * * * /path/to/script.sh

# 매일 새벽 3시
0 3 * * * /path/to/script.sh

# 매일 새벽 3시 30분
30 3 * * * /path/to/script.sh

# 5분마다
*/5 * * * * /path/to/script.sh

# 매주 일요일 자정
0 0 * * 0 /path/to/script.sh

# 매월 1일 새벽 2시
0 2 1 * * /path/to/script.sh

# 평일(월~금) 오전 9시
0 9 * * 1-5 /path/to/script.sh

# 매시 0분, 30분 (30분마다)
0,30 * * * * /path/to/script.sh&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;crontab 관리&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 현재 사용자의 crontab 편집 ★★★
crontab -e

# 현재 사용자의 crontab 보기
crontab -l

# 현재 사용자의 crontab 전체 삭제 (주의!)
crontab -r

# 특정 사용자의 crontab 편집 (root만 가능)
sudo crontab -u deploy -e

# 특정 사용자의 crontab 보기
sudo crontab -u deploy -l&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;crontab 작성 시 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 환경변수가 다르다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cron은 사용자의 로그인 셸과 &lt;b&gt;다른 환경&lt;/b&gt;에서 실행된다. &lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;HOME&lt;/code&gt; 등이 최소한으로 설정되어 있다. 그래서 crontab에서는 &lt;b&gt;절대 경로&lt;/b&gt;를 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;# ✗ 잘못된 예 &amp;mdash; PATH에 /usr/local/bin이 없을 수 있음
* * * * * python3 script.py

# ✓ 올바른 예 &amp;mdash; 절대 경로 사용
* * * * * /usr/bin/python3 /home/devops/script.py

# 또는 crontab 상단에 PATH를 직접 설정
PATH=/usr/local/bin:/usr/bin:/bin
* * * * * python3 /home/devops/script.py&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 출력을 반드시 처리하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cron 작업의 stdout/stderr는 기본적으로 사용자에게 &lt;b&gt;메일&lt;/b&gt;로 전송된다. 메일 시스템이 없으면 에러가 쌓인다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 출력을 로그 파일로 저장
0 3 * * * /opt/backup.sh &amp;gt;&amp;gt; /var/log/backup.log 2&amp;gt;&amp;amp;1

# 출력을 아예 버리기
0 3 * * * /opt/backup.sh &amp;gt; /dev/null 2&amp;gt;&amp;amp;1

# 에러만 로그로 남기기
0 3 * * * /opt/backup.sh &amp;gt; /dev/null 2&amp;gt;&amp;gt; /var/log/backup-error.log&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ 중복 실행 방지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업이 오래 걸려서 다음 실행 시점에도 아직 실행 중이면 중복 실행된다. &lt;code&gt;flock&lt;/code&gt;으로 방지할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# flock을 사용한 중복 실행 방지
*/5 * * * * flock -n /tmp/myjob.lock /opt/slow_task.sh
# -n: 이미 잠겨있으면 즉시 종료 (대기하지 않음)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;systemd timer &amp;mdash; 현대적 스케줄러&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 systemd timer를 쓰는가&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;cron&lt;/th&gt;
&lt;th&gt;systemd timer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;의존성 관리&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;다른 서비스에 의존 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로그&lt;/td&gt;
&lt;td&gt;직접 리다이렉션 필요&lt;/td&gt;
&lt;td&gt;journalctl로 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 기록&lt;/td&gt;
&lt;td&gt;별도 관리 필요&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl list-timers&lt;/code&gt;로 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;놓친 실행 처리&lt;/td&gt;
&lt;td&gt;서버 꺼져있으면 놓침&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Persistent=true&lt;/code&gt;로 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초 단위 스케줄링&lt;/td&gt;
&lt;td&gt;불가 (분 단위)&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리소스 제한&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;MemoryMax, CPUQuota 등 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;timer 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd timer는 &lt;b&gt;두 개의 unit 파일&lt;/b&gt;로 구성된다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;.timer&lt;/code&gt; &amp;mdash; 언제 실행할지 (스케줄)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.service&lt;/code&gt; &amp;mdash; 무엇을 실행할지 (작업 내용)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OnCalendar 시간 형식&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 형식: 요일 년-월-일 시:분:초

OnCalendar=*-*-* 03:00:00           # 매일 3시
OnCalendar=Mon *-*-* 09:00:00       # 매주 월요일 9시
OnCalendar=*-*-01 02:00:00          # 매월 1일 2시
OnCalendar=*-01-01 00:00:00         # 매년 1월 1일
OnCalendar=*-*-* *:*:00             # 매분
OnCalendar=*-*-* *:00/30:00         # 30분마다
OnCalendar=hourly                    # 매시간
OnCalendar=daily                     # 매일 자정
OnCalendar=weekly                    # 매주 월요일 자정&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 시간 표현식 검증
systemd-analyze calendar &quot;*-*-* 03:00:00&quot;
# Original form: *-*-* 03:00:00
# Next elapse: Tue 2025-03-24 03:00:00 KST&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;at&lt;/code&gt; &amp;mdash; 일회성 예약 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 시간에 &lt;b&gt;한 번만&lt;/b&gt; 실행할 작업을 예약한다. cron은 반복 실행, &lt;code&gt;at&lt;/code&gt;은 일회성이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# 설치 (없는 경우)
sudo apt install at

# 10분 후에 실행
at now + 10 minutes
&amp;gt; /opt/scripts/deploy.sh
&amp;gt; [Ctrl+D]

# 특정 시간에 실행
at 3:00 AM
&amp;gt; /opt/scripts/maintenance.sh
&amp;gt; [Ctrl+D]

# 내일 오후 5시에 실행
at 5:00 PM tomorrow
&amp;gt; echo &quot;Reminder&quot; | mail -s &quot;Task due&quot; admin@example.com
&amp;gt; [Ctrl+D]

# 예약된 작업 목록
atq

# 예약 취소
atrm &amp;lt;작업번호&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>Learning Log</category>
      <category>Linux</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/40</guid>
      <comments>https://allluck777.tistory.com/40#entry40comment</comments>
      <pubDate>Wed, 25 Mar 2026 09:06:47 +0900</pubDate>
    </item>
    <item>
      <title>[멋사 클라우드 5기] Day 33 &amp;amp; 34 - Linux (1)</title>
      <link>https://allluck777.tistory.com/39</link>
      <description>&lt;h1&gt;Chapter 01. 리눅스 개요&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리눅스란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스(Linux)는 오픈소스 운영체제(Operating System)이다. 정확히 말하면, 리눅스는 커널(Kernel)의 이름이다. 커널이란 하드웨어와 소프트웨어 사이에서 중재자 역할을 하는 운영체제의 핵심 구성 요소이다. &quot;리눅스&quot;라고 부르는 것은 이 리눅스 커널 위에 다양한 시스템 유틸리티, 라이브러리, 셸(Shell), 패키지 매니저 등을 결합한 리눅스 배포판(Distribution)을 의미한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영체제의 구조&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;┌─────────────────────────────────────┐
│          사용자 애플리케이션             │  &amp;larr; 웹 브라우저, 에디터, Docker 등
├─────────────────────────────────────┤
│              셸 (Shell)              │  &amp;larr; bash, zsh 등 명령어 해석기
├─────────────────────────────────────┤
│         시스템 유틸리티 / 라이브러리       │  &amp;larr; GNU 도구들 (ls, cp, grep 등)
├─────────────────────────────────────┤
│         커널 (Kernel) &amp;mdash; Linux        │  &amp;larr; 프로세스, 메모리, 파일시스템, 네트워크 관리
├─────────────────────────────────────┤
│            하드웨어 (Hardware)        │  &amp;larr; CPU, RAM, 디스크, NIC 등
└─────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하드웨어(Hardware)&lt;/b&gt;&lt;br /&gt;CPU, 메모리(RAM), 디스크(SSD/HDD), 네트워크 인터페이스 카드(NIC) 등 물리적 장치이다. 운영체제 없이는 이 하드웨어를 직접 제어하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커널(Kernel)&lt;/b&gt;&lt;br /&gt;운영체제의 심장이다. 커널이 수행하는 핵심 기능은 다음과 같다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로세스 관리&lt;/b&gt;: 어떤 프로그램이 CPU를 얼마나 사용할지 결정한다 (스케줄링)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 관리&lt;/b&gt;: 각 프로그램에 메모리를 할당하고, 부족하면 스왑(swap) 영역을 활용한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일시스템 관리&lt;/b&gt;: 디스크에 데이터를 읽고 쓰는 방식을 제어한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디바이스 드라이버&lt;/b&gt;: 하드웨어 장치와 통신하기 위한 인터페이스를 제공한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 스택&lt;/b&gt;: TCP/IP 통신을 처리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시스템 유틸리티 / 라이브러리&lt;/b&gt;&lt;br /&gt;커널 위에서 동작하는 기본 도구들이다. 파일을 복사하는 &lt;code&gt;cp&lt;/code&gt;, 텍스트를 검색하는 &lt;code&gt;grep&lt;/code&gt;, C 라이브러리인 &lt;code&gt;glibc&lt;/code&gt; 등이 여기에 해당한다. 이 도구들의 상당수는 GNU 프로젝트에서 만들었기 때문에, 리눅스의 정식 명칭은 &quot;GNU/Linux&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;셸(Shell)&lt;/b&gt;&lt;br /&gt;사용자가 키보드로 입력한 명령어를 해석하여 커널에 전달하는 프로그램이다. 대표적으로 &lt;code&gt;bash&lt;/code&gt;(Bourne Again Shell), &lt;code&gt;zsh&lt;/code&gt;, &lt;code&gt;sh&lt;/code&gt; 등이 있다. 셸은 단순한 명령어 해석기를 넘어서, 변수, 조건문, 반복문 등을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자 애플리케이션&lt;/b&gt;&lt;br /&gt;최종 사용자가 직접 사용하는 프로그램이다. 웹 서버(Nginx, Apache), 컨테이너 런타임(Docker), 데이터베이스(MySQL, PostgreSQL) 등이 이 계층에서 동작한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 서버 환경에서 리눅스가 지배적인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 세계 서버의 약 96% 이상이 리눅스를 운영체제로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;무료 &amp;amp; 오픈소스&lt;/b&gt;&lt;br /&gt;라이선스 비용이 없다. 수천 대의 서버를 운영해도 OS 라이선스 비용은 0원이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;안정성 &amp;amp; 보안&lt;/b&gt;&lt;br /&gt;리눅스는 수십 년간 서버 환경에서 검증되었다. 커널과 주요 구성 요소들이 오픈소스이므로 전 세계 개발자들이 보안 취약점을 빠르게 발견하고 수정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;경량성&lt;/b&gt;&lt;br /&gt;GUI 없이 CLI(Command Line Interface)만으로 운영할 수 있어 시스템 리소스를 효율적으로 사용한다. 이는 클라우드 환경에서 비용 절감과 직결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자동화 친화적&lt;/b&gt;&lt;br /&gt;모든 작업을 CLI와 스크립트로 수행할 수 있다. 이것이 DevOps의 핵심인 &lt;b&gt;Infrastructure as Code(IaC)&lt;/b&gt;, &lt;b&gt;CI/CD 파이프라인&lt;/b&gt;, 설정 관리(Configuration Management)**를 가능하게 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커널 (Kernel)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제의 핵심이다. 하드웨어를 직접 제어하며, 애플리케이션이 하드웨어 리소스를 사용할 수 있도록 시스템 콜(System Call)이라는 인터페이스를 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 콜(System Call)이란, 사용자 공간(User Space)의 프로그램이 커널 공간(Kernel Space)의 기능을 요청하는 메커니즘이다. 예를 들어 파일을 열 때(&lt;code&gt;open()&lt;/code&gt;), 프로세스를 생성할 때(&lt;code&gt;fork()&lt;/code&gt;), 네트워크 데이터를 보낼 때(&lt;code&gt;send()&lt;/code&gt;) 모두 시스템 콜을 통해 커널에 요청한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;┌──────────────────────────────┐
│       User Space (사용자 공간)  │  &amp;larr; 애플리케이션, 셸, 유틸리티
│                              │
│    open(), read(), write()   │  &amp;larr; 시스템 콜 호출
├──────────────────────────────┤
│      Kernel Space (커널 공간)  │  &amp;larr; 프로세스 스케줄러, 메모리 관리자
│                              │     파일시스템 드라이버, 네트워크 스택
│      하드웨어 제어 코드           │
└──────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;셸 (Shell)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자와 커널 사이의 인터페이스이다. 사용자가 명령어를 입력하면 셸이 이를 해석하여 커널에 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 셸 종류:&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;셸&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sh&lt;/code&gt; (Bourne Shell)&lt;/td&gt;
&lt;td&gt;가장 오래된 셸이다. POSIX 표준의 기반이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bash&lt;/code&gt; (Bourne Again Shell)&lt;/td&gt;
&lt;td&gt;sh의 개선판이다. 대부분의 리눅스 배포판에서 기본 셸로 사용된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zsh&lt;/code&gt; (Z Shell)&lt;/td&gt;
&lt;td&gt;bash의 기능을 확장한 셸이다. macOS의 기본 셸이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Debian 계열에서 &lt;code&gt;/bin/sh&lt;/code&gt;로 사용하는 경량 셸이다. 스크립트 실행 속도가 빠르다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fish&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용자 친화적인 셸이다. 자동완성 기능이 강력하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 사용 중인 셸 확인:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 현재 셸 확인
echo $SHELL

# 사용 가능한 셸 목록 확인
cat /etc/shells&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;터미널 (Terminal)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널, 콘솔, 셸 &amp;mdash; 이 세 가지 용어는 자주 혼용되지만 엄밀히 다르다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;터미널(Terminal)&lt;/b&gt;: 셸에 접근하기 위한 &lt;b&gt;입출력 장치 또는 프로그램&lt;/b&gt;이다. 물리적 터미널에서 유래했으며, 현재는 소프트웨어 터미널 에뮬레이터(iTerm2, Windows Terminal, GNOME Terminal 등)를 의미한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;콘솔(Console)&lt;/b&gt;: 시스템에 직접 연결된 물리적 터미널이다. 서버실에서 모니터와 키보드로 직접 접속하는 것이 콘솔 접속이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;셸(Shell)&lt;/b&gt;: 터미널 안에서 실행되는 &lt;b&gt;명령어 해석기 프로그램&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 터미널은 창(window)이고, 셸은 그 창 안에서 동작하는 &lt;b&gt;프로그램&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프롬프트 (Prompt) 읽는 법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에 접속하면 다음과 같은 프롬프트가 표시된다:&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;username@hostname:~$&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;부분&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;username&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 로그인한 사용자 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;구분자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hostname&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 시스템의 호스트 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;구분자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 위치한 디렉터리 (&lt;code&gt;~&lt;/code&gt;는 홈)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;일반 사용자를 의미한다. &lt;code&gt;#&lt;/code&gt;이면 root(관리자) 사용자이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;명령어 기본 구조&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;command [options] [arguments]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;command&lt;/b&gt;: 실행할 명령어 (예: &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;options&lt;/b&gt;: 명령어의 동작을 변경하는 플래그 (예: &lt;code&gt;l&lt;/code&gt;, &lt;code&gt;-all&lt;/code&gt;, &lt;code&gt;v&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;arguments&lt;/b&gt;: 명령어가 처리할 대상 (예: 파일명, 디렉터리명)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 예시
ls -la /var/log
#  │  │    └── argument (대상 디렉터리)
#  │  └── options (-l: 자세히, -a: 숨김파일 포함)
#  └── command (파일 목록 출력)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;옵션의 두 가지 형태&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 짧은 옵션 (하이픈 하나 + 알파벳 한 글자)
ls -l
ls -a
ls -la        # 짧은 옵션은 합칠 수 있다

# 긴 옵션 (하이픈 두 개 + 단어)
ls --long
ls --all&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도움말 확인하는 방법&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 1. --help 옵션 (간략한 도움말)
ls --help

# 2. man 명령어 (상세한 매뉴얼)
man ls
# q를 누르면 종료, /키워드로 검색, n으로 다음 검색 결과 이동

# 3. tldr (커뮤니티 기반 간결한 예시 &amp;mdash; 별도 설치 필요)
tldr ls

# 4. type 명령어 (명령어의 종류 확인)
type ls      # ls is aliased to 'ls --color=auto'
type cd      # cd is a shell builtin
type grep    # grep is /usr/bin/grep&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 02. 디렉터리 구조 &amp;amp; 탐색&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리눅스 디렉터리 구조 개요&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;루트 디렉터리 (/)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스의 모든 디렉터리는 루트 디렉터리(&lt;code&gt;/&lt;/code&gt;) 에서 시작한다. Windows가 &lt;code&gt;C:\&lt;/code&gt;, &lt;code&gt;D:\&lt;/code&gt; 등 드라이브 문자로 구분하는 것과 달리, 리눅스는 하나의 트리(tree) 구조로 모든 파일시스템을 통합한다. 외장 디스크를 연결하든, 네트워크 드라이브를 마운트하든, 모두 이 단일 트리 아래에 위치한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;/                     &amp;larr; 루트 디렉터리 (모든 것의 시작점)
├── bin/              &amp;larr; 기본 명령어
├── boot/             &amp;larr; 부팅에 필요한 파일
├── dev/              &amp;larr; 디바이스 파일
├── etc/              &amp;larr; 시스템 설정 파일
├── home/             &amp;larr; 일반 사용자 홈 디렉터리
├── lib/              &amp;larr; 공유 라이브러리
├── media/            &amp;larr; 이동식 미디어 마운트 포인트
├── mnt/              &amp;larr; 임시 마운트 포인트
├── opt/              &amp;larr; 서드파티 소프트웨어
├── proc/             &amp;larr; 프로세스 정보 (가상 파일시스템)
├── root/             &amp;larr; root 사용자의 홈 디렉터리
├── run/              &amp;larr; 런타임 데이터
├── sbin/             &amp;larr; 시스템 관리 명령어
├── srv/              &amp;larr; 서비스 데이터
├── sys/              &amp;larr; 커널/하드웨어 정보 (가상 파일시스템)
├── tmp/              &amp;larr; 임시 파일
├── usr/              &amp;larr; 사용자 프로그램 및 데이터
└── var/              &amp;larr; 가변 데이터 (로그, 캐시 등)&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 디렉터리 상세 설명&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/bin&lt;/code&gt; &amp;mdash; 필수 사용자 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 사용자가 사용할 수 있는 필수 명령어 바이너리(실행 파일)가 위치한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ls /bin/
# ls, cp, mv, rm, cat, echo, grep, mkdir, chmod, chown, ps, mount ...&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: 최신 배포판(Ubuntu 20.04+, Fedora 등)에서는 /bin이 /usr/bin의 심볼릭 링크인 경우가 많다. 이를 UsrMerge라고 하며, /bin, /sbin, /lib을 각각 /usr/bin, /usr/sbin, /usr/lib으로 통합하는 추세이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/sbin&lt;/code&gt; &amp;mdash; 시스템 관리 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 관리에 필요한 명령어가 위치한다. 주로 root 사용자가 사용하는 명령어들이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;ls /sbin/
# fdisk, mkfs, iptables, reboot, shutdown, ifconfig, route ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/bin&lt;/code&gt;과 &lt;code&gt;/sbin&lt;/code&gt;의 차이는 &lt;b&gt;대상 사용자&lt;/b&gt;이다. &lt;code&gt;/bin&lt;/code&gt;은 모든 사용자용, &lt;code&gt;/sbin&lt;/code&gt;은 관리자용이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/etc&lt;/code&gt; &amp;mdash; 시스템 설정 파일&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 전체에 적용되는 설정 파일(configuration files)이 위치한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 주요 설정 파일들
/etc/hostname          # 시스템 호스트명
/etc/hosts             # 호스트명-IP 매핑 (로컬 DNS 역할)
/etc/resolv.conf       # DNS 서버 설정
/etc/fstab             # 파일시스템 마운트 정보 (부팅 시 자동 마운트)
/etc/passwd            # 사용자 계정 정보
/etc/shadow            # 사용자 비밀번호 (해시)
/etc/group             # 그룹 정보
/etc/sudoers           # sudo 권한 설정
/etc/ssh/sshd_config   # SSH 서버 설정
/etc/nginx/            # Nginx 웹 서버 설정 (설치 시)
/etc/systemd/          # systemd 서비스 설정
/etc/crontab           # 시스템 크론 작업
/etc/environment       # 시스템 전역 환경변수
/etc/apt/              # APT 패키지 매니저 설정 (Debian 계열)
/etc/yum.repos.d/      # YUM/DNF 저장소 설정 (Red Hat 계열)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 설정을 변경할 때는 거의 항상 &lt;code&gt;/etc&lt;/code&gt; 아래의 파일을 편집하게 된다. 설정 파일을 수정하기 전에 반드시 백업하는 습관을 들여야 한다.&lt;/p&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;# 설정 파일 수정 전 백업
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%Y%m%d)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/home&lt;/code&gt; &amp;mdash; 일반 사용자 홈 디렉터리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 일반 사용자의 개인 공간이다. 사용자 &lt;code&gt;devops&lt;/code&gt;가 있다면 &lt;code&gt;/home/devops&lt;/code&gt;가 해당 사용자의 홈 디렉터리이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;/home/
├── devops/           # devops 사용자의 홈
│   ├── .bashrc       # bash 셸 설정 (숨김 파일)
│   ├── .ssh/         # SSH 키 저장소
│   ├── .profile      # 로그인 시 실행되는 스크립트
│   └── projects/     # 사용자가 만든 디렉터리
└── deploy/           # deploy 사용자의 홈&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;숨김 파일&lt;/b&gt;: 리눅스에서 파일명이 &lt;code&gt;.&lt;/code&gt;(점)으로 시작하면 숨김 파일이다. &lt;code&gt;ls&lt;/code&gt;로는 보이지 않고, &lt;code&gt;ls -a&lt;/code&gt;를 사용해야 보인다. 사용자 설정 파일들은 대부분 숨김 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;~&lt;/code&gt; (틸드)&lt;/b&gt;: 현재 사용자의 홈 디렉터리를 나타내는 축약어이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 다음 두 명령어는 동일하다 (현재 사용자가 devops인 경우)
cd /home/devops
cd ~

# 다른 사용자의 홈 디렉터리
cd ~deploy    # /home/deploy로 이동&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/var&lt;/code&gt; &amp;mdash; 가변 데이터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템이 운영되면서 &lt;b&gt;크기가 변하는(variable)&lt;/b&gt; 데이터가 저장된다. 로그, 캐시, 메일, 스풀 등이 여기에 위치한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;/var/
├── log/              # 시스템 및 애플리케이션 로그 ★★★
│   ├── syslog        # 시스템 로그 (Debian 계열)
│   ├── messages      # 시스템 로그 (Red Hat 계열)
│   ├── auth.log      # 인증 로그 (로그인, sudo 등)
│   ├── kern.log      # 커널 로그
│   ├── nginx/        # Nginx 로그
│   └── journal/      # systemd 저널 로그
├── cache/            # 애플리케이션 캐시
│   └── apt/          # APT 패키지 캐시
├── lib/              # 가변 상태 데이터
│   ├── docker/       # Docker 데이터 (이미지, 컨테이너, 볼륨)
│   └── mysql/        # MySQL 데이터
├── tmp/              # 재부팅 시에도 유지되는 임시 파일
├── spool/            # 처리 대기 중인 데이터 (메일, 프린트)
└── run/              # 실행 중인 프로세스의 런타임 데이터&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/var/log&lt;/code&gt;는 트러블슈팅의 시작점이다. 문제가 발생하면 가장 먼저 확인하는 곳이다. &lt;code&gt;/var&lt;/code&gt;가 위치한 파티션이 꽉 차면 시스템 전체에 문제가 생길 수 있으므로, 디스크 용량 모니터링에서 &lt;code&gt;/var&lt;/code&gt;는 특별히 주의해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/proc&lt;/code&gt; &amp;mdash; 프로세스 정보 (가상 파일시스템)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/proc&lt;/code&gt;는 실제 디스크에 존재하는 디렉터리가 아니다. 커널이 &lt;b&gt;메모리 상에서 동적으로 생성&lt;/b&gt;하는 가상 파일시스템이다. 현재 실행 중인 프로세스 정보와 시스템 상태를 파일 형태로 제공한다.&lt;/p&gt;
&lt;pre class=&quot;tcl&quot;&gt;&lt;code&gt;/proc/
├── 1/                # PID 1 (init/systemd) 프로세스 정보
│   ├── cmdline       # 프로세스 실행 명령어
│   ├── status        # 프로세스 상태 (메모리 사용량 등)
│   ├── fd/           # 열린 파일 디스크립터 목록
│   └── environ       # 프로세스 환경변수
├── cpuinfo           # CPU 정보
├── meminfo           # 메모리 정보
├── diskstats         # 디스크 I/O 통계
├── net/              # 네트워크 관련 정보
│   ├── tcp           # TCP 연결 정보
│   └── dev           # 네트워크 인터페이스 통계
├── loadavg           # 시스템 부하 평균
├── uptime            # 시스템 가동 시간
└── version           # 커널 버전 정보&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 활용 예시
cat /proc/cpuinfo       # CPU 정보 확인
cat /proc/meminfo       # 메모리 상세 정보
cat /proc/loadavg       # 시스템 부하 (load average)
cat /proc/version       # 커널 버전
cat /proc/1/cmdline     # PID 1 프로세스의 실행 명령어
ls -la /proc/self/fd    # 현재 프로세스의 열린 파일 목록&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 중요한가&lt;/b&gt;: 모니터링 도구(Prometheus node_exporter, Datadog agent 등)가 시스템 메트릭을 수집할 때 바로 이 &lt;code&gt;/proc&lt;/code&gt; 파일시스템에서 데이터를 읽는다. &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;htop&lt;/code&gt;, &lt;code&gt;free&lt;/code&gt; 같은 명령어도 내부적으로 &lt;code&gt;/proc&lt;/code&gt;를 읽어서 정보를 표시한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/sys&lt;/code&gt; &amp;mdash; 커널/하드웨어 정보 (가상 파일시스템)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/proc&lt;/code&gt;와 유사한 가상 파일시스템이지만, &lt;code&gt;/sys&lt;/code&gt;는 &lt;b&gt;커널 오브젝트와 하드웨어 장치&lt;/b&gt;에 대한 정보를 체계적으로 제공한다. &lt;code&gt;/proc&lt;/code&gt;가 프로세스 중심이라면, &lt;code&gt;/sys&lt;/code&gt;는 하드웨어/디바이스 중심이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 네트워크 인터페이스 정보
ls /sys/class/net/
# eth0  lo

# 블록 디바이스(디스크) 정보
ls /sys/block/
# sda  nvme0n1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/dev&lt;/code&gt; &amp;mdash; 디바이스 파일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드웨어 장치를 &lt;b&gt;파일로 표현&lt;/b&gt;한 것이다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;/dev/
├── sda               # 첫 번째 SCSI/SATA 디스크
├── sda1              # 첫 번째 디스크의 첫 번째 파티션
├── nvme0n1           # 첫 번째 NVMe SSD
├── nvme0n1p1         # NVMe SSD의 첫 번째 파티션
├── tty0              # 가상 콘솔
├── null              # 블랙홀 &amp;mdash; 쓴 데이터는 사라진다
├── zero              # 무한히 0(null byte)을 출력한다
├── random            # 랜덤 데이터를 생성한다
└── urandom           # 비차단 랜덤 데이터 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특수 장치 파일의 활용:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 출력을 버리고 싶을 때
command &amp;gt; /dev/null 2&amp;gt;&amp;amp;1

# 파일을 0으로 채워서 보안 삭제
dd if=/dev/zero of=file_to_wipe bs=1M count=100

# 랜덤 비밀번호 생성
cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/tmp&lt;/code&gt; &amp;mdash; 임시 파일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시 파일을 저장하는 디렉터리이다. &lt;b&gt;재부팅하면 삭제&lt;/b&gt;될 수 있다 (배포판에 따라 다름).&lt;br /&gt;모든 사용자가 읽고 쓸 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 임시 파일 생성
mktemp    # /tmp/tmp.XXXXXXXXXX 형식의 고유한 파일 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;보안 주의&lt;/b&gt;: &lt;code&gt;/tmp&lt;/code&gt;는 모든 사용자가 접근 가능하므로, 민감한 데이터를 저장하면 안 된다. &lt;b&gt;Sticky Bit&lt;/b&gt;가 설정되어 있어서 다른 사용자의 파일은 삭제할 수 없지만, 읽기는 가능할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;/usr&lt;/code&gt; &amp;mdash; 사용자 프로그램 및 데이터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Unix System Resources&quot;의 약자이다 (User가 아니다). 시스템에 설치된 프로그램, 라이브러리, 문서 등이 위치한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;/usr/
├── bin/              # 사용자 명령어 (대부분의 명령어가 여기에)
├── sbin/             # 시스템 관리 명령어
├── lib/              # 라이브러리
├── local/            # 로컬에서 직접 컴파일하여 설치한 소프트웨어
│   ├── bin/
│   ├── lib/
│   └── etc/
├── share/            # 아키텍처 독립적 데이터 (매뉴얼, 문서 등)
│   └── man/          # man 페이지
└── include/          # C/C++ 헤더 파일&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;경로 (Path)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;절대 경로 (Absolute Path)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;루트 디렉터리(&lt;code&gt;/&lt;/code&gt;)부터 시작&lt;/b&gt;하는 전체 경로이다. 어디서든 동일한 위치를 가리킨다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;/home/devops/projects/myapp/config.yaml
/var/log/nginx/access.log
/etc/ssh/sshd_config&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상대 경로 (Relative Path)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 위치(CWD, Current Working Directory)를 기준으로 하는 경로이다. &lt;code&gt;/&lt;/code&gt;로 시작하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;# 현재 /home/devops에 있다고 가정
cd projects/myapp      # /home/devops/projects/myapp로 이동
cat config.yaml        # /home/devops/projects/myapp/config.yaml

# 현재 위치에 따라 가리키는 곳이 달라진다&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특수 경로 표기&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표기&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 디렉터리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./script.sh&lt;/code&gt; (현재 디렉터리의 스크립트 실행)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;..&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;상위(부모) 디렉터리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cd ..&lt;/code&gt; (한 단계 위로 이동)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 사용자의 홈 디렉터리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cd ~&lt;/code&gt; &amp;rarr; &lt;code&gt;/home/username&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특정 사용자의 홈 디렉터리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cd ~deploy&lt;/code&gt; &amp;rarr; &lt;code&gt;/home/deploy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이전 디렉터리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cd -&lt;/code&gt; (직전에 있던 디렉터리로 이동)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디렉터리 탐색 명령어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;pwd&lt;/code&gt; &amp;mdash; 현재 위치 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Print Working Directory&lt;/b&gt;의 약자이다. 현재 작업 중인 디렉터리의 절대 경로를 출력한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;$ pwd
/home/devops/projects

# 심볼릭 링크를 따르지 않고 실제 물리 경로를 표시
$ pwd -P
/data/projects    # 실제 경로 (심볼릭 링크가 아닌)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팁&lt;/b&gt;: 셸 스크립트에서 현재 스크립트의 위치를 기준으로 작업할 때 자주 사용한다.&lt;/p&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;# 스크립트 파일이 위치한 디렉터리로 이동
SCRIPT_DIR=&quot;$(cd &quot;$(dirname &quot;$0&quot;)&quot; &amp;amp;&amp;amp; pwd)&quot;
cd &quot;$SCRIPT_DIR&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;cd&lt;/code&gt; &amp;mdash; 디렉터리 이동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Change Directory&lt;/b&gt;의 약자이다. 디렉터리를 이동하는 가장 기본적인 명령어이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 기본 사용법
cd /var/log              # 절대 경로로 이동
cd nginx                 # 상대 경로로 이동 (현재 위치 기준)
cd ..                    # 상위 디렉터리로 이동
cd ../..                 # 두 단계 상위로 이동
cd ~                     # 홈 디렉터리로 이동
cd                       # 인자 없이 &amp;mdash; 홈 디렉터리로 이동 (cd ~와 동일)
cd -                     # 이전 디렉터리로 이동 (토글)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;ls&lt;/code&gt; &amp;mdash; 디렉터리 내용 나열&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;List&lt;/b&gt;의 약자이다. 디렉터리의 내용물(파일, 하위 디렉터리)을 나열한다. 가장 자주 사용하는 명령어 중 하나이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;ls                       # 현재 디렉터리의 내용
ls /var/log              # 특정 디렉터리의 내용
ls file.txt              # 특정 파일 정보
ls *.log                 # 와일드카드 &amp;mdash; .log로 끝나는 파일만&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 옵션&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;ls -l                    # 긴 형식 (자세한 정보)
ls -a                    # 숨김 파일(.으로 시작) 포함
ls -la                   # -l과 -a 조합 (가장 많이 사용하는 조합)
ls -lh                   # 파일 크기를 사람이 읽기 쉬운 형태로 (K, M, G)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;ls -l&lt;/code&gt; 출력 상세 해석&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ ls -la /var/log/
total 12345
drwxrwxr-x  12 root   syslog  4096 Mar 23 10:30 .
drwxr-xr-x  14 root   root    4096 Jan 15 08:00 ..
-rw-r-----   1 syslog adm    98234 Mar 23 10:30 syslog
-rw-r-----   1 syslog adm   234567 Mar 22 23:59 syslog.1
drwxr-xr-x   2 root   root    4096 Mar 20 00:00 nginx
lrwxrwxrwx   1 root   root      39 Jan 15 08:00 mail.log -&amp;gt; /var/log/mail/current&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 필드의 의미를 분해하면:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;-rw-r-----   1   syslog   adm   98234   Mar 23 10:30   syslog
│├─┤├─┤├─┤   │     │       │      │         │             │
│ │  │  │    │     │       │      │         │             └─ 파일명
│ │  │  │    │     │       │      │         └─ 최종 수정 시간
│ │  │  │    │     │       │      └─ 파일 크기 (byte)
│ │  │  │    │     │       └─ 소유 그룹
│ │  │  │    │     └─ 소유자
│ │  │  │    └─ 하드 링크 수
│ │  │  └─ 기타(other) 사용자 권한 (---)
│ │  └─ 그룹(group) 권한 (r--)
│ └─ 소유자(owner) 권한 (rw-)
└─ 파일 타입&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파일 타입 문자:&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;문자&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;일반 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;디렉터리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;심볼릭 링크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;문자 디바이스 (터미널 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;블록 디바이스 (디스크 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;소켓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Named pipe (FIFO)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;tree&lt;/code&gt; &amp;mdash; 트리 구조로 표시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디렉터리 구조를 시각적인 트리 형태로 보여준다. 기본 설치가 아닌 경우가 많으므로 설치가 필요할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 설치 (없는 경우)
sudo apt install tree       # Debian/Ubuntu

# 기본 사용법
tree                        # 현재 디렉터리의 트리 구조
tree /etc/nginx             # 특정 디렉터리의 트리 구조
tree -L 2                   # 깊이 2단계까지만 표시 ★
tree -d                     # 디렉터리만 표시
tree -a                     # 숨김 파일 포함
tree -h                     # 파일 크기 표시 (사람이 읽기 쉬운 형태)
tree -P &quot;*.conf&quot;            # 특정 패턴 파일만 표시
tree -I &quot;node_modules|.git&quot; # 특정 디렉터리 제외
tree --dirsfirst            # 디렉터리를 먼저 표시&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;find&lt;/code&gt; &amp;mdash; 파일/디렉터리 검색&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일시스템에서 파일이나 디렉터리를 &lt;b&gt;다양한 조건&lt;/b&gt;으로 검색하는 강력한 명령어이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;find [검색 시작 경로] [검색 조건] [동작]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이름으로 검색&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 파일명으로 검색 (대소문자 구분)
find /etc -name &quot;nginx.conf&quot;

# 파일명으로 검색 (대소문자 무시)
find /etc -iname &quot;*.CONF&quot;

# 와일드카드 사용
find /var/log -name &quot;*.log&quot;
find / -name &quot;docker-compose*.yml&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입으로 검색&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 파일만 검색
find /home -type f -name &quot;*.sh&quot;

# 디렉터리만 검색
find /var -type d -name &quot;log&quot;

# 심볼릭 링크만 검색
find /etc -type l&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;크기로 검색&lt;/h3&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;# 100MB 이상인 파일 검색
find / -type f -size +100M

# 1KB 미만인 파일 검색
find /tmp -type f -size -1k

# 정확히 0byte (빈 파일)
find /var/log -type f -empty

# 빈 디렉터리
find /home -type d -empty&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시간으로 검색&lt;/h3&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;# 최근 7일 이내에 수정된 파일
find /var/log -type f -mtime -7

# 30일 이상 된(수정되지 않은) 파일
find /tmp -type f -mtime +30

# 최근 60분 이내에 수정된 파일
find /var/log -type f -mmin -60

# 최근 1일 이내에 접근된 파일
find /home -type f -atime -1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시간 옵션:&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;단위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-mtime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;내용이 수정(modify)된 시간&lt;/td&gt;
&lt;td&gt;일(day)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-atime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;접근(access)된 시간&lt;/td&gt;
&lt;td&gt;일(day)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-ctime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;메타데이터가 변경(change)된 시간&lt;/td&gt;
&lt;td&gt;일(day)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-mmin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;내용이 수정된 시간&lt;/td&gt;
&lt;td&gt;분(minute)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-amin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;접근된 시간&lt;/td&gt;
&lt;td&gt;분(minute)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-cmin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;메타데이터가 변경된 시간&lt;/td&gt;
&lt;td&gt;분(minute)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한/소유자로 검색&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# 권한이 777인 파일 (보안 위험 &amp;mdash; 누구나 읽기/쓰기/실행 가능)
find / -type f -perm 777

# 소유자가 없는 파일 (사용자가 삭제된 경우)
find / -type f -nouser

# 특정 사용자 소유 파일
find /home -type f -user devops

# 특정 그룹 소유 파일
find /var -type f -group www-data&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색 결과에 동작 수행 (&lt;code&gt;exec&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;find&lt;/code&gt;의 가장 강력한 기능은 검색 결과에 대해 &lt;b&gt;명령어를 실행&lt;/b&gt;할 수 있다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 검색된 파일의 상세 정보 출력
find /var/log -name &quot;*.log&quot; -exec ls -lh {} \;

# 30일 이상 된 로그 파일 삭제
find /var/log -name &quot;*.log&quot; -mtime +30 -exec rm -f {} \;

# 검색된 파일의 권한을 644로 변경
find /var/www -type f -exec chmod 644 {} \;

# 검색된 디렉터리의 권한을 755로 변경
find /var/www -type d -exec chmod 755 {} \;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;exec&lt;/code&gt; 문법 설명:&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{}&lt;/code&gt; : 검색된 각 파일/디렉터리의 경로가 들어가는 자리이다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\;&lt;/code&gt; : &lt;code&gt;exec&lt;/code&gt; 명령어의 끝을 표시한다 (세미콜론을 이스케이프)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+&lt;/code&gt; : &lt;code&gt;\;&lt;/code&gt; 대신 사용하면 검색 결과를 한 번에 전달하여 성능이 향상된다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# \; &amp;rarr; 파일마다 명령어를 한 번씩 실행 (느림)
find /var/log -name &quot;*.log&quot; -exec ls -l {} \;

# + &amp;rarr; 파일을 모아서 명령어를 한 번에 실행 (빠름)
find /var/log -name &quot;*.log&quot; -exec ls -l {} +&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건 조합&lt;/h3&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;# AND (기본 동작 &amp;mdash; 조건을 나열하면 AND)
find /var/log -name &quot;*.log&quot; -size +10M

# OR (-o 옵션)
find /etc -name &quot;*.conf&quot; -o -name &quot;*.cfg&quot;

# NOT (! 또는 -not)
find /home -type f ! -name &quot;*.tmp&quot;

# 복합 조건: /var/log에서 7일 이상 되고 100MB 이상인 .log 파일
find /var/log -type f -name &quot;*.log&quot; -mtime +7 -size +100M&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 시나리오&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 1. 디스크 정리 &amp;mdash; 큰 파일 찾기
find / -type f -size +500M 2&amp;gt;/dev/null | head -20

# 2. 보안 점검 &amp;mdash; 권한이 너무 열려있는 파일 찾기
find / -type f -perm -o+w 2&amp;gt;/dev/null

# 3. 최근 변경된 설정 파일 확인 (트러블슈팅)
find /etc -type f -mmin -30

# 4. 특정 확장자 파일 일괄 삭제
find /tmp -type f -name &quot;*.tmp&quot; -mtime +7 -delete

# 5. SUID 비트 설정된 파일 찾기 (보안 감사)
find / -type f -perm -4000 2&amp;gt;/dev/null&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;which&lt;/code&gt;, &lt;code&gt;whereis&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt; &amp;mdash; 명령어 위치 찾기&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# which: 실행 파일의 경로 (PATH에서 검색)
$ which python3
/usr/bin/python3

$ which nginx
/usr/sbin/nginx

# whereis: 바이너리, 소스, man 페이지 위치
$ whereis nginx
nginx: /usr/sbin/nginx /etc/nginx /usr/share/nginx /usr/share/man/man8/nginx.8.gz

# type: 명령어의 종류 (alias, builtin, file 등)
$ type cd
cd is a shell builtin

$ type ls
ls is aliased to 'ls --color=auto'

$ type python3
python3 is /usr/bin/python3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;which&lt;/code&gt;와 &lt;code&gt;type&lt;/code&gt;의 차이:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;which&lt;/code&gt;는 PATH 환경변수에서 실행 파일의 경로만 찾는다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;은 셸 빌트인(built-in) 명령어, 앨리어스(alias), 함수까지 구분하여 알려준다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일시스템과 마운트 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일시스템 (Filesystem)이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일시스템은 &lt;b&gt;디스크에 데이터를 저장하고 조직하는 방식&lt;/b&gt;이다.&lt;br /&gt;도서관에서 책을 분류하고 찾는 시스템에 비유할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마운트 (Mount)란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마운트는 &lt;b&gt;파일시스템을 디렉터리 트리의 특정 위치에 연결하는 것&lt;/b&gt;이다. 리눅스에서는 디스크를 연결하면 자동으로 접근할 수 있는 것이 아니라, 반드시 특정 디렉터리에 &lt;b&gt;마운트&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;물리 디스크 /dev/sdb1  ──mount──&amp;rarr;  /data  (디렉터리 트리에 연결)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마운트 이후에는 &lt;code&gt;/data&lt;/code&gt; 경로를 통해 해당 디스크에 접근할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;# 현재 마운트된 파일시스템 확인
mount | column -t
df -hT

# 수동 마운트
sudo mount /dev/sdb1 /mnt/data

# 언마운트
sudo umount /mnt/data&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;inode와 파일시스템 내부 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;inode란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;inode(Index Node)는 파일시스템에서 파일의 메타데이터(metadata)를 저장하는 데이터 구조이다. 모든 파일과 디렉터리는 하나의 inode를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;inode에 저장되는 정보:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 타입 (일반 파일, 디렉터리, 심볼릭 링크 등)&lt;/li&gt;
&lt;li&gt;권한 (permissions)&lt;/li&gt;
&lt;li&gt;소유자 (UID)와 그룹 (GID)&lt;/li&gt;
&lt;li&gt;파일 크기&lt;/li&gt;
&lt;li&gt;타임스탬프 (생성, 수정, 접근 시간)&lt;/li&gt;
&lt;li&gt;하드 링크 수&lt;/li&gt;
&lt;li&gt;데이터 블록의 위치 (포인터)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중요&lt;/b&gt;: inode에는 &lt;b&gt;파일 이름이 저장되지 않는다&lt;/b&gt;. 파일 이름은 디렉터리 엔트리에 저장된다. 디렉터리란 본질적으로 &quot;파일명 &amp;harr; inode 번호&quot;의 매핑 테이블이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 파일의 inode 번호 확인
ls -i /etc/hostname
# 131074 /etc/hostname

# 상세 inode 정보 확인
stat /etc/hostname
#   File: /etc/hostname
#   Size: 12          Blocks: 8          IO Block: 4096   regular file
# Device: 801h/2049d  Inode: 131074      Links: 1
# Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
# Access: 2025-03-23 10:00:00.000000000 +0900
# Modify: 2025-01-15 08:00:00.000000000 +0900
# Change: 2025-01-15 08:00:00.000000000 +0900&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 03. 파일 &amp;amp; 디렉터리 관리&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일과 디렉터리의 기본 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서 파일은 &lt;b&gt;바이트(byte)의 연속된 스트림&lt;/b&gt;이다. Windows와 달리 리눅스는 파일 확장자에 의존하지 않는다. &lt;code&gt;.txt&lt;/code&gt;, &lt;code&gt;.log&lt;/code&gt;, &lt;code&gt;.conf&lt;/code&gt; 같은 확장자는 사람이 파일의 용도를 구분하기 위한 관례(convention)일 뿐이며, 시스템이 파일의 종류를 결정하는 기준은 아니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일명 규칙&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 파일명은 Windows보다 자유도가 높지만, 알아두어야 할 규칙이 있다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대소문자를 구분한다&lt;/b&gt;: &lt;code&gt;File.txt&lt;/code&gt;와 &lt;code&gt;file.txt&lt;/code&gt;는 다른 파일이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최대 255자&lt;/b&gt;까지 가능하다 (ext4 기준)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;거의 모든 문자를 사용할 수 있다&lt;/b&gt;: 사용할 수 없는 문자는 &lt;code&gt;/&lt;/code&gt;(경로 구분자)와 &lt;code&gt;NULL&lt;/code&gt;(문자열 종결자)뿐이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;.&lt;/code&gt;으로 시작하면 숨김 파일&lt;/b&gt;이다: &lt;code&gt;.bashrc&lt;/code&gt;, &lt;code&gt;.ssh/&lt;/code&gt;, &lt;code&gt;.gitignore&lt;/code&gt; 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공백과 특수문자는 피하는 것이 좋다&lt;/b&gt;: 셸 스크립트에서 문제를 일으킬 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 공백이 포함된 파일명 다루기 (반드시 따옴표 사용)
ls &quot;my file.txt&quot;
rm &quot;my file.txt&quot;

# 하이픈(-)으로 시작하는 파일명 다루기 (옵션으로 인식되는 문제)
rm -- -filename       # -- 이후는 옵션이 아님을 표시
rm ./-filename        # 상대 경로로 지정&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임스탬프 (Timestamps)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스의 모든 파일은 세 가지 타임스탬프를 가진다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;타임스탬프&lt;/th&gt;
&lt;th&gt;약자&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;변경되는 시점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Modify time (mtime)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;m&lt;/td&gt;
&lt;td&gt;파일 &lt;b&gt;내용&lt;/b&gt;이 마지막으로 변경된 시간&lt;/td&gt;
&lt;td&gt;파일에 데이터를 쓸 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Access time (atime)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;파일이 마지막으로 &lt;b&gt;읽힌&lt;/b&gt; 시간&lt;/td&gt;
&lt;td&gt;파일을 읽을 때 (cat, less 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Change time (ctime)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;파일의 &lt;b&gt;메타데이터&lt;/b&gt;가 마지막으로 변경된 시간&lt;/td&gt;
&lt;td&gt;권한, 소유자, 이름 변경 시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# stat 명령어로 세 가지 타임스탬프 확인
$ stat config.yaml
  File: config.yaml
  Size: 1234        Blocks: 8          IO Block: 4096   regular file
Access: 2025-03-23 14:30:00.000000000 +0900
Modify: 2025-03-20 09:15:00.000000000 +0900
Change: 2025-03-20 09:15:00.000000000 +0900
 Birth: 2025-03-01 10:00:00.000000000 +0900&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;: 생성 시간(Birth/Creation time)은 전통적인 UNIX에는 없었으나, ext4와 최신 커널에서는 지원한다. &lt;code&gt;stat&lt;/code&gt; 명령어의 &lt;code&gt;Birth&lt;/code&gt; 필드에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;noatime 마운트 옵션&lt;/b&gt;: 서버 환경에서는 &lt;code&gt;/etc/fstab&lt;/code&gt;에 &lt;code&gt;noatime&lt;/code&gt; 옵션을 설정하여 atime 업데이트를 비활성화하는 경우가 많다. 파일을 읽을 때마다 디스크에 쓰기(atime 업데이트)가 발생하면 성능 저하의 원인이 되기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디렉터리 생성 &amp;mdash; &lt;code&gt;mkdir&lt;/code&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Make Directory&lt;/b&gt;의 약자이다. 새 디렉터리를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 단일 디렉터리 생성
mkdir projects

# 여러 디렉터리 한 번에 생성
mkdir dir1 dir2 dir3

# 중첩 디렉터리 한 번에 생성 (-p 옵션) ★★★
mkdir -p projects/backend/src/main
# -p 없이 하면 상위 디렉터리가 없으면 에러 발생
# mkdir: cannot create directory 'projects/backend/src/main': No such file or directory&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Brace Expansion을 활용한 구조 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셸의 &lt;b&gt;Brace Expansion(&lt;code&gt;{}&lt;/code&gt;)&lt;/b&gt; 기능을 활용하면 복잡한 디렉터리 구조를 한 줄로 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# 프로젝트 기본 구조 한 번에 생성
mkdir -p myapp/{src,test,docs,config,scripts}

# 더 복잡한 구조
mkdir -p myapp/{src/{main,lib},test/{unit,integration},deploy/{dev,staging,prod}}

# 결과 확인
$ tree myapp/
myapp/
├── deploy
│   ├── dev
│   ├── prod
│   └── staging
├── src
│   ├── lib
│   └── main
└── test
    ├── integration
    └── unit&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;touch&lt;/code&gt; &amp;mdash; 빈 파일 생성 / 타임스탬프 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;touch&lt;/code&gt;의 원래 목적은 파일의 &lt;b&gt;타임스탬프를 변경&lt;/b&gt;하는 것이다. 하지만 대상 파일이 존재하지 않으면 &lt;b&gt;빈 파일을 생성&lt;/b&gt;하기 때문에, 빈 파일 생성 용도로도 자주 사용한다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;# 빈 파일 생성
touch newfile.txt

# 여러 파일 한 번에 생성
touch file1.txt file2.txt file3.txt

# Brace Expansion 활용
touch log-{01..12}.txt
# log-01.txt log-02.txt ... log-12.txt 생성&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리다이렉션으로 파일 생성&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 빈 파일 생성 (touch과 동일한 효과)
&amp;gt; newfile.txt

# 내용이 있는 파일 생성
echo &quot;Hello, World!&quot; &amp;gt; hello.txt

# 여러 줄 파일 생성 (Here Document)
cat &amp;lt;&amp;lt; EOF &amp;gt; config.yaml
server:
  host: 0.0.0.0
  port: 8080
database:
  host: localhost
  port: 5432
EOF&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Here Document (&lt;code&gt;&amp;lt;&amp;lt; EOF&lt;/code&gt;)&lt;/b&gt;: &lt;code&gt;EOF&lt;/code&gt;(End Of File)는 구분자이며, 임의의 문자열을 사용할 수 있다. 여기서부터 같은 구분자가 나올 때까지의 내용이 입력으로 전달된다. 설정 파일을 스크립트로 생성할 때 매우 유용하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;복사 &amp;mdash; &lt;code&gt;cp&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Copy&lt;/b&gt;의 약자이다. 파일이나 디렉터리를 복사한다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;# 파일 복사
cp source.txt destination.txt

# 파일을 다른 디렉터리로 복사
cp config.yaml /tmp/

# 파일을 다른 디렉터리로 복사하면서 이름 변경
cp config.yaml /tmp/config.yaml.bak&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활용 패턴&lt;/h3&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 1. 설정 파일 백업 (날짜 포함) ★★★
cp -p /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%Y%m%d_%H%M%S)

# 2. 디렉터리 전체 백업 (메타데이터 보존)
cp -a /var/www/html/ /backup/www-$(date +%Y%m%d)/

# 3. 여러 파일을 디렉터리로 복사
cp file1.txt file2.txt file3.txt /destination/

# 4. 와일드카드로 특정 파일만 복사
cp /var/log/*.log /backup/logs/

# 5. 빈 디렉터리 구조만 복사 (find + mkdir 조합)
cd /source &amp;amp;&amp;amp; find . -type d -exec mkdir -p /dest/{} \;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;cp&lt;/code&gt;의 동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cp&lt;/code&gt;는 원본 파일의 데이터를 읽어서 새로운 inode를 가진 새 파일을 생성한다. 즉, &lt;b&gt;원본과 복사본은 완전히 독립적인 파일&lt;/b&gt;이다. 한쪽을 수정해도 다른 쪽에 영향이 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이동 / 이름 변경 &amp;mdash; &lt;code&gt;mv&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Move&lt;/b&gt;의 약자이다. 파일이나 디렉터리를 이동하거나 이름을 변경한다. 리눅스에는 별도의 &quot;rename&quot; 명령어가 기본 제공되지 않으며, &lt;code&gt;mv&lt;/code&gt;가 이름 변경 기능을 겸한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 파일 이름 변경
mv old_name.txt new_name.txt

# 파일을 다른 디렉터리로 이동
mv report.pdf /home/devops/documents/

# 이동하면서 이름 변경
mv config.yaml /etc/myapp/application.yaml

# 디렉터리 이름 변경 (디렉터리도 동일하게 동작)
mv old_dir/ new_dir/

# 디렉터리를 다른 위치로 이동
mv projects/ /opt/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삭제 &amp;mdash; &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;rmdir&lt;/code&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;rm&lt;/code&gt; &amp;mdash; 파일/디렉터리 삭제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Remove&lt;/b&gt;의 약자이다. 파일이나 디렉터리를 삭제한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 경고: 리눅스에는 휴지통(Recycle Bin)이 없다. rm으로 삭제한 파일은 복구가 매우 어렵다. 특히 rm -rf는 확인 없이 재귀적으로 모든 것을 삭제하므로 극도로 주의해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 파일 삭제
rm file.txt

# 여러 파일 삭제
rm file1.txt file2.txt file3.txt

# 와일드카드로 삭제
rm *.tmp
rm *.log&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;rmdir&lt;/code&gt; &amp;mdash; 빈 디렉터리만 삭제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rmdir&lt;/code&gt;은 &lt;b&gt;비어있는 디렉터리만&lt;/b&gt; 삭제할 수 있다. 내용물이 있으면 에러가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 빈 디렉터리 삭제
rmdir empty_dir/

# 에러 발생 &amp;mdash; 디렉터리가 비어있지 않음
rmdir non_empty_dir/
# rmdir: failed to remove 'non_empty_dir/': Directory not empty

# -p : 빈 상위 디렉터리까지 함께 삭제
rmdir -p a/b/c/     # c, b, a 순서로 삭제 (모두 비어있어야 함)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안전한 삭제 습관&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 1. 삭제 전에 항상 먼저 ls로 확인
ls /var/log/*.log.1          # 삭제 대상 확인
rm /var/log/*.log.1          # 확인 후 삭제

# 2. rm -rf를 사용할 때는 경로를 절대 변수에 넣지 말 것
# 위험한 패턴 &amp;mdash; 변수가 비어있으면 / 를 삭제하게 된다
rm -rf ${DIR}/               # DIR이 빈 문자열이면 rm -rf / 실행 ☠️

# 안전한 패턴
rm -rf &quot;${DIR:?Variable is not set}/&quot;   # 변수가 비어있으면 에러 발생

# 3. 삭제 대신 이동으로 대체 (안전한 삭제 패턴)
mv old_files/ /tmp/trash/    # 나중에 확인 후 /tmp는 재부팅 시 자동 삭제

# 4. interactive 옵션 활용
alias rm='rm -i'             # 별칭 설정 (모든 rm에 확인 절차 추가)

# 5. 중요한 작업 전에 pwd 확인
pwd                          # 현재 위치가 맞는지 확인
rm -rf build/                # 그 후에 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;wc&lt;/code&gt; &amp;mdash; 파일 통계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Word Count&lt;/b&gt;의 약자이다. 파일의 줄 수, 단어 수, 바이트 수를 출력한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;$ wc /etc/passwd
  42   68  2156  /etc/passwd
#  │    │    │
#  │    │    └─ 바이트 수
#  │    └─ 단어 수
#  └─ 줄 수

# 줄 수만 출력 (-l: lines) ★ 가장 자주 사용
wc -l /var/log/syslog
# 15234 /var/log/syslog

# 단어 수만 출력 (-w: words)
wc -w document.txt

# 바이트 수만 출력 (-c: bytes)
wc -c data.bin

# 문자 수만 출력 (-m: characters, 멀티바이트 문자 지원)
wc -m korean_text.txt

# 여러 파일의 줄 수 확인
wc -l /var/log/*.log
#   1234 /var/log/auth.log
#   5678 /var/log/syslog
#    234 /var/log/kern.log
#   7146 total

# 파이프와 조합 &amp;mdash; 프로세스 수 세기
ps aux | wc -l&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;와일드카드 (Globbing)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셸에서 파일명 패턴을 매칭하는 특수 문자를 &lt;b&gt;와일드카드(wildcard)&lt;/b&gt; 또는 글로빙(globbing)이라고 한다. &lt;code&gt;find&lt;/code&gt;의 정규표현식과는 다른 개념이다. 와일드카드는 셸이 명령어를 실행하기 전에 파일명으로 확장(expand)한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 와일드카드&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;패턴&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0개 이상의 임의의 문자&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*.log&lt;/code&gt; &amp;rarr; error.log, access.log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;정확히 1개의 임의의 문자&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file?.txt&lt;/code&gt; &amp;rarr; file1.txt, fileA.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[abc]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;괄호 안의 문자 중 하나&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file[123].txt&lt;/code&gt; &amp;rarr; file1.txt, file2.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[a-z]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;범위 내의 문자 중 하나&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file[a-c].txt&lt;/code&gt; &amp;rarr; filea.txt, fileb.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[!abc]&lt;/code&gt; 또는 &lt;code&gt;[^abc]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;괄호 안의 문자가 아닌 것&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file[!0-9].txt&lt;/code&gt; &amp;rarr; filea.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확장 글로빙 (bash)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bash에서는 &lt;code&gt;extglob&lt;/code&gt; 옵션으로 더 강력한 패턴 매칭을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# extglob 활성화 (보통 기본 활성화)
shopt -s extglob

# ?(pattern)  : 패턴이 0번 또는 1번
# *(pattern)  : 패턴이 0번 이상
# +(pattern)  : 패턴이 1번 이상
# @(pattern)  : 패턴이 정확히 1번
# !(pattern)  : 패턴에 매치되지 않는 것

# .log와 .tmp를 제외한 모든 파일
ls !(*.log|*.tmp)

# .conf 또는 .yaml 파일만
ls *.@(conf|yaml)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Brace Expansion (&lt;code&gt;{}&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와일드카드와는 다른 메커니즘이지만, 파일 관리에서 매우 유용하다. 글로빙은 &lt;b&gt;존재하는 파일&lt;/b&gt;을 매칭하지만, Brace Expansion은 &lt;b&gt;문자열을 생성&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 문자열 목록 생성
echo {apple,banana,cherry}
# apple banana cherry

# 범위 생성
echo {1..10}
# 1 2 3 4 5 6 7 8 9 10

echo {a..z}
# a b c d e f g h i j k l m n o p q r s t u v w x y z

echo {01..12}
# 01 02 03 04 05 06 07 08 09 10 11 12

# 증가값 지정
echo {0..100..10}
# 0 10 20 30 40 50 60 70 80 90 100

# 실무 활용
# 1. 파일 백업
cp config.yaml{,.bak}           # config.yaml &amp;rarr; config.yaml.bak으로 복사
# 확장: cp config.yaml config.yaml.bak

# 2. 여러 디렉터리 생성
mkdir -p project/{src,test,docs,build}

# 3. 확장자 변경 (rename 효과)
mv report.{txt,md}              # report.txt &amp;rarr; report.md
# 확장: mv report.txt report.md&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;복수 파일/디렉터리 일괄 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;xargs&lt;/code&gt; &amp;mdash; 표준 입력을 명령어 인자로 변환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;xargs&lt;/code&gt;는 파이프를 통해 전달된 표준 입력(stdin)을 명령어의 인자(argument)로 변환한다. &lt;code&gt;find&lt;/code&gt;와 함께 사용하면 매우 강력하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# find 결과를 xargs로 전달
find /tmp -name &quot;*.tmp&quot; -mtime +7 | xargs rm -f

# 파일명에 공백이 있을 때 안전하게 처리 ★★★
find /tmp -name &quot;*.tmp&quot; -print0 | xargs -0 rm -f
# -print0: null 문자로 구분
# -0: null 문자를 구분자로 인식

# 한 번에 처리할 인자 수 제한
find . -name &quot;*.log&quot; | xargs -n 5 ls -l
# 5개씩 묶어서 ls -l 실행

# 자리 표시자(placeholder) 사용 (-I)
find . -name &quot;*.conf&quot; | xargs -I {} cp {} /backup/{}.bak

# 병렬 처리 (-P: 동시 실행할 프로세스 수)
find . -name &quot;*.png&quot; | xargs -P 4 -I {} convert {} -resize 50% resized_{}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;find&lt;/code&gt; + &lt;code&gt;exec&lt;/code&gt; vs &lt;code&gt;xargs&lt;/code&gt; 비교&lt;/h3&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;# find -exec: 파일 하나씩 명령어 실행
find . -name &quot;*.log&quot; -exec gzip {} \;

# find -exec +: 가능한 많은 파일을 한 번에 전달
find . -name &quot;*.log&quot; -exec gzip {} +

# xargs: find의 출력을 파이프로 받아서 전달
find . -name &quot;*.log&quot; | xargs gzip&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 &lt;code&gt;xargs&lt;/code&gt;가 &lt;code&gt;-exec \;&lt;/code&gt;보다 빠르다. &lt;code&gt;-exec +&lt;/code&gt;와 &lt;code&gt;xargs&lt;/code&gt;는 성능이 비슷하다. 파일명에 특수문자(공백, 개행)가 있을 수 있다면 &lt;code&gt;find -print0 | xargs -0&lt;/code&gt; 조합을 사용하는 것이 가장 안전하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 원칙 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;삭제 전에 반드시 &lt;code&gt;ls&lt;/code&gt;로 확인&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 파일 수정 전에 반드시 백업&lt;/b&gt;한다 (&lt;code&gt;cp -p file file.bak.$(date +%Y%m%d)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;rm -rf&lt;/code&gt;에 변수를 사용할 때는 변수가 비어있지 않은지 검증&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일명에 공백이 있을 수 있으면 &lt;code&gt;find -print0 | xargs -0&lt;/code&gt; 조합&lt;/b&gt;을 사용한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;같은 파티션의 &lt;code&gt;mv&lt;/code&gt;는 즉시, 다른 파티션의 &lt;code&gt;mv&lt;/code&gt;는 시간이 걸린다&lt;/b&gt;는 점을 기억한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;rm&lt;/code&gt;은 unlink이다&lt;/b&gt; &amp;mdash; 프로세스가 파일을 잡고 있으면 공간이 해제되지 않는다&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 04. 파일 내용 확인 &amp;amp; 검색&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 내용을 확인하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서 파일 내용을 확인하는 명령어는 여러 가지가 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;적합한 명령어&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;짧은 파일 전체 보기&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cat&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;긴 파일을 페이지 단위로 읽기&lt;/td&gt;
&lt;td&gt;&lt;code&gt;less&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일의 앞/뒤 일부만 보기&lt;/td&gt;
&lt;td&gt;&lt;code&gt;head&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실시간 로그 모니터링&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tail -f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 간 차이 빠르게 비교&lt;/td&gt;
&lt;td&gt;&lt;code&gt;diff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;cat&lt;/code&gt; &amp;mdash; 파일 전체 출력&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Concatenate&lt;/b&gt;의 약자이다. 원래 목적은 여러 파일을 연결(concatenate)하여 출력하는 것이지만, 단일 파일의 내용을 확인하는 용도로 가장 흔히 사용한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 파일 내용 출력
cat /etc/hostname

# 줄 번호와 함께 출력
cat -n /etc/passwd

# 빈 줄을 제외하고 줄 번호 표시
cat -b /etc/ssh/sshd_config

# 여러 파일 연결하여 출력
cat header.txt body.txt footer.txt

# 여러 파일을 하나로 합치기
cat part1.log part2.log part3.log &amp;gt; combined.log&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제 쓰고, 언제 쓰지 말아야 하는가:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수십 줄 이하의 짧은 파일 &amp;rarr; &lt;code&gt;cat&lt;/code&gt; 적합&lt;/li&gt;
&lt;li&gt;수백~수천 줄 이상의 파일 &amp;rarr; &lt;code&gt;cat&lt;/code&gt;은 부적합 (터미널이 스크롤로 넘쳐버린다). &lt;code&gt;less&lt;/code&gt;를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;tac&lt;/code&gt; &amp;mdash; 역순 출력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cat&lt;/code&gt;의 철자를 뒤집은 이름 그대로, 파일의 &lt;b&gt;마지막 줄부터 거꾸로&lt;/b&gt; 출력한다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# 최신 로그가 아래에 쌓이는 파일을 역순으로 보기
tac /var/log/auth.log | head -20&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;less&lt;/code&gt; &amp;mdash; 페이지 단위 탐색&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;less&lt;/code&gt;는 파일 내용을 &lt;b&gt;페이지 단위로 스크롤&lt;/b&gt;하며 읽을 수 있는 뷰어(pager)이다. &lt;code&gt;more&lt;/code&gt;라는 오래된 명령어의 개선판이며, 이름은 &quot;less is more&quot;라는 말장난에서 유래했다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;less /var/log/syslog
less +G /var/log/syslog    # 파일 끝부터 열기&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;less&lt;/code&gt; 내부 조작 키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;less&lt;/code&gt;는 열린 상태에서 다양한 키로 탐색할 수 있다. 외울 필요 없이, 자주 쓰는 것만 알면 된다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;키&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Space&lt;/code&gt; / &lt;code&gt;f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;한 페이지 아래로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;한 페이지 위로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;j&lt;/code&gt; / &lt;code&gt;k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;한 줄 아래 / 위로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 끝으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 처음으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/키워드&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;아래 방향으로 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?키워드&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;위 방향으로 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;다음 검색 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이전 검색 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;패턴&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;패턴이 포함된 줄만 표시 (필터)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 팁&lt;/b&gt;: &lt;code&gt;less&lt;/code&gt;의 &lt;code&gt;/&lt;/code&gt; 검색은 로그 파일에서 특정 에러 메시지를 찾을 때 매우 유용하다. &lt;code&gt;&amp;amp;ERROR&lt;/code&gt;를 입력하면 &quot;ERROR&quot;가 포함된 줄만 필터링하여 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;more&lt;/code&gt;와의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;more&lt;/code&gt;는 앞으로만 스크롤할 수 있고, &lt;code&gt;less&lt;/code&gt;는 앞뒤로 자유롭게 이동할 수 있다. 사실상 &lt;code&gt;less&lt;/code&gt;만 사용한다고 보면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;head&lt;/code&gt;와 &lt;code&gt;tail&lt;/code&gt; &amp;mdash; 파일의 앞/뒤 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;head&lt;/code&gt; &amp;mdash; 파일 앞부분&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 기본: 처음 10줄 출력
head /etc/passwd

# 줄 수 지정
head -n 20 /var/log/syslog
head -20 /var/log/syslog       # 동일 (축약형)

# 바이트 수 지정
head -c 100 binary_file        # 처음 100바이트&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;tail&lt;/code&gt; &amp;mdash; 파일 뒷부분&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 기본: 마지막 10줄 출력
tail /var/log/syslog

# 줄 수 지정
tail -n 50 /var/log/syslog
tail -50 /var/log/syslog       # 동일

# 특정 줄부터 끝까지 (+N: N번째 줄부터)
tail -n +100 /etc/passwd       # 100번째 줄부터 끝까지&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;tail -f&lt;/code&gt; &amp;mdash; 실시간 로그 모니터링 ★★★&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tail -f&lt;/code&gt;는 &lt;b&gt;가장 자주 사용하는 명령어 조합&lt;/b&gt; 중 하나이다. 파일에 새로운 내용이 추가되면 &lt;b&gt;실시간으로 화면에 표시&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 실시간 로그 모니터링
tail -f /var/log/syslog

# 여러 파일 동시 모니터링
tail -f /var/log/nginx/access.log /var/log/nginx/error.log

# 특정 키워드만 필터링하며 모니터링
tail -f /var/log/syslog | grep --line-buffered &quot;error&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;f&lt;/code&gt; vs &lt;code&gt;F&lt;/code&gt;의 차이:&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 디스크립터를 추적한다. 파일이 삭제되고 재생성되면 추적이 끊긴다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 이름을 추적한다. 로그 로테이션으로 파일이 교체되어도 계속 추적한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 로그 로테이션(logrotate)이 적용된 환경이 대부분이므로, &lt;code&gt;tail -F&lt;/code&gt;를 사용하는 것이 더 안전하다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# 로그 로테이션 환경에서 안전한 모니터링
tail -F /var/log/nginx/access.log&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 내용 가공 출력&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;cut&lt;/code&gt; &amp;mdash; 필드/컬럼 추출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cut&lt;/code&gt;은 텍스트에서 &lt;b&gt;특정 필드(열)만 잘라내는&lt;/b&gt; 명령어이다. CSV나 구분자 기반 파일을 다룰 때 유용하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# /etc/passwd에서 사용자명(1번째 필드)만 추출
# /etc/passwd는 : 으로 필드가 구분된다
cut -d ':' -f 1 /etc/passwd

# 1번, 3번 필드 (사용자명, UID)
cut -d ':' -f 1,3 /etc/passwd

# 1~4번 필드 (범위)
cut -d ':' -f 1-4 /etc/passwd

# CSV 파일에서 2번째 컬럼 추출
cut -d ',' -f 2 data.csv

# 문자 위치 기준으로 추출 (고정 폭 데이터)
cut -c 1-10 fixed_width.txt    # 1~10번째 문자&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 옵션:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;d&lt;/code&gt; : 구분자(delimiter) 지정 (기본값: TAB)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;f&lt;/code&gt; : 추출할 필드 번호&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c&lt;/code&gt; : 추출할 문자 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;tr&lt;/code&gt; &amp;mdash; 문자 변환/삭제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Translate&lt;/b&gt;의 약자이다. 문자를 &lt;b&gt;다른 문자로 치환&lt;/b&gt;하거나 &lt;b&gt;삭제&lt;/b&gt;한다. 주의할 점은, &lt;code&gt;tr&lt;/code&gt;은 파일을 직접 읽지 못하고 반드시 표준 입력(stdin)으로 데이터를 받아야 한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 소문자 &amp;rarr; 대문자 변환
echo &quot;hello world&quot; | tr 'a-z' 'A-Z'
# HELLO WORLD

# 공백을 줄바꿈으로 변환 (단어를 한 줄에 하나씩)
echo &quot;one two three&quot; | tr ' ' '\n'

# 특정 문자 삭제 (-d)
echo &quot;Hello, World! 123&quot; | tr -d '0-9'
# Hello, World!

# 연속된 중복 문자를 하나로 압축 (-s: squeeze)
echo &quot;hello     world&quot; | tr -s ' '
# hello world

# 실무: 로그에서 탭을 쉼표로 변환 (TSV &amp;rarr; CSV)
cat data.tsv | tr '\t' ',' &amp;gt; data.csv&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;nl&lt;/code&gt; &amp;mdash; 줄 번호 추가&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 줄 번호를 붙여서 출력 (빈 줄 제외)
nl /etc/ssh/sshd_config

# 빈 줄에도 번호 부여
nl -ba /etc/ssh/sshd_config&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;표준 스트림 (Standard Streams)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스의 모든 프로세스는 실행될 때 &lt;b&gt;3개의 기본 스트림&lt;/b&gt;을 자동으로 부여받는다:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;┌─────────────────────────────────────────────────┐
│                  프로세스                         │
│                                                 │
│  stdin (fd 0)  ──&amp;rarr;  [처리]  ──&amp;rarr;  stdout (fd 1)   │
│                            ──&amp;rarr;  stderr (fd 2)   │
└─────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;스트림&lt;/th&gt;
&lt;th&gt;파일 디스크립터(fd)&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;기본 연결&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;stdin&lt;/b&gt; (표준 입력)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;프로세스가 데이터를 &lt;b&gt;읽어오는&lt;/b&gt; 통로&lt;/td&gt;
&lt;td&gt;키보드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;stdout&lt;/b&gt; (표준 출력)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;프로세스가 정상 결과를 &lt;b&gt;내보내는&lt;/b&gt; 통로&lt;/td&gt;
&lt;td&gt;터미널 화면&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;stderr&lt;/b&gt; (표준 에러)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;프로세스가 에러 메시지를 &lt;b&gt;내보내는&lt;/b&gt; 통로&lt;/td&gt;
&lt;td&gt;터미널 화면&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 디스크립터(File Descriptor, fd)란 커널이 열린 파일(스트림 포함)을 관리하기 위해 부여하는 &lt;b&gt;정수 번호&lt;/b&gt;이다. 0, 1, 2는 예약된 번호이고, 이후 열리는 파일은 3, 4, 5... 순서로 번호가 부여된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 stdout과 stderr가 분리되어 있는가:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 출력과 에러 메시지를 &lt;b&gt;독립적으로 처리&lt;/b&gt;할 수 있기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 정상 출력은 파일에 저장하고, 에러만 화면에 표시
find / -name &quot;*.conf&quot; &amp;gt; result.txt 2&amp;gt;&amp;amp;1
# 1(stdout) &amp;rarr; result.txt
# 2(stderr) &amp;rarr; 화면에 표시 (권한 없는 디렉터리 접근 에러 등)

# 에러 메시지만 파일에 저장
find / -name &quot;*.conf&quot; 2&amp;gt; errors.txt

# 에러를 아예 무시
find / -name &quot;*.conf&quot; 2&amp;gt;/dev/null&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;grep&lt;/code&gt; &amp;mdash; 패턴으로 텍스트 검색&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;grep&lt;/code&gt;은 &lt;b&gt;Global Regular Expression Print&lt;/b&gt;의 약자이다. 파일에서 특정 패턴(문자열)이 포함된 &lt;b&gt;줄을 검색&lt;/b&gt;하여 출력한다. 가장 많이 사용되는 명령어 중 하나이며, 로그 분석과 트러블슈팅의 핵심 도구이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 파일에서 문자열 검색
grep &quot;error&quot; /var/log/syslog

# 대소문자 무시 (-i)
grep -i &quot;error&quot; /var/log/syslog

# 줄 번호 함께 표시 (-n)
grep -n &quot;error&quot; /var/log/syslog

# 매치된 줄 수만 출력 (-c)
grep -c &quot;error&quot; /var/log/syslog

# 패턴이 포함되지 않은 줄만 출력 (-v: invert)
grep -v &quot;^#&quot; /etc/ssh/sshd_config     # 주석(#) 제외

# 디렉터리 내 모든 파일에서 재귀 검색 (-r)
grep -r &quot;database_url&quot; /etc/myapp/

# 매치된 파일명만 출력 (-l)
grep -rl &quot;TODO&quot; ./src/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주변 줄 함께 보기 (Context)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 로그를 검색할 때, 에러가 발생한 줄만 보면 맥락을 파악하기 어렵다. 주변 줄을 함께 보면 원인을 파악하기 쉽다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 매치된 줄 + 아래 3줄 (-A: After)
grep -A 3 &quot;Exception&quot; /var/log/app.log

# 매치된 줄 + 위 3줄 (-B: Before)
grep -B 3 &quot;Exception&quot; /var/log/app.log

# 매치된 줄 + 위아래 3줄 (-C: Context)
grep -C 3 &quot;Exception&quot; /var/log/app.log&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 패턴&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;# 설정 파일에서 주석과 빈 줄 제거하고 실제 설정만 보기
grep -v &quot;^#&quot; /etc/ssh/sshd_config | grep -v &quot;^$&quot;

# 로그에서 특정 시간대의 에러 검색
grep &quot;2025-03-23 14:&quot; /var/log/syslog | grep -i &quot;error&quot;

# 특정 IP의 접속 로그 검색
grep &quot;192.168.1.100&quot; /var/log/nginx/access.log

# 프로세스 목록에서 특정 프로세스 찾기
ps aux | grep nginx | grep -v grep&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;핵심 포인트&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;짧은 파일 전체 출력, 파일 연결&lt;/td&gt;
&lt;td&gt;긴 파일에는 부적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;less&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;긴 파일 페이지 탐색&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;로 검색, &lt;code&gt;&amp;amp;&lt;/code&gt;로 필터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;head&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 앞부분 확인&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-n N&lt;/code&gt;으로 줄 수 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 뒷부분 확인&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;-F&lt;/code&gt;로 실시간 모니터링&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cut&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;필드/컬럼 추출&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-d&lt;/code&gt;(구분자) + &lt;code&gt;-f&lt;/code&gt;(필드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;문자 변환/삭제&lt;/td&gt;
&lt;td&gt;stdin으로만 입력 받음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grep&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;패턴 검색&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-i&lt;/code&gt;, &lt;code&gt;-r&lt;/code&gt;, &lt;code&gt;-v&lt;/code&gt;, &lt;code&gt;-C&lt;/code&gt; 가 핵심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;바이너리에서 텍스트 추출&lt;/td&gt;
&lt;td&gt;파일 정체 파악에 유용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 05. 링크 (심볼릭 링크, 하드 링크)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서 링크(Link)란 하나의 파일에 대해 &lt;b&gt;여러 이름(경로)을 부여&lt;/b&gt;하는 메커니즘이다. Windows의 &quot;바로가기&quot;와 유사하지만, 동작 방식이 근본적으로 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일의 실제 데이터와 메타데이터는 &lt;b&gt;inode&lt;/b&gt;에 저장된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 이름&lt;/b&gt;은 inode에 저장되지 않는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디렉터리&lt;/b&gt;란 &quot;파일명 &amp;rarr; inode 번호&quot;의 매핑 테이블이다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;디렉터리 엔트리 (매핑 테이블)
┌──────────────────────────────┐
│  파일명         &amp;rarr; inode 번호    │
│  config.yaml   &amp;rarr; 131074      │
│  readme.md     &amp;rarr; 131075      │
│  deploy.sh     &amp;rarr; 131076      │
└──────────────────────────────┘

inode 테이블
┌──────────────────────────────────────────┐
│  inode 131074: 권한, 소유자, 크기, 데이터 위치  │
│  inode 131075: 권한, 소유자, 크기, 데이터 위치  │
│  inode 131076: 권한, 소유자, 크기, 데이터 위치  │
└──────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크란 이 매핑 관계를 &lt;b&gt;추가로 만드는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하드 링크 (Hard Link)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드 링크는 &lt;b&gt;동일한 inode를 가리키는 새로운 디렉터리 엔트리&lt;/b&gt;를 만드는 것이다. 원본과 하드 링크는 완전히 &lt;b&gt;동등한 관계&lt;/b&gt;이며, 어느 쪽이 &quot;원본&quot;이고 어느 쪽이 &quot;링크&quot;인지 구분할 수 없다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;원본 생성 후:
  &quot;config.yaml&quot;  ──&amp;rarr;  inode 131074  ──&amp;rarr;  [디스크의 실제 데이터]
                       (Links: 1)

하드 링크 생성 후:
  &quot;config.yaml&quot;  ──&amp;rarr;  inode 131074  ──&amp;rarr;  [디스크의 실제 데이터]
  &quot;config.bak&quot;   ──&amp;rarr;  inode 131074  ──&amp;rarr;  (같은 데이터를 가리킴)
                       (Links: 2)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 파일명이 &lt;b&gt;동일한 inode&lt;/b&gt;(동일한 데이터)를 가리킨다&lt;/li&gt;
&lt;li&gt;어느 쪽을 수정해도 &lt;b&gt;양쪽 모두 반영&lt;/b&gt;된다 (같은 데이터이므로)&lt;/li&gt;
&lt;li&gt;한쪽을 삭제해도 &lt;b&gt;다른 쪽은 영향 없다&lt;/b&gt; (inode의 링크 카운트가 0이 되어야 데이터가 삭제된다)&lt;/li&gt;
&lt;li&gt;디스크 공간을 추가로 차지하지 않는다 (데이터 복제가 아니므로)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성 및 확인&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 하드 링크 생성
ln original.txt hardlink.txt

# inode 번호 확인 &amp;mdash; 동일한 번호를 가진다
ls -li original.txt hardlink.txt
# 131074 -rw-r--r-- 2 devops devops 1234 Mar 23 10:00 hardlink.txt
# 131074 -rw-r--r-- 2 devops devops 1234 Mar 23 10:00 original.txt
#   │                 │
#   └ 같은 inode      └ Links: 2 (하드 링크 수)

# 한쪽 수정 &amp;rarr; 양쪽 반영 확인
echo &quot;new content&quot; &amp;gt;&amp;gt; original.txt
cat hardlink.txt    # &quot;new content&quot;가 보인다

# 한쪽 삭제 &amp;rarr; 다른 쪽은 살아있다
rm original.txt
cat hardlink.txt    # 여전히 정상 접근 가능
ls -li hardlink.txt
# 131074 -rw-r--r-- 1 devops devops ...    &amp;larr; Links가 2&amp;rarr;1로 줄어듦&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제약사항&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;제약&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;디렉터리에는 하드 링크를 만들 수 없다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;순환 참조(circular reference)가 발생하면 파일시스템이 무한 루프에 빠질 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;다른 파일시스템(파티션)을 넘어서 만들 수 없다&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;inode 번호는 파일시스템 내에서만 유일하다. 다른 파일시스템의 inode와는 연결할 수 없다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;심볼릭 링크 (Symbolic Link / Symlink)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심볼릭 링크는 &lt;b&gt;다른 파일의 경로(문자열)를 저장하는 별도의 파일&lt;/b&gt;이다. 하드 링크와 달리 자체 inode를 가지며, 그 안에 &quot;원본 파일의 경로&quot;를 텍스트로 저장한다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;심볼릭 링크 생성 후:
  &quot;config.yaml&quot;       ──&amp;rarr;  inode 131074  ──&amp;rarr;  [실제 데이터]
  &quot;config-link.yaml&quot;  ──&amp;rarr;  inode 131099  ──&amp;rarr;  &quot;/home/devops/config.yaml&quot; (경로 문자열)
                             (타입: symlink)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows의 &quot;바로가기&quot;와 가장 유사한 개념이다. 다만 리눅스의 심볼릭 링크는 OS 수준에서 투명하게 동작하므로, 대부분의 프로그램이 심볼릭 링크를 &lt;b&gt;원본 파일처럼&lt;/b&gt; 취급한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성 및 확인&lt;/h3&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 심볼릭 링크 생성 (-s 옵션)
ln -s /etc/nginx/nginx.conf ~/nginx-config

# 확인
ls -la ~/nginx-config
# lrwxrwxrwx 1 devops devops 21 Mar 23 10:00 nginx-config -&amp;gt; /etc/nginx/nginx.conf
# │                            │                              │
# └ 타입: l (심볼릭 링크)        └ 링크 파일 자체의 크기           └ 가리키는 원본 경로
#                                (경로 문자열의 길이)

# 심볼릭 링크를 통해 원본 파일 읽기
cat ~/nginx-config    # /etc/nginx/nginx.conf의 내용이 출력된다

# 디렉터리에도 심볼릭 링크 생성 가능
ln -s /var/log/nginx ~/logs
ls ~/logs/            # /var/log/nginx/의 내용이 보인다

# 다른 파일시스템(파티션)을 넘어서도 생성 가능
ln -s /mnt/external/data ~/external-data&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;끊어진 심볼릭 링크 (Dangling Symlink)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심볼릭 링크의 가장 큰 약점은 &lt;b&gt;원본이 삭제되면 링크가 끊어진다&lt;/b&gt;는 것이다. 끊어진 심볼릭 링크를 &lt;b&gt;dangling symlink&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 원본 생성 &amp;rarr; 심볼릭 링크 생성 &amp;rarr; 원본 삭제
touch /tmp/original.txt
ln -s /tmp/original.txt ~/my-link
rm /tmp/original.txt

# 끊어진 링크 &amp;mdash; 접근하면 에러
cat ~/my-link
# cat: /home/devops/my-link: No such file or directory

# ls에서 끊어진 링크는 빨간색으로 표시된다 (색상 지원 터미널)
ls -la ~/my-link
# lrwxrwxrwx 1 devops devops 17 Mar 23 10:00 my-link -&amp;gt; /tmp/original.txt (빨간색)

# 끊어진 심볼릭 링크 찾기
find /home -type l ! -exec test -e {} \; -print&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서의 심볼릭 링크 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nginx 사이트 활성화/비활성화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx의 가상 호스트 관리는 심볼릭 링크의 대표적 활용 사례이다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 사이트 설정은 sites-available에 저장
/etc/nginx/sites-available/mysite.conf    # 설정 파일 (원본)

# 활성화: sites-enabled에 심볼릭 링크 생성
sudo ln -s /etc/nginx/sites-available/mysite.conf /etc/nginx/sites-enabled/

# 비활성화: 심볼릭 링크만 삭제 (원본은 보존)
sudo rm /etc/nginx/sites-enabled/mysite.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점: 설정 파일 자체를 삭제하지 않고도 사이트를 활성화/비활성화할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무중단 배포 (Zero-Downtime Deployment)&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 디렉터리 구조
/opt/myapp/
├── releases/
│   ├── v1.0.0/          # 이전 버전
│   ├── v1.1.0/          # 이전 버전
│   └── v1.2.0/          # 새 버전
└── current -&amp;gt; releases/v1.1.0   # 현재 운영 중인 버전 (심볼릭 링크)

# 배포: 심볼릭 링크만 교체 (거의 즉시 완료)
ln -sfn /opt/myapp/releases/v1.2.0 /opt/myapp/current
#  -s: 심볼릭 링크
#  -f: 기존 링크 덮어쓰기
#  -n: 대상이 디렉터리여도 링크 자체를 교체 (안으로 들어가지 않음)

# 롤백: 이전 버전으로 링크만 되돌리기 (즉시 완료)
ln -sfn /opt/myapp/releases/v1.1.0 /opt/myapp/current&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서버의 DocumentRoot가 &lt;code&gt;/opt/myapp/current&lt;/code&gt;를 가리키고 있다면, 심볼릭 링크 교체만으로 배포와 롤백이 거의 즉시 이루어진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;개념&lt;/th&gt;
&lt;th&gt;핵심 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;하드 링크&lt;/td&gt;
&lt;td&gt;동일한 inode를 공유, 원본 삭제해도 데이터 유지, 같은 파티션만 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;심볼릭 링크&lt;/td&gt;
&lt;td&gt;별도 inode, 경로 문자열 저장, 파티션/디렉터리 제약 없음, 원본 삭제 시 끊어짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실무 기본 선택&lt;/td&gt;
&lt;td&gt;&lt;b&gt;심볼릭 링크&lt;/b&gt; (99%의 경우)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;핵심 활용&lt;/td&gt;
&lt;td&gt;Nginx sites-enabled, 무중단 배포, 바이너리 버전 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성 명령어&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ln&lt;/code&gt; (하드), &lt;code&gt;ln -s&lt;/code&gt; (심볼릭), &lt;code&gt;ln -sfn&lt;/code&gt; (덮어쓰기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주의사항&lt;/td&gt;
&lt;td&gt;심볼릭 링크 디렉터리 삭제 시 &lt;code&gt;/&lt;/code&gt; 붙이지 않기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 06. 압축 &amp;amp; 아카이브&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아카이브와 압축의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;아카이브&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 파일과 디렉터리를 &lt;b&gt;하나의 파일로 묶는 것&lt;/b&gt;이다. 파일의 크기를 줄이지는 않는다. 마치 여러 서류를 하나의 봉투에 넣는 것과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;압축&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 크기를 &lt;b&gt;줄이는&lt;/b&gt; 것이다. 반복되는 패턴을 효율적으로 인코딩하여 파일 크기를 감소시킨다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[파일A] [파일B] [파일C]
         │
    아카이브 (tar)
         &amp;darr;
  [파일A+B+C.tar]        &amp;larr; 크기는 거의 동일 (묶기만 함)
         │
      압축 (gzip)
         &amp;darr;
  [파일A+B+C.tar.gz]     &amp;larr; 크기가 줄어듦&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;tar&lt;/code&gt; &amp;mdash; 아카이브 도구&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tape Archive&lt;/b&gt;의 약자이다. 원래 자기 테이프에 백업하기 위해 만들어졌지만, 현재는 파일 묶기/풀기의 표준 도구이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tar&lt;/code&gt;의 옵션은 동작(무엇을 할 것인가)과 수식(어떻게 할 것인가)으로 나뉜다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;동작 옵션&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Create&lt;/b&gt; &amp;mdash; 새 아카이브 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Extract&lt;/b&gt; &amp;mdash; 아카이브 풀기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;lisT&lt;/b&gt; &amp;mdash; 아카이브 내용 목록 보기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;수식 옵션&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;File&lt;/b&gt; &amp;mdash; 아카이브 파일명 지정 (거의 항상 사용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Verbose&lt;/b&gt; &amp;mdash; 처리 과정 출력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;gzip으로 압축/해제 (&lt;code&gt;.tar.gz&lt;/code&gt;, &lt;code&gt;.tgz&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-j&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bzip2로 압축/해제 (&lt;code&gt;.tar.bz2&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-J&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;xz로 압축/해제 (&lt;code&gt;.tar.xz&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-C&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;지정한 디렉터리에서 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아카이브 생성&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 기본 아카이브 생성 (압축 없이 묶기만)
tar -cvf archive.tar file1.txt file2.txt dir1/
#  -c: 생성
#  -v: 과정 출력
#  -f: 파일명 지정

# gzip 압축 아카이브 (가장 흔히 사용) ★★★
tar -czvf archive.tar.gz /var/log/nginx/

# bzip2 압축 아카이브 (더 높은 압축률, 더 느림)
tar -cjvf archive.tar.bz2 /var/log/nginx/

# xz 압축 아카이브 (가장 높은 압축률, 가장 느림)
tar -cJvf archive.tar.xz /var/log/nginx/

# 특정 파일/디렉터리 제외
tar -czvf backup.tar.gz --exclude='*.log' --exclude='.git' /opt/myapp/
tar -czvf backup.tar.gz --exclude-vcs /opt/myapp/   # VCS 디렉터리 전체 제외&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아카이브 풀기&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# gzip 압축 아카이브 풀기
tar -xzvf archive.tar.gz

# 특정 디렉터리에 풀기 (-C 옵션)
tar -xzvf archive.tar.gz -C /opt/restore/

# 아카이브 내용 목록 보기 (풀지 않고 확인)
tar -tzvf archive.tar.gz

# 아카이브에서 특정 파일만 추출
tar -xzvf archive.tar.gz path/to/specific/file.txt&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 패턴&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;# 서버 설정 백업 (날짜 포함)
tar -czvf /backup/etc-backup-$(date +%Y%m%d).tar.gz /etc/

# 애플리케이션 배포 패키지 생성 (불필요한 파일 제외)
tar -czvf release-v1.2.0.tar.gz \
  --exclude='node_modules' \
  --exclude='.env' \
  --exclude='*.log' \
  ./myapp/

# 원격 서버로 직접 전송 (로컬에 파일을 만들지 않고)
tar -czf - /opt/myapp/ | ssh user@remote &quot;tar -xzf - -C /opt/&quot;
# '-'는 stdout/stdin을 의미한다&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;압축 도구 비교&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;확장자&lt;/th&gt;
&lt;th&gt;압축률&lt;/th&gt;
&lt;th&gt;속도&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;gzip&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.gz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;빠름&lt;/td&gt;
&lt;td&gt;가장 범용적, 사실상 표준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;bzip2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.bz2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;느림&lt;/td&gt;
&lt;td&gt;gzip보다 높은 압축률&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;xz&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.xz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;매우 높음&lt;/td&gt;
&lt;td&gt;매우 느림&lt;/td&gt;
&lt;td&gt;배포판 패키지에서 자주 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;zstd&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.zst&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;매우 빠름&lt;/td&gt;
&lt;td&gt;최신, 압축률과 속도 모두 우수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;lz4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.lz4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;극히 빠름&lt;/td&gt;
&lt;td&gt;실시간 압축에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;선택 기준:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;일반적인 백업/전송&lt;/b&gt;: &lt;code&gt;gzip&lt;/code&gt; (호환성 최고)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대용량 로그 아카이브&lt;/b&gt;: &lt;code&gt;xz&lt;/code&gt; 또는 &lt;code&gt;zstd&lt;/code&gt; (높은 압축률)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빠른 처리가 중요한 경우&lt;/b&gt;: &lt;code&gt;zstd&lt;/code&gt; 또는 &lt;code&gt;lz4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;gzip&lt;/code&gt; / &lt;code&gt;gunzip&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# 파일 압축 (원본 파일이 .gz로 대체된다)
gzip access.log
# access.log &amp;rarr; access.log.gz (원본 사라짐)

# 원본 유지하면서 압축
gzip -k access.log
# access.log 유지, access.log.gz 생성

# 해제
gunzip access.log.gz
# 또는
gzip -d access.log.gz

# 압축률 조절 (1=빠름/낮은압축, 9=느림/높은압축, 기본=6)
gzip -9 large_file.log    # 최대 압축

# 압축 파일의 내용을 해제하지 않고 보기
zcat access.log.gz        # cat처럼 출력
zless access.log.gz       # less처럼 페이지 단위
zgrep &quot;error&quot; access.log.gz  # grep처럼 검색&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;xz&lt;/code&gt; / &lt;code&gt;unxz&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# 압축
xz large_file.log

# 원본 유지하면서 압축
xz -k large_file.log

# 해제
unxz large_file.log.xz

# 멀티스레드 압축 (속도 개선) ★
xz -T 0 large_file.log    # 사용 가능한 모든 코어 활용

# 압축 파일 내용 보기
xzcat large_file.log.xz&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;zip&lt;/code&gt; / &lt;code&gt;unzip&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;zip&lt;/code&gt;은 Windows 환경과의 호환성을 위해 사용한다. 리눅스 자체적으로는 &lt;code&gt;tar.gz&lt;/code&gt;가 표준이지만, Windows 사용자에게 파일을 전달하거나 받을 때 &lt;code&gt;zip&lt;/code&gt;을 사용하게 된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 설치 (없는 경우)
sudo apt install zip unzip

# 압축 (파일/디렉터리)
zip -r archive.zip mydir/
zip archive.zip file1.txt file2.txt

# 해제
unzip archive.zip
unzip archive.zip -d /opt/extract/    # 특정 디렉터리에 풀기

# 내용 목록 보기
unzip -l archive.zip

# 특정 파일만 추출
unzip archive.zip path/to/file.txt

# 비밀번호 설정 압축
zip -e -r secret.zip sensitive_data/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;압축 알고리즘의 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무손실 압축(Lossless Compression)은 데이터에서 &lt;b&gt;반복되는 패턴&lt;/b&gt;을 찾아서 더 짧은 표현으로 대체한다. 해제하면 원본과 100% 동일한 데이터가 복원된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 &amp;mdash; &lt;code&gt;AAAAABBBCC&lt;/code&gt;라는 10바이트 데이터:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;압축 후: &lt;code&gt;5A3B2C&lt;/code&gt; (6바이트) &amp;mdash; &quot;A가 5번, B가 3번, C가 2번&quot;&lt;/li&gt;
&lt;li&gt;이를 Run-Length Encoding (RLE)이라고 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 gzip, bzip2, xz 등은 이보다 훨씬 정교한 알고리즘을 사용하지만, 핵심 원리는 동일하다 &amp;mdash; &lt;b&gt;반복과 패턴을 효율적으로 인코딩&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;핵심 사용법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;아카이브(묶기/풀기)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-czf&lt;/code&gt;(생성), &lt;code&gt;-xzf&lt;/code&gt;(풀기), &lt;code&gt;-tzf&lt;/code&gt;(목록)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gzip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;범용 압축&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gzip file&lt;/code&gt;, &lt;code&gt;zcat&lt;/code&gt;, &lt;code&gt;zgrep&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;고압축&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xz -T0 file&lt;/code&gt; (멀티스레드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zip/unzip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Windows 호환&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zip -r&lt;/code&gt;, &lt;code&gt;unzip -l&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;추천&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;리눅스 서버 간 백업/전송&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tar.gz&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장기 아카이브 (공간 절약)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tar.xz&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows 사용자에게 전달&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zip&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;압축된 로그 검색&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zgrep&lt;/code&gt;, &lt;code&gt;zcat&lt;/code&gt;, &lt;code&gt;zless&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서버 간 실시간 전송&lt;/td&gt;
&lt;td&gt;`tar -czf -&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 07. 권한 &amp;amp; 소유자 관리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스는 태생적으로 &lt;b&gt;멀티유저(multi-user)&lt;/b&gt; 시스템이다. 여러 사용자가 동시에 접속하여 작업하기 때문에, &quot;누가 어떤 파일을 읽고, 쓰고, 실행할 수 있는가&quot;를 제어하는 것이 필수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 문제는 매우 빈번하게 발생할 수 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배포 스크립트가 실행 권한이 없어서 실패&lt;/li&gt;
&lt;li&gt;애플리케이션이 로그 파일에 쓰기 권한이 없어서 에러&lt;/li&gt;
&lt;li&gt;설정 파일이 누구나 읽을 수 있어서 보안 위험 (DB 비밀번호 노출 등)&lt;/li&gt;
&lt;li&gt;Docker 컨테이너 내부의 파일 소유자 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;권한의 구조&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세 가지 주체 (Who)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스는 파일에 접근하는 주체를 &lt;b&gt;세 그룹&lt;/b&gt;으로 나눈다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주체&lt;/th&gt;
&lt;th&gt;약자&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Owner (소유자)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt; (user)&lt;/td&gt;
&lt;td&gt;파일을 소유한 사용자. 보통 파일을 생성한 사람이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Group (그룹)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일에 지정된 그룹에 속한 사용자들.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Others (기타)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;소유자도 아니고 그룹에도 속하지 않는 나머지 모든 사용자.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세 가지 권한&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;권한&lt;/th&gt;
&lt;th&gt;문자&lt;/th&gt;
&lt;th&gt;숫자&lt;/th&gt;
&lt;th&gt;파일에 대한 의미&lt;/th&gt;
&lt;th&gt;디렉터리에 대한 의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Read&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;파일 내용을 읽을 수 있다&lt;/td&gt;
&lt;td&gt;디렉터리 내 파일 목록을 볼 수 있다 (&lt;code&gt;ls&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Write&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;파일 내용을 수정할 수 있다&lt;/td&gt;
&lt;td&gt;디렉터리 내 파일을 생성/삭제할 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Execute&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;파일을 실행할 수 있다&lt;/td&gt;
&lt;td&gt;디렉터리에 진입할 수 있다 (&lt;code&gt;cd&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디렉터리의 &lt;code&gt;x&lt;/code&gt;(실행) 권한은 직관적이지 않으므로 주의가 필요하다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt; 없이 &lt;code&gt;x&lt;/code&gt;만 있으면: 디렉터리 안의 파일 목록은 볼 수 없지만, 파일명을 정확히 알면 접근할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; 없이 &lt;code&gt;r&lt;/code&gt;만 있으면: 파일 목록은 볼 수 있지만, 실제로 파일에 접근하거나 &lt;code&gt;cd&lt;/code&gt;로 진입할 수 없다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;w&lt;/code&gt;는 &lt;code&gt;x&lt;/code&gt;와 함께 있어야 의미가 있다: 디렉터리에 진입(&lt;code&gt;x&lt;/code&gt;)할 수 있어야 파일을 생성/삭제(&lt;code&gt;w&lt;/code&gt;)할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한 표기법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ls -l&lt;/code&gt; 출력에서 권한은 9자리 문자로 표시된다:&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;-rwxr-xr--
│├─┤├─┤├─┤
│ │  │  └── Others: r-- (읽기만)
│ │  └── Group: r-x (읽기+실행)
│ └── Owner: rwx (읽기+쓰기+실행)
└── 파일 타입 (- = 일반 파일)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8진수(Octal) 표기법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 권한을 숫자로 표현하며, 세 자리 숫자로 소유자/그룹/기타의 권한을 나타낸다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;r=4, w=2, x=1 &amp;rarr; 합산하여 표현

rwx = 4+2+1 = 7
r-x = 4+0+1 = 5
r-- = 4+0+0 = 4
--- = 0+0+0 = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자주 사용하는 권한 조합:&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;8진수&lt;/th&gt;
&lt;th&gt;문자 표기&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;755&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rwxr-xr-x&lt;/td&gt;
&lt;td&gt;소유자 전체, 나머지 읽기+실행&lt;/td&gt;
&lt;td&gt;실행 파일, 디렉터리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;644&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-r--r--&lt;/td&gt;
&lt;td&gt;소유자 읽기+쓰기, 나머지 읽기&lt;/td&gt;
&lt;td&gt;일반 파일, 설정 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;700&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rwx------&lt;/td&gt;
&lt;td&gt;소유자만 전체 권한&lt;/td&gt;
&lt;td&gt;개인 디렉터리, SSH 키 디렉터리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;600&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rw-------&lt;/td&gt;
&lt;td&gt;소유자만 읽기+쓰기&lt;/td&gt;
&lt;td&gt;SSH 개인 키, 민감한 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;777&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;rwxrwxrwx&lt;/td&gt;
&lt;td&gt;모두에게 전체 권한&lt;/td&gt;
&lt;td&gt;&lt;b&gt;보안 위험 &amp;mdash; 거의 사용하지 않는다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;400&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;r--------&lt;/td&gt;
&lt;td&gt;소유자만 읽기&lt;/td&gt;
&lt;td&gt;AWS 키 파일 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;chmod&lt;/code&gt; &amp;mdash; 권한 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Change Mode&lt;/b&gt;의 약자이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8진수 방식&lt;/h3&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 파일 권한을 644로 설정
chmod 644 config.yaml

# 스크립트에 실행 권한 부여
chmod 755 deploy.sh

# SSH 개인 키 &amp;mdash; 소유자만 읽기 (AWS에서 필수)
chmod 400 my-key.pem

# 디렉터리와 내용물에 재귀적으로 적용
chmod -R 755 /var/www/html/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;심볼릭 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누구(u/g/o/a)에게 무엇(r/w/x)을 어떻게(+/-/=) 할 것인지를 문자로 표현한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기호&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;owner(소유자)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;group(그룹)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;others(기타)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;all(전체) &amp;mdash; u+g+o&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;권한 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;권한 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;권한을 정확히 설정 (기존 권한 대체)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 소유자에게 실행 권한 추가
chmod u+x script.sh

# 그룹과 기타에서 쓰기 권한 제거
chmod go-w sensitive.conf

# 기타 사용자의 모든 권한 제거
chmod o= secret.env

# 모든 사용자에게 읽기 권한 부여
chmod a+r public.html

# 실행 권한만 추가 (기존 권한 유지)
chmod +x deploy.sh&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;chown&lt;/code&gt; &amp;mdash; 소유자/그룹 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Change Owner&lt;/b&gt;의 약자이다. 파일의 소유자와 소유 그룹을 변경한다. &lt;b&gt;root 권한(sudo)이 필요&lt;/b&gt;하다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 소유자 변경
sudo chown nginx /var/www/html/index.html

# 소유자 + 그룹 동시 변경
sudo chown nginx:www-data /var/www/html/index.html

# 그룹만 변경 (: 앞을 비움)
sudo chown :www-data /var/www/html/index.html

# 재귀적으로 변경 (디렉터리 전체)
sudo chown -R nginx:www-data /var/www/html/

# 심볼릭 링크 자체의 소유자 변경 (기본은 대상 파일 변경)
sudo chown -h devops symlink_file&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;chgrp&lt;/code&gt; &amp;mdash; 그룹만 변경&lt;/h3&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# chown :group과 동일한 효과
sudo chgrp www-data /var/www/html/
sudo chgrp -R docker /opt/containers/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특수 권한&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 rwx 외에 세 가지 &lt;b&gt;특수 권한&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SUID (Set User ID) &amp;mdash; 4000&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 파일에 SUID가 설정되면, 해당 파일을 &lt;b&gt;누가 실행하든 파일 소유자의 권한&lt;/b&gt;으로 실행된다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 대표적인 SUID 파일
ls -la /usr/bin/passwd
# -rwsr-xr-x 1 root root 68208 ... /usr/bin/passwd
#    ^
#    s = SUID가 설정되어 있다는 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 상황에 필요할까?&lt;br /&gt;&lt;code&gt;passwd&lt;/code&gt; 명령어는 &lt;code&gt;/etc/shadow&lt;/code&gt; 파일을 수정해야 한다. 이 파일은 root만 쓸 수 있다. 하지만 일반 사용자도 자신의 비밀번호를 변경할 수 있어야 한다. SUID를 통해 일반 사용자가 &lt;code&gt;passwd&lt;/code&gt;를 실행하면 root 권한으로 동작하여 &lt;code&gt;/etc/shadow&lt;/code&gt;를 수정할 수 있게 된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# SUID 설정
chmod u+s executable
chmod 4755 executable

# 보안 감사 &amp;mdash; SUID 파일 찾기 (비정상적인 SUID 파일은 보안 위험)
find / -type f -perm -4000 2&amp;gt;/dev/null&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SGID (Set Group ID) &amp;mdash; 2000&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파일에 설정&lt;/b&gt;: 실행 시 파일 소유 그룹의 권한으로 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디렉터리에 설정&lt;/b&gt;: 해당 디렉터리 안에서 생성되는 새 파일의 &lt;b&gt;그룹이 디렉터리의 그룹을 상속&lt;/b&gt;한다. 팀 공유 디렉터리에서 매우 유용하다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 팀 공유 디렉터리 설정
sudo mkdir /shared/project
sudo chown :devteam /shared/project
sudo chmod 2775 /shared/project
#          ^
#          2 = SGID

# 이후 이 디렉터리에서 생성되는 모든 파일은 자동으로 devteam 그룹 소유&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sticky Bit &amp;mdash; 1000&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디렉터리에 설정하면, 해당 디렉터리 안의 파일을 &lt;b&gt;파일의 소유자와 root만 삭제&lt;/b&gt;할 수 있다. 다른 사용자의 파일을 삭제할 수 없게 보호한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# /tmp의 권한을 확인해보면 Sticky Bit가 설정되어 있다
ls -ld /tmp
# drwxrwxrwt 15 root root 4096 ... /tmp
#          ^
#          t = Sticky Bit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/tmp&lt;/code&gt;는 모든 사용자가 읽고 쓸 수 있는(&lt;code&gt;rwxrwxrwx&lt;/code&gt;) 디렉터리이다. Sticky Bit가 없으면 사용자 A가 사용자 B의 임시 파일을 삭제할 수 있어 위험하다. Sticky Bit 덕분에 자기 파일만 삭제할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# Sticky Bit 설정
chmod +t /shared/tmp
chmod 1777 /shared/tmp&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특수 권한 정리&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특수 권한&lt;/th&gt;
&lt;th&gt;숫자&lt;/th&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;th&gt;효과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SUID&lt;/td&gt;
&lt;td&gt;4000&lt;/td&gt;
&lt;td&gt;파일&lt;/td&gt;
&lt;td&gt;실행 시 소유자 권한으로 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SGID&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;디렉터리&lt;/td&gt;
&lt;td&gt;새 파일이 디렉터리의 그룹을 상속&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sticky Bit&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;디렉터리&lt;/td&gt;
&lt;td&gt;소유자만 자기 파일 삭제 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;umask &amp;mdash; 기본 권한 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;umask&lt;/code&gt;는 새로 생성되는 파일과 디렉터리의 &lt;b&gt;기본 권한을 결정하는 마스크 값&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서 파일/디렉터리 생성 시 기본 최대 권한:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일&lt;/b&gt;: 666 (rw-rw-rw-) &amp;mdash; 보안상 실행 권한은 기본 부여하지 않는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디렉터리&lt;/b&gt;: 777 (rwxrwxrwx)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;umask 값을 빼면&lt;/b&gt; 실제 생성되는 파일의 권한이 된다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;파일 기본 권한   = 666 - umask
디렉터리 기본 권한 = 777 - umask&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 현재 umask 확인
$ umask
0022

# umask가 0022일 때:
# 파일:      666 - 022 = 644 (rw-r--r--)
# 디렉터리:   777 - 022 = 755 (rwxr-xr-x)

# umask 변경
umask 0077
# 파일:      666 - 077 = 600 (rw-------)
# 디렉터리:   777 - 077 = 700 (rwx------)
# &amp;rarr; 소유자만 접근 가능하게 된다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안이 중요한 환경에서는 umask를 &lt;code&gt;0027&lt;/code&gt; 또는 &lt;code&gt;0077&lt;/code&gt;로 설정하여 기타 사용자의 접근을 기본적으로 차단한다. umask 설정은 &lt;code&gt;~/.bashrc&lt;/code&gt;나 &lt;code&gt;/etc/profile&lt;/code&gt;에 넣어 영구 적용할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: umask는 엄밀히는 단순 뺄셈이 아닌 비트 마스크(NOT AND) 연산이다. 대부분의 경우 뺄셈과 결과가 같지만, 이론적으로는 최대권한 AND (NOT umask)가 정확한 계산이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;sudo&lt;/code&gt; &amp;mdash; 관리자 권한 실행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sudo&lt;/code&gt;는 &lt;b&gt;Superuser Do&lt;/b&gt;의 약자이다. 일반 사용자가 &lt;b&gt;임시로 root 권한&lt;/b&gt;을 빌려서 명령어를 실행할 수 있게 한다. root 계정으로 직접 로그인하는 것보다 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 root 직접 로그인 대신 sudo를 사용하는가:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;감사 로그&lt;/b&gt;: 누가, 언제, 어떤 명령어를 실행했는지 기록된다 (&lt;code&gt;/var/log/auth.log&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최소 권한 원칙&lt;/b&gt;: 필요한 순간에만 일시적으로 권한을 상승시킨다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실수 방지&lt;/b&gt;: 항상 root로 작업하면 한 번의 실수가 시스템 전체에 영향을 미친다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# root 권한으로 명령어 실행
sudo systemctl restart nginx

# root 셸로 전환
sudo -i       # root의 로그인 환경으로 전환
sudo su -     # 위와 유사

# 다른 사용자로 명령어 실행
sudo -u nginx cat /etc/nginx/nginx.conf

# 직전 명령어를 sudo로 재실행 ★
sudo !!
# 예: apt update &amp;rarr; 권한 에러 &amp;rarr; sudo !!로 재실행&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;핵심&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chmod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;권한 변경&lt;/td&gt;
&lt;td&gt;&lt;code&gt;755&lt;/code&gt;(8진수), &lt;code&gt;u+x&lt;/code&gt;(심볼릭)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;chown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;소유자/그룹 변경&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-R&lt;/code&gt;(재귀), root 권한 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;umask&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기본 권한 마스크&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0022&lt;/code&gt;(기본), &lt;code&gt;0077&lt;/code&gt;(보안 강화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sudo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;관리자 권한 실행&lt;/td&gt;
&lt;td&gt;&lt;code&gt;visudo&lt;/code&gt;로 설정, &lt;code&gt;!!&lt;/code&gt;로 재실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 08. 디스크 &amp;amp; 용량 관리&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디스크 관리가 중요한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드/서버 환경에서 &quot;디스크 용량 부족&quot;은 가장 빈번하게 발생하는 장애 원인 중 하나이다. 디스크가 꽉 차면 다음과 같은 문제가 발생한다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그를 더 이상 기록할 수 없어서 애플리케이션이 비정상 종료&lt;/li&gt;
&lt;li&gt;데이터베이스가 쓰기를 멈추거나 손상&lt;/li&gt;
&lt;li&gt;시스템 자체가 부팅 불가 상태에 빠짐&lt;/li&gt;
&lt;li&gt;배포(deploy)가 실패&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디스크 구조 기본 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;물리 디스크 &amp;rarr; 파티션 &amp;rarr; 파일시스템&lt;/h3&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;물리 디스크 (/dev/sda 또는 /dev/nvme0n1)
  ├── 파티션 1 (/dev/sda1) &amp;rarr; 파일시스템(ext4) &amp;rarr; / (루트)에 마운트
  ├── 파티션 2 (/dev/sda2) &amp;rarr; 파일시스템(ext4) &amp;rarr; /home에 마운트
  └── 파티션 3 (/dev/sda3) &amp;rarr; 스왑(swap)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파티션(Partition)&lt;/b&gt;: 하나의 물리 디스크를 논리적으로 나눈 영역이다. 각 파티션은 독립적으로 포맷하고 마운트할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파일시스템(Filesystem)&lt;/b&gt;: 파티션 위에 생성되는 데이터 저장 구조이다. 포맷(format)이란 파티션에 파일시스템을 생성하는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;마운트(Mount)&lt;/b&gt;: 파일시스템을 디렉터리 트리의 특정 위치에 연결하는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디스크 용량 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;df&lt;/code&gt; &amp;mdash; 파일시스템 단위 용량 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Disk Free&lt;/b&gt;의 약자이다. 마운트된 파일시스템별 전체/사용/남은 용량을 표시한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 기본 사용 (사람이 읽기 쉬운 형태) ★★★
df -h
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/sda1        50G   32G   16G  67% /
# /dev/sda2       100G   45G   50G  48% /home
# tmpfs            3.9G  1.2M  3.9G   1% /run

# 파일시스템 타입도 함께 표시
df -hT
# Filesystem      Type  Size  Used Avail Use% Mounted on
# /dev/sda1       ext4   50G   32G   16G  67% /

# 특정 경로가 어느 파일시스템에 속하는지 확인
df -h /var/log/
# &amp;rarr; /var/log/가 위치한 파일시스템의 용량 정보

# inode 사용량 확인 ★ (파일 수 관련 문제 진단)
df -i
# Filesystem      Inodes  IUsed   IFree IUse% Mounted on
# /dev/sda1     3276800 234567 3042233    8% /&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;df&lt;/code&gt;는 &lt;b&gt;파일시스템 단위&lt;/b&gt;로 보여준다. &quot;어느 파티션이 꽉 찼는가&quot;를 파악하는 첫 번째 단계이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;du&lt;/code&gt; &amp;mdash; 디렉터리/파일 단위 용량 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Disk Usage&lt;/b&gt;의 약자이다. &lt;code&gt;df&lt;/code&gt;로 어느 파티션이 문제인지 파악했다면, &lt;code&gt;du&lt;/code&gt;로 &lt;b&gt;어떤 디렉터리가 공간을 많이 차지하는지&lt;/b&gt; 추적한다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 현재 디렉터리의 총 용량
du -sh .

# 하위 디렉터리별 용량 (1단계)
du -h --max-depth=1 /var/

# 용량 순 정렬 (큰 것부터) ★★★
du -sh /var/*/ 2&amp;gt;/dev/null | sort -rh | head -10

# 특정 디렉터리의 총 용량
du -sh /var/log/ /var/cache/ /var/lib/

# 파일 단위까지 표시
du -ah /var/log/ | sort -rh | head -20&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;lsblk&lt;/code&gt; &amp;mdash; 블록 디바이스 목록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템에 연결된 디스크와 파티션의 &lt;b&gt;트리 구조&lt;/b&gt;를 보여준다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda           8:0    0   50G  0 disk
├─sda1        8:1    0   49G  0 part /
└─sda2        8:2    0    1G  0 part [SWAP]
nvme0n1     259:0    0  100G  0 disk
└─nvme0n1p1 259:1    0  100G  0 part /data

# 파일시스템 정보 포함
lsblk -f
# NAME    FSTYPE LABEL UUID                                 MOUNTPOINT
# sda1    ext4         a1b2c3d4-...                         /&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;디스크 용량 문제 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;디스크가 꽉 찼다&quot; &amp;mdash; 체계적인 진단 절차&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Step 1: 어느 파일시스템이 꽉 찼는지 확인
df -h
# Use%가 90% 이상인 파일시스템을 찾는다

# Step 2: 해당 파일시스템에서 큰 디렉터리 찾기
du -h --max-depth=1 / 2&amp;gt;/dev/null | sort -rh | head -10
# /var가 크다면 &amp;rarr; /var 안에서 다시 추적

# Step 3: 범인 디렉터리 좁히기
du -h --max-depth=1 /var/ | sort -rh | head -10
# /var/log가 크다면 &amp;rarr; /var/log 안에서 다시 추적

# Step 4: 큰 파일 직접 찾기
find / -type f -size +100M 2&amp;gt;/dev/null | xargs ls -lhS

# Step 5: 삭제되었지만 공간을 차지하는 파일 확인
lsof +L1 2&amp;gt;/dev/null&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;inode 고갈 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 공간은 남아있는데 &quot;No space left on device&quot; 에러가 발생하면 &lt;b&gt;inode 고갈&lt;/b&gt;을 의심해야 한다. 작은 파일이 수백만 개 생성된 경우에 발생한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# inode 사용량 확인
df -i

# 어느 디렉터리에 파일이 많은지 찾기
find / -xdev -printf '%h\n' 2&amp;gt;/dev/null | sort | uniq -c | sort -rn | head -10
# 각 디렉터리의 파일 수를 세어서 정렬&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Chapter 09. 텍스트 검색 &amp;amp; 치환&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스에서는 거의 모든 것이 텍스트이다.&lt;br /&gt;설정 파일, 로그, 명령어 출력, &lt;code&gt;/proc&lt;/code&gt; 파일시스템 &amp;mdash; 전부 텍스트 스트림이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 처리의 3대 도구&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;핵심 용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grep&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;검색 (Search)&lt;/td&gt;
&lt;td&gt;패턴이 포함된 &lt;b&gt;줄을 찾는다&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;치환 (Replace)&lt;/td&gt;
&lt;td&gt;텍스트를 &lt;b&gt;변환/치환&lt;/b&gt;한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;awk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;추출/가공 (Extract)&lt;/td&gt;
&lt;td&gt;필드 단위로 &lt;b&gt;데이터를 추출/계산&lt;/b&gt;한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정규표현식 (Regular Expression)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt; 모두 정규표현식(regex)을 사용한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메타문자&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;th&gt;매치 대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;임의의 한 문자&lt;/td&gt;
&lt;td&gt;&lt;code&gt;h.t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hat, hot, hit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄의 시작&lt;/td&gt;
&lt;td&gt;&lt;code&gt;^Error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Error로 시작하는 줄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄의 끝&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\.log$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;.log로 끝나는 줄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;앞 문자가 0회 이상&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ab*c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ac, abc, abbc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;앞 문자가 1회 이상 (ERE)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ab+c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;abc, abbc (ac는 안됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;앞 문자가 0회 또는 1회 (ERE)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;colou?r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;color, colour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;문자 클래스 (하나)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[aeiou]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모음 한 글자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[^]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;부정 문자 클래스&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[^0-9]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;숫자가 아닌 문자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`&lt;/td&gt;
&lt;td&gt;`&lt;/td&gt;
&lt;td&gt;OR (ERE)&lt;/td&gt;
&lt;td&gt;`cat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;그룹핑 (ERE)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(ab)+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ab, abab, ababab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;단어 경계&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\berror\b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&quot;error&quot; 단어만 (errors 제외)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BRE vs ERE&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BRE (Basic Regular Expression)&lt;/b&gt;: &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt;의 기본 모드. &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;()&lt;/code&gt; 사용 시 앞에 &lt;code&gt;\&lt;/code&gt;를 붙여야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ERE (Extended Regular Expression)&lt;/b&gt;: &lt;code&gt;grep -E&lt;/code&gt; (또는 &lt;code&gt;egrep&lt;/code&gt;), &lt;code&gt;sed -E&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;의 기본 모드. &lt;code&gt;\&lt;/code&gt; 없이 바로 사용 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# BRE (grep 기본)
grep &quot;error\|fail&quot; logfile        # \| 필요

# ERE (grep -E) &amp;mdash; 더 깔끔하다
grep -E &quot;error|fail&quot; logfile      # | 그대로 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 이유가 없으면 항상 &lt;code&gt;grep -E&lt;/code&gt;를 사용하는 것이 편하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 패턴 모음&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# IP 주소 패턴
[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+

# 날짜 패턴 (YYYY-MM-DD)
[0-9]{4}-[0-9]{2}-[0-9]{2}

# 이메일 패턴 (간략)
[a-zA-Z0-9.]+@[a-zA-Z0-9.]+

# 빈 줄
^$

# 주석 줄 (# 또는 //)
^#|^//

# 숫자만
^[0-9]+$&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;grep&lt;/code&gt; &amp;mdash; 텍스트 검색&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정규표현식 검색&lt;/h3&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# ERE 모드로 여러 패턴 동시 검색
grep -E &quot;error|warning|critical&quot; /var/log/syslog

# IP 주소 추출
grep -Eo &quot;[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+&quot; /var/log/auth.log

# 특정 날짜의 로그만 추출
grep &quot;^2025-03-23&quot; /var/log/app.log

# 특정 패턴 사이의 줄 (시작 패턴 ~ 끝 패턴)
grep -A 100 &quot;BEGIN CERTIFICATE&quot; cert.pem | grep -B 100 &quot;END CERTIFICATE&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;o&lt;/code&gt; 옵션 &amp;mdash; 매치된 부분만 출력&lt;/h3&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# 줄 전체가 아닌, 매치된 부분만 출력
grep -oE &quot;[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+&quot; access.log

# 로그에서 HTTP 상태 코드만 추출
grep -oE &quot;HTTP/[0-9.]+ [0-9]{3}&quot; access.log&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;grep&lt;/code&gt; 조합 패턴&lt;/h3&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# 여러 조건을 AND로 결합 (grep 파이프 체인)
grep &quot;2025-03-23&quot; app.log | grep &quot;ERROR&quot; | grep &quot;database&quot;
# &amp;rarr; 3월 23일 + ERROR + database 포함하는 줄

# 특정 패턴 제외 (NOT)
grep &quot;error&quot; app.log | grep -v &quot;timeout&quot;
# &amp;rarr; error를 포함하지만 timeout은 포함하지 않는 줄

# 전후 맥락과 함께 보기
grep -C 5 &quot;OOM&quot; /var/log/syslog
# &amp;rarr; Out Of Memory 전후 5줄씩&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;sed&lt;/code&gt; &amp;mdash; 스트림 편집기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Stream Editor&lt;/b&gt;의 약자이다.&lt;br /&gt;텍스트를 &lt;b&gt;줄 단위로 읽어서 변환&lt;/b&gt;하는 도구이다. 가장 많이 사용하는 기능은 치환이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;치환&lt;/h3&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 기본 치환: 각 줄에서 첫 번째 매치만 변환
sed 's/old/new/' file.txt

# 모든 매치 변환 (g = global) ★★★
sed 's/old/new/g' file.txt

# 대소문자 무시 (i 플래그)
sed 's/error/ERROR/gi' file.txt

# 특정 줄만 치환
sed '3s/old/new/' file.txt        # 3번째 줄만
sed '1,5s/old/new/g' file.txt     # 1~5번째 줄만&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원본 파일 직접 수정 (&lt;code&gt;i&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 &lt;code&gt;sed&lt;/code&gt;는 결과를 &lt;b&gt;stdout으로 출력&lt;/b&gt;하며, 원본 파일은 변경하지 않는다. &lt;code&gt;-i&lt;/code&gt; 옵션을 사용하면 원본 파일을 직접 수정한다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 원본 파일을 직접 수정 ★★★
sed -i 's/old/new/g' config.yaml

# 백업을 만들면서 수정 (안전한 방법)
sed -i.bak 's/old/new/g' config.yaml
# config.yaml &amp;rarr; 수정됨
# config.yaml.bak &amp;rarr; 원본 백업

# macOS에서는 -i 뒤에 빈 문자열을 반드시 지정해야 한다
sed -i '' 's/old/new/g' config.yaml    # macOS&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활용 예시&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 1. 설정 파일의 특정 값 변경
sed -i 's/^port=.*/port=8080/' app.conf
# port= 으로 시작하는 줄의 값을 8080으로 변경

# 2. 여러 파일에서 일괄 치환
find /etc/myapp -name &quot;*.conf&quot; -exec sed -i 's/old-server/new-server/g' {} +

# 3. 파일에서 특정 범위 추출
sed -n '10,20p' file.txt    # 10~20번째 줄만 출력 (-n + p)

# 4. 환경별 설정 파일 생성
sed 's/{{DB_HOST}}/prod-db.example.com/g; s/{{DB_PORT}}/5432/g' template.conf &amp;gt; prod.conf&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;awk&lt;/code&gt; &amp;mdash; 필드 기반 텍스트 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;awk&lt;/code&gt;는 텍스트를 &lt;b&gt;필드(열) 단위로 분리하여 처리&lt;/b&gt;하는 프로그래밍 언어이다. 이름은 개발자 세 명(Aho, Weinberger, Kernighan)의 이니셜에서 유래했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;awk&lt;/code&gt;는 입력을 &lt;b&gt;줄(record) 단위&lt;/b&gt;로 읽고, 각 줄을 필드(field)로 분리한 뒤 처리한다.&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;입력: &quot;devops  1234  /bin/bash&quot;

$0 = &quot;devops  1234  /bin/bash&quot;    (줄 전체)
$1 = &quot;devops&quot;                     (1번째 필드)
$2 = &quot;1234&quot;                       (2번째 필드)
$3 = &quot;/bin/bash&quot;                  (3번째 필드)
NF = 3                            (필드 수)
NR = (현재 줄 번호)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 필드 구분자는 &lt;b&gt;공백/탭&lt;/b&gt;이다. &lt;code&gt;-F&lt;/code&gt; 옵션으로 변경할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 특정 필드만 출력
awk '{print $1}' /etc/passwd           # 첫 번째 필드 (구분자: 공백)
awk -F: '{print $1}' /etc/passwd       # 구분자를 :으로 지정 &amp;rarr; 사용자명

# 여러 필드 출력
awk -F: '{print $1, $3}' /etc/passwd   # 사용자명, UID
awk -F: '{print $1 &quot;:&quot; $3}' /etc/passwd  # 사용자명:UID (포맷 지정)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건부 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;awk&lt;/code&gt;는 &lt;b&gt;패턴 { 동작 }&lt;/b&gt; 형식으로 조건부 처리를 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 특정 조건에 맞는 줄만 처리
awk -F: '$3 &amp;gt;= 1000 {print $1, $3}' /etc/passwd
# UID가 1000 이상인 사용자만 출력 (일반 사용자)

# 특정 문자열이 포함된 줄
awk '/error/ {print}' /var/log/syslog
# grep &quot;error&quot;와 동일하지만, awk는 필드 추출까지 가능

# 특정 필드가 조건을 만족하는 줄
awk '$9 == 500 {print $7}' access.log
# HTTP 상태 코드(9번째 필드)가 500인 줄에서 URL(7번째 필드)만 추출&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;계산과 집계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;awk&lt;/code&gt;의 가장 강력한 기능은 &lt;b&gt;데이터 집계&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 합계 계산
awk '{sum += $1} END {print sum}' numbers.txt

# 평균 계산
awk '{sum += $1; count++} END {print sum/count}' numbers.txt

# 최대값
awk 'BEGIN {max=0} $1 &amp;gt; max {max=$1} END {print max}' numbers.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;BEGIN&lt;/code&gt;과 &lt;code&gt;END&lt;/code&gt; 블록:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;BEGIN { ... }&lt;/code&gt;: 입력을 처리하기 전에 한 번 실행된다 (초기화)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;END { ... }&lt;/code&gt;: 모든 입력을 처리한 후에 한 번 실행된다 (결과 출력)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예시&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 1. Nginx 액세스 로그에서 IP별 요청 수 집계
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

# 2. 디스크 사용률이 80% 이상인 파티션 알림
df -h | awk 'NR&amp;gt;1 &amp;amp;&amp;amp; +$5 &amp;gt;= 80 {print &quot;WARNING:&quot;, $6, &quot;is&quot;, $5, &quot;full&quot;}'

# 3. 프로세스별 메모리 사용량 합계
ps aux | awk 'NR&amp;gt;1 {mem[$11] += $6} END {for (p in mem) print mem[p]/1024 &quot;MB&quot;, p}' | sort -rn | head -10

# 4. CSV에서 특정 열의 합계
awk -F',' '{sum += $3} END {printf &quot;Total: %.2f\n&quot;, sum}' sales.csv

# 5. 로그에서 시간대별 에러 수 집계
grep &quot;ERROR&quot; app.log | awk '{print substr($1,1,13)}' | sort | uniq -c
# 시간(hour) 단위로 에러 발생 빈도 확인&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Learning Log</category>
      <category>Linux</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/39</guid>
      <comments>https://allluck777.tistory.com/39#entry39comment</comments>
      <pubDate>Wed, 25 Mar 2026 09:05:50 +0900</pubDate>
    </item>
    <item>
      <title>[멋사 클라우드 5기] Day 31 &amp;amp; 32 - OSI 7 Layers</title>
      <link>https://allluck777.tistory.com/38</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;OSI 7 Layers&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 문제가 발생했을 때 &quot;인터넷이 안 돼요&quot;라는 말만으로는 원인을 파악할 수 없다. &lt;br /&gt;DNS가 안 되는 건지, 방화벽이 막는 건지, 물리 케이블이 빠진 건지.&lt;br /&gt;문제의 위치를 특정하려면 네트워크가 어떤 단계로 동작하는지 이해해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI(Open Systems Interconnection) 모델은 네트워크 통신을 7개의 계층으로 분리한 참조 모델이다. &lt;br /&gt;1984년 ISO(International Organization for Standardization)가 제정했다. &lt;br /&gt;실제 인터넷은 TCP/IP 4계층 모델로 동작하지만, OSI 7계층은 &lt;b&gt;문제를 진단하고 설명하는 공통 언어&lt;/b&gt;로서 표준이다.&lt;/p&gt;
&lt;!-- Titles --&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-03-16 at 11.36.13 PM.png&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;1442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ERzFf/dJMcajg9B2v/utm3hZIMV10Kw1R52nQkRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ERzFf/dJMcajg9B2v/utm3hZIMV10Kw1R52nQkRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ERzFf/dJMcajg9B2v/utm3hZIMV10Kw1R52nQkRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FERzFf%2FdJMcajg9B2v%2Futm3hZIMV10Kw1R52nQkRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2008&quot; height=&quot;1442&quot; data-filename=&quot;Screenshot 2026-03-16 at 11.36.13 PM.png&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;1442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layer 1 &amp;mdash; Physical (물리 계층)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리 계층은 &lt;b&gt;0과 1의 비트(bit)를 실제 전기 신호, 광 신호, 무선 전파로 변환하여 전송하는 계층&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 계층은 &quot;데이터&quot;가 뭔지 모른다. 단지 비트 스트림을 물리적 매체를 통해 보내고 받을 뿐이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구성 요소&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전송 매체:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UTP 케이블(Cat5e, Cat6, Cat6a): 구리선으로 전기 신호를 전달한다. 사무실 LAN에 가장 많이 쓰인다.&lt;/li&gt;
&lt;li&gt;광섬유(Fiber Optic): 빛으로 데이터를 전달. 장거리, 고속 통신에 사용한다. 데이터센터 간 연결의 핵심이다.&lt;/li&gt;
&lt;li&gt;무선(Wi-Fi, 5G): 전파로 데이터를 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장비:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리피터(Repeater): 약해진 신호를 증폭한다.&lt;/li&gt;
&lt;li&gt;허브(Hub): 들어온 신호를 연결된 모든 포트로 복사하여 내보낸다. 현재는 거의 사용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 단위&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비트(Bit)&lt;/b&gt; &amp;mdash; 0 또는 1. 물리 계층이 다루는 최소 단위이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layer 2 &amp;mdash; Data Link (데이터링크 계층)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터링크 계층은 &lt;b&gt;같은 네트워크(LAN) 안에서 장치 간 통신을 담당&lt;/b&gt;한다. 물리 계층의 비트 스트림을 의미 있는 &lt;b&gt;프레임(Frame)&lt;/b&gt; 단위로 구성하고, &lt;b&gt;MAC 주소&lt;/b&gt;를 사용하여 특정 장치를 식별한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MAC 주소&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MAC(Media Access Control) 주소는 네트워크 인터페이스 카드(NIC)에 부여된 고유한 48비트 하드웨어 주소이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;예: 00:1A:2B:3C:4D:5E

앞 24비트(00:1A:2B) = OUI (제조사 식별자, IEEE가 할당)
뒤 24비트(3C:4D:5E) = 제조사가 부여한 고유 번호
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MAC 주소는 &lt;b&gt;로컬 네트워크 내에서만 유효&lt;/b&gt;하다. 라우터를 넘어가면 MAC 주소는 바뀐다(라우터가 자신의 MAC으로 교체한다).&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ARP (Address Resolution Protocol)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 주소는 알지만 MAC 주소를 모를 때 ARP를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;1. PC-A가 &quot;192.168.1.10의 MAC 주소가 뭐야?&quot; 라고 브로드캐스트(FF:FF:FF:FF:FF:FF)를 보낸다.
2. 해당 IP를 가진 PC-B가 &quot;내 MAC은 00:1A:2B:3C:4D:5E야&quot; 라고 유니캐스트로 응답한다.
3. PC-A는 이 매핑을 ARP 테이블에 캐시한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장비&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스위치(Switch):&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MAC 주소 테이블(CAM 테이블)을 학습하여, 프레임을 해당 포트로만 전달한다.&lt;/li&gt;
&lt;li&gt;허브와 달리 불필요한 트래픽을 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브리지(Bridge):&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 개의 네트워크 세그먼트를 연결한다.&lt;/li&gt;
&lt;li&gt;현재는 스위치에 기능이 통합되어 거의 쓰이지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layer 3 &amp;mdash; Network (네트워크 계층)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 계층은 &lt;b&gt;서로 다른 네트워크 간의 통신을 담당&lt;/b&gt;한다. L2가 &quot;같은 방 안에서 누구에게 전달할까&quot;를 결정한다면, L3는 &quot;어느 방(네트워크)으로 보낼까&quot;를 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 기능 두 가지: 논리적 주소 지정(IP 주소)과 &lt;b&gt;라우팅(최적 경로 선택)&lt;/b&gt;.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;IP 주소&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IPv4:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;192.168.1.100 (32비트, 약 43억 개)

네트워크 부분 + 호스트 부분으로 나뉜다.
서브넷 마스크가 경계를 결정한다.

예: 192.168.1.100/24
- /24 = 서브넷 마스크 255.255.255.0
- 네트워크: 192.168.1.0
- 호스트 범위: 192.168.1.1 ~ 192.168.1.254
- 브로드캐스트: 192.168.1.255
- 사용 가능 호스트: 254개 (2^8 - 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IPv6:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2001:0db8:85a3:0000:0000:8a2e:0370:7334 (128비트)
축약: 2001:db8:85a3::8a2e:370:7334

사실상 무한한 주소 공간을 제공한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;서브넷팅 (Subnetting)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 네트워크를 작은 단위로 나누는 것.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;VPC CIDR: 10.0.0.0/16 (65,536개 IP)

이것을 서브넷으로 나눈다:
├── 10.0.1.0/24  &amp;rarr; Public Subnet AZ-a  (256개 IP)
├── 10.0.2.0/24  &amp;rarr; Public Subnet AZ-b  (256개 IP)
├── 10.0.10.0/24 &amp;rarr; Private Subnet AZ-a (256개 IP)
├── 10.0.20.0/24 &amp;rarr; Private Subnet AZ-b (256개 IP)
└── 나머지        &amp;rarr; 향후 확장용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CIDR(Classless Inter-Domain Routing) 계산:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/24 = 256 IP (호스트 254개)
/25 = 128 IP (호스트 126개)
/26 = 64 IP  (호스트 62개)
/27 = 32 IP  (호스트 30개)
/28 = 16 IP  (호스트 14개)

공식: 사용 가능 호스트 = 2^(32 - 서브넷 비트 수) - 2
AWS에서는 서브넷당 5개 IP를 추가로 예약한다(네트워크, 라우터, DNS, 예약, 브로드캐스트).
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;라우팅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷이 목적지까지 도달하는 최적 경로를 선택하는 과정이다. &lt;br /&gt;라우터는 패킷의 목적지 IP를 라우팅 테이블과 대조하여 가장 구체적인(longest prefix match) 경로로 전달한다.&lt;/p&gt;
&lt;!-- Packet source --&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2026-03-16 at 11.35.18 PM.png&quot; data-origin-width=&quot;2912&quot; data-origin-height=&quot;1404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qJsXJ/dJMcagLyYtd/J4D88nhxwLrRLCNIe8TGFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qJsXJ/dJMcagLyYtd/J4D88nhxwLrRLCNIe8TGFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qJsXJ/dJMcagLyYtd/J4D88nhxwLrRLCNIe8TGFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqJsXJ%2FdJMcagLyYtd%2FJ4D88nhxwLrRLCNIe8TGFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2912&quot; height=&quot;1404&quot; data-filename=&quot;Screenshot 2026-03-16 at 11.35.18 PM.png&quot; data-origin-width=&quot;2912&quot; data-origin-height=&quot;1404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;NAT (Network Address Translation)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사설 IP를 공인 IP로 변환하는 기술이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;내부 서버(10.0.1.50) &amp;rarr; NAT Gateway &amp;rarr; 인터넷(52.78.100.200으로 변환)

- SNAT(Source NAT): 출발지 IP 변환. 사설 &amp;rarr; 공인. AWS NAT Gateway가 이 역할이다.
- DNAT(Destination NAT): 목적지 IP 변환. 공인 &amp;rarr; 사설. 포트 포워딩이 대표적이다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;패킷 구조&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;┌──────────┬──────────┬─────┬──────────┬──────────┬─────────┐
│ Version  │ IHL      │ TTL │ Protocol │ Src IP   │ Dst IP  │
│ (4/6)    │ 헤더길이   │     │ TCP=6    │          │         │
│          │          │     │ UDP=17   │          │         │
└──────────┴──────────┴─────┴──────────┴──────────┴─────────┘

- TTL(Time To Live): 라우터를 지날 때마다 1씩 감소. 0이 되면 패킷 폐기. 무한 루프 방지 장치이다.
- Protocol: 상위 계층 프로토콜 식별 (6=TCP, 17=UDP, 1=ICMP)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layer 4 &amp;mdash; Transport (전송 계층)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전송 계층은 &lt;b&gt;종단 간(End-to-End) 데이터 전송의 신뢰성과 흐름을 제어&lt;/b&gt;한다. &quot;어떻게 안전하게 보낼까&quot;와 &quot;어떤 애플리케이션에 전달할까&quot;를 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 개념: &lt;b&gt;포트 번호&lt;/b&gt;로 애플리케이션을 식별하고, &lt;b&gt;TCP 또는 UDP&lt;/b&gt; 프로토콜로 전송 방식을 결정한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;포트 번호&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 IP에서 여러 서비스를 구분하는 논리적 식별자이다. 0~65535 범위를 가진다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Well-Known Ports (0-1023): 시스템 서비스용
  22   = SSH
  53   = DNS
  80   = HTTP
  443  = HTTPS
  3306 = MySQL
  5432 = PostgreSQL
  6379 = Redis

Registered Ports (1024-49151): 등록된 애플리케이션
  3000 = 개발 서버 (관례)
  8080 = 대체 HTTP (관례)
  8443 = 대체 HTTPS (관례)
  27017 = MongoDB

Dynamic/Ephemeral Ports (49152-65535): 클라이언트가 임시로 사용
  브라우저가 웹서버에 접속할 때, 출발지 포트로 이 범위의 임의 번호를 사용한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소켓(Socket)&lt;/b&gt; = IP 주소 + 포트 번호 + 프로토콜. 네트워크 통신의 종단점이다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;192.168.1.100:443/TCP &amp;rarr; 하나의 소켓
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TCP (Transmission Control Protocol)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연결 지향(Connection-Oriented), 신뢰성 있는&lt;/b&gt; 전송 프로토콜이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-Way Handshake (연결 수립):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                    Server
  │                         │
  │──── SYN (seq=100) ─────&amp;rarr;│   1. &quot;연결하고 싶다&quot; (SYN 플래그)
  │                         │
  │&amp;larr;── SYN-ACK (seq=300,    │   2. &quot;좋다, 나도 준비됐다&quot; (SYN+ACK)
  │     ack=101) ───────────│
  │                         │
  │──── ACK (ack=301) ─────&amp;rarr;│   3. &quot;확인, 시작하자&quot; (ACK)
  │                         │
  │     연결 수립 완료 (ESTABLISHED)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정이 완료되어야 데이터를 주고받을 수 있다. 모든 HTTP, SSH, DB 연결이 이 핸드셰이크로 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4-Way Handshake (연결 종료):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                    Server
  │──── FIN ───────────────&amp;rarr;│   1. &quot;보낼 데이터 다 보냈다&quot;
  │&amp;larr;──── ACK ───────────────│   2. &quot;알겠다&quot;
  │&amp;larr;──── FIN ───────────────│   3. &quot;나도 다 보냈다&quot;
  │──── ACK ───────────────&amp;rarr;│   4. &quot;알겠다. 연결 종료&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TCP의 신뢰성 보장 메커니즘:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시퀀스 번호(Sequence Number):&lt;/b&gt; 모든 바이트에 번호를 매긴다. 수신 측은 이를 통해 데이터를 올바른 순서로 재조립한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확인 응답(ACK):&lt;/b&gt; 수신한 데이터에 대해 확인 메시지를 보낸다. ACK가 안 오면 재전송한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;흐름 제어(Flow Control):&lt;/b&gt; 수신 측의 처리 능력에 맞춰 전송 속도를 조절한다. Window Size로 제어한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;혼잡 제어(Congestion Control):&lt;/b&gt; 네트워크 혼잡 시 전송 속도를 줄인다. Slow Start, Congestion Avoidance 등의 알고리즘을 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;체크섬(Checksum):&lt;/b&gt; 데이터 무결성을 검증한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;UDP (User Datagram Protocol)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비연결(Connectionless), 신뢰성 없는&lt;/b&gt; 전송 프로토콜이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client                   Server
  │                        │
  │──── 데이터 ─────────────&amp;rarr;│   핸드셰이크 없이 바로 전송
  │──── 데이터 ─────────────&amp;rarr;│   도착 확인도 하지 않음
  │──── 데이터 ─────────────&amp;rarr;│   순서 보장도 하지 않음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UDP를 쓰는 이유:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠르다. 핸드셰이크 오버헤드가 없다.&lt;/li&gt;
&lt;li&gt;실시간성이 중요한 서비스에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UDP 사용 사례:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DNS 조회 (53번 포트): 작은 요청/응답이므로 TCP 오버헤드가 불필요하다.&lt;/li&gt;
&lt;li&gt;실시간 스트리밍, VoIP: 약간의 패킷 손실보다 지연이 더 치명적이다.&lt;/li&gt;
&lt;li&gt;게임 서버: 실시간 상태 업데이트에 UDP가 적합하다.&lt;/li&gt;
&lt;li&gt;DHCP: IP 할당 과정에서 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TCP vs UDP 비교&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;특성            TCP                    UDP
──────────────────────────────────────────────────
연결 방식       연결 지향 (3-way)       비연결
신뢰성         보장 (ACK, 재전송)       미보장
순서 보장       보장                    미보장
속도           상대적으로 느림          빠름
헤더 크기       20~60 바이트            8 바이트
사용 사례       웹, 이메일, 파일전송     DNS, 스트리밍, 게임
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layer 5 &amp;mdash; Session (세션 계층)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 계층은 &lt;b&gt;통신 세션의 수립, 유지, 종료를 관리&lt;/b&gt;한다. 두 장치 간의 대화(dialog)를 제어하고, 데이터 교환의 동기화 지점을 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 네트워크에서 L5는 독립된 계층으로 명확히 구분되기 어렵다. 대부분의 세션 관리 기능은 L4(TCP)나 L7(HTTP) 안에 녹아 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;세션의 의미&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;웹 브라우저로 쇼핑몰에 로그인한 상황을 생각해 보자:

1. 세션 수립: 로그인 성공 &amp;rarr; 서버가 세션 ID 발급
2. 세션 유지: 페이지를 이동해도 로그인 상태 유지
3. 세션 종료: 로그아웃 또는 타임아웃 &amp;rarr; 세션 파기
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;세션 계층의 역할&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대화 제어(Dialog Control):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전이중(Full-Duplex): 양방향 동시 통신 (전화 통화)&lt;/li&gt;
&lt;li&gt;반이중(Half-Duplex): 한 번에 한쪽만 통신 (무전기)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동기화(Synchronization):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대용량 파일 전송 시 체크포인트를 설정한다.&lt;/li&gt;
&lt;li&gt;전송 중 끊기면 처음부터가 아니라 마지막 체크포인트부터 재개한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션 복구(Recovery):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연결이 끊겼을 때 세션을 복원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관련 프로토콜/기술&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;NetBIOS:&lt;/b&gt; Windows 네트워크 세션 관리에 사용했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layer 6 &amp;mdash; Presentation (표현 계층)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현 계층은 &lt;b&gt;데이터의 형식(포맷) 변환, 암호화/복호화, 압축/해제를 담당&lt;/b&gt;한다. 애플리케이션이 이해할 수 있는 형태로 데이터를 변환하는 &quot;번역가&quot; 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L5와 마찬가지로 현대 네트워크에서는 독립 계층으로 존재하기보다 L7에 통합된 경우가 많다. 하지만 TLS/SSL이 이 계층의 대표적 기능이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;세 가지 핵심 기능&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 데이터 변환 (Translation):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;- 문자 인코딩: UTF-8, ASCII, ISO-8859-1 간 변환
- 데이터 직렬화: JSON, XML, Protocol Buffers, YAML
- 이미지 포맷: JPEG, PNG, WebP
- 영상 코덱: H.264, H.265, VP9

예: 한국어가 포함된 JSON을 UTF-8로 인코딩하여 전송하고,
    수신 측에서 디코딩한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 암호화/복호화 (Encryption/Decryption):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;TLS(Transport Layer Security) &amp;mdash; 이전 이름: SSL

TLS 핸드셰이크 과정 (TLS 1.3 기준):

Client                           Server
  │                                │
  │── ClientHello ────────────────&amp;rarr;│  지원하는 암호 스위트, TLS 버전 전달
  │   (+ 키 공유)                    │
  │                                │
  │&amp;larr;── ServerHello ────────────────│  선택한 암호 스위트, 서버 인증서 전달
  │    (+ 키 공유, 인증서)            │
  │                                │
  │   [양측이 공유 비밀키 계산]          │
  │                                │
  │&amp;larr;&amp;rarr; 암호화된 데이터 통신 &amp;larr;&amp;rarr;───────────│  대칭키로 암호화된 통신 시작
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인증서 체인:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;Root CA (DigiCert, Let's Encrypt 등)
  └── Intermediate CA
        └── 서버 인증서 (example.com)

브라우저/OS에 Root CA가 내장되어 있다.
서버 인증서 &amp;rarr; Intermediate CA &amp;rarr; Root CA 순으로 검증한다.
하나라도 검증 실패 시 &quot;이 사이트는 안전하지 않습니다&quot; 경고가 뜬다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 압축/해제 (Compression/Decompression):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;- gzip, brotli: HTTP 응답 압축
- 이미지 손실/무손실 압축
- 데이터 전송량을 줄여 속도를 높인다.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Layer 7 &amp;mdash; Application (응용 계층)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응용 계층은 &lt;b&gt;사용자와 가장 가까운 계층으로, 네트워크 서비스를 직접 제공&lt;/b&gt;한다. 웹 브라우저, 이메일 클라이언트, API 등 사용자가 실제로 사용하는 애플리케이션 프로토콜이 이 계층에서 동작한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 프로토콜&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP/HTTPS (포트 80/443):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;웹 통신의 기본. 요청-응답 모델이다.

요청:
GET /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: application/json

응답:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 234

{&quot;users&quot;: [...]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DNS (포트 53):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;도메인 이름을 IP 주소로 변환하는 프로토콜이다.

www.example.com &amp;rarr; ?

1. 브라우저 캐시 확인
2. OS 캐시 확인 (/etc/hosts 포함)
3. Local DNS 서버(리졸버)에 질의
4. Root DNS &amp;rarr; .com TLD DNS &amp;rarr; example.com 권한 DNS
5. IP 주소 반환: 93.184.216.34

레코드 타입:
  A      = 도메인 &amp;rarr; IPv4
  AAAA   = 도메인 &amp;rarr; IPv6
  CNAME  = 도메인 &amp;rarr; 다른 도메인 (별칭)
  MX     = 메일 서버
  NS     = 네임서버
  TXT    = 텍스트 (SPF, DKIM, 도메인 검증 등)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SSH (포트 22):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# 원격 서버 접속
ssh -i key.pem ubuntu@10.0.1.50

# 포트 포워딩 (로컬 &amp;rarr; 원격)
ssh -L 3306:rds-endpoint:3306 bastion-host
# 로컬 3306 포트로 접속하면 bastion을 통해 RDS에 연결된다.

# 포트 포워딩 (원격 &amp;rarr; 로컬)
ssh -R 8080:localhost:3000 remote-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기타 주요 프로토콜:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SMTP(25)/IMAP(143)/POP3(110): 이메일&lt;/li&gt;
&lt;li&gt;FTP(21)/SFTP(22): 파일 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OSI 모델 vs TCP/IP 모델&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 인터넷은 TCP/IP 4계층 모델로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;OSI 7 계층                TCP/IP 4 계층          주요 프로토콜
───────────────────────────────────────────────────────────
L7 Application  ┐
L6 Presentation ├───&amp;rarr;  Application Layer      HTTP, DNS, SSH,
L5 Session      ┘                             TLS, SMTP, FTP

L4 Transport    ────&amp;rarr;  Transport Layer        TCP, UDP

L3 Network      ────&amp;rarr;  Internet Layer         IP, ICMP, ARP

L2 Data Link    ┐
L1 Physical     ┘───&amp;rarr;  Network Access Layer   Ethernet, Wi-Fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Learning Log</category>
      <category>network</category>
      <category>osi 7</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/38</guid>
      <comments>https://allluck777.tistory.com/38#entry38comment</comments>
      <pubDate>Mon, 16 Mar 2026 23:23:25 +0900</pubDate>
    </item>
    <item>
      <title>[멋사 클라우드 5기] Day 29 &amp;amp; 30 - 게시판 구현을 통해 확인한 Spring Data JPA</title>
      <link>https://allluck777.tistory.com/37</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;findById vs getReferenceById 차이&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메서드&lt;/th&gt;
&lt;th&gt;사용하는 곳&lt;/th&gt;
&lt;th&gt;대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findById&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getPost()&lt;/code&gt;, &lt;code&gt;getPostWithComments()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Post 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getReferenceById&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;registerPost()&lt;/code&gt;, &lt;code&gt;updatePost()&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;User, Post&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;findById &amp;mdash; 즉시 SELECT 실행&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// PostService.java:29-33
@Transactional
public PostDto getPost(Long pid) {
    return postRepository.findById(pid)           // &amp;larr; 이 시점에 SELECT 실행
                         .map(PostDto::from)
                         .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;해당 게시글 존재 x&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;findById(pid)&lt;/code&gt;가 호출되는 &lt;b&gt;그 즉시&lt;/b&gt; DB에 SELECT 쿼리를 날린다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- findById(pid) 호출 시 즉시 실행
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by,
       p1_0.created_date, p1_0.modified_by, p1_0.modified_date, p1_0.title,
       u1_0.uid, u1_0.created_by, u1_0.created_date, u1_0.email,
       u1_0.modified_by, u1_0.modified_date, u1_0.password, u1_0.role_type, u1_0.username
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ?&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Optional&amp;lt;Post&amp;gt;&lt;/code&gt;를 반환한다.&lt;/li&gt;
&lt;li&gt;DB에 해당 row가 없으면 &lt;code&gt;Optional.empty()&lt;/code&gt;가 온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과:&lt;/b&gt; 실제 데이터가 채워진 진짜 Post 엔티티 객체를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;getReferenceById &amp;mdash; SELECT 없이 프록시 반환&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// PostService.java:51
User user = userRepository.getReferenceById(postDto.getUserDto().getUid());
// &amp;uarr; 이 시점에서 SELECT 실행 안 함! 프록시 객체만 반환&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// PostService.java:60
Post post = postRepository.getReferenceById(pid);
// &amp;uarr; 역시 SELECT 실행 안 함! 프록시 객체만 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;getReferenceById()&lt;/code&gt;는 호출 시점에 &lt;b&gt;DB에 쿼리를 보내지 않는다.&lt;/b&gt;&lt;br /&gt;대신 Hibernate가 만든 &lt;b&gt;프록시(Proxy) 객체&lt;/b&gt;를 반환한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프록시 객체란?&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;실제 User 객체:
┌──────────────────────┐
│ uid = &quot;user1&quot;        │  &amp;larr; 모든 필드에 실제 값이 들어있음
│ username = &quot;홍길동&quot;   │
│ email = &quot;a@b.com&quot;    │
│ password = &quot;1234&quot;    │
└──────────────────────┘

프록시 User 객체:
┌──────────────────────┐
│ uid = &quot;user1&quot;        │  &amp;larr; ID만 가지고 있음 (파라미터로 넘긴 값)
│ username = ???       │  &amp;larr; 나머지 필드는 비어있음
│ email = ???          │
│ password = ???       │
└──────────────────────┘
   &amp;darr; username에 접근하면?
   그제서야 SELECT 쿼리 실행!&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시는 실제 엔티티 클래스를 &lt;b&gt;상속&lt;/b&gt;한 가짜 객체다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ID 값만&lt;/b&gt; 가지고 있고, 나머지 필드는 비어있다.&lt;/li&gt;
&lt;li&gt;프록시의 ID가 아닌 다른 필드(예: &lt;code&gt;user.getUsername()&lt;/code&gt;)에 접근하면, 그 시점에 SELECT가 실행된다.&lt;/li&gt;
&lt;li&gt;DB에 해당 row가 없으면 &lt;code&gt;EntityNotFoundException&lt;/code&gt;이 발생한다 (Optional이 아님).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 어떤 것을 쓸까?&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;적절한 메서드&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;조회한 데이터를 &lt;b&gt;화면에 보여줘야&lt;/b&gt; 할 때&lt;/td&gt;
&lt;td&gt;&lt;code&gt;findById&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실제 데이터가 필요하므로 즉시 SELECT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FK 설정만 필요할 때 &lt;br /&gt;(INSERT/UPDATE 시 연관 엔티티)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getReferenceById&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ID만 있으면 되므로 SELECT 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;존재 여부를 확인해야 할 때&lt;/td&gt;
&lt;td&gt;&lt;code&gt;findById&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;프록시는 존재 여부를 즉시 알 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(1) registerPost&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// User의 실제 데이터가 필요 없다.
// Post 테이블의 uid 컬럼에 FK만 넣으면 된다.
// &amp;rarr; getReferenceById가 적절하다.
User user = userRepository.getReferenceById(postDto.getUserDto().getUid());
Post post = postDto.toEntity(user);  // Post의 user 필드에 프록시를 세팅
postRepository.save(post);           // INSERT 시 프록시의 uid만 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(2) getPost&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// Post의 모든 필드를 화면에 보여줘야 한다.
// &amp;rarr; findById가 적절하다.
return postRepository.findById(pid)
                     .map(PostDto::from)  // Post의 모든 필드 + User의 모든 필드에 접근&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Lazy Loading vs Eager Loading&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JPA의 기본 fetch 전략&lt;/h3&gt;
&lt;table style=&quot;height: 100px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;어노테이션&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;기본값&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;code&gt;@ManyToOne&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;EAGER&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&quot;Many&quot; 쪽에서 &quot;One&quot;을 조회 &amp;rarr; 항상 1건이므로 부담 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;code&gt;@OneToMany&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;LAZY&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&quot;One&quot; 쪽에서 &quot;Many&quot;를 조회 &amp;rarr; 수백~수천 건일 수 있어 위험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;code&gt;@OneToOne&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;EAGER&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;1건이므로 부담 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;code&gt;@ManyToMany&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;LAZY&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;대량 데이터 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Post &amp;amp; PostComment Entity의 fetch 전략&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// Post.java:46-48
@ManyToOne(cascade = CascadeType.PERSIST)   // &amp;larr; fetch 속성 생략 = 기본값 EAGER
@JoinColumn(name = &quot;uid&quot;)
private User user;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Post &amp;rarr; User: EAGER&lt;/b&gt; (fetch 속성을 지정하지 않았으므로 &lt;code&gt;@ManyToOne&lt;/code&gt;의 기본값인 EAGER 적용)&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Post.java:43-44
@OneToMany(mappedBy = &quot;post&quot;, cascade = CascadeType.ALL)   // &amp;larr; 기본값 LAZY
private final Set&amp;lt;PostComment&amp;gt; postComments = new LinkedHashSet&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Post &amp;rarr; PostComment: LAZY&lt;/b&gt; (&lt;code&gt;@OneToMany&lt;/code&gt;의 기본값)&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// PostComment.java:25-27
@ManyToOne(optional = false)   // &amp;larr; 기본값 EAGER
@JoinColumn(name = &quot;pid&quot;, referencedColumnName = &quot;pid&quot;)
private Post post;

// PostComment.java:29-31
@ManyToOne(optional = false)   // &amp;larr; 기본값 EAGER
@JoinColumn(name = &quot;uid&quot;, referencedColumnName = &quot;uid&quot;)
private User user;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PostComment &amp;rarr; Post: EAGER&lt;/b&gt;&lt;br /&gt;&lt;b&gt;PostComment &amp;rarr; User: EAGER&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Post 조회 시:
  ├── User (EAGER)         &amp;rarr; Post SELECT할 때 LEFT JOIN으로 함께 가져옴
  └── PostComment (LAZY)   &amp;rarr; postComments에 접근하기 전까지 SELECT 안 함
                               접근하면 그때 SELECT 실행

PostComment 조회 시 (이론상):
  ├── Post (EAGER)         &amp;rarr; PostComment를 findById()로 직접 조회하면 LEFT JOIN (이론상!)
  └── User (EAGER)         &amp;rarr; PostComment를 findById()로 직접 조회하면 LEFT JOIN&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서의 실제 동작:&lt;br /&gt;사실 PostComment를 findById()로 직접 조회하는 코드는 없다.&lt;br /&gt;PostComment가 로딩되는 경우는 두 가지뿐이다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;post.getPostComments()&lt;/code&gt; &amp;mdash; Lazy 로딩 시&lt;br /&gt;&amp;rarr; Post는 이미 영속성 컨텍스트에 존재하므로 &lt;b&gt;Post JOIN 없음&lt;/b&gt;. User만 LEFT JOIN.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deleteByIdAndPost_IdAndUser_Uid()&lt;/code&gt; &amp;mdash; 댓글 삭제 시&lt;br /&gt;&amp;rarr; Post, User JOIN이 보이지만 EAGER 로딩이 아니라 &lt;b&gt;WHERE 조건 검색용&lt;/b&gt; JOIN.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 프로젝트에서 PostComment &amp;rarr; Post EAGER LEFT JOIN은 &lt;b&gt;실제로 발생하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lazy 로딩이 실제 발생하는 시점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 참고로&amp;nbsp;&lt;b&gt;getPost&lt;/b&gt;&amp;nbsp;메서드는 &lt;br /&gt;&lt;span&gt;상세페이지(&lt;/span&gt;&lt;span&gt;GET /posts/{pid}&lt;/span&gt;&lt;span&gt;)에서 &lt;br /&gt;수정페이지(&lt;/span&gt;&lt;span&gt;GET &lt;/span&gt;&lt;span&gt;/posts/{pid}/form&lt;/span&gt;&lt;span&gt;)로 넘어갈 때 &lt;/span&gt;&lt;span&gt;호출된다. (수정 폼에서는 &lt;/span&gt;&lt;span&gt;댓글이 필요 없다)&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 영속성 컨텍스트가 공유되지는 않는다.&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;HTTP 요청마다 영속성 컨텍스트가 새로 만들어지기 때문이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;[1번째 요청] GET /posts/1&lt;span&gt;&amp;nbsp; &lt;/span&gt;(상세페이지)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; @Transactional 시작 &amp;rarr; 영속성 컨텍스트 생성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; getPostWithComments(1) &amp;rarr; SELECT Post, SELECT PostComment&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; @Transactional 종료 &amp;rarr; 영속성 컨텍스트 소멸 &amp;larr; 여기서 전부 사라짐&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;[2번째 요청] GET /posts/1/form&lt;span&gt;&amp;nbsp; &lt;/span&gt;(수정 폼)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; @Transactional 시작 &amp;rarr; 새 영속성 컨텍스트 생성 (1번 요청과 무관)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; getPost(1) &amp;rarr; SELECT Post JOIN User &amp;larr; DB에 다시 쿼리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&amp;rarr; @Transactional 종료 &amp;rarr; 영속성 컨텍스트 소멸&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;영속성 컨텍스트는 &lt;b&gt;하나의&lt;/b&gt; &lt;b&gt;트랜잭션(=&lt;/b&gt; &lt;b&gt;하나의&lt;/b&gt; &lt;b&gt;HTTP&lt;/b&gt; &lt;b&gt;요청)&lt;/b&gt; &lt;b&gt;안에서만&lt;/b&gt; 살아있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;브라우저에서 페이지를 이동하면 완전히 새로운 요청이므로, 이전에 조회했던&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Post/User 캐시는 이미 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;getPost()&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// PostService.java:30

postRepository.findById(pid)
// &amp;rarr; SELECT post LEFT JOIN user 실행 (User는 EAGER이므로 JOIN)
// &amp;rarr; PostComment는 LAZY이므로 아직 로딩 안 됨

.map(PostDto::from)
// &amp;rarr; PostDto.from() 내부에서 post.getUser() 호출 (이미 JOIN으로 로드됨, 추가 쿼리 없음)
// &amp;rarr; PostComment에는 접근하지 않으므로 PostComment SELECT 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;getPostWithComments()&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// PostService.java:37

postRepository.findById(pid)
// &amp;rarr; SELECT post LEFT JOIN user 실행

// PostService.java:38
.map(PostWithCommentsDto::from)
// &amp;rarr; PostWithCommentsDto.from() 내부:

// PostWithCommentsDto.java:42
post.getPostComments().stream()   // &amp;larr; 이 순간! Lazy 로딩 발동!
// &amp;rarr; SELECT post_comment LEFT JOIN user WHERE pid = ? 실행
// &amp;rarr; PostComment들이 이 시점에 DB에서 로딩됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심:&lt;/b&gt; &lt;code&gt;post.getPostComments()&lt;/code&gt;를 호출하는 &lt;b&gt;그 순간&lt;/b&gt;에 PostComment를 조회하는 SELECT가 실행된다. &lt;code&gt;getPost()&lt;/code&gt;에서는 PostComment에 접근하지 않으므로 이 쿼리가 실행되지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행되는 SQL 추적&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;getPost() &amp;mdash; 게시글 단건 조회&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 코드&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// PostService.java:28-33
@Transactional
public PostDto getPost(Long pid) {
    return postRepository.findById(pid)
                         .map(PostDto::from)
                         .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;해당 게시글 존재 x&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행되는 SQL&lt;/h4&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- [1] findById(pid) &amp;rarr; Post + User를 LEFT JOIN으로 한 번에 조회
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;총 1개의 쿼리가 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;findById()&lt;/code&gt;는 즉시 SELECT를 실행한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Post.user&lt;/code&gt;가 &lt;code&gt;@ManyToOne&lt;/code&gt; (기본 EAGER)이므로, Hibernate가 LEFT JOIN으로 User를 &lt;b&gt;함께&lt;/b&gt; 가져온다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Post.postComments&lt;/code&gt;는 &lt;code&gt;@OneToMany&lt;/code&gt; (기본 LAZY)이고, &lt;code&gt;PostDto::from&lt;/code&gt;에서 postComments에 접근하지 않으므로 PostComment 쿼리는 &lt;b&gt;실행되지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;getPostWithComments() &amp;mdash; 게시글 + 댓글 조회&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 코드&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// PostService.java
@Transactional
public PostWithCommentsDto getPostWithComments(Long pid) {
    return postRepository.findById(pid)
                         .map(PostWithCommentsDto::from)
                         .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;해당 게시글 존재 x&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// PostWithCommentsDto.java
public static PostWithCommentsDto from(Post post) {
    return new PostWithCommentsDto(
        post.getId(),
        post.getTitle(),
        post.getContent(),
        post.getCategoryType(),
        post.getPostComments().stream()          // &amp;larr; Lazy 로딩 발동 지점
                              .map(PostCommentDto::from)
                              .collect(Collectors.toCollection(LinkedHashSet::new)),
        UserDto.from(post.getUser()),            // &amp;larr; 이미 EAGER로 로드됨
        post.getCreatedDate(),
        post.getCreatedBy(),
        post.getModifiedDate(),
        post.getModifiedBy()
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행되는 SQL&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;-- [1] findById(pid) &amp;rarr; Post + User 조회 (EAGER JOIN)
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ?

-- [2] post.getPostComments() 접근 &amp;rarr; Lazy 로딩으로 PostComment + User 조회
SELECT pc1_0.pid, pc1_0.id, pc1_0.content, pc1_0.created_by, ...
FROM post_comment pc1_0
LEFT JOIN user u1_0 ON u1_0.uid = pc1_0.uid
WHERE pc1_0.pid = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;총 2개의 쿼리가 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1번 쿼리: &lt;code&gt;findById()&lt;/code&gt;에 의해 Post + User(EAGER) 조회.&lt;/li&gt;
&lt;li&gt;2번 쿼리: &lt;code&gt;PostWithCommentsDto.from()&lt;/code&gt; 안에서 &lt;code&gt;post.getPostComments()&lt;/code&gt;를 호출하는 순간, Lazy 로딩이 발동하여 해당 Post의 PostComment들을 조회한다. PostComment &amp;rarr; User도 &lt;code&gt;@ManyToOne&lt;/code&gt;(EAGER)이므로 LEFT JOIN으로 함께 가져온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;registerPost() &amp;mdash; 게시글 등록&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 코드&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// PostService.java:42-55
@Transactional
public void registerPost(PostDto postDto) {
    User user = userRepository.getReferenceById(postDto.getUserDto().getUid());
    // &amp;uarr; SELECT 실행 안 함. uid만 가진 프록시 반환.

    Post post = postDto.toEntity(user);
    // &amp;uarr; Post 객체 생성. user 필드에 프록시가 들어감.

    postRepository.save(post);
    // &amp;uarr; INSERT 실행. 프록시의 uid만 FK 값으로 사용.
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행되는 SQL&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;-- [1] postRepository.save(post) &amp;rarr; INSERT (getReferenceById는 쿼리 없음)
INSERT INTO post (category_type, content, created_by, created_date,
                  modified_by, modified_date, title, uid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;총 1개의 쿼리가 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;getReferenceById()&lt;/code&gt;는 프록시를 반환할 뿐, SELECT를 실행하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;save()&lt;/code&gt;가 호출되면 Hibernate는 Post가 새 엔티티임을 감지하고(&lt;code&gt;@GeneratedValue(strategy = IDENTITY)&lt;/code&gt; + id가 null) INSERT를 실행한다.&lt;/li&gt;
&lt;li&gt;INSERT 시 &lt;code&gt;uid&lt;/code&gt; 컬럼에는 프록시가 가진 ID 값(&quot;user1&quot; 등)만 필요하므로, User의 다른 필드를 조회할 필요가 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이것이 &lt;code&gt;getReferenceById&lt;/code&gt;의 장점이다.&lt;/b&gt; FK 설정을 위해 불필요한 SELECT를 하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;updatePost() &amp;mdash; 게시글 수정 (Dirty Checking)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 코드&lt;/h4&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;// PostService.java:57-66
@Transactional
public void updatePost(Long pid, PostDto postDto) {
    Post post = postRepository.getReferenceById(pid);
    // &amp;uarr; 프록시 반환 (SELECT 아직 안 함)

    post.updateTitleAndContentAndCategoryType(
        postDto.getTitle(),
        postDto.getContent(),
        postDto.getCategoryType()
    );
    // &amp;uarr; 프록시의 필드에 접근 &amp;rarr; 이 시점에 SELECT 실행
    // &amp;uarr; 그 후 title, content, categoryType 값을 변경
    // &amp;uarr; save() 호출 없음! @Transactional 종료 시 dirty checking으로 UPDATE 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// Post.java:63-67
public void updateTitleAndContentAndCategoryType(String title, String content, CategoryType categoryType) {
    this.title = title;        // &amp;larr; setter로 값 변경
    this.content = content;
    this.categoryType = categoryType;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행되는 SQL&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;-- [1] post.updateTitleAndContent...() 호출 시 &amp;rarr; 프록시 초기화를 위한 SELECT
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ?

-- [2] @Transactional 종료 시 &amp;rarr; dirty checking에 의한 UPDATE
UPDATE post
SET category_type = ?, content = ?, created_date = ?,
    modified_by = ?, modified_date = ?, title = ?, uid = ?
WHERE pid = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;총 2개의 쿼리가 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;getReferenceById(pid)&lt;/code&gt;는 프록시만 반환한다. (SELECT 없음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;post.updateTitleAndContentAndCategoryType()&lt;/code&gt;이 호출되면, 프록시의 실제 필드에 접근하게 되므로 &lt;b&gt;프록시가 초기화&lt;/b&gt;된다. 이때 SELECT가 실행된다.&lt;/li&gt;
&lt;li&gt;SELECT로 가져온 원본 데이터(스냅샷)를 영속성 컨텍스트가 보관한다.&lt;/li&gt;
&lt;li&gt;setter로 title, content, categoryType이 변경된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transactional&lt;/code&gt; 메서드가 끝나면 트랜잭션이 커밋된다.&lt;/li&gt;
&lt;li&gt;커밋 직전, Hibernate가 &lt;b&gt;현재 엔티티 상태&lt;/b&gt;와 &lt;b&gt;스냅샷(원본)&lt;/b&gt;을 비교한다 &amp;rarr; &lt;b&gt;Dirty Checking&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;변경된 필드가 발견되면 UPDATE SQL을 자동으로 실행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;save()&lt;/code&gt;를 호출하지 않아도 UPDATE가 실행되는 이유가 바로 이것이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;deletePost() &amp;mdash; 게시글 삭제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 코드&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// PostService.java:68-74
@Transactional
public void deletePost(long pid, String uid) {
    System.out.println(&quot;DELETE POST&quot;);
    postRepository.deleteByIdAndUser_Uid(pid, uid);
    // &amp;uarr; Spring Data JPA가 메서드 이름을 파싱하여 쿼리 생성
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행되는 SQL&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;-- [1] 삭제 대상 Post 조회 (pid + uid 조건)
SELECT p1_0.pid, p1_0.category_type, p1_0.content, p1_0.created_by, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid
WHERE p1_0.pid = ? AND u1_0.uid = ?

-- [2] Post의 User를 로딩 (EAGER이므로)
SELECT u1_0.uid, u1_0.created_by, u1_0.created_date, u1_0.email, ...
FROM user u1_0
WHERE u1_0.uid = ?

-- [3] CascadeType.ALL에 의해 연관된 PostComment 로딩
SELECT pc1_0.pid, pc1_0.id, pc1_0.content, pc1_0.created_by, ...
FROM post_comment pc1_0
LEFT JOIN user u1_0 ON u1_0.uid = pc1_0.uid
WHERE pc1_0.pid = ?

-- [4] Post 삭제 (댓글이 있었다면 댓글 먼저 삭제 후 Post 삭제)
DELETE FROM post WHERE pid = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;총 3~4개의 쿼리가 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Data JPA의 &lt;code&gt;deleteByXxx()&lt;/code&gt; 메서드는 내부적으로 먼저 &lt;b&gt;SELECT로 엔티티를 조회&lt;/b&gt;한 뒤, &lt;code&gt;EntityManager.remove()&lt;/code&gt;로 삭제한다. (바로 DELETE를 날리지 않는다!)&lt;/li&gt;
&lt;li&gt;Post의 &lt;code&gt;postComments&lt;/code&gt;에 &lt;code&gt;CascadeType.ALL&lt;/code&gt;이 걸려있으므로, Post를 삭제하기 전에 연관된 PostComment도 함께 삭제해야 한다.&lt;/li&gt;
&lt;li&gt;이를 위해 Hibernate는 PostComment도 미리 SELECT한다.&lt;/li&gt;
&lt;li&gt;댓글이 있으면 댓글 DELETE &amp;rarr; Post DELETE 순서로 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JOIN이 발생하는지 vs 추가 SELECT가 발생하는지&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조회 방식&lt;/th&gt;
&lt;th&gt;User 로딩 방식&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findById(pid)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;LEFT JOIN&lt;/b&gt; (한 방 쿼리)&lt;/td&gt;
&lt;td&gt;EntityManager.find() 사용 &amp;rarr; &lt;br /&gt;Hibernate가 EAGER 관계를 JOIN으로 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findAll()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;별도 SELECT&lt;/b&gt; (추가 쿼리)&lt;/td&gt;
&lt;td&gt;JPQL 기반 &amp;rarr; &lt;br /&gt;Hibernate가 EAGER이더라도 별도 SELECT로 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByTitleContains()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;별도 SELECT&lt;/b&gt; (추가 쿼리)&lt;/td&gt;
&lt;td&gt;Spring Data JPA 쿼리 메서드 &amp;rarr; JPQL 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findByUser_UidContains()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;LEFT JOIN(조건)&lt;/b&gt; + &lt;b&gt;별도 SELECT(로딩)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;WHERE 절을 위해 JOIN은 하지만, User 데이터 로딩은 별도 SELECT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 로그로 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;findById(1) &amp;mdash; JOIN 발생:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 한 번의 쿼리로 Post + User를 함께 가져온다
SELECT p1_0.pid, ..., u1_0.uid, u1_0.username, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid    -- &amp;larr; JOIN!
WHERE p1_0.pid = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;findAll() (목록 조회) &amp;mdash; 별도 SELECT 발생:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;-- 1번째 쿼리: Post만 가져온다 (User JOIN 없음!)
SELECT p1_0.pid, ..., p1_0.uid
FROM post p1_0
LIMIT ?, ?

-- 2번째 쿼리: 각 Post의 User를 개별적으로 가져온다
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ?
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ?
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ?
-- ... Post 개수만큼 반복 (같은 uid면 영속성 컨텍스트 캐시로 생략)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 각 Post의 User를 개별적으로 가져오는 쿼리는 findAll() 뒤에 이어지는 map(PostDto::from) 때문에 발생하는 것이 아니다.&lt;br /&gt;Post가 User를 참조로 갖고 있고 이는 ManyToOne 관계이기 때문에 EAGER가 적용되어 findAll() 후 User를 가져온다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 이런 차이가 생기는가?&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;findById()
  &amp;rarr; EntityManager.find() 호출
  &amp;rarr; Hibernate가 직접 SQL을 생성
  &amp;rarr; EAGER 관계를 LEFT JOIN으로 최적화함

findAll(), findByXxx()
  &amp;rarr; JPQL 실행 (&quot;SELECT p FROM Post p&quot; 등)
  &amp;rarr; JPQL은 엔티티 기준이라 JOIN을 자동 추가하지 않음
  &amp;rarr; Post만 SELECT한 뒤, EAGER 관계의 User를 별도 SELECT로 로딩
  &amp;rarr; 이것이 N+1 문제의 원인!&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;적용 예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Fetch Join (JPQL)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;JPQL에 JOIN FETCH 추가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Query(&quot;SELECT p FROM Post p JOIN FETCH p.user&quot;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;@EntityGraph&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;메서드에 어노테이션 추가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@EntityGraph(attributePaths = {&quot;user&quot;})&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;@BatchSize&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;N+1의 N을 줄임 (IN절로 묶음)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@BatchSize(size = 100)&lt;/code&gt; on User&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;N+1 문제에 대해 고민해보자&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;N+1 문제란?&lt;/h3&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;1번의 쿼리로 N개의 데이터를 가져온 뒤,
각 데이터의 연관 엔티티를 가져오기 위해 N번의 추가 쿼리가 실행되는 것.
총 1 + N번의 쿼리가 실행된다.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;N+1이 발생하는 시나리오&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;발생 위치: &lt;code&gt;getPosts()&lt;/code&gt;, &lt;code&gt;getPostsWithPage()&lt;/code&gt;, &lt;code&gt;getPostsWithSearch()&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// PostService.java:76-82
@Transactional
public List&amp;lt;PostDto&amp;gt; getPosts() {
    return postRepository.findAll().stream()   // &amp;larr; Post Select 1번 + User Select N번
                         .map(PostDto::from)
                         .toList();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 SQL 로그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Post 쿼리: Post 10개 가져옴&lt;br /&gt;User 쿼리: 10개 중 3개 게시글은 동일한 작성자. 총 8번의 User 쿼리 발생.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- [1번째 쿼리] Post 목록 조회
SELECT p1_0.pid, p1_0.category_type, p1_0.content, ..., p1_0.uid
FROM post p1_0
LIMIT ?, ?

-- [2번째] 1번 Post의 User 조회
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ?    -- uid = 'user1'

-- [3번째] 2번 Post의 User 조회
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ?    -- uid = 'user2'

-- [4번째] 3번 Post의 User 조회
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid = ?    -- uid = 'user3'

-- ... uid가 같은 경우 영속성 컨텍스트 캐시로 생략되지만,
-- 서로 다른 uid가 있으면 그만큼 추가 쿼리 발생!

-- [마지막] count 쿼리 (페이징 시)
SELECT count(p1_0.pid) FROM post p1_0&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법 ① &amp;mdash; Fetch Join (JPQL)&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;// PostRepository.java에 추가
@Query(&quot;SELECT p FROM Post p JOIN FETCH p.user&quot;)
List&amp;lt;Post&amp;gt; findAllWithUser();

@Query(&quot;SELECT p FROM Post p JOIN FETCH p.user&quot;)
Page&amp;lt;Post&amp;gt; findAllWithUser(Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행되는 SQL:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 1번의 쿼리로 Post + User를 함께 가져온다
SELECT p1_0.pid, ..., u1_0.uid, u1_0.username, ...
FROM post p1_0
INNER JOIN user u1_0 ON u1_0.uid = p1_0.uid&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법 ② &amp;mdash; @EntityGraph&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// PostRepository.java에 추가
@EntityGraph(attributePaths = {&quot;user&quot;})
@Override
List&amp;lt;Post&amp;gt; findAll();

@EntityGraph(attributePaths = {&quot;user&quot;})
Page&amp;lt;Post&amp;gt; findByTitleContains(String searchValue, Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행되는 SQL:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- findAll()이지만 LEFT JOIN이 추가된다
SELECT p1_0.pid, ..., u1_0.uid, u1_0.username, ...
FROM post p1_0
LEFT JOIN user u1_0 ON u1_0.uid = p1_0.uid&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법 ③ &amp;mdash; @BatchSize&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;// User.java에 추가
@BatchSize(size = 100)    // &amp;larr; 클래스 레벨에 추가
@Entity
public class User extends AuditingFields {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행되는 SQL:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- Post 조회
SELECT p1_0.pid, ..., p1_0.uid FROM post p1_0

-- User를 한 번에 IN절로 묶어서 조회 (N번 &amp;rarr; 1번으로 줄어듦)
SELECT u1_0.uid, ... FROM user u1_0 WHERE u1_0.uid IN (?, ?, ?, ?, ?, ?, ?, ?)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 비교&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;장점&lt;/th&gt;
&lt;th&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fetch Join&lt;/td&gt;
&lt;td&gt;가장 확실, 1번의 쿼리&lt;/td&gt;
&lt;td&gt;페이징과 함께 쓰면 메모리에서 페이징(경고 발생)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@EntityGraph&lt;/td&gt;
&lt;td&gt;기존 메서드에 어노테이션만 추가&lt;/td&gt;
&lt;td&gt;복잡한 조건에서는 한계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@BatchSize&lt;/td&gt;
&lt;td&gt;기존 코드 변경 최소&lt;/td&gt;
&lt;td&gt;완전한 1번은 아님(배치 수에 따라 나뉨)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Learning Log</category>
      <category>spring data jpa</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/37</guid>
      <comments>https://allluck777.tistory.com/37#entry37comment</comments>
      <pubDate>Thu, 12 Mar 2026 16:10:46 +0900</pubDate>
    </item>
    <item>
      <title>[멋사 클라우드 5기] Day 28 - JWT 그런데 Redis를 곁들인</title>
      <link>https://allluck777.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 Spring Security, JWT, Redis를 학습하며 보안에 대해 조금 더 고민해 볼 수 있었다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;3,1&quot; data-ke-size=&quot;size16&quot;&gt;기술 하나하나만 보면 단순해 보이는데, 이들을 조합해서 쓰면 구조는 복잡해지는 한편&lt;/p&gt;
&lt;p data-path-to-node=&quot;3,1&quot; data-ke-size=&quot;size16&quot;&gt;훨씬 다양한 문제들을 해결할 수 있게 되는 점이 흥미롭다.&lt;br /&gt;그럼에도 불구하고, 보안에 있어 완벽한 기술이란 없다고 한다. &lt;br /&gt;다만, 최선의 트레이드오프를 결정하기 위해서&lt;br /&gt;어떤 보안 위협을 최우선으로 방어할 것인지, &lt;br /&gt;그리고 프로젝트의 복잡도와 아키텍처 사이에서&lt;br /&gt;어느정도의 타협점을 찾을 것인지 고민하는 것이 중요한 문제라고 느껴졌다.&lt;br /&gt;어제 정리한 내용들과 겹치는 부분이 있지만, 헷갈리던 점을 명확히 짚고 넘어가기 위해 다시 정리했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션 기반 인증의 구조와 한계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션 기반 인증이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 &lt;b&gt;Stateless(무상태) 프로토콜&lt;/b&gt;이다. 각 요청은 독립적이며, 서버는 이전 요청을 기억하지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stateless란? 서버가 클라이언트의 이전 상태를 기억하지 않는다는 의미다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 로그인은 &quot;나 이전에 인증했어&quot;라는 상태를 유지해야 한다. &lt;br /&gt;이 간극을 메우기 위해 &lt;b&gt;세션(Session)&lt;/b&gt;이 활용된다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;1. 사용자가 id/pw로 로그인 요청
2. 서버가 인증 후 세션 생성 &amp;rarr; 서버 메모리에 저장
   { sessionId: &quot;abc123&quot;, userId: 42, role: &quot;USER&quot; }
3. 클라이언트에게 sessionId만 쿠키로 전달
4. 이후 요청마다 쿠키에 sessionId 포함
5. 서버는 sessionId로 메모리 조회 &amp;rarr; &quot;아, userId 42구나&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하고 직관적이다. 지금도 많은 서비스에서 잘 쓰이고 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버가 한 대일 땐 문제없다&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;[클라이언트] ──&amp;rarr; [Server 1 (세션 메모리 보유)] ──&amp;rarr; 정상 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 항상 같은 서버와 통신하고, 서버 메모리에 세션이 있으니 문제없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버가 여러 대가 되는 순간 문제가 생긴다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 성장하면 트래픽을 감당하기 위해 &lt;b&gt;Scale-out&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scale-out이란? 서버 1대의 성능을 높이는 대신(Scale-up), 서버를 여러 대 추가해 부하를 분산하는 방식이다. &lt;br /&gt;앞단에 로드밸런서(Load Balancer)가 요청을 각 서버에 나눠준다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;[클라이언트]
     │
     ▼
[Load Balancer]
   ┌──┴──┐
   ▼     ▼
[S1]   [S2]   &amp;larr; 세션이 S1 메모리에만 있음
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;시나리오:
1. 사용자가 S1으로 로그인 &amp;rarr; 세션이 S1 메모리에 저장
2. 다음 요청을 로드밸런서가 S2로 라우팅
3. S2는 그 세션을 모름 &amp;rarr; 로그인 풀림  
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 늘어날수록 이 문제는 심각해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결하기 위한 시도들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① Sticky Session (고정 세션)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 사용자는 항상 같은 서버로 보내는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;문제: 특정 서버에 부하가 몰릴 수 있다.
     그 서버가 죽으면 해당 사용자들은 전부 로그인이 풀린다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② Session Replication (세션 복제)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서버에 세션 데이터를 복제해서 동기화하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;문제: 서버가 늘어날수록 복제 비용이 기하급수적으로 증가한다.
      서버 10대면 모든 세션 변경사항을 10곳에 동기화해야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식 모두 타당하나 완전한 해결책이 아니다. 그래서 나온 아이디어가 이것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;서버가 상태를 아예 들고 있지 않으면 어떨까?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아이디어에서 JWT가 등장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JWT와 Refresh Token 패턴&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Stateless가 가능한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션은 서버 메모리에서 &quot;이 사람이 누구인지&quot; 조회했다. JWT는 토큰 자체에 그 정보가 담겨있다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;세션 방식: 서버가 sessionId &amp;rarr; 메모리 조회 &amp;rarr; userId 확인
JWT  방식: 서버가 토큰의 서명만 검증 &amp;rarr; 토큰 안의 userId 바로 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 아무것도 저장하지 않아도 되니, 어느 서버로 요청이 가든 상관없다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;[클라이언트]
     │
     ▼
[Load Balancer]
   ┌──┴──┐
   ▼     ▼
[S1]   [S2]   &amp;larr; 둘 다 토큰 서명만 검증하면 됨. 저장소 불필요.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scale-out 문제가 깔끔하게 해결된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Access Token 만료 시간 딜레마&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 JWT는 한 번 발급하면 &lt;b&gt;만료 시간이 될 때까지 항상 유효&lt;/b&gt;하다. 서버가 &quot;이 토큰 무효화해줘&quot;를 할 수 없다. &lt;br /&gt;서버에서 저장해서 관리하지 않기 때문이다. 그래서 만료 시간 설정에 딜레마가 생긴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;짧게 (예: 5분)&lt;/b&gt; &amp;rarr; 탈취당해도 곧 만료. 보안적으로는 좋음. 하지만 5분마다 재로그인 필요 &amp;rarr; UX 최악&lt;/li&gt;
&lt;li&gt;&lt;b&gt;길게 (예: 7일)&lt;/b&gt; &amp;rarr; 편리함. 하지만 탈취당하면 7일 동안 무방비&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Refresh Token를 곁들여 보자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 딜레마를 해결하기 위해 두 종류의 토큰을 함께 쓰는 패턴이 고안됐다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 72px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;토큰&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;역할&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;만료 시간&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Access Token&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;API 요청 시 인증에 사용&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;짧게 (5~30분)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Refresh Token&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Access Token 재발급용&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;길게 (7~30일)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;로그인 &amp;rarr; Access Token (15분) + Refresh Token (7일) 발급

... 15분 후 Access Token 만료 ...

클라이언트가 Refresh Token 제출 &amp;rarr; 서버가 검증 &amp;rarr; 새 Access Token 발급
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 재로그인 없이 계속 서비스를 이용하고, Access Token은 항상 짧게 유지된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 그런데 Refresh Token도 탈취당하면 마찬가지 아닌가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞다. Refresh Token도 탈취당하면 문제가 생긴다. 게다가 만료 시간이 더 길다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 근본적인 문제가 있다. JWT 특성상 서버는 이 Refresh Token을 &lt;b&gt;무효화할 방법이 없다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;공격자가 Refresh Token 탈취
&amp;rarr; 서버는 이 사실을 알 방법도, 막을 방법도 없음
&amp;rarr; 7일 동안 계속 새 Access Token 발급 가능
&amp;rarr; 사실상 영구 접근
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Redis가 등장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis까지 곁들여 보자&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis(Remote Dictionary Server)는 데이터를 &lt;b&gt;메모리(RAM)에 저장하는 인메모리 데이터 저장소&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 DB(MySQL 등)는 디스크에 저장해 영구적이지만 상대적으로 느리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 메모리에 저장해 &lt;b&gt;매우 빠르고 (1ms 이하)&lt;/b&gt;, TTL 자동 만료 기능을 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Key-Value 구조로 저장한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;refresh:allluck&quot;           &amp;rarr;  &quot;eyJhbGc...BBB&quot;
&quot;blacklist:eyJhbGc...AAA&quot;   &amp;rarr;  &quot;logout&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TTL (Time To Live) &amp;mdash; Redis의 핵심 기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장할 때 만료 시간을 설정하면, 그 시간이 지나면 &lt;b&gt;Redis가 자동으로 삭제&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;// 7일 후 자동 삭제
redisTemplate.opsForValue().set(&quot;refresh:allluck&quot;, token, 7, TimeUnit.DAYS);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능이 JWT와 결합했을 때 강력해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis가 없으면 왜 Refresh Token 관리가 안 되는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 없이 Refresh Token을 쓰면 서버는 &lt;b&gt;어떤 Refresh Token을 발급했는지 알 방법이 없다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;클라이언트: &quot;이 Refresh Token으로 새 Access Token 주세요&quot;
서버:       &quot;...서명은 유효한데, 내가 실제로 발급한 건지, 이미 무효화된 건지 알 방법이 없네&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교할 정답지가 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis가 생기면 서버가 &quot;통제권&quot;을 갖는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에 Refresh Token을 저장하는 순간, 서버는 정답지를 갖게 된다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;Redis: { &quot;refresh:allluck&quot; &amp;rarr; &quot;eyJhbGc...BBB&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 가능해지는 것들:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 보낸 토큰과 Redis 값 비교 &amp;rarr; 불일치 시 거부&lt;/li&gt;
&lt;li&gt;비밀번호 변경, 계정 정지 시 Redis에서 삭제 &amp;rarr; 즉시 무효화&lt;/li&gt;
&lt;li&gt;탈취 의심 시 삭제 &amp;rarr; 이후 재발급 요청 전부 차단&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Access Token과 Refresh Token의 저장위치에 대해 고민해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 토큰을 저장할 수 있는 곳은 크게 세 곳이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 93px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;저장 위치&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;특징&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;탈취 위협&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;localStorage&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;새로고침/탭 종료 후에도 영구 유지&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;XSS 공격으로 탈취 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;&lt;s&gt;Cookie (일반)&lt;/s&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JS에서 접근 가능&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;XSS 공격으로 탈취 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;메모리 (JS 변수)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;페이지 새로고침 시 사라짐&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JS로 접근 불가, XSS에 안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;HttpOnly Cookie&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;JS에서 접근 불가, 브라우저가 자동으로 요청에 포함&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;XSS로 탈취 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS(Cross-Site Scripting)란? 공격자가 웹페이지에 악성 JS 코드를 심어서, 그 페이지를 방문한 사용자의 브라우저에서 코드가 실행되게 하는 공격이다. localStorage나 일반 쿠키에 저장된 값은 JS로 읽을 수 있어서 그대로 탈취된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token을 메모리(JS 변수)에 저장하면 &lt;b&gt;JS 코드가 접근할 수 없으니&lt;/b&gt; XSS 공격으로 훔쳐갈 수가 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하지만 새로고침하면 사라지잖아?!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 재발급을 받아서 사용할 수 있다&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;1. 사용자가 새로고침
2. 메모리의 Access Token 사라짐
3. 앱이 초기화되면서 &quot;Access Token 없네, 로그인 상태 확인 필요&quot;
4. HttpOnly Cookie에 담긴 Refresh Token으로 /reissue 요청 자동 발송
   &amp;larr; 사용자는 이 과정을 모름. 보통 100ms 이내
5. 새 Access Token 발급 &amp;rarr; 메모리에 저장
6. 사용자 입장에선 그냥 새로고침 후 정상 이용
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Refresh Token을 HttpOnly Cookie에 저장하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpOnly는 쿠키에 붙일 수 있는 속성 중 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpOnly 속성이 붙은 쿠키는 &lt;b&gt;JS에서 아예 읽을 수 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 요청 시 자동으로 포함시켜 줄 뿐이다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// 이런 코드로 접근 시도해도
document.cookie  // HttpOnly 쿠키는 여기에 안 나온다
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, XSS 공격자가 JS를 심어도 Refresh Token을 읽어갈 수 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 저장 전략의 의도&lt;/h2&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Access Token  &amp;rarr; 메모리 저장
  이유: 수명이 짧아서 새로고침 시 재발급해도 부담 없음
        JS 접근 불가 &amp;rarr; XSS 안전

Refresh Token &amp;rarr; HttpOnly Cookie 저장
  이유: 수명이 길어서 영구 보관 필요
        HttpOnly &amp;rarr; JS 접근 불가 &amp;rarr; XSS 안전
        새로고침 시에도 유지되어 자동 재발급에 활용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 JS에서 읽을 수 없게 설계한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token은 메모리로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Refresh Token은 HttpOnly Cookie로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자의 방식으로 XSS를 막는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 한 가지 아쉬운 점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token과 Refresh Token 둘을 비교한다면 보안적으로 더 중요한 대상은 Refresh Token이다. 유효기간이 길기 때문이다. 탈취당하면 만료전까지 계속해서 사용될 우려가 있다. 하지만 Access Token이 만약 메모리에서 관리된다면, 사용자의 페이지 이탈 또는 새로고침 등의 이유로 쉽게 소실될 수 있다. 이때 소실된 Access Token에 대하여 새로 발급받기 위해 Refresh Token을 담은 https 요청을 보내게된다. 재발급 요청이 빈번해진다는 것은 Refresh Token이 네트워크를 타는 빈도도 증가하는 것이다. 아주 미세하게지만 Refresh Token의 탈취 기회가 증가되는게 아닐까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이 부분도 어떻게 보면 트레이드오프라고 볼 수 있다. &lt;br /&gt;Access Token을 메모리에서 관리하는 대신 localStorage에 저장한다고 가정해보자. 새로고침해도 유지가 되므로 재발급 빈도는 낮을 것이다. 하지만 JS로 읽힐 수 있다는 문제로 인해 XSS 공격에 노출되기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access Token을 HttpOnly Cookie로 관리한다고 가정해보자. 또 다른 문제가 생긴다. 브라우저는 쿠키를 자동으로 요청에 포함시키기 때문에 CSRF 공격에 취약해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;즉, Access Token을 메모리에서 관리함으로써 발생할 수 있는 (희박한)&lt;b&gt; 네트워크 노출 위협&lt;/b&gt;은 LocalStorage 또는 Cookie로 관리할 때 발생할 수 있는 &lt;b&gt;XSS 위협&lt;/b&gt;, &lt;b&gt;CSRF 위협&lt;/b&gt;보다는 훨씬 감수할만한 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시나리오로 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id:allluck / pw:1234 로 로그인하고, API 요청 후, 로그아웃하는 시나리오&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST /login { id: &quot;allluck&quot;, pw: &quot;1234&quot; }&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서버가 생성한 토큰:
Access Token  (15분 만료): eyJhbGc...AAA
Refresh Token (7일 만료):  eyJhbGc...BBB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis에 Refresh Token 저장&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;KEY:   &quot;refresh:allluck&quot;
VALUE: &quot;eyJhbGc...BBB&quot;
TTL:   604800초 (7일)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라이언트에게 발급:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;Access Token:  eyJhbGc...AAA  &amp;larr; 메모리에 저장
Refresh Token: eyJhbGc...BBB  &amp;larr; HttpOnly Cookie에 저장
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 API 요청 (Access Token 유효)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GET /api/mypage + Authorization: Bearer eyJhbGc...AAA&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서버:
1. 서명 검증 &amp;rarr; 유효
2. 만료 시간 확인 &amp;rarr; 유효
3. 블랙리스트 확인: Redis에 &quot;blacklist:eyJhbGc...AAA&quot; 없음
4. 정상 응답 ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 상태 변화 없음.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Access Token 만료 &amp;rarr; 재발급&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST /reissue + Refresh Token eyJhbGc...BBB&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;서버:
1. Refresh Token 서명 검증 &amp;rarr; 유효
2. 토큰에서 userId 추출 &amp;rarr; &quot;allluck&quot;
3. Redis 조회: GET &quot;refresh:allluck&quot; &amp;rarr; &quot;eyJhbGc...BBB&quot;
4. 클라이언트 값 == Redis 값 &amp;rarr; 일치 ✅
5. 새 Access Token 발급: eyJhbGc...CCC
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 상태 변화 없음. Refresh Token은 그대로 유지.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그아웃&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST /logout + Authorization: Bearer eyJhbGc...CCC&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서버:
1. Access Token 남은 만료 시간 계산 &amp;rarr; 720초
2. Access Token 블랙리스트 등록
3. Refresh Token 삭제
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis 변화:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 블랙리스트 추가
SET &quot;blacklist:eyJhbGc...CCC&quot;  &amp;rarr;  &quot;logout&quot;  (TTL: 720초)

# Refresh Token 삭제
DEL &quot;refresh:allluck&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그아웃 후 공격 시도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;탈취한 Access Token으로 API 요청:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;서버:
1. 서명 검증 &amp;rarr; 유효 (서명은 멀쩡함)
2. 만료 확인 &amp;rarr; 아직 유효 (720초 남음)
3. 블랙리스트 확인: &quot;blacklist:eyJhbGc...CCC&quot; 존재 &amp;rarr; 거부 ❌
&amp;rarr; 401 Unauthorized
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;탈취한 Refresh Token으로 재발급 시도:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;서버:
1. 서명 검증 &amp;rarr; 유효
2. userId 추출 &amp;rarr; &quot;allluck&quot;
3. Redis 조회: &quot;refresh:allluck&quot; &amp;rarr; null (삭제됨) &amp;rarr; 거부 ❌
&amp;rarr; 401 Unauthorized
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그런데, 이건 Stateless로 보기힘들지 않을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에 상태를 저장하는 순간, &lt;b&gt;완전한 Stateless는 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 구분하면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;순수 JWT          &amp;rarr; 완전한 Stateless  (하지만 로그아웃 무효화 불가)
JWT + Redis       &amp;rarr; 부분적 Stateless  (보안 확보, 대신 상태 일부 존재)
순수 Session      &amp;rarr; 완전한 Stateful
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제대로된 로그아웃을 구현하기 위해서는 어느정도의 트레이드오프를 인정하고, &lt;br /&gt;JWT + Redis 구조로 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽한 Stateless 보다 실제로 안전한 인증이 더 중요하기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그럼 세션의 Scale-out 문제가 다시 생기는 건가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 쓰면 서버에 상태가 생기는 거니까 세션과 같은 문제가 발생하는게 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조금 다르다.&lt;/b&gt; 세션과 Redis의 구조는 다르기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션 문제의 본질&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션은 &lt;b&gt;각 서버의 메모리&lt;/b&gt;에 저장된다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;사용자가 S1으로 로그인 &amp;rarr; 세션이 S1 메모리에 저장
로드밸런서가 다음 요청을 S2로 라우팅
&amp;rarr; S2에는 그 세션이 없음 &amp;rarr; 인증 실패  
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 늘어날수록 데이터가 흩어진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis는 외부 공유 저장소다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 서버 안에 있지 않다. &lt;b&gt;모든 서버가 바라보는 독립적인 외부 저장소&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Server 1 ─┐
Server 2 ─┼──&amp;rarr; Redis (중앙 저장소)
Server 3 ─┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 서버로 요청이 가든, 모두 같은 Redis를 조회한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;사용자가 S1으로 로그인 &amp;rarr; Refresh Token이 Redis에 저장
로드밸런서가 다음 요청을 S3으로 라우팅
&amp;rarr; S3도 같은 Redis를 조회 &amp;rarr; 정상 동작 ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 자체의 고가용성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Redis가 단일 장애점(Single Point of Failure)이 되지 않냐는 의문이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 자체도 Scale-out이 가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Redis Sentinel&lt;/b&gt;: 마스터-슬레이브 구조로 고가용성 확보. 마스터가 죽으면 슬레이브가 자동으로 마스터 승격&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis Cluster&lt;/b&gt;: 데이터를 여러 노드에 분산 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버와 Redis를 독립적으로 각각 확장할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그럼 애초에 세션 + Redis면 됐던 거 아닌가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;맞다.&lt;/b&gt; 그리고 실제로 그렇게도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Session + Redis&lt;/b&gt;라는 이름으로 이미 널리 쓰이는 구조라고 한다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;세션 ID만 쿠키로 클라이언트에 전달
세션 데이터는 Redis에 저장
&amp;rarr; 어느 서버로 요청이 가도 Redis에서 꺼내면 됨
&amp;rarr; Scale-out 문제 해결
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그러면 JWT는 왜 쓰는거야...?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT가 세션의 &quot;유일한 해결책&quot;인 것은 아니다. JWT만의 진짜 장점이 따로 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① MSA 환경에서의 독립적 인증&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스 환경에서 서비스가 수십 개라면, 각 서비스마다 Redis 연결을 붙이는 게 부담이다. JWT는 서명 검증만으로 Redis 연결없이 독립적으로 인증이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Session + Redis 방식:
서비스 A &amp;rarr; Redis 조회 &amp;rarr; 인증
서비스 B &amp;rarr; Redis 조회 &amp;rarr; 인증
서비스 C &amp;rarr; Redis 조회 &amp;rarr; 인증  &amp;larr; 모든 서비스가 Redis에 의존

JWT 방식 (이상적):
서비스 A &amp;rarr; 서명 검증만 &amp;rarr; 인증  &amp;larr; Redis 불필요
서비스 B &amp;rarr; 서명 검증만 &amp;rarr; 인증
서비스 C &amp;rarr; 서명 검증만 &amp;rarr; 인증
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 모바일 / 서드파티 환경&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션은 쿠키 기반이라 브라우저에 최적화되어 있다. 모바일 앱이나 외부 API 클라이언트에서는 JWT가 훨씬 다루기 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ 단순한 서비스라면 Session + Redis가 오히려 나을 수 있다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식 웹 서비스라면 JWT의 복잡한 토큰 관리, 블랙리스트 처리 없이 세션 만료시키면 끝이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트레이드오프를 알고 선택하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 방식의 특성을 비교하면 이렇다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&amp;nbsp;&lt;/td&gt;
&lt;td&gt;Session&amp;nbsp;&lt;/td&gt;
&lt;td&gt;Session + Redis&amp;nbsp;&lt;/td&gt;
&lt;td&gt;순수 JWT&amp;nbsp;&lt;/td&gt;
&lt;td&gt;JWT + Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale-out&lt;/td&gt;
&lt;td&gt;❌ 취약&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로그아웃 무효화&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;강제 로그아웃&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSA 환경&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모바일/서드파티&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인프라 복잡도&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;완전한 Stateless&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어떤 기술이 더 좋다는 건 없다.&lt;/b&gt; 서비스의 규모, 구조, 클라이언트 환경에 따라 선택이 달라진다.&lt;/p&gt;</description>
      <category>Learning Log</category>
      <category>JWT</category>
      <category>REDIS</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/36</guid>
      <comments>https://allluck777.tistory.com/36#entry36comment</comments>
      <pubDate>Tue, 10 Mar 2026 02:07:58 +0900</pubDate>
    </item>
    <item>
      <title>[멋사 클라우드 5기] Day 26, 27 - Spring Security &amp;amp; JWT</title>
      <link>https://allluck777.tistory.com/35</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 인증과 인가&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;개념&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의미&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;비유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;인증 (Authentication)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;너는 누구인가?&quot;를 확인하는 과정&lt;/td&gt;
&lt;td&gt;건물 출입 시 신분증을 보여주는 것&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;인가 (Authorization)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;너는 이것을 할 수 있는가?&quot;를 확인하는 과정&lt;/td&gt;
&lt;td&gt;신분증 확인 후 특정 층에 들어갈 권한이 있는지 확인하는 것&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 사용자가 로그인하면 &lt;b&gt;인증&lt;/b&gt;이 완료된 것이고, &lt;br /&gt;로그인한 사용자가 관리자 페이지에 접근하려 할 때 관리자 권한이 있는지 확인하는 것이 &lt;b&gt;인가&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 세션 방식의 한계와 JWT의 등장&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션(Session) 방식이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT가 등장하기 전, 대부분의 웹 서버는 &lt;b&gt;세션(Session)&lt;/b&gt; 방식으로 사용자를 관리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세션 방식 동작 흐름:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 사용자가 로그인 &amp;rarr; 서버가 세션 생성 &amp;rarr; 서버 메모리에 저장
2. 서버가 클라이언트에게 Session ID(세션 식별자) 발급 (보통 쿠키에 저장)
3. 클라이언트가 요청할 때마다 Session ID를 보냄
4. 서버가 메모리에서 해당 Session ID를 찾아 사용자 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션 방식의 문제점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서버 메모리 부담:&lt;/b&gt; 동시 접속자 수가 많을수록 메모리 사용량 급증&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수평 확장(Scale-out) 어려움:&lt;/b&gt; 서버 A에서 발급한 세션을 서버 B는 모른다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;사용자 ---&amp;gt; [서버 A] (세션 있음) ✅
사용자 ---&amp;gt; [서버 B] (세션 없음) ❌ &amp;rarr; 다시 로그인 요구
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Scale-out(수평 확장)이란? 서버 한 대의 성능을 올리는 대신, 동일한 서버를 여러 대 추가하여 부하를 분산하는 방식이다. &lt;br /&gt;현대의 클라우드 환경에서 매우 일반적이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MSA 환경에서 복잡성 증가:&lt;/b&gt; 마이크로서비스마다 세션을 따로 관리하거나 공유 스토리지(Redis 등)가 필요하다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  MSA(Microservices Architecture)란? 하나의 큰 애플리케이션을 여러 개의 작은 독립적인 서비스로 나누는 설계 방식이다. &lt;br /&gt;예를 들어 쇼핑몰을 '상품 서비스', '주문 서비스', '결제 서비스'로 분리하는 것이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 한계를 극복하기 위해 &lt;b&gt;서버가 상태를 저장하지 않아도 되는&lt;/b&gt; 인증 방식이 필요했고, 그 해답이 바로 &lt;b&gt;JWT&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. JWT란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT(JSON Web Token)는 당사자 간에 정보를 &lt;b&gt;JSON 형태&lt;/b&gt;로 안전하게 전달하기 위한 개방형 표준(RFC 7519)이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  RFC(Request for Comments)란? 인터넷 기술의 표준 문서 시리즈다. RFC 7519는 JWT의 공식 명세(spec)다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자기완결적(Self-contained):&lt;/b&gt; 토큰 자체에 사용자 정보가 담겨 있다. &lt;br /&gt;서버가 DB나 메모리를 조회하지 않아도 토큰만으로 사용자를 검증할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서명(Signature) 포함:&lt;/b&gt; 토큰이 위조되지 않았음을 수학적으로 검증할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;URL-safe:&lt;/b&gt; 토큰이 URL에 포함되어도 깨지지 않는 문자로만 구성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. JWT의 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 &lt;b&gt;3개의 파트&lt;/b&gt;로 구성되며, 각 파트를 점(.)으로 구분한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;xxxxx.yyyyy.zzzzz
  │      │      │
Header Payload Signature
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 JWT 예시:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6Iuq5jOuhneyekCIsInJvbGUiOiJVU0VSIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDB9
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. Header (헤더)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더는 토큰의 &lt;b&gt;타입&lt;/b&gt;과 &lt;b&gt;서명 알고리즘&lt;/b&gt; 정보를 담는다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 60px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;필드&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;alg&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;서명에 사용된 알고리즘 (예: HS256, RS256)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;typ&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;토큰 타입 (항상 &quot;JWT&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 JSON을 &lt;b&gt;Base64Url 인코딩&lt;/b&gt;하면 헤더 파트가 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Base64Url 인코딩이란? 바이너리 데이터나 텍스트를 URL에서 안전하게 사용할 수 있는 64개의 문자(A-Z, a-z, 0-9, -, _)로 변환하는 방식이다. 암호화가 아니라 단순 인코딩이므로, 누구나 디코딩해서 원본 내용을 볼 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Payload (페이로드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이로드는 실제로 전달하려는 &lt;b&gt;데이터(클레임)&lt;/b&gt; 를 담는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  클레임(Claim)이란? &quot;주장&quot;이라는 뜻으로, JWT 안에 담긴 키-값 쌍의 정보를 의미한다. &lt;br /&gt;&quot;이 사용자의 ID는 123이다&quot;, &quot;이 사용자의 역할은 ADMIN이다&quot; 같은 주장(정보)들이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클레임은 3가지 종류로 나뉜다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 등록된 클레임 (Registered Claims) - 표준으로 정의된 예약어&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;클레임&amp;nbsp;&lt;/td&gt;
&lt;td&gt;전체 이름&amp;nbsp;&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iss&lt;/td&gt;
&lt;td&gt;Issuer&lt;/td&gt;
&lt;td&gt;토큰 발급자 (예: &quot;my-app&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sub&lt;/td&gt;
&lt;td&gt;Subject&lt;/td&gt;
&lt;td&gt;토큰 주체 (보통 사용자 ID)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aud&lt;/td&gt;
&lt;td&gt;Audience&lt;/td&gt;
&lt;td&gt;토큰 수신자 (예: &quot;mobile-app&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exp&lt;/td&gt;
&lt;td&gt;Expiration Time&lt;/td&gt;
&lt;td&gt;만료 시각 (Unix 타임스탬프)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nbf&lt;/td&gt;
&lt;td&gt;Not Before&lt;/td&gt;
&lt;td&gt;이 시각 이전에는 토큰 사용 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iat&lt;/td&gt;
&lt;td&gt;Issued At&lt;/td&gt;
&lt;td&gt;발급 시각&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jti&lt;/td&gt;
&lt;td&gt;JWT ID&lt;/td&gt;
&lt;td&gt;토큰 고유 식별자&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Unix 타임스탬프란? 1970년 1월 1일 00:00:00 UTC부터 현재까지 경과한 초(second) 의 수다. &lt;br /&gt;예를 들어 1700000000은 2023년 11월 14일을 나타낸다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 공개 클레임 (Public Claims) - 충돌 방지를 위해 URI 형식 권장&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;https://myapp.com/roles&quot;: [&quot;ADMIN&quot;, &quot;USER&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ 비공개 클레임 (Private Claims) - 당사자 간 합의한 커스텀 정보&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;userId&quot;: &quot;user123&quot;,
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;role&quot;: &quot;USER&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 Payload 예시:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;sub&quot;: &quot;user123&quot;,
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;role&quot;: &quot;USER&quot;,
  &quot;iat&quot;: 1700000000,
  &quot;exp&quot;: 1700003600
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ 중요: Payload도 Base64Url로 인코딩될 뿐, 암호화되지 않는다. &lt;br /&gt;누구나 디코딩해서 내용을 볼 수 있으므로, 비밀번호나 카드번호 같은 민감한 정보를 넣으면 절대 안 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. Signature (서명)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서명은 토큰이 &lt;b&gt;위조되지 않았음을 검증&lt;/b&gt;하는 핵심 파트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서명 생성 공식 (HS256 기준):&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;Signature = HMAC-SHA256(
  Base64Url(Header) + &quot;.&quot; + Base64Url(Payload),
  서버의_비밀키(Secret Key)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  HMAC(Hash-based Message Authentication Code)이란? &lt;br /&gt;비밀키와 해시 함수를 조합하여 메시지의 무결성(변조 여부)을 검증하는 알고리즘이다. &lt;br /&gt;비밀키를 모르면 같은 서명을 만들 수 없다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  SHA-256이란? SHA(Secure Hash Algorithm)의 256비트 버전이다. &lt;br /&gt;어떤 데이터도 항상 256비트(32바이트)의 고정된 해시값으로 변환한다. &lt;br /&gt;원본 데이터가 1비트라도 바뀌면 해시값이 완전히 달라진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서명의 역할:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;공격자가 Payload를 수정하려 시도:
  원본:  {&quot;role&quot;: &quot;USER&quot;}   &amp;rarr; Signature: abc123
  조작:  {&quot;role&quot;: &quot;ADMIN&quot;}  &amp;rarr; 새 Signature: xyz789  &amp;larr; 기존 서명과 다름!

서버가 수신 시 서명을 재계산 &amp;rarr; 토큰의 서명과 불일치 &amp;rarr; 토큰 거부 ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. JWT의 동작 원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 흐름&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;클라이언트                          서버
    │                                │
    │  1. POST /login                │
    │  { id: &quot;user&quot;, pw: &quot;1234&quot; }    │
    │ ─────────────────────────────&amp;gt; │
    │                                │ 2. DB에서 사용자 검증
    │                                │ 3. JWT 생성
    │  4. 200 OK                     │
    │  { token: &quot;eyJ...&quot; }           │
    │ &amp;lt;───────────────────────────── │
    │                                │
    │  5. GET /api/profile           │
    │  Authorization: Bearer eyJ...  │
    │ ─────────────────────────────&amp;gt; │
    │                                │ 6. JWT 서명 검증
    │                                │ 7. Payload에서 사용자 정보 추출
    │  8. 200 OK { ... }             │
    │ &amp;lt;───────────────────────────── │&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Bearer 토큰이란? HTTP 인증 스킴(scheme) 중 하나로, &quot;이 토큰을 소지한 자(Bearer)에게 접근을 허용한다&quot;는 의미다. &lt;br /&gt;보통 Authorization: Bearer &amp;lt;token&amp;gt; 헤더 형식으로 전달한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버의 JWT 검증 과정&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;수신한 JWT: header.payload.signature

1. header와 payload를 추출
2. 서버의 비밀키로 signature를 재계산
3. 재계산한 signature == 수신한 signature ? &amp;rarr; 위조 없음 ✅
4. exp(만료 시각) 확인 &amp;rarr; 현재 시각보다 이전이면 만료 ❌
5. 검증 성공 &amp;rarr; Payload에서 사용자 정보 사용
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. JWT의 종류: JWS vs JWE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 사용 목적에 따라 두 가지 형태로 구분된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 60px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;구분&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;이름&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;JWS&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;JSON Web Signature&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;서명만 포함 &amp;rarr; 내용은 누구나 읽을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;JWE&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;JSON Web Encryption&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;내용까지 암호화 &amp;rarr; 키 없이는 내용 불가독&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  일반적으로 &quot;JWT&quot;라고 부르는 것은 대부분 JWS다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Spring Security란 무엇인가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Spring Security를 사용하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 JWT라는 인증 기술을 살펴보았다. &lt;br /&gt;그런데 실제 Spring 기반 애플리케이션에서 인증/인가를 구현하려면, JWT를 검증하는 로직만 있으면 되는 것이 아니다. &lt;br /&gt;보안은 생각보다 고려할 것이 매우 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;직접 구현할 경우 신경 써야 하는 것들:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비밀번호 암호화 및 안전한 저장&lt;/li&gt;
&lt;li&gt;세션 관리 및 세션 고정 공격(Session Fixation) 방지&lt;/li&gt;
&lt;li&gt;CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 방지&lt;/li&gt;
&lt;li&gt;CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유) 설정&lt;/li&gt;
&lt;li&gt;Remember-Me 기능&lt;/li&gt;
&lt;li&gt;OAuth2 / JWT 기반 인증&lt;/li&gt;
&lt;li&gt;권한별 접근 제어&lt;/li&gt;
&lt;li&gt;보안 관련 HTTP 헤더 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 것을 직접 구현하면 코드가 복잡해지고, 보안 취약점이 발생할 가능성이 높다. &lt;b&gt;Spring Security&lt;/b&gt;는 Spring 기반 애플리케이션의 인증과 인가를 담당하는 보안 프레임워크로, 이러한 보안 기능들을 검증된 방식으로 제공한다. 덕분에 개발자는 비즈니스 로직에 집중할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 핵심 아키텍처: 두 컨테이너와 필터 체인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security의 동작 원리를 이해하려면 전체 구조를 먼저 파악해야 한다. &lt;br /&gt;그런데 그 전에, Spring Security가 &lt;b&gt;왜&lt;/b&gt; 이런 구조를 가지게 되었는지를 이해하기 위해 &quot;컨테이너&quot;라는 개념부터 짚고 넘어가자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 컨테이너(Container)란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는 &lt;b&gt;&quot;객체의 생명주기를 대신 관리해주는 환경&quot;&lt;/b&gt;이라고 이해하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 직접 new로 객체를 만들고, 필요 없으면 버리는 대신, &lt;br /&gt;컨테이너가 &quot;이 객체를 언제 만들고, 언제 초기화하고, 언제 제거할지&quot;를 알아서 관리해주는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 웹 애플리케이션에는 이런 컨테이너가 &lt;b&gt;두 개&lt;/b&gt; 공존한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 서블릿 컨테이너 (Servlet Container)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot로 프로젝트를 실행하면 내장 Tomcat이 자동으로 뜨는데, 이 Tomcat이 바로 서블릿 컨테이너이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 컨테이너가 관리하는 것들은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Servlet&lt;/b&gt; &amp;mdash; HTTP 요청을 받아서 처리하는 객체. Spring MVC의 DispatcherServlet도 결국 하나의 Servlet이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Filter&lt;/b&gt; &amp;mdash; Servlet에 요청이 도달하기 전에 가로채는 객체.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Listener&lt;/b&gt; &amp;mdash; 특정 이벤트(서버 시작, 세션 생성 등)를 감지하는 객체.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은, 서블릿 컨테이너는 &lt;b&gt;Java EE(Jakarta EE) 표준 스펙&lt;/b&gt;에 따라 동작한다는 것이다. &lt;br /&gt;Tomcat은 Spring이 뭔지 모른다. 그냥 &quot;Servlet 스펙에 맞는 객체&quot;만 관리한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;┌─── 서블릿 컨테이너 (Tomcat) ───────────────────┐
│                                            │
│  Filter A &amp;rarr; Filter B &amp;rarr; DispatcherServlet   │
│                                            │
│  &quot;나는 Servlet, Filter, Listener만 안다.      │
│   Spring Bean? 그게 뭔데?&quot;                   │
│                                            │
└────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.3 스프링 컨테이너 (Spring Container)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApplicationContext&lt;/b&gt;라고도 부른다. &lt;br /&gt;@Component, @Service, @Repository, @Bean 등으로 등록하는 모든 객체(Bean)를 생성하고 관리하는 주체이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;┌─── 스프링 컨테이너 (ApplicationContext) ───────┐
│                                            │
│  @Service UserService                      │
│  @Repository UserRepository                │
│  @Component JwtTokenProvider               │
│  @Bean SecurityFilterChain                 │
│  @Bean PasswordEncoder                     │
│                                            │
│  &quot;나는 Spring Bean을 관리한다.                 │
│   Servlet Filter? 그건 내 관할이 아닌데?&quot;       │
│                                            │
└────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security의 모든 보안 로직(SecurityFilterChain, AuthenticationManager, 각종 Provider 등)은 &lt;b&gt;Spring Bean&lt;/b&gt;이다. &lt;br /&gt;즉, 스프링 컨테이너 안에 산다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.4 두 컨테이너의 충돌: 문제 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 문제가 보인다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;HTTP 요청이 들어오면

   서블릿 컨테이너가 먼저 처리한다
   (Filter들이 순서대로 실행됨)
         │
         ▼
   그 다음에 DispatcherServlet에 도달한다
   (여기서부터 Spring 세계)
         │
         ▼
   Controller &amp;rarr; Service &amp;rarr; Repository
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Filter는 서블릿 컨테이너가 관리&lt;/b&gt;하는 것이고, &lt;br /&gt;&lt;b&gt;Spring Security의 보안 로직은 스프링 컨테이너가 관리&lt;/b&gt;하는 Bean이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Spring Security는 Filter 단계에서 보안 처리를 하고 싶다. &lt;br /&gt;Controller에 도달하기 &lt;b&gt;전에&lt;/b&gt; 인증/인가를 해야 하니까. &lt;br /&gt;하지만 서블릿 컨테이너는 Spring Bean을 모른다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.5 해결: DelegatingFilterProxy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 &lt;b&gt;DelegatingFilterProxy&lt;/b&gt;가 존재한다. &lt;br /&gt;이 녀석은 &lt;b&gt;서블릿 필터이면서 Spring Bean에게 실제 작업을 위임(delegate)하는 다리 역할&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;┌─── 서블릿 컨테이너 ────────────────────────────────────────────┐
│                                                            │
│  Filter A                                                  │
│     │                                                      │
│  DelegatingFilterProxy  ──── &quot;나는 서블릿 필터인데,             │
│     │                         실제 일은 Spring Bean에게       │
│     │                         시킬게&quot;                        │
│     │                                                      │
│     │    ┌─── 스프링 컨테이너 ─────────────────────────┐       │
│     └───▶│  FilterChainProxy (Spring Bean)         │       │
│          │    ├── SecurityFilter 1                 │       │
│          │    ├── SecurityFilter 2                 │       │
│          │    ├── SecurityFilter 3                 │       │
│          │    └── ...                              │       │
│          └─────────────────────────────────────────┘       │
│     │                                                      │
│  Filter C                                                  │
│     │                                                      │
│  DispatcherServlet                                         │
└────────────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DelegatingFilterProxy는 &lt;b&gt;서블릿 컨테이너 입장에서는 평범한 Filter&lt;/b&gt;이다. 그래서 서블릿 컨테이너가 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 녀석은 자기가 직접 보안 처리를 하지 않는다. &lt;br /&gt;대신 스프링 컨테이너에서 FilterChainProxy라는 Bean을 찾아서, 요청 처리를 위임(delegate)한다. &lt;br /&gt;이름 그대로 &quot;위임하는 필터 프록시&quot;인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 서블릿 컨테이너의 Filter 자리에 끼어들면서도, 실제 보안 로직은 Spring이 관리하는 Bean들이 수행할 수 있게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.6 FilterChainProxy와 SecurityFilterChain&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FilterChainProxy&lt;/b&gt;는 Spring Security의 핵심 엔진이다. &lt;br /&gt;이 안에 하나 이상의 &lt;b&gt;SecurityFilterChain&lt;/b&gt;이 등록되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityFilterChain은 &quot;이 URL 패턴에 대해서는 이 필터들을 적용하라&quot;는 규칙의 묶음이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;FilterChainProxy
├── SecurityFilterChain (&quot;/api/**&quot;)
│   ├── CorsFilter
│   ├── CsrfFilter (비활성)
│   ├── JwtAuthenticationFilter
│   └── AuthorizationFilter
│
└── SecurityFilterChain (&quot;/**&quot;)
    ├── CorsFilter
    ├── CsrfFilter
    ├── UsernamePasswordAuthenticationFilter
    ├── SessionManagementFilter
    └── AuthorizationFilter
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시를 보면, /api/** 경로와 그 외 경로에 서로 다른 보안 정책을 적용할 수 있다. &lt;br /&gt;API 요청에는 JWT 기반 인증을, 웹 페이지 요청에는 세션 기반 인증을 적용하는 식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.7 두 컨테이너 정리&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 116px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;서블릿 컨테이너&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;스프링 컨테이너&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;대표 예시&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Tomcat, Jetty&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;ApplicationContext&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;관리 대상&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Servlet, Filter, Listener&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;@Component, @Bean 등 모든 Spring Bean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;기반 스펙&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Java EE / Jakarta EE&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;Spring Framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;요청 처리 순서&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;먼저 (Filter 단계)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;나중에 (Controller 단계)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 컨테이너는 별개의 세계이고, DelegatingFilterProxy가 &lt;b&gt;두 세계를 연결하는 다리&lt;/b&gt; 역할을 한다. &lt;br /&gt;Spring Security의 모든 필터가 Controller보다 먼저 실행될 수 있는 이유가 바로 이 구조 덕분이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.8 전체 아키텍처 다이어그램&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[클라이언트 요청]
       │
       ▼
┌─────────────────────────┐
│   Servlet Filter Chain  │  &amp;larr; 서블릿 컨테이너(Tomcat 등)가 관리
│   ┌───────────────────┐ │
│   │ DelegatingFilter  │ │  &amp;larr; Spring Security의 진입점
│   │ Proxy             │ │
│   └───────┬───────────┘ │
│           │             │
│   ┌───────▼───────────┐ │
│   │ FilterChainProxy  │ │  &amp;larr; 실제 Security 필터들을 관리
│   │ ┌───────────────┐ │ │
│   │ │ Security      │ │ │
│   │ │ Filter Chain  │ │ │  &amp;larr; 여러 보안 필터가 순서대로 실행
│   │ │ (15개 이상)     │ │ │
│   │ └───────────────┘ │ │
│   └───────────────────┘ │
└─────────────────────────┘
       │
       ▼
  [DispatcherServlet &amp;rarr; Controller]&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Security Filter Chain의 주요 필터들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityFilterChain 안에는 약 15개 이상의 필터가 &lt;b&gt;정해진 순서대로&lt;/b&gt; 실행된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 필터 실행 순서&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;요청(Request) 들어옴
    │
    ▼
[SecurityContextPersistenceFilter]
    │  &amp;rarr; 이전 요청에서 저장된 SecurityContext를 복원한다
    ▼
[CorsFilter]
    │  &amp;rarr; CORS 정책을 확인한다
    ▼
[CsrfFilter]
    │  &amp;rarr; CSRF 토큰을 검증한다
    ▼
[LogoutFilter]
    │  &amp;rarr; 로그아웃 요청이면 로그아웃 처리를 한다
    ▼
[UsernamePasswordAuthenticationFilter]
    │  &amp;rarr; 로그인 요청이면 아이디/비밀번호로 인증을 시도한다
    ▼
[DefaultLoginPageGeneratingFilter]
    │  &amp;rarr; 커스텀 로그인 페이지가 없으면 기본 로그인 페이지를 생성한다
    ▼
[BasicAuthenticationFilter]
    │  &amp;rarr; HTTP Basic 인증 헤더가 있으면 처리한다
    ▼
[SessionManagementFilter]
    │  &amp;rarr; 세션 관련 처리를 한다 (세션 고정 공격 방지 등)
    ▼
[ExceptionTranslationFilter]
    │  &amp;rarr; 보안 예외를 적절한 HTTP 응답으로 변환한다
    ▼
[AuthorizationFilter]
    │  &amp;rarr; 최종적으로 해당 리소스에 접근할 권한이 있는지 확인한다
    ▼
  Controller에 도달
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 필터는 자기가 처리할 요청이 아니면 아무것도 하지 않고 다음 필터로 넘긴다. &lt;br /&gt;예를 들어, UsernamePasswordAuthenticationFilter는 POST /login 요청일 때만 동작하고, 그 외의 요청은 그냥 통과시킨다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 인증(Authentication) 동작 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증은 Spring Security에서 가장 중요한 부분이다. &lt;br /&gt;사용자가 로그인할 때 내부에서 어떤 일이 벌어지는지 단계별로 살펴보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.1 인증 처리 흐름&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[사용자: POST /login (username, password)]
           │
           ▼
┌──────────────────────────────────────┐
│ UsernamePasswordAuthenticationFilter │
│   &amp;rarr; UsernamePasswordAuthentication   │
│     Token 생성 (미인증 상태)             │
└──────────────┬───────────────────────┘
               │
               ▼
┌──────────────────────────┐
│    AuthenticationManager │  &amp;larr; 인증 작업을 총괄하는 관리자
│    (ProviderManager)     │
└──────────────┬───────────┘
               │ 적절한 Provider에게 위임
               ▼
┌──────────────────────────┐
│  AuthenticationProvider  │  &amp;larr; 실제 인증 로직 수행
│  (DaoAuthentication      │
│   Provider)              │
│                          │
│  1. UserDetailsService   │  &amp;larr; DB에서 사용자 정보 조회
│     .loadUserByUsername()│
│  2. PasswordEncoder      │  &amp;larr; 비밀번호 일치 여부 확인
│     .matches()           │
└──────────────┬───────────┘
               │
          인증 성공 시
               │
               ▼
┌───────────────────────────┐
│ SecurityContextHolder     │
│  └── SecurityContext      │
│       └── Authentication  │  &amp;larr; 인증된 사용자 정보 저장
│            ├── Principal  │     (이후 어디서든 꺼내 쓸 수 있음)
│            ├── Credentials│
│            └── Authorities│
└───────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.2 각 구성 요소 상세 설명&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Authentication 객체&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Authentication&lt;/b&gt;은 인증 정보를 담는 객체이다. 크게 세 가지 정보를 가지고 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;속성&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Principal&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;인증된 사용자 자체를 나타낸다. 보통 UserDetails 객체가 들어간다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Credentials&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;비밀번호 등 인증에 사용된 자격 증명이다. 인증 후에는 보안을 위해 비워진다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Authorities&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사용자에게 부여된 권한 목록이다. ROLE_USER, ROLE_ADMIN 등이 들어간다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 객체는 인증 전에는 &quot;사용자가 인증을 요청한 정보&quot;를, 인증 후에는 &quot;인증이 완료된 사용자의 정보&quot;를 담는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AuthenticationManager&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AuthenticationManager&lt;/b&gt;는 인터페이스이며, 기본 구현체는 &lt;b&gt;ProviderManager&lt;/b&gt;이다. 이 녀석의 역할은 단순하다. &lt;br /&gt;자신이 가지고 있는 여러 AuthenticationProvider 중 해당 인증 요청을 처리할 수 있는 Provider를 찾아서 인증 작업을 위임하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 직접 인증하지 않고 Provider에게 위임할까? 인증 방식이 여러 가지일 수 있기 때문이다. 아이디/비밀번호 인증, OAuth2 인증, JWT 인증 등 각기 다른 방식을 각각의 Provider가 처리하도록 분리한 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AuthenticationProvider&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AuthenticationProvider&lt;/b&gt;는 실제로 인증 로직을 수행하는 컴포넌트이다. &lt;br /&gt;가장 일반적으로 사용되는 구현체는 &lt;b&gt;DaoAuthenticationProvider&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DaoAuthenticationProvider는 내부적으로 두 가지를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) UserDetailsService&lt;/b&gt; &amp;mdash; 사용자 정보를 어디서 가져올지를 정의한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 인터페이스를 구현하여 DB, LDAP, 메모리 등 원하는 곳에서 사용자 정보를 가져올 수 있다. &lt;br /&gt;가장 흔한 패턴은 DB에서 사용자를 조회하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) PasswordEncoder&lt;/b&gt; &amp;mdash; 비밀번호를 안전하게 비교한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력한 비밀번호(평문)와 DB에 저장된 비밀번호(암호화된 형태)를 비교한다. Spring Security는 기본적으로 &lt;b&gt;BCryptPasswordEncoder&lt;/b&gt;를 권장한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SecurityContextHolder&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증에 성공하면 Authentication 객체가 &lt;b&gt;SecurityContext&lt;/b&gt;에 저장되고, &lt;br /&gt;이 SecurityContext는 &lt;b&gt;SecurityContextHolder&lt;/b&gt;가 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityContextHolder는 기본적으로 &lt;b&gt;ThreadLocal&lt;/b&gt; 전략을 사용한다. ThreadLocal이란 각 쓰레드(Thread)마다 독립적인 저장 공간을 제공하는 기술이다. 웹 요청은 각각 별도의 쓰레드에서 처리되므로, 요청 A의 인증 정보와 요청 B의 인증 정보가 섞이지 않는다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 현재 인증된 사용자 정보를 꺼내는 방법
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities = authentication.getAuthorities();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 Controller, Service 등 어디에서든 현재 요청의 인증된 사용자 정보에 접근할 수 있게 해준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 인가(Authorization) 동작 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증이 완료된 후, 사용자가 특정 리소스에 접근할 수 있는 &lt;b&gt;권한&lt;/b&gt;이 있는지 확인하는 과정이 인가이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.1 권한의 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 권한은 &lt;b&gt;GrantedAuthority&lt;/b&gt; 인터페이스로 표현된다. 보통 문자열 형태이며, 두 가지 관례가 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;유형&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;형식&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;예시&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;용도&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Role&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;ROLE_ 접두사 필수&lt;/td&gt;
&lt;td&gt;ROLE_USER, ROLE_ADMIN&lt;/td&gt;
&lt;td&gt;역할 기반의 넓은 권한 구분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Authority&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;접두사 없음&lt;/td&gt;
&lt;td&gt;READ_ARTICLE, DELETE_USER&lt;/td&gt;
&lt;td&gt;세밀한 기능 단위 권한 구분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROLE_ 접두사는 Spring Security가 내부적으로 Role을 구분하기 위한 관례이다. hasRole(&quot;ADMIN&quot;)이라고 작성하면, 내부적으로 ROLE_ADMIN을 가지고 있는지 확인한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2 인가 처리 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 두 가지 수준에서 인가를 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) URL 기반 인가 (HTTP Request Level)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecurityFilterChain 설정에서 URL 패턴별로 접근 권한을 지정한다. AuthorizationFilter가 이를 처리한다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -&amp;gt; auth
        // /api/admin/** 경로는 ADMIN 역할만 접근 가능
        .requestMatchers(&quot;/api/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
        // /api/user/** 경로는 USER 또는 ADMIN 역할이 접근 가능
        .requestMatchers(&quot;/api/user/**&quot;).hasAnyRole(&quot;USER&quot;, &quot;ADMIN&quot;)
        // /api/public/** 경로는 누구나 접근 가능 (인증 불필요)
        .requestMatchers(&quot;/api/public/**&quot;).permitAll()
        // 그 외 모든 요청은 인증 필요
        .anyRequest().authenticated()
    );
    return http.build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 메서드 기반 인가 (Method Level)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러나 서비스의 메서드에 어노테이션을 붙여서 권한을 체크한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@PreAuthorize(&quot;hasRole('ADMIN')&quot;)
public void deleteUser(Long userId) {
    // ADMIN 역할이 있는 사용자만 이 메서드를 호출할 수 있다
}

@PreAuthorize(&quot;hasRole('USER') and #userId == authentication.principal.id&quot;)
public UserProfile getProfile(Long userId) {
    // USER 역할이면서, 자기 자신의 프로필만 조회할 수 있다
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@PreAuthorize는 메서드 실행 &lt;b&gt;전&lt;/b&gt;에 권한을 확인하고, @PostAuthorize는 메서드 실행 &lt;b&gt;후&lt;/b&gt;에 결과를 보고 권한을 확인한다. 메서드 레벨 보안을 사용하려면 @EnableMethodSecurity 어노테이션을 설정 클래스에 추가해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. Spring Security + JWT 통합 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이론을 바탕으로, Spring Security 위에서 JWT 기반 인증을 구현하는 방법을 살펴보자. 최근 프로젝트에서는 서버 렌더링 대신 &lt;b&gt;프론트엔드(React, Vue 등) + 백엔드 API&lt;/b&gt; 구조가 일반적이고, 이 경우 세션 대신 JWT를 사용하는 것이 보편적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.1 세션 vs JWT 비교&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&amp;nbsp;&lt;/td&gt;
&lt;td&gt;세션 기반&amp;nbsp;&lt;/td&gt;
&lt;td&gt;JWT 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상태 저장 위치&lt;/td&gt;
&lt;td&gt;서버 메모리&lt;/td&gt;
&lt;td&gt;클라이언트 (토큰 자체)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateless 여부&lt;/td&gt;
&lt;td&gt;Stateful&lt;/td&gt;
&lt;td&gt;Stateless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장성&lt;/td&gt;
&lt;td&gt;서버 간 세션 공유 필요&lt;/td&gt;
&lt;td&gt;서버 간 공유 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안 취약점&lt;/td&gt;
&lt;td&gt;세션 하이재킹&lt;/td&gt;
&lt;td&gt;토큰 탈취&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주 사용 환경&lt;/td&gt;
&lt;td&gt;서버 렌더링 웹&lt;/td&gt;
&lt;td&gt;SPA + REST API, 모바일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.2 JWT 인증 흐름 (Spring Security 관점)&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[로그인 요청]
POST /api/auth/login { username, password }
         │
         ▼
   서버: 인증 성공 &amp;rarr; JWT 토큰 생성 &amp;rarr; 클라이언트에 반환
         │
         ▼
[이후 모든 API 요청]
GET /api/user/profile
Header: Authorization: Bearer eyJhbGciOiJI...
         │
         ▼
   서버: JWT 검증 &amp;rarr; SecurityContext에 인증 정보 설정 &amp;rarr; 요청 처리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.3 JWT 인증 필터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 필터는 매 요청마다 실행되며, 요청 헤더에 JWT가 있으면 검증하고 SecurityContext에 인증 정보를 설정한다. &lt;br /&gt;Spring Security의 필터 체인에 끼워넣는 커스텀 필터이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  OncePerRequestFilter란? 하나의 HTTP 요청에 대해 딱 한 번만 실행되는 것을 보장하는 필터 기반 클래스이다. &lt;br /&gt;일반 Filter는 포워딩(forward) 등으로 같은 요청 내에서 여러 번 실행될 수 있지만, OncePerRequestFilter는 이를 방지한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain filterChain)
            throws ServletException, IOException {

        // 1. 요청 헤더에서 토큰 추출
        String token = resolveToken(request);

        // 2. 토큰이 존재하고 유효하면
        if (token != null &amp;amp;&amp;amp; jwtTokenProvider.validateToken(token)) {

            // 3. 토큰에서 사용자 이름 추출
            String username = jwtTokenProvider.getUsername(token);

            // 4. DB에서 사용자 정보 조회
            UserDetails userDetails =
                userDetailsService.loadUserByUsername(username);

            // 5. 인증 객체 생성 (이미 토큰으로 인증되었으므로 credentials는 null)
            UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.getAuthorities()
                );

            // 6. SecurityContext에 인증 정보 저장
            SecurityContextHolder.getContext()
                .setAuthentication(authentication);
        }

        // 7. 다음 필터로 요청 전달
        filterChain.doFilter(request, response);
    }

    /**
     * Authorization 헤더에서 &quot;Bearer &quot; 접두사를 제거하고 토큰만 추출한다.
     */
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if (bearerToken != null &amp;amp;&amp;amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.4 JWT용 SecurityConfig&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class JwtSecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
            // REST API이므로 CSRF 비활성화
            .csrf(csrf -&amp;gt; csrf.disable())

            // 세션을 사용하지 않음 (JWT는 stateless)
            .sessionManagement(session -&amp;gt; session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            // URL별 접근 권한
            .authorizeHttpRequests(auth -&amp;gt; auth
                .requestMatchers(&quot;/api/auth/**&quot;).permitAll()
                .anyRequest().authenticated()
            )

            // JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 추가
            // 이렇게 하면 JWT 필터가 먼저 실행되어 토큰 기반 인증을 처리한다
            .addFilterBefore(jwtAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 .addFilterBefore() 메서드이다. &lt;br /&gt;이 메서드로 우리가 만든 JWT 필터를 Spring Security 필터 체인의 원하는 위치에 삽입할 수 있다. &lt;br /&gt;여기서는 UsernamePasswordAuthenticationFilter 앞에 넣었으므로, 폼 로그인 필터보다 먼저 JWT 인증을 시도하게 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. Access Token &amp;amp; Refresh Token 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 토큰을 두 종류로 나눠 사용한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;토큰 종류&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;유효 기간&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;Access Token&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;짧게 (15분 ~ 1시간)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;API 호출 시 인증에 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;Refresh Token&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;길게 (7일 ~ 30일)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;만료된 Access Token 재발급에 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 두 종류를 쓰는가?&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access Token만 사용하면:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유효 기간이 짧으면 &amp;rarr; 사용자가 자주 로그인해야 함  &lt;/li&gt;
&lt;li&gt;유효 기간이 길면 &amp;rarr; 토큰 탈취 시 오랫동안 악용 가능  &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Refresh Token을 도입하면:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access Token은 짧은 수명 &amp;rarr; 탈취되어도 피해 최소화&lt;/li&gt;
&lt;li&gt;Refresh Token으로 새 Access Token 자동 발급 &amp;rarr; 사용자 편의성 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Access Token 만료 시 재발급 흐름:

클라이언트                        서버
    │  GET /api/data              │
    │  Authorization: Bearer [만료된 AT] 
    │ ────────────────────────&amp;gt;   │
    │  401 Unauthorized           │
    │ &amp;lt;────────────────────────   │
    │                             │
    │  POST /api/token/refresh    │
    │  { refreshToken: &quot;eyJ...&quot; } │
    │ ────────────────────────&amp;gt;   │
    │                             │ Refresh Token 검증
    │  200 OK                     │
    │  { accessToken: &quot;eyJ...&quot; }  │
    │ &amp;lt;────────────────────────   │
    │                             │
    │  GET /api/data              │
    │  Authorization: Bearer [새 AT] 
    │ ────────────────────────&amp;gt;   │&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;14. CORS와 CSRF 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.1 CORS (Cross-Origin Resource Sharing)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS는 &lt;b&gt;다른 출처(Origin)의 리소스에 접근하는 것을 제어하는 브라우저의 보안 정책&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;출처(Origin)&quot;란 &lt;b&gt;프로토콜 + 도메인 + 포트&lt;/b&gt;의 조합이다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;https://example.com:443    &amp;larr; 이것이 하나의 출처
http://example.com:80      &amp;larr; 프로토콜이 다르므로 다른 출처
https://api.example.com    &amp;larr; 도메인이 다르므로 다른 출처
https://example.com:8080   &amp;larr; 포트가 다르므로 다른 출처
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드가 http://localhost:3000에서 실행되고, 백엔드 API가 http://localhost:8080에서 실행되면, &lt;br /&gt;포트가 다르므로 &lt;b&gt;다른 출처&lt;/b&gt;이다. 브라우저는 기본적으로 다른 출처로의 요청을 차단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Security에서 CORS 설정:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -&amp;gt; cors.configurationSource(corsConfigurationSource()))
        // ... 나머지 설정
    ;
    return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();

    config.setAllowedOrigins(List.of(&quot;http://localhost:3000&quot;));
    config.setAllowedMethods(List.of(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;));
    config.setAllowedHeaders(List.of(&quot;*&quot;));
    config.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source =
        new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration(&quot;/**&quot;, config);

    return source;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.2 CSRF (Cross-Site Request Forgery)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSRF는 &lt;b&gt;사용자가 의도하지 않은 요청을 악의적인 사이트가 대신 보내는 공격&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 시나리오:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 은행 사이트에 로그인한 상태이다 (세션 쿠키가 브라우저에 존재).&lt;/li&gt;
&lt;li&gt;사용자가 악의적인 사이트를 방문한다.&lt;/li&gt;
&lt;li&gt;악의적인 사이트에 숨겨진 코드가 은행 사이트로 송금 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;브라우저가 자동으로 세션 쿠키를 포함시키므로, 은행 서버는 정상적인 요청으로 인식한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 기본적으로 CSRF 방어가 활성화되어 있다. &lt;b&gt;CSRF 토큰&lt;/b&gt;이라는 임의의 값을 발행하고, 모든 상태 변경 요청(POST, PUT, DELETE 등)에 이 토큰을 포함하도록 요구한다. 악의적인 사이트는 이 토큰을 알 수 없으므로 공격이 차단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;언제 CSRF를 비활성화해도 되는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST API가 세션 쿠키 대신 JWT를 사용하고, 클라이언트가 매 요청마다 Authorization 헤더에 토큰을 직접 넣는 경우에는 CSRF 공격이 성립하지 않는다. 브라우저가 자동으로 토큰을 보내지 않기 때문이다. 따라서 이 경우 .csrf(csrf -&amp;gt; csrf.disable())로 비활성화해도 안전하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;15. 예외 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서 발생하는 보안 예외는 크게 두 가지이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;예외&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;발생 상황&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;HTTP 상태 코드&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;AuthenticationException&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;인증 실패 (로그인 실패, 토큰 만료 등)&lt;/td&gt;
&lt;td&gt;401 Unauthorized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;AccessDeniedException&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;인가 실패 (권한 부족)&lt;/td&gt;
&lt;td&gt;403 Forbidden&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ExceptionTranslationFilter&lt;/b&gt;가 이 예외들을 잡아서 적절한 응답으로 변환한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  핵심 요약&lt;/h2&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;JWT = Header.Payload.Signature
       (Base64Url 인코딩)

✅ 서명으로 위조 방지
✅ 서버가 상태를 저장하지 않음 (Stateless)
✅ 확장성 뛰어남 (Scale-out, MSA)

⚠️ Payload는 암호화되지 않으므로 민감정보 금지
⚠️ 만료 전 강제 무효화 어려움 &amp;rarr; Refresh Token + 블랙리스트로 보완
⚠️ 비밀키는 절대 외부 노출 금지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security에서의 핵심 흐름:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;요청 &amp;rarr; DelegatingFilterProxy &amp;rarr; FilterChainProxy
     &amp;rarr; SecurityFilterChain (JWT 필터 포함)
     &amp;rarr; JWT 검증 &amp;rarr; SecurityContext에 인증 정보 저장
     &amp;rarr; Controller에 도달
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청이 들어오면 &lt;b&gt;필터 체인&lt;/b&gt;을 순서대로 통과한다.&lt;/li&gt;
&lt;li&gt;JWT 인증 필터에서 &lt;b&gt;토큰을 검증하고 사용자가 누구인지&lt;/b&gt; 확인한다.&lt;/li&gt;
&lt;li&gt;인가 필터에서 &lt;b&gt;이 사용자가 이 리소스에 접근할 수 있는지&lt;/b&gt; 확인한다.&lt;/li&gt;
&lt;li&gt;모든 검증을 통과하면 Controller에 요청이 도달한다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Learning Log</category>
      <category>JWT</category>
      <category>Spring Security</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/35</guid>
      <comments>https://allluck777.tistory.com/35#entry35comment</comments>
      <pubDate>Mon, 9 Mar 2026 01:15:57 +0900</pubDate>
    </item>
    <item>
      <title>[멋사 클라우드 5기] Day 25 - Spring Data JPA (3)</title>
      <link>https://allluck777.tistory.com/34</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fetch Type&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계를 설정할 때 반드시 확인해야하는 부분이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;즉시 로딩 (Eager Loading)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관된 엔티티를 &lt;b&gt;즉시 함께 조회&lt;/b&gt; 한다. FetchType.EAGER 로 설정한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.EAGER) // 기본값
private Customer customer;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- Order를 조회할 때 Customer도 JOIN해서 함께 가져옴
SELECT o.*, c.*
FROM orders o
LEFT JOIN customer c ON o.customer_id = c.id
WHERE o.id = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제: 지금 당장 Customer 정보가 필요 없어도 항상 JOIN 쿼리가 실행된다. &lt;br /&gt;연관된 엔티티가 많을수록 불필요한 데이터를 많이 가져와 성능이 저하된다. 특히 N+1 문제가 발생하기 쉽다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;지연 로딩 (Lazy Loading)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관된 엔티티를 &lt;b&gt;실제로 사용할 때&lt;/b&gt; 조회한다. FetchType.LAZY 로 설정한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.LAZY)  // 권장
private Customer customer;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Order order = orderRepository.findById(1L).get();
// 이 시점: order만 조회, customer는 아직 조회 안 함

String customerName = order.getCustomer().getName();
// 이 시점: getName()이 호출되는 순간 Customer SELECT SQL 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate는 지연 로딩을 구현하기 위해 프록시(Proxy)객체를 사용한다. order.getCustomer() 가 반환하는 것은 실제 Customer 객체가 아니라, 실제 조회를 미루는 가짜 프록시 객체다. 실제 데이터에 접근하는 순간 DB에서 조회한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 fetch 전략&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;어노테이션&lt;/td&gt;
&lt;td&gt;기본 fetch 전략&lt;/td&gt;
&lt;td&gt;&lt;b&gt;권장 설정&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ManyToOne&lt;/td&gt;
&lt;td&gt;EAGER&lt;/td&gt;
&lt;td&gt;&lt;b&gt;LAZY로 변경 권장&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@OneToOne&lt;/td&gt;
&lt;td&gt;EAGER&lt;/td&gt;
&lt;td&gt;&lt;b&gt;LAZY로 변경 권장&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@OneToMany&lt;/td&gt;
&lt;td&gt;LAZY&lt;/td&gt;
&lt;td&gt;LAZY 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ManyToMany&lt;/td&gt;
&lt;td&gt;LAZY&lt;/td&gt;
&lt;td&gt;LAZY 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 원칙: 모든 연관관계는 LAZY 로딩으로 설정 하고, 필요한 경우에만 페치 조인(FETCH JOIN)으로 한 번에 가져오는 것이 성능 최적화의 기본이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;N+1 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LAZY 로딩 덕분에 불필요한 데이터를 미리 안 가져오는 건 좋은데, &lt;br /&gt;&lt;b&gt;막상 연관 데이터가 필요한 순간 쿼리가 폭발적으로 늘어나는&lt;/b&gt; 부작용이 생긴다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상황&lt;/h4&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Customer (1) ──── Order (N) ──── Product (1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 아래 데이터가 있다고 가정하자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[customer]             [orders]
┌────┬──────────┐      ┌────┬─────────────┬────────────┐
│ id │ name     │      │ id │ customer_id │ product_id │
├────┼──────────┤      ├────┼─────────────┼────────────┤
│  1 │ 홍길동     │      │  1 │      1      │     1      │
│  2 │ 김철수     │      │  2 │      1      │     2      │
│  3 │ 이영희     │      │  3 │      2      │     1      │
└────┴──────────┘      │  4 │      3      │     3      │
                       └────┴─────────────┴────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;N+1이 터지는 코드&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// OrderService.java
@Transactional(readOnly = true)
public void printAllOrders() {
    List&amp;lt;Order&amp;gt; orders = orderRepository.findAll();
    // ① 이 시점: SELECT * FROM orders &amp;rarr; 쿼리 1번

    for (Order order : orders) {
        System.out.println(order.getCustomer().getName());
        // ② 이 시점: SELECT * FROM customer WHERE id = ?
        //    &amp;rarr; order마다 1번씩, 총 4번 추가 실행
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 실행되는 SQL을 보면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- ① findAll() 실행 시
SELECT * FROM orders;

-- ② 루프를 돌며 각 order의 customer를 LAZY 로딩
SELECT * FROM customer WHERE id = 1;
SELECT * FROM customer WHERE id = 1;  -- 이미 조회했는데 또 나감 (1차 캐시 덕분에 실제론 생략되지만)
SELECT * FROM customer WHERE id = 2;
SELECT * FROM customer WHERE id = 3;
-- 총 1 + 3 = 4번 (고객이 중복되지 않을 경우 기준)
-- 최악의 경우 1 + N번
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 문제인가? Order가 1,000개라면 SQL이 1,001번 실행된다. &lt;br /&gt;각 SQL은 DB 커넥션을 열고 닫는 네트워크 비용을 수반하기 때문에, 성능이 급격히 저하된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자들이 이 문제를 풀려고 시도하면서 세 가지 접근법이 등장했는데, 각각 &lt;b&gt;해결 방식이 다르고, 장단점도 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책 1️⃣ Fetch Join&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핵심 아이디어&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;처음 조회할 때, 연관된 엔티티를 JOIN으로 한 번에 다 가져오자&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL의 JOIN과 개념은 같다. &lt;br /&gt;차이점은 JPA에서 LAZY로 설정된 연관관계도 &lt;b&gt;강제로 즉시 함께 로딩&lt;/b&gt;한다는 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL에서 JOIN FETCH 키워드를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// OrderRepository.java
public interface OrderRepository extends JpaRepository&amp;lt;Order, Long&amp;gt; {

    // 일반 JPQL &amp;mdash; LAZY 설정이므로 customer는 나중에 따로 조회됨
    @Query(&quot;SELECT o FROM Order o&quot;)
    List&amp;lt;Order&amp;gt; findAll();

    // Fetch Join JPQL &amp;mdash; customer를 즉시 함께 조회
    @Query(&quot;SELECT o FROM Order o JOIN FETCH o.customer&quot;)
    List&amp;lt;Order&amp;gt; findAllWithCustomer();

    // 여러 연관관계를 한 번에 Fetch Join
    @Query(&quot;SELECT o FROM Order o JOIN FETCH o.customer JOIN FETCH o.product&quot;)
    List&amp;lt;Order&amp;gt; findAllWithCustomerAndProduct();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제로 실행되는 SQL&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- findAllWithCustomer() 실행 시 &amp;rarr; 쿼리 딱 1번
SELECT o.*, c.*
FROM orders o
INNER JOIN customer c ON o.customer_id = c.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order와 Customer를 한 번의 SQL로 한 번에 가져온다. 루프를 돌며 추가 SQL이 실행되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 이제 N+1이 발생하지 않음
@Transactional(readOnly = true)
public void printAllOrders() {
    List&amp;lt;Order&amp;gt; orders = orderRepository.findAllWithCustomer();
    // ① 이 시점: 위의 JOIN SQL 1번 실행, customer까지 모두 로딩 완료

    for (Order order : orders) {
        System.out.println(order.getCustomer().getName());
        // ② 추가 SQL 없음! 이미 다 메모리에 있다
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Fetch Join의 한계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편리하지만 단점도 있다. 반드시 알아야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점 1: 컬렉션(1:N) Fetch Join + 페이징은 함께 쓸 수 없다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OneToMany 관계(컬렉션)를 Fetch Join하면서 동시에 페이징(LIMIT, OFFSET)을 사용하면 &lt;br /&gt;Hibernate가 경고를 띄우고 &lt;b&gt;모든 데이터를 메모리에 올린 후 페이징&lt;/b&gt;을 처리한다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;// ❌ 위험한 코드 &amp;mdash; 데이터가 많으면 OOM(Out of Memory) 발생 가능
@Query(&quot;SELECT c FROM Customer c JOIN FETCH c.orders&quot;)
Page&amp;lt;Customer&amp;gt; findAllWithOrders(Pageable pageable);
// &amp;rarr; Hibernate 경고: &quot;HHH90003004: firstResult/maxResults specified with collection fetch&quot;
// &amp;rarr; 전체 데이터를 메모리에 올린 후 Java에서 페이징 처리 &amp;rarr; 매우 위험
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 일이 생기냐면, JOIN FETCH 로 1:N 관계를 조인하면 결과 행(row)이 뻥튀기되기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[orders JOIN customer 결과]
┌──────────┬──────────┐
│ order_id │ customer │
├──────────┼──────────┤
│    1     │  홍길동    │ &amp;larr; customer 홍길동이
│    2     │  홍길동    │ &amp;larr; 두 번 등장 (order 2개)
│    3     │  김철수    │
└──────────┴──────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 입장에서는 행이 3개지만, Java 입장에서 Customer는 2명이다. &lt;br /&gt;DB 레벨에서 LIMIT 1 을 하면 Customer 1명이 아니라 row 1개만 자르기 때문에, 결과가 뒤틀린다. &lt;br /&gt;그래서 Hibernate는 메모리에 전부 올린 다음 Java에서 직접 자른다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책: 컬렉션(1:N) Fetch Join과 페이징은 함께 쓰지 말 것. 이 상황에서는 BatchSize 를 사용하는 것이 정답이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점 2: 두 개 이상의 컬렉션을 동시에 Fetch Join할 수 없다&lt;/h4&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;// ❌ 불가능 &amp;mdash; MultipleBagFetchException 발생
@Query(&quot;SELECT c FROM Customer c JOIN FETCH c.orders JOIN FETCH c.reviews&quot;)
List&amp;lt;Customer&amp;gt; findAllWithOrdersAndReviews();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;List 타입 컬렉션을 두 개 이상 동시에 Fetch Join하면 Hibernate가 예외를 던진다. (Set으로 바꾸면 가능하지만 카테시안 곱(Cartesian Product) 문제가 생긴다.)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단점 3: JPQL을 직접 작성해야 한다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 @Query 에 JPQL을 직접 써야 하기 때문에, 쿼리 메서드처럼 선언적으로 사용할 수 없다. 코드가 다소 복잡해진다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;언제 쓰면 좋을까?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단일 연관관계(N:1, 1:1)&lt;/b&gt; 를 함께 조회할 때 &amp;rarr; 가장 깔끔하고 효율적&lt;/li&gt;
&lt;li&gt;&lt;b&gt;페이징이 필요 없는&lt;/b&gt; 컬렉션 조회&lt;/li&gt;
&lt;li&gt;성능이 중요한 핵심 쿼리에서 &lt;b&gt;명시적으로 제어&lt;/b&gt;하고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책 2️⃣ EntityGraph&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핵심 아이디어&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Fetch Join이랑 결과는 같은데, JPQL 없이 어노테이션으로 선언하고 싶다&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityGraph는 Fetch Join과 &lt;b&gt;동일하게 JOIN을 통해 연관 엔티티를 함께 로딩&lt;/b&gt;한다. &lt;br /&gt;차이는 JPQL 대신 &lt;b&gt;어노테이션&lt;/b&gt;으로 설정한다는 것이다. &lt;br /&gt;내부적으로 Hibernate는 EntityGraph를 보고 Fetch Join SQL을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 1: @EntityGraph 어노테이션 &amp;mdash; 즉석에서 선언&lt;/h4&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// OrderRepository.java
public interface OrderRepository extends JpaRepository&amp;lt;Order, Long&amp;gt; {

    // attributePaths에 함께 로딩할 연관관계 필드명을 적는다
    @EntityGraph(attributePaths = {&quot;customer&quot;})
    List&amp;lt;Order&amp;gt; findAll();

    // 여러 개도 가능
    @EntityGraph(attributePaths = {&quot;customer&quot;, &quot;product&quot;})
    List&amp;lt;Order&amp;gt; findAllBy();

    // 기존 쿼리 메서드에도 그냥 붙일 수 있다
    @EntityGraph(attributePaths = {&quot;customer&quot;})
    List&amp;lt;Order&amp;gt; findByCustomerId(Long customerId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제로 실행되는 SQL&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- findAll() with @EntityGraph 실행 시 &amp;rarr; Fetch Join과 동일한 SQL
SELECT o.*, c.*
FROM orders o
LEFT OUTER JOIN customer c ON o.customer_id = c.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Fetch Join vs EntityGraph SQL 차이&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fetch Join: INNER JOIN (기본)&lt;/li&gt;
&lt;li&gt;EntityGraph: LEFT OUTER JOIN (기본)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LEFT OUTER JOIN이기 때문에 연관 엔티티가 null인 경우에도 Order가 조회된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 2: @NamedEntityGraph &amp;mdash; Entity 클래스에 미리 정의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 재사용하는 그래프 패턴을 엔티티 클래스에 이름을 붙여 정의해두는 방법이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// Order.java
@Entity
@Table(name = &quot;orders&quot;)
@NamedEntityGraph(
    name = &quot;Order.withCustomerAndProduct&quot;,       // 그래프에 이름 부여
    attributeNodes = {
        @NamedAttributeNode(&quot;customer&quot;),
        @NamedAttributeNode(&quot;product&quot;)
    }
)
public class Order {
    // ... 필드들
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// OrderRepository.java
public interface OrderRepository extends JpaRepository&amp;lt;Order, Long&amp;gt; {

    // 이름으로 참조해서 사용
    @EntityGraph(&quot;Order.withCustomerAndProduct&quot;)
    List&amp;lt;Order&amp;gt; findAll();

    @EntityGraph(&quot;Order.withCustomerAndProduct&quot;)
    Optional&amp;lt;Order&amp;gt; findById(Long id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EntityGraph의 한계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityGraph도 내부적으로 JOIN을 사용하기 때문에 &lt;b&gt;Fetch Join과 동일한 한계&lt;/b&gt; 를 공유한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컬렉션(1:N) + 페이징 조합 시 메모리 페이징 문제 발생&lt;/li&gt;
&lt;li&gt;두 개 이상 컬렉션 동시 로딩 시 문제 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 EntityGraph만의 단점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JPQL과 함께 쓰면 충돌&lt;/b&gt;이 생기기 쉽다. @Query 에 이미 JOIN이 있는데 EntityGraph도 붙이면 예상치 못한 SQL이 생성될 수 있다.&lt;/li&gt;
&lt;li&gt;Fetch Join보다 &lt;b&gt;동작이 덜 명시적&lt;/b&gt;이라 디버깅이 어려울 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Fetch Join과의 선택 기준&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상황&lt;/td&gt;
&lt;td&gt;권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;쿼리 재사용이 많고, 선언적으로 관리하고 싶다&lt;/td&gt;
&lt;td&gt;EntityGraph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복잡한 조건의 JPQL과 함께 사용한다&lt;/td&gt;
&lt;td&gt;Fetch Join&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능 튜닝이 중요하고 SQL을 직접 제어하고 싶다&lt;/td&gt;
&lt;td&gt;Fetch Join&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Data JPA의 쿼리 메서드에 간단히 붙이고 싶다&lt;/td&gt;
&lt;td&gt;EntityGraph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;언제 쓰면 좋을까?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기존 쿼리 메서드에 빠르게 Fetch 전략을 추가&lt;/b&gt;하고 싶을 때&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자주 재사용되는 로딩 패턴&lt;/b&gt;을 @NamedEntityGraph 로 이름 붙여 관리할 때&lt;/li&gt;
&lt;li&gt;JPQL 없이 &lt;b&gt;선언형으로 연관관계 로딩을 제어&lt;/b&gt;하고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책 3️⃣ BatchSize&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핵심 아이디어&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;한 방에 JOIN으로 가져오는 게 아니라, 여러 ID를 묶어서 IN 쿼리로 한 번에 처리하자&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch Join과 EntityGraph는 &lt;b&gt;JOIN&lt;/b&gt; 으로 N+1을 해결했다. BatchSize는 완전히 다른 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LAZY 로딩은 유지하되, &lt;b&gt;연관 엔티티를 하나씩 가져오는 대신, 여러 개의 ID를 모아서 WHERE id IN (...) 쿼리로 한 번에 가져온다.&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;작동 방식 &amp;mdash; 단계별로 이해하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;batchSize = 100 으로 설정했다고 가정하자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;기존 N+1 방식:
① SELECT * FROM orders                    &amp;rarr; order 300개 조회
② SELECT * FROM customer WHERE id = 1    &amp;rarr; 1번
③ SELECT * FROM customer WHERE id = 2    &amp;rarr; 1번
④ SELECT * FROM customer WHERE id = 3    &amp;rarr; 1번
... (총 300번 추가)
&amp;rarr; 합계: 301번의 SQL

BatchSize = 100 방식:
① SELECT * FROM orders                              &amp;rarr; order 300개 조회
② SELECT * FROM customer WHERE id IN (1,2,...,100)  &amp;rarr; 100개씩 묶어서
③ SELECT * FROM customer WHERE id IN (101,...,200)  &amp;rarr; 1번
④ SELECT * FROM customer WHERE id IN (201,...,300)  &amp;rarr; 1번
&amp;rarr; 합계: 4번의 SQL (1 + 300/100 = 4번)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1이 N+1 &amp;rarr; 1 + (N / BatchSize) 로 줄어든다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 1: 전역 설정 (application.yml)&lt;/h4&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100  # 전체 프로젝트에 적용
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정 하나만 추가하면 &lt;b&gt;모든 LAZY 로딩 연관관계에 자동으로 BatchSize가 적용&lt;/b&gt;된다. 가장 간단하고 실무에서 많이 쓰는 방법이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;방법 2: 특정 필드에만 @BatchSize 어노테이션&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// Customer.java
@Entity
public class Customer {

    @OneToMany(mappedBy = &quot;customer&quot;)
    @BatchSize(size = 100)  // 이 컬렉션에만 적용
    private List&amp;lt;Order&amp;gt; orders = new ArrayList&amp;lt;&amp;gt;();
}

// Order.java
@Entity
public class Order {

    @ManyToOne(fetch = FetchType.LAZY)
    @BatchSize(size = 50)   // 이 연관관계에만 적용
    @JoinColumn(name = &quot;customer_id&quot;)
    private Customer customer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BatchSize가 유용한 상황 &amp;mdash; 페이징과의 조합&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BatchSize의 가장 큰 장점은 &lt;b&gt;컬렉션(1:N) 조회 + 페이징을 동시에 해결&lt;/b&gt;한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch Join은 컬렉션 + 페이징을 함께 쓰면 메모리 페이징 문제가 생겼다. BatchSize는 이 문제가 없다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// ✅ BatchSize + 페이징 &amp;mdash; 완벽하게 동작
@Transactional(readOnly = true)
public Page&amp;lt;Customer&amp;gt; getCustomersWithOrders(Pageable pageable) {

    // ① 먼저 Customer를 페이징으로 조회 (SQL 1번)
    Page&amp;lt;Customer&amp;gt; customers = customerRepository.findAll(pageable);
    // &amp;rarr; SELECT * FROM customer LIMIT 10 OFFSET 0

    // ② customers의 orders에 접근하는 순간 BatchSize IN 쿼리 실행
    customers.getContent().forEach(c -&amp;gt; c.getOrders().size());
    // &amp;rarr; SELECT * FROM orders WHERE customer_id IN (1, 2, 3, ..., 10)
    // &amp;rarr; 페이징된 10명의 orders를 한 번에 가져옴

    return customers;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 실행되는 SQL (총 2번)

-- ① 페이징 Customer 조회
SELECT * FROM customer LIMIT 10 OFFSET 0;

-- ② BatchSize IN 쿼리로 10명의 orders 한 번에 조회
SELECT * FROM orders WHERE customer_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이징도 정확하고, SQL도 2번뿐이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BatchSize 크기는 어떻게?&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;일반적으로 100 ~ 1000 사이를 권장한다.

너무 작으면 (예: 10):
  IN 쿼리가 자주 나가서 성능 개선 효과가 줄어든다

너무 크면 (예: 10000):
  IN 절에 들어가는 ID가 너무 많아 DB 쿼리 자체가 무거워진다
  일부 DB는 IN 절 개수 제한이 있다 (Oracle: 1000개)

실무 기본값: 100
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle 사용 시 주의: Oracle은 IN 절에 최대 1000개까지만 허용한다. &lt;br /&gt;default_batch_fetch_size = 1000 이 Oracle에서의 사실상 최대값이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BatchSize의 한계&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JOIN이 아닌 &lt;b&gt;별도의 SELECT 쿼리를 추가로 실행&lt;/b&gt;한다. 즉, SQL이 완전히 1번이 되진 않는다.&lt;/li&gt;
&lt;li&gt;연관 엔티티 데이터가 정말 많을 때는 IN 쿼리 자체가 무거워질 수 있다.&lt;/li&gt;
&lt;li&gt;Fetch Join처럼 &lt;b&gt;&quot;이 쿼리에서는 반드시 함께 가져와야 한다&quot;&lt;/b&gt; 는 확신이 없을 때, 예상치 못한 시점에 IN 쿼리가 실행될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;언제 쓰면 좋을까?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;컬렉션(1:N) 관계 + 페이징&lt;/b&gt;이 함께 필요한 상황 &amp;rarr; BatchSize가 사실상 유일한 해결책&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전역 설정&lt;/b&gt;으로 프로젝트 전체의 LAZY 로딩 성능을 간단히 끌어올리고 싶을 때&lt;/li&gt;
&lt;li&gt;Fetch Join의 &lt;b&gt;컬렉션 2개 이상 동시 로딩 제한&lt;/b&gt;을 우회하고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fetch 방식 비교&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핵심 차이 한눈에 보기&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Fetch&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Join&lt;/td&gt;
&lt;td&gt;EntityGraph&lt;/td&gt;
&lt;td&gt;BatchSize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;해결 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;JOIN으로 한 번에&lt;/td&gt;
&lt;td&gt;JOIN으로 한 번에&lt;/td&gt;
&lt;td&gt;IN 쿼리로 묶어서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SQL 실행 횟수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1번&lt;/td&gt;
&lt;td&gt;1번&lt;/td&gt;
&lt;td&gt;1 + N/size 번&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;설정 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;JPQL @Query&lt;/td&gt;
&lt;td&gt;어노테이션&lt;/td&gt;
&lt;td&gt;yml 또는 어노테이션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;컬렉션 + 페이징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;❌ 위험&lt;/td&gt;
&lt;td&gt;❌ 위험&lt;/td&gt;
&lt;td&gt;✅ 안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;컬렉션 2개 동시&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;❌ 불가&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;코드 복잡도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;중간 (JPQL 작성)&lt;/td&gt;
&lt;td&gt;낮음 (어노테이션)&lt;/td&gt;
&lt;td&gt;매우 낮음 (전역 설정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SQL 제어력&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;OSIV&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSIV는 &lt;b&gt;Open Session In View&lt;/b&gt;의 줄임말이다. &lt;br /&gt;Hibernate에서는 Session, JPA에서는 EntityManager라고 부르지만 같은 개념이다. &lt;br /&gt;쉽게 말하면 &lt;b&gt;&quot;영속성 컨텍스트를 언제까지 열어둘 것인가&quot;&lt;/b&gt;에 대한 설정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;배경 &amp;mdash; 트랜잭션이 끝나면 무슨 일이 생기나&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 영속성 컨텍스트는 트랜잭션과 생명주기를 같이 한다고 했다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;트랜잭션 시작 &amp;rarr; 영속성 컨텍스트 생성
트랜잭션 종료 &amp;rarr; 영속성 컨텍스트 종료 &amp;rarr; 엔티티가 준영속 상태
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 일반적인 Spring MVC 요청 흐름을 보면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;HTTP 요청
    &amp;darr;
Controller
    &amp;darr;
Service  &amp;larr; @Transactional, 여기서 트랜잭션 시작/종료
    &amp;darr;
Controller  &amp;larr; 트랜잭션 이미 끝남
    &amp;darr;
View / Response 변환  &amp;larr; 트랜잭션 이미 끝남
    &amp;darr;
HTTP 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service에서 트랜잭션이 끝나면 영속성 컨텍스트도 닫힌다. &lt;br /&gt;그 이후 Controller나 View에서 엔티티의 LAZY 로딩을 시도하면 어떻게 될까?&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Service
@Transactional
public Order getOrder(Long id) {
    return orderRepository.findById(id).get();
    // 트랜잭션 종료 &amp;rarr; 영속성 컨텍스트 닫힘
}

// Controller
public ResponseEntity&amp;lt;?&amp;gt; getOrder(Long id) {
    Order order = orderService.getOrder(id);
    order.getCustomer().getName(); // ❌ LazyInitializationException 발생!
    // 영속성 컨텍스트가 이미 닫혀있어서 LAZY 로딩 불가
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려고 등장한 것이 OSIV다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OSIV On &amp;mdash; 영속성 컨텍스트를 요청 끝까지 열어두기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSIV를 켜면 영속성 컨텍스트의 생명주기가 트랜잭션이 아닌 &lt;b&gt;HTTP 요청 전체&lt;/b&gt;로 늘어난다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;OSIV ON

HTTP 요청 &amp;rarr; [영속성 컨텍스트 시작]
    &amp;darr;
Controller
    &amp;darr;
Service (@Transactional 시작/종료)
    &amp;darr;
Controller  &amp;larr; 영속성 컨텍스트 살아있음
    &amp;darr;
View  &amp;larr; 영속성 컨텍스트 살아있음 &amp;rarr; LAZY 로딩 가능
    &amp;darr;
HTTP 응답 &amp;rarr; [영속성 컨텍스트 종료]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Controller에서 LAZY 로딩 가능
Order order = orderService.getOrder(id);
order.getCustomer().getName(); // ✅ 영속성 컨텍스트 살아있으니 동작함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot의 OSIV 기본값은 &lt;b&gt;true&lt;/b&gt;, 즉 켜져 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OSIV On의 문제점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편하지만 치명적인 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DB 커넥션을 너무 오래 점유한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;OSIV ON일 때 DB 커넥션 점유 시간

HTTP 요청 들어옴  &amp;rarr; DB 커넥션 획득
    &amp;darr;
Service 로직 (DB 작업)
    &amp;darr;
Controller 로직 (DB 작업 없음)
    &amp;darr;
JSON 직렬화 (DB 작업 없음)
    &amp;darr;
HTTP 응답 나감  &amp;rarr; DB 커넥션 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 작업이 없는 구간에도 커넥션을 잡고 있다. 트래픽이 많아지면 커넥션 풀이 고갈되고 장애로 이어질 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OSIV Off&lt;/h4&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application.yml
spring:
  jpa:
    open-in-view: false
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;OSIV OFF

HTTP 요청
    &amp;darr;
Controller
    &amp;darr;
Service (@Transactional 시작)
    &amp;rarr; DB 커넥션 획득
    &amp;rarr; 로직 실행
    &amp;rarr; 트랜잭션 종료 &amp;rarr; 영속성 컨텍스트 종료
    &amp;rarr; DB 커넥션 반환  &amp;larr; 여기서 바로 반환
    &amp;darr;
Controller  &amp;larr; 영속성 컨텍스트 없음
    &amp;darr;
HTTP 응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 커넥션을 실제로 필요한 순간에만 점유한다. 훨씬 효율적이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OSIV Off의 문제점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 밖, 즉 Service를 벗어나면 LAZY 로딩이 불가능하다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// ❌ OSIV Off 상태에서 Controller에서 LAZY 로딩 시도
Order order = orderService.getOrder(id);
order.getCustomer().getName(); // LazyInitializationException 발생!
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 방법이 두 가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법 1 &amp;mdash; Service 안에서 미리 다 로딩하고 나오기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;@Transactional
public Order getOrder(Long id) {
    Order order = orderRepository.findById(id).get();
    order.getCustomer().getName(); // 트랜잭션 안에서 미리 LAZY 로딩 강제
    return order;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;방법 2 &amp;mdash; DTO로 변환해서 반환하기 (권장)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Transactional
public OrderResponse getOrder(Long id) {
    Order order = orderRepository.findById(id).get();
    return new OrderResponse(order); // 트랜잭션 안에서 DTO로 변환
    // DTO는 순수 Java 객체라 영속성 컨텍스트와 무관
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO로 변환하면 영속성 컨텍스트가 닫혀도 아무 문제가 없다. &lt;br /&gt;&lt;b&gt;OSIV를 끄면 DTO 분리가 사실상 강제된다&lt;/b&gt;고 볼 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결론&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;OSIV On&lt;/td&gt;
&lt;td&gt;&amp;nbsp;OSIV Off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;영속성 컨텍스트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;요청 끝까지 유지&lt;/td&gt;
&lt;td&gt;트랜잭션 안에서만 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;LAZY 로딩&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;어디서든 가능&lt;/td&gt;
&lt;td&gt;트랜잭션 안에서만 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DB 커넥션&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;요청 내내 점유&lt;/td&gt;
&lt;td&gt;필요한 순간만 점유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;실무 권장&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;❌ 트래픽 많으면 위험&lt;/td&gt;
&lt;td&gt;✅ 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;주의사항&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;Controller에서 LAZY 로딩 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 &lt;b&gt;OSIV를 끄고, Service 안에서 DTO로 변환해서 반환하는 패턴&lt;/b&gt;을 권장한다.&lt;/p&gt;</description>
      <category>Learning Log</category>
      <category>Java</category>
      <category>JPA</category>
      <category>spring</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/34</guid>
      <comments>https://allluck777.tistory.com/34#entry34comment</comments>
      <pubDate>Thu, 5 Mar 2026 23:18:38 +0900</pubDate>
    </item>
    <item>
      <title>[멋사 클라우드 5기] Day 24 - Spring Data JPA (2)</title>
      <link>https://allluck777.tistory.com/33</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 연관관계 매핑이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현실 세계에서 &lt;b&gt;고객(Customer)&lt;/b&gt; 과 &lt;b&gt;상품(Product)&lt;/b&gt; 은 서로 관계가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 고객은 여러 상품을 주문할 수 있다.&lt;/li&gt;
&lt;li&gt;한 상품은 여러 고객에게 주문될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관계를 Java 객체로 표현하면 &lt;b&gt;참조(Reference)&lt;/b&gt;가 되고, DB로 표현하면 &lt;b&gt;외래 키(Foreign Key)&lt;/b&gt;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연관관계 매핑&lt;/b&gt;은 이 두 세계를 JPA 어노테이션으로 연결하는 작업이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[Java 세계]                    [DB 세계]
Customer &amp;rarr; Product    &amp;harr;    customer_id FK
                            product_id FK
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 연관관계의 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계는 4가지로 나뉜다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;종류&lt;/td&gt;
&lt;td&gt;어노테이션&lt;/td&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다대일 (N:1)&lt;/td&gt;
&lt;td&gt;@ManyToOne&lt;/td&gt;
&lt;td&gt;여러 주문 &amp;rarr; 한 고객&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일대다 (1:N)&lt;/td&gt;
&lt;td&gt;@OneToMany&lt;/td&gt;
&lt;td&gt;한 고객 &amp;rarr; 여러 주문&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일대일 (1:1)&lt;/td&gt;
&lt;td&gt;@OneToOne&lt;/td&gt;
&lt;td&gt;고객 &amp;rarr; 고객 상세정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;다대다 (N:M)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;@ManyToMany&lt;/td&gt;
&lt;td&gt;&lt;b&gt;여러 고객 &amp;harr; 여러 상품&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Customer와 Product의 관계는 &lt;b&gt;다대다(N:M)&lt;/b&gt; 다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 고객이 여러 상품을 살 수 있고, 한 상품이 여러 고객에게 팔릴 수 있으니까.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념 먼저 잡기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방향 (Direction)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계에는 &lt;b&gt;단방향(Unidirectional)&lt;/b&gt;과 &lt;b&gt;양방향(Bidirectional)&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 단방향: Customer가 Product를 알지만, Product는 Customer를 모른다
class Customer {
    List&amp;lt;Product&amp;gt; products; // Customer &amp;rarr; Product (일방통행)
}

class Product {
    // Customer에 대한 참조 없음
}

// 양방향: 서로가 서로를 안다
class Customer {
    List&amp;lt;Product&amp;gt; products; // Customer &amp;rarr; Product
}

class Product {
    List&amp;lt;Customer&amp;gt; customers; // Product &amp;rarr; Customer
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 테이블은 외래 키 하나로 양쪽 조인이 가능하다. &lt;br /&gt;하지만 Java 객체는 참조가 없으면 반대 방향으로 탐색이 불가능하다. &lt;br /&gt;양방향은 사실 단방향 2개를 연결한 것이라고 이해하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연관관계의 주인 (Owner)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향 관계에서는 반드시 &lt;b&gt;&quot;누가 외래 키를 관리할 것인가&quot;&lt;/b&gt;를 정해야 한다. &lt;br /&gt;이것이 &lt;b&gt;연관관계의 주인(Owner)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;규칙:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주인 쪽&lt;/b&gt;: 외래 키를 직접 관리한다. mappedBy 속성을 쓰지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비주인 쪽&lt;/b&gt;: 읽기 전용이다. mappedBy = &quot;상대 클래스의 필드명&quot; 을 반드시 붙여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// 주인: Order (orders 테이블에 customer_id FK가 있음)
@ManyToOne
@JoinColumn(name = &quot;customer_id&quot;)  // 주인 &amp;rarr; @JoinColumn 사용
private Customer customer;

// 비주인: Customer (mappedBy로 주인이 누구인지 알려줌)
@OneToMany(mappedBy = &quot;customer&quot;)  // 비주인 &amp;rarr; mappedBy 사용
private List&amp;lt;Order&amp;gt; orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  흔한 실수: 비주인 쪽에만 값을 세팅하면 DB에 반영이 안 된다. 주인 쪽에 값을 세팅해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@JoinColumn&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@JoinColumn 은 외래 키 컬럼의 이름을 지정하는 어노테이션이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@JoinColumn(name = &quot;customer_id&quot;)
// &amp;rarr; DB에서 외래 키 컬럼 이름이 &quot;customer_id&quot;가 됨
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 일대다 / 다대일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다대다로 넘어가기 전에, 이해를 돕기 위해 1:N 관계를 먼저 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;한 Customer가 여러 Order(주문)를 가진다&quot;&lt;/b&gt; 는 1:N 관계다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Customer (1) ──────── Order (N)
            1개의 고객이 여러 주문을 가짐
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
public class Customer {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // 1:N 관계 &amp;mdash; Customer가 비주인 (mappedBy 사용)
    @OneToMany(mappedBy = &quot;customer&quot;)
    private List&amp;lt;Order&amp;gt; orders = new ArrayList&amp;lt;&amp;gt;();
}

@Entity
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // N:1 관계 &amp;mdash; Order가 주인 (@JoinColumn으로 FK 관리)
    @ManyToOne
    @JoinColumn(name = &quot;customer_id&quot;)
    private Customer customer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관계에서 &lt;b&gt;DB 테이블&lt;/b&gt;은 아래처럼 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[customer 테이블]          [orders 테이블]
┌────┬────────┐           ┌────┬─────────────┐
│ id │ name   │           │ id │ customer_id │  &amp;larr; FK
├────┼────────┤           ├────┼─────────────┤
│  1 │ 홍길동   │           │  1 │      1      │
│  2 │ 김철수   │           │  2 │      1      │
└────┴────────┘           │  3 │      2      │
                          └────┴─────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외래 키(customer_id)는 &lt;b&gt;N 쪽(orders 테이블)&lt;/b&gt;에 존재한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  원칙: 외래 키는 항상 &quot;N 쪽&quot; 테이블에 존재한다. &lt;br /&gt;따라서 연관관계의 주인도 보통 N 쪽(더 구체적인 쪽)이 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 다대다 (N:M) &amp;mdash; @ManyToMany의 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본론이다. Customer와 Product의 관계는 &lt;b&gt;다대다(N:M)&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Customer (N) ──────── Product (M)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB에서 N:M은 직접 표현이 불가능하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 DB는 N:M을 직접 표현할 수 없다. &lt;br /&gt;반드시 &lt;b&gt;중간 테이블(Junction Table / 연결 테이블)&lt;/b&gt; 이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[customer 테이블]    [customer_product 테이블]    [product 테이블]
┌────┬────────┐      ┌─────────────┬────────────┐  ┌────┬──────────┐
│ id │ name   │      │ customer_id │ product_id │  │ id │ name     │
├────┼────────┤      ├─────────────┼────────────┤  ├────┼──────────┤
│  1 │ 홍길동   │      │      1      │     1      │  │  1 │ 노트북    │
│  2 │ 김철수   │      │      1      │     2      │  │  2 │ 마우스    │
└────┴────────┘      │      2      │     1      │  └────┴──────────┘
                     └─────────────┴────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ManyToMany로 간단히 표현하면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 @ManyToMany 어노테이션으로 N:M 관계를 쉽게 표현할 수 있게 해준다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Entity
public class Customer {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany
    @JoinTable(
        name = &quot;customer_product&quot;,             // 중간 테이블 이름
        joinColumns = @JoinColumn(name = &quot;customer_id&quot;),         // 이 엔티티의 FK
        inverseJoinColumns = @JoinColumn(name = &quot;product_id&quot;)   // 반대 엔티티의 FK
    )
    private List&amp;lt;Product&amp;gt; products = new ArrayList&amp;lt;&amp;gt;();
}

@Entity
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private int price;

    @ManyToMany(mappedBy = &quot;products&quot;) // 비주인
    private List&amp;lt;Customer&amp;gt; customers = new ArrayList&amp;lt;&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 간결하고 깔끔해 보인다. &lt;br /&gt;하지만 &lt;b&gt;실무에서는 @ManyToMany를 사용하지 않는다.&lt;/b&gt; 왜일까?&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@ManyToMany의 치명적인 문제점&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 1: 중간 테이블에 컬럼을 추가할 수 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스에서 &quot;주문 내역&quot;에는 주문 날짜, 수량, 가격 등 추가 정보가 필요하다. &lt;br /&gt;하지만 @ManyToMany 가 자동 생성한 중간 테이블에는 &lt;b&gt;외래 키 두 개 외에 컬럼을 추가할 방법이 없다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[실제로 필요한 테이블]
┌─────────────┬────────────┬──────────────┬──────┬────────┐
│ customer_id │ product_id │ ordered_at   │ qty  │ price  │  &amp;larr; 이런 컬럼들이 필요!
├─────────────┼────────────┼──────────────┼──────┼────────┤
│      1      │     1      │  2024-01-15  │  2   │ 1200000│
└─────────────┴────────────┴──────────────┴──────┴────────┘

[@ManyToMany가 만드는 테이블]
┌─────────────┬────────────┐
│ customer_id │ product_id │  &amp;larr; FK 두 개만 존재, 추가 컬럼 불가
└─────────────┴────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 2: 예상치 못한 쿼리가 발생한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ManyToMany 는 내부적으로 복잡한 조인 쿼리와 중간 테이블 관련 SQL을 발생시키는데, 개발자가 이를 예측하고 제어하기 어렵다. &lt;br /&gt;이는 성능 문제로 이어지기 쉽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 올바른 다대다 해결책 &amp;mdash; 중간 테이블을 Entity로 승격&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 @ManyToMany 대신, &lt;b&gt;중간 테이블을 직접 Entity 클래스로 만드는 방법&lt;/b&gt; 을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Customer &amp;harr; Product 사이에 &lt;b&gt;Order (주문)&lt;/b&gt; 엔티티를 만드는 것이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Customer (1) ──── Order (N:M의 중간) ──── Product (1)
              N                       N
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Customer와 Order 는 1:N, Product와 Order 도 1:N 관계가 된다. &lt;br /&gt;N:M이 두 개의 1:N으로 분해된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 구조 설계&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[customer]        [orders]                                              [product]
┌────┬──────┐  ┌────┬─────────────┬────────────┬────────────┬──────┐  ┌────┬────────┬────────┐
│ id │ name │  │ id │ customer_id │ product_id │ ordered_at │ qty  │  │ id │ name   │ price  │
└────┴──────┘  └────┴─────────────┴────────────┴────────────┴──────┘  └────┴────────┴────────┘
     1 ──────────────── N                                N ──────────────────── 1&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Learning Log</category>
      <category>JPA</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/33</guid>
      <comments>https://allluck777.tistory.com/33#entry33comment</comments>
      <pubDate>Wed, 4 Mar 2026 23:34:52 +0900</pubDate>
    </item>
    <item>
      <title>[멋사 클라우드 5기] Day 23 - Spring Data JPA (1)</title>
      <link>https://allluck777.tistory.com/32</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 왜 JPA가 필요한가? &amp;mdash; 패러다임 불일치 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;객체 지향 vs 관계형 데이터베이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java는 &lt;b&gt;객체 지향 &lt;/b&gt;언어다. &lt;br /&gt;개발자는 현실 세계를 &lt;b&gt;객체(Object)&lt;/b&gt; 로 모델링하고, 객체 간의 관계를 &lt;b&gt;참조(Reference)&lt;/b&gt; 로 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 우리가 데이터를 저장하는 공간인 &lt;b&gt;RDBMS&lt;/b&gt;는 &lt;br /&gt;데이터를 &lt;b&gt;테이블&lt;/b&gt;과 &lt;b&gt;외래 키(Foreign Key)&lt;/b&gt; 로 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 세계는 근본적으로 사고방식이 다르다. &lt;br /&gt;이것을 &lt;b&gt;패러다임 불일치(Paradigm Mismatch)&lt;/b&gt; 라고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패러다임 불일치의 구체적인 예시&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  문제 1: 상속(Inheritance)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서는 클래스 간 상속이 자연스럽다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;// Java: 상속 관계
class Vehicle {
    Long id;
    String name;
}

class Car extends Vehicle {
    int doorCount;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 RDBMS에는 상속이라는 개념이 없다. &lt;br /&gt;Vehicle과 Car를 테이블로 표현하려면 다양한 전략이 필요하고, 이를 수동으로 처리하는 건 매우 번거롭다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  문제 2: 연관관계(Association)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 객체 간 관계는 &lt;b&gt;참조&lt;/b&gt; 로 표현된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// Java: 참조로 관계 표현
class Order {
    Member member; // Member 객체를 직접 참조
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS에서는 &lt;b&gt;외래 키(FK)&lt;/b&gt; 로 관계를 표현한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- SQL: FK로 관계 표현
CREATE TABLE orders (
    id BIGINT,
    member_id BIGINT  -- Member의 PK를 저장
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 객체는 order.getMember() 처럼 객체를 직접 꺼내오지만, SQL은 JOIN 을 통해 데이터를 합쳐야 한다. &lt;br /&gt;이 간극을 개발자가 직접 메워야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  문제 3: 동일성(Identity)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 같은 객체인지 비교할 때는 == 또는 equals() 를 쓴다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;Member m1 = memberDAO.findById(1L);
Member m2 = memberDAO.findById(1L);

m1 == m2; // false! 서로 다른 인스턴스
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 회원(id=1)을 두 번 조회해도, 매번 새로운 객체가 반환되어 == 비교 시 false 가 나온다. &lt;br /&gt;데이터베이스 입장에서 같은 행(row)이지만, Java 입장에서는 다른 객체다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패러다임 불일치를 직접 해결하면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 없이 JDBC만으로 위 문제들을 해결하려면, 개발자가 직접 SQL을 작성하고, &lt;br /&gt;ResultSet에서 데이터를 꺼내 객체에 매핑하는 &lt;b&gt;지루하고 반복적인 코드&lt;/b&gt; 를 수없이 작성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// JPA 없이 직접 작성해야 하는 코드 예시
public Member findById(Long id) {
    String sql = &quot;SELECT * FROM member WHERE id = ?&quot;;
    PreparedStatement ps = connection.prepareStatement(sql);
    ps.setLong(1, id);
    ResultSet rs = ps.executeQuery();
    
    Member member = new Member();
    member.setId(rs.getLong(&quot;id&quot;));
    member.setName(rs.getString(&quot;name&quot;));
    member.setEmail(rs.getString(&quot;email&quot;));
    // ... 필드가 많아질수록 코드가 늘어남
    return member;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 코드를 모든 테이블, 모든 CRUD 작업에 대해 작성해야한다...&lt;br /&gt;&lt;b&gt;ORM&lt;/b&gt; 은 바로 이 문제를 해결하기 위해 등장했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. ORM이란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ORM(Object-Relational Mapping)&lt;/b&gt; 은 &lt;b&gt;객체 세계와 관계형 데이터베이스 세계를 자동으로 연결해주는 기술&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM 프레임워크를 사용하면 개발자는 SQL을 직접 작성하지 않아도 된다. &lt;br /&gt;대신 &lt;b&gt;Java 객체를 조작&lt;/b&gt; 하면 ORM이 알아서 적절한 SQL을 생성하고 실행한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;개발자 &amp;rarr; Java 객체 조작 &amp;rarr; ORM &amp;rarr; SQL 자동 생성 &amp;rarr; RDBMS
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ORM의 핵심 아이디어는 &lt;b&gt;&quot;Java 클래스와 DB 테이블을 연결(매핑)하자&quot;&lt;/b&gt; 는 것이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Member 클래스  &amp;harr;  MEMBER 테이블
id 필드        &amp;harr;  ID 컬럼
name 필드      &amp;harr;  NAME 컬럼
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. JPA란 무엇인가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JPA = Java ORM의 표준 명세&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JPA(Java Persistence API, 혹은 Jakarta Persistence API)&lt;/b&gt;는 &lt;br /&gt;Java 진영에서 ORM 기술을 사용하기 위한 &lt;b&gt;표준 인터페이스(Interface) 모음&lt;/b&gt; 이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 포인트: JPA는 구현체(Implementation)가 아니라 명세(Specification) 다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마치 JDBC가 데이터베이스 연결을 위한 표준 인터페이스이고, 실제 구현은 MySQL Driver, PostgreSQL Driver 등이 담당하는 것처럼,&lt;br /&gt;JPA는 ORM의 표준 인터페이스이고, 실제 구현은 &lt;b&gt;Hibernate&lt;/b&gt;, EclipseLink, OpenJPA 등이 담당한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;JPA (인터페이스/명세)
    ├── Hibernate (가장 많이 쓰이는 구현체)
    ├── EclipseLink
    └── OpenJPA
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 사용한다는 것은 JPA가 정의한 인터페이스를 통해 ORM 기능을 사용한다는 뜻이다. &lt;br /&gt;구현체를 Hibernate에서 다른 것으로 바꿔도 코드가 크게 변하지 않는 이유가 바로 이 덕분이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JPA의 핵심 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 크게 세 가지 역할을 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;CRUD SQL 자동 생성&lt;/b&gt; &amp;mdash; persist(), find(), remove() 등의 메서드로 SQL을 자동 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과를 자동으로 객체에 매핑&lt;/b&gt; &amp;mdash; SQL 실행 결과를 Java 객체로 자동 변환한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패러다임 불일치 해결&lt;/b&gt; &amp;mdash; 상속, 연관관계, 객체 그래프 탐색 등을 처리한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 전체 구조 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 그림이 나타내는 구조를 층별로 이해해보자.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;┌──────────────────────────────────────┐
│        Spring Boot Application       │
└──────────────┬───────────────────────┘
               │ ↕
┌──────────────▼───────────────────────┐
│   ┌──────────────────────────────┐   │
│   │      Spring Data JPA         │   │  &amp;larr; 편의 레이어 (Repository 자동 구현)
│   └──────────────────────────────┘   │
│   ┌──────────────────────────────┐   │
│   │    Java Persistence API      │   │  &amp;larr; ORM 표준 명세 (인터페이스)
│   └──────────────────────────────┘   │
│   ┌──────────────────────────────┐   │
│   │         Hibernate            │   │  &amp;larr; JPA 구현체 (실제 ORM 동작)
│   └──────────────────────────────┘   │
│   ┌──────────────────────────────┐   │
│   │           JDBC               │   │  &amp;larr; DB와의 저수준 통신 담당
│   └──────────────────────────────┘   │
└──────────────┬───────────────────────┘
               │ ↕
        ┌──────▼──────┐
        │    RDBMS    │
        └─────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JDBC (Java Database Connectivity)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JDBC&lt;/b&gt; 는 Java 프로그램이 데이터베이스와 통신하기 위한 &lt;b&gt;가장 기본적인 API&lt;/b&gt;다. &lt;br /&gt;SQL 작성, Connection 열기, PreparedStatement 실행, ResultSet에서 데이터 꺼내기와 같은 모든 저수준 작업을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Java ORM은 내부적으로 JDBC를 사용한다. &lt;br /&gt;JPA, Hibernate 모두 최종적으로는 JDBC를 통해 DB와 통신한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hibernate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Hibernate&lt;/b&gt; 는 JPA 명세를 실제로 구현한 ORM 프레임워크다. &lt;br /&gt;JPA가 정의한 인터페이스의 실제 로직을 담고 있으며, &lt;br /&gt;SQL 자동 생성, 캐싱, 영속성 컨텍스트 관리 등의 핵심 기능을 제공한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java Persistence API (JPA)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 같은 구현체들이 공통적으로 따르는 &lt;b&gt;표준 인터페이스 집합&lt;/b&gt; 이다. &lt;br /&gt;EntityManager, @Entity, @Table 등이 JPA가 정의한 표준 요소들이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Data JPA&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 더 편리하게 사용할 수 있도록 &lt;b&gt;Spring 팀이 추가로 감싼 편의 레이어&lt;/b&gt; 다. &lt;br /&gt;Repository 인터페이스만 정의하면 기본 CRUD를 자동으로 구현해주는 등, 반복 코드를 크게 줄여준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Hibernate란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate는 현재 사실상 Java ORM의 표준 구현체로 자리 잡은 오픈소스 프레임워크다. &lt;br /&gt;JPA 명세의 거의 모든 기능을 구현하며, 추가적인 고급 기능도 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 spring-boot-starter-data-jpa 의존성을 추가하면, 기본적으로 Hibernate가 JPA 구현체로 자동 설정된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hibernate의 핵심 기능&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 118px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;자동 SQL 생성&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;객체 조작에 따라 INSERT/UPDATE/DELETE/SELECT SQL을 자동 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;영속성 컨텍스트&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;객체의 상태를 관리하는 1차 캐시 영역&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;지연 로딩 (Lazy Loading)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;연관된 객체를 실제로 사용할 때까지 DB 조회를 미룸&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;HQL / JPQL&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;객체 지향적인 쿼리 언어 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;캐싱 (2차 캐시)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;성능 향상을 위한 캐시 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Spring Data JPA란 무엇인가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JPA만 사용할 때의 불편함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 직접 사용하면 EntityManager 를 주입받아 매번 비슷한 CRUD 코드를 작성해야 한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// JPA EntityManager를 직접 사용하는 경우
@Repository
public class MemberRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    public void save(Member member) {
        em.persist(member);
    }
    
    public Member findById(Long id) {
        return em.find(Member.class, id);
    }
    
    public List&amp;lt;Member&amp;gt; findAll() {
        return em.createQuery(&quot;SELECT m FROM Member m&quot;, Member.class).getResultList();
    }
    
    public void delete(Member member) {
        em.remove(member);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;save, findById, findAll, delete 같은 기본 CRUD는 모든 엔티티에 반복적으로 등장한다. &lt;br /&gt;Member 뿐만 아니라 Order, Product, Review 등 엔티티마다 동일한 코드를 작성해야 한다면 매우 비효율적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Data JPA의 해결책&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Data JPA&lt;/b&gt; 는 이 반복을 제거한다. &lt;br /&gt;개발자가 &lt;b&gt;인터페이스만 선언&lt;/b&gt; 하면, Spring이 런타임에 자동으로 구현체를 만들어준다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// Spring Data JPA 사용 &amp;mdash; 이게 전부다!
public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
    // 기본 CRUD는 자동으로 제공됨
    // 필요한 쿼리만 메서드 이름으로 선언하면 됨
    List&amp;lt;Member&amp;gt; findByName(String name);
    Optional&amp;lt;Member&amp;gt; findByEmail(String email);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaRepository&amp;lt;Member, Long&amp;gt; 을 상속받는 것만으로 &lt;br /&gt;save(), findById(), findAll(), delete() 등 수십 개의 메서드가 자동으로 생긴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메서드 이름으로 쿼리 생성 (Query Method)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA의 강력한 기능 중 하나는 &lt;b&gt;메서드 이름만으로 쿼리를 자동 생성&lt;/b&gt; 해주는 것이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;// 메서드 이름 &amp;rarr; 자동 생성되는 SQL
findByName(String name)
// &amp;rarr; SELECT * FROM member WHERE name = ?

findByAgeGreaterThan(int age)
// &amp;rarr; SELECT * FROM member WHERE age &amp;gt; ?

findByNameAndEmail(String name, String email)
// &amp;rarr; SELECT * FROM member WHERE name = ? AND email = ?

findByNameContaining(String keyword)
// &amp;rarr; SELECT * FROM member WHERE name LIKE '%keyword%'
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Entity란 무엇인가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Entity의 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Entity(엔티티)&lt;/b&gt; 는 &lt;b&gt;데이터베이스 테이블과 매핑되는 Java 클래스&lt;/b&gt;다. &lt;br /&gt;JPA는 Entity 클래스를 보고 어떤 테이블에 어떤 컬럼으로 저장할지 파악한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import jakarta.persistence.*;

@Entity                          // 이 클래스가 JPA Entity임을 선언
@Table(name = &quot;member&quot;)          // 매핑할 테이블 이름 (생략하면 클래스 이름 사용)
public class Member {
    
    @Id                          // 기본 키(PK) 필드임을 선언
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // PK 자동 생성 전략
    private Long id;
    
    @Column(name = &quot;member_name&quot;, nullable = false, length = 50)
    private String name;         // DB 컬럼과 매핑
    
    @Column(unique = true)
    private String email;
    
    protected Member() {}        // JPA는 기본 생성자가 반드시 필요 (public 또는 protected)
    
    public Member(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
    // Getter/Setter...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 JPA 어노테이션&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;annotation&lt;/td&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Entity&lt;/td&gt;
&lt;td&gt;이 클래스가 JPA 관리 대상 엔티티임을 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Table&lt;/td&gt;
&lt;td&gt;매핑할 DB 테이블 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Id&lt;/td&gt;
&lt;td&gt;기본 키(Primary Key) 필드 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@GeneratedValue&lt;/td&gt;
&lt;td&gt;PK 자동 생성 전략 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Column&lt;/td&gt;
&lt;td&gt;컬럼 세부 설정 (이름, null 허용 여부, 길이 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ManyToOne, @OneToMany&lt;/td&gt;
&lt;td&gt;연관관계 매핑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Transient&lt;/td&gt;
&lt;td&gt;이 필드는 DB에 저장하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@GeneratedValue 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PK를 자동으로 생성할 때 4가지 전략이 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;전략&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;td&gt;주로 사용하는 DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDENTITY&lt;/td&gt;
&lt;td&gt;DB가 PK 자동 증가&lt;/td&gt;
&lt;td&gt;MySQL, MariaDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEQUENCE&lt;/td&gt;
&lt;td&gt;DB 시퀀스 사용&lt;/td&gt;
&lt;td&gt;Oracle, PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TABLE&lt;/td&gt;
&lt;td&gt;별도 키 생성 테이블 사용&lt;/td&gt;
&lt;td&gt;모든 DB (잘 안 씀)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AUTO&lt;/td&gt;
&lt;td&gt;DB에 따라 자동 선택&lt;/td&gt;
&lt;td&gt;기본값&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 영속성 컨텍스트 (Persistence Context)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA를 이해하는 데 있어 &lt;b&gt;가장 중요한 핵심&lt;/b&gt;이다. &lt;br /&gt;영속성 컨텍스트는 JPA의 1차 캐시, 쓰기 지연, 변경 감지를 위해 필수적이기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;영속성 컨텍스트란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;영속성 컨텍스트(Persistence Context)&lt;/b&gt; 는 &lt;b&gt;엔티티를 영구 저장하는 환경&lt;/b&gt;이다. &lt;br /&gt;쉽게 말하면 &lt;b&gt;JPA가 관리하는 엔티티들을 담아두는 메모리 공간&lt;/b&gt;이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; ️ 영속성 컨텍스트 = JPA가 관리하는 엔티티 저장소 (메모리 위)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컨텍스트는 &lt;b&gt;EntityManager&lt;/b&gt; 를 통해 접근한다. &lt;br /&gt;하나의 EntityManager는 하나의 영속성 컨텍스트를 가진다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;EntityManager &amp;harr; Persistence Context &amp;harr; DB
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EntityManager&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EntityManager&lt;/b&gt; 는 영속성 컨텍스트를 통해 엔티티를 관리하는 JPA의 핵심 인터페이스다. &lt;br /&gt;CRUD 작업, 트랜잭션, 쿼리 실행 등의 작업을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA를 쓰면 EntityManager를 직접 다룰 일은 거의 없지만, 내부적으로는 항상 EntityManager가 동작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션과 영속성 컨텍스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 영속성 컨텍스트의 생명주기는 기본적으로 &lt;b&gt;트랜잭션(Transaction)&lt;/b&gt;과 함께한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 시작&lt;/b&gt; &amp;rarr; 영속성 컨텍스트 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 커밋(commit)&lt;/b&gt; &amp;rarr; DB에 반영 + 영속성 컨텍스트 종료&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 롤백(rollback)&lt;/b&gt; &amp;rarr; 변경 취소 + 영속성 컨텍스트 종료&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션(Transaction) 이란? 데이터베이스의 작업 단위다. &quot;모두 성공하거나, 하나라도 실패하면 전부 취소&quot;하는 원자성(Atomicity)을 보장한다. @Transactional 어노테이션으로 선언한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Entity의 생명주기 (Life Cycle)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티는 영속성 컨텍스트와의 관계에 따라 4가지 상태를 가진다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;        new Member()
             │
             ▼
     ┌─────────────────┐
     │  비영속 상태       │  &amp;larr; JPA가 모르는 상태 (일반 Java 객체)
     │  (New/Transient)│
     └───────┬─────────┘
             │ em.persist(member)
             ▼
     ┌───────────────┐
     │   영속 상태     │  &amp;larr; JPA가 관리하는 상태 (영속성 컨텍스트에 저장됨)
     │  (Managed)    │◄──────────────────────────┐
     └─────────┬─────┘                           │
               │                           em.merge(member)
    em.detach()│                                 │
    em.clear() │                                 │
               ▼                                 │
     ┌───────────────┐                           │
     │  준영속 상태     │  &amp;larr; 한때 영속이었지만 분리됨     │
     │  (Detached)   │ ──────────────────────────┘
     └─────────┬─────┘
               │
    em.remove()│
               ▼
     ┌───────────────┐
     │   삭제 상태     │  &amp;larr; DB에서 삭제 예정
     │  (Removed)    │
     └───────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;각 상태 설명&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 비영속 (New / Transient)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 객체를 방금 new 로 생성한 상태다. &lt;br /&gt;JPA와 전혀 관계가 없다. 일반 Java 객체와 동일하다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;Member member = new Member(&quot;홍길동&quot;, &quot;hong@email.com&quot;);
// 이 시점의 member는 비영속 상태
// JPA는 이 객체의 존재를 모른다
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 영속 (Managed)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티가 영속성 컨텍스트에 저장된 상태다. &lt;br /&gt;JPA가 이 객체를 &lt;b&gt;추적하고 관리&lt;/b&gt; 한다. &lt;br /&gt;em.persist() 를 호출하거나, em.find() 로 DB에서 조회한 엔티티는 영속 상태가 된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;em.persist(member); // 영속 상태로 전환
// 또는
Member found = em.find(Member.class, 1L); // DB에서 조회 &amp;rarr; 영속 상태
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요: em.persist() 를 호출한다고 즉시 DB에 INSERT SQL이 실행되는 게 아니다. &lt;br /&gt;영속성 컨텍스트에 등록될 뿐이며, 실제 SQL은 트랜잭션 커밋 시점에 실행된다. (&amp;rarr; 쓰기 지연 참고)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 준영속 (Detached)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트에서 분리된 상태다. 한때 영속 상태였지만, JPA의 관리에서 벗어난 상태다. &lt;br /&gt;변경 감지, 지연 로딩 등의 JPA 기능이 동작하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;em.detach(member); // 특정 엔티티만 분리
em.clear();        // 영속성 컨텍스트 전체 초기화
em.close();        // EntityManager 종료
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 끝난 후, Controller나 View 계층에서 엔티티를 사용할 때 이 상태가 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 삭제 (Removed)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티를 삭제 요청한 상태다. 트랜잭션 커밋 시점에 DELETE SQL이 실행된다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;em.remove(member); // 삭제 상태로 전환
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 1차 캐시 (First-Level Cache)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1차 캐시란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트 내부에는 &lt;b&gt;Map&lt;/b&gt; 형태의 저장소가 있다. 이것이 바로 &lt;b&gt;1차 캐시&lt;/b&gt; 다. &lt;br /&gt;Key는 엔티티의 @Id 값(PK), Value는 엔티티 객체다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;영속성 컨텍스트 (1차 캐시)
┌─────────────────────────────────┐
│  Key (PK)  │  Value (Entity)    │
├────────────┼────────────────────┤
│  1L        │  Member{id=1, ...} │
│  2L        │  Member{id=2, ...} │
└─────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1차 캐시의 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;em.find() 로 엔티티를 조회할 때, JPA는 &lt;b&gt;먼저 1차 캐시를 확인&lt;/b&gt; 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 1차 캐시 동작 예시
Member m1 = em.find(Member.class, 1L); // DB 조회 &amp;rarr; 1차 캐시에 저장
Member m2 = em.find(Member.class, 1L); // 1차 캐시에서 반환 (DB 조회 없음!)

m1 == m2; // true! 같은 인스턴스
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째 find()&lt;/b&gt;: 1차 캐시에 없음 &amp;rarr; DB에 SELECT SQL 실행 &amp;rarr; 결과를 1차 캐시에 저장 &amp;rarr; 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 번째 find()&lt;/b&gt;: 1차 캐시에 있음 &amp;rarr; DB 조회 없이 캐시에서 바로 반환&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1차 캐시의 장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;불필요한 DB 조회 감소&lt;/b&gt; &amp;mdash; 동일한 트랜잭션 내에서 같은 엔티티를 여러 번 조회해도 SQL은 한 번만 실행된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동일성 보장&lt;/b&gt; &amp;mdash; 같은 PK로 조회한 엔티티는 항상 같은 인스턴스다 (== 비교 가능).&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1차 캐시의 범위&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 캐시는 &lt;b&gt;트랜잭션 단위로 존재&lt;/b&gt; 한다. 트랜잭션이 끝나면 영속성 컨텍스트가 종료되고 1차 캐시도 사라진다. &lt;br /&gt;애플리케이션 전체에서 공유되는 캐시가 아님을 주의해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2차 캐시(Second-Level Cache) 란? Hibernate는 여러 트랜잭션에 걸쳐 공유되는 2차 캐시도 지원한다. &lt;br /&gt;애플리케이션 레벨에서 공유되는 캐시로, 별도 설정이 필요하다. (예: EhCache, Redis 연동)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 쓰기 지연 (Write-Behind)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쓰기 지연이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 내부에 &lt;b&gt;쓰기 지연 SQL 저장소(ActionQueue)&lt;/b&gt; 를 가지고 있다. &lt;br /&gt;엔티티를 persist() 하거나 변경해도, SQL이 &lt;b&gt;즉시 DB로 전송되지 않는다&lt;/b&gt;. &lt;br /&gt;대신, SQL을 이 저장소에 모아두었다가 &lt;b&gt;트랜잭션 커밋 직전에 한꺼번에 DB로 전송&lt;/b&gt; 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 동작을 &lt;b&gt;쓰기 지연(Transactional Write-Behind)&lt;/b&gt; 이라고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쓰기 지연의 동작 과정&lt;/h3&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Transactional
public void saveMembersExample() {
    Member m1 = new Member(&quot;홍길동&quot;, &quot;hong@email.com&quot;);
    Member m2 = new Member(&quot;김철수&quot;, &quot;kim@email.com&quot;);
    
    em.persist(m1); // SQL 저장소에 INSERT 쌓기 (DB 전송 X)
    em.persist(m2); // SQL 저장소에 INSERT 쌓기 (DB 전송 X)
    
    // 이 시점까지 DB에는 아무 변화가 없음
    
} // 메서드 종료 &amp;rarr; 트랜잭션 커밋 &amp;rarr; flush() 실행 &amp;rarr; SQL 일괄 전송 &amp;rarr; DB 반영
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;1. em.persist(m1) 호출
   &amp;rarr; 영속성 컨텍스트에 m1 저장
   &amp;rarr; SQL 저장소: [INSERT INTO member VALUES (m1)]
   &amp;rarr; DB: 변화 없음

2. em.persist(m2) 호출
   &amp;rarr; 영속성 컨텍스트에 m2 저장
   &amp;rarr; SQL 저장소: [INSERT INTO member VALUES (m1), INSERT INTO member VALUES (m2)]
   &amp;rarr; DB: 변화 없음

3. 트랜잭션 커밋
   &amp;rarr; flush() 실행: 저장소의 SQL 일괄 DB 전송
   &amp;rarr; DB: m1, m2 저장 완료
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;flush()란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;flush()&lt;/b&gt; 는 영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업이다. 트랜잭션 커밋 시 자동으로 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flush()를 한다고 트랜잭션이 커밋되는 건 아니다. DB에 SQL을 전송할 뿐이며, 커밋은 별개다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쓰기 지연의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;성능 최적화&lt;/b&gt;: 여러 SQL을 하나의 배치(Batch)로 전송할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB와의 통신 횟수 감소&lt;/b&gt;: 10번 persist해도 DB 요청은 한 번.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;불필요한 DB 부하 제거&lt;/b&gt;: 취소될 수 있는 작업을 미리 DB에 보내지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 변경 감지 (Dirty Checking)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 감지란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA의 가장 마법 같은 기능 중 하나다. &lt;br /&gt;영속 상태의 엔티티를 &lt;b&gt;수정하기만 해도&lt;/b&gt;, 트랜잭션 커밋 시점에 &lt;b&gt;JPA가 자동으로 UPDATE SQL을 실행&lt;/b&gt; 한다. &lt;br /&gt;개발자가 직접 save() 나 update() 를 호출할 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void updateMember(Long id) {
    Member member = em.find(Member.class, id); // 영속 상태로 조회
    
    member.setName(&quot;새이름&quot;); // 그냥 setter 호출만 해도...
    
    // em.persist(member); // 이 코드가 없어도!
    // em.update(member);  // 이런 메서드도 없음!
    
} // 트랜잭션 커밋 &amp;rarr; JPA가 자동으로 UPDATE SQL 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 감지의 동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 어떻게 변경을 감지할까? 바로 &lt;b&gt;스냅샷(Snapshot)&lt;/b&gt; 을 활용한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;영속화 시점&lt;/b&gt;: 엔티티가 영속성 컨텍스트에 들어올 때, JPA는 해당 엔티티의 &lt;b&gt;초기 상태를 복사해 스냅샷으로 저장&lt;/b&gt; 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커밋 시점&lt;/b&gt;: flush()가 호출될 때, JPA는 현재 엔티티 상태와 스냅샷을 비교한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;차이 발생 시&lt;/b&gt;: 변경된 필드가 있으면 UPDATE SQL을 자동으로 생성해 실행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;영속성 컨텍스트
┌────────────────────────────────────────────┐
│ 1차 캐시                                     │
│  Member{id=1, name=&quot;홍길동&quot;, email=&quot;hong@&quot;}  │ &amp;larr; 현재 상태
│                                            │
│ 스냅샷                                       │
│  Member{id=1, name=&quot;홍길동&quot;, email=&quot;hong@&quot;}  │ &amp;larr; 최초 상태 (복사본)
└────────────────────────────────────────────┘

member.setName(&quot;김길동&quot;) 호출 후:

┌────────────────────────────────────────────┐
│ 1차 캐시                                     │
│  Member{id=1, name=&quot;김길동&quot;, email=&quot;hong@&quot;}  │ &amp;larr; 현재 상태 (변경됨)
│                                            │
│ 스냅샷                                       │
│  Member{id=1, name=&quot;홍길동&quot;, email=&quot;hong@&quot;}  │ &amp;larr; 최초 상태 (그대로)
└────────────────────────────────────────────┘
      &amp;darr; flush() 시 비교
UPDATE member SET name='김길동' WHERE id=1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변경 감지 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 감지는 &lt;b&gt;영속 상태의 엔티티에만 동작&lt;/b&gt; 한다. 트랜잭션 밖에서 엔티티를 수정하거나, &lt;br /&gt;준영속(Detached) 상태의 엔티티를 수정해도 UPDATE SQL은 실행되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;// ❌ 변경 감지 동작 안 함 (트랜잭션 없음)
Member member = memberRepository.findById(1L).get();
member.setName(&quot;새이름&quot;); // 준영속 상태 &amp;rarr; DB 반영 안 됨

// ✅ 변경 감지 동작 (트랜잭션 있음)
@Transactional
public void update(Long id) {
    Member member = memberRepository.findById(id).get(); // 영속 상태
    member.setName(&quot;새이름&quot;); // &amp;rarr; 커밋 시 UPDATE 자동 실행
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. Spring Data JPA 실전 사용법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 추가&lt;/h3&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- Maven --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-data-jpa&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.mysql&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mysql-connector-j&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;// Gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml 설정&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: update       # 애플리케이션 시작 시 테이블 자동 생성/수정
    show-sql: true            # 실행되는 SQL 로그 출력
    properties:
      hibernate:
        format_sql: true      # SQL 로그를 보기 좋게 포맷
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ddl-auto 옵션 설명&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;create: 시작 시 테이블 삭제 후 재생성 (데이터 유실 위험)&lt;/li&gt;
&lt;li&gt;update: 변경된 스키마만 반영 (기존 데이터 보존)&lt;/li&gt;
&lt;li&gt;validate: 스키마 검증만 (변경 없음)&lt;/li&gt;
&lt;li&gt;none: 아무것도 안 함&lt;/li&gt;
&lt;li&gt;운영 환경에서는 반드시 validate 또는 none 사용 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 예시 코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 1. Entity 정의
@Entity
@Table(name = &quot;member&quot;)
@Getter @Setter
@NoArgsConstructor
public class Member {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    public Member(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

// 2. Repository 정의
public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
    Optional&amp;lt;Member&amp;gt; findByEmail(String email);
    List&amp;lt;Member&amp;gt; findByNameContaining(String keyword);
    
    // JPQL 직접 작성도 가능
    @Query(&quot;SELECT m FROM Member m WHERE m.name = :name AND m.email = :email&quot;)
    Optional&amp;lt;Member&amp;gt; findByNameAndEmail(@Param(&quot;name&quot;) String name, 
                                        @Param(&quot;email&quot;) String email);
}

// 3. Service 계층 &amp;mdash; 트랜잭션 관리
@Service
@RequiredArgsConstructor
public class MemberService {
    
    private final MemberRepository memberRepository;
    
    @Transactional
    public Member createMember(String name, String email) {
        Member member = new Member(name, email);
        return memberRepository.save(member); // persist &amp;rarr; 영속화
    }
    
    @Transactional(readOnly = true)  // 읽기 전용 트랜잭션 (성능 최적화)
    public Member getMember(Long id) {
        return memberRepository.findById(id)
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;회원을 찾을 수 없습니다.&quot;));
    }
    
    @Transactional
    public void updateMemberName(Long id, String newName) {
        Member member = memberRepository.findById(id)
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;회원을 찾을 수 없습니다.&quot;));
        member.setName(newName); // 변경 감지 &amp;rarr; 자동 UPDATE
        // save() 호출 불필요!
    }
    
    @Transactional
    public void deleteMember(Long id) {
        memberRepository.deleteById(id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JPQL이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JPQL(Java Persistence Query Language)&lt;/b&gt; 은 JPA에서 사용하는 객체 지향 쿼리 언어다.&lt;br /&gt;SQL과 비슷하지만, &lt;b&gt;테이블과 컬럼이 아닌 엔티티 클래스와 필드명을 사용&lt;/b&gt; 한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- SQL (테이블/컬럼 대상)
SELECT * FROM member WHERE name = 'hong'

-- JPQL (엔티티/필드 대상)
SELECT m FROM Member m WHERE m.name = 'hong'
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;14. 정리 및 흐름 요약&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 흐름 한 눈에 보기&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;[Spring Boot Application]
        │
        │ Repository 메서드 호출
        ▼
[Spring Data JPA]
  JpaRepository 구현체 자동 생성
        │
        │ EntityManager 호출
        ▼
[JPA / Hibernate]
  영속성 컨텍스트 관리
  ┌─────────────────────────────┐
  │ 1차 캐시                      │
  │ 쓰기 지연 SQL 저장소            │
  │ 스냅샷 (변경 감지용)             │
  └─────────────────────────────┘
        │
        │ 트랜잭션 커밋 &amp;rarr; flush()
        ▼
[JDBC]
  SQL 실행 (커넥션 풀 관리)
        │
        ▼
[RDBMS]
  실제 데이터 저장&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Learning Log</category>
      <category>Java</category>
      <category>JPA</category>
      <category>orm</category>
      <author>allluck777</author>
      <guid isPermaLink="true">https://allluck777.tistory.com/32</guid>
      <comments>https://allluck777.tistory.com/32#entry32comment</comments>
      <pubDate>Tue, 3 Mar 2026 11:42:35 +0900</pubDate>
    </item>
  </channel>
</rss>