<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>상어코딩</title>
    <link>https://dding-shark.tistory.com/</link>
    <description>뒷마당 설계와 스프링을 공부하는 상어를 수상할 정도로 조아 하는 개발자 입니다.
git : https://github.com/DDINGJOO</description>
    <language>ko</language>
    <pubDate>Fri, 17 Apr 2026 00:50:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>dding-shark</managingEditor>
    <image>
      <title>상어코딩</title>
      <url>https://tistory1.daumcdn.net/tistory/7478079/attach/c1a9629103a44558a12f3a8bb820a390</url>
      <link>https://dding-shark.tistory.com</link>
    </image>
    <item>
      <title>Oracle 아키텍처 해부 &amp;mdash; SGA&amp;middot;PGA&amp;middot;백그라운드 프로세스와 RAC의 기반</title>
      <link>https://dding-shark.tistory.com/412</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;oracle-아키텍처-해부--sgapga백그라운드-프로세스와-rac의-기반&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;Oracle
아키텍처 해부 — SGA·PGA·백그라운드 프로세스와 RAC의 기반&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PostgreSQL은 프로세스 기반, MySQL은 스레드 기반. Oracle은 기본
Dedicated Server의 한 세션 = 한 OS 프로세스 모델이면서, 동시에
Shared·Threaded·DRCP라는 세 갈래를 OS·버전·워크로드에 따라 골라 쓸 수
있는 유일한 상용 DB다. 이 &amp;quot;선택권&amp;quot;이 장점이자 함정이다. 연결 모델 하나
바꿨을 뿐인데 OS 인증이 막히기도
하고(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;THREADED_EXECUTION=TRUE&lt;/code&gt;), LOB·AQ 같은 기능이 호환되지
않기도 한다(Shared Server). 하나의 추상 모델로 모든 것을 설명하는
PG·MySQL과 달리, Oracle을 읽을 때는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어떤 연결 모델을 전제로
하는지&lt;/strong&gt;부터 먼저 확인해야 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글을 쓰는 관점은 하나다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;단일 인스턴스 Oracle을 설명하는
4개의 축(연결 모델·SGA·백그라운드 프로세스·공유 장치)이 왜 이렇게
설계됐는지&lt;/strong&gt;를 한 번에 훑는다. RAC·Data Guard 같은 분산 확장은 이
단일 인스턴스 구조를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;그대로 복제하고 조율하는&lt;/strong&gt; 방향으로
붙는다. 즉 여기서 잡는 그림이 뒤이어 나올 RAC 시리즈의 기반이 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Oracle 21c 기준이다. 버전 특화 기능(ACS, DRCP, Threaded Execution
등)은 도입 버전을 함께 명시한다. 1편 PostgreSQL·2편 MySQL InnoDB와 짝을
이루는 편이므로, 같은 주제가 나올 때마다 PG/MySQL과 한 문장씩 비교해
좌표계를 맞춘다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;MTS&amp;quot;라는 용어는 더 이상 쓰지 않는다&lt;/strong&gt; — 공식 명칭은
Shared Server다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;THREADED_EXECUTION=TRUE&lt;/code&gt;는 속도 튜닝이
아니다&lt;/strong&gt; — OS 인증이 막히는 부작용을 동반한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CKPT가 블록을 디스크에 쓴다는 설명은 틀렸다&lt;/strong&gt; —
control file과 datafile header만 갱신한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;UNDO가 &amp;quot;MVCC 우월 구조&amp;quot;라는 도식은 절반만 맞다&lt;/strong&gt; —
ORA-01555는 바로 이 분리가 만드는 장애다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AWR/ASH로 관측하는 게 표준이라는 전제는 라이선스
문제다&lt;/strong&gt; — Diagnostic Pack은 유료다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;왜 → 구조 → 동작 → 주의 → 실무&lt;/strong&gt; 순으로
정리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-연결-모델-네-가지-dedicatedsharedthreadeddrcp&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1) 연결
모델 네 가지: Dedicated·Shared·Threaded·DRCP&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-sga-해부-여섯-영역과-그-쓰임&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2) SGA 해부: 여섯 영역과
그 쓰임&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-pga-세션-사설-메모리와-두-개의-한도&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3) PGA: 세션 사설
메모리와 두 개의 한도&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-백그라운드-프로세스-생태계-여덟-주연&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4) 백그라운드
프로세스 생태계: 여덟 주연&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-scn과-undo-논리-시계와-read-consistency&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5) SCN과 UNDO:
논리 시계와 Read Consistency&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-library-cache-hardsoft-parse와-acs&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6) Library Cache:
hard/soft parse와 ACS&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-rac로-가는-길-단일-인스턴스에서-공유-everything으로&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7)
RAC로 가는 길: 단일 인스턴스에서 공유 everything으로&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#9-한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;9) 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-연결-모델-네-가지-dedicatedsharedthreadeddrcp&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1) 연결 모델 네
가지: Dedicated·Shared·Threaded·DRCP&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Oracle을 처음 잡을 때 반드시 먼저 정리해야 할 축이 연결 모델이다.
PG는 모든 연결이 fork, MySQL은 모두 스레드지만 Oracle은 워크로드별로 둘
다 혼용하는 대신 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;호환성 매트릭스&lt;/strong&gt;가 존재한다.
Dedicated·Shared·Threaded·DRCP 네 가지 중 무엇을 쓰느냐에 따라 OS 인증
여부, 메모리 배치, 세션 상태 저장소가 전부 달라진다. 공식 문서가 짧게
정리한다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;Each client process communicates with a server
process.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/21/cncpt/process-architecture.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;Oracle
Database 21c Concepts §15.2&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;클라이언트 프로세스 : 서버 프로세스&amp;quot; 관계가 1:1이냐, N:1이냐, 혹은
N:M 풀이냐가 곧 연결 모델의 차이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;db-deepdive-03-oracle-architecture-01&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;db-deepdive-03-oracle-architecture-01&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;1-1-dedicated-server--표준-모델&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-1) Dedicated Server — 표준
모델&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;기본값이자 대부분의 OLTP 현장이 쓰는 모델이다. 세션 하나당 OS
프로세스(Linux에서는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oracle&lt;/code&gt;로 시작하는 프로세스) 하나가
붙는다. 세션이 유휴 상태여도 프로세스는 살아 있고, 세션 상태(UGA, User
Global Area)는 PGA 안에 들어간다. 직관적이고 격리가 좋지만, 커넥션 수천
개를 OS 프로세스 수천 개로 올리면 메모리·스케줄링 비용이 그대로 부담이
된다.&lt;/p&gt;
&lt;h3 id=&quot;1-2-shared-server--mts가-아니다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-2) Shared Server — &amp;quot;MTS&amp;quot;가
아니다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;과거에 MTS(Multi-Threaded Server)라 불리던 모델의 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;공식 명칭은
Shared Server&lt;/strong&gt;다. MTS 용어는 더 이상 문서에 쓰지 않는다. 구조는
Dispatcher와 Shared Server 프로세스 풀을 두고, 클라이언트 요청을
Dispatcher가 받아 Shared Server에 큐잉해 분배한다. 여기서 세션 상태
UGA가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PGA에서 SGA의 Large Pool로 이동&lt;/strong&gt;한다는 점이
특이하다. 이게 나중에 Large Pool을 별도로 튜닝해야 하는 이유다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;함정 두 개. 하나는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;LOB 전송·Advanced Queuing 등 일부 기능이
Shared Server와 호환되지 않는다&lt;/strong&gt;는 점이고, 다른 하나는
Dispatcher가 Listener처럼 보여서 네트워크 진단이 헷갈린다는 점이다. 장기
트랜잭션이 섞인 OLTP에서는 세션 하나가 Shared Server 하나를 오래 점유해
큐가 밀리므로 OLAP 혼용 환경에는 쉽게 안 맞는다.&lt;/p&gt;
&lt;h3 id=&quot;1-3-threaded-execution--12c-linux-전용&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-3) Threaded Execution
— 12c+ Linux 전용&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;12c부터 Linux 전용으로 들어온 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;THREADED_EXECUTION=TRUE&lt;/code&gt;
모드는 백그라운드 프로세스 일부와 서버 프로세스를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스레드로
전환&lt;/strong&gt;한다. 프로세스 수가 줄어드니 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ps&lt;/code&gt;로 본 모습이
깔끔해지지만, 진짜 함정은 따로 있다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;If this parameter is set to TRUE, operating system
authentication is not supported.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/THREADED_EXECUTION.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;Oracle
Database 21c Reference — THREADED_EXECUTION&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;OS 인증이 아예 차단된다&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sqlplus / as sysdba&lt;/code&gt; 같은 무비밀번호 접속이 ORA-01031로
거절된다. 실제 터미널에서 보는 모습은 이렇다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;$ sqlplus / as sysdba

SQL*Plus: Release 21.0.0.0.0 - Production
ERROR:
ORA-01031: insufficient privileges&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;우회는 패스워드 파일을 다시 만드는 방향이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre class=&quot;sourceCode bash&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode bash&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;orapwd&lt;/span&gt; file=&lt;span class=&quot;va&quot;&gt;$ORACLE_HOME&lt;/span&gt;/dbs/orapwORCL password=... force=y&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;sqlplus&lt;/span&gt; sys/pwd@ORCL as sysdba&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;운영 스크립트가 OS 인증에 의존하는 경우 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스크립트 전량을
고쳐야&lt;/strong&gt; 하므로, 도입은 신중하게. 메모리 절감 효과도 광고만큼
크지 않다. 서버 프로세스가 차지하던 PGA는 스레드로 전환돼도
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;세션당 사설 영역이 여전히 동일 수준&lt;/strong&gt;으로 필요하고,
절감되는 건 프로세스 제어 블록과 페이지 테이블 오버헤드 정도다. 관리
비용은 분명히 오른다.&lt;/p&gt;
&lt;h3 id=&quot;1-4-drcp--웹api-워크로드용-커넥션-풀&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-4) DRCP — 웹·API
워크로드용 커넥션 풀&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Database Resident Connection Pooling. 애플리케이션이 짧은 연결을
대량으로 만들어 쓰는 패턴(PHP, Python, Node 등)에서 서버 측에
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;공유된 Pooled Server 풀&lt;/strong&gt;을 두고 클라이언트들이 빌려 쓰게
하는 방식이다. Shared Server와 비슷해 보이지만 결정적 차이가 있다 —
DRCP는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;세션 상태까지 pool에 묶였다 풀렸다&lt;/strong&gt; 하는 반면,
Shared Server는 세션은 유지되고 서버 프로세스만 공유된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;설정은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DBMS_CONNECTION_POOL.CONFIGURE_POOL&lt;/code&gt;로 한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;POOL_SIZE&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MIN_SIZE&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;INACTIVITY_TIMEOUT&lt;/code&gt; 같은 파라미터가 붙어 있고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ALTER SYSTEM&lt;/code&gt;이 아니라 PL/SQL 프로시저로 제어한다는 점이
독특하다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;BEGIN&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  DBMS_CONNECTION_POOL.CONFIGURE_POOL(&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    pool_name          &lt;span class=&quot;op&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;SYS_DEFAULT_CONNECTION_POOL&amp;#39;&lt;/span&gt;,&lt;/span&gt;
&lt;span id=&quot;cb3-4&quot;&gt;&lt;a href=&quot;#cb3-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    minsize            &lt;span class=&quot;op&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;10&lt;/span&gt;,&lt;/span&gt;
&lt;span id=&quot;cb3-5&quot;&gt;&lt;a href=&quot;#cb3-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;maxsize&lt;/span&gt;            &lt;span class=&quot;op&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;80&lt;/span&gt;,&lt;/span&gt;
&lt;span id=&quot;cb3-6&quot;&gt;&lt;a href=&quot;#cb3-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    inactivity_timeout &lt;span class=&quot;op&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;300&lt;/span&gt;);&lt;/span&gt;
&lt;span id=&quot;cb3-7&quot;&gt;&lt;a href=&quot;#cb3-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  DBMS_CONNECTION_POOL.START_POOL;&lt;/span&gt;
&lt;span id=&quot;cb3-8&quot;&gt;&lt;a href=&quot;#cb3-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;END&lt;/span&gt;;&lt;/span&gt;
&lt;span id=&quot;cb3-9&quot;&gt;&lt;a href=&quot;#cb3-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;주의할 점은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;세션 상태의 단절&lt;/strong&gt;이다. 애플리케이션이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ALTER SESSION&lt;/code&gt;으로 NLS·optimizer 힌트를 바꿔 놓아도, 풀에
반환된 뒤 다른 커넥션이 해당 Pooled Server를 잡으면 앞서 설정한 상태를
보장받지 못한다. DRCP를 쓰려면 애플리케이션이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;매 요청마다 세션
상태를 자기 책임으로 초기화&lt;/strong&gt;해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;1-5-네-모델-비교와-선택-기준&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-5) 네 모델 비교와 선택 기준&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;모델&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;연결당 리소스&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;UGA 위치&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;OS 인증&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;대표 제약&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Dedicated&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;OS 프로세스 1개&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;PGA&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;지원&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;동시 연결 수 확장성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Shared&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Dispatcher + Shared Server 풀&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Large Pool (SGA)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;지원&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;LOB·AQ 비호환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Threaded&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;OS 스레드&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;PGA&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;차단 (ORA-01031)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Linux 12c+ 전용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;DRCP&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Pooled Server 풀&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Large Pool (SGA)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;제한적&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;세션 상태 단절 고려 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;결론이 명확하다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;특별한 사유가 없으면 Dedicated이
표준&lt;/strong&gt;이고, 나머지는 각자의 제약을 이해한 뒤 고르는 선택지다. &amp;quot;네
가지 중에서 마음에 드는 것을 고르세요&amp;quot;가 아니라 &amp;quot;기본은 Dedicated, 예외
상황에 나머지&amp;quot;가 올바른 독해다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-sga-해부-여섯-영역과-그-쓰임&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2) SGA 해부: 여섯 영역과 그
쓰임&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;SGA(System Global Area)는 인스턴스가 기동되면 공유 메모리 세그먼트로
올라와 모든 세션이 공유하는 영역이다. PG &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_buffers&lt;/code&gt;, MySQL &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_buffer_pool&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;log_buffer&lt;/code&gt;를 합친 것이 Oracle SGA이되, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Library
Cache까지 같은 공유 메모리에 올린다&lt;/strong&gt;. 즉 Oracle은 &amp;quot;실행 계획과
파싱된 SQL&amp;quot;조차 공유 자원으로 관리한다. 이게 뒤에 나올 &lt;a href=&quot;#6-library-cache-hardsoft-parse와-acs&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§6 Library Cache&lt;/a&gt;의 경합
구조로 이어진다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;db-deepdive-03-oracle-architecture-02&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;db-deepdive-03-oracle-architecture-02&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-1-여섯-영역과-역할&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-1) 여섯 영역과 역할&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;영역&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;용도&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;대표 V$ 뷰&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;대표 대기 이벤트&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Database Buffer Cache&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;데이터 블록 캐싱 (DEFAULT/KEEP/RECYCLE)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$BUFFER_POOL_STATISTICS&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;db file sequential read&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Shared Pool&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Library Cache + Row Cache(데이터 딕셔너리)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SGASTAT&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$LIBRARYCACHE&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;library cache: mutex X&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Redo Log Buffer&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;커밋 전 redo 레코드 적재&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SYSSTAT&lt;/code&gt;
(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;redo buffer allocation retries&lt;/code&gt;)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;log buffer space&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Large Pool&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;RMAN, parallel slave, Shared UGA&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SGASTAT&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;enq: RO - fast object reuse&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Java Pool&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Java 스토어드 프로시저 JVM 힙&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SGASTAT&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;실무 관측 드묾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Streams Pool&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GoldenGate Capture, AQ 스트리밍&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$STREAMS_POOL_STATISTICS&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;STREAMS pool&lt;/code&gt; 관련&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;KEEP·RECYCLE 두 버퍼 풀은 &amp;quot;자주 쓰는 작은 테이블은 KEEP에 고정, 한 번
쓰고 버리는 스캔은 RECYCLE로 격리&amp;quot;하는 분리 전략의 도구다. MySQL
InnoDB의 LRU midpoint와 해결하려는 문제가 같지만, Oracle은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스키마 설계자가 명시적으로 선언&lt;/strong&gt;하는 쪽이고 InnoDB는
엔진이 midpoint로 자동 처리하는 쪽이다.&lt;/p&gt;
&lt;h3 id=&quot;2-2-amm-vs-asmm--메모리-자동화의-두-단계&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-2) AMM vs ASMM —
메모리 자동화의 두 단계&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Oracle은 SGA 크기 관리를 두 단계로 자동화해 뒀다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ASMM(Automatic Shared Memory Management)&lt;/strong&gt; 은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SGA_TARGET&lt;/code&gt;을 주면 Shared Pool·Buffer Cache·Large Pool 등의
내부 배분을 Oracle이 동적으로 조정한다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AMM(Automatic Memory
Management)&lt;/strong&gt; 은 거기서 한 발 더 나아가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MEMORY_TARGET&lt;/code&gt; 하나로 SGA+PGA 경계까지 Oracle이 옮긴다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;보기엔 AMM이 편해 보이지만 현장 권장은 다르다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AMM은 4GB 이하
소규모 환경 권장&lt;/strong&gt;, 그 이상이면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ASMM +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_TARGET&lt;/code&gt; 분리&lt;/strong&gt;가 표준이다. 이유는 두
가지다. 하나는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Linux HugePages가 ASMM에서만 제대로 쓸 수
있다&lt;/strong&gt;는 점. AMM은 내부적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/dev/shm&lt;/code&gt;에 tmpfs로
공유 메모리 세그먼트를 만드는데, HugePages는 커널 기동 시점에 미리
예약되는 고정 페이지다. tmpfs가 쓰는 일반 페이지와 pre-reserved
HugePages는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서로 다른 물리 풀&lt;/strong&gt;이라 공존이 안 된다. 즉
AMM이 켜진 순간 HugePages는 버려지는 셈이다. 운영 환경에서 64GB SGA를
AMM으로 돌리면 TLB miss 비용이 그대로 누적된다. 다른 하나는 AMM의 동적
리사이징이 Library Cache 같은 민감 영역을 흔들 때 경합을 키울 수 있다는
점이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;ASMM + HugePages로 가는 설정은 이렇게 생겼다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;-- SGA 고정 (ASMM)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-2&quot;&gt;&lt;a href=&quot;#cb4-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SYSTEM&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SET&lt;/span&gt; SGA_TARGET&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;8&lt;/span&gt;G      &lt;span class=&quot;kw&quot;&gt;SCOPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;SPFILE;&lt;/span&gt;
&lt;span id=&quot;cb4-3&quot;&gt;&lt;a href=&quot;#cb4-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SYSTEM&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SET&lt;/span&gt; SGA_MAX_SIZE&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;12&lt;/span&gt;G   &lt;span class=&quot;kw&quot;&gt;SCOPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;SPFILE;&lt;/span&gt;
&lt;span id=&quot;cb4-4&quot;&gt;&lt;a href=&quot;#cb4-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SYSTEM&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SET&lt;/span&gt; MEMORY_TARGET&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;0&lt;/span&gt;    &lt;span class=&quot;kw&quot;&gt;SCOPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;SPFILE;  &lt;span class=&quot;co&quot;&gt;-- AMM off&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-5&quot;&gt;&lt;a href=&quot;#cb4-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SYSTEM&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SET&lt;/span&gt; MEMORY_MAX_TARGET&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SCOPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;SPFILE;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb5&quot;&gt;&lt;pre class=&quot;sourceCode bash&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode bash&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb5-1&quot;&gt;&lt;a href=&quot;#cb5-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# /etc/sysctl.conf — 2MB HugePage 6144개 = 12GB 예약&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-2&quot;&gt;&lt;a href=&quot;#cb5-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;vm.nr_hugepages&lt;/span&gt; = 6144&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;21c부터는 HugePage 배정 현황을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$HUGEPAGE_INFO&lt;/code&gt;에서 직접
조회할 수 있어, 진단 시 OS &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/proc/meminfo&lt;/code&gt;만 보지 않고
인스턴스 안에서도 확인 가능하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;SGA가 크면 빠르다&amp;quot;는 도식은 성립하지
않는다&lt;/strong&gt;. HugePages 없이 수십 GB SGA를 올리면 OS 페이지 테이블
비용이 폭증하고, 내부 래치·mutex 경합도 같이 커진다. 사이즈는 워크로드가
요구할 때만 키우는 것이 안전하다.&lt;/p&gt;
&lt;h3 id=&quot;2-3-shared-pool의-세-덩어리&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-3) Shared Pool의 세 덩어리&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Shared Pool은 외부에서 한 덩어리로 보이지만 실제로는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Library
Cache + Row Cache(데이터 딕셔너리 캐시) + Reserved Area&lt;/strong&gt;로
나뉜다. Library Cache는 파싱된 SQL·PL/SQL 캐시, Row Cache는 딕셔너리
객체 메타데이터 캐시, Reserved는 큰 연속 청크 할당용 예약 공간이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SHARED_POOL_RESERVED_SIZE&lt;/code&gt;로 Reserved 크기를 명시해 두면
ORA-04031(&amp;quot;unable to allocate&amp;quot;) 장애에서 한 겹 방어선이 생긴다.&lt;/p&gt;
&lt;h3 id=&quot;2-4-redo-log-buffer의-세-트리거&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-4) Redo Log Buffer의 세
트리거&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Redo Log Buffer는 커밋을 기다리지 않고도 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;(1) 3초 경과, (2)
1MB 적재, (3) 버퍼의 1/3 full&lt;/strong&gt; 세 조건 중 하나만 맞아도 LGWR이
파일로 내린다. 이 덕에 긴 트랜잭션도 커밋 시점에 수백 MB를 한 번에 밀지
않는다. MySQL의 redo log가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_log_buffer_size&lt;/code&gt;에
쌓이다가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_log_at_trx_commit&lt;/code&gt; 값대로 flush되는
것과 매커니즘은 비슷하지만, Oracle 쪽 상시 flush의 트리거가 명확하게 세
개로 정의돼 있다는 점이 다르다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Log Buffer가 부족하면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;redo buffer allocation retries&lt;/code&gt;(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SYSSTAT&lt;/code&gt;) 값이
증가하고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;log buffer space&lt;/code&gt; 대기
이벤트(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SYSTEM_EVENT&lt;/code&gt;)가 쌓인다. 12c 이후 기본값(수 MB~수십
MB)으로도 대부분 커버되지만, 대량 batch insert나 CTAS 같은 벌크 DML이
몰리는 시간대에는 이 두 지표로 부족 여부를 판단한다.&lt;/p&gt;
&lt;h3 id=&quot;2-5-관측의-라이선스-함정&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-5) 관측의 라이선스 함정&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;SGA 내부 상태를 AWR/ASH 리포트로 보는 것이 Oracle 튜닝의 표준 경로다.
그런데 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AWR·ASH·ADDM은 Diagnostic Pack 유료 옵션&lt;/strong&gt;이다.
Standard Edition에는 애초에 들어 있지 않고, Enterprise Edition에서도
라이선스를 별도로 사야 한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$ACTIVE_SESSION_HISTORY&lt;/code&gt;를
무심코 조회해도 라이선스 위반이 될 수 있다. 무료로 쓸 수 있는 것은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SESSION&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SQL&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SGASTAT&lt;/code&gt; 같은
기본 동적 성능 뷰와 Statspack 정도다. 이 한 번 경계만 지키고 이후는
줄여서 말한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-pga-세션-사설-메모리와-두-개의-한도&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3) PGA: 세션 사설
메모리와 두 개의 한도&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PGA(Program Global Area)는 서버 프로세스마다 따로 잡는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;사설
메모리&lt;/strong&gt;다. SGA가 공유 영역이라면 PGA는 세션 격리 영역이다.
안에는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Private SQL Area&lt;/strong&gt;(커서별 실행 상태, bind 변수,
fetch 상태 등)와 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Work Area&lt;/strong&gt;(정렬·해시 조인·비트맵 머지
작업 공간)가 들어간다. Dedicated Server에서는 UGA도 PGA 안에 들어가고,
Shared Server·DRCP에서는 UGA가 Large Pool로 이동한다는 점은 &lt;a href=&quot;#1-2-shared-server--mts가-아니다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§1-2&lt;/a&gt;에서 본 대로다.&lt;/p&gt;
&lt;h3 id=&quot;3-0-uga와-cga--pga-안의-두-하위-영역&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-0) UGA와 CGA — PGA 안의
두 하위 영역&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PGA를 덩어리로만 보면 구조가 잘 안 잡힌다. 한 겹 더 내려가면 두 하위
영역이 있다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;UGA(User Global Area)&lt;/strong&gt; 는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;세션
수명&lt;/strong&gt; 동안 살아 있는 세션 상태 저장소다. 로그온/로그오프 트리거
상태, 패키지 변수, OPEN된 커서 정보 같은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;세션 전역
변수&lt;/strong&gt;가 여기 머문다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CGA(Call Global Area)&lt;/strong&gt; 는
한 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;DB 콜(call) 수명&lt;/strong&gt;만큼만 사는 더 짧은 영역이다.
recursive SQL 실행 중 쓰는 스택, bind 변수 해석 영역처럼 콜이 끝나면
사라지는 일시 상태가 들어간다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;배치는 연결 모델에 따라 갈린다. Dedicated이면 UGA·CGA 모두 PGA 안,
Shared Server면 UGA는 Large Pool(SGA)로 빠지고 CGA만 PGA에 남는다.
Shared Server에서 Large Pool 사이징이 중요해지는 이유가 바로 이 UGA
이동이다.&lt;/p&gt;
&lt;h3 id=&quot;3-1-두-한도--target과-limit&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-1) 두 한도 — target과 limit&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Oracle 12c부터 PGA 제어는 두 축으로 쪼개졌다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파라미터&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;성격&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;초과 시 동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_TARGET&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;soft&lt;/strong&gt;, 권장치&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;초과해도 점진 조정, 경고만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_LIMIT&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;hard&lt;/strong&gt;, 절대 상한&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;초과 시 ORA-04036, 세션 강제 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_TARGET&lt;/code&gt;은 오래전부터 있던 &amp;quot;대략 이 정도에서
맞춰 줘&amp;quot;라는 목표치다. 실제로는 워크로드가 몰리면 Oracle이 이 수치를
일시적으로 넘어가기도 한다. 12c에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_LIMIT&lt;/code&gt;이
도입되면서 비로소 절대 상한이 생겼다. 기본값은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_TARGET&lt;/code&gt;의 2배 또는 서버 메모리의 일부 중 큰
값이다. 초과 시 Oracle은 ORA-04036을 던지고, 가장 많이 쓰는 세션부터
종료해 상한을 지킨다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;운영 관점에서 실수가 잦은 지점은 여기다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_TARGET&lt;/code&gt;만 설정하고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;LIMIT&lt;/code&gt;를 명시하지 않으면&lt;/strong&gt;, Oracle이 자동으로 계산한
큰 값이 상한이 돼 OS swap까지 밀어넣는 사고가 난다. 대형 OLAP 쿼리 한
건이 서버 전체를 흔드는 전형적 사례가 이 구조에서 나온다. 두 값을 모두
명시하는 습관이 필요하다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;-- 두 값을 함께 못 박기&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SYSTEM&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SET&lt;/span&gt; PGA_AGGREGATE_TARGET&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;4&lt;/span&gt;G &lt;span class=&quot;kw&quot;&gt;SCOPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;BOTH&lt;/span&gt;;&lt;/span&gt;
&lt;span id=&quot;cb6-3&quot;&gt;&lt;a href=&quot;#cb6-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SYSTEM&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;SET&lt;/span&gt; PGA_AGGREGATE_LIMIT&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;8&lt;/span&gt;G  &lt;span class=&quot;kw&quot;&gt;SCOPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;BOTH&lt;/span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;ORA-04036이 실제로 발생하면 Oracle은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$PROCESS.PGA_USED_MEM&lt;/code&gt;이 큰 세션부터 종료한다. 즉
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;상한에 도달한 &amp;quot;죄&amp;quot;가 아니라 &amp;quot;많이 쓴 세션&amp;quot;이 희생&lt;/strong&gt;된다.
대형 보고 쿼리가 희생양이 될 수 있으니, 야간 배치와 OLTP를 같은
인스턴스에 두는 환경에서는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_LIMIT&lt;/code&gt;을 여유 있게
잡거나 Resource Manager로 세션 그룹을 갈라 둔다.&lt;/p&gt;
&lt;h3 id=&quot;3-2-work-area의-세-가지-실행-경로&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-2) Work Area의 세 가지 실행
경로&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;정렬·해시 조인 같은 연산은 Work Area 크기에 따라 실행 경로가 세
단계로 나뉜다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;optimal&lt;/strong&gt;(메모리에서 끝남),
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;one-pass&lt;/strong&gt;(temp tablespace 한 번 경유),
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;multi-pass&lt;/strong&gt;(여러 번 경유).
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SQL_WORKAREA_HISTOGRAM&lt;/code&gt;에서 쿼리 유형별로 어떤 경로가
얼마나 찍히는지 볼 수 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb7&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb7-1&quot;&gt;&lt;a href=&quot;#cb7-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;SELECT&lt;/span&gt; low_optimal_size, high_optimal_size,&lt;/span&gt;
&lt;span id=&quot;cb7-2&quot;&gt;&lt;a href=&quot;#cb7-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;       optimal_executions, onepass_executions, multipass_executions&lt;/span&gt;
&lt;span id=&quot;cb7-3&quot;&gt;&lt;a href=&quot;#cb7-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;FROM&lt;/span&gt;   v$sql_workarea_histogram&lt;/span&gt;
&lt;span id=&quot;cb7-4&quot;&gt;&lt;a href=&quot;#cb7-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ORDER&lt;/span&gt;  &lt;span class=&quot;kw&quot;&gt;BY&lt;/span&gt; low_optimal_size;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;세 경로는 비용 구조가 다르다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;경로&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;메모리 사용&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;temp I/O&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;실행 시간 특성&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;optimal&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Work Area 안에서 완결&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;가장 빠름, 변동 작음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;one-pass&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Work Area + temp 한 번&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;중간 (1패스)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2~5배 느려짐, 예측 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;multi-pass&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Work Area + temp 반복&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;큼 (여러 패스)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;수십 배 느려짐, p99 튐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;multi-pass&lt;/code&gt; 비율이 높아지면 PGA 총량 자체보다는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;개별 Work Area 상한&lt;/strong&gt;을 키워야 하는 경우가 많다. Oracle은
세션당 Work Area 최대치를 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;_pga_max_size&lt;/code&gt; 같은 숨은
파라미터로 제한한다. 단 이건 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;hidden parameter&lt;/strong&gt;라
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Oracle Support(MOS) SR 승인 없이 건드리지 않는다&lt;/strong&gt; — 잘못
올리면 한 세션이 PGA 대부분을 소진해 ORA-04036 연쇄 장애를 만들 수 있다.
안전한 대안은 병렬 실행 전략 재검토와 쿼리 재작성이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-백그라운드-프로세스-생태계-여덟-주연&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4) 백그라운드 프로세스
생태계: 여덟 주연&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;인스턴스가 올라오면 SGA와 함께 일군의 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;백그라운드
프로세스&lt;/strong&gt;가 기동된다. PG는 checkpointer+bgwriter+walwriter 3인,
MySQL은 master+page cleaner, Oracle은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$BGPROCESS&lt;/code&gt;에서 8종 이상이 독립 관측&lt;/strong&gt;된다.
각자가 맡은 책임이 분리돼 있어 이름과 역할을 외우는 것이 SGA 그림을 잡는
것만큼 중요하다.&lt;/p&gt;
&lt;h3 id=&quot;4-1-여덟-프로세스-요약&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-1) 여덟 프로세스 요약&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;프로세스&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;역할&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;트리거&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;실패 시 영향&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;DBWR(DBWn)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Buffer Cache의 더티 블록을 datafile에 기록&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;체크포인트, free buffer 부족, 3초 timeout, dirty list 임계&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;인스턴스 crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;LGWR&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Redo Log Buffer를 online redo log에 기록&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;commit(동기 fsync), 3초, 1MB, 1/3 full&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;인스턴스 crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CKPT&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;control file과 datafile header에 체크포인트 위치 기록&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;LOG_CHECKPOINT_INTERVAL&lt;/code&gt; 등&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;인스턴스 crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SMON&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;인스턴스 복구, temp segment 정리&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;기동 시, 주기 sweep&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;인스턴스 crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PMON&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;비정상 종료 서버 프로세스 정리, 락 해제&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;세션 종료 감지&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;인스턴스 crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ARCn&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;redo log 스위치 시 archive log 복사&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;LOG_MODE=ARCHIVELOG&lt;/code&gt;에서 스위치&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;커밋 중단(공간 고갈 시)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;MMON&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;AWR 스냅샷, metric 수집 (Diagnostic Pack 유료)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1시간 주기(기본)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;통계 수집 중단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;RECO&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;분산 트랜잭션 in-doubt 복구&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;네트워크 복구 시&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;분산 커밋 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;4-2-dbwr의-쓰기-네-조건&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-2) DBWR의 쓰기 네 조건&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;DBWR가 더티 블록을 datafile에 쓰는 타이밍은 네 가지로 정리된다 — (1)
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;체크포인트 발생&lt;/strong&gt;, (2) &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;free buffer
부족&lt;/strong&gt;(쓸 블록 공간이 없을 때), (3) &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;3초 timeout&lt;/strong&gt;,
(4) &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;dirty list 임계 초과&lt;/strong&gt;. 1, 2, 4는 부하에 반응하는
트리거고 3은 상시 idle flush다. 한 덩어리를 한 번에 쓰지 않고 여러
조건으로 쪼개 분산 flush하는 구조라, DBWR가 &amp;quot;쓰기를 몰아친다&amp;quot;는 인식은
실제 동작과 맞지 않는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;각 트리거는 서로 다른 관측 지표로 드러난다. 장애 분석에서 &amp;quot;DBWR가
느리다&amp;quot;를 붙잡을 때 어느 방향으로 봐야 하는지가 여기서 갈린다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;트리거&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;관측 지표&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;확인처&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;free buffer 부족&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;free buffer waits&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SYSTEM_EVENT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;checkpoint&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DBWR checkpoint buffers written&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SYSSTAT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;3초 timeout&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DB Writer Timeout&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SYSTEM_EVENT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;dirty list 임계&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;dirty buffers inspected&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SYSSTAT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;free buffer waits&lt;/code&gt;가 높다는 건 Buffer Cache가 작거나
DBWR가 쓰기를 못 따라간다는 뜻이고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;dirty buffers inspected&lt;/code&gt;가 부풀면 LRU 스캔 범위가 넓어진다는
신호다. 원인이 다른 만큼 대응도 다르다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;MySQL InnoDB가 page cleaner를 도입해 flush를 비동기 분산한 것과 같은
문제의식이고, 내부 매커니즘은 Oracle 쪽이 먼저다.&lt;/p&gt;
&lt;h3 id=&quot;4-3-lgwr--커밋의-동기-fsync&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-3) LGWR — 커밋의 동기 fsync&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;LGWR는 성격이 다르다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;커밋 시점에 fsync를 동기로 걸고, 이
fsync가 끝나야 비로소 클라이언트에 COMMIT OK를 돌려준다&lt;/strong&gt;. 이게
D(urability)의 뼈대다. 상시 flush 조건은 Redo Log Buffer 쪽에 적었듯
3초·1MB·1/3 full 셋이다. 동기 fsync 비용을 줄이려고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;COMMIT_WRITE=BATCH,NOWAIT&lt;/code&gt; 같은 옵션이 존재하지만,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;지속성 보장을 깎는 옵션&lt;/strong&gt;이라 금융·결제 시스템에선
건드리지 않는다. 참고로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;COMMIT_WRITE&lt;/code&gt;는 11g 이후
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;COMMIT_LOGGING&lt;/code&gt;(BATCH/IMMEDIATE)과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;COMMIT_WAIT&lt;/code&gt;(WAIT/NOWAIT) 두 파라미터로 분리돼 각각 제어할
수 있게 됐다.&lt;/p&gt;
&lt;h3 id=&quot;4-4-ckpt--쓰지-않는-체크포인터&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-4) CKPT — 쓰지 않는
체크포인터&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이름 때문에 오해하기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CKPT는 실제 블록을 쓰지
않는다&lt;/strong&gt;. 공식 문서가 단호하다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;CKPT does not write data blocks to data files or redo blocks to
online redo log files.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/21/cncpt/process-architecture.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;Oracle
Database 21c Concepts §15.3&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;CKPT가 하는 일은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;control file과 datafile header에 체크포인트
SCN을 기록&lt;/strong&gt;하는 것뿐이다. 실제 더티 블록 flush는 DBWR가, redo
flush는 LGWR가 맡는다. 이 분리 덕분에 체크포인트가 자주 일어나도 I/O는
DBWR의 페이스로 분산된다. 여기서 틀리기 쉬운 지점을 다시 못 박아 두자 —
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;CKPT가 쓴다&amp;quot;는 설명은 틀렸다&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;4-5-smonpmonarcnreco&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-5) SMON·PMON·ARCn·RECO&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;SMON(System Monitor)은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;인스턴스 복구의 총책임자&lt;/strong&gt;다.
비정상 종료 후 재기동 시 복구는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;두 단계&lt;/strong&gt;로 갈린다. (1)
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;roll-forward&lt;/strong&gt; — online redo log를 따라 커밋·미커밋
가리지 않고 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;변경분을 전부 재적용&lt;/strong&gt;해 crash 직전 상태까지
끌어올린다. 이 시점에 DB는 &amp;quot;열린 상태&amp;quot;가 돼 사용자 세션을 받을 수 있다.
(2) &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;roll-back&lt;/strong&gt; — 아직 끝나지 않은(=COMMIT이 없었던)
트랜잭션을 UNDO로 되돌린다. roll-back은 DB가 열린 뒤 백그라운드에서
진행되므로, 가용성 복구 시간은 roll-forward 길이에 거의 수렴한다.
PMON(Process Monitor)은 죽은 세션이 남긴 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;락·리소스를
회수&lt;/strong&gt;한다. ARCn은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ARCHIVELOG&lt;/code&gt; 모드에서 redo가
스위치될 때마다 archive log 영역으로 복사하는 역할이고, 이게 끊기면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;online redo log가 다 찰 때 커밋 자체가 막힌다&lt;/strong&gt;. 증상은
명확하다 — 어느 순간부터 커밋이 전부
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;log file switch (archiving needed)&lt;/code&gt; 대기에 걸려 응답이
돌아오지 않는다. 복구는 archive log destination 공간 확보가 최우선이고,
그 전까지는 인스턴스가 사실상 read-only가 된다. RECO는 분산 트랜잭션에서
in-doubt 상태(2PC prepare 후 commit 응답 유실)를 네트워크 복구 뒤
정리한다. MMON은 AWR 스냅샷을 찍는데, 이 기능 자체가 Diagnostic Pack
유료 영역이라는 점은 &lt;a href=&quot;#2-5-관측의-라이선스-함정&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§2-5&lt;/a&gt;에서
이미 못 박았다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-scn과-undo-논리-시계와-read-consistency&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5) SCN과 UNDO: 논리
시계와 Read Consistency&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Oracle의 MVCC는 두 장치가 받친다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SCN&lt;/strong&gt;(System Change
Number)이라는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;논리 시계&lt;/strong&gt;와 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;UNDO&lt;/strong&gt;라는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;사전 이미지 창고&lt;/strong&gt;다. PG는 튜플 버전을 heap에 쌓고
VACUUM으로 치우고, Oracle은 UNDO라는 전용 창고에 보낸다. PG는 bloat,
Oracle은 ORA-01555. 같은 문제(오래된 버전 유지)의 두 갈래 해법이다.&lt;/p&gt;
&lt;h3 id=&quot;5-1-scn--64비트-단조-증가-논리-시계&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-1) SCN — 64비트 단조 증가
논리 시계&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;SCN은 64비트 정수로, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;모든 커밋마다 부여되는 단조 증가하는
논리 시계 값&lt;/strong&gt;이다. 트랜잭션 간 순서, redo 레코드의 위치, Read
Consistency 스냅샷 기준 시점, 분산 DB 링크 동기화 등 Oracle 내부의 모든
시간 개념이 SCN 위에 앉아 있다. 그래서 SCN은 사실상 Oracle의
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;논리 클록&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;함정은 SCN이 무한히 오른쪽으로 갈 수 있다는 생각이다. 실제로는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Headroom&lt;/strong&gt; 개념이 있다. SCN이 현재 시각에 대해 허용되는
상한을 계산하는 공식이 따로 있고(Oracle Support 문서 MOS 1376995.1 — My
Oracle Support 내부 노트, 공개 링크 없음), 이 상한을 넘어서면 인스턴스가
더 이상 커밋을 받지 않는다. 일상 운영에서는 마주치기 어렵지만,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SCN Headroom 고갈 이슈는 DB Link로 서로 연결된 노드들 사이에서
전파되는 위험&lt;/strong&gt;이 보고된 바 있다. 한 노드의 SCN 급증이 링크로
연결된 다른 노드로 번져 동시 고갈을 유발할 수 있다. 멀티 인스턴스 복합
환경을 다룰 땐 SCN도 운영 지표에 포함해 둔다.&lt;/p&gt;
&lt;h3 id=&quot;5-2-undo-tablespace--사전-이미지-창고&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-2) UNDO Tablespace —
사전 이미지 창고&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;UPDATE·DELETE가 발생하면 Oracle은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;변경 전 이미지&lt;/strong&gt;를
UNDO Tablespace에 쌓는다. 이 저장소는 PG처럼 heap에 버전을 섞어 두는
것이 아니라, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;별도 tablespace로 물리 분리&lt;/strong&gt;된다. 읽기
트랜잭션이 과거 SCN 기준으로 블록을 읽으려 하면, 현재 블록에서 UNDO
체인을 거꾸로 타고 올라가 해당 SCN 시점의 이미지로 재구성한다. 이게
Oracle의 Read Consistency 구현이다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;The UNDO_RETENTION parameter is honored only if the current undo
tablespace has enough space.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/UNDO_RETENTION.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;Oracle
Database 21c Reference — UNDO_RETENTION&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNDO_RETENTION&lt;/code&gt; 기본값은 900초(15분)다. &amp;quot;15분은 UNDO를
보관하겠다&amp;quot;는 값처럼 들리지만 실제로는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;best-effort&lt;/strong&gt;다.
UNDO tablespace에 여유 공간이 있을 때만 지켜지고, 공간이 부족하면
retention 값 안에 있는 UNDO도 덮어쓴다. 15분 이상 도는 long-running
쿼리가 있는 환경에서는 이 값만으로는 부족하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실제 운영 상태는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$UNDOSTAT&lt;/code&gt;에서 10분 단위 스냅샷으로
확인한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb8&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb8-1&quot;&gt;&lt;a href=&quot;#cb8-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;SELECT&lt;/span&gt; end_time, undoblks, tuned_undoretention, maxquerylen&lt;/span&gt;
&lt;span id=&quot;cb8-2&quot;&gt;&lt;a href=&quot;#cb8-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;FROM&lt;/span&gt;   v$undostat&lt;/span&gt;
&lt;span id=&quot;cb8-3&quot;&gt;&lt;a href=&quot;#cb8-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ORDER&lt;/span&gt;  &lt;span class=&quot;kw&quot;&gt;BY&lt;/span&gt; end_time &lt;span class=&quot;kw&quot;&gt;DESC&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-4&quot;&gt;&lt;a href=&quot;#cb8-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;FETCH  &lt;span class=&quot;fu&quot;&gt;FIRST&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;ROWS&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;ONLY&lt;/span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 핵심 컬럼은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TUNED_UNDORETENTION&lt;/code&gt;이다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Oracle이 파라미터 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNDO_RETENTION&lt;/code&gt;과 실제 UNDO
tablespace 크기, 그리고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MAXQUERYLEN&lt;/code&gt;(최근 관측된 가장 긴
쿼리 수명)을 종합해 자동 튜닝하는 실효 retention&lt;/strong&gt; 이다. 즉
운영자가 설정한 값이 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Oracle이 &amp;quot;이 정도는 유지해 보겠다&amp;quot;고
실제로 쥐고 있는 값&lt;/strong&gt;이다. ORA-01555 분석에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNDO_RETENTION&lt;/code&gt;을 보는 게 아니라
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TUNED_UNDORETENTION&lt;/code&gt;과 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MAXQUERYLEN&lt;/code&gt;을 같이 보는
게 정공법이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;UNDO 용량 사이징은 감각적으로는 이 공식이다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;분당 UNDO
생성량 × retention(분) × 안전 계수(1.5~2)&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$UNDOSTAT.UNDOBLKS&lt;/code&gt;로 10분 구간별 생성량을 뽑아 피크를
기준으로 잡고, 여기에 retention과 여유를 곱한다. 이 계산 없이 &amp;quot;대충
크게&amp;quot;로 잡으면 피크 시간대에 TUNED_UNDORETENTION이 확 떨어져 ORA-01555를
부르는 상황이 생긴다.&lt;/p&gt;
&lt;h3 id=&quot;5-3-retention-guarantee--진짜-보장이-필요할-때&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-3) RETENTION
GUARANTEE — 진짜 보장이 필요할 때&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;UNDO를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;절대 덮어쓰지 말라&lt;/strong&gt;고 강제하려면 UNDO
tablespace에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RETENTION GUARANTEE&lt;/code&gt; 절을 명시해야 한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb9&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb9-1&quot;&gt;&lt;a href=&quot;#cb9-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;TABLESPACE&lt;/span&gt; undotbs1 RETENTION GUARANTEE;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 옵션을 켜면 retention 안에 있는 UNDO는 공간이 부족해도 보호되지만,
대신 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;DML이 ORA-30036(&amp;quot;unable to extend segment&amp;quot;) 으로
실패&lt;/strong&gt;할 수 있다. 즉 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;쿼리 실패&amp;quot; vs &amp;quot;DML 실패&amp;quot;의
교환&lt;/strong&gt;이다. 장기 분석 쿼리가 도는 DW성 인스턴스에선 켜는 것이,
최우선 OLTP에선 끄는 것이 대체로 맞다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;현재 설정 여부는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DBA_TABLESPACES.RETENTION&lt;/code&gt; 컬럼으로
확인한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GUARANTEE&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;NOGUARANTEE&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;NOT APPLY&lt;/code&gt;
세 값 중 하나로 드러난다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;NOT APPLY&lt;/code&gt;는 수동 UNDO 관리 모드라
AUM(Automatic Undo Management)을 쓰는 11g 이후 환경에서는 거의 마주치지
않는다.&lt;/p&gt;
&lt;h3 id=&quot;5-4-ora-01555--구조적-장애&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-4) ORA-01555 — 구조적 장애&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ORA-01555(&amp;quot;snapshot too old&amp;quot;)&lt;/strong&gt; 는 Oracle UNDO 구조의
대표적 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;구조적 장애&lt;/strong&gt;다. 읽기 쿼리가 과거 SCN 블록을
UNDO로 재구성하려 할 때, 해당 UNDO가 이미 덮어쓰여 버리면 발생한다.
원인은 (1) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNDO_RETENTION&lt;/code&gt;보다 긴 쿼리, (2) UNDO tablespace
용량 부족, (3) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;COMMIT&lt;/code&gt; 이후 일정 시점 뒤에야 재사용 가능한
delayed block cleanout 등 여러 갈래다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;진단 순서는 항상 같다. 먼저 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$UNDOSTAT&lt;/code&gt;에서 에러 발생
시각 구간의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TUNED_UNDORETENTION&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MAXQUERYLEN&lt;/code&gt;을 비교한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MAXQUERYLEN &amp;gt; TUNED_UNDORETENTION&lt;/code&gt;이면 (1)이 유력하고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNDOBLKS&lt;/code&gt; 피크가 평소의 몇 배라면 (2)가 의심된다. 둘 다
아닌데 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;commit cleanout failures: block lost&lt;/code&gt; 같은 통계가
늘어나 있으면 (3)의 지문이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;대응도 원인별로 다르다. (1)은 UNDO tablespace 증설 +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNDO_RETENTION&lt;/code&gt; 상향 + 필요 시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RETENTION GUARANTEE&lt;/code&gt;. (2)는 일시 피크를 유발한 쿼리/배치
식별 후 시간대 분산. (3)은 커밋 직후 같은 블록을 다시 읽는 패턴이
원인이라 쿼리 재작성이나 통계 수집 재조정이 답이다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;ORA-01555 =
쿼리가 너무 길다&amp;quot;로 뭉뚱그리면 원인의 1/3만 맞힌다&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 한 도식만 못 박자. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;UNDO 분리 = MVCC 우월&amp;quot;은 절반만
맞다&lt;/strong&gt;. heap에 버전을 섞지 않아 읽기 경로가 깔끔해지는 장점은
있지만, 대신 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;UNDO 용량과 retention이라는 별도 운영 축&lt;/strong&gt;이
붙는다. PG VACUUM bloat가 없는 대가로 ORA-01555라는 고유 장애를
껴안는다. 어느 한쪽이 구조적으로 우월한 게 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서로 다른 운영
체감을 가진 두 설계&lt;/strong&gt;일 뿐이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-library-cache-hardsoft-parse와-acs&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6) Library Cache:
hard/soft parse와 ACS&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Library Cache는 Shared Pool의 핵심 덩어리로, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파싱된
SQL·PL/SQL과 그 실행 계획을 공유 메모리에 캐싱&lt;/strong&gt;하는 영역이다.
새로운 SQL이 들어오면 Oracle은 먼저 Library Cache를 뒤져 같은 SQL이
있는지 확인한다. 찾으면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;soft parse&lt;/strong&gt;(계획 재사용), 못
찾으면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;hard parse&lt;/strong&gt;(파싱+최적화 새로 수행)다. hard
parse는 CPU·latch 비용이 크므로 OLTP 성능의 주요 튜닝 축이다.&lt;/p&gt;
&lt;h3 id=&quot;6-1-cursor_sharing--세-가지-전략&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-1) CURSOR_SHARING — 세 가지
전략&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;하지만 &amp;quot;같은 SQL&amp;quot;의 판단 기준이 까다롭다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WHERE id=123&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WHERE id=456&lt;/code&gt;은 Oracle 눈에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;다른
SQL&lt;/strong&gt;(리터럴이 다르므로)이다. 이걸 제어하는 것이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CURSOR_SHARING&lt;/code&gt; 파라미터다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;값&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;동작&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;장점&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;EXACT&lt;/strong&gt; (기본)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;리터럴이 같아야 공유&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최적 실행 계획&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;hard parse 폭증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;FORCE&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;리터럴을 bind로 자동 치환&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;parse 비용 절감&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;스큐 데이터에서 나쁜 공유 계획 고착&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ACS&lt;/strong&gt; (11g+)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;bind 값에 따라 다른 계획 생성&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;두 마리 토끼&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;관측·이해 난도 ↑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CURSOR_SHARING=FORCE&lt;/code&gt;는 한때 만능 튜닝처럼 쓰였지만
함정이 크다. 컬럼 통계 분포가 스큐한 경우(예: 상태값 99%가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;N&lt;/code&gt;, 1%가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Y&lt;/code&gt;), bind 값에 따라 최적 계획이 달라야
하는데 FORCE는 첫 번째 들어온 값 기준 계획을 굳혀 나머지 쿼리에
재사용한다. 결과는 &amp;quot;공유는 됐는데 느리다&amp;quot;다.&lt;/p&gt;
&lt;h3 id=&quot;6-2-acs--adaptive-cursor-sharing&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-2) ACS — Adaptive Cursor
Sharing&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;11g에서 도입된 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Adaptive Cursor Sharing&lt;/strong&gt;이 이 문제를
풀어 보려는 장치다. 같은 SQL이라도 bind 값의 분포가 계획에 영향을 준다고
Oracle이 판단하면(&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;bind-sensitive&lt;/strong&gt;), 실제 실행 결과를
관측해 bind 값 범위별로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;여러 개의 plan&lt;/strong&gt;을
생성한다(&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;bind-aware&lt;/strong&gt;). 관측은 두 개의 플래그로
드러난다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb10&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb10-1&quot;&gt;&lt;a href=&quot;#cb10-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;-- V$SQL에서 cursor의 sharing 상태 확인&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-2&quot;&gt;&lt;a href=&quot;#cb10-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;SELECT&lt;/span&gt; sql_id, child_number, is_bind_sensitive, is_bind_aware&lt;/span&gt;
&lt;span id=&quot;cb10-3&quot;&gt;&lt;a href=&quot;#cb10-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;FROM&lt;/span&gt;   v$sql&lt;/span&gt;
&lt;span id=&quot;cb10-4&quot;&gt;&lt;a href=&quot;#cb10-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;WHERE&lt;/span&gt;  sql_text &lt;span class=&quot;kw&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;SELECT ... FROM t WHERE col=?&amp;#39;&lt;/span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;IS_BIND_SENSITIVE=&amp;#39;Y&amp;#39;&lt;/code&gt;는 &amp;quot;이 커서는 bind 값에 민감할
가능성이 있다&amp;quot;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;IS_BIND_AWARE=&amp;#39;Y&amp;#39;&lt;/code&gt;는 &amp;quot;실제로 여러 계획을
쓰는 상태다&amp;quot;를 뜻한다. ACS는 자동이지만 관측이 필요하고, 갑자기 p99가
튀는 쿼리가 있으면 여기 두 플래그를 먼저 확인하는 게 루틴이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 걸음 더 들어가려면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SQL_CS_STATISTICS&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SQL_CS_HISTOGRAM&lt;/code&gt;을 본다. 전자는 child cursor별로 peek된
bind 범위와 실행 통계를, 후자는 실행 버킷을 보여준다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb11&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb11-1&quot;&gt;&lt;a href=&quot;#cb11-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;SELECT&lt;/span&gt; sql_id, child_number, bind_set_hash_value,&lt;/span&gt;
&lt;span id=&quot;cb11-2&quot;&gt;&lt;a href=&quot;#cb11-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;       peeked_low, peeked_high, executions, rows_processed&lt;/span&gt;
&lt;span id=&quot;cb11-3&quot;&gt;&lt;a href=&quot;#cb11-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;FROM&lt;/span&gt;   v$sql_cs_statistics&lt;/span&gt;
&lt;span id=&quot;cb11-4&quot;&gt;&lt;a href=&quot;#cb11-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;WHERE&lt;/span&gt;  sql_id &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;&amp;amp;sql_id&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;#39;&lt;/span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉬운 지점이 있다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ACS는 cold-start에
약하다&lt;/strong&gt;. bind-aware로 전환되려면 Oracle이 &amp;quot;이 bind 값 범위에서는
실행 통계가 다르다&amp;quot;는 증거를 충분히 모아야 하는데, 그 학습 기간에는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;첫 번째 hard parse 때 peek된 값 기반의 계획이 모든 bind에
재사용&lt;/strong&gt;된다. 배포 직후나 Shared Pool flush 직후에는 ACS가 아직
bind-sensitive만 찍혀 있고 bind-aware로 못 올라간 상태라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스큐
데이터에서 나쁜 계획이 몇 분~몇 시간 돌 수 있다&lt;/strong&gt;. 이 구간을
줄이려면 SQL Plan Baseline이나 SQL Profile로 미리 계획을 고정해 두는
편이 안전하다.&lt;/p&gt;
&lt;h3 id=&quot;6-3-library-cache-lock--pin--oracle-고유-경합&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-3) library
cache lock / pin — Oracle 고유 경합&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 함정 하나. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Library Cache 공유 = 효율 단독
금지&lt;/strong&gt;다. 파싱된 SQL을 공유 메모리에 둔다는 것은 곧
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;래치·mutex·lock·pin 같은 동기화 장치가 이 위에 올려져
있다&lt;/strong&gt;는 의미다. 특히 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;library cache lock&lt;/strong&gt;과
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;library cache pin&lt;/strong&gt;은 Oracle에 고유한 경합 이벤트로,
DDL과 DML이 같은 객체를 동시에 건드리거나, 대규모 hard parse가 몰릴 때
심하게 나타난다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$SESSION_WAIT&lt;/code&gt;나 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;V$LOCK&lt;/code&gt;에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;library cache&lt;/code&gt;로 시작하는 이벤트가 자주 보인다면 Shared Pool
내부 경합이 병목이라는 신호다. DDL을 야간으로 미루거나, bind 변수 사용을
강제해 hard parse를 줄이는 쪽으로 대응한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;두 이벤트는 성격이 다르다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;library cache lock&lt;/strong&gt;은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;객체 정의 자체의 변경&lt;/strong&gt;을 직렬화하는 락으로, DDL이
주범이다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;library cache pin&lt;/strong&gt;은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;객체를 실행하는
동안 정의가 바뀌지 않도록 고정&lt;/strong&gt;하는 장치라, 장시간 PL/SQL 실행과
DDL이 겹칠 때 pin 대기가 길어진다. 둘을 구분해야 대응도 갈린다 — lock
경합이면 DDL 타이밍과 객체 범위 축소, pin 경합이면 장시간 실행 PL/SQL의
분할이 답이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-rac로-가는-길-단일-인스턴스에서-공유-everything으로&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7) RAC로
가는 길: 단일 인스턴스에서 공유 everything으로&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기까지 그린 단일 인스턴스 구조 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SGA + PGA + 백그라운드
프로세스 + SCN/UNDO/Library Cache&lt;/strong&gt; — 가 RAC(Real Application
Clusters)의 출발점이다. RAC는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;여러 노드가 같은 datafile을
공유&lt;/strong&gt;하는 shared-everything 아키텍처로, 각 노드가 위에서 본
구조를 그대로 하나씩 가진 뒤 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Cache Fusion&lt;/strong&gt;으로 버퍼
캐시를 노드 간에 조율한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Cache Fusion과 GRD&lt;/strong&gt;. 노드 A가 갖고 있는 버퍼 블록을
노드 B가 읽으려 하면, 디스크를 경유하지 않고 인터커넥트로 블록 이미지를
전송한다. 이 과정을 관리하는 게 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;GRD(Global Resource
Directory)&lt;/strong&gt; 로, 클러스터 전체에 분산돼 각 블록·락의 &amp;quot;마스터
노드&amp;quot;를 기록한다. 실무 조율 주체는 두 서비스다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;GCS(Global
Cache Service)&lt;/strong&gt; 는 데이터 블록의 global 소유권을,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;GES(Global Enqueue Service)&lt;/strong&gt; 는 트랜잭션 락·library
cache lock 같은 non-data 자원을 관리한다. 단일 인스턴스의 래치·mutex
개념이 클러스터 단위로 확장된 모습이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SCN의 노드 간 동기화&lt;/strong&gt;. SCN은 단일 인스턴스에서 그냥
단조 증가 카운터였지만, RAC에서는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;여러 노드가 동일한 SCN 공간을
공유&lt;/strong&gt;해야 한다. Oracle은 Lamport 방식 broadcast SCN을 기본으로
쓰면서, 노드 간 메시지에 현재 SCN을 piggyback해 서로를 끌어올린다(필요
시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;_lgwr_async_broadcasts&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MAX_COMMIT_PROPAGATION_DELAY&lt;/code&gt;
계열 파라미터로 조정). 한 노드의 SCN 급증이 다른 노드로 번지는 &lt;a href=&quot;#5-1-scn--64비트-단조-증가-논리-시계&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§5-1&lt;/a&gt; Headroom 위험은 이
동기화 메커니즘의 자연스러운 귀결이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Library Cache의 global 경합&lt;/strong&gt;. 단일 인스턴스에서도
library cache lock·pin 경합이 있었는데, RAC에서는 이 자원 자체가
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;global resource&lt;/strong&gt;로 확장된다. 한 노드가 패키지를
컴파일하는 동안 다른 노드에서 같은 객체를 실행하면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;gc library cache lock&lt;/code&gt; 같은 global 대기가 발생한다. 단일
인스턴스에선 지역 문제였던 DDL 배치 타이밍이 RAC에선 클러스터 전체 성능
이슈로 번지는 이유다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;즉 이 글에서 잡은 네 축은 그대로 RAC 운영 축과 1:1 대응된다 — Buffer
Cache → Cache Fusion/GCS, SCN → broadcast SCN, 락/래치 → GES, Library
Cache → global library cache. 반대로 말하면, 단일 인스턴스에서 이 네
축을 제대로 못 잡아 두면 RAC에서는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;인터커넥트 지연이
곱해져&lt;/strong&gt; 같은 문제가 몇 배로 커진다. 예컨대 hard parse가 많은
애플리케이션을 RAC로 올리면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;gc library cache lock&lt;/code&gt; 대기가
새로 붙어 단일 인스턴스 때보다 훨씬 큰 부하를 만든다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;RAC 이야기는 별도 편으로 분리한다. 여기서는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;단일 인스턴스
구조가 곧 분산 확장의 기반 언어&lt;/strong&gt;라는 점만 기억해 두자.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;연결 모델은 기본 Dedicated을 쓰고, 바꿀 땐 제약 체크를
선행한다&lt;/strong&gt; — Shared는 LOB/AQ 비호환, Threaded는 OS 인증 차단,
DRCP는 세션 상태 단절.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;HugePages는 ASMM과 세트로 간다&lt;/strong&gt; — AMM + HugePages
조합은 구성 난도가 높아 운영에서는 피하는 쪽이 낫다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_TARGET&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PGA_AGGREGATE_LIMIT&lt;/code&gt;을 둘 다 명시한다&lt;/strong&gt; — hard limit
없이 운영하면 ORA-04036 방어선이 없어 OS swap까지 밀려갈 수 있다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNDO_RETENTION&lt;/code&gt;은 best-effort임을 잊지
않는다&lt;/strong&gt; — 장기 분석 쿼리가 있는 환경에선
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RETENTION GUARANTEE&lt;/code&gt;와 UNDO tablespace 사이징을 같이
손본다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ORA-01555는 쿼리 튜닝 실패가 아니라 UNDO 운영
이슈다&lt;/strong&gt; — 먼저 UNDO tablespace 용량·retention·delayed
cleanout부터 확인한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Library Cache 경합 이벤트는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;library cache&lt;/code&gt;
접두어로 검색한다&lt;/strong&gt; — hard parse 감소(bind 변수), DDL 야간 배치,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CURSOR_SHARING&lt;/code&gt; 검토가 표준 대응 세트다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AWR/ASH는 라이선스 경계선을 먼저 확인한다&lt;/strong&gt; — 무료
범위는 Statspack + 기본 V$ 뷰. 모르고 쿼리하면 Diagnostic Pack 위반이 될
수 있다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CKPT가 쓴다는 설명은 다시 보면 틀렸다&lt;/strong&gt; — 더티 블록
flush는 DBWR, redo flush는 LGWR, CKPT는 control file과 datafile header의
SCN 갱신. 장애 분석 중 I/O 주체를 헷갈리지 말자.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TUNED_UNDORETENTION&lt;/code&gt;을 운영 지표에
포함한다&lt;/strong&gt; — 파라미터 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNDO_RETENTION&lt;/code&gt;이 아니라
Oracle이 실제로 쥐고 있는 값을 기준으로 장기 쿼리 허용 한도를
잡는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ACS가 bind-aware로 올라올 때까지의 cold-start 구간을
각오한다&lt;/strong&gt; — 배포·Shared Pool flush 직후 몇 분은 첫 peek 기반
계획이 모두에게 적용되는 위험 구간이다. 핵심 쿼리는 SQL Plan
Baseline으로 고정해 두면 안전하다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;DRCP를 쓰면 애플리케이션이 매 요청마다 세션 상태를
초기화한다&lt;/strong&gt; — Pooled Server가 다른 요청으로부터 &amp;quot;오염된&amp;quot; 상태일
수 있다고 가정하고 코딩한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Library Cache lock과 pin은 성격이 다르다&lt;/strong&gt; — lock은
DDL, pin은 장시간 실행 PL/SQL이 주원인. 구분해서 진단한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;숨은 파라미터(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;_&lt;/code&gt;로 시작)는 Oracle Support SR
없이 건드리지 않는다&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;_pga_max_size&lt;/code&gt; 같은 값은
되돌릴 수 없는 사이드 이펙트가 있을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;9-한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;9) 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Oracle은 연결 모델 4종·SGA 6영역·백그라운드 프로세스
8종·SCN·UNDO·Library Cache라는 세 공유 장치로 단일 인스턴스를
설명한다&lt;/strong&gt;. 이 구조는 우연이 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;RAC·Data Guard 확장을
염두에 둔 설계 언어&lt;/strong&gt;다. 관측 가능성의 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;상당 부분이
유료이고 UNDO·SCN·Library Cache 각각이 고유 장애 모드&lt;/strong&gt;를
가진다는 점을 잊지 말자.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;tags: oracle, oracle-database, sga, pga, lgwr, dbwr, scn, undo,
library-cache, drcp&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/Database</category>
      <category>DBWR</category>
      <category>drcp</category>
      <category>LGWR</category>
      <category>library-cache</category>
      <category>oracle</category>
      <category>oracle-database</category>
      <category>pga</category>
      <category>scn</category>
      <category>SGA</category>
      <category>undo</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/412</guid>
      <comments>https://dding-shark.tistory.com/412#entry412comment</comments>
      <pubDate>Wed, 15 Apr 2026 14:49:02 +0900</pubDate>
    </item>
    <item>
      <title>MySQL(InnoDB) 아키텍처 해부 &amp;mdash; 스레드&amp;middot;버퍼 풀&amp;middot;redo/undo&amp;middot;purge&amp;middot;doublewrite&amp;middot;binlog</title>
      <link>https://dding-shark.tistory.com/411</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;mysqlinnodb-아키텍처-해부--스레드버퍼-풀redoundopurgedoublewritebinlog&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;MySQL(InnoDB)
아키텍처 해부 — 스레드·버퍼 풀·redo/undo·purge·doublewrite·binlog&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PostgreSQL은 OS를 믿고 편집기를 얹었다. MySQL InnoDB는 OS를 믿지 않고
스토리지 엔진 레이어를 따로 세웠다. 같은 &amp;quot;관계형 DB&amp;quot;라는 이름을 달고
있지만, 두 엔진의 내부 철학은 이 한 문장에서 갈라진다. PG가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;를 두고 나머지 캐싱을 OS page cache에
위임하는 쪽이라면, InnoDB는 버퍼 풀·redo·undo·doublewrite·binlog까지
전부 자기 손으로 관리한다. 심지어 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서버 레이어&lt;/strong&gt;와
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스토리지 엔진 레이어&lt;/strong&gt;를 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;handlerton&lt;/code&gt;이라는
가상 테이블 계약으로 나눠 두는 구조다. 이 구조가 머릿속에 없으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_log_at_trx_commit&lt;/code&gt;이나 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sync_binlog&lt;/code&gt;
같은 파라미터가 &amp;quot;뭔지는 모르지만 일단 1로 둬라&amp;quot;로 끝난다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글을 쓰는 관점은 하나다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본값이 왜 이런 숫자인지,
그리고 언제 바꿔야 하는지&lt;/strong&gt;를 각 서브시스템 단위로 정리한다.
운영에서 만나는 파라미터가 200개가 넘지만, 실제로 장애와 직결되는 것은
20개 안팎이다. 이 20개를 쓰임새 기준으로 묶어 두면 다음 튜닝 때
머릿속에서 빠르게 소환할 수 있다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 MySQL 8.0/8.4 기준으로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스레드 모델 → 버퍼 풀 → redo →
doublewrite → undo/purge → binlog → 실무&lt;/strong&gt; 순서로, InnoDB의
지도를 한 번 그려 둔다. 1편의 PostgreSQL 아키텍처와 짝을 이루는 편이다.
두 엔진을 같은 좌표계에 올려 두면 이후 락·MVCC·복제 비교가 훨씬
가벼워진다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;버퍼 풀은 크게 잡을수록 좋다는 도식은 절반만 맞다&lt;/strong&gt; —
instance 자동 분할과 mutex contention이 숨어 있다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;change buffer가 쓰기를 빠르게 만들어 준다는 서술은 SSD에서
뒤집힌다&lt;/strong&gt; — merge 시점의 p99 스파이크&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;doublewrite를 &amp;quot;무결성 전능 장치&amp;quot;로 오해하지 말자&lt;/strong&gt; —
partial page write만 방어한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_log_at_trx_commit=2&lt;/code&gt;는 &amp;quot;OS crash만
1초 손실&amp;quot;이다&lt;/strong&gt; — mysqld crash는 방어한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;binlog와 redo의 2PC는 group commit 2층 구조다&lt;/strong&gt; — TPS
회복은 동시성 조건부다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;왜 → 구조 → 동작 → 주의 → 실무&lt;/strong&gt; 순으로
정리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-스레드-모델-thread-per-connection과-handlerton&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1)
스레드 모델: thread-per-connection과 handlerton&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-innodb-버퍼-풀-lru-midpointahichange-buffer&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2) InnoDB
버퍼 풀: LRU midpoint·AHI·change buffer&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-redo-log-group-commit-1층과-flush-전략&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3) redo log:
group commit 1층과 flush 전략&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-doublewrite-buffer-partial-page-write-방어선&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4)
doublewrite buffer: partial page write 방어선&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-undo-tablespace와-purge-thread&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5) undo tablespace와
purge thread&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-binlog와-2pc-group-commit-2층&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6) binlog와 2PC: group
commit 2층&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8) 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-스레드-모델-thread-per-connection과-handlerton&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1) 스레드
모델: thread-per-connection과 handlerton&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;MySQL을 기동하면 올라오는 단일 프로세스가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mysqld&lt;/code&gt; 하나다.
1편에서 본 PostgreSQL의 postmaster와 역할이 비슷해 보이지만 결정적으로
다르다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PG는 postmaster가 fork로 프로세스를 찍는다면, MySQL은
listener 스레드가 accept 후 worker 스레드에 넘긴다&lt;/strong&gt;. 즉 MySQL은
단일 프로세스 안에서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스레드 풀 모델&lt;/strong&gt;을 돈다. 이 차이가
연결 비용, 격리 실패 영향 범위, 메모리 사용 패턴까지 줄줄이 달라지게
만든다. 공식 문서의 정의가 한 줄이다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;one-thread-per-connection: Creates a new thread for each client
connection.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/connection-threads.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;MySQL
8.0 Reference Manual §7.1.12 Connection Management&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;기본값은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;thread_handling=one-thread-per-connection&lt;/code&gt;. 연결
하나당 스레드 하나를 새로 만든다. 끝나면 회수하고, 다음 연결에
재사용한다. 프로세스 per connection이 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스레드 per
connection&lt;/strong&gt;이다. fork 비용이 없는 대신, 동시 연결 수천 개가
몰리면 컨텍스트 스위칭이 그대로 병목이 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Linux 커널 관점에서 한 줄 — 스레드는 커널에서 &amp;quot;경량 프로세스&amp;quot;로
스케줄된다. PG의 프로세스 per connection과 구조적 차이는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;메모리
공간 공유 여부&lt;/strong&gt;뿐이다. 스레드는 동일 주소 공간을 공유하므로 연결
간 데이터 공유가 쉽지만, 한 스레드의 힙 손상이 전체 프로세스를 끌어내릴
수 있다는 취약성도 가진다. PG가 &amp;quot;한 세션 죽어도 전체가 살아남는&amp;quot;
안정성을 얻는 대신 fork 비용을 감수하는 이유다.&lt;/p&gt;
&lt;h3 id=&quot;1-1-thread_handling-세-값&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-1) thread_handling 세 값&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;값&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;동작&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;가용성&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;one-thread-per-connection&lt;/code&gt; (기본)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;연결당 스레드 1개&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Community + Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;no-threads&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;단일 스레드 (디버그·임베디드)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;모든 에디션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pool-of-threads&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;고정 풀에서 dispatch&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Enterprise 전용&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;thread pool을 쓰면 되지 않나&amp;quot;는 OSS
MySQL에선 해당 없다&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pool-of-threads&lt;/code&gt;는 MySQL
Enterprise 또는 Percona Server/MariaDB의 자체 구현에서만 제공된다.
Community 8.0은 여전히 one-thread-per-connection만 쓸 수 있다. 5천 동시
연결 OLTP에서 Community 그대로 돌리면, 스레드 수 자체가 커널 스케줄러를
끌어내리는 단계로 진입한다. 현실적 대응은 ProxySQL/PgBouncer 같은 풀러를
앞단에 두거나, Percona·MariaDB로 넘어가는 쪽이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;연결당 스레드가 생기는 비용이 PG의 fork보다 싸다는 점은 맞다.
그렇지만 싸다는 것이 &amp;quot;무한히 만들어도 된다&amp;quot;는 뜻은 아니다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;thread_cache_size=N&lt;/code&gt;을 통해 종료된 스레드를 일정 수까지
재사용해 생성 비용을 줄일 수 있다. 기본값은 서버 사양에 따라 자동
결정되지만, 연결 rate이 높으면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Threads_created&lt;/code&gt; 카운터의
증가 속도를 보고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;thread_cache_size&lt;/code&gt;를 키운다. 이 값이 커넥션
생성당 1 가깝게 증가하고 있다면 캐시가 부족하다는 뜻이다.&lt;/p&gt;
&lt;h3 id=&quot;1-2-handlerton--서버와-엔진-사이의-vtable-계약&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-2) handlerton
— 서버와 엔진 사이의 vtable 계약&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;MySQL의 아키텍처에서 가장 먼저 익혀야 할 개념이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;handlerton&lt;/code&gt;이다. 한 단어로 말하면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;스토리지 엔진이
서버 레이어에 자신을 등록하는 가상 테이블&amp;quot;&lt;/strong&gt; 이다. InnoDB,
MyISAM, Memory, Archive 같은 엔진들이 각자 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;handlerton&lt;/code&gt;
구조체에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;create&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;open&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;commit&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;rollback&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;prepare&lt;/code&gt; 등의 함수 포인터를 채워 넣어
서버에 내민다. 서버 레이어(파서·옵티마이저·binlog·캐시)는 엔진을
추상화된 API로만 부르고, 구체 구현은 엔진 쪽이 책임진다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PG와 비교가 명확하다. PG는 heap access method 하나를 중심으로 돌고
(pluggable storage가 12부터 들어왔지만 사실상 heap 단일),
플래너·WAL·MVCC가 heap 구조를 직접 알고 있다. MySQL은 반대로
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서버가 엔진을 모르고, 엔진이 서버에 맞춘다&lt;/strong&gt;. 그래서
MySQL은 엔진을 바꿔 끼울 수 있고 (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ENGINE=MyISAM&lt;/code&gt; 같은 테이블
옵션), PG는 그럴 수 없다. InnoDB가 MySQL 5.5.5부터 default로 승격된 뒤로
사실상 MySQL = InnoDB처럼 보이지만, 구조상 여전히 별도 레이어다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 분리는 기능적으로 양날의 칼이다. 장점은 엔진 선택의 유연성 — 로그
테이블에 MyISAM을 쓰거나, in-memory 세션 테이블에 MEMORY 엔진을 쓰거나,
아카이빙에 ARCHIVE를 쓰는 식의 혼용이 가능하다. 단점은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;최적화
깊이가 얕아진다&lt;/strong&gt;는 점이다. 플래너가 엔진 내부를 모르고
handlerton API로만 부르다 보니, PG처럼 heap 레이아웃에 특화된 실행
계획을 만들기 어렵다. 그래서 MySQL은 Index Condition Pushdown,
Multi-Range Read 같은 장치를 handlerton 위에 덧붙여 깊이 부족을 메우는
방향으로 진화해 왔다.&lt;/p&gt;
&lt;h3 id=&quot;1-3-binlog가-서버-레이어에-있는-이유&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-3) binlog가 서버
레이어에 있는 이유&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;handlerton 얘기가 중요한 이유 하나 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;binlog는 서버 레이어에
붙어 있다&lt;/strong&gt;. 엔진별 로그가 아니다. 그래서 InnoDB redo와 binlog의
동기화를 위해 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;2PC&lt;/strong&gt;가 필요하고, 이게 &lt;a href=&quot;#6-binlog와-2pc-group-commit-2층&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§6 binlog와 2PC&lt;/a&gt;의 핵심
주제가 된다. PG가 WAL 하나에 모든 정보를 쌓는 것과 달리, MySQL은 엔진
레이어의 redo와 서버 레이어의 binlog를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;두 개의 로그로
분리&lt;/strong&gt;하고 그 사이를 2PC로 묶는다. 복잡해 보이지만 이유가 있다 —
binlog는 엔진 중립 포맷이라 이기종 엔진 간 복제가 가능하고,
statement/row/mixed 포맷 선택이 엔진과 독립적이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 이중 로그 구조가 만들어 내는 대가는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;쓰기 경로가 PG보다
길다&lt;/strong&gt;는 점이다. PG는 WAL 한 번만 fsync하면 복제와 복구가 같은
로그로 해결되지만, MySQL은 &lt;a href=&quot;#3-2-innodb_flush_log_at_trx_commit--012의-실제-의미&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§3-2
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_log_at_trx_commit&lt;/code&gt;&lt;/a&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sync_binlog&lt;/code&gt; 두 값을 각각 조율해야 한다. 두 축의 조합이 곧
지속성 SLA다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;스레드 모델과 레이어 분리를 잡았으니, 이제 InnoDB 엔진 안쪽으로
들어간다. 가장 먼저 만나는 영역이 버퍼 풀이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-innodb-버퍼-풀-lru-midpointahichange-buffer&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2) InnoDB 버퍼
풀: LRU midpoint·AHI·change buffer&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;InnoDB 버퍼 풀은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;페이지 캐시 + 쓰기 버퍼 + 인덱스
가속기&lt;/strong&gt;를 한 덩어리로 묶은 공간이다. PG의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;와 역할은 같지만 내부 구조가 훨씬 많다. 기본
페이지 크기는 16KB (PG는 8KB). 기본 크기는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_buffer_pool_size=128MB&lt;/code&gt;, 운영 권장은 RAM의 50~75%다.
PG의 25% 권장과 확연히 다른데, 이유는 간단하다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;InnoDB는 OS page
cache를 이중으로 쓰지 않는다&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_method=O_DIRECT&lt;/code&gt;로 OS 캐시를 우회하므로 버퍼
풀이 사실상 유일한 캐시 레이어다. 그래서 RAM을 더 많이 먹어야 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;db-deepdive-02-mysql-innodb-architecture-01&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;db-deepdive-02-mysql-innodb-architecture-01&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-1-lru-midpoint--full-scan이-hot을-쫓아내지-않게&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-1) LRU
midpoint — full scan이 hot을 쫓아내지 않게&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;InnoDB의 LRU는 단순한 LRU가 아니다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;중간 지점(midpoint)으로
쪼갠 변형 LRU&lt;/strong&gt;다. 리스트의 5/8이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;new sublist&lt;/code&gt;(hot),
3/8이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;old sublist&lt;/code&gt;(cold)로 나뉘고, 새로 들어온 페이지는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;new의 맨 앞이 아니라 old의 맨 앞&lt;/strong&gt;에 꽂힌다. 공식 문서가
정확히 적는다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;3/8 of the buffer pool is devoted to the old sublist.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;MySQL
8.0 Reference Manual §17.5.1 Buffer Pool&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 구조가 막는 것은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;full table scan이 hot 페이지를 전부
쓸어내는 현상&lt;/strong&gt;이다. 전체 테이블 스캔이 들어와도 페이지가 일단
old sublist에만 채워지고,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;1초(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_old_blocks_time=1000ms&lt;/code&gt;) 이상 머물면서
재참조되어야&lt;/strong&gt; new로 승격된다. 대부분의 full scan 페이지는 한 번
보고 버려지므로 old 구간에서 바로 evict된다. hot 워킹셋은 흔들림 없이
new에 남는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;기본값은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_old_blocks_pct=37&lt;/code&gt;(3/8 ≈ 37%),
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_old_blocks_time=1000&lt;/code&gt;(ms). 대용량 배치 쿼리가 OLTP와
섞여 도는 환경에서는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;old_blocks_time&lt;/code&gt;을 더 길게(5000ms 등)
키우는 것이 보호에 유리하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;관측은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;information_schema.innodb_buffer_page_lru&lt;/code&gt;에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;is_old=&amp;#39;YES&amp;#39;&lt;/code&gt;인 페이지 수를 집계해 old sublist의 실제 비율을
확인할 수 있다. 이 값이 설정 비율과 크게 다르다면 scan 워크로드가 왜곡을
만들고 있다는 신호다.&lt;/p&gt;
&lt;h3 id=&quot;2-2-ahi--적응형-해시-인덱스&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-2) AHI — 적응형 해시 인덱스&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Adaptive Hash Index&lt;/strong&gt;(AHI)는 InnoDB가 자주 조회되는
B-tree 경로를 관측해서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동으로 만드는 해시 인덱스&lt;/strong&gt;다.
사용자가 건드리는 게 아니고, 엔진이 런타임에 &amp;quot;이 패턴이 반복적이면
해시로 줄여주는 게 낫다&amp;quot;고 판단해 붙인다. 덕분에 B-tree 3~4단 내려가는
경로가 O(1) 탐색으로 바뀐다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;구조 제약도 있다. AHI는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;같은 prefix의 같은 길이
조회&lt;/strong&gt;가 반복될 때만 만들어진다. 즉 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WHERE col=?&lt;/code&gt;처럼
동일 컬럼·동일 패턴 조회가 일관되게 들어오는 OLTP에 가장 잘 맞고, ad-hoc
분석 쿼리처럼 매번 다른 경로를 타는 워크로드에는 득이 없다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;함정은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;글로벌 mutex 경합&lt;/strong&gt;이다. 8.0에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_adaptive_hash_index_parts=8&lt;/code&gt;(기본)로 파티션이
쪼개지긴 했지만, 여전히 높은 동시성 OLTP에서 AHI latch가 병목으로
올라오는 경우가 있다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt;의
SEMAPHORES 섹션에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;btr_search_latch&lt;/code&gt; 대기가 자주 보이면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_adaptive_hash_index=OFF&lt;/code&gt;를 검토한다. &amp;quot;성능 가속기&amp;quot;가
&amp;quot;글로벌 병목&amp;quot;으로 뒤집히는 지점이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;판정의 기준은 세 가지다 — (1) AHI hit rate
(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Hash searches/s&lt;/code&gt; vs &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Non-hash searches/s&lt;/code&gt;), (2)
SEMAPHORES 섹션의 AHI 관련 대기, (3) OFF로 전환했을 때 응답 시간 p99의
변화. 운영 데이터 일부 구간에서 OFF로 바꿔 AB 테스트를 돌려 보면 성능
회귀가 있는지 없는지 금방 드러난다.&lt;/p&gt;
&lt;h3 id=&quot;2-3-change-buffer--non-unique-secondary-index-전용&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-3) change
buffer — non-unique secondary index 전용&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;change buffer는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;secondary index에 대한 변경을 버퍼 풀에 없는
페이지에 대해서는 물리 페이지를 읽어오지 않고 미뤄 두는&lt;/strong&gt; 장치다.
나중에 그 페이지가 다른 이유로 읽힐 때 한 번에 merge한다. 랜덤 디스크
IO를 줄이는 효과가 큰데, 두 가지 제약이 있다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 장치도 역사적 맥락에서 태어났다. 5.5 이전의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;insert buffer&lt;/code&gt;가 INSERT만 처리하던 것을, 5.5부터
INSERT/UPDATE/DELETE 모두를 포괄하는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;change buffer&lt;/code&gt;로 확장한
것이다. 이름이 남아 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ibuf&lt;/code&gt;로 부르기도 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;unique secondary index에는 적용되지 않는다&lt;/strong&gt;.
unique constraint는 즉시 중복 검사가 필요하므로 미룰 수 없다. primary
key에도 당연히 안 붙는다. 즉 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;non-unique secondary
index만&lt;/strong&gt; 혜택을 본다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;같은 이유로 full-text index, spatial index 등 특수 인덱스 유형도
change buffer 대상이 아니다. 결과적으로 혜택을 보는 구간은 &amp;quot;non-unique
B-tree secondary index&amp;quot; 한 가지로 좁혀진다. 실제 인덱스 구성에서 이
조건을 만족하는 비율이 생각보다 크지 않은 경우가 많다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;둘째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SSD에서는 득실이 뒤집힌다&lt;/strong&gt;. change buffer의
존재 이유는 &amp;quot;랜덤 IO가 비싼 HDD&amp;quot;를 전제로 한다. SSD·NVMe에서는 랜덤 IO가
순차 IO와 거의 같은 비용이고, 대신 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;merge 시점에 한꺼번에
쏟아지는 IO 스파이크&lt;/strong&gt;가 p99 레이턴시를 망친다. 8.0 기본값은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_change_buffer_max_size=25&lt;/code&gt;(버퍼 풀의 25%까지),
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_change_buffering=all&lt;/code&gt;. SSD 환경에서는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_change_buffering=&amp;#39;none&amp;#39;&lt;/code&gt;을 진지하게 검토할 만하다.
&amp;quot;쓰기가 빠르다&amp;quot;는 이득보다 &amp;quot;예측 가능한 쓰기&amp;quot;가 더 값질 수 있다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;관측은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt;의 INSERT BUFFER AND
ADAPTIVE HASH INDEX 섹션이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ibuf: size N, free list len M, seg size K&lt;/code&gt;로 change buffer
사용량이 보이고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;merged operations&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;discarded operations&lt;/code&gt; 카운터로 merge 효율을 볼 수 있다.
size가 꾸준히 커지는데 merge가 적게 일어나면 &amp;quot;빚이 쌓이고 있다&amp;quot;는
신호다.&lt;/p&gt;
&lt;h3 id=&quot;2-4-instance-분할--1gb-임계와-mutex-contention&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-4) instance
분할 — 1GB 임계와 mutex contention&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;버퍼 풀을 크게 잡을수록 좋다&amp;quot;는 명제는
instance 분할 없이는 틀린다.&lt;/strong&gt; 버퍼 풀은 단일 mutex로 보호되는
구조였고, 여러 스레드가 동시에 페이지 할당·eviction을 하면 이 mutex가
병목이 된다. 해결책이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_buffer_pool_instances&lt;/code&gt;로
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;여러 풀로 쪼개는 것&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;공식 문서의 규칙은 이렇다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;버퍼 풀 총 크기가 1GB 이상이면
instance가 자동으로 8개로 분할&lt;/strong&gt;된다. 1GB 미만은 instance 1개. 즉
256MB로 잡아 두고 &amp;quot;쪼개져 있겠지&amp;quot; 가정하면 틀린다. 하나의 mutex 위에서
전부 경합한다. 128GB 서버에 버퍼 풀 96GB를 잡는다면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_buffer_pool_instances=16&lt;/code&gt; 혹은 32로 명시적으로 키우는
것이 표준이다. 각 instance 크기가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;1GB 미만으로 떨어지지
않도록&lt;/strong&gt; 하는 선에서 결정한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;instance는 페이지의 해시 기반으로 분배된다. 즉 특정 테이블이 특정
instance에만 몰리지 않고 전 instance에 고르게 분산된다. 따라서
instance를 늘리면 mutex 경합은 줄지만, 특정 워크로드에서 특정 instance가
뜨거워지는 현상은 발생하지 않는다. instance별 사용률을 확인하고 싶다면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;information_schema.innodb_buffer_pool_stats&lt;/code&gt;를
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;POOL_ID&lt;/code&gt;로 조회한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;설정&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;권장&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;메모&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;버퍼 풀 크기&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;RAM 50~75%&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O_DIRECT 전제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_buffer_pool_instances&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1GB 이상 자동 8&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;instance당 최소 1GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_old_blocks_pct&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;37 (기본)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;full scan 내성 ↑ 시 낮춤&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_old_blocks_time&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1000ms&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;배치 섞이면 5000ms 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_adaptive_hash_index&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;ON (기본)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;AHI latch 경합 시 OFF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_change_buffering&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;all&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;SSD는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;none&lt;/code&gt; 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;2-5-flush-list와-dirty-ratio&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-5) flush list와 dirty ratio&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;버퍼 풀을 LRU로만 이해하면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;쓰기 경로의 관측 지표를
놓친다&lt;/strong&gt;. 버퍼 풀 내부에는 두 개의 리스트가 공존한다 —
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;LRU list&lt;/strong&gt;(hot/cold 관리)와 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;flush
list&lt;/strong&gt;(dirty 페이지를 LSN 오름차순으로 이어 둔 체인). 한 페이지가
변경되면 LRU에 있는 상태 그대로 flush list에도 끼워 넣어진다. page
cleaner thread는 flush list의 꼬리(가장 오래된 LSN)부터 내려가면서
디스크에 써 내린다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;dirty 비율을 조절하는 두 다이얼이 있다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_max_dirty_pages_pct=90&lt;/code&gt; (기본) — 버퍼 풀에서
dirty 페이지가 이 비율을 넘으면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;aggressive flush&lt;/strong&gt;가
들어간다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_max_dirty_pages_pct_lwm=10&lt;/code&gt; — low water mark. 이
비율을 넘는 순간부터 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;preflush&lt;/strong&gt;가 시작되어 조금씩
내려보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;관측은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt;의 BUFFER POOL AND
MEMORY 섹션 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Modified db pages&lt;/code&gt; 라인으로 본다. 이 값이 총
페이지 수의 10%를 넘어가는 타이밍이 preflush 시작점과 맞아떨어지는지
확인하면 디스크 서브시스템이 버텨내고 있는지 감이 잡힌다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 번 더 정리해 두면 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;LRU list는 &amp;quot;메모리에 남겨 둘 페이지&amp;quot;를
고르고, flush list는 &amp;quot;디스크에 내려보낼 dirty 페이지&amp;quot;를 고른다&lt;/strong&gt;.
두 리스트는 같은 페이지 집합을 다른 기준으로 정렬한 것이고, page cleaner
thread는 flush list만 본다. 이 두 축을 분리해 이해해야 &amp;quot;dirty ratio가
높은데 왜 cache hit은 여전히 좋은가&amp;quot; 같은 질문이 자연스럽게 풀린다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;버퍼 풀 구조를 잡았으니 쓰기 경로의 첫 번째 로그로 간다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-redo-log-group-commit-1층과-flush-전략&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3) redo log: group
commit 1층과 flush 전략&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;InnoDB의 redo log는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;커밋된 트랜잭션이 crash 후에도 살아
있음&amp;quot;을 보장하는 첫 번째 축&lt;/strong&gt;이다. PG의 WAL과 역할이 같다.
트랜잭션이 commit하면 redo 레코드가 디스크에 fsync되고, 데이터 페이지는
나중에 lazy하게 내려간다. PG WAL은 FPW로 partial write까지 방어하지만,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;InnoDB redo는 그 책임을 doublewrite에 넘긴다&lt;/strong&gt;. 그래서
redo 자체는 논리 변경만 기록하는 얇은 로그다.&lt;/p&gt;
&lt;h3 id=&quot;3-1-redo-log-파일--8030-이후-통합-파라미터&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-1) redo log 파일 —
8.0.30 이후 통합 파라미터&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;MySQL 8.0.30 이전에는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_log_file_size × innodb_log_files_in_group&lt;/code&gt;으로 redo
총 용량을 계산했다. 8.0.30+부터 이 둘이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_redo_log_capacity&lt;/code&gt; 하나로 통합되었고 이전 두
파라미터는 deprecated다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본값은 100MB&lt;/strong&gt;. 운영에서는
거의 항상 올려야 하는 값이다. 쓰기 TPS가 높은 환경에선 1~8GB 수준으로
잡는다. redo가 작으면 checkpoint가 자주 강제되고, 자주 강제되면 dirty
페이지 flush 스파이크가 튄다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;8.0.30 이전 방식에서 올라오는 경우 주의할 점이 있다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_log_file_size&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_log_files_in_group&lt;/code&gt;을 설정한 채로 업그레이드하면 경고
로그가 남고 값이 무시된다. 마이그레이션 스크립트에서 이 두 파라미터를
걷어내고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_redo_log_capacity&lt;/code&gt;로 교체해야 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;redo 파일은 디스크에서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ring buffer&lt;/strong&gt;처럼 돈다.
capacity를 다 쓰면 앞쪽으로 돌아가 덮어쓰고, 덮어쓰기 전에 해당 구간
LSN까지의 dirty 페이지가 반드시 디스크에 내려가 있어야 한다. 즉
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;redo 크기는 checkpoint 간격과 직결&lt;/strong&gt;된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;내부 구조는 32개의 redo 파일로 쪼개져 있다. 파일명은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;#ib_redo0&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;#ib_redo1&lt;/code&gt;, ... 순이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_redo_log_capacity&lt;/code&gt;를 변경하면 파일 개수와 크기를
자동으로 재배치한다. 8.0.30 이전의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_log_file_size&lt;/code&gt;
방식과 달리 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;재시작이 필요 없어&lt;/strong&gt; 런타임에 사이징 조정이
가능해진 점이 실무 관점에서 가장 큰 개선이다.&lt;/p&gt;
&lt;h3 id=&quot;3-2-innodb_flush_log_at_trx_commit--012의-실제-의미&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-2)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_log_at_trx_commit&lt;/code&gt; — 0/1/2의 실제 의미&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 파라미터는 MySQL 튜닝 가이드에 가장 자주 등장하면서 가장 자주
오해받는다. 세 값의 동작이 완전히 다르다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;값&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;fsync 타이밍&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;mysqld crash 손실&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;OS crash 손실&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;권장 환경&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;1&lt;/code&gt; (기본, ACID)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;매 커밋 write + fsync&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;금융·ACID 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;매 커밋 write, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;1초마다 fsync&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최대 1초&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;일반 OLTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;master thread가 1초마다 write+fsync&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최대 1초&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최대 1초&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;로그성 데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;핵심은 2의 의미다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;=2&lt;/code&gt;는 &amp;quot;OS crash만 1초
손실&amp;quot;이다&lt;/strong&gt;. 매 커밋마다 redo가 OS 페이지 캐시까지는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;반드시 write&lt;/strong&gt;되므로, mysqld 프로세스가 crash해도
데이터는 OS에 남아 있고 자동 복구된다. 1초 손실이 발생하는 경우는 오직
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;OS/커널/하드웨어 레벨 crash&lt;/strong&gt;뿐이다. 전원 차단, 커널
panic, VM 강제 종료 같은 드문 이벤트다. 대부분의 일반 OLTP에서 2는
충분히 현실적인 선택이 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;반면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;=0&lt;/code&gt;은 커밋 자체가 redo write를 하지 않고 백그라운드
스레드가 1초마다 묶어서 write+fsync한다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;mysqld crash로도 1초가
날아간다&lt;/strong&gt;. 정말 &amp;quot;로그 테이블&amp;quot; 류가 아니면 쓰지 않는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;벤치마크 상의 차이는 스토리지와 동시성에 따라 크게 갈린다. 높은
동시성 OLTP에서 1과 2의 성능 차이는 group commit 덕분에 체감 미미할 수
있지만, 저동시성 단일 연결 커밋 루프에서는 2가 1의 3~5배 처리량을 보이는
경우도 있다. &amp;quot;무조건 1이 맞다&amp;quot;가 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;워크로드와 지속성 SLA를
함께 보고&lt;/strong&gt; 결정한다.&lt;/p&gt;
&lt;h3 id=&quot;3-3-group-commit-1층--redo-log-buffer-묶기&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-3) group commit
1층 — redo log buffer 묶기&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;redo log buffer(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_log_buffer_size&lt;/code&gt;, 기본 16MB)는
여러 트랜잭션의 redo 레코드를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;하나의 fsync에 묶어
내려보내는&lt;/strong&gt; 구조다. 이게 &amp;quot;group commit 1층&amp;quot;이다. 동시 연결 N개가
거의 동시에 커밋하면, 리더 하나가 fsync를 호출하고 나머지는 그 fsync가
끝날 때까지 대기했다가 &amp;quot;이미 내가 쓴 LSN까지 내려갔으니 OK&amp;quot;로
반환받는다. 커밋 처리량이 동시성에 따라 선형 이상으로 올라가는
이유다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;다만 함정이 있다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;group commit의 TPS 회복 효과는 동시 커밋이
충분히 있어야 성립한다&lt;/strong&gt;. 직렬 배치처럼 한 스레드가 순차적으로
커밋하는 워크로드에서는 묶을 상대가 없으므로 매 커밋이 독립 fsync를
친다. &amp;quot;group commit 켜면 빨라진다&amp;quot;는 기대는 고동시 OLTP 조건부
명제다.&lt;/p&gt;
&lt;h3 id=&quot;3-4-checkpoint와-flush-다이얼&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-4) checkpoint와 flush
다이얼&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;redo 크기를 키우고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;trx_commit&lt;/code&gt;을 조율하는 것만으로는
부족하다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;dirty 페이지가 디스크로 내려가는 속도 자체&lt;/strong&gt;를
조절하는 네 개의 다이얼이 있다. §3-2의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;trx_commit&lt;/code&gt; 표와
역할이 겹치지 않는다 — 그쪽은 &amp;quot;커밋 시점 redo fsync&amp;quot;이고, 이쪽은 &amp;quot;page
cleaner가 테이블 페이지를 디스크에 내려쓰는 속도&amp;quot;다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파라미터&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;기본값&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_io_capacity&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;200&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;초당 I/O 기준선 (HDD 기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_io_capacity_max&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2000&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;aggressive flush 시 상한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_neighbors&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;0 (SSD) / 1&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;인접 페이지 동시 flush, SSD는 0 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_lru_scan_depth&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1024&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;LRU tail에서 flush할 수 있는 깊이&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;io_capacity&lt;/code&gt;는 &amp;quot;page cleaner가 정상 상태에서 쓸 수 있는
IOPS&amp;quot;의 기준이다. 2020년 이후의 NVMe라면 기본 200은 턱없이 낮다.
2000~10000 범위에서 스토리지 스펙을 보고 잡는 것이 현실이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;io_capacity_max&lt;/code&gt;는 dirty ratio가 상한에 가까워지면 달려
들어가는 aggressive flush의 천장이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;fuzzy checkpoint&lt;/strong&gt;라는 이름을 붙여 두면 이해가 쉽다.
Oracle이나 초창기 DB들이 주기적으로 전체 flush를 하던 방식과 달리,
InnoDB는 page cleaner thread가 flush list 꼬리를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;지속적으로
조금씩&lt;/strong&gt; 내려보낸다. 한 번에 폭포처럼 쏟아지지 않으므로 IO
스파이크가 줄어든다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PG와 대비하면 설계 철학 차이가 선명하다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PG는 checkpointer가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpoint_completion_target&lt;/code&gt;에 맞춰 spread write로 한 번의
checkpoint를 펼쳐 쓰는 방식&lt;/strong&gt;이고, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;InnoDB는 page cleaner
thread가 상시로 조금씩 내려 쓰는 방식&lt;/strong&gt;이다. 둘 다 스파이크
회피라는 같은 목표에 도달했지만, 큐잉 모델이 다르다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_neighbors&lt;/code&gt;는 HDD 시절의 유산 중 하나다.
HDD에서는 인접 페이지를 함께 쓰면 seek 비용이 한 번으로 줄어들어
이득이지만, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SSD에서는 오히려 write amplification을
만든다&lt;/strong&gt;. 8.0 기본값은 스토리지 감지에 따라 자동 조정되지
않으므로, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_neighbors=0&lt;/code&gt;을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SSD 환경에서는
명시적으로&lt;/strong&gt; 설정하는 것이 안전하다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_lru_scan_depth&lt;/code&gt;도 비슷한 맥락으로, 버퍼 풀 instance
수가 많아질수록 instance별 스캔 깊이가 곱해지므로 기본 1024를 절반으로
낮춰도 되는 환경이 있다.&lt;/p&gt;
&lt;h3 id=&quot;3-5-redo-capacity-사이징&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-5) redo capacity 사이징&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_redo_log_capacity&lt;/code&gt;는 단순히 &amp;quot;로그 파일 크기&amp;quot;가
아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;checkpoint 간격의 지렛대&lt;/strong&gt;다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;크게 올리면&lt;/strong&gt; checkpoint가 덜 자주 강제되고, dirty
페이지가 더 오래 메모리에 머문다. 같은 페이지의 여러 변경이 한 번의
디스크 쓰기로 합쳐지므로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;쓰기 처리량이 올라간다&lt;/strong&gt;.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;대가는 crash recovery 시간&lt;/strong&gt;이다. redo 재적용량이
비례해 늘어난다. 32GB redo를 full로 채운 상태에서 crash가 나면 복구에 수
분이 걸릴 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;운영 권장은 워크로드에 따라 다르다. 일반 OLTP는 1~4GB, 쓰기 무거운
워크로드는 8~32GB 범위에서 결정한다. 8.0.30+의 핫 변경 덕분에 다시 시작
없이 조절할 수 있으므로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;작게 시작해 관측하며 올리는&lt;/strong&gt;
순서가 안전하다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SHOW ENGINE INNODB STATUS&lt;/code&gt;의 LOG 섹션
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Log sequence number&lt;/code&gt;와 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Last checkpoint at&lt;/code&gt;
차이가 capacity에 근접해 있다면 checkpoint가 빡빡하다는 신호다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;판단 흐름은 이런 순서가 된다. 먼저
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Innodb_os_log_written&lt;/code&gt;의 증가 속도를 측정해서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;1분당
redo 생성량&lt;/strong&gt;을 계산한다. capacity가 그 값의
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;10~30배&lt;/strong&gt;를 수용할 수 있는 크기면 충분하다. capacity가
너무 작아 checkpoint가 자주 강제되면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Innodb_buffer_pool_wait_free&lt;/code&gt;가 치솟으며 커밋 레이턴시가
요동친다. 이 지표가 0이 아닌 순간이 있다면 redo를 먼저 키우는 것이 거의
항상 답이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-doublewrite-buffer-partial-page-write-방어선&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4) doublewrite
buffer: partial page write 방어선&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;InnoDB가 OS를 믿지 않는 가장 직관적인 증거가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;doublewrite
buffer&lt;/strong&gt;다. 설계 전제는 이것이다. &amp;quot;OS나 디스크가 16KB 페이지 쓰기
중간에 죽으면, 일부분만 쓰여서 페이지가 썩을 수 있다.&amp;quot; 이게
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;partial page write&lt;/strong&gt;(또는 torn page)다. redo log가 논리
변경만 담고 있기 때문에, 물리 페이지가 깨져 있으면 redo만으로 복구할 수
없다. 그래서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;페이지를 실제 테이블스페이스에 쓰기 전에 먼저
doublewrite 영역에 순차 기록&lt;/strong&gt;한다. crash 복구 시 doublewrite의
사본으로 테이블스페이스 페이지를 복원한 뒤 redo를 재생한다.&lt;/p&gt;
&lt;h3 id=&quot;4-1-8020-이후-분리된-파일&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-1) 8.0.20 이후 분리된 파일&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;8.0.20 이전에는 doublewrite 영역이 시스템
테이블스페이스(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ibdata1&lt;/code&gt;) 안에 있었다. 8.0.20+부터는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;별도 파일로 분리&lt;/strong&gt;되었다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;#ib_16384_0.dblwr&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;#ib_16384_1.dblwr&lt;/code&gt; 같은
이름이다. 분리 덕분에 doublewrite IO를 별도 디스크로 배치할 수 있고,
파일 자체가 순차 쓰기만 하므로 IO 패턴이 단순하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;관련 파라미터로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_doublewrite_dir&lt;/code&gt;(파일 경로),
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_doublewrite_files&lt;/code&gt;(파일 개수, 기본 buffer pool
instance 수), &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_doublewrite_pages&lt;/code&gt;(파일당 페이지 수,
기본 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_write_io_threads × 128&lt;/code&gt;)가 있다. 운영에서는 거의
건드리지 않지만, doublewrite IO를 전용 NVMe로 분리해 p99 레이턴시를 더
낮추고 싶은 경우 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_doublewrite_dir&lt;/code&gt;만 바꿔 마운트
포인트를 변경한다.&lt;/p&gt;
&lt;h3 id=&quot;4-2-오버헤드--nvme-510-hdd-최대-30&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-2) 오버헤드 — NVMe 5~10%,
HDD 최대 30%&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;doublewrite는 공짜가 아니다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;같은 페이지를 두 번
쓴다&lt;/strong&gt;. 이론상 쓰기량이 2배가 된다. 현실적인 오버헤드는
스토리지에 따라 크게 다르다. &amp;quot;2배&amp;quot;라는 숫자는 단순화된 표현이고,
실제로는 doublewrite 쓰기가 순차 패턴이라 절대 시간상 2배로 늘어나지는
않는다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스토리지&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;오버헤드&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;메모&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;NVMe SSD&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;약 5~10%&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;순차 write 특성으로 저렴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;SATA SSD&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;약 10~15%&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HDD&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최대 20~30%&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;랜덤 seek 비중에 민감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;atomic write 지원 스토리지&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;꺼도 됨&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_doublewrite=OFF&lt;/code&gt; &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;수동&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 수치는 벤치마크마다 편차가 있다. write-heavy sysbench
oltp_write_only에서 측정한 평균이고, 워크로드가 read-heavy면 오버헤드는
더 낮다 — 쓰기 자체가 드물기 때문. 반대로 대량 bulk insert 지배적이면
NVMe에서도 15% 내외까지 올라간다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;마지막 행이 중요하다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Fusion-io, ScaleFlux, 일부 엔터프라이즈
NVMe는 하드웨어 레벨에서 atomic write(16KB 단위)를 보장&lt;/strong&gt;한다. 이
경우 partial write 자체가 발생하지 않으므로 doublewrite를 꺼도 안전하다.
단 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본값은 ON&lt;/strong&gt;이고, 끄는 건 스토리지 펌웨어를 검증한 뒤
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;명시적으로&lt;/strong&gt; 선택해야 한다. &amp;quot;SSD니까 꺼도 되지 않나&amp;quot;는
일반 소비자 SSD에는 적용되지 않는다.&lt;/p&gt;
&lt;h3 id=&quot;4-3-오해-정리--doublewrite는-무결성-전능이-아니다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-3) 오해
정리 — doublewrite는 무결성 전능이 아니다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;doublewrite는 partial page write만
방어한다.&lt;/strong&gt; 비트 플립, 스토리지 펌웨어 버그, bit rot은 막지
못한다. 이 영역은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_checksum_algorithm=crc32&lt;/code&gt;(기본)의
체크섬 검증, 그리고 스토리지 레이어의 ECC/ZFS/Btrfs 같은 파일시스템
체크섬이 담당한다. doublewrite는 &amp;quot;쓰는 도중 전원이 나가도 페이지가
반쪽짜리로 남지 않는다&amp;quot;는 매우 특정한 보장일 뿐이다.&lt;/p&gt;
&lt;h3 id=&quot;4-4-atomic-write-검증-체크리스트&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-4) atomic write 검증
체크리스트&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;SSD니까 doublewrite 꺼도 되지 않나&amp;quot;는 검증 없이는 자살 행위다.
atomic write를 신뢰하려면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;네 레이어 모두&lt;/strong&gt;가 16KB 단위
원자성을 명시해야 한다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스토리지 펌웨어&lt;/strong&gt; — 제조사 문서에 &amp;quot;atomic write
16KB&amp;quot;가 명시되어 있는가. Fusion-io, ScaleFlux, 일부 엔터프라이즈 NVMe가
여기에 해당한다. 일반 소비자 SSD에는 명시가 없다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파일시스템 alignment&lt;/strong&gt; — xfs/ext4에서 4KB 또는 16KB
block alignment가 InnoDB 페이지 경계와 맞아야 한다. mkfs 시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;-b 4096&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;su=16384&lt;/code&gt; 같은 옵션 점검.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;커널/드라이버 지원&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;nvme-cli id-ns&lt;/code&gt;
출력에서 atomic write unit을 확인. 일부 드라이버는 atomic 보장을 그대로
노출하지 않는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;MySQL 플래그&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_use_fdatasync=ON&lt;/code&gt;(8.0.26+)과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_method=O_DIRECT&lt;/code&gt;의 조합이 의도대로 동작하는지
점검. 둘을 섞을 때 파일시스템 별로 동작이 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;그리고 프로덕션 전에 반드시 두 시나리오를 테스트한다 —
**&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;kill -9 mysqld&lt;/code&gt;**와 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;전원 차단(ipmi power
off)&lt;/strong&gt;. 둘 다 돌린 뒤 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SELECT ... CHECK TABLE&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innochecksum&lt;/code&gt;으로 partial write가 하나도 없는지 검증하고
나서야 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_doublewrite=OFF&lt;/code&gt;를 논의할 자격이 생긴다. 반복
횟수도 중요하다 — 한두 번 돌려서 깨지지 않았다고 안심할 수 없고, 최소
수십 회의 강제 종료 하에서도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innochecksum&lt;/code&gt; 경고가 0이어야
한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;doublewrite로 물리 페이지 무결성을 막았으니, 논리 레벨의 MVCC를
지탱하는 undo로 넘어간다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-undo-tablespace와-purge-thread&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5) undo tablespace와 purge
thread&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;InnoDB의 MVCC는 PG와 같은 원리지만 구현이 다르다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PG가 heap에
dead tuple을 쌓고 VACUUM이 청소하듯, InnoDB는 undo에 이전 이미지를 쌓고
purge thread가 회수한다&lt;/strong&gt;. 차이는 &amp;quot;어디에 쌓느냐&amp;quot;다. PG는 테이블
자체(heap)에 이전 버전과 새 버전을 나란히 두고, InnoDB는 테이블에는 최신
버전만 두고 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;undo log에 이전 이미지&lt;/strong&gt;를 분리해 쌓는다.
그래서 InnoDB 테이블은 bloat가 덜 보이고, 대신 long-running
transaction이 있으면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;undo가 무한히 자란다&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 분리 덕분에 InnoDB는 secondary index에 MVCC 포인터를 직접 심지
않아도 된다. secondary index는 PK만 가리키고, PK row에서 undo 체인을
따라가 올바른 버전을 찾는다. 대가는 secondary index로 조회할 때
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한 번 더 lookup&lt;/strong&gt;이 발생한다는 점이다. 이 비용이 쌓이면
&amp;quot;왜 PK 조회는 빠른데 secondary index 조회는 느린가&amp;quot;라는 질문의 답이
된다.&lt;/p&gt;
&lt;h3 id=&quot;5-1-undo-tablespace--80부터-독립-파일&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-1) undo tablespace —
8.0부터 독립 파일&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;8.0부터 undo는 시스템 테이블스페이스에서 완전히 분리되어
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;undo_001&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;undo_002&lt;/code&gt;라는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;독립 파일 두
개&lt;/strong&gt;로 기본 제공된다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CREATE UNDO TABLESPACE&lt;/code&gt;로
추가도 가능하고, 별도 디스크에 배치할 수도 있다. rollback segment는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_rollback_segments=128&lt;/code&gt;(기본)개로 쪼개져 있어 동시성
경합을 줄인다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;undo 전용 디스크를 분리하는 전략도 선택지다. 쓰기 지배적인
워크로드에서는 undo write가 redo와 함께 가장 무거운 IO 소비자다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ALTER TABLESPACE innodb_undo_001 SET INACTIVE&lt;/code&gt;로 비활성화한
뒤 별도 경로의 undo tablespace를 추가하면 IO 경합을 분산할 수 있다. 작업
중 undo가 모두 차단되지 않도록 최소 3개 이상의 undo tablespace를
유지하는 것이 안전하다.&lt;/p&gt;
&lt;h3 id=&quot;5-2-purge-thread--mvcc-가비지-수집&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-2) purge thread — MVCC
가비지 수집&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_purge_threads=4&lt;/code&gt;(기본)가 undo에 쌓인 구 이미지 중
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어떤 트랜잭션도 더 이상 보지 않는 것&lt;/strong&gt;을 회수한다. &amp;quot;더
이상 보지 않는&amp;quot; 판정은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;현재 살아 있는 가장 오래된 read
view&lt;/strong&gt;의 시작 시점을 기준으로 한다. 이게 핵심이다. 한 세션이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;REPEATABLE READ&lt;/code&gt; 아래서 트랜잭션을 열고 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;몇 시간째
커밋도 롤백도 안 하면&lt;/strong&gt;, purge는 그 세션의 read view 시작 시점
이후 생성된 모든 undo를 회수할 수 없다. undo가 끝없이 자라는 유명한
안티패턴이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;READ COMMITTED&lt;/code&gt; 격리 수준에서는 read view가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;매
문장마다 새로 잡힌다&lt;/strong&gt;. 그래서 RC에서는 long-running
transaction이 있어도 purge 차단이 RR보다 약하다. 이 점도 MySQL 8.0 기본
격리 수준이 RR이라는 사실과 맞물려, &amp;quot;왜 우리 시스템이 undo로
고생하는가&amp;quot;의 간접 답이 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PG의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;VACUUM&lt;/code&gt;이 오래된 트랜잭션에 막히면 dead tuple이 안
정리되는 것과 같은 구조다. 엔진은 달라도 MVCC 청소를 막는 원인은
동일하다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;오래 살아 있는 트랜잭션&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;purge thread 개수를 늘리는 것만으로는 근본 원인이 풀리지 않는다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_purge_threads&lt;/code&gt;를 4에서 32로 올려도, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;purge가
회수할 수 없는 undo&lt;/strong&gt;는 그대로다. purge thread는 오로지 &amp;quot;read
view에 가려지지 않은&amp;quot; undo만 지울 수 있다. long-running transaction
하나가 read view를 잡고 있는 한, 그 이후 생성된 모든 undo는 스레드를
아무리 늘려도 못 지운다.&lt;/p&gt;
&lt;h3 id=&quot;5-3-undo-truncate--80-기본-on&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-3) undo truncate — 8.0 기본
ON&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;8.0부터 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_undo_log_truncate=ON&lt;/code&gt;이 기본이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_max_undo_log_size=1GB&lt;/code&gt;(기본)를 넘은 undo tablespace가
감지되면, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;purge가 끝난 영역을 잘라내는 truncate 작업&lt;/strong&gt;이
비동기로 돈다. 그 사이 해당 undo tablespace는 일시적으로 사용 중단된다.
그래서 기본 undo tablespace가 최소 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;2개&lt;/strong&gt;인 것이다 —
하나가 truncate되는 동안 다른 하나가 계속 쓰기를 받는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;truncate가 자주 발생하면 워크로드가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;정상 purge 속도를
초과&lt;/strong&gt;하는 쓰기 부하를 만들고 있다는 뜻이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_purge_threads&lt;/code&gt;를 기본 4에서 8~16으로 올리는 것이 1차
대응이지만, 더 중요한 진단은 &amp;quot;왜 undo가 자라는가&amp;quot;다. 장기 트랜잭션이
없는데도 undo가 자라면 단순 쓰기량이 문제이므로, 배치 작업의 청크 크기나
인덱스 갱신 패턴을 점검한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;관측창은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SELECT * FROM INFORMATION_SCHEMA.INNODB_TABLESPACES WHERE NAME LIKE &amp;#39;innodb_undo%&amp;#39;&lt;/code&gt;.
undo 파일 크기가 의심스럽게 오래 크게 유지되면,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;information_schema.innodb_trx&lt;/code&gt;에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;trx_started&lt;/code&gt;가 오래된 트랜잭션을 찾는 것이 첫 수사다.&lt;/p&gt;
&lt;h3 id=&quot;5-4-history-list-length-관측&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-4) history list length 관측&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;undo가 자라는 현상을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;수치로 잡는 가장 빠른 지표&lt;/strong&gt;가
history list length다. purge가 아직 회수하지 못한 undo 레코드 개수를
직접 센다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;-- 방법 1: STATUS 텍스트&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;SHOW ENGINE INNODB STATUS;&lt;/span&gt;
&lt;span id=&quot;cb1-3&quot;&gt;&lt;a href=&quot;#cb1-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;-- TRANSACTIONS 섹션에서 &amp;quot;History list length 20&amp;quot; 라인&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-4&quot;&gt;&lt;a href=&quot;#cb1-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-5&quot;&gt;&lt;a href=&quot;#cb1-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;-- 방법 2: metrics 테이블&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-6&quot;&gt;&lt;a href=&quot;#cb1-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;SELECT&lt;/span&gt; NAME, &lt;span class=&quot;fu&quot;&gt;COUNT&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-7&quot;&gt;&lt;a href=&quot;#cb1-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;kw&quot;&gt;FROM&lt;/span&gt; information_schema.innodb_metrics&lt;/span&gt;
&lt;span id=&quot;cb1-8&quot;&gt;&lt;a href=&quot;#cb1-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt; &lt;span class=&quot;kw&quot;&gt;WHERE&lt;/span&gt; NAME&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;#39;trx_rseg_history_len&amp;#39;&lt;/span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;임계치의 감은 이렇다. 평상시 수천~수만 수준은 정상이다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;10만
이상&lt;/strong&gt;이면 purge가 따라오지 못하거나 long-running transaction이
의심되는 단계로, 모니터링 경보를 건다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;100만 이상&lt;/strong&gt;이면
즉시 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;information_schema.innodb_trx&lt;/code&gt;에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;trx_started&lt;/code&gt;가 가장 오래된 세션을 찾아 끊어야 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;흔한 오해 하나 — &amp;quot;락이 없으면 undo도 쌓이지 않겠지&amp;quot;는 틀렸다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;read view가 열린 동안에는 metadata lock이나 row lock이 없어도
undo가 purge되지 않는다&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SELECT&lt;/code&gt;만 치고 커밋하지
않는 세션, BI 툴이 장시간 결과를 붙잡고 있는 세션이 그 주범이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_trx&lt;/code&gt;에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;trx_state=&amp;#39;RUNNING&amp;#39;&lt;/code&gt;인데
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;trx_rows_locked=0&lt;/code&gt;인 세션이 있다면 바로 이 패턴이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 발 더 나간 수사가 필요하면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;performance_schema.events_transactions_current&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;events_statements_history&lt;/code&gt;를 조인해 &amp;quot;언제 어떤 쿼리로 시작된
트랜잭션인지&amp;quot;까지 역추적한다. 커넥션 풀의 idle 세션이 무심코 트랜잭션을
열어 둔 채 돌아오지 않는 경우가 가장 흔한 범인이고, 앱 레이어의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autocommit=false&lt;/code&gt; + 예외 처리 누락 조합이 원인일 때가 많다.
DB 튜닝이 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;앱 코드를 고쳐야 해결&lt;/strong&gt;되는 문제인
경우가 대부분이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-binlog와-2pc-group-commit-2층&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6) binlog와 2PC: group commit
2층&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;binlog는 MySQL 서버 레이어의 로그다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;엔진 중립
포맷&lt;/strong&gt;이라 복제·PITR(point-in-time recovery)·감사에 쓰인다.
InnoDB redo가 엔진 내부 복구용이라면, binlog는 외부 소비자(replica, CDC,
백업)를 위한 공개 로그다. &lt;a href=&quot;#1-3-binlog가-서버-레이어에-있는-이유&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§1-3 binlog가 서버 레이어에
있는 이유&lt;/a&gt;에서 말한 대로 이 분리 때문에 2PC가 필요하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 번 더 짚으면 — redo는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;물리 로그에 가깝다&lt;/strong&gt;. 어떤
페이지의 몇 바이트 오프셋에서 무엇이 바뀌었다는 기록이다. binlog는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;논리 로그&lt;/strong&gt;다. 어떤 테이블의 어떤 행이 어떤 값으로
바뀌었다는 기록이다. 물리 로그는 해당 엔진·해당 버전에서만 재생
가능하고, 논리 로그는 엔진과 버전을 넘어 재생 가능하다. 두 로그의 성질
차이가 2PC의 필요성을 만든다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;db-deepdive-02-mysql-innodb-architecture-02&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;db-deepdive-02-mysql-innodb-architecture-02&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;6-1-2pc-순서--prepare-binlog-commit&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-1) 2PC 순서 — prepare,
binlog, commit&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;내부 2PC의 순서가 정해져 있다.&lt;/p&gt;
&lt;ol type=&quot;1&quot; style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;InnoDB prepare&lt;/strong&gt; — redo에 prepare 레코드 write +
fsync. 이 시점에 트랜잭션은 &amp;quot;커밋 가능&amp;quot; 상태.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;binlog write + fsync&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sync_binlog=1&lt;/code&gt;(기본)이면 여기서 fsync. binlog에 들어간
순간이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;복제 순서&lt;/strong&gt;의 기준.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;InnoDB commit&lt;/strong&gt; — redo에 commit 레코드 추가. 메모리
상태 전환. (이 단계는 별도 fsync 안 함)&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;recovery 시 판정 규칙은 이렇다. redo에 prepare는 있는데 commit은 없고
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;binlog에 있다&lt;/strong&gt;면 → 커밋 재생. redo에 prepare는 있는데
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;binlog에 없다&lt;/strong&gt;면 → 롤백. 이 판정 덕분에 redo와 binlog의
&amp;quot;같은 트랜잭션 집합&amp;quot;이 crash 이후에도 일치한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;commit 레코드의 지속성 경로를 한 번 더 짚어 둔다. prepare 단계에서
이미 redo에 prepare 레코드가 기록되어 fsync까지 내려가 있고, commit
레코드는 다음 redo flush 주기에 같이 나간다. crash 시 binlog에 해당
트랜잭션이 있으면 redo commit을 재주입해 복구한다. 즉 3단계의 마지막
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;InnoDB commit&lt;/code&gt;이 별도 fsync를 치지 않아도 안전한 이유는,
&amp;quot;prepare 레코드의 fsync + binlog의 fsync + 판정 규칙&amp;quot;이 지속성과
원자성을 이미 확보했기 때문이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 순서를 거꾸로 놓고 &amp;quot;왜 binlog fsync가 redo commit fsync보다
먼저인가&amp;quot;를 묻는 사람이 있다. 답은 단순하다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;binlog가 복제의
기준점&lt;/strong&gt;이기 때문이다. binlog에 들어가지 않은 트랜잭션은 존재하지
않은 것과 같고, binlog에 들어간 트랜잭션은 반드시 primary와 replica
모두에서 커밋 상태로 수렴해야 한다. 그래서 binlog fsync가 진실의 기준이
되고, redo commit은 그 뒤를 따른다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 가지 더 — 이 2PC는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;내부 XA&lt;/strong&gt;다. 외부에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;XA START&lt;/code&gt;로 시작하는 분산 트랜잭션과는 별개이고, 서버가
클라이언트에게 노출하지 않는다. MySQL 내부에서 엔진 레이어와 서버
레이어(binlog)를 조율하는 용도로만 쓰이고, 복구 알고리즘도 내부용이다.
외부 XA는 별도의 오버헤드와 제약이 있으므로 혼동하지 말 것.&lt;/p&gt;
&lt;h3 id=&quot;6-2-group-commit-2층--flushsynccommit-단계&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-2) group commit
2층 — FLUSH/SYNC/COMMIT 단계&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기가 MySQL 복제 튜닝의 핵심이다. 2PC 안에서 binlog write는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;3단계 파이프라인&lt;/strong&gt;으로 돈다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;FLUSH stage&lt;/strong&gt; — 커밋하려는 트랜잭션들을 큐에 모아 한
리더가 binlog 버퍼에 일괄 기록.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SYNC stage&lt;/strong&gt; — 리더가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;fsync 1회&lt;/strong&gt;로
큐에 묶인 N개 트랜잭션의 binlog를 한꺼번에 디스크에 내린다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;COMMIT stage&lt;/strong&gt; — 리더가 엔진 commit을 순서대로
호출.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이게 &amp;quot;group commit 2층&amp;quot;이다. 1층(redo)과 2층(binlog)이 각각 group
commit을 돌려서 fsync 비용을 묶는다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sync_binlog=1&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_log_at_trx_commit=1&lt;/code&gt;(둘 다 기본값)이 &amp;quot;커밋마다
fsync 2번&amp;quot;처럼 보이지만 실제로는 고동시 OLTP에서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;fsync당
트랜잭션 수가 수십~수백 개&lt;/strong&gt;로 묶이므로, 평균 commit 레이턴시는
단일 fsync 수준에 머무른다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;묶음 크기는 두 변수로 조절한다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_group_commit_sync_delay&lt;/code&gt;(μs)와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_group_commit_sync_no_delay_count&lt;/code&gt;. 전자는 &amp;quot;SYNC
stage에 진입하기 전 몇 마이크로초를 기다려 더 모을까&amp;quot;를, 후자는 &amp;quot;이
개수를 채우면 delay를 기다리지 않고 즉시 묶어 낸다&amp;quot;를 의미한다. 둘 다
기본 0이고 대부분의 환경에서 기본 그대로가 낫다. 평균 커밋 레이턴시를 몇
μs 희생해서 fsync 횟수를 극단적으로 줄이고 싶은 쓰기 폭주 환경에서만
건드린다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;관측창은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;performance_schema.events_stages_history_long&lt;/code&gt;에서 stage별
누적 시간을 보는 것이다. SYNC stage의 평균 시간이 1ms를 넘는다면
스토리지의 fsync 레이턴시 자체가 문제이거나, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sync_binlog&lt;/code&gt;를
1보다 크게 잡아 여러 트랜잭션을 누적한 뒤 fsync하도록 전환하는 옵션이
있다 (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sync_binlog=N&lt;/code&gt;은 N개 커밋마다 한 번만 fsync). 금융권
외에는 100이나 1000이 현실적인 선택이 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;다시 강조 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;group commit의 효과는 동시성 조건부&lt;/strong&gt;다.
직렬 배치 스크립트 하나가 5천만 행을 커밋별로 insert하는 경우에는 2층
모두에서 묶을 상대가 없고, 매 커밋이 fsync 2회를 그대로 친다. 이 패턴을
만나면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autocommit=0&lt;/code&gt;으로 묶어서 한 트랜잭션으로 commit
횟수를 줄이거나,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_log_at_trx_commit=2&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sync_binlog=0&lt;/code&gt;으로
내려가는 선택이 현실이다.&lt;/p&gt;
&lt;h3 id=&quot;6-3-binlog-사이징과-rotation&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-3) binlog 사이징과 rotation&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;binlog는 지워주지 않으면 디스크를 끊임없이 먹는다. 사이징과 rotation
정책을 기본값 그대로 두면 디스크 가득참 장애가 예정된 순서로
찾아온다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파라미터&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;기본값&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_binlog_size&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1GB&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;파일 1개 최대 크기. 초과 시 새 파일로 rotate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_expire_logs_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2592000 (30일)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;만료 시간. 지나면 자동 purge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_cache_size&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;32KB&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;트랜잭션별 in-memory binlog 버퍼&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;rotation은 두 조건 중 먼저 도달하는 쪽에서 발생한다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;크기
초과&lt;/strong&gt; 또는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FLUSH BINARY LOGS&lt;/code&gt; 같은 명시
호출&lt;/strong&gt;. 파일 하나가 가득 차면 번호가 붙은 다음 파일로 끊어진다
(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mysql-bin.000123&lt;/code&gt; → &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mysql-bin.000124&lt;/code&gt;).&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_cache_size&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;트랜잭션이 커밋 전까지
binlog 이벤트를 들고 있는 메모리 버퍼&lt;/strong&gt;다. 이 크기를 넘어서는 큰
트랜잭션은 tmpfs·디스크로 swap된다
(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_stmt_cache_disk_use&lt;/code&gt; 카운터로 관측). 대량
INSERT·대용량 DML이 자주 돌면 4MB~16MB로 키우는 것이 현실적이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;만료 정책은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PITR(point-in-time recovery)의 소급
범위&lt;/strong&gt;를 그대로 결정한다. 너무 짧게 잡으면 30일 전 장애를 되돌릴
수 없고, 너무 길게 잡으면 디스크를 과하게 차지한다. 조직의 RTO/RPO와
백업 주기에 맞춰 7일~30일 범위에서 정하는 것이 보통이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;replica가 살아 있는지도 고려해야 한다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;replica가 아직 읽지
않은 binlog 파일은 expire 대상에서 제외&lt;/strong&gt;되지만, GTID 기반
복제에서 replica가 오랫동안 끊겨 있다가 다시 붙으려 할 때 필요한
binlog가 이미 만료되어 있으면 복제를 재구성해야 한다. 장애 복구 절차
설계 시 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_expire_logs_seconds&lt;/code&gt;와 replica의 최장 허용
중단 시간이 함께 고려되어야 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;운영 체크리스트 한 줄 — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SHOW BINARY LOGS&lt;/code&gt;로 현재 파일
수와 총 크기를 주기적으로 관측하고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Binlog_cache_disk_use&lt;/code&gt;
카운터가 증가하는지 확인한다. 이 카운터가 꾸준히 오르면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_cache_size&lt;/code&gt;가 작다는 직접 증거이므로 4MB~16MB 범위로
올린다.&lt;/p&gt;
&lt;h3 id=&quot;6-4-binlog_format--row가-기본&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-4) binlog_format — ROW가
기본&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;binlog는 세 포맷이 있다. MySQL 5.7.7+ 기본이 ROW이고, 8.0.34+부터
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_format&lt;/code&gt; 변경 자체가
deprecated&lt;/strong&gt;되었다. 사실상 ROW만 쓴다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;Row-based logging is the default method.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/replication-formats.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;MySQL
8.0 Reference Manual §19.2.1 Replication Formats&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;포맷&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;기록 대상&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;장점&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;단점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ROW&lt;/strong&gt; (기본)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;변경된 행 이미지&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;비결정 함수 안전, 정확&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;binlog 크기 ↑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;STATEMENT&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;실행된 SQL&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;binlog 작음&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;NOW()&lt;/code&gt;·AUTO_INCREMENT 등 비결정 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;MIXED&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;상황에 따라 자동 선택&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;절충&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;8.0.34+ deprecated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;비결정 함수 하나만 봐도 ROW의 선택이 정당해진다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UPDATE t SET ts = NOW() WHERE ...&lt;/code&gt;를 STATEMENT로 찍으면
replica가 재생할 때 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;NOW()&lt;/code&gt;가 다른 시각으로 평가되어 데이터가
어긋난다. ROW는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;변경 후 행 이미지 자체&lt;/strong&gt;를 기록하므로
재생이 결정적이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;ROW의 트레이드오프는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;binlog 용량&lt;/strong&gt;이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UPDATE&lt;/code&gt; 한 줄이 100만 행을 고치면 STATEMENT는 SQL 한
줄이지만 ROW는 100만 행의 before/after 이미지가 모두 들어간다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_row_image=minimal&lt;/code&gt;(기본은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;full&lt;/code&gt;)을 쓰면
before는 PK만, after는 변경 컬럼만 기록해 용량을 크게 줄일 수 있지만,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CDC 소비자(Debezium 등)가 before full image를 요구하는&lt;/strong&gt;
경우가 많아 운영에서는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;full&lt;/code&gt; 또는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;noblob&lt;/code&gt; 조합이
현실적이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;압축 옵션도 있다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_transaction_compression=ON&lt;/code&gt;(8.0.20+)을 켜면 트랜잭션
단위로 ZSTD 압축이 적용되어 binlog 크기가 체감 30~60% 줄어든다. 네트워크
복제 대역폭과 디스크 모두에서 이득을 보는데, 대신 replica 쪽 CPU가 압축
해제로 약간 올라간다. 일반 OLTP에서는 켜 두는 것이 거의 항상
이득이다.&lt;/p&gt;
&lt;h3 id=&quot;6-5-병렬-복제--writeset--preserve_commit_order&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-5) 병렬 복제 —
WRITESET + preserve_commit_order&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;binlog 이야기의 마지막 축은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;replica의 병렬 적용&lt;/strong&gt;이다.
기본 replica는 binlog를 단일 스레드로 재생한다. 병렬화 없이는
write-heavy primary를 replica가 못 쫓아간다. 8.0에서 두 다이얼이 궁합을
이룬다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_transaction_dependency_tracking=WRITESET&lt;/code&gt; —
primary가 binlog에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;각 트랜잭션이 건드린 행 키 해시&lt;/strong&gt;를
함께 기록한다. replica는 WRITESET이 겹치지 않는 트랜잭션끼리
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;동시 재생&lt;/strong&gt;한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;replica_preserve_commit_order=ON&lt;/code&gt; — 동시 재생하더라도
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;커밋 순서는 원본과 동일&lt;/strong&gt;하도록 강제한다.
read-your-writes 보장의 전제.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;둘을 같이 켜야 한다. WRITESET만 켜면 순서가 뒤틀릴 수 있고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;preserve_commit_order&lt;/code&gt;만 켜면 병렬성이 안 나온다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;replica_preserve_commit_order&lt;/code&gt;는 8.0.26까지 기본 OFF였고,
8.0.27부터 기본 ON으로 전환됐다. 구체 기본값은 운영 전 버전별로 확인해야
한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;병렬 복제 적용 스레드 수는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;replica_parallel_workers=4&lt;/code&gt;(기본)로 잡혀 있다. write-heavy
primary를 쫓아가는 replica라면 8~32 범위로 올린다. 스레드를 늘린 만큼
비례해 성능이 오르지는 않고, WRITESET 충돌 빈도에 따라 체감 상승분이
갈린다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;performance_schema.replication_applier_status_by_worker&lt;/code&gt;에서
각 worker의 처리량을 비교하면 병렬성이 실제로 살아 있는지 관측할 수
있다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;연결 수가 수천 단위라면 OSS MySQL 단독으로 버티려 하지
말자&lt;/strong&gt;. Community는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pool-of-threads&lt;/code&gt;가 없다.
ProxySQL이나 앱 레이어 커넥션 풀러를 앞단에 두고, 그게 안 되면
Percona/MariaDB로 전환하는 게 현실적이다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;버퍼 풀은 크기보다 instance 수를 먼저 확인&lt;/strong&gt;한다. 총
크기가 1GB 이상이면 자동 8 분할이지만, 수십 GB 규모에서는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_buffer_pool_instances=16&lt;/code&gt;이나 32를 명시한다. 각
instance가 1GB 미만이 되면 안 된다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SSD/NVMe에 올라간 MySQL은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_change_buffering=&amp;#39;none&amp;#39;&lt;/code&gt;을 한번 검토&lt;/strong&gt;한다.
merge 시점의 p99 스파이크가 기본값에서 자주 잡히면 끄는 편이 체감
안정성이 높다. 단, 쓰기 IOPS는 늘어난다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;일반 OLTP에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_flush_log_at_trx_commit=2&lt;/code&gt;는 충분히 합리적인
선택&lt;/strong&gt;이다. &amp;quot;OS crash만 1초 손실&amp;quot;이라는 의미를 조직이 이해하고
있다면. 금융·결제는 1 고정.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;doublewrite는 일반적으로 끄지 않는다&lt;/strong&gt;. 엔터프라이즈
NVMe의 atomic write를 펌웨어·드라이버·파일시스템 단에서 검증한 경우에
한해 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_doublewrite=OFF&lt;/code&gt;를 논의한다. &amp;quot;SSD니까 꺼도
되겠지&amp;quot;는 안 된다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;undo가 자란다면 먼저 long-running transaction을
찾는다&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SELECT trx_started FROM information_schema.innodb_trx ORDER BY trx_started LIMIT 5&lt;/code&gt;
한 줄이 대부분의 범인을 잡는다. purge thread를 올리기 전에 원인
트랜잭션을 끊는 게 먼저다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;복제 지연이 심한데 CPU는 놀고 있다면 WRITESET 병렬
복제&lt;/strong&gt;를 확인한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_transaction_dependency_tracking=WRITESET&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;replica_preserve_commit_order=ON&lt;/code&gt; 조합이 켜져 있는지
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SHOW REPLICA STATUS&lt;/code&gt;와 변수로 검증.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;group commit 효과는 동시성이 있을 때만 체감&lt;/strong&gt;된다.
직렬 배치 성능이 나쁘다고 redo/binlog를 원망하기 전에, 트랜잭션 경계부터
다시 본다. 100만 건을 1만 행씩 묶는 것만으로 fsync 수가 100배 준다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;dirty ratio 알람은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Modified db pages / buffer pool pages&lt;/code&gt;로 계산&lt;/strong&gt;해
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_max_dirty_pages_pct_lwm&lt;/code&gt;(기본 10%)를 넘는 시점에
경보를 건다. 그 위로 장시간 머무르면 page cleaner 대비 쓰기 부하가
과다하다는 의미다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_io_capacity&lt;/code&gt;를 먼저 의심한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;**redo capacity가 너무 작은 증상은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Innodb_buffer_pool_wait_free &amp;gt; 0&lt;/code&gt;**이다. 이 카운터가 0이
아닌 구간이 있다면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;innodb_redo_log_capacity&lt;/code&gt;를 2배로 올리는
것이 거의 항상 즉효다. 8.0.30+는 런타임 변경이라 다운타임이 없다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;binlog 디스크 가득참 장애는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_expire_logs_seconds&lt;/code&gt;를 먼저 본다&lt;/strong&gt;. 기본
30일이 무난하지만, write-heavy 샤드에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_binlog_size=1GB&lt;/code&gt;
× 30일이면 수백 GB가 깔린다. 백업·PITR 범위와 맞춰 단축하거나, binlog
파티션을 별도 디스크로 분리한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;history list length 10만은 경보, 100만은 긴급&lt;/strong&gt;이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;trx_rseg_history_len&lt;/code&gt; 메트릭을 모니터링에 걸고, 임계 돌파 시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;information_schema.innodb_trx&lt;/code&gt; 덤프를 자동 수집해 둔다.
사후에 &amp;quot;누가 열어뒀냐&amp;quot;를 찾는 것보다 10배 빠르다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ROW 기반 binlog에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_row_image&lt;/code&gt;를
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;minimal&lt;/code&gt;로 바꾸는 변경은 CDC 계약과 맞물린다&lt;/strong&gt;.
Debezium·Maxwell 같은 소비자가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;full&lt;/code&gt; 이미지를 요구하면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;minimal&lt;/code&gt;로 내리지 말 것. 용량 이슈는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;binlog_rotate_immediate&lt;/code&gt; 등 다른 축으로 푼다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8) 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;MySQL InnoDB는 서버 레이어와 스토리지 엔진을
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;handlerton&lt;/code&gt; 계약으로 나누고, 버퍼 풀 3층과
redo·undo·doublewrite·binlog 네 축으로 지속성을 만든다&lt;/strong&gt;. 각
서브시스템은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;독립적으로 튜닝&lt;/strong&gt;되며 기본값은 안전
우선이다. group commit 2층과 2PC 덕분에 고동시 OLTP에서도 fsync 비용이
묶여 처리량이 유지된다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;다음 편: Oracle의 SGA·UNDO 구조로 넘어가 세 DB의 메모리 레이아웃을
머릿속에 완성한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;태그&lt;/strong&gt;: mysql, innodb, buffer-pool, redo-log,
undo-log, binlog, mvcc, doublewrite, group-commit, handlerton&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/Database</category>
      <category>Binlog</category>
      <category>buffer-pool</category>
      <category>doublewrite</category>
      <category>group-commit</category>
      <category>handlerton</category>
      <category>innodb</category>
      <category>MVCC</category>
      <category>mysql</category>
      <category>redo-log</category>
      <category>undo-log</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/411</guid>
      <comments>https://dding-shark.tistory.com/411#entry411comment</comments>
      <pubDate>Wed, 15 Apr 2026 14:48:32 +0900</pubDate>
    </item>
    <item>
      <title>gRPC Deadline &amp;middot; Cancellation &amp;middot; Context Deep Dive &amp;mdash; 5쌍의 레이어가 동시에 서야 한다</title>
      <link>https://dding-shark.tistory.com/410</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;grpc-deadline--cancellation--context-deep-dive--5쌍의-레이어가-동시에-서야-한다&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;gRPC
Deadline · Cancellation · Context Deep Dive — 5쌍의 레이어가 동시에 서야
한다&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;3초 SLA&amp;quot;라고 써놓고 실제 호출은 9초 뒤에 503으로 돌아온다.
BANDER에서 이 현상을 처음 봤을 때, 타임아웃 설정을 잘못 읽은 줄 알고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;을 뒤졌다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc.client.deadline: 5s&lt;/code&gt;가 멀쩡히 있었다. 스타트업 로그에도
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;deadline=PT5S&lt;/code&gt;가 찍혀 있었다. 그런데 서버 쪽
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ServerInterceptor&lt;/code&gt;에서 꺼내본 호출의 Deadline은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;null&lt;/code&gt;이었고, HTTP/2 HEADERS 프레임을 Wireshark로 덤프해보니
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더 자체가 존재하지 않았다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;선언은
있는데 wire에는 아무것도 나가지 않은 상태&lt;/strong&gt;, 전형적인 dead
config였다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;원인을 추적해보니 한 줄짜리 누락이었다. 채널을 조립하는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcChannelFactory&lt;/code&gt; 코드가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;properties.getDeadline()&lt;/code&gt;을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한 번도 읽지
않았다&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ManagedChannelBuilder&lt;/code&gt;에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;intercept(...)&lt;/code&gt;를 건 적도 없었다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration&lt;/code&gt;
필드만 스프링 프로퍼티에 선언해 두면 알아서 gRPC 호출에 붙을 거라는
암묵적 가정이 틀렸다. gRPC-Java는 그런 자동 배선을 전혀 해주지 않는다.
인터셉터를 직접 등록해야 비로소 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CallOptions&lt;/code&gt;에 Deadline이
얹히고, 그제야 wire에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더가 실린다. 이 한
줄이 빠져 있으면 그 뒤에 있는 모든 정책 — retry, hedging, 서킷 브레이커,
분산 트레이싱 — 이 전부 같은 시각을 공유하지 못한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 그 사고를 해부한다. Deadline은 단일 설정 값이 아니라
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;다섯 개의 레이어가 동시에 서야&lt;/strong&gt; 동작하는 협업이다.
인터셉터 등록이 1번, 서버 로직의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled()&lt;/code&gt; 폴링이 2번,
비동기 경계에서의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.wrap()&lt;/code&gt;이 3번, retry 예산의 남은
시간 재산정이 4번, 서킷 브레이커와 Deadline의 시간 축 정합이 5번이다.
하나라도 빠지면 나머지 넷이 얼마나 정교하게 서 있어도 SLA는 깨진다. 이
글은 다섯 레이어를 한 장에 모은 뒤, 각 레이어를 한 섹션씩 확대해서
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;왜 · 구조 · 동작 · 함정 · 실무&lt;/strong&gt; 순으로 풀어낸다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 시리즈 4편을 읽다가 거의 다 같은 지점에서 막힌다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Deadline은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration&lt;/code&gt; 선언만으로는 전파되지
않는다&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientInterceptor&lt;/code&gt;가 짝으로 등록돼야
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더가 wire에 실린다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;헤더가 전달돼도 서버는 자동으로 멈추지 않는다&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current().isCancelled()&lt;/code&gt;를 직접 폴링하거나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;addListener&lt;/code&gt;로 훅을 걸어야 한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CompletableFuture.supplyAsync()&lt;/code&gt;에 gRPC 호출을
넘기면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context&lt;/code&gt;가 증발한다&lt;/strong&gt; — 디폴트
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ForkJoinPool&lt;/code&gt; 스레드에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current()&lt;/code&gt;는
ROOT로 돌아온다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;를 retryable로 두면 예산이
배수로 늘어난다&lt;/strong&gt; — retry budget을 안 쓰면 장애가 전파
증폭된다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Hedging은 이중 호출이다&lt;/strong&gt; — 백엔드 부하를 2~3배로
밀어넣는다는 계산을 안 하면 retry만으로 서비스가 죽는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;불변식 5쌍 → Deadline vs Timeout → 전달 ≠ 강제 →
Context → 비동기 경계 → 서버 체크 → 시계 비대칭 → Retry/Hedging → 3-hop
전파 → 실무&lt;/strong&gt; 순으로 정리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#0-불변식-5쌍-지도-deadline이-서려면-동시에-서야-하는-것들&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;0)
불변식 5쌍 지도: Deadline이 서려면 동시에 서야 하는 것들&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-deadline-vs-timeout-절대-시각과-상대-시간의-차이&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1)
Deadline vs Timeout: 절대 시각과 상대 시간의 차이&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-헤더-전달은-강제가-아니다-bander-dead-config-실증&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2)
헤더 전달은 강제가 아니다: BANDER dead config 실증&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-iogrpccontext-parent-child와-attachdetach&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3)
io.grpc.Context: parent-child와 attach/detach&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-비동기-경계-3종-context가-증발하는-지점&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4) 비동기 경계
3종: Context가 증발하는 지점&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-서버는-자동으로-멈추지-않는다-iscancelled와-listener&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5)
서버는 자동으로 멈추지 않는다: isCancelled와 listener&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-분산-시계-비대칭-deadline_exceeded-vs-cancelled&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6) 분산
시계 비대칭: DEADLINE_EXCEEDED vs CANCELLED&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-retryhedgingcircuit-breaker의-정합성&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7)
Retry·Hedging·Circuit Breaker의 정합성&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-3-hop-예산-전파-시퀀스-client--gateway--service-a--service-b&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8)
3-hop 예산 전파 시퀀스: Client → Gateway → Service A → Service
B&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#9-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;9) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#10-한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;10) 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#부록-a-운영-체크리스트&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;부록 A) 운영 체크리스트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;0-불변식-5쌍-지도-deadline이-서려면-동시에-서야-하는-것들&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;0)
불변식 5쌍 지도: Deadline이 서려면 동시에 서야 하는 것들&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Deadline 하나가 SLA를 지킨다는 말은 반은 맞고 반은 틀리다. 정확히는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;다섯 개의 독립된 레이어가 같은 시각을 공유&lt;/strong&gt;해야 한다.
하나라도 다른 시각을 보고 있거나 아예 시각 자체가 비어 있으면, 전체
호출은 선언한 Deadline 안에 끊기지 않는다. BANDER 사고가 정확히 그
상태였다 — 1번 레이어가 비어 있었고, 2~5번이 아무리 잘 짜여 있어도 1번이
없으니 전부 의미를 잃었다. 이 글의 나머지 섹션은 이 표의 한 행씩을
확대한 것이다. 읽다가 맥락을 놓치면 이 표로 돌아오면 된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;레이어&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;무엇을 하는가&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;빠지면 생기는 증상&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;상세 섹션&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1. 인터셉터 등록&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientInterceptor&lt;/code&gt;로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더를
wire에 붙인다&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;선언된 Deadline이 wire에 실리지 않음 → 호출이 끊기지 않고 흐름&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;a href=&quot;#2-헤더-전달은-강제가-아니다-bander-dead-config-실증&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§2&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2. 서버 isCancelled 폴링&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;서버 로직이 주기적으로 취소 신호를 감지한다&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;클라가 끊어도 서버는 쿼리를 끝까지 실행 → DB·메모리 리소스 누수&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;a href=&quot;#5-서버는-자동으로-멈추지-않는다-iscancelled와-listener&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§5&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;3. Context.wrap&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;비동기 스레드로 작업을 넘길 때 Context를 같이 싸서 넘긴다&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;하위 gRPC 호출이 ROOT Context로 흘러 Deadline 상속 실패&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;a href=&quot;#4-비동기-경계-3종-context가-증발하는-지점&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§4&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;4. Retry budget 재산정&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;남은 시간을 기준으로 재시도 횟수·backoff를 결정한다&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;를 다시 시도해 예산이 배수로 증폭&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;a href=&quot;#7-retryhedgingcircuit-breaker의-정합성&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;5. Circuit breaker 정합&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;브레이커 상태와 Deadline이 동일한 시간 축을 본다&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Open 구간에서도 Deadline이 깎여 Half-open 시점에 새 호출이 즉시
만료&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;a href=&quot;#7-retryhedgingcircuit-breaker의-정합성&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§7&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 다섯 레이어에는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;순서가 있다&lt;/strong&gt;. 1번이 서 있지 않으면
나머지는 논의할 필요가 없다. 2~5번은 1번이 동작한다는 가정 위에서만
의미를 가진다. 장애 분석을 이 순서대로 — &amp;quot;1번부터 확인, 서 있으면 2번,
그 다음 3번&amp;quot; — 내려가는 게 가장 빠르다. 5쌍이 한 번에 다 무너진 상황은
거의 없다. 대부분의 사고는 정확히 한 레이어가 비어 있고, 나머지는 잘 서
있지만 그 한 레이어 때문에 전부가 움직이지 않는 구조다. 이 글을 &amp;quot;불변식
지도 + 각 레이어별 해부&amp;quot;로 읽으면 된다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-deadline-vs-timeout-절대-시각과-상대-시간의-차이&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1) Deadline
vs Timeout: 절대 시각과 상대 시간의 차이&lt;/h2&gt;
&lt;h3 id=&quot;1-1-왜-grpc는-timeout이-아니라-deadline이라는-단어를-쓰는가&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-1) 왜
gRPC는 Timeout이 아니라 Deadline이라는 단어를 쓰는가&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이름이 다른 건 개념이 다르기 때문이다. HTTP 클라이언트는 대부분 &amp;quot;읽기
타임아웃 5초&amp;quot;처럼 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;상대 시간&lt;/strong&gt;을 쓴다. 호출이 시작된 시각
+ 5초가 각 호출마다 새로 계산된다. 한 호출에서만 보면 무해하지만, RPC
체인에 들어가면 이 모델이 시간을 몇 배로 늘린다. 서비스 A가 B를 5초
타임아웃으로 호출하고, B가 다시 C를 5초 타임아웃으로 호출하면,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;홉마다 타임아웃이 재시작&lt;/strong&gt;되어 전체 최악 지연이 홉 수만큼
곱해진다. 3-hop 체인이면 최악 15초, 4-hop이면 20초다. 사용자 관점의
SLA는 &amp;quot;5초&amp;quot;라고 생각했는데 실제 최악은 4배가 넘는 식이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;gRPC는 이 문제를 **절대 시각(Deadline)**으로 푼다.
&amp;quot;2026-04-15T12:34:56.123Z까지 끝나야 한다&amp;quot;는 단일 시점을 체인 전체가
공유한다. 클라이언트가 최초 호출 시점에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;withDeadlineAfter(3, TimeUnit.SECONDS)&lt;/code&gt;를 부르면,
gRPC-Java는 그 시각 + 3초를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;절대 타임스탬프로 고정&lt;/strong&gt;해서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CallOptions&lt;/code&gt;에 심는다. 호출이 네트워크로 나갈 때 이 절대
시각은 &amp;quot;현재 시각 대비 남은 밀리초&amp;quot;로 환산돼 HTTP/2 HEADERS 프레임의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더에 실린다. 수신 서버는 자신의 시계로
&amp;quot;이제부터 남은 N ms&amp;quot;를 더해 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;새 절대 시각&lt;/strong&gt;을 자기
Context에 다시 심는다. 이렇게 각 홉은 &amp;quot;한 번 더 당겨진 공통 마감&amp;quot;을
공유하게 되고, 최악 지연은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;홉 수에 곱해지지 않고&lt;/strong&gt; 초기
Deadline에 수렴한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 차이가 실무에서 느껴지는 지점은 두 곳이다. 첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;사용자
체감 SLA와 내부 호출 체인 예산이 동일한 단위로 측정&lt;/strong&gt;된다.
3초라고 써놓으면 정말로 3초다. 둘째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한 홉이 느려지면 그 만큼 뒤
홉 예산이 줄어든다&lt;/strong&gt;. 앞 홉이 2초를 써버리면 뒤 홉은 1초 안에
끝내야 한다. 이게 처음에는 불편해 보이지만, &amp;quot;전체 예산을 공유한다&amp;quot;는
계약이 있어야 체인 끝단이 과부하에 걸렸을 때도 클라이언트가 약속된
시각에 결과를 받는다.&lt;/p&gt;
&lt;h3 id=&quot;1-2-grpc-timeout-헤더-bnf와-8자리-제약&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-2) grpc-timeout 헤더
BNF와 8자리 제약&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더의 문법은 gRPC wire 스펙에 BNF로 정의돼
있다. 짧지만 두 가지 함정이 들어 있다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Timeout          -&amp;gt; &amp;quot;grpc-timeout&amp;quot; TimeoutValue TimeoutUnit
TimeoutValue     -&amp;gt; {positive integer as ASCII string of at most 8 digits}
TimeoutUnit      -&amp;gt; Hour / Minute / Second / Millisecond / Microsecond / Nanosecond
Hour             -&amp;gt; &amp;quot;H&amp;quot;
Minute           -&amp;gt; &amp;quot;M&amp;quot;
Second           -&amp;gt; &amp;quot;S&amp;quot;
Millisecond      -&amp;gt; &amp;quot;m&amp;quot;
Microsecond      -&amp;gt; &amp;quot;u&amp;quot;
Nanosecond       -&amp;gt; &amp;quot;n&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;첫 번째 함정은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;값이 최대 8자리&lt;/strong&gt;라는 제한이다.
99,999,999까지만 실을 수 있다. 단위가 초(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;S&lt;/code&gt;)면 약 3.17년까지
가능하므로 실무에서는 문제 될 일이 없지만, 나노(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;n&lt;/code&gt;)나
마이크로(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;u&lt;/code&gt;) 단위로 큰 Deadline을 실으려고 하면 값이 잘린다.
gRPC-Java 스택은 내부적으로 단위를 자동 선택해 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;8자리 안에
들어오는 가장 정밀한 단위&lt;/strong&gt;로 직렬화한다. 3초는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;3S&lt;/code&gt;가
아니라 보통 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;3000000u&lt;/code&gt;(마이크로초)나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;3000m&lt;/code&gt;(밀리초)로 나간다. 이 동작은 구현 최적화지 스펙 위반이
아니다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;두 번째 함정은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;단위가 대소문자에 민감&lt;/strong&gt;하다는 것이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;S&lt;/code&gt;는 초, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;m&lt;/code&gt;은 밀리초, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;M&lt;/code&gt;은 분이다.
수작업으로 헤더를 만드는 일은 거의 없지만, 프록시 레이어에서 문자열로
파싱하고 재조립할 때 대소문자를 섞으면 Deadline이 60배로 늘거나 1000배로
줄어든다. Envoy나 nginx에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt;을 건드리는 설정을
쓸 일이 있으면 이 지점을 반드시 테스트해야 한다. 공식 정의는 &lt;a href=&quot;https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;gRPC
PROTOCOL-HTTP2 Requests&lt;/a&gt; 섹션에 있다.&lt;/p&gt;
&lt;h3 id=&quot;1-3-3-hop-rest-vs-3-hop-grpc-최악-지연&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-3) 3-hop REST vs 3-hop
gRPC 최악 지연&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;비교가 가장 선명한 지점은 표 하나다. 상대 타임아웃과 절대 Deadline이
체인에서 어떻게 다르게 누적되는지를 한 장에 보여준다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;구조&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;각 홉 설정&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;최악 지연&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;REST 3-hop, 상대 타임아웃&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;3s / 3s / 3s&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;최대 9s&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;홉마다 타임아웃이 재시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;gRPC 3-hop, Deadline 전파&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;초기 3s, 체인 공유&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;최대 3s&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;공통 절대 시각, 체인 전체가 한 번에 만료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;REST 4-hop, 상대 타임아웃&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2s × 4&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최대 8s&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;3-hop과 같은 메커니즘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;gRPC 4-hop, Deadline 전파&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;초기 2s, 체인 공유&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최대 2s&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;홉 수와 무관&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 한 줄이 Deadline이라는 개념의 존재 이유다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;공유 시계가
있으면 체인 전체가 하나의 SLA 안에 들어간다&lt;/strong&gt;. 없으면 홉 수만큼
곱해진다. 특히 4번째 열의 &amp;quot;홉 수와 무관&amp;quot;이 중요하다. 서비스 메쉬가
커지면서 체인이 4-hop, 5-hop으로 길어져도, Deadline 모델은 초기 시각을
바꾸지 않는 한 최악 지연이 늘어나지 않는다. 상대 타임아웃 모델은 체인이
길어질수록 비례해서 악화된다. 실제 시퀀스는 &lt;a href=&quot;#8-3-hop-예산-전파-시퀀스-client--gateway--service-a--service-b&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§8&lt;/a&gt;에서
다이어그램으로 본다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-헤더-전달은-강제가-아니다-bander-dead-config-실증&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2) 헤더
전달은 강제가 아니다: BANDER dead config 실증&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 섹션이 이 글에서 가장 두껍다. 설정 값이 분명히 선언돼 있는데
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;동작은 전혀 하지 않는&lt;/strong&gt; 상태가 어떻게 만들어지는지,
BANDER 저장소의 실제 세 파일을 증거로 풀어낸다. 읽으면서 &amp;quot;내 프로젝트도
이렇지 않을까&amp;quot;라고 의심하는 지점을 코드로 열어서 확인하게 만드는 게 이
섹션의 목적이다.&lt;/p&gt;
&lt;h3 id=&quot;2-1-bander가-실제로-틀렸던-지점-선언은-멀쩡한데-조립이-비어-있다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-1)
BANDER가 실제로 틀렸던 지점: 선언은 멀쩡한데 조립이 비어 있다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;먼저 설정 빈이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcClientProperties&lt;/code&gt;는 Deadline을
정직하게 선언한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// libraries/grpc/client-common/.../GrpcClientProperties.java&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;im&quot;&gt; com&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-3&quot;&gt;&lt;a href=&quot;#cb2-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-4&quot;&gt;&lt;a href=&quot;#cb2-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-5&quot;&gt;&lt;a href=&quot;#cb2-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-6&quot;&gt;&lt;a href=&quot;#cb2-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; GrpcClientProperties &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-7&quot;&gt;&lt;a href=&quot;#cb2-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-8&quot;&gt;&lt;a href=&quot;#cb2-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; host &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;localhost&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-9&quot;&gt;&lt;a href=&quot;#cb2-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;int&lt;/span&gt; port &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;9090&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-10&quot;&gt;&lt;a href=&quot;#cb2-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; deadline &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;ofSeconds&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-11&quot;&gt;&lt;a href=&quot;#cb2-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;boolean&lt;/span&gt; plaintextEnabled &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-12&quot;&gt;&lt;a href=&quot;#cb2-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-13&quot;&gt;&lt;a href=&quot;#cb2-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;getDeadline&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-14&quot;&gt;&lt;a href=&quot;#cb2-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; deadline&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-15&quot;&gt;&lt;a href=&quot;#cb2-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-16&quot;&gt;&lt;a href=&quot;#cb2-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-17&quot;&gt;&lt;a href=&quot;#cb2-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;setDeadline&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; deadline&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-18&quot;&gt;&lt;a href=&quot;#cb2-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;kw&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;deadline&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; deadline&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-19&quot;&gt;&lt;a href=&quot;#cb2-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-20&quot;&gt;&lt;a href=&quot;#cb2-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// host/port/plaintextEnabled의 getter·setter 생략&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-21&quot;&gt;&lt;a href=&quot;#cb2-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc.client.deadline: 3s&lt;/code&gt;로 덮어쓸 수 있고, 기본값은 5초다.
스프링 바인딩이 걸려 있다면 기동 로그에도 &amp;quot;deadline=PT5S&amp;quot;가 찍힌다.
여기까지는 완벽하다. 선언 자체는 문제가 없다. 문제는 이 값을 실제로
채널에 연결하는 다음 파일이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// libraries/grpc/client-common/.../GrpcChannelFactory.java&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;im&quot;&gt; com&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-4&quot;&gt;&lt;a href=&quot;#cb3-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;ManagedChannel&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-5&quot;&gt;&lt;a href=&quot;#cb3-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;ManagedChannelBuilder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-6&quot;&gt;&lt;a href=&quot;#cb3-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-7&quot;&gt;&lt;a href=&quot;#cb3-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; GrpcChannelFactory &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-8&quot;&gt;&lt;a href=&quot;#cb3-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-9&quot;&gt;&lt;a href=&quot;#cb3-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; ManagedChannel &lt;span class=&quot;fu&quot;&gt;createChannel&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;GrpcClientProperties properties&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-10&quot;&gt;&lt;a href=&quot;#cb3-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        ManagedChannelBuilder&lt;span class=&quot;op&quot;&gt;&amp;lt;?&amp;gt;&lt;/span&gt; builder &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; ManagedChannelBuilder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;forAddress&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-11&quot;&gt;&lt;a href=&quot;#cb3-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getHost&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(),&lt;/span&gt; properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getPort&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-12&quot;&gt;&lt;a href=&quot;#cb3-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isPlaintextEnabled&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-13&quot;&gt;&lt;a href=&quot;#cb3-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            builder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;usePlaintext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-14&quot;&gt;&lt;a href=&quot;#cb3-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-15&quot;&gt;&lt;a href=&quot;#cb3-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; builder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-16&quot;&gt;&lt;a href=&quot;#cb3-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-17&quot;&gt;&lt;a href=&quot;#cb3-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;properties.getDeadline()&lt;/code&gt;을 호출하는 코드가
어디에도 없다.&lt;/strong&gt; &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;host&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;port&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;plaintextEnabled&lt;/code&gt;는 정확히 참조되지만
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;deadline&lt;/code&gt;은 한 번도 읽히지 않는다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;intercept(...)&lt;/code&gt; 호출도 없다. 채널이 조립되는 이 단일
지점에서 Deadline은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;완전히 무시&lt;/strong&gt;된다. 오토컨피그도
상황이 같다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// libraries/grpc/client-common/.../config/GrpcClientAutoConfiguration.java&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-2&quot;&gt;&lt;a href=&quot;#cb4-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;im&quot;&gt; com&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-3&quot;&gt;&lt;a href=&quot;#cb4-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-4&quot;&gt;&lt;a href=&quot;#cb4-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;com&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;example&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;GrpcChannelFactory&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-5&quot;&gt;&lt;a href=&quot;#cb4-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;org&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;springframework&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;boot&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;autoconfigure&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;AutoConfiguration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-6&quot;&gt;&lt;a href=&quot;#cb4-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;org&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;springframework&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;Bean&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-7&quot;&gt;&lt;a href=&quot;#cb4-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-8&quot;&gt;&lt;a href=&quot;#cb4-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfiguration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-9&quot;&gt;&lt;a href=&quot;#cb4-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; GrpcClientAutoConfiguration &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-10&quot;&gt;&lt;a href=&quot;#cb4-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-11&quot;&gt;&lt;a href=&quot;#cb4-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-12&quot;&gt;&lt;a href=&quot;#cb4-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; GrpcChannelFactory &lt;span class=&quot;fu&quot;&gt;grpcChannelFactory&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-13&quot;&gt;&lt;a href=&quot;#cb4-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;GrpcChannelFactory&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-14&quot;&gt;&lt;a href=&quot;#cb4-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-15&quot;&gt;&lt;a href=&quot;#cb4-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 파일에는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcClientProperties&lt;/code&gt;를 빈으로 등록하는
로직조차 없다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableConfigurationProperties(GrpcClientProperties.class)&lt;/code&gt;도
없고, 클래스에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt;도 붙어 있지 않다.
이 말은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc.client.deadline: 3s&lt;/code&gt;를 아무리 써도 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;그 값이
빈으로 바인딩되지도 않는다&lt;/strong&gt;는 뜻이다. 파일만 멀쩡히 있고, 기동
시점에 아무 일도 안 일어난다. 이 상태를 나는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;3중 dead
config&lt;/strong&gt;라고 부른다 — 프로퍼티 바인딩 없음, Factory가 값을 읽지
않음, interceptor 등록 없음. 세 줄이 전부 동시에 비어 있다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 상태에서 실제 wire는 어떻게 나가는가. 결과는 분명하다. HTTP/2
HEADERS 프레임에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;존재하지
않는다&lt;/strong&gt;. 서버는 Deadline이 없는 것으로 간주하고 요청을 무한히
처리하려 한다. 프런트엔드가 5초를 기다리다가 LB 타임아웃에 걸려 끊기고,
사용자는 503을 본다. 서버 로그에는 8.9초짜리 &amp;quot;정상 완료&amp;quot; 쿼리가 남는다.
양쪽이 서로 다른 말을 하는 전형적 장애다.&lt;/p&gt;
&lt;h3 id=&quot;2-2-어떻게-찾아냈는가-세-가지-증거&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-2) 어떻게 찾아냈는가: 세
가지 증거&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 dead config를 찾아낸 방법은 셋이다. 첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Wireshark로
HTTP/2 HEADERS 프레임을 덤프&lt;/strong&gt;했다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;:method=POST&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;:path=/...&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;content-type=application/grpc&lt;/code&gt;는 다
있는데 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt;만 없었다. 이게 가장 확실한 증거다.
gRPC-Java가 Deadline을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CallOptions&lt;/code&gt;에 가지고 있었다면 반드시
이 헤더가 나간다. 나가지 않았다는 건 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CallOptions&lt;/code&gt;에도 없다는
뜻이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;둘째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서버 쪽에 간단한 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ServerInterceptor&lt;/code&gt;를
걸어&lt;/strong&gt; 수신된 호출의 Deadline을 로그로 찍었다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current().getDeadline()&lt;/code&gt;을 꺼내보니
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;null&lt;/code&gt;이었다. 서버가 Deadline을 못 받았다는 직접 증거다.
셋째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;부하 테스트에서 p99 응답 시간이 9초를 넘어가는 걸&lt;/strong&gt;
발견했다. 평균은 500ms인데 p99만 9초 근처에 붙어 있었다. 이건 LB
타임아웃(보통 10초 내외)에 걸려 끊긴 요청들의 흔적이다. 정상 Deadline이
3초로 걸려 있으면 p99가 3초 언저리에서 잘려야 하는데, 잘리지 않고
9초까지 갔다는 건 Deadline이 실제로 동작하지 않는다는 뜻이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 셋 중 하나만 봐도 충분하지만, 장애 복기 문서에는 셋 다 적어두는 게
좋다. Wireshark 덤프가 없어도 서버 인터셉터와 p99 그래프만으로도 원인이
좁혀진다. 셋을 병렬로 돌려보고 각자가 같은 결론(헤더 없음)을 가리키는지
확인하는 과정 자체가 &amp;quot;아, 이건 설정의 문제가 아니라 코드의 문제구나&amp;quot;를
팀에게 납득시키는 수단이기도 하다.&lt;/p&gt;
&lt;h3 id=&quot;2-3-고치는-방법-defaultdeadlineinterceptor&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-3) 고치는 방법:
DefaultDeadlineInterceptor&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;표준 해법은 gRPC-Java 코어에 내장돼 있지 않다. 커뮤니티에서는
Salesforce가 공개한 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DefaultDeadlineInterceptor&lt;/code&gt; 패턴이
사실상 de facto다. 핵심은 &amp;quot;이미 호출자가 Deadline을 지정했으면 건드리지
말고, 없을 때만 기본값을 얹는다&amp;quot;는 한 가지 규칙이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb5&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb5-1&quot;&gt;&lt;a href=&quot;#cb5-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;CallOptions&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-2&quot;&gt;&lt;a href=&quot;#cb5-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;Channel&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-3&quot;&gt;&lt;a href=&quot;#cb5-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;ClientCall&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-4&quot;&gt;&lt;a href=&quot;#cb5-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;ClientInterceptor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-5&quot;&gt;&lt;a href=&quot;#cb5-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;io&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;grpc&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;MethodDescriptor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-6&quot;&gt;&lt;a href=&quot;#cb5-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-7&quot;&gt;&lt;a href=&quot;#cb5-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-8&quot;&gt;&lt;a href=&quot;#cb5-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;im&quot;&gt;java&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;util&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;concurrent&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;TimeUnit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-9&quot;&gt;&lt;a href=&quot;#cb5-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-10&quot;&gt;&lt;a href=&quot;#cb5-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; DefaultDeadlineInterceptor &lt;span class=&quot;kw&quot;&gt;implements&lt;/span&gt; ClientInterceptor &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-11&quot;&gt;&lt;a href=&quot;#cb5-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-12&quot;&gt;&lt;a href=&quot;#cb5-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;long&lt;/span&gt; defaultDeadlineMillis&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-13&quot;&gt;&lt;a href=&quot;#cb5-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-14&quot;&gt;&lt;a href=&quot;#cb5-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;DefaultDeadlineInterceptor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; defaultDeadline&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-15&quot;&gt;&lt;a href=&quot;#cb5-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;kw&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;defaultDeadlineMillis&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; defaultDeadline&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;toMillis&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-16&quot;&gt;&lt;a href=&quot;#cb5-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-17&quot;&gt;&lt;a href=&quot;#cb5-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-18&quot;&gt;&lt;a href=&quot;#cb5-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-19&quot;&gt;&lt;a href=&quot;#cb5-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ReqT&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; RespT&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; ClientCall&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ReqT&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; RespT&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;interceptCall&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-20&quot;&gt;&lt;a href=&quot;#cb5-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;bu&quot;&gt;MethodDescriptor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ReqT&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; RespT&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; method&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-21&quot;&gt;&lt;a href=&quot;#cb5-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            CallOptions callOptions&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-22&quot;&gt;&lt;a href=&quot;#cb5-22&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;bu&quot;&gt;Channel&lt;/span&gt; next&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-23&quot;&gt;&lt;a href=&quot;#cb5-23&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;co&quot;&gt;// 호출자가 이미 Deadline을 지정했으면 건드리지 않는다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-24&quot;&gt;&lt;a href=&quot;#cb5-24&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;callOptions&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getDeadline&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-25&quot;&gt;&lt;a href=&quot;#cb5-25&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            callOptions &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; callOptions&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDeadlineAfter&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-26&quot;&gt;&lt;a href=&quot;#cb5-26&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                    defaultDeadlineMillis&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;TimeUnit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;MILLISECONDS&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-27&quot;&gt;&lt;a href=&quot;#cb5-27&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-28&quot;&gt;&lt;a href=&quot;#cb5-28&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; next&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;newCall&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;method&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; callOptions&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-29&quot;&gt;&lt;a href=&quot;#cb5-29&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-30&quot;&gt;&lt;a href=&quot;#cb5-30&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;원리가 단순하다는 게 중요하다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CallOptions&lt;/code&gt;에 Deadline이
없으면 기본값을 얹고, 있으면 그대로 둔다. 호출자가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;stub.withDeadlineAfter(1, SECONDS)&lt;/code&gt;로 더 짧게 지정한 경우를
덮어쓰지 않는 게 핵심이다. 만약 덮어쓰면 &amp;quot;이 호출만 1초 Deadline&amp;quot;이라는
의도가 기본값 5초로 되살아나 버린다. 이 실수를 피하려면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;null&lt;/code&gt; 체크 한 줄이 반드시 있어야 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이제 Factory에 이 인터셉터를 등록한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; ManagedChannel &lt;span class=&quot;fu&quot;&gt;createChannel&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;GrpcClientProperties properties&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    ManagedChannelBuilder&lt;span class=&quot;op&quot;&gt;&amp;lt;?&amp;gt;&lt;/span&gt; builder &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; ManagedChannelBuilder&lt;/span&gt;
&lt;span id=&quot;cb6-3&quot;&gt;&lt;a href=&quot;#cb6-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;forAddress&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getHost&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(),&lt;/span&gt; properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getPort&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-4&quot;&gt;&lt;a href=&quot;#cb6-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;intercept&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;DefaultDeadlineInterceptor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getDeadline&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-5&quot;&gt;&lt;a href=&quot;#cb6-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isPlaintextEnabled&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-6&quot;&gt;&lt;a href=&quot;#cb6-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        builder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;usePlaintext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-7&quot;&gt;&lt;a href=&quot;#cb6-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-8&quot;&gt;&lt;a href=&quot;#cb6-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; builder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-9&quot;&gt;&lt;a href=&quot;#cb6-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.intercept(...)&lt;/code&gt; &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한 줄&lt;/strong&gt;. 이 한 줄이
추가되면 비로소 Deadline이 wire에 실린다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration&lt;/code&gt;을 빈에
선언하는 것과 실제로 그 값이 RPC에 동작하는 것 사이에 이 한 줄이 있다.
그리고 오토컨피그도 같이 고쳐서 Properties를 빈으로 올리고, Factory가 그
Properties를 주입받도록 체인을 완성한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb7&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb7-1&quot;&gt;&lt;a href=&quot;#cb7-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfiguration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-2&quot;&gt;&lt;a href=&quot;#cb7-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@EnableConfigurationProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;GrpcClientProperties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-3&quot;&gt;&lt;a href=&quot;#cb7-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; GrpcClientAutoConfiguration &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-4&quot;&gt;&lt;a href=&quot;#cb7-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-5&quot;&gt;&lt;a href=&quot;#cb7-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-6&quot;&gt;&lt;a href=&quot;#cb7-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; GrpcChannelFactory &lt;span class=&quot;fu&quot;&gt;grpcChannelFactory&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-7&quot;&gt;&lt;a href=&quot;#cb7-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;GrpcChannelFactory&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-8&quot;&gt;&lt;a href=&quot;#cb7-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-9&quot;&gt;&lt;a href=&quot;#cb7-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;세 파일이 동시에 수정되어야 dead config가 살아난다. 한 군데만 고치면
다른 지점이 여전히 비어 있어서 증상은 그대로다. 이 세 지점을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한
PR로 묶어서&lt;/strong&gt; 올리는 게 실무 원칙이다. 나눠서 머지하면 중간
상태에서 &amp;quot;바인딩은 되는데 Factory가 안 읽는다&amp;quot; 같은 이상한 절반 상태가
생긴다.&lt;/p&gt;
&lt;h3 id=&quot;2-4-lifo-순서-함정-deadline은-가장-먼저-등록&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-4) LIFO 순서
함정: Deadline은 가장 먼저 등록&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;인터셉터는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;여러 개를 등록할 수 있고 실행 순서가 정해져
있다&lt;/strong&gt;. 여기서 두 번째 함정이 나온다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ManagedChannelBuilder.intercept(...)&lt;/code&gt;는 여러 번 호출하거나
한 번에 varargs로 여러 개를 넘길 수 있는데, 실행 순서는
**LIFO(Last-In-First-Out)**다. 나중에 등록한 인터셉터가 바깥쪽을 감싸고,
먼저 등록한 인터셉터가 가장 안쪽, 즉 실제 네트워크 호출에 가장 가까운
위치에 선다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;구체 예시로 트레이싱(OpenTelemetry) 인터셉터와 Deadline 인터셉터를
같이 쓰는 경우를 보자.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb8&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb8-1&quot;&gt;&lt;a href=&quot;#cb8-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기: Deadline이 트레이싱보다 바깥에 있음&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-2&quot;&gt;&lt;a href=&quot;#cb8-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;builder&lt;/span&gt;
&lt;span id=&quot;cb8-3&quot;&gt;&lt;a href=&quot;#cb8-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;intercept&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;tracingInterceptor&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;    &lt;span class=&quot;co&quot;&gt;// 먼저 등록 → 가장 안쪽&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-4&quot;&gt;&lt;a href=&quot;#cb8-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;intercept&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;deadlineInterceptor&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// 나중 등록 → 바깥쪽&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-5&quot;&gt;&lt;a href=&quot;#cb8-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-6&quot;&gt;&lt;a href=&quot;#cb8-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: Deadline이 가장 안쪽&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-7&quot;&gt;&lt;a href=&quot;#cb8-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;builder&lt;/span&gt;
&lt;span id=&quot;cb8-8&quot;&gt;&lt;a href=&quot;#cb8-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;intercept&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;deadlineInterceptor&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;co&quot;&gt;// 먼저 등록 → 가장 안쪽&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-9&quot;&gt;&lt;a href=&quot;#cb8-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;intercept&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;tracingInterceptor&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;   &lt;span class=&quot;co&quot;&gt;// 나중 등록 → 바깥쪽&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;왜 Deadline을 가장 안쪽에 두는가. 트레이싱 인터셉터는 span을
생성하면서 &amp;quot;이 호출의 Deadline은 얼마였다&amp;quot;는 메타데이터를 span
attribute로 찍는다. 이 값을 정확히 찍으려면 트레이싱이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;newCall&lt;/code&gt;로 체인을 넘기기 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;전에 이미&lt;/strong&gt;
Deadline이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CallOptions&lt;/code&gt;에 심어져 있어야 한다. Deadline을
바깥에 두면 트레이싱 span이 먼저 시작되고, 그 span이 끝나기 직전에야
Deadline이 얹어진다. 그 사이 수십 밀리초의 공백이 span attribute에
기록되지 않아 디버깅이 어려워진다. p99 튜닝 단계에 가면 이게 실제로
드러난다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;정리하면, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Deadline 인터셉터는 가장 먼저 등록한다&lt;/strong&gt;.
코드 리뷰 체크리스트에 이 한 줄을 넣어두면 다른 인터셉터가 추가돼도
자연스럽게 유지된다. 트레이싱, 메트릭, 인증, 로깅 인터셉터가 뒤에 계속
쌓여도 Deadline은 항상 안쪽에 남는다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-iogrpccontext-parent-child와-attachdetach&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3) io.grpc.Context:
parent-child와 attach/detach&lt;/h2&gt;
&lt;h3 id=&quot;3-1-context는-threadlocal-기반이지만-threadlocal이-아니다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-1)
Context는 ThreadLocal 기반이지만 ThreadLocal이 아니다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;io.grpc.Context&lt;/code&gt;는 현재 호출에 걸린 Deadline, 취소 토큰,
그리고 커스텀 key-value 쌍을 담는 불변 객체다. 겉보기는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ThreadLocal&lt;/code&gt;처럼 동작하지만, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;불변 · fork 기반 ·
스코프 명시&lt;/strong&gt;라는 세 가지가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ThreadLocal&lt;/code&gt;과 다르다.
가장 핵심은 &amp;quot;불변&amp;quot;이라는 점이다. 한 번 만들어진 Context는 수정되지
않는다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;withValue(key, value)&lt;/code&gt;나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;withDeadlineAfter(...)&lt;/code&gt;를 부르면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;새로운 자식
Context&lt;/strong&gt;가 돌아온다. 부모는 그대로 남아 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb9&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb9-1&quot;&gt;&lt;a href=&quot;#cb9-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt; parent &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-2&quot;&gt;&lt;a href=&quot;#cb9-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt; child &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; parent&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDeadlineAfter&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;TimeUnit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;SECONDS&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; scheduler&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-3&quot;&gt;&lt;a href=&quot;#cb9-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-4&quot;&gt;&lt;a href=&quot;#cb9-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// parent는 여전히 Deadline이 없다. child만 있다.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-5&quot;&gt;&lt;a href=&quot;#cb9-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 두 객체는 다른 노드이고, parent는 child의 존재를 알지 못한다.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 parent-child 관계가 취소 전파의 뼈대다. 부모 Context가 취소되면
자식 Context도 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동으로 취소&lt;/strong&gt;된다. 반대 방향은 성립하지
않는다 — 자식이 취소돼도 부모는 영향받지 않는다. 이 방향성이 중요하다.
상위 요청이 끊어지면 그 안에서 파생된 모든 하위 호출이 동시에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;로 터진다. &amp;quot;상위 요청은 이미 끝났는데 서버는 내부
쿼리를 계속 돌리는&amp;quot; 낭비를 이 전파가 막는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;또 하나 기억할 지점은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Context가 fork된다&lt;/strong&gt;는 것이다.
같은 parent에서 두 개의 자식이 독립적으로 만들어질 수 있다. 각자 다른
Deadline을 가질 수도 있고, 각자 다른 값을 덮어쓸 수도 있다. 둘은 서로
간섭하지 않는다. 이 fork 모델이 여러 스레드가 동시에 같은 부모 맥락에서
작업할 때 정합성을 보장한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ThreadLocal&lt;/code&gt;은 이 모델이 없다 —
한 스레드가 값을 바꾸면 그게 그 스레드의 유일한 상태가 되고, 다른
스레드는 완전히 독립된 별도 값을 가진다. gRPC Context는 &amp;quot;부모를 공유하고
자식만 다르게 fork되는&amp;quot; 트리 구조라서 여러 자식이 공통 부모의 Deadline을
동시에 상속할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;3-2-attachdetach의-try-finally와-contextrun&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-2)
attach/detach의 try-finally와 Context.run&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Context를 &amp;quot;현재 스레드의 기본값으로 설정&amp;quot;하려면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;attach()&lt;/code&gt;를 부른다. 이 메서드는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;이전 Context를
리턴&lt;/strong&gt;하고, 작업이 끝나면 반드시 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;detach(previous)&lt;/code&gt;를
불러 원래 상태로 복원해야 한다. 패턴은 고정돼 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb10&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb10-1&quot;&gt;&lt;a href=&quot;#cb10-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt; previous &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; newContext&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;attach&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-2&quot;&gt;&lt;a href=&quot;#cb10-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-3&quot;&gt;&lt;a href=&quot;#cb10-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// 이 블록 안에서는 Context.current()가 newContext를 리턴한다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-4&quot;&gt;&lt;a href=&quot;#cb10-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;doWork&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-5&quot;&gt;&lt;a href=&quot;#cb10-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-6&quot;&gt;&lt;a href=&quot;#cb10-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    newContext&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;previous&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-7&quot;&gt;&lt;a href=&quot;#cb10-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;try-finally&lt;/code&gt;를 빼먹으면 예외 경로에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;detach&lt;/code&gt;가 호출되지 않고, 스레드가 풀로 반환될 때
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;엉뚱한 Context가 남아&lt;/strong&gt; 다음 작업으로 흘러간다. 이 누수는
디버깅이 극악하다. 현상은 &amp;quot;왜 이 요청에 있지도 않은 Deadline이 걸려
있지?&amp;quot; 같은 식이고, 원인은 이전 요청이 남긴 Context다. 자원 풀 기반
아키텍처(스레드풀·executor)에서는 이게 치명적이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;더 안전한 방법은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.run(Runnable)&lt;/code&gt;이나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.call(Callable)&lt;/code&gt;을 쓰는 것이다. 내부에서
attach/detach를 자동으로 try-finally로 감싸준다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb11&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb11-1&quot;&gt;&lt;a href=&quot;#cb11-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;newContext&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;doWork&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-2&quot;&gt;&lt;a href=&quot;#cb11-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-3&quot;&gt;&lt;a href=&quot;#cb11-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 값을 리턴받으려면:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-4&quot;&gt;&lt;a href=&quot;#cb11-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; result &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; newContext&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;doWorkReturning&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실무에서는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;run&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;call&lt;/code&gt;을 우선 쓴다. 직접
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;attach&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;detach&lt;/code&gt;를 부르는 건 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스레드
경계를 수동으로 넘기는&lt;/strong&gt; 인터셉터 내부나 Context-aware executor
래퍼 같은 저수준 코드에서만이다. 비즈니스 로직에 직접 쓰지 않는 게
원칙이다.&lt;/p&gt;
&lt;h3 id=&quot;3-3-cancellablecontext--명시적-취소-핸들&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-3)
CancellableContext — 명시적 취소 핸들&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Deadline이 아니라 &amp;quot;명시적 취소 신호&amp;quot;를 주고받고 싶을 때가 있다. 예를
들어 사용자가 &amp;quot;이 조회 작업을 취소&amp;quot; 버튼을 눌렀을 때, 그 신호를 서버의
Context에 연결해 진행 중인 쿼리를 중단시키는 구조다. 이럴 때는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.withCancellation()&lt;/code&gt;이
**&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CancellableContext&lt;/code&gt;**라는 자식을 돌려주는데, 이 자식에는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;cancel(Throwable)&lt;/code&gt; 메서드가 달려 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb12&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb12-1&quot;&gt;&lt;a href=&quot;#cb12-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;CancellableContext&lt;/span&gt; cancellable &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withCancellation&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-2&quot;&gt;&lt;a href=&quot;#cb12-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 어디선가 취소 트리거 발생&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-3&quot;&gt;&lt;a href=&quot;#cb12-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;cancellable&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;cancel&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;RuntimeException&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;user aborted&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-4&quot;&gt;&lt;a href=&quot;#cb12-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 그 순간 cancellable 하위의 모든 작업이 취소 신호를 받는다&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CancellableContext&lt;/code&gt;는 명시적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;close()&lt;/code&gt;
또는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;cancel()&lt;/code&gt;을 호출해 정리해야 한다. 안 하면 리스너가
GC되지 않고 남을 수 있다. Deadline 기반 Context는 만료 시점에 자동으로
취소되지만, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CancellableContext&lt;/code&gt;는 수동 제어 핸들이라 누군가
반드시 닫아줘야 한다. 이 차이가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;withDeadlineAfter&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;withCancellation&lt;/code&gt;의 운영 차이다. 실무에서는 Deadline을
기본으로 쓰고, 명시적 사용자 취소가 필요한 드문 케이스에만
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CancellableContext&lt;/code&gt;를 추가로 얹는다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-비동기-경계-3종-context가-증발하는-지점&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4) 비동기 경계 3종:
Context가 증발하는 지점&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 진짜 함정이 시작된다. 스레드 경계를 넘는 순간
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current()&lt;/code&gt;가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ROOT로 돌아가는&lt;/strong&gt; 경계가
실무에 세 종류 있다. 셋 다 겉보기에는 Context가 자동으로 따라갈 것처럼
보이는데, 실제로는 따라가지 않는다. 이 섹션이 가장 자주 사고가 나는
지점이다.&lt;/p&gt;
&lt;h3 id=&quot;4-1-completablefuture-supplyasync의-기본-풀은-forkjoinpoolcommonpool&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-1)
CompletableFuture: supplyAsync의 기본 풀은 ForkJoinPool.commonPool&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CompletableFuture.supplyAsync(() -&amp;gt; stub.call(req))&lt;/code&gt;는
기본적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ForkJoinPool.commonPool&lt;/code&gt;의 스레드에서 람다를
실행한다. 이 풀의 스레드는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;애플리케이션 수명 동안
공유&lt;/strong&gt;되고, 이전에 어떤 작업이 끝났든 상관없이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current()&lt;/code&gt;는 그 시점의 ROOT를 가리킨다. 부모
스레드에 Deadline이 있어도 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동으로 상속되지 않는다&lt;/strong&gt;.
gRPC-Java는 이 상속을 자동으로 해주는 메커니즘을 두지 않았다.
명시적이어야 한다는 철학이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb13&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb13-1&quot;&gt;&lt;a href=&quot;#cb13-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기: Context가 새 스레드로 따라가지 않는다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-2&quot;&gt;&lt;a href=&quot;#cb13-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;CompletableFuture&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;supplyAsync&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; stub&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getStudio&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-3&quot;&gt;&lt;a href=&quot;#cb13-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;thenAccept&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;::&lt;/span&gt;handle&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 코드에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;stub.getStudio(req)&lt;/code&gt;가 실행되는 스레드의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current()&lt;/code&gt;는 ROOT다. 부모가 5초 Deadline 안에서 돌고
있어도, 이 호출은 Deadline 없이 나간다. 증상은 &amp;quot;대시보드에서는 3초 SLA를
맞추고 있는데 특정 비동기 경로만 9초까지 튀는&amp;quot; 형태다. 고치려면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current().wrap(...)&lt;/code&gt;으로 람다를 감싼다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb14&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb14-1&quot;&gt;&lt;a href=&quot;#cb14-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: Context.current().wrap(...)으로 싸서 넘긴다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-2&quot;&gt;&lt;a href=&quot;#cb14-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt; ctx &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-3&quot;&gt;&lt;a href=&quot;#cb14-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;CompletableFuture&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;supplyAsync&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ctx&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;wrap&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; stub&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getStudio&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;op&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-4&quot;&gt;&lt;a href=&quot;#cb14-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;thenAccept&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;::&lt;/span&gt;handle&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ctx.wrap(Supplier)&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;람다 실행 직전에 ctx를
attach하고, 끝나면 detach&lt;/strong&gt;하는 래퍼를 리턴한다. 이 한 줄이
Deadline 상속의 전부다. 빼먹으면 비동기 경계 뒤의 호출은 SLA 계약에서
조용히 탈락한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Runnable&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Callable&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Supplier&lt;/code&gt; 모두
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wrap&lt;/code&gt; 오버로드가 있다. 같은 원리로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;executor.submit(...)&lt;/code&gt;에 직접 람다를 넘기면 안 된다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ctx.wrap(runnable)&lt;/code&gt;로 감싸거나, 아예 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Context-aware
executor&lt;/strong&gt;를 써서 경계 자체를 자동화한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb15&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb15-1&quot;&gt;&lt;a href=&quot;#cb15-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;ExecutorService&lt;/span&gt; wrapped &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;currentContextExecutor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;executor&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-2&quot;&gt;&lt;a href=&quot;#cb15-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;wrapped&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;submit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; stub&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getStudio&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-3&quot;&gt;&lt;a href=&quot;#cb15-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// wrapped는 제출되는 모든 작업에 현재 Context를 자동 attach한다&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 executor 래핑은 스프링 빈 정의 지점에 한 번만 해두면 되므로
실무에서는 이 쪽을 선호한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Async&lt;/code&gt; 메서드가 쓰는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TaskExecutor&lt;/code&gt;를 이 방식으로 감싸두면 비동기 서비스 계층이
자동으로 Context를 이어받는다. 람다마다 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ctx.wrap&lt;/code&gt;을 붙이는
건 사람 실수가 너무 잘 나는 지점이라, executor 자체를 감싸는 게 훨씬
안전하다.&lt;/p&gt;
&lt;h3 id=&quot;4-2-reactor-schedulers-경계와-contextview의-분리&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-2) Reactor:
Schedulers 경계와 ContextView의 분리&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Reactor의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Mono&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Flux&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;assembly
time과 subscription time이 분리&lt;/strong&gt;돼 있어서 스레드 경계가 더
미묘하다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;publishOn(Schedulers.boundedElastic())&lt;/code&gt;을 타는
순간 실행 스레드가 바뀌고, 그 스레드에서는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current()&lt;/code&gt;가 ROOT로 돌아온다. Reactor에는 자체적인
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ContextView&lt;/code&gt;가 있는데, 이건 gRPC의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;io.grpc.Context&lt;/code&gt;와는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;완전히 별개의 객체&lt;/strong&gt;다.
둘은 자동으로 연동되지 않는다. 구조가 비슷하지만 서로 존재를 모른다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;해법은 두 갈래다. 첫째, 스케줄러를
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.currentContextExecutor(...)&lt;/code&gt;로 감싼 executor로
생성한다. 이렇게 하면 Reactor가 작업을 옮길 때마다 gRPC Context가 같이
따라간다. 둘째, Micrometer의 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Context Propagation&lt;/strong&gt;
라이브러리를 써서 gRPC Context와 Reactor ContextView 사이에 자동 매핑을
건다. Reactor 2023년 이후 버전은 이 라이브러리를 공식 지원하고, 한 번
등록하면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ThreadLocal&lt;/code&gt; 기반 Context들이 Reactor 체인을
따라가도록 배선된다. 운영 코드베이스가 크면 두 번째 방식이 훨씬 낫다.
경계가 하나만 빠져 있어도 사고가 나는데, 그 경계를 사람이 일일이
찾아내는 건 비용이 감당 안 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;간단한 원칙 하나는 — Reactor를 쓰면서 gRPC 호출을
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;flatMap&lt;/code&gt; 안에 넣는다면, 그 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;flatMap&lt;/code&gt;이 타는
스케줄러에서 Context가 보장되는지 반드시 점검한다. 보장되지 않으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;deferContextual&lt;/code&gt;을 써서 스레드 위의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current()&lt;/code&gt;를 매번 재조합하거나, Context
Propagation을 걸거나, 둘 중 하나여야 한다. 자동 상속을 믿고 넘어가면 안
된다. 특히 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;subscribeOn&lt;/code&gt;과 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;publishOn&lt;/code&gt;이 같은
체인에 섞여 있으면 어느 시점에 스레드가 바뀌는지 사람이 추적하기
어려워지므로, Context Propagation을 거는 쪽이 현실적이다.&lt;/p&gt;
&lt;h3 id=&quot;4-3-virtual-thread-jdk-21의-threadlocal-상속-차이&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-3) Virtual
Thread: JDK 21+의 ThreadLocal 상속 차이&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;JDK 21의 가상 스레드는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ThreadLocal&lt;/code&gt;을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;부모로부터
상속하지 않는다&lt;/strong&gt;. 캐리어 스레드가 바뀌어도 가상 스레드 자신은
ThreadLocal을 유지하지만, 새 가상 스레드를
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Thread.ofVirtual().start(runnable)&lt;/code&gt;로 만들면 부모 스레드의
ThreadLocal이 새 가상 스레드로 복사되지 않는다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;io.grpc.Context&lt;/code&gt;가 내부적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ThreadLocal&lt;/code&gt;을
쓰므로 같은 문제를 그대로 안는다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb16&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb16-1&quot;&gt;&lt;a href=&quot;#cb16-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기: 새 가상 스레드에 Context가 따라가지 않는다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-2&quot;&gt;&lt;a href=&quot;#cb16-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Thread&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;ofVirtual&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; stub&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getStudio&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-3&quot;&gt;&lt;a href=&quot;#cb16-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-4&quot;&gt;&lt;a href=&quot;#cb16-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: wrap으로 감싼다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-5&quot;&gt;&lt;a href=&quot;#cb16-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt; ctx &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-6&quot;&gt;&lt;a href=&quot;#cb16-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Thread&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;ofVirtual&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ctx&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;wrap&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; stub&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getStudio&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;op&quot;&gt;)));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Executor 형태로 쓴다면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Executors.newVirtualThreadPerTaskExecutor()&lt;/code&gt;를
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.currentContextExecutor&lt;/code&gt;로 감싸는 게 같은 효과다.
가상 스레드가 &amp;quot;blocking을 마음 편히 해도 되는 곳&amp;quot;이라는 장점을 주지만,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Context 상속은 명시적으로 처리해야 한다&lt;/strong&gt;는 점은
동일하다. 가상 스레드로 옮긴 뒤 Deadline이 사라졌다는 사고가 JDK 21 도입
첫 달에 가장 흔하다. &amp;quot;플랫폼 스레드에서 잘 되던 게 왜 갑자기?&amp;quot;라는
질문이 바로 이 상속 차이 때문이다.&lt;/p&gt;
&lt;h3 id=&quot;4-4-세-경계를-한-장에&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-4) 세 경계를 한 장에&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;세 경계가 공통으로 공유하는 원인을 다이어그램으로 본다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;04_grpc-deadline-cancellation-context-deep-dive-01&quot; src=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAsEAAAHwCAMAAABwoULTAAAAYFBMVEUAAAAJCQkREREYGBgjIyMrKys2NjY/Pz9ERERNTU1WVlZfX19iYmJsbGxwcHB6enqGhoaIiIiSkpKcnJyhoaGtra2wsLC+vr7BwcHMzMzU1NTb29vm5ubv7+/x8fH///9sTjwiAAAAKnRFWHRjb3B5bGVmdABHZW5lcmF0ZWQgYnkgaHR0cHM6Ly9wbGFudHVtbC5jb212zsofAAACNmlUWHRwbGFudHVtbAABAAAAeJyFU11LG0EUfd9fcfEpeXCoEfpQaLFYfSiFSBSf8jLZnSSLszPb+TD6pjSIxEoDbTCFDViQfrwtGkoK7R/aGf9DZ7NrG0HwbZh77rnnnDuzJhUWSkfUU6GiBMyvvvnwOZulkF3/yW769mJo+wmEHHVE7KN1zhQ5UFl6BPbrxKTJ7WhsBp/A9mdgvx3Zy6En90IWY4EjiDjjflfwiIASmixUZBcHvBeyDrQxlcTzBPEVZh0nYOl2PLM/E7CDK3N5aj4mTVYORb4WgjBVqcJzeEVwQENGYFUuAZbgY0qJuEdUGICVnCCKKVG4RcmmVloQJHUc08OX8pD5TVbZ5GLvNQ/ZFucU+TxywvNjtWBuP0Baa7IGwb7iAmLdoqHs1pkj2va7JNBOiEQtrllAgg2KpQr9gkoULQ/wrTbZTteVA8Tbu6FbCKaVKprvZt65r+5nNBrbpA+dxtY6FHk1mVS6hTpEbSsdhLwiyNtiqAva1Rx35HlFSrC8/MLZgmfQEzgGe3Fik9/mxxQa9frOIqbU64DlCnZD0svXbW76WZpAlk7s9XSxY185cOHkDXf3YN8d25NzsKNTMMPvTkF7jvsvyuH/7TJXMnnv3Y19FOimPYrxGFcEWlwpHgFvL8YBc/8VhFAVzHjojLmr8pGVhjcOiK+dljnIfEkcIEuH9mzmHrszNSiDuxtqJ1Mwx4NsNjVnVx5hAeTTvTV3yr+Yt4JqT2pPUe0vkl9wJn5D010AAEdfSURBVHja7Z0HY6pI24afoSPYTTv7lv//s753z54kdnqHb4YmKCYaYxSda/cYgencDlOeGdB/gUJpMcylE0ChnARVMKXdUAVT2g1VMKXdUAVT2g1VMKXdUAVT2g1VMKXdUAVT2g1VMKXdUAVT2g07uHQKvod1IELihRkMqp3fcZjhgGeJaPd7g6cfxLH4hlolwWmLATkWx2wcku8RoKZAatnZydMl8/ftcJdOwImsORWMcAia0IXwPT85UcBP8F8kgMZ301PJLM3tKD2RpGemKphdNrHCjhBPVWR0WQAr7OEbX3iCdaR+4V5HXphwHH/k480lH0h0dZnNTmjr/IoysWejbvy3/OgYEr5hU3zuAbk6/h7+o0y2IxfZOMtamksvPcvyOE/BMgtOLfN3C7RcwYkm4ZvlD/NDKXui8ACv5C/z743LCFdioWKQr8Ef/KGMsvMLC/QXtnBlut1q1WUH0vEKXuvpL2SsNl2MELPnbPrzY/+1OYkzE5ogSSQ7YMdJJdcfxO7OaxFH2a8603mCf9jH/rCunpYreAuUSRGL8BELdl7JHHoB0FdCWguxQ4h0xk/Ph5bcfzP7hbOkro7H5Pjy0TQ0VBjfF5ouTp1hb8/ZJyy3aopBFMMZMH5PJgeum518R2OfKHpdBP+RmjHcMyz9kZCWC/8Mv6NBx9TOexN+mNYrONQhLA+cf9I/WA8yeSzXRRRoSFHgN66ae+DoeZPDBUXk3FLBEYSMt/GyCoZysrYjTsldxAsX9azkmZkFfdv5tQr6jsMMFPIwsENGHmMXa+j1iPzwucLnLBi4Fj+QQHdBM19gZcfiiHeXMBHcpdLPzkqk+VpN8dIJ4ZF7nzIyflwMu/FvclJmrUzLnfy5Yf9f+qeseH3ww6gSiojzxBQPEjsCraMHl75n30r7Fbza5OUBNwoS/LzMdGCAUnGXGFryFC7yIxu6ypx8iXAJcO4sPx2H4MH7xlMQxDBzUMdaB1lzc+qhjh7hii8IFrh3hT8hiecyg12BlP6UcNVeNDJLn9hZjLzVC7BMxPEwdXnBmf4lMd78ZRGp+dkyxSt2krZkEjRQOPhl2zF+8GtGFmYPC91wkNSFFdihyuZtISgbOx74C7dWQBF4eUGYS0ayXyfC/136pn0nrVew9ASvWYPAIJVnkthYnzyu+FybkzbOzGXCTMRQhrQCikxRrFZUceEK/68+VTUMngNPojK1+kRjvgePUphV9OwTKTvhKf4TOwp2NclkgsNnd30yv8I3P2YUM1J6ngvPzO/QEyd/gj/hiIX0LPbgODwRYpSlxmY7iUm+CGAKadODz8Yd5haLG+gjIng+KlrwQZxrGNfBj+q8mgGcp0Ha+F1rMJHntvZw6Xv2rbRewRtCIk4+lSi+08EMqndKZLu4j8YMwSL3fAbDpZWeZyEUQ+Hp7/TAXTMdc1G/wT4gEVevkLY+A1LZ5V2hflp0XcQKrqNgV53sNL4aMzs++2w1RBx/jOPlBqtQ3IwK+HOURj3JtOjYm8SrnE4yuEYkUgt3936bWMEP0qLi5jn9s0r4wBhX0h+v0GD19kTi73pDAR5sCcQbuuvtVzDurue16RDfdTcCViRacubxqNqo5P8C3cQC5NQBxDNPxdpKWxQSmCjMG7neNBmorj2vjU8l5OdQGZ9ADV/S/l9+jOXqyTs+UX3Ylsf/sVvTSfYiHvOV4/EYHA9nRyDZibNGRJJIXBpfCsOMhwmqBWzo7PObXQk3mEX9nuu84o4sfmpoJM+31ZG7AQUvyq/Jm4/zE+L+N7vW0ESpO9TWbI8LHS0aT73eEEQ+9cippotyl0ai9uDpPax5EyAOeFxt8oGHFCxNR15XLyfkMc1jV4mDLxI3XLgSiYSS0mfFOcI/N3zcSds30ZKJPaObnV3pzEOnFnMlO8BlPzKbNJRU429IsqqbCV+7o6ofm3lgnt4qP10rwD/Zx7mfiTrwO+SL+ckARqtouYLRfyoHrs+/4Brp3bW7qj/azpkLI6wQ5dWFiVMZ0B/LYYeL869YytxL/fZKgj/tGiAK+opRGHxA6rxNvbeK7RhU4mqmsiGP3Yzfg39U1nN7/cJnNTTHYhXRm3U4r6MukkdHX3XY7CxpEddwNtmBZJVV2GS2cSS6iZz/6Lal+BBzwP4VL8sTA4n8WCZJkeIhKRbz0rftO2m5gmsPZ9wGXvEQerhG5B53XEru0uVDB2TgajNSm4ovq4u3R/wf5+4KOnlNN1n4grqoxCqsEzTBhfi4cAwQSHUrvSx8HRDP131mKI67kh6WtgmsYjldSXSCxWN69mWnf1VmJ63EuWy4jPzWlOrTxZmmf9QsG8zOfIVULyadhRuj7Qqu5eUpHXMS+o3zaH1k4KtsvzAEQT3Rgc13t8nPLyCtxyQipdRLxwMeGVgBi9IrBPUpTIuQfUwifJq4EV6SgOEqPlPHTLoxB/tEpt8eIMRuydAZ+rU5uw3/pJHHvdhLRRhmjRdVrrlhxHwcpVYZV7NWA6G0+kW3NC+HbmTHk0IC8VYdWpdGXL8YJ5saKYnZPZ6qTB0+ijfTB3+C7db2yVTTQbpuzGEOt6lm7ZM8tZxbqYOL+8XsOd94tXqI2H2eqnQ5H8RuWQ9K3Lc/lFEtRIQOdLjNjlxvrvVQlsON1MGUe+VGHy2Uu4EqmNJuqIIp7YYqmNJuqIIp7YYqmNJuqIIp7YYqmNJuqIIp7YYqmNJuqIIp7YYqmNJuqIIp7YYqmNJuqIIp7YYqmNJubmWNxneQGJdOwY8h3s4GwlTBGwzlbp5Ijt05PZDr4G7u2QEk91MY8u3sX3k/N41SBZ0exJVAFUxpN1TBlHZDFUxpN1TBlHZDFUxpN1TBlHZDFUxpN1TBlHZDZ5UPgbw1pvPBJECcgH1DL3ptFVTBe/DJazaG+fu8yH7Sck3BXpJdIvtPI7RO1FUXkmKqlr+dGa/rhyp4Dxx5V3O6/7+bmawtavunm1GqYI+8em6Qv2g2fMtLcyIcHA3lVKiCm0j+lF+fOF4FX4OuVHspkRunL1cR/wPwtjn7TLsVPw5VcBPoL4jcCBiJFI/r+cEELRxRVMrWgYb45fiECCjfBlVwM+aS5yFc9ga4cZD0JAb+cly/eH1GsjYfuLf3EQ/+FDeFxddIPikyyglQBTejdUk72Fj1kWZB8bb7FTkHiW4lTwI8zd+6vSR+RoDkYnGHkVXSMn98hJQvQhXcDO8IPApsDmX9Nz0mL/HKWrko6JLFHPyzZaNs3IGz05dhMUr+OlDpi5FSvgBVcDMTTQ9AkHC7gY3eyeva8MdDPsSQv3cZqaRVEUAU5it2WNoyvgBUwc0wQ1hFmVT5p+zUW4MzhN4Rw93OsskWQhXchENavl5MXqPJK8Fb9i42Nh+JMKzSnTT498ZT7fylc3A/UAXvZdOaHWfazQd7pU2Z1V4zuO885axQBTchV0fHkKhlX8SsZuV3RxrUBFDTecr5oQr+lKId/AG4i/fvA0KinAE6DUppN1TBlHZDFUxpN1TB+0j8MPmZmLzY/WpMKxsM/cNcBMEP5eJS0J5cM/7Sx5/crx+Iyp3/8rTHPUbx7x4A4rrqHr8GdOykB9NkT29TX+NKanQzu/w1QRXciP+GRlLs2g2XzHBw8NlPSD0lyw6j6uaeRUoJO4hCexkMPwxor0U9N5KDhUYVfH9o8CBhYfQaLjmNWnW+ouDUkxX1ge1q6p5KmFEAelND+XDZx964sXZZ2UxuedUTVXATkSMWM3LJ2o74oQSLpKNFyhDWbvLKT5KVnagDFMylISRTcZCdTd1rdsj2VZixI4imPWWRiLoyyD5zX0VYmSdLYgFU3f3AxJgbzM1R4dm0PdTtk7rbgaLiXiSTMoFQOCzwyXS4r7mpr9uDKriJcDOlvLQU0Zg+iaHvKJHREWUn7rKwdPqRznV5WReUtT+B7GzufhiyJAhMEEPou6qUf+a+IA8r9ZR4pKLnwMkUHMVZtKh2Y2QISs8+p9qawuGIVKnoxJE+Z5HA0mF2xXMC8staJuP4Nue6qYKbCMtyiSx5DJ3f+gMkD3L0jyuKXKjis90uOHYX+u4SjBEL6dnMPbdtY/ko5J+lrzysPvEUZFFxuWmxnhvLyw/VIBAbl55HALztqpHVGYH8d8VRnsBNLISVAxIxnQt5+UabElTBTbCQV4VYYLg2ZvgAN4rlzdBjALYLIT5Ek9eFVB0oCGC7NcALxWfpqxpWnH1j8wgH+ZN+S24xX3pObI+oPSTdt5qrPNBNLISHyF29/kLQ1f701JvUMFVwExz4+bckVQmKt12QdjJKr28X4UcyKXxVYCEif8K8p+bkK5r42viZn/CF52QaKLJLImYPioVVQs2ToM9rK29y6XI9B1TBTbC81csszXjwVIj96rqhmJyNM4UlC543U0u2XOM8OIPS2dZgXOmrGhSLSI2aRLlhWxzlKag6SzRQmdyz443UaJXgGthTIGxI+04sTJq2jryw41ucv6IKbgINZu9DMfHMZ06yBVGHiiYExxFZxVp3klABLXjm/MUzl55lFu4T17HnaoQ6IFq2vzVbVvqqBsXIpLoPIB+zVbfnLmI3CW2vz0PumQXfMbB7RrQ4RmtIey0WmxF8A+H6fSnzN6lfquA9yI/LBa7OcB9ovFhhQXcAFZOzij0Tn4ag6yApnj7iYfK2eMrORrgCHYFtox6WojtnH2aQ+8s+c19lWKmn3rsrwVraN94bTfET4VEqPYsd01Rlk+8OZms0XGVBF/+gGgvBsXAdPMY32TWBH92khNF/L52C60GrTWBEIZ/d8Thm623XkBwnUdPZzH1WLYQNtcO2L+LJ0l4c7ekAdeWeI8SkYe+koCmWKGS49HtSH0wzenAj0Dp4H2xxx5ltcaVlhrims1X3TUW77YscKmEUPBxSPeae2dwf4j5xWMsFus3RYKrga6APNzlZ9kPcZNOIckdQBVPaDVUwpd1QBVPaDVUwpd1QBVPaDVUwpd1QBd8l0e3c99vJyelIzpe9XuuK9j3pigz1uHCuGDont0G0ja95jL3w69bj+hktFBKT4xsnt4dHB3W1UAVX+Nqq9MBwle7Xn2XnVDB0bStQOze5NqOAKvhEfN3vDq9WIoyq+pYmqze8MSxV8Em4etQdX61+UwRhYC+RerO7nlAFn4CjJ70WKAMpSmDgivg27/Vt5upHsHXUa8ubEPlRbM04tS3JPQaq4C9i6dygTe+NY7pd11yp6s0Nn1IFfwnT4Mete4G9JIXma6d7Y7f8xrLzM5i6OGll754b9Kyp0L2p999RBR9LYuniQyv1S8CNCXsN3Rb0Pw+FKvg4EtNosX5TOh3P0LrKdY8BHg5V8DEQ/T62v8hEMdD17o1so9b+2/FzYP1KN6BfAj8O9ddT5sKvh9u4IT/BDemXwI0i41XptV/Dt3NLzktimLekXwI76Olv7a+Hb+umnIsbq38LmFvQ8O3dljNg3KR+Ccygq7+p3Tb36W7zxnwrpn4L4w/7YIc9/bXbPT2gS3G7t+absHS+5eO/n8EOu9prv7VzHFTBH4L1287546Pgxv7a6LfJTqma+Esn4JqxdbZ99jtfQnh01+yglb9VquC9YP0Ob8oG5kOkZ2sm9Vu4yTBV8B4cjbkj/RKUjvHewi4dVXAjrgatsl//FlBPWb+1LttUwQ14Wty/xQU5n8KOvRU3aJcm2pXaH8HXojas3zwP4rMxVXptmuGgCt4i0IKecnow7aWLmxKjFvUAqIJrhLrbu8l3tx4BM/KW4qA1xhKtSehPEK2m/Mvt7In3ZcRn9s0+PZifgdbBJbFuqy9tagGeD9TvLK1ROwaHaR2cE2tv6LlVXZizwj9J79alE3EQtA5OSUxDfmpHnfNTdOWl04Y3MbcgiT+A+eo/DqmA63CP/Jt76UR8DlUwgP3qTMbHPYxi95jTV0E1bU51b/ckqR478eZKf7Ja7d+dPow+iTD4+PqWf684DHei9Oou66XMDs5dcteOs/CHvYPr31gzIx6Bu0z3rY7CvArwIwyD8tOg227AkDBjXbcCnql8WztuilDUHXoPEt0wvYA7uDYxgtRiTkOcHjfYk4Wz6nhKRI6mIlOkDQxN+ZNvn2YFQYDclYqPIzeIOP9P/61TKQtWdXShXjab9L9FH89bWtMB6NF+e7eN/5mIkzMVMqfJb3HjJ3DJG82LS5bpYIRgXt00/N7bwUdOIMd/OMnSX/L7vw65kOn18elXrGp/woe5MwN4YzVRIHxPFGQZT8LmW5TgKqU+5RdMI1UMjN33Bfiwa9tJzkV6B6cgWL+ALTYkPvarRwmpsVzHx2nzZ/hr5QUEiYMfQOPse2QAu2sSgUbOXK1tMt+Q/g8wpEOcOtW93AzQyzwtDdEXnsrutZNqfqu3fd8KDjS/f9QEnA3PAP8YMZGqu5jICLx5+mb6ZwZ+B3b5tJPG8NtUYBn94qD3Z/Gy+YYFM/fqkybL6IXUMbvP6yU3aTynajauV01eIKn5nD84bFbGEhaeAN7ZzSMZTSCxBD/CQvZ9XA5mgzG0LCzn1Q5dQ/pPvguJQ347AYuj9yzr1/Ktm704wTN+8cmrtmkmiA1vbLhnBUeaW9uA/U3pwjx8Bkd7ngqh/YvTbJ8fSfghFtlokD6ZU9cJEhGu5pwOqV/EntXP/UsDZ7kJjY0hdDq4fJmO4bHlt9352tDt81nY0dIFacTiZ35ssiNp5fn/DJRo7rHDTjDDVaGlP2npOZG8i8jqkdZBP7teTTzE71l68oT9AvhfROpgfLeTSKjV0EuZ92Os4Djy3eYXerAP2vuedSorkkpIywplqchLDHCiuIqfImP4b8z9VTgqWEumgsVruY/wJ+z+ZU7s9fKZ+A5wGEjG6Z2hf6cuDTLPMq4n4n4VHBum+lxrenJWN7Yg5Ewea095wA2wPr/QJHygjo1FutGYrL3JDlIZZAEIqxCXXmKXFZc1T8rwQtsfQQDp81A2fK78tqvgAIq7OU0myXr6giOUxtr6uesQO7F39tlcdnhlJXCLEZOdUxcB70a41gyL69XE8y9ZcEViVjiVHOelTRxdQOD8XUQdzYNnsj22AZJkOH3NbCypvjAbNDUGjDSVWVm9ZqnISwzn5BEqv+YiY9OwL4elozwNs/jZfB90YYCL6JFFsJa73SCVv7Rw5Jg8bh6ydsUocVcvwHi1VNytgg1Dft7qv8nz2OEjR3Ue8XfyoOxDIun4rzIG/o9DbiLz4vrE8J3BslX8f3gh8OW0YfkP/kko47IONk3A9ySC9EYIuIYrv+2mJCqk5vtjGVdTvgDiE27gAMfgaLxASSQj4PvejJPU7BwoS3NoSmnyi+ubxNtlV50llTBL6qyxyLAk6kAneXv8v+y6OxVeyjJwExftmdCR+bnX8P6uLJVpWZWpzEoM50SC3qJwWGQs8Xs9XBBFsWasuTHqCZkq0zo2WSCydA/Ix4yJOuWjwbSSOHqLUb0SvlMFW7qwu4JeBsfqhA6LpLxcDC2rVPEHD35aDYXOyF8PoiWp5oZ9L1LS/hfz362gOv13jwTikio3Ao4tv+2mhYMwq8ZD0nETyZGw6a9E+MYBj+uk0T/+vwovSLEG9qh2fZP4TTWfJh6pcyeth/FvKJwq1WeAOBEj/PtR/C4DjjtYPEfNlTBwT8vpeGe8pkglt0lFXmJhvQtaZAyyp01RrBlYj74XM66Mw++Uqcuyr3YiduNUxm0KxDAQ1haS3KWCvTVqWsHJiLY79Bfla+W85VjV19n3ALIbGNuS5w4CPyKHzGYgYJEOSzJF248RRnO9JyCH1IIWiEz5bTdaARlZjCwEAvhQVwoL/WxsbCEE67L2Uc0FKLXrm8QLaP1QC2KcVvzuGpxZrzZ4ijpmasBjOz139tAJ/+ztoaGxMf1o0WueiqLEePIzTCpXi4x5cr1YU94DmfMN0iFm3fydlEou0r9/EQFzmZ5ZduaQwwT1K77vUMHhOtwzgNZZcTybOE/5YQSsZycJAtfnNJTrOiHtSRvsrl2+w5YrC1R8LMNS7JUkDFZ6F9f3XQ4233Zg+uv5gPWcjsgZApjcRuQcrptEQeOkgGE1/5c7F9X0HI6Is/INgIvrlcTH9YYi4E4W+SMA/7CdbzX7dfwfoEcZxh/tx9oV5oP9I2N5KooS41mdDVfkAhm1QWXGeJOXvaQo1hzP/YuUyh/S4JWzzK/rkyGbUh2QJoU7r168OwXHmtPbZ0Aprzq49gyLTkZHmqK+//oL0HuMJnnVyKiuqZsPcySUfZH0OedlLbvNBk5jb/arh1YrQAOs8M23XfpobQGSEDzO/gCf3y4STtf9e6w+zN+BmTDrJ1bxFoKQnsPSWxejgNl1uZr4JPtxMcVvQc1qNNyd21ssqbRFH/YjPs6inXWgZW6zVJQlNly8cV1SIjau5Lm/iow9zubAvhSOcji0VrnITZtSDLMp0RQzLXau+O2EJIV1eaPtFtyNYxid/uEGaLi6SwD9kUZR8WgPcPeED4eCbdefuHZe7w22Aw/LSiJsri5+k9ZtmGTtj3hrmj+NN4nZnXNVtq5DkD+ihfz3ssy7dpO8uR2Jv5/rSSmOY0/+s3+HongmfPA+5iwVWYntprLIWPq3dJSnV/cjpvZ2j2Xx4uc8J1xe56yznNSGye9LwbYm9I9/6mAFnzFNv/91ehg/RLKA63uB6T21Ivw1fGkLnlt7f9WXQZPVdHJtJnz3c3NCzf/i9nbt2wXkbAyN6bXtI3cvCk40uzs+PZi7p8vNJte1ldydKNjU5WdqCv0dyGh+XRK+CwU7GnfDe1j/MNJ4Pr6m7STu4MYG6/jO9vA7L+JkPryiPbluXsGx5hxnAkz5DOFhllzPtly3rmDDUOgeEN8N/zhLrqZWuG0Fu2vaAD4H3MMMrkXCt3x/w1VEG8DnAUsYXUlD4nYVnGh2j+6Bdi6IhK+jO3ezCjb1Dh0BPiPcZIauYrv3G1Wwt2Zu/C1wF4d/mF3FuPBNKjjUgvt8i8CPwk+uYmrjBh+0iTYVnqmAz48wXvinh3Iqt6dg6y1+ptZkP4I4moenh3Iit9aK8NdwbeZ/N4w0mD1dug68LQXHa6+9r7huI51o9njhKc9L/4K+FeONfaYC/lG64vz0QE7ihhTsvnmPR6zipHwLA2Z5eiCncDMKDufrwaRlbaKw4VvrGEfr0wM5gXYruNyFLNGm4vNVTBEdg1tsTeJd8d7vnzLxzNMD+TqtVrBdVF32W9TGEbTOPJOwtWhz6x1NjEv+ANus4HCVLdgKpsa4DW/d2YGRZkTC1kpuY+pL2PEyOD2Ur9Liokum6dYF8WqmPF3V2sPD6QOWsLVq+3p+YTCPTw/li7RYwYuYzMqbb+jlWoytj4YTYAYrRmpZD3SHjnK5MbX2KthyWQG8d+dx0OIRtHT0D7W8Csb0uIuNqbVWwWRzT3ax7D20uv4ir6FimSuw8DqVUZjvyv7jnbq2KjiZkbdAhK3f0qxHNlm/dCK+g4mVbflq/3TEbVXAMn2nFNlnVFFa2o1LUdYQt7YZX4WZzNIXMjgnh3QkLVWw7TCQJCwnSG1/AqtG+1vBKfxg/oTAT/wfrlAKBSfGpUvgGCKLYVmWwd0gzzved3KI7H+qQBIU66eHcgDiuX/rHX8xAQ+CCynYUNrUIo5Os+Bx7M/nwH6sQH7qKXJIpk9jMNP6Pvg/3CgqblPSJgEDe9oAmnzAFNKPFchPNYMOyfTX8UjoY9sJ4acXHrW0HXwiLR5BvtJMC8aU4Xl1HcNPTzDfp4Ip3w3qKWvHBZQA88NduVY1HihXDDt+EhB5C+IPV8JUwZTvgn8cc8zO+xjPDVUw5fuQnvsM+uGuHG0HU6qYp5pJKp6v/VBHOXvTJ1UwpcKqe/rr4iL0Qw92xyMjkbQVQdlgfoOAgf0pTcmpHRxVMGVDfG0v7DyADxTslqaecRRWlqMm0aUTfSH2FEgUJJdO2T3T0A5OG/OMChYUy9fXSWddGLGuPa87PDDwG+HDAtGNCNDoJgwk20mDgkNcx3qcABELwRQfK4PqVbN1uzKczIcFEvYUf7Fs92LjVtOgYHx/kj/SGgIWuEeAJfKjyv35F/x96TT/NB8WyAhAUjX//n7X10LzaJoGSg8WAIjHlUw8j9q8Ice38HGBMPdpKnQdNCpYMx6DGQTpbXLj/nB5712VjwsksVGbFzq1nAYFu1owFqM+ZIsUdBUldy7gzwpE8x5oHXwxdhXsTrsPDLBytupU8ydz52re33gRPisQTR/Tt3Zcjl0FS784ywpjhudkSHR9zEyS1aVTeVE+KRBdG9/0D9wtxxDjJHHzMcQkBO5KnjsNrQhO17oqF/o6D4b+IAFC6L6bER8WiLvu36aAPxoF19f40ug6+vdNPTmHDHgKndBVu/LOW1V8gMhn7ssg6KMCWQFjAsgtnI79hI9GwbmRHCy061WwrLECG/rOKB082uINwLbV0aXT/aN8VCARkCbF4+0p+KNRcKxdVjaTq2hHNCm4x9oWbvZNyv6JApun5n8uneIL8FGB/OvSiTsjH46C+ycuGP8uGpsDylbLTrztG/U591kgH4yCh54TTC6dvoz7atBSDufDUfCV82M7tXwGVTClkY9HwR8id/X66yqaEc0KTgLm81aOkfQ+ubDXxXdw1sDPnOSttFcP45C7Cju3T0bBWSXUvKswZ2pSsL8ky025X594tfdJqLxQdfGeLsL+9/f9bOvRh3/4l3OVEU46EpVvGPWtFcw0edrNir8IcCt7fA1Pxs+mBRi43LszauncPeW/oZEUu0dtZWyGg09cJOzgSB9HYUJwtr1iErbv2S6/N/ivZaUpOP8dTQRv+fZyDYNzH4yC24zgG1diztSgYA3wLw6Eo57Rzuc3kVGO9XEMFkqss5Uoo6qctv8H8rWsNPnRkycBODTXr2EVzAej4I6FC+UqnhRNCo4csWjfJGs74ocSLBJZj9Te0hGGAj6Q9EjO39+WrOxEHaC1m7zyE9P2ULdPTps7LorQZ+wIomlPyXwURzhQUVf6Vae+5pLAKi6yaKvRT5kJgLec8OBGo7WNvWa+yvOLpKNFyhASzQ7Z/im7/XNkRWGRlyKjWbBhmpWirLKspPIsk1pmolowi2RSZBJXamkyY1siP5MObw6hSPrl+GAUfDwImeu1iwjLeXBYWopoTJ/E0He7nu4wfU2fQOh7imfz/cyF0490ris7cZcFn1NtTcEhBtqOi03omCCGzEdxhAN1VanmdJmMycrZioss2mr0gh5yuBvEkyq445u4Z5H5Ks/jGkSJjI6IMzIMT3kyRwaSN3kpMpoFy6dZKcoqy0qW1yKpZSaqBRMmZSYhWKTJDPKiF4OILZJ+QW18MArOXkMzJ6NJwcW5yJLH0PmtPwA8Cp7LTVC6YzrzxMGr089cdLvg2F2RC9V0wQ1vE+ulBhf4Lv0P/3bLMsl8VHgUNk7TZPBy/UdeBFoJXNGtfky+JnaHUUxLyn2V5yF5kKN/XM7ixqcUUvjmoxHa5CXPaJQHS7JSKSucla0k7+ShDDjPZJZMMYRMGPinzRbnfloQrWNXwWzZx0yrBIbHvWPci0GgoGxtPov9iGbMpC5sF8JyF23by1503OiCxXeb35sOHEM9sK72p6dWNZwHWg2cF6y+lSikMy8GDGsPmcxXeR4EmaQ5gNMMeJEQ8J1KXvKMVoPdlBVsunyVpDafKDKZJbMs+oQc5econ8A1nCm2bkvS5V/ow0ET0mbOdZZMA0V297pgPh89rAQGfV5beZ9NXCor1yRmYRakL+RzlNxXcb7ktDYbO+JXer9M4Cajm2APKatdtjLJ5TuX+nSi6XAa6mDe6mV1JQ+eCnHjMlyfYTIXcd4WiMk2ViM1WiX7XEDp0C6+VI52nXbkhR0zNRdFoOVfZbUKcKcpdHvYZ/xmKbmv/PwmZOfEgY+urclCkcAio2Ww8QdllSa1moki7dVMFjdDsAbkkeHKV9JLSjlg6mglbllaelYkH9xxziLweF/8SrZ3H1RoAO9WGJhvwEm24S9hOyW+Fmp+dpJV3LXvWfiRFzm4S+I7i7QWaXKRI/r2Op3bIT42RzWni39C0muKyK3duCgCrQQOTCdgJaIOleM4QXLD3Fd+vgy5E8xd66R39Y3QIikSWGS0CJZkpbGsiqSWmaimnZAnt2SYvHuxM0XfOtD4Nf7+X8Yr2J8XnLG1Z7D/Hkto9jutzYz/xdP3D32nEbgz8KZfWUnR8LiSH5cLXKngPsR4scKC7kB9MoZ1NOjgZyo5OwRdB0kBxZ6JTx3TVGWT7wLf4CJHdefswwx/SX2UR2kMhdOIbGvlmsCPmI37Mtpq9DgU3DoHK3u1turaua/8fJnwES4ldNIkNN9fr4d5AsUio3mwaVaKsqoWVpHUMhPVgiH/8uSWXsSnBb7ZwiMPcOl1MeMEPBM/jL/UFvdggO+I45J+gi0xZcdg/9xPsuwwqm7W363X4Hz3FPpv9ler3d8o5LOUx/GOfcR78lzdXzOJMhch/kPOhxx+ZjJNLnLC4kdDfGyOdpwm+TZ0uYsi2nr05vJX/TeY+9o5H8fVY+NzNWuNTvIE5hktg02zslNWlaRmmdgqmEomG4r+2zkg03pNQdbiScyy8Zm//21tRKZp/0YQ/9PBnffon9HmoTMLm6b+SQTm6hcLa/OvWgk2OK+dSvsmzV2GcriPaSzNWh8pD4HLz3Npy6TJBWwcbr7Ur1WdInbLfRloJXBd2spA7mvnPPM9ssgTyBbpyoOtfm8sqczbVsFUMlnxcT0jrSXZfEs2VbMzq7N0oNR9Pq2jG/DKPjGyPUK4hddJJ2+Ib84ncz/lBM9mBow8RnG+Vd3NRneymZ76PFkaPaSnaok7vtMrXubxVkRbiz7gm7sL+85fJsltJ59vyaZqdmd1VKl8KWk+rSN5Tg//Wju20yGNiHTyhvhmYzL3U07wlDNgWPoeeUBw4GQKzmZ66vNkafQoPVXjeAVfqJcx2Pqbwj80O953/jJJbjvl3Mqj0DCr0xmBnG+lV07rCA7phsiM1Qm9ci4J+3Zr01jlDFg5jcZl8wn5TM/WPFk6U7Q9EUYt3CmfU8ytkKma7VmdkNjYFY3XyrQOAXXM2C5nfXZt+8oZsLSHQP6w+Xh6dTqrdLXHOLCxeejtGZg3vv4O6xO8HhS4e9YnNimQ2A83cTRnZ1UZd/o4wwcWx8ref+KAIM5SKGK3OxxAMl0RSxFIqi37fFqnjBW3Iez9+9IWQRBYSLdVL2xX+hNmtdh11UhTHezOfzUbpJeW2MbqefODqJhqp8bgrNrQ6bW/YUXFR4F72uP5ZgFIgVhLfH8eikHm5uwYsBnY/zjDBxZHNcDyRH5r9gaxuR9nKJTtWR0BPAXC8mJ9Wkdi9bBukBIXn3Z9BoxFJIwkKswONtNZ9Xky2DWrb1AwGZrbMUj/gGrlTozBnbVwnuUnHwW+M5b4nfHiAknW/BBZVzFVtv/WZIOlm/txhkJhFWvdSUKFzOoYuNnAiBbHaPjCwn3iOMkWRL0yrdMx6vWn4DgiA6Jl+zpAEQQBycSUISDjFjgcWMo8mekhzquuyhCqNCjYisgIB3PwqppaeTKqaqzOtYDqg8DZrqaeS1+kQNxoIMJ1LErYf2syU/vN/fieQilaBdnf7VmdwWyNhivIpqHKaZ0cxZAY2EzgZHM/xQRPGQS51Ht3JVhLQm06qzZPlkWfhVClScHSpmlTt9smZzT7gdu+NKnZY+MHzeZi+aUMMTM2bzSerx9tnFRsvauB18IuxxK/H1IgLHlabpJfjJGWRu/lsOjGmn1zMRtJLc3ZUzJb9zILFUv+3Ci/CHAnhgrFWGl1/DS9H3kyTyuU3D74kWjwP9lfsjRjlM7qTPJZHfEvfKjmV9nHbFqnn2dT+E8ZQnqdeyEtXeEX9vefTRDkkjBavjjRU+7uVzbTkzqvuipCqLLbzk68ig3k0pBGMMUNr9DRFKIVS+ty25dCcnkhivnsuAXK5mL5pQxRFx96bHk+dFYdQX+P+uS5Uj/aONmEXQu8FjYHzpkEnBaIIJizaJN8CPI0Le1+1zDJSGhnlCaAbDeWWrPD5mJWeMtwPChr8UDrCLZRKcTCW5HbMsCdGCAJgjwCnxtLGjEiSYOWObarZPcjT+ZZCgVlqzPIa+O4zWGhp0+WbmSXcwkVQRAUJQoeSjWW01mo7qoMoXK8E0c+NJcapEt1u20NvEU3b+VUTbphM2YYr51AFsqLW66It9QqfI/xfPVo46Qcj6wHPqqHXYwlfjtZgTzOnT/jTmHUXqSpGB6tDYtu8lqa7JOhzJrNfmbrrm4XD2yM8vMAG2IIX4u1AsVYaXX8tFLKZyyU76cP/S/521VwMTRHDNK37bajmVi0srbG/ooxw8jmlO7m4par0ip8j/F89WjjpLT1rge+FTZ7rsXfWYEwj+ZqPmaL4c08TcXwaG1YdJPXwmQ/LbyazX5m6+5vF08ZchlgQwxkI76sjirGSreXA5S292crlOuhaY1GOjSXGqT723bbKIhyH3tMurNdG8qLDa7QB76rNDipB77lIDxXPysvEFDFV7O/I9Pc6B0azRmqJvtNNvv7i6ES4E4MpfVIaWq/GzQ6c6FcD7vt4GxoLoMHjwzllw1j9iGZxs2XapQXd1zxWcvsQ9+fRFBcqDvYjCV+N2WB8FzIbzUsyfCoqipkWDSf769YsxcXCzrPHbsqVp8RNlmoWfLDJsCGGEocbzDspAuI86DjMlnOmQvletitg7Ohufzq9gCf8DCdPZIKYfdSLdTiYs0VGepjO/ZcjVDnI9974t6+UHcQwLk2ZE4LxPY7rB0OiuQX14rh0XJYtBjsrF7M3eaDnOmAJ/iaYvm9TRYq3lLKAHdjgJhUu0Sa5VhpZfw0awLmyTxfoVwPDaNp6dBcTt1uG39I48X8Ie3c1C5t22OXF6tG8ulQX24V3mw8XztqdlK5UEvBWjrbAzMtEEMHVu0WyS/TVFjlF8OipTV71bo/c50PcqalUNi6l1mo2/pjygB3YoCIbKk+wQ7LEdXq+GkaQp7MMxbK1dBk4W5pL5vW3q6Nu+8lOvfceKlCeXHHVW4V/qHvfXFvXdg4sLWnIyyAj7RwTwskjIVq8ktyo/eNcX7VaL9m3V8xZ9/YupdZCLcqk43XnRg2FKb25XKA0glJZr1QjrZwv3r2WrgrYbQ5vWu3Ha2QOGq+VKG8uOMqP3GAzfleJ8WFjYPKWOL3kxZIUSTbicr7VahuuV+/mB9sensbW/cyuO07sfG6E8OGwtR+dzkACfeshXItNFpXfjwwJ1/jiwi+NpZ4HaFfT7LbaJJ/Bz9SysHw55rXPAtxdX6PQsF0vHOacX8zKDMToQqmVBAP3qdNt56vwtaUKpjyFdw1f8YVBUdBFUw5mmgVDq9mU02qYMqx6Gb3St4lR6AKphyHuxKermlXFqpgyjGE63B0NQ2IlELBKL6jkWHngN/tzRXIIZn+jMS4qgZESmEXkRhtnI/5IuIBK1FvrkC+4S2yzlrsX1MDIqVQcEtJ3jrdlteVv9vyjvFwFQ+uqwGR0vJ2MHrS3tTulYxM3jSJbvWuYTvFHQ7f2OQ6QbLsrK/k7ZJfQ2/F66GdOTO5wgoYWl8HkxyMAs3ofcNbjyl7wQ2I8bXWEu1XMAA/8TWjf679TiiJZl9nAyLlFhRMlu95mt4/015X946tSc9X3Fu+DQUDiI/Omu1f65OuxQQruNoGRMqtKBhAlu0F37/91eU/Cm5A9K+8i3E7CgbodMyZ1LulHF0aS5OvuQGRclv3W1XMqdy7ummjloIbEA/X/0y7LQUD6qr6u9L2abqrINacQRv2S7m5e436z8mbfmM2DRfAekMvbRDwrdXBBGbQ1V677dq749rwV0wLGhApN6hgAHYUaq90mu7LxJrbb0X9S7hJBeNsjYO10WvNXbguTF25knXIh3CjCiav9fQ0g07THQ9uQDy2SRVtSuuRiI+upvev06DqaonX3qBdFiY3rGAASbJX7KAlPZKrwDDa1IBIuWkFk2k6ay70bzyT34a34lrVgEhpXYKPRVGMqXR9q7uukGjtD1vYb7h5BQN0VeO907u5qZvvxjDU8emh/Dx3oGBAPVV/U3ota9/9LO6av6p9TA7nHhScTtPpr92zvXi59UTrYNDCBkTKfSgYgB1iDV/xWpmLopvdVjYgUu5FwfmK0PZMlv4czvq6NkI7kvtRcLYiVKcrQutc30ZoR3JPCiYrQl1Nv8aNZy7F1e5jcjj3pWAyTees6IrQAmctXv0qos+4NwWTFaEWXRGacs37mBzO/SmYTNPRFaE30YBIuc/7SFeEXvs+Jodznwq++xWhV7+PyeHc7T08aUVoHOBWZPSpK/f0ZJKYvj2qZD1THj8R8M9l8UTavvvqCSCp46xh79CabrtusOcZZU0H8BbJoMeNPcJYMyMegbvsEYvF1dpwmP09R733WUwkqj0xHRdVEeacnwjfmMWDIz4Ld9qKyPhwRagBQhRGH9tr2WLT/Ej8h5Ms/SV9vAXv/R4X+cv4g07T12M6OiqMvy73MfmeLB4a8Zm4awV/vCJUwnd2aXAfvSD+ufGsTc7/Y8Qh/h6wPQSs4PofpeLLMR0fVX0Z8rdk8bCIz8adK/izFaEj1+hDNPfYYQc02+dHEjniiuflVOzjf7HJZueJKyCvrMYkSET4lnac3zwXBdynbwD6UkxHR2Vpne1VRCdn8fA8noW7V/AnK0I7Wsy8s8/mEt+4Pr/QJJgmj7DMr4a4+EJHGmvrZyhcAcjam+wglUEWPhgPw4jhDhi4+0pMR0bVvAz55CwensdzQBUMH64IZSDxAiWRjIDvQyLp4PtjCXqLihPxCQINClfEz4vrk/cOMwKsjMKV8PxJIr4S01FRxWuv0TTvxCwek8dzQBWcsndFaIBYD0wL+IA3tAT3XELYHoYS0odqlLsiZ0Jn5K8H0fIFhsODk/ClmI6Iau8+Jidm8Zg8ngOq4JzmFaGxJQMLfdLJ9pZjVV8DD6EADcPIuavMly157iDwIxacaRpkrI4+if9rMR0clbfet4/JyVk8PI9ngSq4ZHtFaBwm/hqNQBQ0TgqYCFjPThKe1dlwtes7d5X9BBIvJP11uwsJ/4ucWH04dXJCTIdFFWnN+5h8SxYPy+PZoAregHqqUVkRatvAywOs6If5OzCTjjRFff/113DxxnX1LZ9QuMp0wqiuqZsPc6RCko4xhR92c06I6aCoDEMdom+OuBLvQXk8311r91tpv51IdxpWhCYxuTsx7vOk7cE9typzhQlwL4gPh4JtT7y8LlP27gZbfyvtkTEdEpW34oafi+vrWYTP83hOqIK3CXX3R60Oz/xe5TYvQz4I2orY5rZWhOpmO/cxORyq4F1uZ0Voe/cxORyq4CaEB2/d/neEhutwePurWqmCmxGf2v6O0MQwu5exVPhZqIL30fJ3hDpr8eYbEClUwftp8TtCw1Xc7n1MDqeV9+fHaOmK0FtZhnwQVMEfkq4Ibdvmw7bW/n1MDocq+BNQv6u/qd32bNwarG9hH5PDoQr+lFa9IzTR7P59vQqSKvgA2vOOUHst31EDIoUq+CDa8Y7QYAVteRvy90EVfCDX/47Q+2tApFAFH8yVvyPU0jov7elvfh9UwUdwxe8IrexjcmdQBR/Flb4jtL6PyX1xdTfj2rnGd4Q27GNyP1AFH821vSO0eR+Tu+GOs/5ltlaEXpZ9+5jcDddTlbQJpv8UvxqXWV1ex3xjn+9awLQO/iLf8o7QkNv9dhT79zG5H+49/19ne0VocnyrwuXz0WUvOOa34ORL+PbtY3Jf0FbE1+EnY+vNKY5C++gAOgsv/estjmkI2HnrxXjnnqmAqYJPQngY6NNMhcCvwmO9M8KMePZm8hG3IVilsvXevMcr6Uxeljt+j8a3wKnM2uHT4WHLUY5VlGA6oi3OYHy4gpN3UcENiJU5vKIBvUtCFXwqvAprj2jYjsJjH+qMHTtgI+mIVvAi6PFgLOUJ7cFkUAWfjtCNVwHPhH5y9Ot8ONJ4ZgaHq9EyYeTPkwltABfQX/J3oCrGVBZQshaOLE+JibGCD7d3C1cgLG98I7QjoXXwt4BEJTBQzNlHN4U9YHsHL2tLpgkk8phWOxWogr+HwA2jGGLWP/LxLhqADt+ab+ljFUce2U6dkkN/zqcTuU7xctYgso+c4+1Yh3fj7HTwOQzxH1ES72lB8gfQ/YO/hh3UmwsxJomTJAH1uEGuxDh4JX9sAmIYBjEINcXB3M0mJzVoHfwlPOjtuxQfOUwrHtyPi/sfXo5Wl32p0IWgo+Jfwt3f3D22RA8fiPgkZLZrXrBELgZV8O3AxpdOwSWgCqa0G6pgSruhCqa0G6pgSruhCqa0G6pgSruhCqa0G6pgSruhs8rfRGRXXijuxQeYqCUBwzabRFj4X4eugTsMquCT0UNAQ4hWxLDGtiKhI4GzbWT57hETYiX7wqo9AJ9YSgL3K3fgL/DHMLdcJyZoMlXwYVAFnwyzMdY1l13Fm052DSwTtu/ZLi+kX5y1IPlvaCTFbrlCnyNWOekaJddITyxApSuJDoEq+ETsdLH9iksNdCx1CJ3IajARZlSV03wh/WKsPEmDB1zhCpmFW/KndPfE8Sr4GnQluM/tgI+GKvhE2ExoWY+Y8xOUBBXT82TpQNdWUqFyRbeZhzhyxMpaN/QXRG4EjETuhuv5wQQtHFE8esXSXUIVfCKioFkgDRkfTKT23/8RfFQx411aimwEEfkaGShvFlighFBfrGkueR7CZW8AECY9iYG/HNe/T4v1Y6EKPhXdHLDr+SOuPEHlfvm+KqNyU8vIkscg/42/hW8+GpE6NV47gSxYWwWvdUk72Fj1Ef495JsAwX1arB8LVfCpOIoK8QKLdoIMBxJI1jEUtWdAqtq0LYCEgE+bx5HNKV1goW7LyzsCjwKbQ1n/TY/JAlw6Vn8IVMGnwnkJ8plUpgKgSHtgGVbLryEomrLsiF/ppHnBv6SewK8FMtH0AASydw8bvePmM+CPB7qU8xCogk8Ft32ZKFswL4rga2Kl6hQYV03nJzBdW5NLTbK81auONTBDWEWT9Cv/lJ16u3TGWgJV8Knwf3nxvq16kGy9MUFeEY/eFuX7WtBg9j4UE898JkdOuoNlvCahKcFbNr7M0pGIg6AKPhn0wR5QQ8Zj+++ZFvn+el32zeTH5QKfqizz3IQyztzTZvBBUAV/L0w6iIuK+hO3DnArgoe0qu31IPtCkH5FIZ+LVK7OviExb0WLdDulQ6A7nnwJrXeYu3UoBjb69UMNAuPAVN0UtA4+K3xook6ftmjPCFXwWVHu8GXzPwztLlDaDVXwWTD0z93EfrjnnYqrndcieXu243Gv4a2Ml4W2Is6CnXzaqbKWCaCHxpE4AzrT5Klywp3/ag7E0x7vvZFNFfxNmOHhg1/EbbLmh8hC+zzWZpSTZWfPs1LVze6lc35haCvim3Cc49y6UVcUhuI+j4OqrK1o376rbFe793YErYNPxddc1O2v3eSVn8zYEUTTHhmAMPVIHmX1Q2nmnqzsRB2g1G0PvHScYstj6hafXiQTXE3bEU/WzlkSS86IutLPQtgEqerunS9Gogo+lWUyjlmQnbjLQvpWz4D0ugJN8Ww+qzpLM/el0490rpu6FQQzGmFhbnlcWqpEeoGkk4f9icb0SUw80qgOfVeV8hAyZyRIrnzL8r1CWxGnEjKyKoPIMbWVmczT4InP2gfEzL3zmH5RugPezt0+Ss4fG7Y8RlZn1HmC0p/6BDqWd1bPPA74PISKM+7od+HeGLQOPpWu9qen7gwIsLhgRTN9I0Fp5h6A7WLB5y6YR3M1H2/NeISkC5cHlvpj+ADirJ7hhTKEirP73Pa6AlXwqfR5beVNPnCwMXMHUYLNAajiq7ml4KTynq0kdYpifCqCeggVZ+G928FTBZ9MR17YuLJN60L8sZmN8Jm07izN3HmI8+VHeb3Jp02AqkeB9O/ydgEPngqxLwGLipZCEcLGWRLd+6J8quBTWco8aSwIjiMyomX72WycrymWjztgC/eJK8zcWcVad5JQSd26foe1yVBwzSP+wjG5dSUn2YKogwpILpYkFSFsnAVw5Pvrbg76Ts8v4W1M01emgYYCcK7hqaxrhhNbFsFiwrXXGSIwgy4jJRE3sCQJpMg0rURJ3Ypr0wiUPtryyNuOO3AF2U5UkHzTDAe40uV0kQNypgwhdeaIMiy4yuSff/h7kW4Hah/8Jar2wUmcNUpDsi4ozB5quPMV1V5baC3SvaiSKFs8RNyGcd6ErXosXWTBxNmBpb0UJ/Pr5M//ukNbe6rEQu2DKV8B5b0qrvxIByk3XbLUzJ1Nh8wQt3FbFn3VY+kiCyaXpxJGW9fzP8HD3Q+HUgWfn28wc2+cVZZ56B8b0O1BFXx+zmXm/nDpjF0Fd/8QorQcqmBKu6EKprQbqmBKu6EKprQbqmBKu6EKprQbquAvga7RKte5y8H9u8z06XSNK1xgKd2jYQ9V8NdA32dD492l7r4P2oq4NNalE9ByqIIvzb2v1DwVquBLQxV8GlTBl+YKu4Stgir4wsR3v1z+RKiCL0zMUQWfBFXwhaEKPhGq4AtDFXwiVMEXhraDT4Qq+MLQOvhEqIIvTMzR4bSToAq+NIgq+CSoginthiqY0m6oginthiqY0m6oginthiqY0m6oginthir4ssQMMHRS7hSogi8LVfCpUAVfFqrgU6EKvixUwadCFXxZqIJPhSr4ssToOnewag9UwZeF1MHUOO0UqIIvDKLmladBFUxpN1TBlHZDFUxpN1TBlHZDFUxpN1TBlHZDFUxpN1TBFyUm5U+nlU+BKviiUAWfDFXwRaEKPhmq4ItCFXwyVMEXhSr4ZKiCL0qM4EpfENoaqIIvSlYHU+O0E6AKvixpHUwVfAJUwZR2QxVMaTdUwZR2QxVMaTdUwZR2QxVMaTdUwZR2QxV8SeKs+Om08glQBV8SquDToQq+JFTBp0MVfEmogk+HKviSUAWfDlXwJUmNK6mCT4Iq+JLkdTA1TjsBquCLktXBVMEnQBVMaTdUwZR2w106AVeMfvYYUPH3TFG5Y/bsebg0VMEf0PupiLpnCjeeT25ewrQVccswD/Po0mk4ex4vnQDKObkDCVMF3za3L2Gq4Bvn5iVMFXzr3LqEqYJPJ/ng6Aq4cQlTBZ9MMK0druz8SxSGYRxgsJP3SybwtiVMx4NPJZmqtePO9IVPv8wCBL0VC8wviP2LJpF5mN3uuDBV8KnoSX3iQ5JXj9m3oQqW8HLJtJVzfeLi8ZLpOCdUwZ/xpnRhFj2DrT9PhcD5xWm2zw9lmAqhgwYqmF1U99B9j3YqvDhrRoiDn037Zq7v/DPkl4Iq+DM4qxvbEPAWD6GjPGBx9vmFLuMDdWQsOnHIb3ngwVWyb3EYk44dAuE5O3Gzj/ILQhX8GfI8tvnIZR38HJYm+EQfYtHAf5UxCH9cZqcIWRRmX5YrTgp/A/qX5ZXX+pfOzs1BFfwZMjh2J3QYJJLqFWNoSTqCw5DS86W9Hkm9u/hFqt1NIdOhn2+HKvgzGNFyB8ECOnlz112OVX2dfQ+A5SAQ6h6iZFOoVpcoWETrm+1IXRyq4E+R15zAJs5TfhgD49pJzIDrszrqsHwAsIom6b/0w8fVNsYhTYfExLUu273waNpNQxX8KfK6A6wQisWhNEN9//UvQO8xmrDQX/YYF7d8yb/0Q+vm6zfxv0H+JcmmOVjxC/FTPgT999IpuF70PRbuEYNwRfxHGoUsUek6GlevmvYD2vYR5K0O4WIdOf3HrPV/GloHHw9b9MiywhvUx1qTyY6AgX+4dJpvF6rgr9Iti65evZ1rxRClGargr0KVeh3QAUpKu6EKbi9Xbpf8Q9BWxHcTG37E8crhBauRff8k+eiIguVT9XAldQCifOCZ448Orq1QBX8fHojEmD1WxcDguMarTefMRCTzJAeEXaPRLjnQ0u+BOrx0YfwYVMHfx4rDKlvGqYF70ny18VxqL3RI2DUa7ZKlzAhucemi+EGogj8lWrox9wsRO2HdfobUSHiZWgqHC48ZdfCZxGRH0srz/xkIbj99gCMIlx5II7Z2Vcl8+LNuDyz9UU/PZbEU59g80Hqsue8ySBx7o12yt0q/hsqlC+3noAr+jOQ96ssBVkVEDH4hMxLOLIWn7LO57OAz0lhbP3cdrs+VT/tkloxhPX2pXc19CMpK4BZDNjsHoQlIKc7lTrZiTV1WglQekka75CjK5k7uyBCZKvgz/KDXg6pa0oc++fACJRENXwDxEeEGKMcwIm7P5uLx/bQi9cTq1cLHwJ9xUjc7h1VqAatAfi53ktRjzXyXQeLY3T12yVnFfEcDE1TBnxHClgkwX3yEYFrAhwIIqNyFkoNQyL1hcQqAq9DK1dLH8I//Vxlg3g7OzuVOEpAaUlIEuXekgWHm4HMMsPdjzUkV/BkceOVIV7h1pa9uu+aR0Um/sIAr52DrcV76wO1obbzlNTuXO/EaYq0F2WyXLL0A/O9h68JtQxX8GQJvcrInI871LYutX9FYKWDKIuS8mGF72nzAuq4scqaYGNxGTORq4WPt/3LnQjc9hx/5WKKIzc/lTrZjJS5rQXJNdslVm+RLl9tPwf7w6tk24aWdMiS5pu6pDGMaSA56YHC4ckw/kOzqmi3y5MBz+8Baa06QGAOfRB1OsjSTeeCgelXMfESLB0GItQ6b+jA8wzBcPj/HZU6ErVhT39UggdG6aO33gfxLPxadThgjhCQGfzBCU15uEWofvJ/SpjZCZPI9SXan4OOkVitnBsNhkrVTo602RHq10UdjoFuxpi6rQR5gl9yQl5uDtiIOIFMNahDIlqa5yufukBa310djoFuxcttBHmCXfA9QBbcXapdMoLZplHZDFXwobmX/Rz8ov8bOXlcVIgc+JSabXH64yWS8P5RKBO4Nb1TZAB2L2E/Wf49tsoMqy0x50j+LEYSAlomYZIC/yB7msWZEHAOZK4jCJG3AJkGEQchbZq40x3X9PU03czqA90gGPdqdsIjcGHfk/Fm+UlRfW5Y4tywQwAvCMERMEQFm1mRaebtjEbQd/BlkoXzs/js/+Ptf7GxA5hr8Kdk5Aoq5r/iVkR39JetpWeuQC5len5zmsfAmXFFnm4kYhWt19FGnyxY726e8KRfxjxtPIhcvmC7WOo4LB+0+ktsY/kMujVW4L6iCP4N9ADDIwJaPqlbo4r9hAWOsnuzQTp4RvFo90O2xtxjLTOItgNSYzwz89u3ywS5NIFkZ3Eer7p93Ty27g/jV3PTVRAgQ2wEDp2qEmx6v+Qz0f+5xNIIq+AC8NbFc8CIJRRCzUSHHanuzNKiRReR0FDIR0rMKncqSs6w4HblGH3IjynwrV3xUPvunYr8wycxdRf4EcBVfHW3wiesgvXvJXL1H5RZQBX9KousTUvt28cPdB58hK3l0a8InHsRlR1jW3mWPmOXyEgirEBdrbJXTYua8Phki6zGTW2bmW7nOkgdUiJx4zk0yc1PLCGG9CuUGmLiehyh+wz+htTUBe82P6gleLJ/uZ5ERVfCn6Jr4VGrRZXVidQNKlwWDRVq5mId5dnz1wYwk/EBX/H94IfDlAamaceM0VsbOqhokA0lhZ5lt5er7Yxm6lXo6N8nMXbEJVnWwmc1Q0uYM7ixyKJiH3e0mybBzTyNMVMGf0VFc8jIXkHG95pov2mqor8mWlcbqmXlNyurPQ8MoCGImZFkY9vyoI5CiZfJJ+/pjPkSsm9tZZlu5BrBtTpaZZBbWmJzdS5zNugs2DrGGlwk3wb8OtS7XJAHmngRMFfwpHMw7pPqzRB70IY8YVZxhMZvLsQhPM754XrtJxzNxs2LdxeJiN32+OUfGK1G1nCNLLowo861c+cKqeCfuzBpzOMc/okoz2Ddl8nqk8Pe/mN4qHhcRvCVJjG7WAGIPVMEHMCAyJYMODwiYR0b8aw4gPYv4Wf/CbFqn0GHnED0Illuc2Ay2S4W9ehLGwRqNCsvMfCtXgdXZYLUbcWGN2XlxZales/K/Vr64WZGMI2B/AUIMAuvSxfWzUAUfgE/WwpN/+ME+zP4At21p468CRhrPjDFXrq5INedlBjj5w962cV9vwAJ6nE+BGRdbuQ4Xb1x3920tuSsO+K2uWWQFyH+YWqSGDjPJSuwddd8qUAV/jkTemgFcRa3Mpl2L8qpRBKbLLv7HT0CsTX91vOqg22ZpEfecGlE+RQzCzQOlE7Pkx/ELNv96vdJVEVfxleVdruO8CqSxIuQTykJx8a5awdQ++CO+YFObXOvI7O3aB9/ZD/bcXKuAbxiqYEq7oQqmtBuqYEq7oQqmtBs6mrYfdEOv077dLiZV8H7ude1ku6CtCEq7oQqmtBuqYEq7oQqmtJv/B6U2dPRLbBSpAAAAAElFTkSuQmCC&quot; alt=&quot;04_grpc-deadline-cancellation-context-deep-dive-01&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;세 경계 모두 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;공통 원인&lt;/strong&gt;이 하나다. Context는 스레드에
붙은 게 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;attach된 실행 프레임에 붙은&lt;/strong&gt; 객체다. 새
스레드·새 프레임이 시작하는 순간 그 프레임에는 attach가 일어난 적이
없고, 따라서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current()&lt;/code&gt;는 ROOT다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wrap&lt;/code&gt;을 붙이는 순간 새 프레임 시작 지점에 attach가
명시적으로 박혀서, 상속이 복원된다. &amp;quot;자동으로 따라간다&amp;quot;는 문장은 이
모델에서 성립할 수 없다. gRPC-Java는 자동을 보장하지 않기로 결정한
스택이다. 명시적 wrap이 유일한 안전 장치다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-서버는-자동으로-멈추지-않는다-iscancelled와-listener&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5)
서버는 자동으로 멈추지 않는다: isCancelled와 listener&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;클라이언트가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;로 호출을 끊어도,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서버 쪽 로직이 자동으로 멈추는 건 아니다&lt;/strong&gt;. 서버는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current()&lt;/code&gt;에 취소 신호를 받지만, 그 신호를
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;주기적으로 체크&lt;/strong&gt;하는 책임은 서버 코드에 있다. 안
체크하면 무거운 쿼리는 끝까지 돌아가고, DB 커넥션과 메모리는 그 시간
동안 잡혀 있는다. 이게 &amp;quot;취소 전파는 했는데 리소스는 안 돌아왔다&amp;quot;는
전형적 좀비 호출의 구조다.&lt;/p&gt;
&lt;h3 id=&quot;5-1-polling-iscancelled로-체크-지점-심기&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-1) polling:
isCancelled()로 체크 지점 심기&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;가장 단순한 패턴은 루프 안에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current().isCancelled()&lt;/code&gt;를 주기적으로 확인하는
것이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb17&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb17-1&quot;&gt;&lt;a href=&quot;#cb17-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; StudioListResponse &lt;span class=&quot;fu&quot;&gt;listStudios&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;StudioListRequest req&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-2&quot;&gt;&lt;a href=&quot;#cb17-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;Studio&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; result &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&amp;gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-3&quot;&gt;&lt;a href=&quot;#cb17-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt;long&lt;/span&gt; id &lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; req&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getIdsList&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-4&quot;&gt;&lt;a href=&quot;#cb17-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isCancelled&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-5&quot;&gt;&lt;a href=&quot;#cb17-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;co&quot;&gt;// 클라가 이미 끊었다. 더 할 필요가 없다.&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-6&quot;&gt;&lt;a href=&quot;#cb17-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;cf&quot;&gt;throw&lt;/span&gt; Status&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;CANCELLED&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-7&quot;&gt;&lt;a href=&quot;#cb17-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDescription&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;client cancelled&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-8&quot;&gt;&lt;a href=&quot;#cb17-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;asRuntimeException&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-9&quot;&gt;&lt;a href=&quot;#cb17-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-10&quot;&gt;&lt;a href=&quot;#cb17-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        result&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;studioRepository&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;findById&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-11&quot;&gt;&lt;a href=&quot;#cb17-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-12&quot;&gt;&lt;a href=&quot;#cb17-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; StudioListResponse&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;newBuilder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;addAllStudios&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-13&quot;&gt;&lt;a href=&quot;#cb17-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;루프 안 polling은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;배치 처리와 스트리밍 응답&lt;/strong&gt;에 특히
중요하다. 한 번에 1000건을 돌리는 리포팅 쿼리가 1초에 끝날 수도 있고
10초가 걸릴 수도 있다. 클라이언트가 3초에 끊었으면, 서버도 그 시점에
일을 멈춰 DB·메모리 리소스를 회수해야 한다. 안 하면 &amp;quot;클라는 이미
끊겼는데 서버는 계속 일하는&amp;quot; 좀비 호출이 쌓이고, 부하가 몰리면 커넥션
풀이 동난다. 이 상태는 애플리케이션 로그만으로는 잘 안 드러난다 — 서버
입장에서는 &amp;quot;정상 처리 완료&amp;quot;로 끝난 요청이기 때문이다.&lt;/p&gt;
&lt;h3 id=&quot;5-2-listener-addlistener로-blocking-io-탈출&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-2) listener:
addListener로 blocking I/O 탈출&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;장시간 blocking I/O(예: Kafka consumer poll, JDBC 쿼리, 외부 HTTP
호출)를 타는 경우는 루프 안에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled&lt;/code&gt;를 부를 기회가
없다. 스레드가 I/O에서 block돼 있으므로 polling이 불가능하다. 이때는
취소 시점에 비동기 콜백을 받는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;addListener&lt;/code&gt;를 쓴다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb18&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb18-1&quot;&gt;&lt;a href=&quot;#cb18-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;addListener&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ctx &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-2&quot;&gt;&lt;a href=&quot;#cb18-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// 취소된 순간 이 콜백이 실행된다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-3&quot;&gt;&lt;a href=&quot;#cb18-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;cancelJdbcStatement&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// 예: stmt.cancel()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-4&quot;&gt;&lt;a href=&quot;#cb18-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;},&lt;/span&gt; MoreExecutors&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;directExecutor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;리스너는 보통 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;directExecutor&lt;/code&gt;로 등록한다. 취소 이벤트는
가볍고, 별도 스레드에 넘기면 오히려 지연이 생긴다. JDBC의 경우
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Statement.cancel()&lt;/code&gt;은 드라이버가 지원하면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;진행 중인
쿼리를 DB 서버 쪽에서 중단&lt;/strong&gt;시킨다. PostgreSQL과 MySQL 모두
지원한다. 이렇게 리소스별 취소 훅을 등록해 두는 게 좀비 호출을 막는
유일한 방법이다. Kafka consumer 같은 경우는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;consumer.wakeup()&lt;/code&gt;을 리스너에서 호출하면 blocking
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;poll()&lt;/code&gt;이 즉시 깨어난다. 외부 HTTP 호출은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HttpClient&lt;/code&gt;의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CompletableFuture&amp;lt;Response&amp;gt;&lt;/code&gt;를 취소시키는 훅을
건다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;주의할 지점 하나는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;리스너가 여러 번 호출되지
않는다&lt;/strong&gt;는 것이다. Context가 한 번 취소되면 리스너는 정확히 한
번만 실행된다. 이미 취소된 Context에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;addListener&lt;/code&gt;를 뒤늦게
걸면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;즉시 호출&lt;/strong&gt;된다. 이 규칙 덕분에 &amp;quot;늦게 등록된
리스너가 실행되지 않아 리소스가 안 풀리는&amp;quot; 경우가 생기지 않는다. 반대로
같은 리스너를 여러 번 호출되길 기대하면 안 된다. 재실행이 필요하면
리스너를 다시 등록해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;5-3-어디에-체크-지점을-두는가-롤백-가능성-기준&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-3) 어디에 체크
지점을 두는가: 롤백 가능성 기준&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;체크 지점의 배치 기준은 한 줄이다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;이 지점을 지나면 롤백이
어려운가?&amp;quot;&lt;/strong&gt; 롤백이 어려운 지점 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;직전&lt;/strong&gt;에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled&lt;/code&gt;를 체크한다. DB write 직전, 외부 결제 API 호출
직전, 대용량 파일 쓰기 직전, 이메일 발송 직전이 대표적이다. 이런
지점에서 한 번 더 확인하면 &amp;quot;클라는 이미 끊겼는데 사이드 이펙트는 실행된&amp;quot;
상태를 막을 수 있다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;읽기 전용 루프는 체크 간격을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;10~100건에 한 번&lt;/strong&gt; 정도로
둬도 충분하다. 매 iteration마다 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled&lt;/code&gt;를 호출하면
오버헤드가 보이기 시작한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled&lt;/code&gt;는 volatile read
수준이라 비싸지는 않지만, 1000만 번 반복하는 루프에서는 누적된다.
실무에서는 batch size 기준으로 &amp;quot;매 배치 경계&amp;quot;에서만 체크하는 경우가
많다. 체크 지점이 너무 촘촘해도 성능이 떨어지고, 너무 드물어도 취소가
반영되는 데 시간이 걸린다. 배치 단위가 가장 합리적인 중간 지점이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-분산-시계-비대칭-deadline_exceeded-vs-cancelled&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6) 분산 시계
비대칭: DEADLINE_EXCEEDED vs CANCELLED&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;취소 이벤트를 받았을 때 gRPC는 두 개의 status code를 구분한다.
**&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;(4)**는 Deadline이 자연 만료된 경우고,
**&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;(1)**는 명시적 취소 — 클라가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;stub.cancel()&lt;/code&gt;을 부르거나, 부모 Context가 취소됐거나,
스트림이 중간에 끊겼을 때 — 다. 둘을 구분해야 하는 이유는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;원인
분석과 재시도 전략이 서로 다르기&lt;/strong&gt; 때문이다. 특히 분산 시계가
완전히 동기화되지 않은 환경에서는 같은 이벤트가 양쪽에 다른 상태로
기록되는 비대칭이 생긴다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;상황&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;클라 관점&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;서버 관점&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;재시도 가능성&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;클라 시계 기준 Deadline 만료&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt; (상위 Context 취소 신호)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;원칙상 금지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;서버 시계 기준 Deadline 만료&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;원칙상 금지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;클라 명시 취소 (사용자가 창 닫음)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;부모 요청 취소의 연쇄 전파&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 미묘한 지점은 첫 번째 행이다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;같은 호출을 놓고 클라는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;로, 서버는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;로 기록할
수 있다&lt;/strong&gt;. 이유는 Deadline 체크가 각자의 시계를 기준으로 이뤄지기
때문이다. 클라 쪽에서 먼저 Deadline이 지나가 호출을 끊으면, 네트워크로는
&amp;quot;클라가 스트림을 RST했다&amp;quot;는 신호만 전달된다. 서버는 &amp;quot;위에서 누가
끊었네&amp;quot;라는 사실만 알고, 이유를 모르므로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;로
로깅한다. 장애 추적 시 양쪽 로그를 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;trace_id&lt;/code&gt;로 조인할 때 이
비대칭을 고려하지 않으면 &amp;quot;같은 요청인데 한쪽은 DE, 다른 쪽은 C&amp;quot;를 보고
혼란에 빠진다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실무 가이드는 세 줄이다. 첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;NTP를 신뢰하되 1초 이내
드리프트는 일상&lt;/strong&gt;으로 간주한다. 데이터센터 안이면 보통 수
밀리초지만, 클라우드 가상 머신과 컨테이너 환경에서는 NTP가 동기화되기 전
부팅 직후에 수 초까지 벌어질 수 있다. 절대 시각 기반이므로 이 드리프트가
그대로 체감된다. 둘째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;대시보드에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;와 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;는 합쳐서 SLO
카운트&lt;/strong&gt;를 잡는다. 분리해서 원인을 가르는 건 좋지만, SLO 위반
여부는 둘 다 &amp;quot;예산 내 응답 실패&amp;quot;이므로 묶어서 봐야 한다. 셋째, 양쪽
로그를 조인할 때는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;trace_id 기준으로 매칭&lt;/strong&gt;하고, 상태
코드 자체는 판단 기준에서 빼야 한다. 조인된 후에 양쪽의 실제 시각을
비교해 드리프트 크기를 별도 지표로 뽑아두면 장애 분석이 훨씬
쉬워진다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-retryhedgingcircuit-breaker의-정합성&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7) Retry·Hedging·Circuit
Breaker의 정합성&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Retry를 잘못 걸면 Deadline보다 더 큰 장애를 만든다. 이 섹션이 5쌍 중
4번과 5번을 동시에 다룬다.&lt;/p&gt;
&lt;h3 id=&quot;7-1-deadline_exceeded는-retryable이-아니다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-1)
DEADLINE_EXCEEDED는 retryable이 아니다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;가장 자주 틀리는 지점이다. gRPC-Java의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RetryPolicy&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;serviceConfig.json&lt;/code&gt; 형식으로 retryable status를 지정할 수
있다. 처음 쓰는 팀은 &amp;quot;타임아웃은 당연히 재시도 대상&amp;quot;이라고 생각해
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;를 리스트에 집어넣는다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;이게
장애를 배수로 증폭시킨다.&lt;/strong&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이유는 단순하다. Deadline은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;절대 시각&lt;/strong&gt;이다. 첫 호출이
만료돼 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;가 돌아왔다는 건 이미 마감 시각이
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;지났다&lt;/strong&gt;는 뜻이다. 재시도를 해도 남은 시간이 0이거나
음수다. gRPC-Java는 이 경우 재시도 자체를 하지 않고 즉시 에러를
돌려주지만, 몇몇 구현이나 외부 retry 레이어(스프링의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Retryable&lt;/code&gt;, Resilience4j 등)가 이 규칙을 모르고 재시도하면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;이미 끝난 호출을 서버에 다시 보내는&lt;/strong&gt; 낭비가 생긴다. 서버
입장에서는 동일 호출이 두 번 들어와 부하만 두 배가 된다. 백엔드가 이미
과부하라 Deadline이 터졌을 텐데, 그 상태에 재시도가 더해지면 피드백
루프가 형성돼 장애가 확산된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;retryable로 두는 건 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;일시적 인프라 장애&lt;/strong&gt;를 나타내는
상태여야 한다. 구체적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNAVAILABLE&lt;/code&gt;(14)만 기본 후보다.
드물게 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RESOURCE_EXHAUSTED&lt;/code&gt;(8)도 포함할 수 있지만, 이건
서버가 &amp;quot;지금은 과부하&amp;quot;라고 명시적으로 말한 것이므로 재시도보다 backoff가
먼저다. 다른 상태는 각자 의미가 있으므로 재시도의 정당성이 없다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;NOT_FOUND&lt;/code&gt;(5)는 재시도해도 여전히 없을 것이고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;INVALID_ARGUMENT&lt;/code&gt;(3)는 재시도가 해결하지 않는 결함이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb19&quot;&gt;&lt;pre class=&quot;sourceCode json&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode json&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb19-1&quot;&gt;&lt;a href=&quot;#cb19-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-2&quot;&gt;&lt;a href=&quot;#cb19-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;dt&quot;&gt;&amp;quot;methodConfig&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;ot&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-3&quot;&gt;&lt;a href=&quot;#cb19-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;ot&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt;&amp;quot;service&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;bander.studio.v1.StudioService&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;ot&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-4&quot;&gt;&lt;a href=&quot;#cb19-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;&amp;quot;retryPolicy&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-5&quot;&gt;&lt;a href=&quot;#cb19-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;&amp;quot;maxAttempts&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-6&quot;&gt;&lt;a href=&quot;#cb19-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;&amp;quot;initialBackoff&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;0.1s&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-7&quot;&gt;&lt;a href=&quot;#cb19-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;&amp;quot;maxBackoff&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;1s&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-8&quot;&gt;&lt;a href=&quot;#cb19-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;&amp;quot;backoffMultiplier&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-9&quot;&gt;&lt;a href=&quot;#cb19-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;&amp;quot;retryableStatusCodes&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;ot&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;UNAVAILABLE&amp;quot;&lt;/span&gt;&lt;span class=&quot;ot&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-10&quot;&gt;&lt;a href=&quot;#cb19-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-11&quot;&gt;&lt;a href=&quot;#cb19-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;ot&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-12&quot;&gt;&lt;a href=&quot;#cb19-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;7-2-retry-budget을-남은-시간으로-재산정&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-2) Retry budget을
남은 시간으로 재산정&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;maxAttempts: 3&lt;/code&gt;이라고 써놨어도, 남은 Deadline이 100ms면
세 번 시도할 수 없다. gRPC-Java의 재시도 구현은 각 시도 전에
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;남은 시간을 재계산&lt;/strong&gt;한다. 남은 시간이 현재 backoff 값보다
작으면 더 시도하지 않고 즉시 마지막 에러를 리턴한다. 이 동작은 버그가
아니라 의도된 스펙이다. 대시보드에서 &amp;quot;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;maxAttempts=3&lt;/code&gt;인데
실제 시도 횟수는 1번만&amp;quot;인 현상을 버그로 오해하면 안 된다. 버그가 아니라
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;예산 소진&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;추가로 고려할 건 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;전역 retry budget&lt;/strong&gt; API다. 분당
허용되는 재시도 비율을 전역 예산으로 두고, 그 예산을 넘으면 신규
재시도를 일괄 차단한다. 예를 들어 &amp;quot;전체 요청의 10%까지만 재시도
허용&amp;quot;이라는 규칙이면, 분당 600 요청이 나갈 때 재시도 예산은 60회다. 이걸
넘으면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNAVAILABLE&lt;/code&gt;이 와도 재시도하지 않고 클라이언트에 바로
올려 보낸다. 이 상한선은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;백엔드 과부하 → 재시도 폭증 → 더 심한
과부하&lt;/strong&gt;의 피드백 루프를 끊는 유일한 안전 장치다. Netflix,
Google, Square 모두 운영 gRPC 스택에 이 상한을 명시적으로 건다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;숫자 잡기 예시: BANDER처럼 특정 서비스로 분당 600 요청이 나가는
환경에서 budget을 10%로 두면 60회/분이다. 백엔드가 일시적으로 5%의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNAVAILABLE&lt;/code&gt;을 돌려주면 30회/분 정도의 재시도로 수렴한다.
여유 예산이 50% 남는 안전 구간이다. 백엔드가 20% 이상의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNAVAILABLE&lt;/code&gt;을 돌려주기 시작하면 예산이 포화되고, 그
시점부터는 추가 재시도가 차단되면서 &amp;quot;재시도가 장애를 키우는&amp;quot; 구간으로
넘어가기 전에 차단된다. 10%라는 숫자는 Google SRE book의 기본
권고값이기도 하다.&lt;/p&gt;
&lt;h3 id=&quot;7-3-hedging은-이중-호출이다-부하-배가&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-3) Hedging은 이중
호출이다: 부하 배가&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Hedging은 retry와 다른 개념이다. 첫 응답이 오기 전에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;동일
요청을 병렬로 한 번 더 쏘고&lt;/strong&gt;, 먼저 도착하는 쪽을 받는다. p99
지연을 줄이는 데 효과적이지만, 대가가 있다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;백엔드 호출 수가
즉시 2배&lt;/strong&gt;로 뛴다. 3번 hedge로 두면 3배다. 이게 계산에 없으면
&amp;quot;p99는 좋아졌는데 전체 부하가 2배가 됐다&amp;quot;는 상황이 나온다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb20&quot;&gt;&lt;pre class=&quot;sourceCode json&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode json&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb20-1&quot;&gt;&lt;a href=&quot;#cb20-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-2&quot;&gt;&lt;a href=&quot;#cb20-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;dt&quot;&gt;&amp;quot;hedgingPolicy&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-3&quot;&gt;&lt;a href=&quot;#cb20-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;&amp;quot;maxAttempts&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-4&quot;&gt;&lt;a href=&quot;#cb20-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;&amp;quot;hedgingDelay&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;0.3s&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-5&quot;&gt;&lt;a href=&quot;#cb20-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;&amp;quot;nonFatalStatusCodes&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;ot&quot;&gt;[]&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-6&quot;&gt;&lt;a href=&quot;#cb20-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-7&quot;&gt;&lt;a href=&quot;#cb20-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;적용 기준은 엄격하다. 첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;idempotent한 호출만&lt;/strong&gt;
대상이다. 결제·예약·포인트 차감 같은 non-idempotent 호출에 hedging을
걸면 중복 과금·중복 예약이 난다. 이게 실제 사고로 나오는 경우가 꽤 많다.
둘째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;백엔드 용량에 여유가 있는 경우&lt;/strong&gt;에만 적용한다.
부하가 90%를 넘은 서비스에 hedging을 걸면 즉시 100%를 넘겨 장애가 난다.
셋째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hedgingDelay&lt;/code&gt;를 p50보다 약간 길게&lt;/strong&gt;
둬서 대부분의 호출은 첫 시도에서 끝나도록 설계한다. 구체적으로는 p50 ×
1.5 정도가 일반적이다. 모든 호출에 동시에 두 번 쏘면 그건 hedging이
아니라 그냥 부하 테스트다.&lt;/p&gt;
&lt;h3 id=&quot;7-4-jitter와-circuit-breaker의-시간-축-정합&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-4) Jitter와
Circuit Breaker의 시간 축 정합&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;재시도 backoff에는 반드시 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;jitter&lt;/strong&gt;가 있어야 한다. 고정
backoff는 다수 클라이언트가 같은 순간 재시도해 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Thundering
Herd&lt;/strong&gt;를 만든다. 100 대의 클라이언트가 모두 정확히 500ms 뒤에
재시도하면, 백엔드는 그 순간 100배의 부하 스파이크를 받는다. ±20%
jitter가 일반적이다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;actualDelay = initialBackoff × random(0.8, 1.2)&lt;/code&gt;. 이
무작위화 덕분에 재시도가 시간적으로 분산되어 스파이크가 완만해진다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Circuit breaker와 Deadline의 정합성은 한 줄로 요약된다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Open
상태에서도 Deadline은 같은 속도로 감소한다&lt;/strong&gt;. 브레이커가 열려
호출이 즉시 fail-fast로 떨어지면 100ms도 안 걸려 리턴되지만, 그 100ms
동안에도 Context의 Deadline은 깎인다. 그리고 브레이커가 Half-open으로
바뀐 뒤 실제 호출이 나갈 때 &amp;quot;남은 시간이 이미 충분히 짧은&amp;quot; 상태라면, 그
호출은 거의 확실히 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;로 돌아온다. 이건
브레이커 관점에서 &amp;quot;실패한 호출&amp;quot;이 하나 더 생긴 것으로 집계돼 브레이커가
다시 Open으로 넘어간다. 영원히 Half-open → Open → Half-open을 반복하는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;진동 상태&lt;/strong&gt;에 빠지는 장애가 가끔 나온다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;해결은 Half-open 시점에 &amp;quot;남은 시간이 최소 임계값 이하면 그냥
fail-fast&amp;quot;로 두는 규칙을 브레이커에 심는 것이다. Resilience4j는 이 훅을
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CallNotPermittedException&lt;/code&gt; 직전에 걸 수 있고, Hystrix는
historic하게 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;fallback&lt;/code&gt;에서 처리한다. 어느 구현을 쓰든
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;브레이커와 Deadline이 같은 시간 축을 본다&lt;/strong&gt;는 원칙 하나는
유지해야 한다. 이 정합이 빠지면 retry 예산과 hedging 설정이 다 정교해도
브레이커 근처에서 SLA가 무너진다. 이게 &lt;a href=&quot;#0-불변식-5쌍-지도-deadline이-서려면-동시에-서야-하는-것들&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§0의
5번 레이어&lt;/a&gt;가 별도 행으로 서 있는 이유다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-3-hop-예산-전파-시퀀스-client--gateway--service-a--service-b&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8)
3-hop 예산 전파 시퀀스: Client → Gateway → Service A → Service B&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 섹션이 &lt;a href=&quot;#1-deadline-vs-timeout-절대-시각과-상대-시간의-차이&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§1&lt;/a&gt; 표의
&amp;quot;3초 공유&amp;quot;가 실제 wire에서 어떻게 흐르는지를 시퀀스로 보여준다. 각
홉에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더의 값이 어떻게 깎여 나가는지,
그리고 체인 어딘가에서 Deadline이 끊어지면 어떻게 전파되는지를 본다.&lt;/p&gt;
&lt;h3 id=&quot;8-1-각-홉에서-grpc-timeout-값의-변화&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-1) 각 홉에서
grpc-timeout 값의 변화&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;초기 3000ms Deadline을 건 호출이 게이트웨이 → Service A → Service B
순으로 전파된다고 하자. 각 홉은 자신이 요청을 받은 시각과 내보낼 시각의
차이를 빼고, 남은 시간을 다시 헤더에 실어 보낸다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;홉&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;수신 시 남은 시간&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;자체 처리&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;다음 홉으로 보낼 grpc-timeout&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Client → Gateway&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;3000 ms&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;20 ms (auth, routing)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;2980m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Gateway → Service A&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2980 ms&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;150 ms (비즈 로직)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;2830m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Service A → Service B&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2830 ms&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;50 ms (조립, fanout)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;2780m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Service B 처리&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2780 ms&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2700 ms (DB 쿼리)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;(응답 생성)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 흐름에서 핵심은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;각 홉이 남은 시간을 자체 시계로 측정한다는
것&lt;/strong&gt;이다. wire에는 &amp;quot;남은 밀리초&amp;quot;가 실리고, 수신 서버는 자신의
시계로 &amp;quot;이제부터 N ms 안에 끝내야 한다&amp;quot;는 새 절대 시각을 다시 계산한다.
홉 간 시계가 ±50ms 드리프트면 전체 예산에서 150ms 정도가 불확실
구간이다. 3초 SLA에서 150ms는 받아들일 만한 오차다. 더 길어지면
Deadline이 실제보다 빡빡해지거나 느슨해져 체인 끝단에서 갑자기 터지는
경우가 생긴다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실무에서는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;각 홉이 자기 처리 시간을 로깅&lt;/strong&gt;하는 것이
중요하다. 홉별 p50, p99를 대시보드에 찍어두면, 어느 홉이 예산의 얼마를
쓰고 있는지 한눈에 보인다. 위 예시에서 Gateway가 20ms, Service A가
150ms, Service B가 2750ms(DB 2700ms 포함)를 쓴다. 예산의 92%를 B가 쓰는
구조다. 이 비율이 정상 운영 기준이고, 여기서 B가 갑자기 2900ms를 쓰면
Deadline이 터진다. 대시보드가 &amp;quot;B의 쿼리가 느려졌다&amp;quot;를 먼저 알려줘야
한다.&lt;/p&gt;
&lt;h3 id=&quot;8-2-시퀀스-다이어그램&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-2) 시퀀스 다이어그램&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;04_grpc-deadline-cancellation-context-deep-dive-02&quot; src=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAcAAAH4CAMAAAAB5nQbAAAAYFBMVEUAAAALCwsREREYGBgjIyMsLCwzMzM8PDxERERLS0tWVlZZWVlhYWFsbGx0dHR+fn6FhYWMjIyTk5Oenp6hoaGvr6+xsbG8vLzFxcXJycnQ0NDe3t7j4+Pv7+/x8fH////6y9dAAAAAKnRFWHRjb3B5bGVmdABHZW5lcmF0ZWQgYnkgaHR0cHM6Ly9wbGFudHVtbC5jb212zsofAAABrWlUWHRwbGFudHVtbAABAAAAeJyVkzFPwkAUx/f7FC9OEIWUEhWNGFoxOjiJI8tZTnqxveL1qsGJGAYjJMrkAsbBQTdjMPEzyfkdvJbW0CAmru//+v+9/3vXii8wF4HrIEGFQ6CYs70WVAluOJQRkI/dr34fZG/41enImyfIyPH158crFDVNc/0s8k8pa2GOXXA95lk291wCggdkRvFt3PAuKGvCCXZ8gpAqC2rRFmYCdhxKmEiV9rAgF7idqi3VCD+nFgFjCbAPxu+iGYkmQlNXyG0nZrAJ+7tGdfewVmdN3rJygrrEC0Q5CoKYJwhw2rSFahRlFW0FJlfPctSJkyKUGKU8cSDsFeDKKIyX0aOdzHQaC7n6Rmmeq8+Cow4FNhKjY3oJjtekFmQKqxEpkszFjFJxnlFYT0HCFgUxE6eqCWcB4W116d5QPgyyoRQPUDWODFhW58XUIRwilno/IvDL2hTjkJM4SSnEyPs7kKPBpPcOU7coTWqDf3r+bFJ9Ep/0f1NshBtNRpjcduXrS52pRy3fxhCLW+X4xFA7MFCFsEb4N6BCXtf0tbz+DeyGFe1bx/x6AABQEElEQVR42u29B2PbuNa1u9mbmlVsJ5n53v//s+6ZTMaW1dn7BdhEybIjx7JFwPs5ZyKSICEsU1zYAEFC+D9AEOSLI167AAiCXB30AQRB0AcQBEEfQBAEfQBBEPQBBEHQBxAEQR9AEAR9AEEQ9AEEQdAHEARBH0AQBH0AQRD0AQRB0AcQBEEfQBAEfeBLsrGbRX+TvpgGYG9eySUm5C8eijCEfO0CfFWyKE51TWhteYgm1mtHhPSSk5QXM/wJf4lFJpu0p53eJ90VH4a+VfphWCyrwc6Q6m8ITJI9SSNXdJHas9xwVCQ9pfetfNzohub2i/zzTSX/RLRogloeeiTOj2RDPL1wAicdXuuMfGnQB67ExhbyrXDXumCz/PUjlnHxMRgJp9Nz8r8iEy/WX/cBUSf/+Ntiud/KLdjItc0IZHMSG02Sn6dSs2KvCnMQb6H6Af1XrP59Qlw2D4VculNOLJxEWiaTTz4TCKAPXI1eX8kfw93sLcf0rTTc7ZLfHXObv3RW1f+DX/G9llBHGQ0iDQJFXu+TCycpMbQ0C+Om1vZz8Pr1SrSSBwALt1rV74AYQrZofele3Dq0ps7y6fuJhZMYhqO/GhUhHwL6wJUg4bRw3D2Tr1xlpEO+8VLZIvHxUzz0fXHUXBeyBqa48UItX3uZNlZgG0S5cSNDtgxEs8lmHd8Y5NhRUOZW7VuSxuBpj0mxm/NdmPfatW8KYa9afMzov3UIkC4Eca3o1doOxqToJvlaG0hDgmRN4oaAKnomLnNgAL117GvPFoyn+MYJ5DEprjUmX7ENEunGhMnPLfrA54M+cC3iMAilw8bwOhPC9Td48gXT3cRTiOMl5NnioDFtbiDS5oGi+vMf4Ei9wMtv4SkAw9nnHGf0n2WZW71vQb4EcadOsiegF3kmFFf6WqoijAjCOo97QRBWbhE4QO5ssqk0fzRvyl+LC7TZYZr5grQdJlXhbLBOiIsLT1DCSHy2YMTxIs+jRQq5bWmwCLR+RMotSXH8Yh8I8lGgD1yLJbnmxuTPHxc1r0h/++L35CHKYh/uNGvuDskm9S77lfntK0wRszQM4F78mYTadwGi/wKIA7g14l9HX1DlVu9LN4WLxOo/LsoggPiAWPhAkpUHRBHEVS/Af1HxsaLfHD3k8tSA+5XnzWjQkYBIr/3YdZOBuvi3Z9FAIPBk/YS4hMYFJDBIny+Qf+V7d5ncDB5CUroI9LJHUknRBz4f9IFrMY0C5+lmAKuArpm0Sh4WF2EEggbkoooU2osnqcGBD5AmvEQu0ifIINHSXZwUR5D9peMvqHOr9i1WEnMs3i3KXkSJxBrFUbNyPX2Cwe7xvqjfh7GQ2MYwFjfEi3oG7S9U7yO3aHukxUHEdtSplN+tdrt7DeInmJ0SJxY9DjkIzxfIvyNBAWkAMr0TYtlbd9AvipUC8tmgD1wLWTbB3g1gvG+JC8XVUVwj+158AQ7uI8Q5KCTaps1yKf0v03WnOOLEPQShzqTYt1jqq0Koa2UbwYvAF8D16kZH9pgYN4n3WNwfNCELbVmbF2UbV3uoZQ+ADNR7lKmqwL/Z39+jSAN/kY3V9nfX4sjPK6N1v/x8oV1Q+iXyNllFExpC4G/y88G/+RVRIM2FZ0GwClmsRDSVXuGkyd7eI1tCEYCb9J9tpt0lTk6CgTxSvZe+o9y3RHvyftBTbkqwiYFYSCDVtxi9WJ3BdJ6Ua+lPpafBX/lj9CxHSSjuIdZBCnGHzVaYWs+/mIqT0tDIYjDkZwvH+w8GK9ud0C4FbBZ8PugDV2IDWrIF60Q9rqvRvG+DRivYdeZlUPfhgxsnUSZMQNfCJ1MOTQXitUs2W3LyoAWnv6fet3ewdUbvLgoCidKhvm/Yo4UR7tK6RPKEjiKYpPsbhMUtQoLlhCasqO+k8JP8q46j8eHvaC9uuFrTLkT5xMIhv3StCBKSTMXf5OeDf/MrEZPrSOrdnEq6XQRrMItoXN3kwrQ5R1EEijEiq7OV54BkmYa/G4Th1ho/5dFkefqLqn2b9WXZEJg+O/OlUzTdDOG8/JTNff1cHjNw1obQGgopy7cviutnmzmYk1MLR9i2YJAjVoADCq+AgPOcXoksfTkAztPiivsVT63kBadOJHohZoJQLiWvGXq5b8GmDhtu636BNe3na/P/Kd/pOMB6dfK8lPZqdPpiLQ49EpfHsvjCwolSBo89HE94BdAHOgz1gQ//kjw7utOQCr99+MxOTgYy5xz6G5z45p05IH8Ctgs6jC5L78/kdwjH33HGd/Zf2P7+4vbenQPyJ6APdJjx+7NAkHPA9w8gCII+gCAI+gCCIOgDCIKgDyAIgj6AIAj6AIIg6AMIgqAPIAjC+HjC3M7ffIzQF958zGcQBu/Po1vi/uTsdF/WRc4T0CfCr62kBeM+YBtvD2hye3DtYp/Cu8jY+qxL4v7k7JymQ+fMyy/0DESQmu/P5FIw3i7I/qD8wqUqqcuSXKR6ELskLr/Yr6tD5yzW359HgR5fW0oLxn0AQZALgD6AIAj6AIIg6AMIgqAPIAiCPoAgCD8+kBdTfIFjX7sgF9HC19xevOnhTxXj44ga4hW9Gyvfg5/34Sm/fXeGXdDCC946b2ZRfMaLJyt5UO6uXfI/VfWCqDmdu7LfpXGENZz4QDQXbvQs8Ms19cQebsLK/BiHWtgq+2m2ygi8lxLVlxI86PYM6K+pekmU1AuD6Pu1S34CTnxgB1MNJKV6n/apqyaIWbmWDrWwVfaTBOlQfflyf1GaK+Reh3W/quqlcov9/jxKP+Ft9G+FDx/IAq0dbK3zMeQbP7dGdNnYpdZwF+QPKhPvAW9pcf1Q6A2gLHslaK4PwNuNdNjAqEx/EicA4XrS2bpThLAcSb8/J6oN+g25ljYzqThZOy+VBladXhCmN1u3wz5Qq2qLMmOpVLV7QVRxXBf75PjwgQQO2lwJabitg0Fqyz1IIt/MbEPz034Hbfh1LZFseTtLKsteCZI84gNJoOfkGinTVTuRwe3wLMGq4qY39I+/PyeBlbhDkVT5UnGyNq5ppI3E8ihXMCM3uNRg/o9T1Ralg12qekkUJEHQoUcn9/DiA8c6Uq/Xg8Anf/58qqd+MJDzj58i7NJaSN2i+IGl0bLXgnQ/FUMxhDA3qnTT9gZZlyNomK2Ch7HROicwU2LftzK/rCdTV6ahWiudnDbfECzX664PVKoORIFoN6pOiYL4AbTOPDnZhg8fECE72pKAF0JCIzBVv8B0W5+vxQ0m5FoIU8gPBWkQSDBcZ74qVemK6g486NAzrM8lTd3Ncmzuz4mikP+7lluVOgG9LbHAz9VEFP28i5VnW5XUFtVWdUoUyDeBs5h2UBMfPiBDdLQlB42chg7+wc/VEvmQL2JTr196UQuS5UAiS4Hfb9LNTejq3fY6S310zcNzYm4Tr3o/QV5uPEh3YUM//C77G1XVP/yh7VWdEgWCpimrLmrixAdkr3/YPlbguB3QnSfYf6tlUJ6VILyx0k1V9kaQ7ooDSbYzo0k3N5vO31BQ5OTonJjbbV1qBYIhHJ6zNOyTUDoj7nHtkv9G1dEPba/qhKgCCbr03oGaLvZd/gEjeHKT2H1sNkhmsI0jd7+HkgaMOMEI5n6apvQXEwUrElgWZW8E6Xmqgx7LcpMuGrHU4XY0+Ns4c5Le0TmRNL8utWTGy9Dz2+ke9CRJUvSws0P2KlVHP7S9qhOi6M0gdw3GtYt+Aj7iAdBn6zUR07prcAO2Ddreik1/oc2uXcxztSxJBKmAarquJThyryh7LUgDRQLdIT+mJt3qYqS5J3dsEK3e8Tkxw+bsjHLfF/pGK93VioaOFXj9P/jGz1R19EPbq3ouioQ5C0EbqH/wbR+N8H/XLsG72Ox/JVkiHwY3eSod9A+kYrXqdDKI3rXCxywRC4POBAESuS77saAm3V3fN37eJXHb5m5ZXrXankuAOkF+Nb07shpRjao/FdUdTdzEAwTx2GaFI23d7kk7qYUam9yUXXh2sqp0W+v4aWyKJ7xU0CpB6LiQk6p4EMVAEZHfkMgXeoMu8mVBH2AfeXrtEiCsw8n9AgRB3gH6AIIg6AMIgqAPdAVGRjkhXMK4DwjZ24/JuvnUgXKR6TM7Je5Pzk7nZckXmuYUgi49Kc74OKLs7TPqCl2aO7dNeIGBzwIMOiQu310oyunUOQsuNd9xl8aCM+4DLxN28W2QqOlriGJPE+PtgpcJr10A1PR1RbGniVsfQBDkbNAHEARBH0AQhFsfYK6n5otq4lIUe5rQBxiCR01cimJPE7c+gCDI2aAPIAiCPoAgCLc+wN5Qjq+piUtR7GlCH2AIHjVxKYo9Tdz6AIIgZ4M+gCAI+gCCINz6AHtDOb6mJi5FsacJfYAheNTEpSj2NHHrAwiCnA3/85jsZDoJ6NrUdnTqXN0AiNwefTUcXZc0rVoiKeRDMkhK6vqS1tXpNRHkA+DfB2zdpG/KkzWbhmv0zZlbL6UzANmgpknam9ClIsUGxV5PLXgQzMhBH0C+ENz6wIlXxOmT4iPzwMvEan1ly8MmhXz8dCw/+dbFmakbTbtrF2NPMHn/5LHsvcuPR01fyQcqHJgs3bq6Hwf2wezTUgYi+J32ARhcuxwN2WL6biNg75rhUdMX6CeMbNsu6tDYcVzy4Vg9yWlSzTRrUiDZRT3QjM08uXah2UCcLdJrlwG5BF/AB9IgCIoB34nn+cSr4z70omj/B8jrFHD+XY9IoHA7Cn651y41G6ARcAK37YI9Bmn85/+rFminoG2n4Iyr1FiQ6hQwh4+FYQytp4X+/pbvV0CcPb2/aYBcHW7jgZdaaJlnqaqhOtVEO5lrtP4Y6tgvWhDyGPxrC3iDpmvy7oigi6LeC3uauI0HTpyKjLT6BQ/GxPuUuWeR9TzaCOM6hVZrlrfWs0SXPOjSpFOvaLo+740IOinqnbCniVsfOIHnAai5RUMgQ7Ituq4YI7FO+Ub3mYRPwyVZmfL7h/HVywby2DTgAG7nN3wPidDl3/Xu9H3DwMks61VV9k3x+e+YNoWiXZiJSs8s03wvFovlOpcmt1ezrYqSoRGwDrf9A+9BZuFXHR6+9CacC+rCfu2A1GmtJA/izd1YX1b3RYLcgqdgn0uT2++zBbxrwAHchr/sDeV4o6a1fKBwq00ANr1zpgffuRMhlGi/iBEHZV1PIgXrn0Bvcmlyez1boR7bqC1vLyKKE9jThD7AEG1N6zD6d7QP2DOfXNfmNtTnauZKk3An0ER/G0p3ymEmORiaAMb2P01O43B/+Sbkt1Dnota5ncx2n+v+IYw/H+zM+4liA259gHf6vjyUwa8GPvZTkOjJTCDxzelqLo+dtQX5wpwm+zZOvsyyNJmCQn6l4rcgSkVrWrcLnSAY9KDORapzO5XtUa4ID6AP1OwU408Ooz1sfXpkaJcdasefH4QsiuRyjqtOgj59LAIE+jilMQPdvxMzPxcEIUr39z+ViSCIkgwbsuwGdEuWBCCNisQkyeJUqnNpcjudbTtXhAvQBygRqOBp5/kA3bdFkFv+/E6H8NHSFln/2efHYlX9/aTazuij01J5RsWqmp8tHozmkUDRLPv7LBLS79sK1Z4jSP5dT+tcmtxOZnuQK8IF3PrAm1poK3kK92/Yt8UNQO9/TQ9b/9nnx2paV8Me/5bEyCQWddQXoP1wl7ubei1fD+iFvFNlYmXru6OsZC2COpcmt5PZHuZ6eVHsw54m/n0gWYayFvyAuZp431dq6gkj0hBeBZn8o96l6HNztSEUvWHTcEt7w9JFKN2Q6pbsCvpYggerD7Z7f9Q/R4nJdZIXHWqbQDv61A/yvJQmihxmIjSWJPSdvrjVDh+Yzl1dFzNYp/VuA1qLF7cP8+hgz1CW/NDa51J/nsq2yvXiJ4oj2NPErQ80zPOZuEqIH/jWTEr83sReWsI8GRr7Z4v7vjySt3Kxz3T1KE/sjQWP0r2zMunx03wz/wZJSjwhKfcNqmN7UPawWUVvOwmfE+no8zDPS9IP/pn09qvD+Kcg13V8dZsv3y5BH0HQKPVpPFA+WZGXjQS5bAxtAkGwbva5NLmdyLbKFeEJ7n0gjCcG9JdkyaC1ojUB5ZcvRYMBrcSLXjZJkUWxqvHIPrp/L6TLPIqtXLdjJYrI8ekqqqvEYt+6e45eh3UPm0Db2tnx50Ge59zcPxvtr4NOe2GWZjR8/07+o09P9kjZxB9FP99oW+1iFq0IjR4m9eJyW5l0l6aK0Mql/jyVrVDmivAE9z6QNt16hVT6jBGQi7ro8M7W9F+jVbfRfSSBXh0pOC4osZLQ4zVIDkLjpnsOaA9b+vN5D9u+g62V54U5OnfSia674np1qoHIwqx17PiFg+uFZxuOc0V4glsfqIdyaBCT+rudEtOfdkjjYenby8dLMOyVn+T4CMqLoQ6w1175+Xe5qxaRMCEyIQT1+PNDNL2R2R8d9VmwN+aGR03c+4CkruJ4/zKBIJK3gikpjmKEelNFF31uh2jqVtZjUdJkWwVH1kAOYseVyn0nk/33yFIQkpZ/0aGmK88+P0ITV/Aoij1N3PpAw2zt64Nm2KvwmAkkiL99WoD0vfEB2uf27LjFI4hTg+z6C5RbgOHil973nvXPFT1sJMgeVR1qx5/vJPMNjMKRD4fb544Pns59yKsWwC99nJYh/lFfV3qicZ1nh7vWMcPhvmlaBf9pLp/8fB/hwujVrYtS06477yve8+eF6qScd8KeJv7jgUWmBFGriVxdxEe17KnhccLRruLJffcdai98vg9t+uTJPRODAuQj4dYHmhZaL4z1cb3WZ0+vNntKdtsiKChVCB2ayKThz++GMNeU5lITt+0CjgifyHkSRAwKkA+DWx/4ee0CXB5hgo/5IR8De3Hymfx17QJcjnCRExMQexbGA8gHwa0P8ENhA0a/o3MuIlzArQ+wN5TjJSFPIFWhADeaDvRxKIo9TegDHSdc7EMBXjQdCuRQFHuauPUBTsiSb9grgHw46APdRvzAVxwiSA1WNgiCcOsDzLXQvqgmLkWxpwl9gCF41MSlKPY0cesDCIKcDfoAgiDoAwiCcOsD4fuz6Bw8auJSFHua0AcY4lVNdH6Fd5NfaJ9LiWIU9jRx6wNfjvn27F13/gsJ8RNE2/mq+BmHi7l7cqf6Zc0IR6AP8EAUvWlPL3ghda7DOjGSB7JT+ChoC/vUTtYiAYQz0Ad4YHX2q8qKPe9fmKR0BwO4nfRvRXL9b7XJcLQ5tZdmrK6tF7k0ErcT1fH45EStabteeYpM38Osgb3urb3EkUI5WLhKS/U8dhZWvlztIl082FOdJzrME7/YPXlaudG6fL/uoqcXLxp0BXLcQANpp8skG2/hat6TLangL5eOKcqb/iWrD55PFDNwGw+wN6TrLZqG30XaHVDPvaroMwPsaCK0K/BkCzNpHk8n8fxoT7qSbMrd58lsEpWBfpKU064E8X7eVppNNs0fnYlCdl4oP6Z0BtfgDYU+XxQ/sKeJOeNCCEPI9VZToJynVbuD+KCv0JjCyVlaS8rd99PA0ikdCx+I5kMLTs/TKkSZTidrxA4C3kAfYBF7mz8P5NTjt4fTidxPzdLa3j09mIKR3hIMH4dDGgqcmqd1tvjPmIrveUs50k24bRfwTLga/d2vV16tmyU6xevxLK1tNLpDNQ2sTPeIChug87bC83latR+zYHehiZqQLsGtD7A3lON8TSlIoZeTylsO4jW9yS+H2X6v9XK/rMl2HFeztD7fE4ppYDdPdr1MDGEJehjGdN7WND+epzV3Ul3MiG1c8v3pPJ8odkAfYIhak6nPHw3hP4Bh/CuigUE//8ehCUW8HrTH+dxmv35lt8/3hHr3me7LgyrOH+4yiPKHh4cVwEj7+U82PSrA9uc/yhA2g0v+ang+UezA7Twm7E01+RZNmUja8gKcnHs12Ny3D3p9ltaCZhrYTdqa9vlU+J8JArju7ceI4gf2NGFLj0mavroTc6/aw+NdX9qTcjAN7Kg9HunUPK00k2wGCG+gD3DH2y7Tg2lg4Zx6rH/GPghjcOsD7A3luI4m/dpzJuKJ6gLoAwyhvWHyVmbmd+TyRF27AG+GWx/gkvMvbg6ne0Y+EG7vGyIIcjboAwiCcOsD7A3l+JqauBTFnib0AYbgUROXotjTxK0PIAhyNugDCIKgDzBK5mbvzwRBSrgdP8DeUI63aIqcYMKJhfN9olgBfYAhKk2Z5yQw40UgLzrY1sStD3BL5PhSyo8NIJ0AfYApSCiQQ85RNIB0A/QBlggW1cLTtUuC8AW3PhByWGOG+veNnwNI+YQfdVyeKOY0cdLp/Bz2hnSdo0kc/xjLkMKSH3n8KGFZE7fxALeYZkaCgsWUuSoH6TDoA+whjsFzlhw1DZCrgz7AJCQo8BVu23TIp4PzHbNES5Og8jK5GOcnig24rVN4jJp51MSlKPY0cesDCIKcDfoAgiDoAwiCcOsD7A3l+JqauBTFnib0AYbgUROXotjTxK0PIAhyNugDCIIwN+ABIexkk/y7NjXYpWRBNwAit6fQFLIuaVq1RFLIh2SQlNT1JQ2nKkZOw60PsDeU4w2abJ34QL6TNbDpRvrG0q2XTmkKqGmS9iZQp9ig2OupBQ+CGTld9AGuTxQzoA8wxClN+qT4yDzwMrFaX9nysEkhHz8dy0++qdcu/vmiWIc9Tdz6wBfDgcnSrav7cWAP24lSBiL4HfWBgt21C/CccCxduwifCPoAk0R22RoAiB0QLOIDVm+zD/vNLYkNqhRIvGgMmrEJxx0+2YNrF+AZm8X0CxkB3i9gkjQIgvImdeJ5Pqm84j70oqhOFiGvU8D5dz0iBnE7Cn651y42S4izRXrtMnweHa4i3gd7r4h7iyaDtP3z/9VLtFPQtlNwxlVyLEh1CpjDx8IxhtbTQu9gDdfZEyXOnv40IuisppfFXrsAHwV7Q7reoSnzLFU1VCevVl1jnyaqY79ofstj8K+t4C2irs6fRwTd1fQS3MYDX4UsARA8GBNDV+aeRdbzaCOM6xRaoVneWs8SXfJAv3Zp2eIdEQFroA8wjucBqLlF4zpDsi26rhgjsU75RveZhE/DJVmZ4tl+G1/HCIT/u3YJPohd93qgr6spEbr5iy5EXexs+eqlVJZFyv7ICNj78XFbQzDXU/PRmrp6pl8RFfqhYtK2TOBklvVqLol9U3yuxnSM9S7MRKVnHuXiOWAUt1ZjN9R68jnZ/mFEwN6Pj9t+QvZOxdfUdCAqPOxg2yRmQm92hHNBXdiv5pI6++XkQby5G+tL9zAX50lSVxuyIXuIDXd5VrZ/2FnI3onqai2BfEXW8sEVNBOh99PRYKtNyOXcO+sFzTt3Ekq0m9SIA+swF2sC+W4ggptPBfkpE1/PVqjHOGrL22v/XT4e9AGkM6zD6N9RK1AX6Q2PFDJ/TIdIhjrM1cyVJuFOIHv521C6U45yyMHQBGP7nyancXh7mEtCGu36LjRAyj0rVMUm29O57p/J6uCg54uDPoB0hr4vD2Xwk2qN/uPHA0hBoj9UsjnxzelqLo+dtZUvzGnSarnnyyxLkykoGojfgigVrem+zUtzEYWIjsVO6CQwCzu722f7Sq5fBm59gL0hXR+haacYbz2EEPmhbBXf5bm52YOiZ03vkesjtH/bs/YOUbIokpW46iSgPhA+DXqQFVV68TyFMQPdvyNVeS4IUdoaDqFMBEGUZCDtfzegG7IkaCbpKXKBvi2JRdWeZWoEgbXP9sVcL6CJEdAHGOJNmiJQyXWsneUDxb571rLhP9DnlJ1lT16mQ8gfdMMmcXb4aGmL7LJvMXgmyjKbxWA+IJeyRK/VjNbexc9VLOv52eLBmDRVt2iWHX6WAvu2gtjOBUaSL/W2JId1dp8uFso+2xdzvZimzsOtD3x5VvIU4P4N++65FaD/jz2Bomst2w4EN5+B/JQLZc/aB7/NZF2Nfv4bovIClsTIJFZ11Beg/XCXu5tmNV8P6JW8U2UV1nftHatcQBgMYCWQK9Q3BXn2M+ifyvYo1y8D+gCLJMtQ1oIfMFcT77s8V1NPGPUgXQWZ/KPapexzm2vDsnNtGm5pN1i6CKUbk+4J+liCB6sPtnt/3D9He9CllFxbtGvN2AWGmLtWqAp50bO2CfR2lpdEDjMR9pa0BD0EURH6Tl/caofvT8hdXRczWKfN7gNajdPbh3l0sGeVCySiGLl9Yhay15dd0E9lW+X69UAfYJF5PhNXRb+ZNZPIv72JvbSEeTI0knqXvi+PZEjkYqfp6lGe2BsLHqV7Z2WS46f5Zv4NEnKxp0m1b1AdS/sDIIiHxA/KrrW07Fm7h4T+XKSiZ22f5SXpB/9MevvVCB4B9DsYxj8Fuanjy/t8+ZZc36Om0ASfxgPFk1Z52UaQjYNcdraYDWhVP1n9EqSxepjtQa5fD259gLkW2hs0hfHEgP6SLBlFbUiid+WXL0Uk8FUgL7rZJEUU65qO7KT790K6zKPYynU7ziNyfLqK6h3kYt+6f45eiNF8SK/wvi1JW/KZ054138ro1SIWPWtNlu+edbl9orS/Djrr6zHvwizNivD9O/mPPk3d64Hwo+jnG23rfcyiOaGR46VeXG46zGU8yJRik3KXpUor2+e5XlITG6APMEStKW269crTR581BHJVFz3d2Zr+a7QqNbqTJNDLIgXHBYVcJSrNLDmMtFv9c+HjsHix2U3VtbbKvqVPC/WwZ63M8mKi2oKeIZ3ouiuuV6ceyS/M9nmMT2eyz7vqFDyV7UUG2LL34+PWB3hGg1iF+GBTTH/TIQ2EpW8vHyjBkNb2IT0+gvIiaALrtVd+/g1RZQOkzU271lQSCoA0++kPaM9aCF160eHs/VkggD7AJJK6iuPWO0WCSN4KpqQ4ihHqdR1d9LkdoalbWY9FTbZVcGQN5CB2XKnadzKpd6t71iAVxMgdiCC7fdkD0hahPWu68tsCIqyBPsAis7WvD/bDXYXHTJhKcPu0AOl77QNHfW7VgYtHEKfG7dMvUG4Bhotfet870T/3UPSswZZ2rY1oz9q/gjRWYHTQYfenZL7B7eNtrMLt+wfYG8rxRk0Ped0A+KWP0zLGP+zkSk+Nh8kzqb1nHTOkp8fOpKlSdaSnZY2R5heoOcKlOmgaF4WoTj+w//bCsffj4zYeYO9UvEHTIlOCqN00rq7iw2r25KVdvY2k3lN8bd9WR5ogv7rf29Ami7nYt8RDURzBniZufYBremGsj5vfWp+5k6hNl/l2q++DAqHLz/Rd4q5I12HuJ4QQ9INnYbo4a+Fv0CZLyIOgCQoYlMAX6AMs8fPaBbgw2XY3eX8uyPvh1geYa6Gdo+mva5fgYoRLOiZJ7PVEHqNu9n586AMMwY+mcJnnoA00rkTtYU8Ttz6AdJhwkYv9Hg4i6A7oA8inEy61PntVJtegDyCfTZbcYyjQMbg9IexNNfllNIlW+1fHiagD2NOEPsAQPGriUhR7mrj1AQRBzgZ9AEEQ9AEEQbj1AR7vS/GoiUtR7GlCH2AIHjVxKYo9Tdz6AIIgZ4M+gHQEOpnC+ZsPyd/wPW/Z98sgfcVJG5Au8pCW847sMuXU5jZHu0C8tCDxdhqp1eJdEAR0srJw45x+s/I6xxetPoPbeIC9oRxfU9NzUV7w22OOd5nrkP67cemEZPEuDMOETsEgaAv71MHWIvntF1xaU+fh9vkC9l4R9zU1PRd1xtysR7vsYADSX8lDsSKWiS9PyaoZq9vP1tR5uPUBhDnyZTFd61wbZv/RdbOZd7iel7WZ31Ubtmd1tek716WDWr6ZkvVgmld/G0r38uAhvcjrVnmC23YBwhwOTJRlTjsGxdlspif7Zvw8nk7iOflMZpMoKfsO58HwezmBWZK0G/zZYzEDbD0lKyTbbJo/OhNlA7BQfkwlkvL7lsdXA30A6QrWxBxDOU2TqqbOsJlZJYoGhtmPojAeGloV6odRb6CUPYgptHxAm2nBrxj2U7KCMTV04d4wkxyEKNMFkIQP7yBgDm59gLkW2hfV1BJVTtdaLkdP1v5OVlLNy5q2bgCk0Hpnc+tWoGSObvMd7Kdkbc/JOsv+m1Nr+Oh3IrJ3otAHGIJHTYei4nry1Ud1ut8q0UldI5A0+hnX25pOeRkO63dZIq0CMYLnU7JqP2bB7jJzMp2viQmwnxDpCuV0rXQpexSmJLKvq+1mXtbW/K7aflZXSaXekGXk/6QlIAhuOoaTU7LmrqGLGXES/fxCfRHQB5CuIJXTtRLCBP4tZ1otqedlbc/v2prVdbjsi+lPgAf4f4K9EaQBMZOTU7Jul6APYTPgNgr+Y7id5xRhjRyEl+/n7Wdw3c/vut+2SZvpUPKknorxVPhPogVw3Y8ePsAg3MYD7A3l+Jqa9qKE12ZRLa/4w/ld97X6qDUHfNMUkF7IJpvBR8PeieI2QmJvaOfX1PQmUT0t1e/NEwlvmZm8//EzKLF3oriNBxAO0bGD74NAH0Cuy+78Kc/5md6xc6APINdlcHZMz9tsz12C2/4B5npqvqgmLkWxpwl9gCF41MSlKPY0cesDCIKcDfoAgiDoAwiCcOsD7A3l+JqauBTFnib0AYbgUVNbVFa8aZQD2DtROH4A6QiRE0y4rZa6DvoA0gUyz0lgxN79Nl5AH0CuT+T4Sg77FxIinw23PsBj1cKjJtBIKJBLEMGw//7MOgJ7Jwp9gCF41AT5L6BvHQXYbq9dlIvB3onC9xEhVyZznOIdwlP2rh5+wHlOkSsjaH2dTkzia9wGp90HfQC5PpLVE+LcQyO4Gtz6QMjhb4pHTZWoIiiweTEC9k4UtwM32BvS9TU17UWps28Jjie8EswZF8IvonXtEnxZuI0HEAQ5G/QBBEG49QEeb0bzqIlLUexpQh9gCB41cSmKPU3c+gCCIGeDPoAgCPoAgiDc+gB7Qzm+piYuRbGnidtxROxNPf01Ne1F7WQ6k/Ha1Hb0KWTdIP9Ebk8hCWRd0uhuVQr5kAySkLq+pHXxrQXsnShufQBhDVsnPpDvZM2m/e3FCOOtl05JAqhpkvYmdKlIsUGx11MLHgQzcrroA+yBPoB0Dn1SfmYeeJlYrq9sedikkI+fjuUn39RrF5UX0AeQzuLAZOlW9f04sIftNCkDEfxO+8D5E7p/PuFYOljn1geYa6F9UU0tUZFdNQdiBwT6yJFj9TZN3G9uSWhQpyReNAbN2ITjTv6AK01nz+j++WwW0wMj4PZ+AY/XDI+aWqLSIAiKnvbE83zyEcZ96EVRlSpC3qQ4/65HxB9uR8Ev99rlf11TVxFni7S93kk7Rb4kBmn85/+rFoD2Ctp2Cs64TI0FqUkxh4+FYQytp4Uu/dGXfXXE2VM7IuA2HkBYJ/MsVTVUJy/XXGOfJKpjv2h9y2Pwr11ORjmMCDAeQDpHlgAIkgtjUkspc4+s59FGGDcpZMHy1nqW6JIH+rVLyyoHEQG3PsDeUI6vqemUKM8DUL/ZFg1WDcmm64oxEpsUus8kfBouycq0i79gNk5U2wi4nb9g1+HOWtR0IVGJ0M3OgVLTxU+Xr15Mblm2rDGCLropgpwHk7/eOAhuSMHd4kYIHT4dOJn16psZE/um+FyNyd7RLsxEpWeWSaEfKmbRNIrdUOvJ4HuxWCT+NldoRwTYT4ggH0p4+NRR8mvr0a7P0CPsSFo4F9SF/VoOqdM6/EG8uRvry+p+6SYxk+LeSfYQGy5pKAW5BU/BOblSms5CJh0VQdhhLR/0Fkh/xY/0k3Z7ek+kxt5qE3I994Qzstq5k1CiBxpxUNb1MxF6Px3yBW4+FeSnTCSRg/VPoP8uV6Ee7Kgtb+kHtz7ARE8NauJTVFvTOoz+HbUCdEGK68VsNVAg88d0sGSoz9XMlSbhTiB7+9tQulOOcs3B0ARj+58mp3F4W24TaYa0SpdyzwrVIrxPyFX9u1z3T2eVhoA+wBA8auJSVFtT35eHpNmeVGvt3TYwpFM9S/QyTCDxzelqLo+dtZUvzGnS6hPMl1mWJlNQNBC/BVEqWtN9g96PaZ+faS7s7I4sOEEw6J2VaxtufQDhlp1i/MFRkR/KFr1AYzfUe1LRodYvMgrt3/eo/TmyKJJvjatOgrYPRDa9mrOiTqcPVhgz0P07MfNzQYjS1rgIZSIIoiQT3wA3oBuyJGhmJAyfyGVPNmVqBLSxkCRZnEpn5HpYyg/TjyAXJwKVtKq1s3yg2HfPWjb8h28q5A+6YZOwOsgtf35Hrovw0dIW2Qe/xsAyn29badR9JHqxZmX1TeL8sp6fLR6MSVN1i2bZ4WcpsG8rVAFBMB8UhrDO7tPFQlFhBMm/6+kZuR6APoAwxEqeAty/Yd89twL0/7En4OYzkJ9y4Qag979Ar/vpPtgH1tXw57/3m7yweFhCEiOTeNZhZ4D2w13ubprVfD2gV/JOlVVY37V3jCobAN8U5NnPgFqfrEVn5dqGWx9gY0gXatqLSpahrAU/YK4m3veVmnrCiAS86SrI5B/VLmWf21wbFr1f03BLe7/SRSjdmMWeoI8leLD6YLv3z/rnyH+0R03MXStUi370mF4nedGjtgn0dp6X0kSRw0yEliWlGWQpqZVtoYgRhL7TF7faQeySu7oukgp9ndbHDWg1Tm8f5tHBNy1BD0EkMmSvL7ugQyhLfmi9musp0AcYgkdNe1HzfCauEuIHvjWTEr83sZeWAPNkaCT1vn1fHsmQyHSf6epRntgbCx6le2dl0uOn+Wb+DRJysadJtW9QHUub0BDEw6pDjYYUtEPNKnvXSfycHOR5KU1FoYN/Jr39avIvwAP8PyEPzDJaH8Y/Bbmu5Mv7fPmWXN+kng8a5T7dt3jiKi/bCHLZNorgEUAnR09WvwRprMImEATr5tVcT8GtDyCMEcYTA/pLsmTQStCagPLLN8NoMAAlL3rZJEUWxaqGI/vo/r2QLvMotnLdjpUoIsenq6iuAst96/45eiFG8yG5wnPaoeY3HWqkES3QxnbWzvOce/lno/110EkvVyP5mxH9wizNaPz+nfxHWwq9Hgg/in4+GG2rXcyiWaGRfKReeduxKmLzWIByl6U0l7s0VYTXcz0F+gDSDdKmW6/4TdJHDSEy0+J5wmxNNxmtuozuIwn0akjBcUGJlYQer0Fy+KqyVv9c+DikbzZbZd/Sp4WqwAjSn+vpUT9dmeeF+d1FJj3vuyuvV6d6QkGY7fMav5RL1RnYZPZirn9SRAT5HDSIVYjbW2JycUoQkgBY+vbycRIMe+UnOT6C8sffxNNrr/z8G6LSBmgoIM1++rSulLSIxg2RCSF08k2Hs/dncSbc+gCPTWkeNTWiJHUVx/uXigSRvBVM0BRHMUK9rqOLPrej49WtrMeipMm2Co6sgRzEjitV+04m9X51j5rs9mUPDNqhFoS0J6DoUdMVuCDsnSj0AYbgUdNe1Gzt64PmHb/CYybQZ+FunxYgfa994KjPrTxu8Qji1CB7/gLlFmC4+KX3vWf7RvBQ9KhNVv8K0lgpO9RokD1q96j9MZlv7B2KvRPF7fsHEBZ5yKsWwC99nFbt28POrfTEOJg8kw72rGOG9PSYmTwtar80rdsCaX6B6jBcav3DxgUbr4soS8ltPICwxiJTgqjVIq4v4sOGwKlLu34biXh0xAtj6YXyR7/vR7vI2z20yZMv9SxGH+THeADpCEEYS2YdUdvynzxDcF3CRQ5g7IMCO792ic5BKEZScusDPI654VETT6IKIxDEnhUzp4nbdgFHPy+uNcHTtQtwYfJ0uzOYO1Hc+gDCCAMmetPOoooH+qbz/rw+GfQBBLkMxAaE3Oh3ckTS70AfQJCLED6B1DcZvV/ArQ8w10L7opq4ERUu96EAe5q4vV+AIJ/JwXhC9uA2HkCQz0T8uDccfkrxr10ABEGuDvoAgiDc+kD4/iw6B4+auBTFnib0AYbgUROXotjTxK0PIAhyNugDCIKgDyAIwq0PsDek62tq4lLUxTQ1bzDIW29uzODyLzbgdhwR/rwYgUdRjaZdSt+KrJVLsvHWqy1e3wIk9HVL0eLvqJzEwVyOjbVuvjGn38GtDyDI9bFBTZO0N6FL0g5+vPFym9N3BW3FckrCJALHkCTqANbj9wtfuOgDCPJx6MQCVrY8pEvRf/bNmw7eQfvdDKaZOwY1hljUjNXtZcuJPoAgH8s4sIsZVKR6spR0Uc7oWs3IWs/UWkzwulTGAEuhmLXILl67njR9eFtpR9+D6oazwUN6kZerNnDbT8jeUI6vqYlLUUeazDQjl/NurlXPIs3T23FIZ3QtZ2SFR7g3VmR1CzNJtzNIneIlrUlCJ1fJwqDsFsxX9jftIQAYzUj1HVy2xOgDDMGjJi5FHWkSaQd/vKunYI2iga63Iv4w1nM9jelEq4bQAwdsqfCBlE7LDltV2RS7LaN7adovZzuWhOS3hXgT2C5AkA8mpj3+xiR8SIvpGZKjyRTrmVrLiVYNZ2DXJkHsY7P7Dv/ltJkwgSQGXY97xTV74dlY0QcQ5GPJ3HIuBq2cvZzU8onaDAFI9jO1lvTny6xclcl+9u5Wgbs5fceJkCyLzZGqXGYGpjboAwjycWRJHm2KXr8sjoMyDlCknZTQmdyrGVnrmVrLIwzJMctFSY3B1Ohs7j9E2tKQ74vNv4BOBa1ftpzc+gDXw1N4gkdRe02eB4oxEoslyRyWG2+WD3J/t5+RtZ6ptaS37VdLw2W/nHztWS/eZnDhjj18PyGCfDqp5CzplVfPyFrP1EpZBj/qxU1aT9seLb7XGx+GhuteePgAv/cLEKS7HM/hKuxtIHP7zfJIqZfUxgbg3oBsBhdGGl37T4IgXxHphRZ+Ivb39wJeaDRpF75bgO0CBEE47ifkcU5QHjVxKSp8w+Stf127sAXoAwzBoyYuRYXnX9w/r13WEuwnRBAEfQBBEPQBBEG49QEOW51cauJSFHua0AcYgkdNXIpiTxO3PoAgyNmgDyDIR5K52bWLcAbcjh9AkA4QOcGEhbqWWx/gcngKh5q4FFVpyjwngRkT+tAHGIJHTVyKKjRFji+mjNgAvz6AIFeEhAIZ5MzYAPoAglyeYFEtvOGBo6uCPoAgF0f/btPbBAJMGYkH+H0PCY8Ox6MmLkXJgt7X6DQlvsaGPG59gI0/P2riUlShSbZ6kOQeG0bArQ8gyLUpggJXZcEIWCgjgrCKdpv5CgMDidAHEOQjEa1rl+CsUl67AB/FF5g+kw94FMWeJvQBhuBRE5ei2NPErQ8gCHI26AMIgqAPIAjCrQ8wMp7zy2viUhR7mri9b8jeqfg4TbtrF/yAYCK1V/FEdQFufQBpMbh2Adpki6n0/lyQi8JtuwDpKuJskV67DMgR6APIZ4NG0D249QH2hnJ8HU0HRvCxopKrOA57J4rb/gFuX3vHA+LsqekjOCFqRy5eSSu3R25PAdjkZcpAqjacxU4x5vr4kzSFdmbVjxKwd6K4jQeQLvN608D2knD3sCyWt7st+TdNU88m/zQbThBFx1u84L3lbOf5PP824aOgLexP+NN9DNzGA8jHkYmeLu5X2km+engvIAuNk0ntiOA5+gRgZctD+r5P8Mg3kPVFOC0yLDfURFGvXlzJ06Ns7t+ttJ3n8/zbbDVSxk3/Qn/hTwd94MsS+qFi6mQhdkOtR34IblHh6QYEzj7EPSLNFIge/376Xl6K3o40heX+ALKiKhwIq3Fz3Sf0xxUv/g6LbEWrlQS/MwLCOLCJDzgwWbrtq+toQ+TVPrAOo39H1lxNvO/y1ouUsQ5zbViUeRFKNyaUSUXJlqGsBT/KLSs19YQRySVdBZn8o87Y34bSvV3kWeZW5v9g9cF278vk1sWT+6T9YW4C/dpn9Q/h1geYa6F9vKbwMIONZPqP9xpkD7phh3fUGMjWRDDCuaUuspNVm70C/Xa/Gi3GEzmN1jBIN8/GKGykm+IzDcHVpWc/tNoIXhRlbkm971i9jXPgA8cbavq+PJIh8a0ZyXWoLLc6JOWXPkr3zsqskwjzfCauEii3JH5vYi8tAebJ0Eia7BbmNJHKPMvcqvxJ0yRNqmSAoDqgV5ieBMlFTtQVQB9giHdqWssHGcxE6P10NHDzqSA/kWuO9qh5T1YV4vaEE1lsJtpjsK/GE9GQQJYc+vMvL3qw/ZujA00zc0165cZi8f1CPbxRW96+JkqEHMJ4Ar1tpDYbDzbEKfl/QEIYuiKLIt1q0OB9CLm+2x9i5bodK2VSmYcB/WW9szUB5ZdvhtFgAEpedPVLCghRpld5lrlV+VcUyaQE1Z2BXgYCLXF2kRN1Bbj1AeSYMrDdr5PIXpBoz3zuWaFaxvnZaqBkRYi7DfW5mrnSJNwJ5CgaB98pcW4Jht+0yMGwfolympbhd4mq1jaQ5vW2nbQziXm4wYym7Svz18c7x4IENth2Cs6+z/9gg+tDlq4BvrWOKn7Q9jbfdyGQ3V1QiA/IzQa1vTPZk7R1zLSwk2xd6BrBbPGfMS0zOcitokq2zGpdohaQAbMDJdEHvgwksB3K4Feha3E1+vGA1tcLO7srt25I5ZfSXzOJgEnYbE5Xc3nsrK28jINDWQBl7dY5Zlm/n2SCLOTEThZZniZ/gVZ3AmShkBeWkG3c7+uHsQEj4w2lhcw1IPMscslmThNiHG4YjcDxbp8fGq4mvd2mXpNg2DtI1iBWIW5viclORB0pn1R7ivbDW+xGz3MDKIL/KnntlZv+lsXIJA0vFRgFfeDLQAJbbR/JUh8InwY9ejWrEQRFoBDZUxrb0kiB1m/GDHT/Tsz8XBCilFSXIm1UW/15laPdOIIyuQVRkCRQm5pzo8KmaCss03tpunPeYgJZkkcbYQwujGltPffqMObZhra+sL6PQJws9PK8Mg9N3cp6LO6rakldxbHfrAaRvBVM0BRHMUK9Oih3DV3Mijzr3Ir85SB2XKlOhsmkzqXv9MWtfu7Ihs7BrQ+wN5TjczQ1kSy5AOYDWuGts/t0sVBoVbbSrHaIS38cYnltzRYPxkTS8kgJRk37fziEOM5EiRbLeDzs/t/Y3/OHnMbvEyGJwTBi6/Rv7ZQozwPFGIlgW/TbDcmuL/tnG1r0g38mZcVv6nNhGP33vUqYLR5BnLZ8aLb29UHTKBEeM4GW/fZpAdL3Wtx2CfqwzLPKrVgeLn7pfa9ObjGKfwry3QVP1OeCPsAQl9C0rirCvyEqbQB8U5BnPwPiA15IqzeJhrgRHFZt2g93ubuRBo8y6PtO9WyeqFIWZ3Q2z8ChPhBWPWX2bqaQK0uXaBBRjgiK1JO1ZUtU5huF6zQ376or+S/6z/R4Q0FvH/Jrf6VStcMdqbkHxd50Xb7PM6k5lm6YATwo9RbtW1o4mPwt2w+rE35kglDlWeVWLBt/Fx2qVXKbWZo3FxN7Pz5ufQB5RhHY7sfCkBotBFEB2evLbtFJZgs0WBCKEFc7aOrmrl7EwTdWqrX6zLz0B70aVlvaSE9ov2DdN2iqGp3lT6KtELkcz/PrN8ULt/H0fcNb9wFJ0bLZIxz23y0yJYhmz487+HZxn1bnJrW2Py8ps32EFPSBr8M+cC6I4JFcqncwWf0SpDG57PPALH7dw3aIW15OOY2DafRw2BGmphtVTmOvGDtg0AvBqVKk4qo4/9LI6LygnzVJeC+M9XH9XX28BAB94Cuh/ZW0L8z/qz6VuywtInah2iLM6KjBMmymDQUSe5M4eF8DyhOQSntQvzluKkpjGkZoRYNbPqonhf26eGpAQkm4jciunzY3sN4e9cfsUOCLUp977mCvifZxmnadeh/RUblCpZgi/BNt4ONh78fH7TynPAY6f6qpqz/LolzJPJSoD5j5E4kp+PiPianMDuA2HkD2dDoegMxzQIozGHS0lF8CHqtNhCnEXi9yIi3ddet1ql8L9AHk+qhjGhTsxN77s0L+CG7fR8TeK+K+pqZKlNi7vzV3vAhkTwe38UBX+8auoUno1kQm+3IdiCJBga/wUS+x9+Pj1geQPazcIhfZ62fnBT78F0GQ94A+gCAItz7AXAvti2riUhR7mtAHGIJHTVyKYk8Ttz6AIMjZoA8gCII+gCAItz7A3pCur6mJS1HsaUIfYAgeNXEpij1N3PoAgiBngz6A1CTp+/NA2ASfL2CbHZ2YTCvvV0duTwHYVK8MHkjVhvPyUQyY6+Mz934vof3ihMrIVeDWB9gbyvFHmmxQ0yTtFdPqbL10WkwrGKblZCXlhlNExzNwedqbJh37XZ7RqzN8hY+W1kyo/EVOVMdBH2CIU5p0YgErWx7S93uBl4n0DcOLsLj8qw01UbR/zcdKPjKI+/eXrp3ns/wPKCdURh/oENz6wFdiHNhDOnXAZOm2HzE+2hB5jQ+UUx/P1cT77nqRMtZhXs1ZnC5C6caEIon+NpJlKGvBj2rDXE09YdSDdBVkcjPrEJ0L+V4u89wW2ZXLD1YfbPe+St+XKy8mVN4EOiBdAX2AB8wtqfcdq7dxDnzgeEND35dHMiS+NZNgqCy3OiTVD+FRundWZpVEmOczcUVnNy02JH5vYi8tYZ4Mjf3sZuVcyGWeZXZV/inxlaROD6oDepDQX50ECSCdAX2AB0TIIYwn0NtG+3b5wYY4Jf8PAMo6WBZFutkgwfsQcn3XOsbKdTtWiqQyDwP6y2pfAGsCyi9figYDUCAvbpNLihBlep1nmV2Vf0WZXk+03IOMvoiIzquMdAZufYC9V0O9Q1MsSGCDbafg7Lv8Dza4PmTpGuBb+zB68u1t3rp3TPZ3QSE+INframvf4jazAsRaCjvJ1vRfYzRb/GfUMxMeZldSpu8nWt5PqPyqKIZhTxP6AEO8pClzDcg8i1yymXNTzx52uGE0Ase7PZHlatLbbZpV0kw4eGewBrEK8cERMZ27MKR3F6TKVLQf3mI3OpUdFMF/mb72yi1/k2ghMiGsHeYrnajuwq0PfBWyJI82whhcGNPKeu7V9+WfbWhTTH1MSUEKvTyvzUNTt7Iei800iJK6imN/f1wQyVvBlBRHMUK9Oih3jWIuZJpnnV2RvxzEjivV6ZNJk0sxobJ+7tAG5BNAH2CUzDeKK9nzQDFGItgWXTUku77sn21o00x9bOpzYRj9971OmC0eQZzuxxLM1r4+2PcfCI+ZMJXg9mkB0vfaPOhcyMMqzyq7Ynm4+KX3vSZ9z6g9oTLSBbid16yrc3ldRlO0jSfviz3Tus7PaCdjayriPDuerfwhr3sVfunj6risPSI9E4Qmzzq7Yr8q6KjTW9+eNxUQ3yeKFbiNB5hroZ2vKXOcDGbvFNhc7CLAwVUqHNjAIlOCaPb8uIPOQLGVVmcntRKeP8TS+g6OTxRDcBsPcEu0DT9xkvAgjCWz+S5bfvfoY6STcBsP8EkRCnyiDYB+MOiPlQlRkLcija5dAuR8gnlYPE04TJ5A23Hy37X/qAhgu4AxMs/O8s+MB5CvAbfxQMhhiyeUBbWvZ4mQ+xo38vg8UdcuwVvh9n1E7L0i7kxN6uT7UMqfuNHHjRCmNTFnXAiIvV5kL985fgBBWqAPMIk6yXyF21gO+XTQBxhFxPf7IZeD2zqFx6iZR01cimJPE/oAQ/CoiUtR7Gni1gcQBDkb9AEEQdAHEATh1gfYG8rxNTVxKYo9TeV9w907c+kC4fjgwXn2XhF3hkQONXEpij1N1fgB5t6f8pzNciK9PxcE+Yrw0y4Qp0ucrxdB/gh+fACNAEH+FI584NAImGuhnQGPmrgUxZ4mnnzgwAjYOxW/h0dNXIpiTxNXPoBNAwT5Iz7KBw4nsfRfvjqDi164aAQI8ge89txx6IeKSd9X6zlg9NsbAiezXnnu1duFJOf+ADKbrg6E1bh633VazZUnKQA/M5hYsLq56KuwxekCbx8iyBs58IHwsGGzkUz/8V4DZ9mTVulovyGcW+oie/El1tFiPJHTaA2DdHM0MCFYlrNbmsQH7g6ms7gUtRGwN5Tj9/CoiUtR7Gk68IG1fFD+mQi9n44GW2sC+W4g7jdoE2IKPeGFPBPRkECWHDrX7U25yfareXiV+2ond0v2E+VbiOAyAYFQD4pUl3ReX/ZOxe/hUROXotjT1PaBdRj9O2qF+3SKKimFLCGVur4LjWaDPyY1+jbU52rmSpNwJ5Cj/G0o3ZVT2BrWL1FOU601t6WqHpuGZSX/6lOy4MeX8YF9eMLDKGkE+UzaPtD35aEMflKt0X/8eACiENF+v3Iz3ZDSeF4mGxLfnK7m8thZW/nCnCbVFJhZv59kgizkKZ0fL0+Tv0B7fq1ny55nky8ZsjhVViJgJwTCEW0fkEWRxDNx9bBU0TH4NOiRJVsSqzq22JCVk1lmpOafge7fiZmfC0KUVlNg2W6doTK5JS4iSaA2tyWifyAnoYE5cVf6pP/kfebs1zviS5JWhGyR2yOxyyYvEwZSveW8fBRjro8/qdCh/WqXLIJcguf3CyyzWQzmAzrNyUjypd5WbjZI1AKyMiggrYfyGp8tHoyyp344hDjORIlecMbj9KDipD/p/93RvkJxYuTKtyQ3P+hNqSdaaDaoaZL2JmRx66VTOvs2hGklt9xyggjUoy2e9t4Qpp3n8/zbhI+W1nTJMtfqPAceRbGn6flVuPbLz78hKm0AhMEAVgLRVm2QxMgkP9/D6lP74S53Za9gNk9UKYszOjV34BSd99mzrzGif4vPVDu3Gn4bp06FTixgZctDyDzwMhHI6iIsr/5qS00U9erFlXzsD/fwXtp5Ps+/Tdkliz7AFuxpOvABOSSXwv5XuQQ9BFGh/fqR2xebDULf6Ytb7aAay11dF6ur3Ut/0F7B1Zb22yc09q7ib/BpmyN3SFZSX/2r2PTvJwseB/YQHJgs3YP7nkdbIq/2gbLzdK4m3nfXi5QxafzMyx7QdBFKNyYUScWfMVmGshb8KLes1NQTRj1IV0Em/2gypt2p93aR57bIrsz/weqT9tR9mdw6J3nRJbsJdECQD+TAB/rBP5PefjWCR1KH3sHOFrPBTWvDMP4pyHXLvrwRkG+JSVRzJarpRpXT2CvGDhg0HnCgtfNof9Q1MLeZ6Fi9jXPoA8+21H8TXx7JkPjWTIKhstySKzIp/2iP0r2zMqskyjyfiasEyi2J35vYS0uYJ0Mj2WdXdKeWeZbZVfmnxFeSKplEUdURvYSeIAkSQJCP5MAHtL+Sdmu+ngp5PMgUob1BmKUZjea/k/9oa7vXA+FH1oxRVr85bipKY9ry1ooORrlKMp43rD+9312EPIwn0NtGrYDmYEuckv8HxPHKoot0q0HCpCHk+q51iJXrdqwUSVUeBvSX1c5gTUD55UsRaVQp5CsLrQoIUaZXeZbZVfnXf1mavO+r7WXUMEXIAEE+EvnV1Re3Ss8v39aTCspNs/jbtvT7G9uneXEoRyxIa7DtFJxWl7/d3uL6kKVrgG9HfwJ7m7dEkt1dUIgPyM0GtbUz/XsoQJylcJNsTf81RjBb/GdMy1wOsyupkpu+2n2X7KuaWIZHUexp4nZes5dOReYamWeRKzZzbprGyeGW0Qgc7/Z5jqtJb7dpVkkzoXeQrkGsQtzeElO/DGkMJNWeov3wFrvRieygCP6r5LVXbvpbpl2yYW0w7P28zoBHUexp4tYHTpElebQRxi6MaWU995r78s+3tP5CYXUfIQUp9PK8Ng9N3cp6LO4DI0ldxbHfrAaRvBVMSXEUI9Trg3LXoN2pNM86uyJ/OYgdV6qTYTKpcym6ZPWPuaWCIDWnfCCN6mZ8Lvga+28oyHyjFOF5oBgj0bboqiHZzVX/fMuepvPU1OfCMPrve50wWzyCOG31eMzWvj5oOhCEx0yYSnD7tADpexN50O7UYZlnlV2xPFz80vtendxi1O6SRZAPQij6/nZF135WBLWC6q9+QBqCYESPf/9zV8akx88gn/sI8kv4Xiz2zPbx9cLrGSZuIGl92MrVLpFrPRuGU6jZ1U86Rrtw+r4wLa3rfFJx5+07HXn2rJvkIa9aAL/0cXVcdvCSh0wQ6jzr7Ir9qqCjSm5/e9549Y6D10o/g0dR7GlqxwPxCmicW/yQkx3I7c7942eQz3wE+UWC3PKf7vT98fXCbzJ8BCty+uBolQ+8OAywvPQz10lh9s7WWnOxi0f3O48eMlhkShDNnh13GFCJ+7Q6O6m1/Xn01foO5lqd58CjKPY0tX1A+waP2iiNU1KJxiQUdlrN0uNnkM98BLkkf97uvgGw/gn0/fH1wusZ+sn9wd84PRwG2BYDNBQIyKX2zmjgfHphrI/rL+t/QM8Lez+vLyqKPU2Hv9Y0TIaem9IetTjot0cOHz+D/NojyO1nkIH2jW2zU4E+HSPTHF8vqK8/0yxCcPA3dp8NDGwgoUCWf6YNgN4e9ff2EAlBroZU3sOqouiFAf6or/gDmVyYt1EYD3e9Ji717aEq7CQTIq94aI+uJ3ZPAWGnq1tPHvlOepO6A8gf9JmuVDV67i6CwYys+EFUUF2Wzm7bH0JzfL0gnsiwlZ8cOpFKSmTL5R32hTF04uNLrlATzINiNPMgfQJtx8l/1/61ILzS7icMFsptPk/vo9UPgLl/Gydu009In3zrk2B+bQ/EXTruV+vhA2nj5/+7GfySb2Hpfxed5f8T4KdUh8ckFsiHZZC/q8bIVQ3oTRDpE6k5vl7QTmW4zw9gu6VvNfxXK3oFgsc7fbP9dtRTWPV6ek5GR+FMcWw+gvyGdrtAHFmCcB8UAYAfjlZ3mbtPPH4G+ZVHkFvPIAebdGKVkUHreWbKCJJ/19Pm+HrhN880Awytp4VWl9oB20nBnsBzQq3XixxfTBf8GAF7w1O+qCj2NLX72NTe046Oqhck8OaTof5f6/GW/TPId9NM0FqPIMOzR5BB+zENyvvoxo/J9pddBOjr/0qavWQt2h9fL5zMcJ8fPWwMQbVIhwEquurm8Bwafajjb30ZFgFwAnvTaX9RUexpOtWrrd8D3Bok/G6ZxPEzyK88gnzwDLLQs9ztdkAi9cPbe6Es+aEFzfHNwuvPNAexIbp0yH5OPEoohwGqc7cHLyDSoGD5eV2FCMIkRz5QVZ3FCB/Qo33C8TPIrzyCfPAMMnWCnrt9Nq5iEwiCRfJpjq8XXn+mOVnRt53JxdBAkAWzHAbovOgDQIOCzFfYHxSJIB9Iu5+QtLerx2SG1eC2wPw1PeqESzLl8N5++QjyIdnv50lK0yqf5vh64USG+/zyRPjtjfnD8YQcwaMmLkWxp+nwojqqV0UTvr9+AJx8BPmc2dKaw54tvPpMs3DuIzc8NgV41MSlKPY0cRsws3cqvqYmLkWxp4lbH0AQ5GzQBxAEQR9AEIRbH2BvKMfX1MSlKPY0ld3/AldTgxb3I9kb2vl7eNTEpSj2NJU+gA/JIshXhtt2AYIgZ4M+gCAItz7AXAvti2riUhR7mtAHGIJHTVyKYk8Ttz6AIMjZoA8gCII+gCAItz7A3pCur6mJS1HsaUIfYAgeNXEpij1N3PoAgiBngz6AIAj6AIIg3PoAe0M5vqYmLkWxp6l8XzGzZHb+1kMEoS+89ZhPIQzerOW5OBh0SFy+g/dLKmR16ZwFF5oXR+/SPFsfMDv3Z2Kbbw9oMruTb5X28t77M+mWuD85O52X5eUXeko/cK33Z3IpGG8X5H9QfvEyddSlSS5SPXRK3J+cnc7Lii9VjevJ+/O4GIz7AIIgFwB9AEEQ9AEEQdAHEARBH0AQBH0AQRB+fCCPi/swjn3tglxES3rtIqCeL6WK8XFEDfEqpmruwc/78JTfXrs8F9HCC946B5i9MNj2xZOVPCh31y75n6p6QdQ8AkHrd3HUMSc+EM2FGz0L/HJNPbGHmwyvXcg/0sJW2U+zVUbgvZSovpTgQRwr1y76H6p6SZTUC4Po+7VLfgJOfGAHUw0kpRrxeeqqCWJWrqVDLWyV/SRBOlRfvtxflOYKuddh3a+qeqncYr8/j1Lp2mV/Dh8+kAVaO9ha52PIN35ujeiysUut4S7IH9Txtcv5Ri2uHwq9AZRlrwTN9QF4u5EOGxiV6U/iBCBcTzpbd4oQmsXC/pyoNug35FrazKTiZO28VBpYdXpBmN5s3Q77QK2qLcqMpVLV7gVRxXFd7JPjwweSwyc9E9JwWweD1JZ7kES+mdmG5qf9Dtrw61oi2fJ2llSWvRIkecQHkkDPyTVSpqt2IoMLnbUBUBU3vaF//P05CazEHYqkypeKk7VxTSNtJJZHuYIZuUGXnsk7raotSge7VPWSKEiCoEOPTu7hxQeOdaRerweBT/78+VRP/WAg5x16uutMLaRuUfzA0mjZa0G6n4qhGEKYG1W6aXuDrMsRNMxWwcPYaJ0TmCmx71uZX9aTqSvTUK2VTk6bbwiW63XXBypVB6JAtBtVp0RB/ABaZ56cbMOHD4iQHW1JwAshoRGYqgMbgcChFjeYkGshTOsn+GtBGgQSDNeZr0pVuqK6Aw/Ma5f7NUlTd7Mcm/tzoijk/67lVqVOQG9LLPBzNRFFP+9i5dlWJbVFtVWdEgXyTeAsph3UxIcPyBAdbclB0+lrOdij0hL5kC9iU69felELkuVAIkuB32/SzU3o6t32Okt9dM3Dc2JuE88QK21CW2KBCxv64XfZ36iq/uEPba/qlCgQNE1ZdVETJz4ge/3D9rECx+2A7jzB/lstg/KsBOGNlW6qsjeCdFccSLKdGU26udl0/oaCIidH58TcbutSKxAM4fCcpWGfhNIZcY9rl/w3qo5+aHtVJ0QVSBBfu9gn6GLf5R8wgic3id3HZoNkBts4cvd7KOkFXvv1SVrmfpqm9BcTBSsSWBZlbwTpeaqDHstyky4asdThdjT42zhzkt7ROZE0vy61ZMbL0PPb6R70JElS9LCzQ/YqVUc/tL2qE6LozSB3Dca1i34CPuIB0GfrNRHTumtwA7YN2t6KTX+hza5dzHO1LEkEqYBquq4lOHKvKHstSANFAt0hP6Ym3epipLknd2wQrd7xOTHD5uyMct8X+kYr3dWKho4VeBd6C9iHqTr6oe1VPRdFwpyFoA3UP/i2j4bx95Ru9r+SLJEPg5s8lQ76B1KxWnU6GUTvWuFjloiFQWeCAIlcl/1YUJPuru8bP++SuG1ztyyvWm3PJUCdIL+a3h1ZjahG1Z+K6o4mbuIBgnhss8KRtm73pJ3UQo1NbsouPDtZVbqtdfw0NsUTXipolSB0XMhJVTyIYqCIyG9I5Eu86Rj5yqAPsI88vXYJENbh5H4BgiDvAH0AQRD0AQRB0Ae6AiOjnJCL0aVR74z7gJi9/ZiOPrqihJfIJeuSOOEPzs5pOnTOlAtNcwpBl/roGR9HlL99vmPo0ty5bcJL/MA6Je5Pzk73ZV3kPBH0Lr2nkHEfQBDkAjDeLkAQ5AKgDyAIgj6AIMj/Dz/sOZHuHkrSAAAAAElFTkSuQmCC&quot; alt=&quot;04_grpc-deadline-cancellation-context-deep-dive-02&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 시퀀스에서 만약 Service B의 DB 쿼리가 5초 걸린다고 하자. B는
자신의 Context가 2830ms 지점에서 만료되는 걸 감지하고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;를 A로 올린다. A는 자신의 Context도 이미
만료됐으므로 같은 상태 그대로 Gateway에, Gateway는 Client로 전파한다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;체인 전체가 약 3초 근처에 한꺼번에 끝난다.&lt;/strong&gt; 이게 상대
타임아웃 모델과의 본질적 차이다. 상대 타임아웃이면 각 홉이 5초씩 기다려
최악 15초가 나왔을 것이다.&lt;/p&gt;
&lt;h3 id=&quot;8-3-여기서-틀리기-쉽다-체인의-진입점&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-3) 여기서 틀리기 쉽다:
체인의 진입점&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Gateway가 외부(HTTP) → 내부(gRPC) 변환 지점에 있으면 함정이 하나 더
있다. HTTP 요청에는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더가 없다. Gateway가 그
요청을 gRPC로 변환할 때 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;명시적으로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;withDeadlineAfter&lt;/code&gt;를 호출&lt;/strong&gt;해야 한다. 안 부르면 gRPC
체인 전체가 Deadline 없는 호출로 굴러가고, 위에서 짠 모든 전파 로직이
무의미해진다. 이 한 지점이 실무 사고의 단골이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;더 나아가 &amp;quot;HTTP 요청의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;X-Request-Timeout&lt;/code&gt; 같은 커스텀
헤더를 Gateway가 읽어서 Deadline으로 변환&amp;quot;하는 구조를 쓰는 팀도 있다. 이
경우 값이 없을 때의 기본값을 반드시 Gateway에 설정해야 한다. 없으면
Deadline 누락이 자동으로 생긴다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;체인의 출발점에서 Deadline을
반드시 심어야 한다&lt;/strong&gt;는 원칙이 이 진입점에서 제일 중요하다. 나머지
모든 홉은 &amp;quot;받은 값을 그대로 계산&amp;quot;하지만, 진입점만은 값을 생성하는 책임을
진다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;9-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;9) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;채널 조립 코드를 열고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;intercept(...)&lt;/code&gt; 라인을
찾는다.&lt;/strong&gt; 없으면 Deadline은 동작하지 않는다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration&lt;/code&gt; 선언만 있는 건 dead config다. BANDER의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcChannelFactory&lt;/code&gt;가 정확히 이 상태였다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;인터셉터 등록 순서를 확인한다.&lt;/strong&gt; Deadline 인터셉터가
가장 먼저(가장 안쪽)에 와야 한다. 트레이싱·메트릭·인증 인터셉터는
바깥쪽에 둔다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;비동기 경계마다 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.current().wrap(...)&lt;/code&gt;을
건다.&lt;/strong&gt; &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CompletableFuture&lt;/code&gt;, Reactor
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;publishOn&lt;/code&gt;, 가상 스레드 셋 다 해당된다. 자동 상속을 믿으면
안 된다. 가능하면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.currentContextExecutor&lt;/code&gt;로 executor
자체를 감싸는 쪽이 낫다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서버 로직의 무거운 루프에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled()&lt;/code&gt; 폴링을
넣는다.&lt;/strong&gt; 배치 쿼리·스트리밍 응답·외부 API 호출 직전에 체크
지점을 둔다. blocking I/O에는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;addListener&lt;/code&gt;로 취소 훅을
건다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;RetryPolicy에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;를
제거한다.&lt;/strong&gt; &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNAVAILABLE&lt;/code&gt;만 기본 후보다. Hedging은
idempotent 호출에만, 백엔드 용량 여유가 있을 때만,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hedgingDelay &amp;gt;= p50*1.5&lt;/code&gt;로 건다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;전역 retry budget&lt;/strong&gt;을 분당 호출량의 10% 이내로
걸어둔다. Thundering Herd는 backoff ±20% jitter로 막는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Circuit breaker Half-open 시점에 남은 Deadline을
체크&lt;/strong&gt;해서 임계값 이하면 fail-fast로 넘긴다. 진동 상태를 막는
유일한 방법이다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;대시보드에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CANCELLED&lt;/code&gt;를 같이 그린다.&lt;/strong&gt; 분리하지 말고 합친 SLO로
본다. 원인 분리는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;trace_id&lt;/code&gt; 조인 뒤에 한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;장애 진단은 &lt;a href=&quot;#0-불변식-5쌍-지도-deadline이-서려면-동시에-서야-하는-것들&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§0의
5쌍 표&lt;/a&gt;를 위에서부터 내려가며 본다.&lt;/strong&gt; 1번이 서 있는지 먼저
확인하는 게 가장 빠르다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;NTP 드리프트 1초는 일상이다.&lt;/strong&gt; Deadline 기반 로그
조인 시 양쪽 시계 차이를 전제로 둔다. 드리프트 자체를 별도 지표로
뽑는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Gateway 진입점에서 Deadline을 반드시 심는다.&lt;/strong&gt; HTTP →
gRPC 변환 지점이 체인의 출발점이고, 여기서 값을 생성하지 않으면 모든
하위 홉이 Deadline 없이 흐른다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;10-한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;10) 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Deadline은 설정 값이 아니라 다섯 레이어가 동시에 서야
동작하는 협업이다&lt;/strong&gt;. 인터셉터 등록·서버 취소 감지·Context
전파·Retry 예산·브레이커 정합 중 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;하나만 빠져도 3초 SLA가 9초가
된다&lt;/strong&gt;. 가장 먼저 확인할 지점은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;채널 빌더에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;intercept(...)&lt;/code&gt;가 붙어 있는지&lt;/strong&gt; — 이게 전부의
출발점이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;부록-a-운영-체크리스트&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;부록 A) 운영 체크리스트&lt;/h2&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;확인 항목&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;통과 기준&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientInterceptor&lt;/code&gt;로 Deadline 주입&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;채널 빌더에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;intercept(deadlineInterceptor)&lt;/code&gt; 한 줄
존재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;인터셉터 등록 순서&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Deadline이 가장 안쪽(가장 먼저 등록)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Wire 확인&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HTTP/2 HEADERS 프레임에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-timeout&lt;/code&gt; 헤더 존재
(Wireshark 또는 서버 인터셉터 로그)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;서버 취소 감지&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;무거운 루프에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled()&lt;/code&gt; 폴링 또는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;addListener&lt;/code&gt; 훅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;비동기 경계 wrap&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CompletableFuture&lt;/code&gt; / Reactor / 가상 스레드 경계에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Context.wrap&lt;/code&gt; 또는 context-aware executor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;RetryPolicy 대상 상태&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNAVAILABLE&lt;/code&gt;만 포함, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;
제외&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;전역 retry budget&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;분당 전체 요청의 10% 이내&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Backoff jitter&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;±20% 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Hedging 적용 기준&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;idempotent + 백엔드 여유 +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hedgingDelay &amp;gt;= p50*1.5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Circuit breaker Half-open&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;남은 Deadline이 임계값 이하면 fail-fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;시계 동기화&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;NTP 운영, 드리프트 ±1s 알람&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Gateway 진입점&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HTTP → gRPC 변환 시 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;withDeadlineAfter&lt;/code&gt; 명시 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;대시보드&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;DE + C 합산 SLO, 홉별 p99, 드리프트 별도 지표&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;태그&lt;/strong&gt;: gRPC, Deadline, Cancellation, io.grpc.Context,
ClientInterceptor, Retry, Hedging, CircuitBreaker, HTTP/2, Spring&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/GRPC</category>
      <category>cancellation</category>
      <category>Circuitbreaker</category>
      <category>ClientInterceptor</category>
      <category>Deadline</category>
      <category>gRPC</category>
      <category>hedging</category>
      <category>HTTP/2</category>
      <category>io.grpc.Context</category>
      <category>retry</category>
      <category>Spring</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/410</guid>
      <comments>https://dding-shark.tistory.com/410#entry410comment</comments>
      <pubDate>Wed, 15 Apr 2026 08:23:41 +0900</pubDate>
    </item>
    <item>
      <title>gRPC Streaming Deep Dive &amp;mdash; 4가지 모드는 왜 프로토콜이 하나인가</title>
      <link>https://dding-shark.tistory.com/409</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;grpc-streaming-deep-dive--4가지-모드는-왜-프로토콜이-하나인가&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;gRPC
Streaming Deep Dive — 4가지 모드는 왜 프로토콜이 하나인가&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;gRPC 입문서를 펼치면 거의 예외 없이 같은 네 줄이 등장한다. Unary,
Server-streaming, Client-streaming, Bidirectional. 네 가지 &amp;quot;모드&amp;quot;라는
표현 때문에 처음 보는 개발자는 이걸 네 개의 다른 프로토콜 내지는 네
종류의 다른 커넥션처럼 읽는다. 한 줄로 바로잡자면, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;네 모드는 한
HTTP/2 stream 위에서 &amp;quot;요청 메시지가 1개냐 N개냐&amp;quot; × &amp;quot;응답 메시지가 1개냐
N개냐&amp;quot;의 2×2 매트릭스일 뿐이다&lt;/strong&gt;. 프로토콜은 하나고, wire 포맷도
동일하고, HEADERS·DATA·trailer HEADERS라는 프레임 흐름의 대골격도 같다.
달라지는 건 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.proto&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;stream&lt;/code&gt; 키워드 위치와 — 그
위치가 강제하는 Java stub의 종류, StreamObserver의 라이프사이클, 그리고
flow control을 누가 책임지는가 — 이 세 가지다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;BANDER의 grpc-contracts를 열어보면 이 지점이 곧바로 체감된다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;media_service.proto&lt;/code&gt;의 서비스 정의에는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;rpc&lt;/code&gt;
선언이 두 개 들어 있는데, 어느 쪽에도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;stream&lt;/code&gt; 키워드가
없다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre class=&quot;sourceCode proto&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode protobuf&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;rpc&lt;/span&gt; CreateUploadGrant(CreateUploadGrantRequest) &lt;span class=&quot;kw&quot;&gt;returns&lt;/span&gt; (CreateUploadGrantResponse);&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;rpc&lt;/span&gt; ValidateOwnership(ValidateOwnershipRequest) &lt;span class=&quot;kw&quot;&gt;returns&lt;/span&gt; (google.protobuf.Empty);&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;studio_service.proto&lt;/code&gt;도 같다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CreateStudio&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UpdateStudioBasicInfo&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GetStudioDetail&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AssertStudioOwnership&lt;/code&gt; 전부 Unary다. 즉 BANDER의 현재
grpc-contracts에는 스트리밍 메서드가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한 개도 없다&lt;/strong&gt;. 이게
이상한 구성이 아니라 오히려 전형적이다. 서비스 간 호출 대부분은 요청 한
건에 응답 한 건으로 충분하고, 스트리밍이 구조적으로 필요한 순간은
생각보다 좁다. 이 글은 그 좁음의 이유를 먼저 프로토콜 레벨에서 설명하고,
스트리밍이 꼭 필요해지는 지점에서 어떤 함정이 기다리는지를 모드별로 한
층씩 뜯는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글을 읽기 전에 막히기 쉬운 지점을 먼저 깔아두자. 네 모드를 처음
구현할 때 거의 모두가 같은 다섯 개를 밟는다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;네 모드는 서로 다른 프로토콜이 아니다&lt;/strong&gt; — 같은 HTTP/2
stream 위에서 메시지 카운트 계약만 달라진 네 가지 형태이고, 그래서 wire
포맷과 trailer grpc-status 규약도 모두 동일하다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;으로 Client-streaming·Bidi 메서드를
호출할 수 없다&lt;/strong&gt; — 호출이 런타임에 터지는 게 아니라 stub에 해당
메서드가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;아예 생성되지 않기 때문에&lt;/strong&gt; 컴파일부터
막힌다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Server-streaming &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Iterator.hasNext()&lt;/code&gt;는 마지막
메시지 뒤에서 무한 블록된다&lt;/strong&gt; — Deadline 없이 쓰면 네트워크가
조용히 끊긴 케이스에서 호출 스레드가 영영 돌아오지 않는다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Client-streaming에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;requestObserver.onCompleted()&lt;/code&gt;를 빼먹으면 서버가 영원히
기다린다&lt;/strong&gt; — half-open stream이 누적되면서 파일 디스크립터와 힙
메모리가 조용히 샌다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Bidi에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isReady()&lt;/code&gt;를 보지 않고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext()&lt;/code&gt;를 계속 부르면 outbound 버퍼가 힙에 쌓인다&lt;/strong&gt;
— HTTP/2 flow control 창을 넘는 순간부터 gRPC는 메시지를 JVM 힙에
적재하기 때문에 결국 OOM으로 간다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;프로토콜 계약 → stub 매트릭스 → 4 모드 개별 해부 →
적용 경계 → 운영 checklist&lt;/strong&gt; 순으로 간다. 시리즈 1편(&amp;quot;HTTP/2
위에서 RPC는 왜 다시 태어났는가&amp;quot;)과 2편(&amp;quot;Deadline과 Interceptor&amp;quot;)을
전제로 하므로, 아직 안 읽었다면 같이 보는 게 좋다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-http2-stream-위-메시지-수-계약&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1) HTTP/2 stream 위
메시지 수 계약&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-java-stub-매트릭스-세-stub--네-모드&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2) Java stub
매트릭스: 세 stub × 네 모드&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-unary--기본값이-기본인-구조적-이유&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3) Unary — 기본값이
기본인 구조적 이유&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-server-streaming--iterator-vs-streamobserver&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4)
Server-streaming — Iterator vs StreamObserver&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-client-streaming--asyncstub와-streamobserver-라이프사이클&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5)
Client-streaming — AsyncStub와 StreamObserver 라이프사이클&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-bidirectional--독립-타이밍과-flow-control&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6)
Bidirectional — 독립 타이밍과 flow control&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-적용-경계--grpc-web과-http2-프록시&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7) 적용 경계 —
grpc-web과 HTTP/2 프록시&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-운영-checklist-종합&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8) 운영 checklist 종합&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#9-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;9) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#10-마치며--한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;10) 마치며 — 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-http2-stream-위-메시지-수-계약&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1) HTTP/2 stream 위 메시지 수
계약&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;gRPC 네 모드가 &amp;quot;사실 같은 프로토콜&amp;quot;이라는 말을 이해하려면 HTTP/2의
stream과 gRPC의 논리 메시지가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서로 다른 레이어&lt;/strong&gt;라는
점부터 분리해야 한다. HTTP/2 stream은 한 번의 요청-응답 쌍을 담는 논리
채널이고, 한 TCP 커넥션 위에 수백 개가 동시에 멀티플렉싱되어 흐를 수
있다. stream은 클라이언트의 HEADERS 프레임으로 열리고, 양쪽 방향에
END_STREAM 플래그가 붙을 때까지 살아 있다. 그 안에서 DATA 프레임이 몇
개가 흐르든, 그리고 그 DATA 프레임들이 담고 있는 &amp;quot;논리 메시지&amp;quot;가 몇
개든, 둘은 1:1로 매칭될 필요가 없다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;gRPC는 이 DATA 프레임 안에 한 겹 더 framing을 씌운다. 1편에서 본
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;5바이트 length-prefixed framing&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;[압축 플래그 1B][메시지 길이 4B 빅엔디안][Protobuf 페이로드 N B]&lt;/code&gt;
— 이다. 이 framing 덕분에 한 stream 위에 논리 메시지를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;몇 개든
순서대로 흘려보낼 수 있고&lt;/strong&gt;, 수신측은 TCP 세그먼트 경계와
무관하게 메시지 경계를 식별한다. 그리고 HTTP/2 stream이 양방향 독립
half-close를 허용하므로, 한 쪽이 END_STREAM을 올려 자기 방향을 닫아도
반대 방향은 계속 흐를 수 있다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 두 성질만 있으면 네 모드는 저절로 나온다. &amp;quot;요청 메시지 1 vs N&amp;quot;과
&amp;quot;응답 메시지 1 vs N&amp;quot;의 조합이 정확히 네 칸이고, 그 네 칸이 네 모드에
일대일로 대응한다. 다섯 번째 모드가 없는 건 이론적 한계 때문이 아니라 이
매트릭스가 이미 닫혀 있기 때문이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;모드&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;요청 메시지 수&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;응답 메시지 수&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;half-close 타이밍&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;전형적 사용처&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Unary&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;요청·응답 직후 양방향 동시&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;조회·명령 대부분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Server-streaming&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;N&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;클라가 요청 직후 먼저 half-close&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;실시간 피드, 구독&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Client-streaming&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;N&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;클라가 마지막 요청 후 half-close&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;로그 일괄 업로드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Bidirectional&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;N&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;N&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;양쪽 독립 타이밍&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;채팅, IoT 제어 루프&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 표에서 주의해서 볼 건 마지막 컬럼이 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;half-close
타이밍&lt;/strong&gt; 컬럼이다. 네 모드의 실제 동작 차이는 &amp;quot;메시지 수&amp;quot;보다
&amp;quot;양쪽이 언제 자기 방향을 닫는가&amp;quot;에서 온다. Unary는 양쪽이 거의 동시에
닫는 특수 케이스이고, Server-streaming은 클라이언트가 요청을 보내자마자
먼저 자기 방향을 닫는다. Client-streaming은 클라이언트가 요청을 다
보내고 half-close하면 그제야 서버가 응답 한 건을 돌려주는 순서 계약이
있다. Bidi는 이 순서 계약이 없고, 양쪽이 완전히 독립적으로 자기 방향을
닫는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;03_grpc-streaming-four-modes-deep-dive-01&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;03_grpc-streaming-four-modes-deep-dive-01&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 시퀀스에서 꼭 두 가지를 확인해야 한다. 첫째, 네 모드 모두
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;HEADERS → DATA* → trailer HEADERS(END_STREAM)&lt;/strong&gt; 라는
대골격이 완전히 동일하다는 점이다. 응답 종료 시점에 trailer HEADERS
프레임으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-status&lt;/code&gt;를 돌려주는 규약도 같다.
스트리밍이라고 해서 별도의 프레임 타입이나 별도의 상태 전달 채널이
생기는 게 아니라, 단지 DATA 프레임이 여러 개로 늘어날 뿐이다. 둘째,
Bidi의 진짜 특징은 &amp;quot;양방향&amp;quot;이 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;타이밍 독립성&lt;/strong&gt;이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Req_1&lt;/code&gt;이 나간 직후에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Resp_1&lt;/code&gt;이 돌아올 수 있고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Req_2&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Resp_1&lt;/code&gt;과 무관하게 다음에 보낼 수 있다.
이 독립성이 Bidi 설계의 모든 어려움의 출발점이고, 나머지 세 모드는 이
독립성의 일부를 포기한 특수 케이스로 읽을 수 있다. 자세한 wire 스펙은 &lt;a href=&quot;https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;gRPC
PROTOCOL-HTTP2&lt;/a&gt;에 정리돼 있다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-java-stub-매트릭스-세-stub--네-모드&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2) Java stub 매트릭스: 세
stub × 네 모드&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.proto&lt;/code&gt;를 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;protoc&lt;/code&gt;으로 Java 코드 생성하면
서비스당 정확히 세 개의 stub 클래스가 나온다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt;(Async), &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FutureStub&lt;/code&gt;이다. 이 세 stub은
지원하는 모드가 겹치지 않고, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;특정 모드에서는 해당 stub에 메서드
자체가 생성되지 않는다&lt;/strong&gt;. 이게 gRPC의 컴파일 타임 안전망이면서,
동시에 처음 오는 개발자가 &amp;quot;내 IDE가 왜 이 메서드를 못 찾지?&amp;quot;로 막히는
지점이기도 하다. 이 매트릭스를 외워두지 않으면 Client-streaming·Bidi
설계 단계에서 stub 선택을 잘못하고, 그 대가를 코드 리팩토링으로 치르게
된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Stub&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;Unary&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;Server-streaming&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;Client-streaming&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;Bidirectional&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O (동기 반환)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Iterator&amp;lt;Resp&amp;gt;&lt;/code&gt; 반환)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;X (생성 안 됨)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;X (생성 안 됨)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt; (Async)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StreamObserver&lt;/code&gt; 콜백)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StreamObserver&lt;/code&gt; 콜백)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StreamObserver&amp;lt;Req&amp;gt;&lt;/code&gt; 반환)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StreamObserver&amp;lt;Req&amp;gt;&lt;/code&gt; 반환)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FutureStub&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ListenableFuture&lt;/code&gt;)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;X (생성 안 됨)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;X (생성 안 됨)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;X (생성 안 됨)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;선택 원칙은 생각보다 단순하다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Unary만 쓰는 서비스&lt;/strong&gt;면
셋 다 가능한데, 블로킹 호출이 자연스러우면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;,
CompletableFuture와 합성해야 하면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FutureStub&lt;/code&gt;을 고른다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Server-streaming이 섞인다면&lt;/strong&gt; BlockingStub(Iterator)로도
쓸 수는 있지만, 실전에서는 Async &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt;이 제어가 훨씬 쉽다 —
뒤에 나오는 무한 블록 함정 때문이다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Client-streaming이나 Bidi가
메서드 목록에 하나라도 있으면&lt;/strong&gt;, 클라이언트는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt;(Async)로 통일한다. 다른 선택지가 없기 때문이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 매트릭스가 왜 이렇게 짜였는지는 JVM 스레드 모델을 조금만
생각해보면 자명하다. Client-streaming과 Bidi는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;같은 stream 위에
요청을 여러 번 나눠 보내는 동안 응답이 독립적으로 돌아올 수
있어야&lt;/strong&gt; 하는데, 그러려면 한 메서드가 &amp;quot;요청을 보낸 뒤 블록하고
응답을 기다린다&amp;quot;는 동기 API로는 설계가 불가능하다. BlockingStub이
&amp;quot;보내는 동안 응답이 함께 올 수 있다&amp;quot;는 동작을 표현할 방법이 없기
때문이다. 그래서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;protoc&lt;/code&gt;은 이 두 모드를
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FutureStub&lt;/code&gt;에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;아예
생성하지 않는다&lt;/strong&gt;. 문서로 &amp;quot;이건 Async stub을 쓰세요&amp;quot;라고 권고하는
수준이 아니라, 컴파일러가 그걸 강제한다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;컴파일 에러가 가장 좋은
문서&lt;/strong&gt;라는 원칙이 여기서도 관철된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 비대칭의 또 다른 결과는, Bidi를 쓰는 순간 클라이언트 코드베이스
전체가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Async 스타일로 기울어진다&lt;/strong&gt;는 점이다. Async
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt;은 반환값 대신 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StreamObserver&lt;/code&gt; 콜백으로
결과를 받기 때문에, 호출부가 &amp;quot;다음 줄에서 결과를 쓴다&amp;quot;는 동기 코드
흐름을 유지할 수 없다. 그래서 Bidi 하나 때문에 서비스 전반의 호출
스타일이 async로 바뀌고, 에러 처리도 try/catch가 아닌
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError&lt;/code&gt; 콜백으로 옮겨간다. 이 전환 비용이 작지 않기 때문에,
설계 단계에서 &amp;quot;정말 Bidi가 필요한가&amp;quot;를 한 번 더 검토해야 한다. 대부분의
경우는 Unary + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated&lt;/code&gt; 필드로 충분하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-unary--기본값이-기본인-구조적-이유&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3) Unary — 기본값이 기본인
구조적 이유&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Unary는 네 모드 중 가장 평범하고, 동시에 가장 많이 쓰인다. BANDER가
전 서비스 Unary만 쓰는 것도 이 &amp;quot;평범함&amp;quot;이 실전에서 거의 항상 충분하기
때문이고, 그 평범함을 이해하면 나머지 세 모드로 넘어갈 때의 비용이
명확해진다. 이 섹션은 Unary의 구조적 이유, BANDER의 실제 구성, Unary의
한계가 시작되는 지점, 그리고 Unary를 떠나기 전에 먼저 고려해야 하는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated&lt;/code&gt; 대체 범위까지 네 층으로 정리한다.&lt;/p&gt;
&lt;h3 id=&quot;3-1-구조적-이유--stream-1개--요청-1--응답-1&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-1) 구조적 이유 —
stream 1개 = 요청 1 + 응답 1&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Unary의 wire 레벨 동작은 1편에서 한 번 다뤘다. 클라이언트가 HEADERS
프레임 하나와 DATA 프레임 하나를 END_STREAM 플래그와 함께 보내고, 서버가
HEADERS + DATA + trailer HEADERS의 세 프레임으로 응답한다. HTTP/2 stream
하나가 이 교환에 정확히 맞춰 열렸다 닫힌다. 여기서 흔히 오해되는 지점은
&amp;quot;Unary는 블로킹이니까 HTTP/2 멀티플렉싱의 이득을 못 받는다&amp;quot;는 쪽인데,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;정확히 반대다&lt;/strong&gt;. 한 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ManagedChannel&lt;/code&gt;이 한
HTTP/2 커넥션 위에서 동시에 수백 개의 Unary stream을 병렬로 띄운다.
블로킹되는 건 호출 스레드이지 채널이 아니다. 그래서 Unary가 &amp;quot;순수한
1요청-1응답이라서 느리다&amp;quot;는 감각은 틀렸고, 성능 관점에서 Unary가
스트리밍 모드보다 본질적으로 느릴 이유가 없다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Unary가 단순한 또 다른 이유는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;에러 전달 경로가 정확히 한 번
일어난다&lt;/strong&gt;는 점이다. 서버가 중간에 예외를 던지면 trailer
HEADERS에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-status=13(INTERNAL)&lt;/code&gt;이 실려서 한 번 돌아오고,
그걸로 끝이다. 스트리밍 모드는 응답 메시지를 여러 개 보낸 뒤 50번째에서
에러가 나는 상황을 다뤄야 하는데, Unary는 그 상황 자체가 없다. 에러 처리
코드가 try/catch 한 블록으로 끝나는 이 단순함이 Unary의 운영상 가장 큰
장점이다.&lt;/p&gt;
&lt;h3 id=&quot;3-2-bander-전-서비스-unary-실사례&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-2) BANDER 전 서비스 Unary
실사례&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;BANDER의 grpc-contracts에서 실제 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.proto&lt;/code&gt;를 그대로 꺼내
보자. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;media_service.proto&lt;/code&gt;의 서비스 블록은 두 줄이
전부다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre class=&quot;sourceCode proto&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode protobuf&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;rpc&lt;/span&gt; CreateUploadGrant(CreateUploadGrantRequest) &lt;span class=&quot;kw&quot;&gt;returns&lt;/span&gt; (CreateUploadGrantResponse);&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;rpc&lt;/span&gt; ValidateOwnership(ValidateOwnershipRequest) &lt;span class=&quot;kw&quot;&gt;returns&lt;/span&gt; (google.protobuf.Empty);&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;요청에도 응답에도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;stream&lt;/code&gt; 키워드가 없다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;studio_service.proto&lt;/code&gt; 역시 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CreateStudio&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UpdateStudioBasicInfo&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GetStudioDetail&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AssertStudioOwnership&lt;/code&gt; 네 개의 RPC가 전부 Unary다. BANDER의
현재 grpc-contracts에는 스트리밍 RPC가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;단 한 개도 없다&lt;/strong&gt;.
이게 &amp;quot;아직 스트리밍 쓸 줄 모르는 초기 조직&amp;quot;의 징후가 아니라, 역으로
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스트리밍이 구조적으로 필요한 요구가 아직 없다&lt;/strong&gt;는 정직한
상태 표시다. 업로드는 요청 한 건(= URL grant를 받고 실제 바이트는 S3로
직송)으로 끝나고, 검증은 요청 한 건에 Empty 응답 한 건으로 끝난다.
여기서 스트리밍을 끼워 넣을 자리가 없다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 단순함은 클라이언트 쪽 구성에도 그대로 반영된다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcChannelFactory&lt;/code&gt;를 보면 딱 필요한 만큼만 써 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; GrpcChannelFactory &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; ManagedChannel &lt;span class=&quot;fu&quot;&gt;createChannel&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;GrpcClientProperties properties&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-4&quot;&gt;&lt;a href=&quot;#cb3-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        ManagedChannelBuilder&lt;span class=&quot;op&quot;&gt;&amp;lt;?&amp;gt;&lt;/span&gt; builder &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; ManagedChannelBuilder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;forAddress&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-5&quot;&gt;&lt;a href=&quot;#cb3-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getHost&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(),&lt;/span&gt; properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getPort&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-6&quot;&gt;&lt;a href=&quot;#cb3-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;properties&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isPlaintextEnabled&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-7&quot;&gt;&lt;a href=&quot;#cb3-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            builder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;usePlaintext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-8&quot;&gt;&lt;a href=&quot;#cb3-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-9&quot;&gt;&lt;a href=&quot;#cb3-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; builder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-10&quot;&gt;&lt;a href=&quot;#cb3-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-11&quot;&gt;&lt;a href=&quot;#cb3-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Interceptor 체인도, keepalive 튜닝도, flow control 설정도 없다. Unary
전용이므로 필요가 없기 때문이다. 그리고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcClientProperties&lt;/code&gt;의 기본 Deadline이 5초다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; host &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;localhost&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-2&quot;&gt;&lt;a href=&quot;#cb4-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;int&lt;/span&gt; port &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;9090&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-3&quot;&gt;&lt;a href=&quot;#cb4-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; deadline &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;ofSeconds&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-4&quot;&gt;&lt;a href=&quot;#cb4-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;boolean&lt;/span&gt; plaintextEnabled &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;여기서 &amp;quot;5초&amp;quot;라는 숫자가 Unary라서 가능한 값&lt;/strong&gt;이라는
점을 반드시 짚어야 한다. Unary는 호출당 응답이 정확히 한 번 돌아오니까
&amp;quot;5초 안에 끝나야 한다&amp;quot;는 계약이 자연스럽다. 뒤에 나올
Server-streaming·Bidi는 이 5초를 그대로 쓸 수 없다 — 스트림 전체가 살아
있을 예상 시간은 응답 한 건 처리 시간과는 다른 차원의 숫자이기 때문이다.
BANDER가 이 5초를 불편 없이 쓸 수 있는 이유는 전 서비스 Unary라는 전제
덕이다. 스트리밍 RPC가 한 개라도 추가되는 순간, Deadline 설계는 모드별로
분리해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;3-3-한계와-전환-기준&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-3) 한계와 전환 기준&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Unary로 표현이 안 되는 순간은 의외로 명확하다. 세 가지 중 하나에
해당하면 Unary를 떠나야 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;응답이 구조적으로 여러 개이고 실시간성이 있는
경우&lt;/strong&gt;다. 실시간 이벤트 피드, 업로드 진행률 푸시, 실시간 가격
스트림처럼 &amp;quot;지금 생긴 값을 지금 받아야&amp;quot; 하는 흐름은 Unary로는 표현이
어렵다. 폴링으로 흉내 내는 건 가능하지만, 폴링 주기보다 짧은 이벤트는
놓치고 주기보다 긴 이벤트는 지연된다. 둘째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;요청이 구조적으로
여러 개이고 전체가 메모리에 한 번에 안 올라오는 경우&lt;/strong&gt;다. 수 GB
로그 일괄 업로드, 대용량 파일 chunked upload 같은 흐름이 여기에 속한다.
한 요청 메시지에 전부 담으면 Protobuf 파싱 시점에 힙이 터진다. 셋째,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;양방향 독립 대화가 필요한 경우&lt;/strong&gt;다. 채팅, IoT 센서와 제어
루프 사이의 대화, 실시간 협업 편집(OT/CRDT)처럼 양쪽이 자유 타이밍으로
메시지를 주고받아야 하는 흐름은 Bidi가 아니면 표현 자체가
불가능하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 셋 중 어느 것도 해당되지 않으면 Unary가 정답이다. 그리고 &amp;quot;응답이
여러 개이긴 한데 실시간성은 없다&amp;quot;는 중간 영역이 가장 흔한데, 여기서
곧바로 Server-streaming으로 가는 건 성급한 결정이다. 그 중간은 대부분
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated&lt;/code&gt; 필드로 풀린다.&lt;/p&gt;
&lt;h3 id=&quot;3-4-repeated로-풀-수-있는-범위&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-4) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated&lt;/code&gt;로 풀
수 있는 범위&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Protobuf &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated&lt;/code&gt;는 배열 필드이고, 한 메시지 안에 같은
타입의 요소를 여러 개 담을 수 있다. &amp;quot;결과가 100개&amp;quot;를
Server-streaming으로 풀지, 아니면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated Item items&lt;/code&gt;가
담긴 Unary 응답 한 건으로 풀지는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;결과 집합의 크기가 경계를
가지는가&lt;/strong&gt;, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;실시간성이 있는가&lt;/strong&gt;, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;클라가
일부만 받고 중단할 수 있어야 하는가&lt;/strong&gt;, 이 세 조건으로 갈린다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb5&quot;&gt;&lt;pre class=&quot;sourceCode proto&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode protobuf&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb5-1&quot;&gt;&lt;a href=&quot;#cb5-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: 결과 크기가 경계를 가지고 실시간성이 없을 때 — Unary + repeated + pagination&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-2&quot;&gt;&lt;a href=&quot;#cb5-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;rpc&lt;/span&gt; ListStudios(ListStudiosRequest) &lt;span class=&quot;kw&quot;&gt;returns&lt;/span&gt; (ListStudiosResponse);&lt;/span&gt;
&lt;span id=&quot;cb5-3&quot;&gt;&lt;a href=&quot;#cb5-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-4&quot;&gt;&lt;a href=&quot;#cb5-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;message&lt;/span&gt; ListStudiosRequest {&lt;/span&gt;
&lt;span id=&quot;cb5-5&quot;&gt;&lt;a href=&quot;#cb5-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;dt&quot;&gt;int32&lt;/span&gt; page_size &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;1&lt;/span&gt;;&lt;/span&gt;
&lt;span id=&quot;cb5-6&quot;&gt;&lt;a href=&quot;#cb5-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;dt&quot;&gt;string&lt;/span&gt; page_token &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;2&lt;/span&gt;;&lt;/span&gt;
&lt;span id=&quot;cb5-7&quot;&gt;&lt;a href=&quot;#cb5-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;}&lt;/span&gt;
&lt;span id=&quot;cb5-8&quot;&gt;&lt;a href=&quot;#cb5-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-9&quot;&gt;&lt;a href=&quot;#cb5-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;message&lt;/span&gt; ListStudiosResponse {&lt;/span&gt;
&lt;span id=&quot;cb5-10&quot;&gt;&lt;a href=&quot;#cb5-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;kw&quot;&gt;repeated&lt;/span&gt; Studio studios &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;1&lt;/span&gt;;&lt;/span&gt;
&lt;span id=&quot;cb5-11&quot;&gt;&lt;a href=&quot;#cb5-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;dt&quot;&gt;string&lt;/span&gt; next_page_token &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;2&lt;/span&gt;;&lt;/span&gt;
&lt;span id=&quot;cb5-12&quot;&gt;&lt;a href=&quot;#cb5-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre class=&quot;sourceCode proto&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode protobuf&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기: 실시간성 없고 결과 크기도 경계 있는데 Server-streaming으로 간다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;rpc&lt;/span&gt; ListStudios(ListStudiosRequest) &lt;span class=&quot;kw&quot;&gt;returns&lt;/span&gt; (stream Studio);&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Server-streaming이 정당화되는 지점은 두 가지다. 첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;결과가
실시간 이벤트&lt;/strong&gt;여서 &amp;quot;모아서 한 번에&amp;quot;라는 전략이 의미가 없을 때.
예를 들어 스튜디오 상태 변경 구독(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SubscribeStudioEvents&lt;/code&gt;)은
언제 이벤트가 올지 모르니 Unary로 묶을 단위가 없다. 둘째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;전체
결과 크기가 메모리 한계를 넘고&lt;/strong&gt;, 동시에 클라이언트가 일부만 받고
cancel할 수 있어야 할 때. 이 조건이 아니면 pagination이 달린 Unary가
훨씬 단순하고, 디버깅하기 쉽고, 에러 전파 경로가 한 번뿐이어서 운영이
편하다. BANDER가 이 선을 지키고 있는 것도 같은 이유에서다 — 스튜디오
목록이나 미디어 목록을 반환해야 하는 요구가 생기면, 일단
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated&lt;/code&gt; + pagination으로 푼다. 스트리밍은 그 뒤의
선택지다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-server-streaming--iterator-vs-streamobserver&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4)
Server-streaming — Iterator vs StreamObserver&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;요청 한 건에 응답 N건을 받는 모드다. 대표적인 사용처는 실시간 이벤트
구독, 서버가 자기 내부 상태 변화를 클라이언트에 밀어주는 구독형 API,
그리고 결과 집합이 수만~수십만 건이고 순차적으로 소비해야 하는 대용량
목록 조회 정도다. Server-streaming을 Java에서 쓸 때는 두 가지 스타일이
있는데, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;의 Iterator와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt;(Async)의 StreamObserver 콜백이다. 둘은 같은 wire
동작을 API 레벨에서 다르게 노출할 뿐이지만, 함정의 모양은 완전히
다르다.&lt;/p&gt;
&lt;h3 id=&quot;4-1-blockingstub의-iterator-반환&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-1) BlockingStub의 Iterator
반환&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;은 Server-streaming RPC를
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Iterator&amp;lt;Resp&amp;gt;&lt;/code&gt;로 반환한다. 코드 모양이 동기적이라
읽기 편하고, for-each 루프와 자연스럽게 어울린다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb7&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb7-1&quot;&gt;&lt;a href=&quot;#cb7-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;StudioServiceGrpc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;StudioServiceBlockingStub&lt;/span&gt; blocking &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-2&quot;&gt;&lt;a href=&quot;#cb7-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    StudioServiceGrpc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;newBlockingStub&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;channel&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-3&quot;&gt;&lt;a href=&quot;#cb7-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDeadlineAfter&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;TimeUnit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;MINUTES&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-4&quot;&gt;&lt;a href=&quot;#cb7-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-5&quot;&gt;&lt;a href=&quot;#cb7-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Iterator&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;StudioEvent&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; it &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; blocking&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;subscribeStudioEvents&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-6&quot;&gt;&lt;a href=&quot;#cb7-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;it&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;hasNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-7&quot;&gt;&lt;a href=&quot;#cb7-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    StudioEvent event &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; it&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-8&quot;&gt;&lt;a href=&quot;#cb7-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-9&quot;&gt;&lt;a href=&quot;#cb7-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasNext()&lt;/code&gt;는 다음 메시지가 도착할 때까지 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;현재
스레드를 블록&lt;/strong&gt;한다. 새 메시지가 도착하면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;next()&lt;/code&gt;로
꺼낸다. 서버가 trailer HEADERS로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-status=0&lt;/code&gt;을 보내
스트림을 정상 종료하면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasNext()&lt;/code&gt;가 false로 떨어지고 루프가
끝난다. 서버가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-status=4(DEADLINE_EXCEEDED)&lt;/code&gt;나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;13(INTERNAL)&lt;/code&gt; 같은 에러로 종료하면, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasNext()&lt;/code&gt;
또는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;next()&lt;/code&gt;가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StatusRuntimeException&lt;/code&gt;을 던지며
루프가 깨진다.&lt;/p&gt;
&lt;h3 id=&quot;4-2-여기서-틀리기-쉽다--무한-블록-함정&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-2) 여기서 틀리기 쉽다
— 무한 블록 함정&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;문제는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서버가 종료 trailer를 안 보내는 상황&lt;/strong&gt;이다.
구독형 API에서 &amp;quot;이벤트가 없으면 다음 이벤트가 올 때까지 기다린다&amp;quot;가 정상
동작이면, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasNext()&lt;/code&gt;는 다음 이벤트가 도착할 때까지 영원히
블록된다. 여기까지는 설계가 의도한 동작이니 문제가 아니다. 진짜 문제는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;네트워크가 조용히 끊겼거나 서버가 죽어서 더 이상 어떤 메시지도
오지 않는 상황&lt;/strong&gt;이다. TCP keepalive가 이걸 검출하는 데는 OS 기본
설정 기준 수 분~수십 분이 걸리고, 그동안 호출 스레드는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasNext()&lt;/code&gt;에서 움직이지 않는다. 스레드 풀 크기가 제한된
서비스에서는 이게 곧 가용 스레드 고갈로 이어진다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb8&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb8-1&quot;&gt;&lt;a href=&quot;#cb8-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기: Deadline 없이 Server-streaming BlockingStub&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-2&quot;&gt;&lt;a href=&quot;#cb8-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Iterator&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; it &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; blocking&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;subscribe&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-3&quot;&gt;&lt;a href=&quot;#cb8-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;it&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;hasNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;                         &lt;span class=&quot;co&quot;&gt;// 네트워크 끊기면 영영 안 끝남&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-4&quot;&gt;&lt;a href=&quot;#cb8-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;it&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-5&quot;&gt;&lt;a href=&quot;#cb8-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-6&quot;&gt;&lt;a href=&quot;#cb8-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-7&quot;&gt;&lt;a href=&quot;#cb8-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: Deadline을 스트림 전체 생애주기로 명시&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-8&quot;&gt;&lt;a href=&quot;#cb8-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Iterator&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; it &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; blocking&lt;/span&gt;
&lt;span id=&quot;cb8-9&quot;&gt;&lt;a href=&quot;#cb8-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDeadlineAfter&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;TimeUnit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;MINUTES&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-10&quot;&gt;&lt;a href=&quot;#cb8-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;subscribe&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-11&quot;&gt;&lt;a href=&quot;#cb8-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;it&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;hasNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-12&quot;&gt;&lt;a href=&quot;#cb8-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;it&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-13&quot;&gt;&lt;a href=&quot;#cb8-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 꼭 짚어야 하는 게 있다. Server-streaming에서 Deadline은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스트림 전체가 살아 있을 최대 시간&lt;/strong&gt;으로 해석해야 한다.
Unary에서의 &amp;quot;응답 하나 받는 데 걸리는 시간&amp;quot;이 아니다. BANDER의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcClientProperties&lt;/code&gt; 기본값인 5초를 Server-streaming에
그대로 쓰면, 정상적으로 이벤트를 주고받는 스트림도 5초 뒤에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DEADLINE_EXCEEDED&lt;/code&gt;로 끊긴다. 스트리밍 API를 추가하는 시점에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcClientProperties&lt;/code&gt;의 Deadline 필드를 &amp;quot;모드별로
구분&amp;quot;하거나, 호출부에서 명시적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;withDeadlineAfter&lt;/code&gt;를 다시
지정해야 한다. 이 숫자는 구독 API의 기대 세션 길이에 맞춰 설계 결정해야
하는 값이지, 기본값을 그대로 빌려 쓸 값이 아니다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;두 번째로, gRPC keepalive와 HTTP/2 PING을 켜 두는 게 무한 블록 함정의
두 번째 방어선이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;keepAliveTime&lt;/code&gt;을 짧게(30초 정도)
걸어두면 TCP가 조용히 끊긴 경우에도 gRPC 레벨에서 PING 타임아웃으로
감지해 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNAVAILABLE&lt;/code&gt; 에러를 던져준다. 이건 1편에서 다룬
keepalive 설정이 Server-streaming에서 특히 중요해지는 이유다.&lt;/p&gt;
&lt;h3 id=&quot;4-3-async-stub--streamobserver-콜백&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-3) Async Stub —
StreamObserver 콜백&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt;(Async)를 쓰면 Iterator 대신
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StreamObserver&lt;/code&gt;로 메시지가 콜백으로 들어온다. 호출 스레드를
블록하지 않으므로 스레드 고갈 위험이 없고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted&lt;/code&gt;를 명시적으로 처리하도록 API가 강제한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb9&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb9-1&quot;&gt;&lt;a href=&quot;#cb9-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;StudioServiceGrpc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;StudioServiceStub&lt;/span&gt; async &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-2&quot;&gt;&lt;a href=&quot;#cb9-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    StudioServiceGrpc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;newStub&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;channel&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-3&quot;&gt;&lt;a href=&quot;#cb9-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-4&quot;&gt;&lt;a href=&quot;#cb9-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;async&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDeadlineAfter&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;TimeUnit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;MINUTES&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-5&quot;&gt;&lt;a href=&quot;#cb9-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;     &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;subscribeStudioEvents&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;StudioEvent&lt;span class=&quot;op&quot;&gt;&amp;gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-6&quot;&gt;&lt;a href=&quot;#cb9-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;StudioEvent event&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-7&quot;&gt;&lt;a href=&quot;#cb9-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;fu&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;                          &lt;span class=&quot;co&quot;&gt;// gRPC 워커 스레드에서 호출&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-8&quot;&gt;&lt;a href=&quot;#cb9-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-9&quot;&gt;&lt;a href=&quot;#cb9-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Throwable&lt;/span&gt; t&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-10&quot;&gt;&lt;a href=&quot;#cb9-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;co&quot;&gt;// DEADLINE_EXCEEDED, UNAVAILABLE, 서버 INTERNAL 등이 여기로&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-11&quot;&gt;&lt;a href=&quot;#cb9-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        log&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;stream failed&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; t&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-12&quot;&gt;&lt;a href=&quot;#cb9-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-13&quot;&gt;&lt;a href=&quot;#cb9-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onCompleted&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-14&quot;&gt;&lt;a href=&quot;#cb9-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        log&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;stream closed normally&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-15&quot;&gt;&lt;a href=&quot;#cb9-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-16&quot;&gt;&lt;a href=&quot;#cb9-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;gRPC 워커 스레드 풀에서
호출된다&lt;/strong&gt;. 여기서 DB 호출이나 다른 블로킹 I/O를 하면 같은 풀을
공유하는 다른 stream의 진행을 막는다. 무거운 처리는 별도 실행기로
오프로드해야 하고, 그때 순서 보존이 필요하면 단일 스레드 실행기에 넣어야
한다. 그리고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError&lt;/code&gt;와 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted&lt;/code&gt;는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;정확히 하나만&lt;/strong&gt; 호출된다는 계약을 기억해야 한다 — 둘 다
오거나, 둘 다 안 오는 경우는 없다. 이 계약을 어기는 gRPC 구현은
버그다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-client-streaming--asyncstub와-streamobserver-라이프사이클&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5)
Client-streaming — AsyncStub와 StreamObserver 라이프사이클&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;요청 N건을 보내고 응답 1건을 받는 모드다. 대용량 파일/로그 일괄
업로드, 센서 데이터 누적 전송, 클라이언트가 배치로 모은 변경사항을 한
번에 커밋하는 흐름에 쓴다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;에는
Client-streaming 메서드가 생성되지 않는다&lt;/strong&gt;. 이유는 앞서 설명한
대로다 — 요청을 여러 번 나눠 보내는 동안 동기적으로 블록할 수 없기
때문이다. 그래서 Client-streaming을 쓰는 순간 해당 서비스의 클라이언트는
Async &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt;으로 고정된다.&lt;/p&gt;
&lt;h3 id=&quot;5-1-호출-패턴--방향이-반대인-두-observer&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-1) 호출 패턴 —
방향이 반대인 두 observer&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Client-streaming의 호출 코드는 처음 보면 두 개의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StreamObserver&lt;/code&gt;가 등장해서 헷갈린다. 하나는 &amp;quot;서버가 보내는
응답을 받기 위해 클라가 만든&amp;quot; observer이고, 다른 하나는 &amp;quot;클라가 서버로
요청을 보내기 위해 gRPC가 돌려준&amp;quot; observer다. 방향이 반대여서, 이름을
헷갈리면 한참 막힌다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb10&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb10-1&quot;&gt;&lt;a href=&quot;#cb10-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// .proto: rpc UploadLogs(stream LogEntry) returns (UploadResponse);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-2&quot;&gt;&lt;a href=&quot;#cb10-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-3&quot;&gt;&lt;a href=&quot;#cb10-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// (1) 서버 응답을 받는 observer — 클라이언트가 구현&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-4&quot;&gt;&lt;a href=&quot;#cb10-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;UploadResponse&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; responseObserver &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&amp;gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-5&quot;&gt;&lt;a href=&quot;#cb10-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;UploadResponse resp&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-6&quot;&gt;&lt;a href=&quot;#cb10-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        log&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;upload ack: count={}&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; resp&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getCount&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-7&quot;&gt;&lt;a href=&quot;#cb10-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-8&quot;&gt;&lt;a href=&quot;#cb10-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Throwable&lt;/span&gt; t&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-9&quot;&gt;&lt;a href=&quot;#cb10-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        log&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;upload failed&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; t&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-10&quot;&gt;&lt;a href=&quot;#cb10-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        latch&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;countDown&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-11&quot;&gt;&lt;a href=&quot;#cb10-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-12&quot;&gt;&lt;a href=&quot;#cb10-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onCompleted&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-13&quot;&gt;&lt;a href=&quot;#cb10-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        latch&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;countDown&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-14&quot;&gt;&lt;a href=&quot;#cb10-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-15&quot;&gt;&lt;a href=&quot;#cb10-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-16&quot;&gt;&lt;a href=&quot;#cb10-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-17&quot;&gt;&lt;a href=&quot;#cb10-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// (2) 클라가 요청을 보내는 observer — gRPC가 반환&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-18&quot;&gt;&lt;a href=&quot;#cb10-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;LogEntry&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; requestObserver &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-19&quot;&gt;&lt;a href=&quot;#cb10-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    async&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDeadlineAfter&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;TimeUnit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;MINUTES&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-20&quot;&gt;&lt;a href=&quot;#cb10-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;         &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;uploadLogs&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;responseObserver&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-21&quot;&gt;&lt;a href=&quot;#cb10-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-22&quot;&gt;&lt;a href=&quot;#cb10-22&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;cf&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-23&quot;&gt;&lt;a href=&quot;#cb10-23&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;LogEntry entry &lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; entries&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-24&quot;&gt;&lt;a href=&quot;#cb10-24&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;          &lt;span class=&quot;co&quot;&gt;// 요청을 N번 푸시&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-25&quot;&gt;&lt;a href=&quot;#cb10-25&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-26&quot;&gt;&lt;a href=&quot;#cb10-26&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onCompleted&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;              &lt;span class=&quot;co&quot;&gt;// half-close&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-27&quot;&gt;&lt;a href=&quot;#cb10-27&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;RuntimeException&lt;/span&gt; e&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-28&quot;&gt;&lt;a href=&quot;#cb10-28&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-29&quot;&gt;&lt;a href=&quot;#cb10-29&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        Status&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;INTERNAL&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDescription&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;client failed: &amp;quot;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;+&lt;/span&gt; e&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getMessage&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;asException&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-30&quot;&gt;&lt;a href=&quot;#cb10-30&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-31&quot;&gt;&lt;a href=&quot;#cb10-31&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;throw&lt;/span&gt; e&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-32&quot;&gt;&lt;a href=&quot;#cb10-32&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-33&quot;&gt;&lt;a href=&quot;#cb10-33&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;latch&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;async.uploadLogs(responseObserver)&lt;/code&gt; 한 호출로 stream이
열리고, gRPC가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StreamObserver&amp;lt;LogEntry&amp;gt;&lt;/code&gt;를 반환한다.
이 반환값에 대고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext(entry)&lt;/code&gt;를 N번 부른 뒤
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;를 부르면 클라→서버 방향이 닫히고, 그제야
서버가 응답 한 건을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;responseObserver.onNext(resp)&lt;/code&gt;로
돌려준다. 그 직후 서버가 자기 쪽 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;를 부르면서
stream이 완전히 종료된다. 이 순서 계약은 Bidi와 다르다 — Bidi는 양방향
독립이지만 Client-streaming은 &amp;quot;클라가 먼저 다 보내고, 서버가 그다음에
응답&amp;quot;이라는 순서가 강제된다.&lt;/p&gt;
&lt;h3 id=&quot;5-2-streamobserver-라이프사이클--onnext-onerror--oncompleted&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-2)
StreamObserver 라이프사이클 — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext*&lt;/code&gt;
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;(onError | onCompleted)&lt;/code&gt;&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;gRPC StreamObserver의 메서드 호출 계약은 정규식 한 줄로 요약된다.&lt;/p&gt;
&lt;pre style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext* (onError | onCompleted)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;즉 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext&lt;/code&gt;가 0회 이상 호출되고, 그 뒤에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError&lt;/code&gt; 또는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted&lt;/code&gt;가 정확히
하나&lt;/strong&gt; 호출되며, 그 이후에는 어떤 메서드도 호출되지 않는다. 이
계약은 gRPC 공식 문서에도 그대로 명시돼 있고, 구현이 이걸 어기는 건
버그로 취급된다. 문제는 클라이언트 쪽 애플리케이션 코드가 자기
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;requestObserver&lt;/code&gt;를 다룰 때 이 계약을 자주 깬다는 점이다. 두
가지가 가장 흔하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;requestObserver.onCompleted()&lt;/code&gt;를
빼먹는다&lt;/strong&gt;. 루프를 다 돌고 빠져나오면서 종료 호출을 잊으면,
서버는 &amp;quot;요청이 더 올지도 모른다&amp;quot;고 판단해 Deadline이 만료될 때까지
기다린다. Deadline을 안 걸었다면 영원히 기다린다. half-open stream이
서버 쪽에 쌓이면서 파일 디스크립터와 힙이 조용히 샌다. 이걸 방지하는
가장 안전한 방법은 try/finally로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;를 감싸는 게
아니라, try/catch로 감싸서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;정상 경로에서는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;를, 예외 경로에서는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError()&lt;/code&gt;를&lt;/strong&gt; 각각 호출하도록 분기하는 것이다.
try/finally 안에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;를 넣으면 예외가 나도
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted&lt;/code&gt;가 불려서 에러 상태가 서버에 전달되지
않는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;둘째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext()&lt;/code&gt; 도중 예외가 터졌는데
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError()&lt;/code&gt;를 안 부른다&lt;/strong&gt;. 루프 안에서 파일 읽기
예외가 나면 그 순간 RuntimeException이 올라오는데, 이걸 catch하지 않고
그대로 호출부로 전파하면 stream은 여전히 열려 있는 상태다. 서버는 여전히
다음 메시지를 기다리고, 클라는 stack unwind로 빠져나가면서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;requestObserver&lt;/code&gt; 참조를 잃어버린다. 이게 가장 흔한 half-open
누수 패턴이다.&lt;/p&gt;
&lt;h3 id=&quot;5-3-statusasexception으로-명시적-에러-래핑&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-3)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Status.asException&lt;/code&gt;으로 명시적 에러 래핑&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;서버 쪽이든 클라이언트 쪽이든, StreamObserver에 Throwable을 넘길 때는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Status&lt;/code&gt; 객체로 한 번 래핑하는 게 안전&lt;/strong&gt;하다.
그래야 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-status&lt;/code&gt;가 정확한 값으로 찍히고, description도
살아서 상대방에 전달된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb12&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb12-1&quot;&gt;&lt;a href=&quot;#cb12-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기: 일반 RuntimeException을 그대로 onError에 넘김&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-2&quot;&gt;&lt;a href=&quot;#cb12-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;RuntimeException&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;invalid entry&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-3&quot;&gt;&lt;a href=&quot;#cb12-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 수신측에서는 grpc-status=2 UNKNOWN으로 오고 이유를 알 수 없음&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-4&quot;&gt;&lt;a href=&quot;#cb12-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-5&quot;&gt;&lt;a href=&quot;#cb12-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: Status.X.withDescription(...).asException()으로 래핑&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-6&quot;&gt;&lt;a href=&quot;#cb12-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-7&quot;&gt;&lt;a href=&quot;#cb12-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    Status&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;INVALID_ARGUMENT&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-8&quot;&gt;&lt;a href=&quot;#cb12-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;          &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDescription&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;entry payload exceeds 1MB&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-9&quot;&gt;&lt;a href=&quot;#cb12-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;          &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;asException&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-10&quot;&gt;&lt;a href=&quot;#cb12-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-11&quot;&gt;&lt;a href=&quot;#cb12-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 수신측에서는 grpc-status=3 INVALID_ARGUMENT + description이 함께 옴&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;일반 예외를 그대로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError&lt;/code&gt;로 넘기면 gRPC는 이걸
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Status.UNKNOWN&lt;/code&gt;(코드 2)으로 래핑하고 description도 대부분
누락된다. 운영 로그에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNKNOWN&lt;/code&gt;이 잔뜩 찍히는 시스템은 거의
예외 없이 이 래핑을 안 하고 있다. 최소한
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Status.INTERNAL.withCause(e).withDescription(e.getMessage()).asException()&lt;/code&gt;
정도는 쓰는 게 옳다. 그리고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Status.asException&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Status.asRuntimeException&lt;/code&gt;이 따로 있는데, Client-streaming
호출부에서는 checked 예외를 던질 수 없으니
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;asRuntimeException&lt;/code&gt;이 편하고, 서버 핸들러에서는 어느 쪽이든
상관없다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-bidirectional--독립-타이밍과-flow-control&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6) Bidirectional —
독립 타이밍과 flow control&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;요청 N건, 응답 N건, 그리고 둘의 타이밍이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;완전히
독립적인&lt;/strong&gt; 모드다. 채팅, IoT 센서와 제어 서버 사이의 양방향 루프,
실시간 협업 편집 같은 곳에 쓴다. 네 모드 중 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;설계 난이도가
압도적으로 높다&lt;/strong&gt;. &amp;quot;가장 유연하다&amp;quot;는 말은 실전에서 &amp;quot;실수할 공간이
가장 넓다&amp;quot;와 같은 뜻이기 때문이다. 이 섹션은 Bidi 특유의 다섯 가지
함정을 한 층씩 뜯는다.&lt;/p&gt;
&lt;h3 id=&quot;6-1-독립-타이밍--요청과-응답은-짝이-아니다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-1) 독립 타이밍 —
요청과 응답은 짝이 아니다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Client-streaming은 &amp;quot;요청을 다 보낸 뒤 서버가 응답 한 건을 돌려준다&amp;quot;는
순서 계약이 있었다. Bidi는 그게 없다. 서버는 첫 요청이 도착하기도 전에
응답을 먼저 보낼 수 있고(예: 초기 핸드셰이크), 클라가 요청을 다 보내고
half-close한 뒤에도 서버 응답은 계속 돌아올 수 있다. 이 독립성이 Bidi의
본질이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb13&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb13-1&quot;&gt;&lt;a href=&quot;#cb13-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// .proto: rpc Chat(stream ChatMessage) returns (stream ChatMessage);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-2&quot;&gt;&lt;a href=&quot;#cb13-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-3&quot;&gt;&lt;a href=&quot;#cb13-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; responseObserver &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&amp;gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-4&quot;&gt;&lt;a href=&quot;#cb13-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ChatMessage msg&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-5&quot;&gt;&lt;a href=&quot;#cb13-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        ui&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;msg&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;                        &lt;span class=&quot;co&quot;&gt;// 워커 스레드에서 콜백&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-6&quot;&gt;&lt;a href=&quot;#cb13-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-7&quot;&gt;&lt;a href=&quot;#cb13-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Throwable&lt;/span&gt; t&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; ui&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;markError&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;t&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-8&quot;&gt;&lt;a href=&quot;#cb13-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onCompleted&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; ui&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;markChatEnded&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-9&quot;&gt;&lt;a href=&quot;#cb13-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-10&quot;&gt;&lt;a href=&quot;#cb13-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-11&quot;&gt;&lt;a href=&quot;#cb13-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; requestObserver &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; async&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;chat&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;responseObserver&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-12&quot;&gt;&lt;a href=&quot;#cb13-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-13&quot;&gt;&lt;a href=&quot;#cb13-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 사용자 입력이 생길 때마다 호출&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-14&quot;&gt;&lt;a href=&quot;#cb13-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;newBuilder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;setText&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;hi&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;중요한 건 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;requestObserver.onNext(msg)&lt;/code&gt;를 호출하는
스레드와 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;responseObserver.onNext(msg)&lt;/code&gt;가 호출되는 스레드가
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;완전히 다르다&lt;/strong&gt;는 점이다. 요청 쪽은 애플리케이션
스레드(UI 이벤트 스레드 등)이고, 응답 쪽은 gRPC 워커 스레드다. 둘 사이에
공유되는 상태(예: UI 버퍼, 메시지 카운터, 세션 플래그)는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;애플리케이션이 직접 동기화&lt;/strong&gt;해야 한다. gRPC는 이걸 대신
해주지 않는다. 이 지점에서 흔히 터지는 버그가 &amp;quot;테스트에서는 재현이 안
되고 운영에서만 가끔 터지는 race condition&amp;quot;이다.&lt;/p&gt;
&lt;h3 id=&quot;6-2-half-close는-양방향-독립적이다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-2) half-close는 양방향
독립적이다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;HTTP/2 stream은 양방향으로 독립적인 half-close를 허용한다.
클라이언트가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;requestObserver.onCompleted()&lt;/code&gt;를 부르면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;클라→서버 방향만&lt;/strong&gt; 닫히고, 서버→클라 방향은 그대로 살아
있다. 서버가 자기 쪽에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;responseObserver.onCompleted()&lt;/code&gt;를
부를 때 비로소 stream이 완전히 닫힌다. Unary나 Server-streaming에 익숙한
사람이 Bidi를 처음 만지면 여기서 두 가지 흔한 실수를 낸다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;클라가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;를 부르자마자
Channel을 닫는다&lt;/strong&gt;. 아직 서버가 응답을 내보내는 중인데 stream이
RST로 끊기고, 이미 도착한 메시지도 애플리케이션까지 전달되지 못한다.
정석은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;responseObserver.onCompleted()&lt;/code&gt; 또는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError()&lt;/code&gt;가 호출될 때까지 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CountDownLatch&lt;/code&gt; 같은
걸로 기다리는 것이다. Channel 종료는 그 뒤에 해야 한다. 둘째,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서버가 응답을 다 보냈는데 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;를 안
부른다&lt;/strong&gt;. 이건 Server-streaming에서 본 것과 같은 함정인데,
Bidi에서는 더 흔하다 — 서버 핸들러가 무한 루프 안에서 &amp;quot;더 보낼 거 없으면
그냥 return하면 되겠지&amp;quot;라고 짜는 경우가 많기 때문이다. 그냥 return하면
stream이 닫히지 않고, 클라는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted&lt;/code&gt; 콜백을 영원히 못
받는다. 서버 핸들러는 종료 시점에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;반드시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;responseObserver.onCompleted()&lt;/code&gt;를 명시적으로
호출&lt;/strong&gt;해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;6-3-isready--setonreadyhandler--outbound-flow-control&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-3)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isReady&lt;/code&gt; / &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;setOnReadyHandler&lt;/code&gt; — outbound flow
control&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Bidi에서 가장 치명적인 함정이 이 지점이다. HTTP/2는 WINDOW_UPDATE
프레임을 통해 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;수신측이 받을 수 있는 용량&lt;/strong&gt;을 송신측에
알리고, 송신측은 그 창(window) 안에서만 DATA 프레임을 보내도록 제한된다.
이게 HTTP/2의 flow control이다. 문제는 gRPC의 기본
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StreamObserver&lt;/code&gt; API가 이 HTTP/2 flow control을
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동으로 존중하지 않는다&lt;/strong&gt;는 것이다. 정확히 말하면,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext(msg)&lt;/code&gt;를 호출해도 창이 가득 찬 경우 예외를 던지거나
블록하는 게 아니라, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;메시지를 JVM 힙의 송신 버퍼에 그냥
쌓는다&lt;/strong&gt;. 수신측이 느리거나 네트워크가 느리면 이 버퍼가
무제한으로 커지고, 결국 OOM으로 간다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;03_grpc-streaming-four-modes-deep-dive-02&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;03_grpc-streaming-four-modes-deep-dive-02&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 함정을 피하는 정석 패턴은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isReady()&lt;/code&gt; 체크와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;setOnReadyHandler()&lt;/code&gt; 콜백의 조합이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientCallStreamObserver&lt;/code&gt;로 캐스팅하면 이 두 API에 접근할 수
있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb14&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb14-1&quot;&gt;&lt;a href=&quot;#cb14-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: manualFlowControl — isReady + setOnReadyHandler로 backpressure&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-2&quot;&gt;&lt;a href=&quot;#cb14-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;ClientCallStreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; requestObserver &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-3&quot;&gt;&lt;a href=&quot;#cb14-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ClientCallStreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;)&lt;/span&gt; async&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;chat&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;responseObserver&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-4&quot;&gt;&lt;a href=&quot;#cb14-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-5&quot;&gt;&lt;a href=&quot;#cb14-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Iterator&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; source &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; messageSource&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;iterator&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-6&quot;&gt;&lt;a href=&quot;#cb14-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-7&quot;&gt;&lt;a href=&quot;#cb14-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;setOnReadyHandler&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-8&quot;&gt;&lt;a href=&quot;#cb14-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// 창에 여유가 생기면 gRPC가 이 콜백을 부른다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-9&quot;&gt;&lt;a href=&quot;#cb14-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isReady&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; source&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;hasNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-10&quot;&gt;&lt;a href=&quot;#cb14-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;source&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;next&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-11&quot;&gt;&lt;a href=&quot;#cb14-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-12&quot;&gt;&lt;a href=&quot;#cb14-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(!&lt;/span&gt;source&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;hasNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-13&quot;&gt;&lt;a href=&quot;#cb14-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        requestObserver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onCompleted&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-14&quot;&gt;&lt;a href=&quot;#cb14-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-15&quot;&gt;&lt;a href=&quot;#cb14-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;setOnReadyHandler&lt;/code&gt;는 &amp;quot;송신 버퍼에 여유가 생겼을 때
불러주겠다&amp;quot;는 콜백이다. 그 안에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isReady()&lt;/code&gt;가 true인 동안만
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext&lt;/code&gt;를 밀어넣고, false가 되면 즉시 빠져나온다. 다음에
여유가 생기면 gRPC가 다시 콜백해준다. 이게 애플리케이션 레벨
backpressure다. 이 패턴을 쓰지 않고 그냥
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;while (source.hasNext()) requestObserver.onNext(...)&lt;/code&gt; 루프로
짜면, 수신측이 느린 순간 힙이 폭발한다. 로컬 테스트에서는 문제가 안
보이고 (수신측이 같은 JVM 안에 있으니 빠르다), 운영에서 원격 수신측이
느려지는 순간 터진다.&lt;/p&gt;
&lt;h3 id=&quot;6-4-manualflowcontrol-deadlock&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-4) manualFlowControl
deadlock&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;양쪽이 모두 manualFlowControl을 켠 상태에서, 양쪽이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;상대가
먼저 데이터를 보내야 내 window를 열어줄 수 있는 패턴&lt;/strong&gt;으로 짜면
교착 상태가 발생한다. 구체적으로 이런 상황이다 — 서버 핸들러가 &amp;quot;클라
요청을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext&lt;/code&gt;로 받아야 응답 window를 연다&amp;quot;고 구현했고, 클라
쪽도 &amp;quot;서버 응답을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext&lt;/code&gt;로 받아야 다음 요청을 보낸다&amp;quot;고
구현했다. 첫 메시지 이후 양쪽 모두 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isReady=false&lt;/code&gt; 상태로
멈추고, 서로 상대방이 먼저 window를 열어주기를 기다린다. 이 시점에
네트워크에서는 WINDOW_UPDATE가 전혀 흐르지 않고, 둘 다 조용히 멈춘 채로
Deadline이 만료될 때까지 기다린다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이걸 피하는 원칙은 한 줄로 요약된다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한 쪽은 반드시 무조건
밀어내는 쪽(initiator)이어야 한다&lt;/strong&gt;. 보통 클라이언트가 그 역할을
맡고, 초기 요청들을 현재 window가 허용하는 만큼 일단 선제적으로
밀어넣는다. 서버는 그에 응답하면서 자연스럽게 자기 쪽 window를 열어준다.
양쪽이 동시에 &amp;quot;상대가 먼저&amp;quot;라고 기다리는 구조를 만들면 안 된다. 이걸
코드 리뷰 체크포인트로 고정해두지 않으면, Bidi 핸들러가 생길 때마다
재발한다.&lt;/p&gt;
&lt;h3 id=&quot;6-5-iscancelled-polling--서버-쪽-취소-감지&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-5)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled&lt;/code&gt; polling — 서버 쪽 취소 감지&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;서버 쪽에서 Bidi 핸들러가 무거운 응답을 만들고 있는 동안,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;클라가 Deadline을 초과하거나 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;cancel()&lt;/code&gt;을
호출&lt;/strong&gt;하면 서버는 계속 작업을 붙잡고 있을 필요가 없다. 이때 서버
핸들러가 조기 종료하려면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ServerCallStreamObserver.isCancelled()&lt;/code&gt;를 주기적으로
폴링해야 한다. gRPC는 cancel이 왔다는 사실을 예외로 던져주지 않고, 다음
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext&lt;/code&gt; 호출이 실패할 때 비로소
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StatusRuntimeException(CANCELLED)&lt;/code&gt;이 올라온다. 그 &amp;quot;다음
onNext&amp;quot;가 몇 초 뒤의 무거운 DB 쿼리 뒤라면, 그 쿼리는 낭비된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb15&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb15-1&quot;&gt;&lt;a href=&quot;#cb15-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 서버 쪽 Bidi 핸들러 — isCancelled 폴링&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-2&quot;&gt;&lt;a href=&quot;#cb15-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-3&quot;&gt;&lt;a href=&quot;#cb15-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;chat&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; responseObserver&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-4&quot;&gt;&lt;a href=&quot;#cb15-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    ServerCallStreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; server &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-5&quot;&gt;&lt;a href=&quot;#cb15-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ServerCallStreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;)&lt;/span&gt; responseObserver&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-6&quot;&gt;&lt;a href=&quot;#cb15-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-7&quot;&gt;&lt;a href=&quot;#cb15-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; StreamObserver&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;ChatMessage&lt;span class=&quot;op&quot;&gt;&amp;gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-8&quot;&gt;&lt;a href=&quot;#cb15-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ChatMessage msg&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-9&quot;&gt;&lt;a href=&quot;#cb15-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;server&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isCancelled&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-10&quot;&gt;&lt;a href=&quot;#cb15-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;co&quot;&gt;// 클라가 끊었거나 Deadline 초과 — 조기 종료&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-11&quot;&gt;&lt;a href=&quot;#cb15-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-12&quot;&gt;&lt;a href=&quot;#cb15-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-13&quot;&gt;&lt;a href=&quot;#cb15-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            ChatMessage reply &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;msg&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;   &lt;span class=&quot;co&quot;&gt;// 여기서 DB 호출 등 무거운 작업&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-14&quot;&gt;&lt;a href=&quot;#cb15-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;server&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isReady&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-15&quot;&gt;&lt;a href=&quot;#cb15-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                server&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onNext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;reply&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-16&quot;&gt;&lt;a href=&quot;#cb15-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-17&quot;&gt;&lt;a href=&quot;#cb15-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-18&quot;&gt;&lt;a href=&quot;#cb15-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onError&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Throwable&lt;/span&gt; t&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;cleanup&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-19&quot;&gt;&lt;a href=&quot;#cb15-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onCompleted&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-20&quot;&gt;&lt;a href=&quot;#cb15-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            server&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;onCompleted&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-21&quot;&gt;&lt;a href=&quot;#cb15-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-22&quot;&gt;&lt;a href=&quot;#cb15-22&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-23&quot;&gt;&lt;a href=&quot;#cb15-23&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled()&lt;/code&gt; 체크는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext&lt;/code&gt; 초입뿐 아니라
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;무거운 루프 안 반복마다 한 번씩&lt;/strong&gt; 넣어두는 게 안전하다.
예를 들어 DB에서 페이지 단위로 결과를 읽으며
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;server.onNext(row)&lt;/code&gt;를 부르는 루프가 있다면, 각 페이지 직후에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled()&lt;/code&gt;를 확인해서 조기 빠져나온다. 그리고 cancel
시점에 cleanup 로직을 태우고 싶으면,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;server.setOnCancelHandler(() -&amp;gt; cleanup())&lt;/code&gt;로 콜백을
등록할 수 있다. 이 콜백은 cancel이 감지되는 즉시 gRPC가 한 번
호출한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-적용-경계--grpc-web과-http2-프록시&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7) 적용 경계 — grpc-web과
HTTP/2 프록시&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;네 모드를 전부 알고 나면 그다음 질문은 &amp;quot;어디까지 쓸 수 있는가&amp;quot;다.
여기서 가장 자주 걸리는 경계가 브라우저이고, 그다음으로 걸리는 게 중간
프록시의 HTTP/2 스트리밍 처리다.&lt;/p&gt;
&lt;h3 id=&quot;7-1-grpc-web은-unary와-server-streaming만&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-1) grpc-web은
Unary와 Server-streaming만&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;브라우저에서 gRPC를 직접 호출할 수 없다는 건 1편에서 이미 다뤘다.
브라우저 fetch API가 HTTP/2 trailer를 노출하지 않기 때문에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-status&lt;/code&gt;를 읽을 방법이 없고, 그래서 grpc-web +
프록시(Envoy, nginx grpc_web 모듈) 조합을 끼워 넣어야 한다. 한 층 더
들어가면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;grpc-web이 지원하는 모드는 Unary와 Server-streaming
둘뿐&lt;/strong&gt;이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;모드&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;gRPC 백엔드&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;grpc-web 브라우저&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Unary&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Server-streaming&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Client-streaming&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;X&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Bidirectional&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;X&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이유는 브라우저 fetch API가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;request body의 streaming write를
안정적으로 지원하지 않는다&lt;/strong&gt;는 점이다. 요청 바디를 만들기 시작한
뒤에 JS에서 추가로 바이트를 밀어넣는 표준 방법은 최근 Chrome의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ReadableStream&lt;/code&gt; 기반 fetch에서 비로소 정착됐지만, 크로스
브라우저 호환성이 2026년 기준으로도 완전하지 않다. WebSocket으로
우회하는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-web-websocket&lt;/code&gt; 같은 스펙이 있지만 표준 경로가
아니고, 실전에서는 관리 비용만 더 크다. 실무 결론은 한 줄이다 —
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;브라우저가 통신 대상이면 Client-streaming과 Bidi를
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.proto&lt;/code&gt; 설계에서 아예 배제한다&lt;/strong&gt;. 나중에 한
메서드라도 Bidi로 바꾸면 grpc-web 호출부 전체가 영향을 받고, 설계를
뒤집는 비용이 훨씬 크다.&lt;/p&gt;
&lt;h3 id=&quot;7-2-l4-vs-l7-프록시의-스트리밍-pass-through&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-2) L4 vs L7
프록시의 스트리밍 pass-through&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;스트리밍을 쓰는 순간 중간 프록시의 행동이 중요해진다. L4 TCP
패스스루(AWS NLB, HAProxy tcp 모드)는 HTTP/2 프레임을 해석하지 않고
바이트를 그대로 넘기므로 스트리밍이 그대로 흐른다. 문제는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;L7
프록시 중 일부가 응답을 버퍼링&lt;/strong&gt;한다는 점이다. nginx는 기본값
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;proxy_buffering on&lt;/code&gt;이 HTTP/2 백엔드에도 일부 경우 영향을
주어 Server-streaming 응답을 모았다가 한꺼번에 넘기는 현상이 관찰된다.
Envoy는 HTTP/2 aware라서 기본적으로 pass-through에 가깝지만,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;per_request_buffer_limit_bytes&lt;/code&gt; 같은 옵션이 큰 값으로
설정되어 있으면 비슷한 증상이 날 수 있다. AWS ALB는 HTTP/2를
프런트엔드에서 받지만 백엔드로는 HTTP/1.1로 다운그레이드하는 기본 구성이
있어서, 이 구성에서는 스트리밍 자체가 동작하지 않는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실제로 Bidi나 Server-streaming을 쓰는 서비스에서는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;중간의
모든 프록시 홉이 HTTP/2 스트리밍을 pass-through&lt;/strong&gt;한다는 걸 e2e
테스트로 확인해두는 게 유일하게 안전한 방법이다. 로컬 개발에서는
프록시가 없으니 잘 동작하고, 운영에서만 &amp;quot;왜 실시간 푸시가 1초쯤
밀리지&amp;quot;가 관찰되는 상황이 여기서 나온다. 중간 홉 하나라도 버퍼링을
끼워넣으면 실시간성은 망가지고, 디버깅은 tcpdump로 프레임을 직접 뜨기
전까지 원인이 안 잡힌다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-운영-checklist-종합&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8) 운영 checklist 종합&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글과 시리즈 1·2편의 함정을 합치면, 스트리밍이 섞인 gRPC 서비스를
운영에 올릴 때 점검해야 하는 항목이 자연스럽게 추려진다. BANDER처럼 전
서비스 Unary인 조직에 스트리밍 RPC가 처음 추가될 때는 특히 이
체크리스트를 한 번 훑고 가는 게 좋다.&lt;/p&gt;
&lt;h3 id=&quot;8-1-stub-선택과-호출-스타일-통일&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-1) stub 선택과 호출 스타일
통일&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;메서드 목록 중 Client-streaming이나 Bidi가 하나라도 있으면, 해당
서비스의 클라이언트는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Async &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt;으로
통일&lt;/strong&gt;한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;과 섞어 쓰면 호출 스타일이
이원화되고, try/catch와 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError&lt;/code&gt; 콜백이 코드베이스에
공존하면서 에러 처리 흐름이 갈라진다. Unary 전용 서비스는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;으로 단순화하는 게 좋고, BANDER의 현재 구성이
정확히 이 경우다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcChannelFactory&lt;/code&gt;가 Interceptor 체인
없이 단순한 이유도 여기에 있다 — Unary만 있으니 flow control과
backpressure를 신경 쓸 필요가 없기 때문이다.&lt;/p&gt;
&lt;h3 id=&quot;8-2-deadline-설계는-모드별로-분리&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-2) Deadline 설계는 모드별로
분리&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Unary의 Deadline은 &amp;quot;응답 한 건을 받는 데 걸리는 시간&amp;quot;이고, BANDER의
기본값 5초가 여기에 해당한다. Server-streaming의 Deadline은 &amp;quot;스트림 전체
생애주기&amp;quot; 기준이고, 구독 API라면 세션 예상 길이(수 분~수십 분)로 잡는다.
Client-streaming은 &amp;quot;업로드 예상 시간 + 여유 버퍼&amp;quot;가 기준이다. Bidi는
호출 자체의 예상 세션 길이인데, 채팅처럼 무기한 세션은 Deadline을 아예
걸지 않고 keepalive로 살리는 편이 맞다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcClientProperties&lt;/code&gt;의 기본 Deadline 하나로 모든 모드를
커버하려 하면, Unary 외의 모드는 정상 동작 중에 끊긴다.&lt;/p&gt;
&lt;h3 id=&quot;8-3-streamobserver-계약을-코드-리뷰-체크포인트로-고정&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-3)
StreamObserver 계약을 코드 리뷰 체크포인트로 고정&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext*&lt;/code&gt; &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;(onError | onCompleted)&lt;/code&gt; 규약을 팀
코드 리뷰 가이드에 박아두는 게 가장 안전하다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;
누락을 방지하려면 try/finally로 무조건 호출하는 게 아니라, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;정상
경로에서만 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted&lt;/code&gt;, 예외 경로에서는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError&lt;/code&gt;를 분기&lt;/strong&gt;해야 한다. 예외를 raw Throwable이
아닌 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Status.X.withDescription(...).asException()&lt;/code&gt;으로
래핑하는 것도 리뷰 체크포인트로 고정한다. 이 둘만 지켜도 half-open
stream 누수와 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UNKNOWN&lt;/code&gt; 로그 홍수는 거의 사라진다.&lt;/p&gt;
&lt;h3 id=&quot;8-4-bidiclient-streaming의-flow-control&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-4)
Bidi·Client-streaming의 flow control&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;outbound가 고처리량인 Bidi나 Client-streaming은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;반드시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isReady()&lt;/code&gt; + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;setOnReadyHandler()&lt;/code&gt;
패턴&lt;/strong&gt;으로 짠다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;while (source.hasNext()) onNext(...)&lt;/code&gt; 루프는 OOM 예약이다.
서버 쪽 Bidi 핸들러에 무거운 처리 루프가 있으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isCancelled()&lt;/code&gt; 폴링과 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;setOnCancelHandler()&lt;/code&gt;
콜백을 같이 건다. manualFlowControl을 양쪽에서 켠 경우, 한 쪽은 반드시
&amp;quot;무조건 밀어내는 initiator&amp;quot;여야 한다는 원칙도 같이 지켜야 deadlock이 안
난다.&lt;/p&gt;
&lt;h3 id=&quot;8-5-프록시와-브라우저-경계를-설계-단계에서-확인&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-5) 프록시와
브라우저 경계를 설계 단계에서 확인&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;브라우저가 통신 대상이면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Client-streaming·Bidi는 설계
단계에서 제거&lt;/strong&gt;한다. grpc-web이 지원하지 않기 때문이고, 나중에
바꾸는 비용이 크기 때문이다. 중간 프록시가 있으면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;HTTP/2
스트리밍 pass-through 여부를 e2e로 확인&lt;/strong&gt;한다. nginx는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;proxy_buffering&lt;/code&gt;을 끄거나 Envoy로 바꾼다. AWS ALB 뒤에 gRPC
백엔드를 두려면 백엔드 프로토콜도 HTTP/2로 설정돼 있는지 확인한다 —
기본값은 HTTP/1.1 다운그레이드인 경우가 많다.&lt;/p&gt;
&lt;h3 id=&quot;8-6-트레이싱과-grpc-status-로깅&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-6) 트레이싱과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-status&lt;/code&gt; 로깅&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;스트리밍 에러는 단일 응답 에러보다 원인을 특정하기 어렵다. &amp;quot;50번째
메시지에서 터진 INTERNAL&amp;quot;과 &amp;quot;첫 메시지에서 터진 INTERNAL&amp;quot;은 디버깅
경로가 다른데, 단순 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-status&lt;/code&gt; 로깅만으로는 구분이 안
된다. 스트리밍 핸들러의 로깅은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;현재까지 처리한 메시지
인덱스&lt;/strong&gt;와 함께 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;grpc-status&lt;/code&gt;를 남기도록 해야 한다.
분산 트레이싱도 마찬가지다 — OpenTelemetry gRPC instrumentation은
기본적으로 stream 단위로 span 하나를 만드는데, 이게 메시지 수준 지연을
드러내지 못하므로, 필요하면 커스텀 span을 메시지 단위로 추가 생성해야
한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;9-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;9) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.proto&lt;/code&gt;를 처음 받으면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;stream&lt;/code&gt;
키워드를 먼저 세어본다&lt;/strong&gt;. 한 개도 없으면 그 서비스는 Unary
전용이고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BlockingStub&lt;/code&gt;으로 충분하다 (BANDER의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;media_service.proto&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;studio_service.proto&lt;/code&gt;가 이
경우다). 하나라도 있으면 클라이언트는 Async &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Stub&lt;/code&gt;으로
통일한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스트리밍이 진짜 필요한지 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated&lt;/code&gt;로 먼저
확인&lt;/strong&gt;한다. &amp;quot;결과가 여러 개&amp;quot;는 Server-streaming의 근거가 아니다.
실시간성이 없고 결과 크기가 경계를 가진다면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated&lt;/code&gt; 필드 +
pagination이 정답이고, 디버깅이 훨씬 쉽다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;스트리밍 코드의 첫 번째 리뷰 포인트는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onError()&lt;/code&gt; 누락&lt;/strong&gt;이다.
StreamObserver 계약 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onNext* (onError | onCompleted)&lt;/code&gt;을
머리에 박아두고, try/catch로 정상·예외 경로를 분기한다. try/finally에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;onCompleted()&lt;/code&gt;를 넣으면 예외 상황에서도 정상 종료가 전송되어
잘못된 상태가 서버에 전달된다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Bidi는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;isReady()&lt;/code&gt; 없이 쓰지 않는다&lt;/strong&gt;.
저처리량이면 그냥 돌아가지만, 고처리량이면 manual flow control이 필수다.
이걸 생략한 Bidi 코드는 로컬 테스트를 통과하고 운영에서 OOM으로
터진다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Deadline 단위가 모드마다 다르다는 걸 잊지 않는다&lt;/strong&gt;.
BANDER의 5초 기본값은 &amp;quot;Unary 전용이니까&amp;quot; 성립하는 값이다. 스트리밍 RPC를
추가하는 순간 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrpcClientProperties&lt;/code&gt;를 모드별로 분리하거나
호출부에서 명시적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;withDeadlineAfter&lt;/code&gt;를 다시 지정해야
한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;grpc-web 뒤에 브라우저 클라이언트가 있으면
Client-streaming·Bidi를 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.proto&lt;/code&gt;에서 아예 배제&lt;/strong&gt;한다.
&amp;quot;나중에 되겠지&amp;quot;로 넘겼다가 설계를 뒤집는 비용이 훨씬 크다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;10-마치며--한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;10) 마치며 — 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;gRPC의 네 모드는 HTTP/2 stream 하나 위의 &amp;quot;요청 수 × 응답 수&amp;quot;
2×2 매트릭스이지, 네 개의 프로토콜이 아니다&lt;/strong&gt;. 그래서 wire 포맷은
동일하고, 달라지는 건 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.proto&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;stream&lt;/code&gt; 키워드
위치와 — 그게 강제하는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;stub 선택·StreamObserver
라이프사이클·flow control 책임&lt;/strong&gt;이다. 스트리밍이 꼭 필요한 순간은
생각보다 좁고, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;BANDER의 전 서비스 Unary 구성&lt;/strong&gt;처럼 Unary
+ &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;repeated&lt;/code&gt; 조합이 현실적으로 가장 안전한 출발점이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;태그&lt;/strong&gt;: grpc, streaming, http2, bidirectional,
stream-observer, flow-control, backpressure, protobuf, bander,
deadline&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/GRPC</category>
      <category>Backpressure</category>
      <category>bander</category>
      <category>Bidirectional</category>
      <category>Deadline</category>
      <category>flow-control</category>
      <category>gRPC</category>
      <category>http2</category>
      <category>protobuf</category>
      <category>stream-observer</category>
      <category>Streaming</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/409</guid>
      <comments>https://dding-shark.tistory.com/409#entry409comment</comments>
      <pubDate>Wed, 15 Apr 2026 08:23:09 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL 아키텍처 해부 &amp;mdash; Postmaster&amp;middot;백엔드&amp;middot;공유 메모리&amp;middot;WAL writer</title>
      <link>https://dding-shark.tistory.com/408</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;postgresql-아키텍처-해부--postmaster백엔드공유-메모리wal-writer&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;PostgreSQL
아키텍처 해부 — Postmaster·백엔드·공유 메모리·WAL writer&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;튜닝 가이드를 따라 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;를 올렸는데
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_backend&lt;/code&gt;가 줄지 않는다면, 건드린 건 증상이지 구조가
아니다. PostgreSQL의 파라미터는 전부 어떤 프로세스·어떤 공유 메모리
영역·어떤 백그라운드 루프에 붙는지 정해져 있고, 그 지도가 머릿속에
없으면 값만 바꿔가며 헤매게 된다. 반대로 지도가 잡혀 있으면 대부분의
파라미터는 &amp;quot;아, 이 루프의 주기를 줄이라는 뜻이구나&amp;quot;로 금방 해석된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 PostgreSQL 16/17 기준으로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;프로세스 토폴로지 → 연결
시퀀스 → 공유 메모리 → 쓰기 경로 3인 1조 → autovacuum → 확장
포인트&lt;/strong&gt; 순서로, 그 지도를 한 번 그려 둔다. 0편에서 말한 &amp;quot;학술
전통의 프로세스 per connection&amp;quot; 모델이 실제 바이너리 레이어에서 어떻게
구현되었는지 확인하는 편이다. 이후 편에서 WAL 포맷·MVCC·락 구조를 파고들
때 이 지도가 계속 기준이 된다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;프로세스 격리 = 안정성이라는 도식은 절반만 맞다&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;work_mem&lt;/code&gt;은 연결당이 아니라 플랜 노드당 곱해진다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;만 올리고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;effective_cache_size&lt;/code&gt;를 방치하면 플래너가 옛날 세계관으로
돈다&lt;/strong&gt; — 전자는 할당, 후자는 힌트다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;full_page_writes&lt;/code&gt;를 integrity 보험으로만 읽지
말자&lt;/strong&gt; — 체크포인트 직후 WAL 폭증이 복제 지연으로 돌아온다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;autovacuum이 자동이라고 방치하면 안 된다&lt;/strong&gt; —
anti-wraparound VACUUM은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;cost_delay&lt;/code&gt;를 무시하고 IO를 통째로
가져간다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_worker_processes&lt;/code&gt;는 하나의 슬롯
풀이다&lt;/strong&gt; — 병렬 쿼리·확장·논리 복제가 같은 우물에서 물을
뜬다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;왜 → 구조 → 동작 → 주의 → 실무&lt;/strong&gt; 순으로
정리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-프로세스-토폴로지-postmaster와-상주-6종&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1) 프로세스
토폴로지: postmaster와 상주 6종&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-클라이언트-연결-시퀀스-fork부터-readyforquery까지&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2)
클라이언트 연결 시퀀스: fork부터 ReadyForQuery까지&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-인증파라미터-협상의-함정&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3) 인증·파라미터 협상의
함정&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-공유-메모리-레이아웃-다섯-영역의-지도&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4) 공유 메모리
레이아웃: 다섯 영역의 지도&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-wal-writerbgwritercheckpointer--쓰기-경로-3인-1조&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5)
WAL writer·bgwriter·checkpointer — 쓰기 경로 3인 1조&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-autovacuum-launcher와-worker&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6) autovacuum launcher와
worker&lt;/a&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-5-wraparound와-anti-wraparound&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6-5) wraparound와
anti-wraparound&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-확장의-자리-background-worker와-shared_preload_libraries&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7)
확장의 자리: background worker와 shared_preload_libraries&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#9-한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;9) 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-프로세스-토폴로지-postmaster와-상주-6종&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1) 프로세스 토폴로지:
postmaster와 상주 6종&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PostgreSQL을 기동하면 가장 먼저 올라오는 단일 프로세스가
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;postmaster&lt;/strong&gt;다. 이 녀석의 임무는 두 가지다. 포트를 열어
클라이언트 연결을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;accept()&lt;/code&gt;하고, 연결이 들어오면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;fork(2)&lt;/code&gt;로 백엔드를 찍어낸다. 그리고 기동 시점에 몇 개의
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;상주 백그라운드 프로세스&lt;/strong&gt;를 미리 띄운다. 연결 수와
무관하게 항상 떠 있는 녀석들이다. 0편에서 &amp;quot;프로세스 per
connection&amp;quot;이라고 표현한 그 모델의 실제 얼굴이 여기 있다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;공식 문서는 이 구조를 한 문장으로 요약한다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;PostgreSQL uses a &amp;#39;process per user&amp;#39; client/server
model.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://www.postgresql.org/docs/17/connect-estab.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;PostgreSQL
17 §50.2 Architectural Fundamentals&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PG 16/17 기준으로 postmaster가 직접 띄우는 상주 프로세스는 여섯
종이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;프로세스&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;역할&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;등장/변화&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;postmaster&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;리스너, fork 모체, 자식 리핑&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;supervisor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpointer&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;체크포인트 실행, dirty 버퍼 디스크 동기화&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;9.2에서 bgwriter에서 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;LRU 꼬리의 dirty 버퍼를 선제적으로 flush&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;8.0~&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;walwriter&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;WAL buffer → WAL 파일 쓰기 전담&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;8.3~&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum launcher&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;naptime마다 DB 스캔, 필요 DB에 worker 스폰&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;8.1~&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;logical replication launcher&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;논리 복제 구독자 worker 관리&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;10~&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기에 선택적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;archiver&lt;/code&gt;(WAL archive 모드),
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_receiver&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_sender&lt;/code&gt;(스트리밍 복제),
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;stats collector&lt;/code&gt;가 붙는다. 다만 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;stats collector는 PG
15에서 제거&lt;/strong&gt;되었다. 15부터는 누적 통계를 별도 프로세스가 UDP로
수집하는 대신 공유 메모리에 직접 누적한다. 15 이전 블로그를 읽을 때는 이
차이를 감안해야 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;프로세스 격리니까 안정적이다&amp;quot;&lt;/strong&gt;
는 서술은 절반만 맞다. 한 백엔드가 SIGSEGV로 죽으면 postmaster는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;모든 백엔드를 강제 종료하고 crash recovery로 들어간다&lt;/strong&gt;.
공유 메모리가 오염되었을 가능성을 배제할 수 없기 때문이다. 즉 격리되는
건 &amp;quot;정상 동작하는 클라이언트 세션 사이의 메모리&amp;quot;지, &amp;quot;장애 폭발 반경&amp;quot;이
아니다.&lt;/p&gt;
&lt;h3 id=&quot;1-3-격리의-진짜-의미--work_mem은-플랜-노드당이다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-3) 격리의
진짜 의미 — work_mem은 플랜 노드당이다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 백엔드가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;work_mem&lt;/code&gt; 기반 플랜 노드를 여러 개 가진
쿼리를 돌리면 그 백엔드 하나가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;work_mem × 노드 수&lt;/code&gt;만큼 RSS를
찢어간다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;work_mem&lt;/code&gt;은 연결당이 아니라 실행 계획
노드당 할당된다&lt;/strong&gt;는 사실이 여기서 발목을 잡는다. 문서적인
설명보다 EXPLAIN 한 번이 빠르다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;EXPLAIN&lt;/span&gt; (&lt;span class=&quot;kw&quot;&gt;ANALYZE&lt;/span&gt;, BUFFERS)&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;*&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-3&quot;&gt;&lt;a href=&quot;#cb1-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;FROM&lt;/span&gt; orders o&lt;/span&gt;
&lt;span id=&quot;cb1-4&quot;&gt;&lt;a href=&quot;#cb1-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;JOIN&lt;/span&gt; customers c &lt;span class=&quot;kw&quot;&gt;ON&lt;/span&gt; o.customer_id &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; c.&lt;span class=&quot;kw&quot;&gt;id&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-5&quot;&gt;&lt;a href=&quot;#cb1-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;BY&lt;/span&gt; o.created_at &lt;span class=&quot;kw&quot;&gt;DESC&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-6&quot;&gt;&lt;a href=&quot;#cb1-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;100&lt;/span&gt;;&lt;/span&gt;
&lt;span id=&quot;cb1-7&quot;&gt;&lt;a href=&quot;#cb1-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;-- Sort 노드 + Hash Join 노드가 각각 work_mem 만큼 할당 가능&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-8&quot;&gt;&lt;a href=&quot;#cb1-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;-- 한 쿼리가 work_mem × (Sort + Hash + Merge) 소비 가능&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-9&quot;&gt;&lt;a href=&quot;#cb1-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;-- 동시 연결 200 × 3 노드 × 4MB = 2.4GB 이론 상한&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;결론은 간단하다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;fork 격리는 &amp;quot;crash 격리&amp;quot;일 뿐, 메모리 예산
격리가 아니다&lt;/strong&gt;. 격리는 메모리 소비의 방어막이 아니라 메모리
소비의 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;청구서를 프로세스별로 분리&lt;/strong&gt;해 주는 것뿐이다. 이
6종이 떠 있는 상태에서 클라이언트가 새 연결을 던지면 어떻게 되는지를
다음 절에서 따라간다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-클라이언트-연결-시퀀스-fork부터-readyforquery까지&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2)
클라이언트 연결 시퀀스: fork부터 ReadyForQuery까지&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;클라이언트가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;psql&lt;/code&gt;이든 JDBC든 간에, 연결 하나가
만들어지는 동안 내부에서 일어나는 일은 같다. TCP를 열고, postmaster가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;fork()&lt;/code&gt;로 백엔드를 낳고, 그 백엔드가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자기 자신을
초기화&lt;/strong&gt;한 뒤 인증·파라미터 협상을 거쳐
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ReadyForQuery&lt;/code&gt;를 돌려준다. 여기서 핵심은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;fork 비용이
인증보다 앞선다&lt;/strong&gt;는 점이다. 인증에 실패해도 자식 프로세스는 이미
만들어진 상태다. 커넥션 풀러가 왜 필수 인프라가 되는지 한 줄로 이해되는
지점이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;db-deepdive-01-postgresql-architecture-01&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;db-deepdive-01-postgresql-architecture-01&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-1-sslrequest의-1바이트-규칙&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-1) SSLRequest의 1바이트
규칙&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PostgreSQL 프론트엔드 프로토콜은 TLS 협상을 시작할 때 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;정확히
1바이트를 읽고 나서&lt;/strong&gt; 핸드셰이크를 시작해야 한다. 공식 문서가
이렇게 규정한다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;The frontend should read exactly one byte before initiating the
SSL handshake to prevent buffer-stuffing attacks.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://www.postgresql.org/docs/17/protocol-flow.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;PostgreSQL
17 §53.2 Message Flow&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;버퍼 스터핑 공격(CVE-2021-23222)을 막기 위한 규칙이다. 서버가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;S&lt;/code&gt; 한 바이트를 보내기 전에 추가 바이트가 같이 딸려 오면,
이후 TLS 핸드셰이크가 평문 데이터를 오해할 수 있다. 그래서 JDBC·libpq 등
드라이버들이 전부 1바이트만 먼저 소비하도록 구현되어 있다. 직접
드라이버를 짤 일이 아니어도, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;평문/암호문 경계가 이 한 바이트에
걸려 있다&lt;/strong&gt;는 사실은 보안 리뷰 때 한 번 짚어 둘 가치가 있다.&lt;/p&gt;
&lt;h3 id=&quot;2-2-backendkeydata--취소의-근거&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-2) BackendKeyData — 취소의
근거&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthenticationOk&lt;/code&gt; 직후 서버가 보내는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BackendKeyData&lt;/code&gt;는 두 개의 정수, 즉 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PID와 secret
key&lt;/strong&gt;를 담는다. 이 둘이 있어야 나중에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;쿼리
취소(CancelRequest)&lt;/strong&gt; 를 보낼 수 있다. CancelRequest는 기존
연결을 재사용하지 않고 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;새 TCP 연결&lt;/strong&gt;을 열어 &amp;quot;이 PID에
시그널을 보내라&amp;quot;고 postmaster에 요청하는 방식이다. 즉 cancel은 백엔드와
직접 말하지 않고 postmaster를 경유한다. JDBC의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Statement.cancel()&lt;/code&gt;이 타임아웃 후에도 살아 있는 이유가 여기
있다.&lt;/p&gt;
&lt;h3 id=&quot;2-3-parameterstatus-폭포&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-3) ParameterStatus 폭포&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;인증이 끝나면 백엔드는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;server_version&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;client_encoding&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DateStyle&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TimeZone&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;integer_datetimes&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;standard_conforming_strings&lt;/code&gt; 같은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ParameterStatus
메시지를 연달아 쏟아낸다&lt;/strong&gt;. 드라이버는 이걸 로컬 상태에
저장했다가 나중에 SQL 파싱·결과 해석에 쓴다. 여기서 특정 파라미터가 바뀔
때마다(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SET TIMEZONE&lt;/code&gt;) 백엔드는 ParameterStatus를 또 보낸다.
즉 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;GUC 변경은 단순한 세션 상태가 아니라 프로토콜
이벤트&lt;/strong&gt;다. 이 사실은 커넥션 풀러가 세션 상태를 초기화할 때 왜
그렇게 예민해지는지 설명해 준다.&lt;/p&gt;
&lt;h3 id=&quot;2-4-fork-비용과-pgbouncer--왜-풀러가-기본-인프라인가&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-4) fork
비용과 PgBouncer — 왜 풀러가 기본 인프라인가&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;fork가 느리니까 풀러를 쓴다&amp;quot;는 설명은 정확하지 않다. Linux x86-64
기준 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;fork()&lt;/code&gt; 시스템콜 자체는 일반적으로 sub-ms 수준이다.
문제는 그 다음이다. PostgreSQL 백엔드는 fork 직후 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;공유 메모리
세그먼트에 attach하고, 시스템 카탈로그 캐시를 초기화하고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_hba.conf&lt;/code&gt;를 파싱하고, 인증 왕복을 완료하고,
ParameterStatus를 쏟아낸 뒤&lt;/strong&gt;에야 비로소
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ReadyForQuery&lt;/code&gt;를 돌려준다. 이 전 과정을 합치면 실측 수
ms에서 수십 ms 영역으로 올라간다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;OLTP처럼 short-lived 연결이 초당 수천 번 만들어지는 워크로드에서는 이
비용이 그대로 쓰로틀이 된다. PgBouncer의 transaction pooling이
PostgreSQL 스택의 사실상 기본 인프라가 된 이유도 여기다. 정확히는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;fork 자체가 느려서&amp;quot;가 아니라 &amp;quot;backend 초기화까지가
비싸서&amp;quot;&lt;/strong&gt; 다. 풀러는 fork를 한 번만 하고 이미 초기화된 백엔드를
재사용한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-인증파라미터-협상의-함정&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3) 인증·파라미터 협상의 함정&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;연결이 실패하면 에러 메시지만 보고 pg_hba를 의심하는 경우가 많은데,
실제 원인은 네 군데로 나뉜다. 한 번 표로 정리해 두면 장애 대응이
빨라진다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;에러 메시지&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;원인&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;조치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FATAL: sorry, too many clients already&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_connections&lt;/code&gt; 초과&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;풀러 도입 or 값 상향(재시작 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FATAL: too many connections for role &amp;quot;x&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;역할별 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CONNECTION LIMIT&lt;/code&gt; 초과&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ALTER ROLE x CONNECTION LIMIT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FATAL: no pg_hba.conf entry for host ...&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;pg_hba.conf 룰이 거부&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;룰 추가 후 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SELECT pg_reload_conf()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FATAL: password authentication failed for user &amp;quot;x&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;비밀번호/메서드 불일치&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;SCRAM-SHA-256 설정 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_connections&lt;/code&gt;와 역할 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CONNECTION LIMIT&lt;/code&gt;는
별개의 카운터다. 전자에 여유가 있어도 후자에서 막힐 수 있고, 반대도
마찬가지다. 그리고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_connections&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;재시작이
필요한 파라미터&lt;/strong&gt;다. 공유 메모리의 proc array 크기를 고정된
값으로 할당하기 때문이다. reload로 바뀐다고 착각하고 운영 중에
바꾸려다가 당황하는 패턴이 제법 흔하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 한 가지 더 — PG 14부터 기본 인증 메서드가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;scram-sha-256&lt;/code&gt;이다. 이전엔 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;md5&lt;/code&gt;였다. 오래된
드라이버(예: JDBC 9.x 이하)는 이걸 이해하지 못해 &amp;quot;password
authentication failed&amp;quot;로 떨어진다. 서버가 MD5로 폴백하게 하는 대신
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;드라이버를 올리는 게 정답&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;인증을 통과한 백엔드들이 한 덩어리의 메모리에서 어떻게 만나는지가
다음 주제다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-공유-메모리-레이아웃-다섯-영역의-지도&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4) 공유 메모리
레이아웃: 다섯 영역의 지도&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;fork()&lt;/code&gt;로 만들어진 백엔드들이 어떻게 서로 협력할까. 답은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;공유 메모리 세그먼트 한 덩어리&lt;/strong&gt;다. postmaster가 기동 시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shmget&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mmap&lt;/code&gt;으로 커다란 세그먼트 하나를 잡고,
그 안에 논리적으로 여러 영역을 레이아웃한다. 자식 백엔드는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;fork()&lt;/code&gt;로 이 포인터를 물려받는다. 운영자가 외울 가치가 있는
건 다섯 영역이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;db-deepdive-01-postgresql-architecture-02&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;db-deepdive-01-postgresql-architecture-02&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;영역&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;기본값/공식&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;관측 뷰&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;shared_buffers&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;128MB (권장 RAM 25%) / 페이지 8KB&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_buffercache&lt;/code&gt; 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;wal_buffers&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;-1&lt;/code&gt; → &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers/32&lt;/code&gt;, 64KB~16MB&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_wal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;proc array&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_connections + autovacuum_max_workers + max_wal_senders + max_worker_processes&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_activity&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;lock table&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_locks_per_transaction × (max_connections + max_prepared_transactions)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_locks&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;CLOG (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_xact&lt;/code&gt;)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;트랜잭션당 2비트, SLRU&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_slru&lt;/code&gt;, PG 17
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;transaction_buffers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;4-1-shared_buffers--할당이고-버퍼-풀이다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-1) shared_buffers —
할당이고, 버퍼 풀이다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PostgreSQL이 직접 할당해서 들고
있는 버퍼 풀&lt;/strong&gt;이다. 8KB 페이지 단위로 관리되며, 읽기·쓰기가
일차적으로 여기에서 일어난다. 기본값 128MB는 사실상 &amp;quot;돌기는 한다&amp;quot; 수준의
최소치이고, 실무에서는 RAM의 25% 정도를 권장값으로 시작한다. 다만 25%는
상한이 아니라 출발점이다. 워크로드와 OS page cache의 관계에 따라 이 값을
조정하게 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;effective_cache_size&lt;/code&gt;를 같은 레이어로 보면 안 된다&lt;/strong&gt;.
전자는 PG가 직접 잡는 메모리이고, 후자는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;플래너에게 &amp;quot;이 정도
캐시 히트율을 가정해라&amp;quot;고 귀띔하는 힌트&lt;/strong&gt;다. PG는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;effective_cache_size&lt;/code&gt;에 1바이트도 할당하지 않는다. 이 값이
작으면 플래너는 디스크 IO를 비싸게 잡아 sequential scan 쪽으로 기울고,
이 값이 크면 index scan 쪽으로 기운다. 보통 RAM의 50~75% 정도로 두는데,
정답이 있는 게 아니라 플래너 편향의 조정 다이얼이라는 뜻이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 가지 더 주의할 게 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;double buffering&lt;/strong&gt;이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;가 잡은 페이지는 대부분 OS page cache에도
그대로 존재한다. 즉 같은 8KB가 커널 공간과 PG 공간에 이중으로 캐시된다.
이게 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;를 RAM의 50% 이상으로 밀어붙이지 않는
경험칙의 근거다. OS page cache가 사라지면 PG의 read-ahead와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;effective_io_concurrency&lt;/code&gt;가 기대하는 패턴도 함께 깨진다.
구체적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;effective_io_concurrency&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;OS 레벨
readahead/posix_fadvise를 몇 개까지 동시에 띄울지&lt;/strong&gt; 플래너에게
알리는 값인데, 이 값이 의미를 가지려면 그 readahead가 실제로 살아 있어야
한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;어느 릴레이션이 shared_buffers 안에 얼마나 들어 있는지는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_buffercache&lt;/code&gt; 확장으로 직접 본다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;CREATE&lt;/span&gt; EXTENSION &lt;span class=&quot;cf&quot;&gt;IF&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;EXISTS&lt;/span&gt; pg_buffercache;&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;SELECT&lt;/span&gt; c.relname,&lt;/span&gt;
&lt;span id=&quot;cb2-3&quot;&gt;&lt;a href=&quot;#cb2-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;       &lt;span class=&quot;fu&quot;&gt;count&lt;/span&gt;(&lt;span class=&quot;op&quot;&gt;*&lt;/span&gt;) &lt;span class=&quot;kw&quot;&gt;AS&lt;/span&gt; buffers,&lt;/span&gt;
&lt;span id=&quot;cb2-4&quot;&gt;&lt;a href=&quot;#cb2-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;       pg_size_pretty(&lt;span class=&quot;fu&quot;&gt;count&lt;/span&gt;(&lt;span class=&quot;op&quot;&gt;*&lt;/span&gt;) &lt;span class=&quot;op&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;8192&lt;/span&gt;) &lt;span class=&quot;kw&quot;&gt;AS&lt;/span&gt; bytes&lt;/span&gt;
&lt;span id=&quot;cb2-5&quot;&gt;&lt;a href=&quot;#cb2-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;FROM&lt;/span&gt; pg_buffercache b&lt;/span&gt;
&lt;span id=&quot;cb2-6&quot;&gt;&lt;a href=&quot;#cb2-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;JOIN&lt;/span&gt; pg_class c &lt;span class=&quot;kw&quot;&gt;ON&lt;/span&gt; b.relfilenode &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; pg_relation_filenode(c.&lt;span class=&quot;kw&quot;&gt;oid&lt;/span&gt;)&lt;/span&gt;
&lt;span id=&quot;cb2-7&quot;&gt;&lt;a href=&quot;#cb2-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;BY&lt;/span&gt; c.relname&lt;/span&gt;
&lt;span id=&quot;cb2-8&quot;&gt;&lt;a href=&quot;#cb2-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;DESC&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-9&quot;&gt;&lt;a href=&quot;#cb2-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;10&lt;/span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;핫 테이블이 정말 버퍼 풀에 상주하는가&amp;quot;에 대한 직접 관측 창이다.
예상과 다른 릴레이션이 상위에 올라와 있다면 인덱스 설계나 쿼리 패턴을
다시 봐야 한다.&lt;/p&gt;
&lt;h3 id=&quot;4-2-wal_buffers--shared_buffers의-132-규칙&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-2) wal_buffers —
shared_buffers의 1/32 규칙&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;WAL을 디스크에 쓰기 전에 잠시 쌓아 두는 작은 영역이다. 기본값이
특이하게 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;-1&lt;/code&gt;인데, 문서가 정확히 적어 둔다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;The default setting of -1 selects a size equal to 1/32nd (about
3%) of shared_buffers, but not less than 64kB nor more than the size of
one WAL segment, typically 16MB.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://www.postgresql.org/docs/17/runtime-config-wal.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;PostgreSQL
17 §19.5 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_buffers&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;즉 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;를 건드리면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_buffers&lt;/code&gt;도
따라 움직인다. 다만 상한이 WAL 세그먼트 하나 크기(보통 16MB)로 걸린다.
대용량 쓰기가 잦은 워크로드는 이 값을 명시적으로 16MB로 박아 두는 편이
낫다. 그래야 commit 시 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;XLogFlush&lt;/code&gt; 대기가 줄어든다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;wal_buffers가 정말 부족한지 확인하는 창은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_wal&lt;/code&gt;이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;SELECT&lt;/span&gt; wal_records, wal_bytes,&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;       wal_write_time, wal_sync_time,&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;       wal_buffers_full&lt;/span&gt;
&lt;span id=&quot;cb3-4&quot;&gt;&lt;a href=&quot;#cb3-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;FROM&lt;/span&gt; pg_stat_wal;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;핵심은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_buffers_full&lt;/code&gt;이다. 이 값은 &amp;quot;backend가 WAL
레코드를 쓰려다가 wal_buffers가 꽉 차서 강제로 flush한 횟수&amp;quot;를 누적한다.
평시에는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;0에 가까워야 정상&lt;/strong&gt;이고, 꾸준히 올라간다면
wal_buffers가 부족하다는 신호다. 이럴 땐 16MB로 고정 박아 두는 것이
답이다.&lt;/p&gt;
&lt;h3 id=&quot;4-3-proc-array--왜-재시작이-필요한가&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-3) proc array — 왜
재시작이 필요한가&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;proc array는 현재 살아 있는 백엔드의 상태를 담는 고정 크기 배열이다.
크기 공식이 중요하다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_connections + autovacuum_max_workers + max_wal_senders + max_worker_processes&lt;/code&gt;.
네 항이 왜 각각 필요한지 한 줄씩 풀면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_connections&lt;/code&gt; — 일반 클라이언트 백엔드 슬롯. 앱
서버가 직접 fork되는 자리.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_max_workers&lt;/code&gt; — autovacuum launcher가 스폰하는
청소 worker 슬롯. 일반 연결과 경쟁하면 안 되니 따로 잡는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_wal_senders&lt;/code&gt; — 스트리밍 복제 송신자. replica가
붙으려면 여기에 자리가 있어야 한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_worker_processes&lt;/code&gt; — 병렬 쿼리·확장·논리 복제
worker가 공유하는 풀. §7-2에서 자세히.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;변경 시 재시작이 필요한 이유는 단순하다&lt;/strong&gt; —
postmaster가 기동할 때 이 네 값의 합만큼 공유 메모리 세그먼트 안에 slot
배열을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;calloc&lt;/code&gt;으로 한 번에 잡는다. 런타임에 배열 크기를
늘리는 코드 경로가 없다. reload로 끝내려다가 무시당한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_activity&lt;/code&gt;가 보여주는 행 하나가 proc array의 슬롯
하나에 대응한다. &amp;quot;idle in transaction&amp;quot; 세션을 잡을 때 이 뷰가 첫
관측창이 된다.&lt;/p&gt;
&lt;h3 id=&quot;4-4-lock-table--mvcc가-필요로-하는-작은-유한-공간&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-4) lock
table — MVCC가 필요로 하는 작은 유한 공간&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PG의 락 테이블도 공유 메모리에 고정 크기로 잡힌다. 크기 공식은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_locks_per_transaction × (max_connections + max_prepared_transactions)&lt;/code&gt;.
기본값 64가 곱해진다. 한 트랜잭션이 이보다 많은 오브젝트에 락을 걸어야
하면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;out of shared memory&lt;/code&gt; 에러가 난다. 파티션 테이블을 많이
쓰는 스키마에서 이 에러가 종종 나오는 이유 — 파티션 프루닝이 안 되어
모든 자식 테이블에 락을 거는 순간 64개를 금방 넘긴다. 이 경우
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_locks_per_transaction&lt;/code&gt;을 128/256으로 올린다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;재현 시나리오 하나를 구체적으로 그려 두면 이해가 빠르다. 월별 파티션
1000개짜리 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;events&lt;/code&gt; 테이블에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WHERE event_time &amp;gt; now() - interval &amp;#39;7 days&amp;#39;&lt;/code&gt; 같은 조건이
들어간다. 상식적으로는 최근 파티션 1~2개만 스캔해야 하지만, partition
key 타입이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;now()&lt;/code&gt; 함수의 반환 타입과 맞지 않거나 immutable이
아닌 함수가 끼면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;플래너가 프루닝을 포기&lt;/strong&gt;하고 1000개
파티션 전부를 실행 계획에 넣는다. 그 순간 한 트랜잭션이 1000개의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AccessShareLock&lt;/code&gt;을 걸게 되고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_locks_per_transaction=64&lt;/code&gt;면 즉시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;out of shared memory&lt;/code&gt; 에러가 튄다. 이 시점에서는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_locks_per_transaction&lt;/code&gt;을 먼저 키워 장애를 막고, 원인인
프루닝 실패를 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;EXPLAIN&lt;/code&gt;으로 추적하는 순서가 현실적이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;현재 락 분포를 관측하는 가장 빠른 쿼리는 이것이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre class=&quot;sourceCode sql&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode sql&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;mode&lt;/span&gt;, &lt;span class=&quot;fu&quot;&gt;count&lt;/span&gt;(&lt;span class=&quot;op&quot;&gt;*&lt;/span&gt;) &lt;span class=&quot;kw&quot;&gt;FROM&lt;/span&gt; pg_locks &lt;span class=&quot;kw&quot;&gt;GROUP&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;mode&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;DESC&lt;/span&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AccessShareLock&lt;/code&gt;이 수천 개로 튀어 있다면 파티션 프루닝
실패를, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RowExclusiveLock&lt;/code&gt; + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ExclusiveLock&lt;/code&gt;의
블로킹 관계를 보고 싶다면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_blocking_pids()&lt;/code&gt;와 조인한 뷰를
쓴다.&lt;/p&gt;
&lt;h3 id=&quot;4-5-clog--트랜잭션당-2비트의-진실&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-5) CLOG — 트랜잭션당
2비트의 진실&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CLOG&lt;/strong&gt;(Commit Log, PG 10부터 디렉토리 이름이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_xact&lt;/code&gt;)는 각 트랜잭션의 상태를 2비트로 저장한다. 상태는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;IN_PROGRESS&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;COMMITTED&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ABORTED&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SUB_COMMITTED&lt;/code&gt; 네 가지. MVCC에서 &amp;quot;이 튜플을 만든 XID가
커밋됐나?&amp;quot;를 물을 때마다 CLOG를 찾아보므로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;모든 읽기에 간접
참조가 하나 더 낀다&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;간단한 산수 하나 — XID는 32비트, 트랜잭션당 2비트면 디스크 위 CLOG의
이론 최대 크기는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;2비트 × 2^32 = 1 GB&lt;/code&gt;다. 실제로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_xact&lt;/code&gt; 디렉토리가 GB 단위로 자라는 걸 본 적이 있다면 놀랄
일이 아니다. freeze와 함께 오래된 segment가 잘려 나가기에 평시에는 수십
MB 수준에 머문다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;CLOG는 디스크 위 파일이면서 동시에 공유 메모리에
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;SLRU&lt;/strong&gt;(Simple LRU)로 캐시된다. 이름 그대로 &amp;quot;Simple&amp;quot; —
작은 고정 슬롯 풀에 단순 LRU를 돌리는 구조다. 일반 buffer pool처럼 clock
sweep이나 ring buffer 같은 정교한 장치가 없는 대신, 코드가 짧고 경합
지점이 명확하다. 대용량 트랜잭션에서 CLOG 컨텐션이 문제가 되는 것도 이
단순함 때문이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PG 17에서는 SLRU 버퍼 크기들이 드디어 GUC로 노출되었다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;transaction_buffers&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;subtransaction_buffers&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;commit_timestamp_buffers&lt;/code&gt; 등. 그 이전까지는 코드에 상수로
박혀 있어서 튜닝이 불가능했다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;transaction_buffers&lt;/code&gt;의
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본값은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers / 512&lt;/code&gt;이고 상한 8MB로
클램프&lt;/strong&gt;된다. 즉 shared_buffers를 크게 잡아도 CLOG 버퍼는
자동으로 끝까지 자라지 않는다. 대용량 트랜잭션을 처리하는 서버에서 CLOG
컨텐션이 의심된다면 17로 올리고 이 값을 명시적으로 키워볼 가치가
있다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;마지막으로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;실측 크기 계산&lt;/strong&gt; 한 번.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_connections=200&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;work_mem=4MB&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers=8GB&lt;/code&gt; 가정. shared_buffers 8GB + wal_buffers
16MB + proc array/lock table(수십 MB 수준) + CLOG 버퍼 8MB ≈
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;공유 메모리 총량 8.1GB&lt;/strong&gt;. 여기에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;백엔드별 사유
메모리&lt;/strong&gt;는 별도 — work_mem 4MB × 플랜 노드 3 × 연결 200 = 최악
2.4GB가 힙에 따로 붙는다. OS 관점에서 PG 한 인스턴스의 실제 RSS 상한은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers + (work_mem × 플랜 노드 × 연결 수)&lt;/code&gt; 정도로
잡아야 현실에 가깝다. 이 산수가 안 맞으면 한밤중에 OOM killer가 친절하게
가르쳐 준다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;공유 메모리 다섯 영역의 지도를 잡았으니, 이제 이 버퍼 풀이 디스크와
만나는 쓰기 경로로 내려간다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-wal-writerbgwritercheckpointer--쓰기-경로-3인-1조&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5) WAL
writer·bgwriter·checkpointer — 쓰기 경로 3인 1조&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PostgreSQL의 쓰기 경로를 이해하는 가장 빠른 길은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;세 명이
각자 다른 것을 쓴다&amp;quot;&lt;/strong&gt; 를 외우는 것이다. walwriter는 WAL을,
bgwriter는 dirty 버퍼 일부를, checkpointer는 모든 dirty 버퍼를 디스크에
내려보낸다. 셋이 같은 디스크를 두고 협력하되, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;주기와 대상이
다르다&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;db-deepdive-01-postgresql-architecture-03&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;db-deepdive-01-postgresql-architecture-03&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;5-1-walwriter--commit-전에-미리-내려보내는-쪽&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-1) walwriter —
commit 전에 미리 내려보내는 쪽&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;walwriter는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;비동기 커밋을 가능하게 만드는 조용한
일꾼&lt;/strong&gt;이다. 기본값은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_writer_delay=200ms&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_writer_flush_after=1MB&lt;/code&gt;. 즉 200ms마다 혹은 WAL 버퍼에
1MB가 쌓이면 디스크로 flush한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;synchronous_commit=on&lt;/code&gt;이면
각 커밋이 자기 WAL이 디스크에 닿을 때까지 기다리지만, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;off&lt;/code&gt;면
walwriter가 나중에 내려줄 때까지 기다리지 않고 반환한다. 즉 walwriter의
주기가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;비동기 커밋의 데이터 손실 창&lt;/strong&gt;을 결정한다. off
상태에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_writer_delay&lt;/code&gt;를 2초로 키우면 최악의 경우
2초어치가 날아갈 수 있다는 뜻이다.&lt;/p&gt;
&lt;h3 id=&quot;5-2-bgwriter--lru-꼬리의-청소부&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-2) bgwriter — LRU 꼬리의
청소부&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;bgwriter는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;shared_buffers에서 곧 교체될 dirty
버퍼들&lt;/strong&gt;을 미리 디스크에 써 두는 역할이다. 백엔드가 새 페이지가
필요해서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BufferAlloc&lt;/code&gt;을 호출했는데 LRU 꼬리의 victim 버퍼가
dirty면, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;백엔드가 직접 디스크에 쓰고 나서&lt;/strong&gt; 교체해야
한다. 이걸 피하려고 bgwriter가 선제적으로 돌아다니며 flush해 두는
것이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;기본값은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_delay=200ms&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_lru_maxpages=100&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_lru_multiplier=2.0&lt;/code&gt;. 200ms마다 깨어나서 &amp;quot;최근에
백엔드가 몇 개를 요청했나&amp;quot;를 보고 그 2배만큼을 미리 flush하되, 한
라운드에 100페이지를 넘지는 않는다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_bgwriter&lt;/code&gt;의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_clean&lt;/code&gt;과 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_backend&lt;/code&gt;가 이 일꾼의
성적표다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_backend&lt;/code&gt;가 꾸준히 올라간다면
bgwriter가 못 따라가고 있다&lt;/strong&gt;는 신호다. 이때는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_lru_maxpages&lt;/code&gt;를 키우거나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_delay&lt;/code&gt;를 줄인다.&lt;/p&gt;
&lt;h3 id=&quot;5-3-checkpointer--spread-write와-full_page_writes&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-3)
checkpointer — spread write와 full_page_writes&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;checkpointer는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;주기적으로 모든 dirty 버퍼를 디스크에
내려보내고 체크포인트 기록을 WAL에 남긴다&lt;/strong&gt;. 크래시 시 recovery가
여기서부터 재생을 시작한다. 주기의 기본값은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpoint_timeout=5min&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpoint_completion_target=0.9&lt;/code&gt;. 즉 5분마다 시작하되, 그
쓰기를 5분의 90%(4분 30초)에 걸쳐 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;퍼트려서&lt;/strong&gt; 내보낸다.
spread write라고 부른다. 이게 없으면 체크포인트마다 IO가 스파이크로
튄다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;full_page_writes=on&lt;/code&gt;(기본)과 맞물리는 함정이 있다.
이 옵션은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;체크포인트 이후 어떤 페이지가 처음 변경될 때 그 페이지
전체(8KB)를 WAL에 함께 기록&lt;/strong&gt;하라는 뜻이다. partial write(토른
페이지) 문제로부터 recovery를 안전하게 만드는 장치다. 그런데 부작용이
있다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;체크포인트 직후에는 거의 모든 업데이트가 full page
image를 동반&lt;/strong&gt;하므로 WAL 양이 수 배에서 수십 배로 튄다. 복제
환경에서는 이 스파이크가 그대로 replica lag으로 보인다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;full_page_writes&lt;/code&gt;를
&amp;quot;integrity 보험&amp;quot;으로만 읽지 말자.&lt;/strong&gt; off로 끄면 복구가
위험해지지만, on인 채로 방치하면 체크포인트 튜닝이 복제 지연 대응의 첫
단추가 된다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpoint_timeout&lt;/code&gt;을 늘리면 체크포인트 빈도가
줄어 full page image 총량이 줄지만, 대신 한 번에 써야 할 양이 늘어난다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_wal_size&lt;/code&gt;도 함께 움직여야 하는 이유다. 참고로 특정
구간의 WAL에서 FPI(full page image)가 차지하는 비율은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_waldump -z &amp;lt;segment&amp;gt;&lt;/code&gt;로 바로 측정할 수 있다. 이
비율이 30%를 계속 넘는다면 체크포인트 주기 튜닝의 타이밍이다.&lt;/p&gt;
&lt;h3 id=&quot;5-4-pg_stat_bgwriter-읽는-법&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-4) pg_stat_bgwriter 읽는 법&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;bgwriter와 checkpointer가 제대로 일하고 있는지 판단하는 가장 빠른
뷰는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_bgwriter&lt;/code&gt;다. 컬럼만 외워 두면 대부분의 판단이
30초 안에 끝난다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;컬럼&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_checkpoint&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;checkpointer가 flush한 dirty buffer 누적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_clean&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;bgwriter가 선제적으로 flush한 dirty buffer 누적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_backend&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;backend가 할 수 없어서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;직접&lt;/strong&gt; 쓴 dirty buffer — 건강
지표의 반대&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_backend_fsync&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;backend가 직접 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;fsync&lt;/code&gt;한 횟수. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;0이어야
정상&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;maxwritten_clean&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;bgwriter가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;lru_maxpages&lt;/code&gt; 한도에 걸려 멈춘 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;건강 진단 공식 하나를 외워 둔다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_backend / (buffers_clean + buffers_backend)&lt;/code&gt;. 이
비율이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;10%를 꾸준히 넘으면 bgwriter 튜닝 시점&lt;/strong&gt;이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_lru_maxpages&lt;/code&gt;를 200/400으로 올리거나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_delay&lt;/code&gt;를 100ms로 줄이는 식으로 먼저 손댄다. 반대로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_backend_fsync&lt;/code&gt;가 0이 아닌 수로 올라가면 &amp;quot;OS가
bgwriter의 request queue를 거부했다&amp;quot;는 심각한 신호다. 스토리지 쪽부터
봐야 한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PG 17 주의사항 — 17에서는 이 뷰의 일부 컬럼이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_checkpointer&lt;/code&gt;로 분리되었다. 16 이전 대시보드 쿼리를
17로 옮길 때 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;buffers_checkpoint&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpoints_timed&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpoints_req&lt;/code&gt; 같은 컬럼이
새 뷰로 이사 간 걸 놓치면 무심코 0으로 읽힌다.&lt;/p&gt;
&lt;h3 id=&quot;5-5-synchronous_commit-5단계--손실-창-vs-지연&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-5)
synchronous_commit 5단계 — 손실 창 vs 지연&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;앞서 walwriter 절에서 &amp;quot;비동기 커밋의 손실 창은 walwriter 주기&amp;quot;라고
했는데, 이 손실 창을 세밀하게 조정하는 다이얼이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;synchronous_commit&lt;/code&gt;이다. 5단계가 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;값&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;primary fsync&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;primary 대기&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;standby 대기&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;손실 창&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;off&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최대 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_writer_delay × 3&lt;/code&gt; (기본 600ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;0 (단, 복제 고려 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;on&lt;/code&gt; (기본)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;remote_write까지&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;standby OS가 받은 시점까지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;remote_write&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;standby OS write&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;standby가 받고 write까지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;remote_apply&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;standby replay까지&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;0 (read-your-writes 보장)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;트레이드오프가 직관적이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;off&lt;/code&gt;는 가장 빠르지만 primary
크래시 시 수백 ms가 날아갈 수 있고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;remote_apply&lt;/code&gt;는 primary
커밋이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;standby의 replay까지 기다리므로&lt;/strong&gt; 네트워크 RTT +
재생 시간이 그대로 커밋 레이턴시에 올라탄다. 기본값 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;on&lt;/code&gt;이
&amp;quot;primary fsync + remote_write&amp;quot;로 잡혀 있는 건 이 둘의 중간 절충이다.
세션별로 바꿀 수 있다는 점이 중요하다 — 중요한 결제 트랜잭션만
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SET LOCAL synchronous_commit = remote_apply&lt;/code&gt;로 올리고
나머지는 기본값으로 두는 전략이 현실적이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;쓰기 경로의 세 일꾼을 따라왔다. 이제 이들이 만들어 놓은 오래된 튜플
버전을 청소하는 마지막 상주 프로세스로 간다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파라미터&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;기본값&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_writer_delay&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;200ms&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;walwriter 주기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;wal_writer_flush_after&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1MB&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;walwriter flush 임계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_delay&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;200ms&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;bgwriter 주기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_lru_maxpages&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;100&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;라운드당 최대 flush 페이지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_lru_multiplier&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2.0&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;수요 예측 배수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpoint_timeout&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;5min&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;체크포인트 주기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpoint_completion_target&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;0.9&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;spread write 비율&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;full_page_writes&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;on&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;체크포인트 후 첫 변경 시 전체 페이지 WAL 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_wal_size&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1GB&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;체크포인트 강제 트리거 임계&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-autovacuum-launcher와-worker&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6) autovacuum launcher와
worker&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PostgreSQL의 MVCC는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;오래된 튜플 버전을 나중에 청소하는
모델&lt;/strong&gt;이다. 이 청소를 전담하는 게 autovacuum이다. 구조는 간단하다
— &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;launcher 하나가 상주&lt;/strong&gt;하며
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_naptime&lt;/code&gt;(기본 1분)마다 DB를 순회하고, 청소가
필요한 DB가 있으면 그쪽에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;worker를 spawn&lt;/strong&gt;한다. 동시에 뛸
수 있는 worker 수는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_max_workers&lt;/code&gt;(기본 3). 이 값은
proc array 크기에 영향을 주기 때문에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;재시작이
필요&lt;/strong&gt;하다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파라미터&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;기본값&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_naptime&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1min&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;launcher가 DB를 살피는 주기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_max_workers&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;동시 worker 수 (재시작 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_vacuum_threshold&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;50&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;기본 변경 건수 하한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_vacuum_scale_factor&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;0.2&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;비례 계수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_vacuum_cost_delay&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2ms&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;비용 기반 throttle 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_vacuum_cost_limit&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;200&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;한 라운드에 쓸 수 있는 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;6-1-threshold-공식과-스케일-계수&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-1) threshold 공식과 스케일
계수&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;테이블이 청소 대상이 되는 조건은 단순한 공식이다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;변경된
튜플 수 &amp;gt; &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;threshold + scale_factor × reltuples&lt;/code&gt;&lt;/strong&gt;.
기본값으로 풀면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;50 + 0.2 × 테이블 크기&lt;/code&gt;. 즉 1만 행짜리
테이블은 2050행이 바뀌면 돌고, 1억 행짜리는 2천만 행이 바뀌어야 돈다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;큰 테이블일수록 autovacuum이 점점 뜸해진다&lt;/strong&gt;는 뜻이다. 큰
테이블에는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ALTER TABLE ... SET (autovacuum_vacuum_scale_factor = 0.02)&lt;/code&gt;
같은 per-table 오버라이드가 거의 필수다.&lt;/p&gt;
&lt;h3 id=&quot;6-2-cost-based-throttle--왜-vacuum이-느린가&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-2) cost-based
throttle — 왜 VACUUM이 느린가&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;autovacuum worker는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;cost-based throttle&lt;/strong&gt;로 IO
사용량을 제한한다. 페이지를 shared_buffers에서 찾으면 1, 디스크에서
읽으면 10, 더럽히면 20의 비용이 쌓인다. 한 라운드의 누적 비용이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_vacuum_cost_limit&lt;/code&gt;(200)을 넘으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_vacuum_cost_delay&lt;/code&gt;(2ms)만큼 잠든다. 이게
&amp;quot;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;VACUUM&lt;/code&gt;이 왜 그렇게 느리냐&amp;quot;에 대한 답이다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;느린 게
아니라 일부러 느리게 하고 있다&lt;/strong&gt;. OLTP 쓰기를 방해하지 않으려는
설계다. 반대로 긴급하게 청소해야 할 때는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;cost_delay&lt;/code&gt;를 0으로
놓고 수동으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;VACUUM (VERBOSE)&lt;/code&gt;를 돌리게 된다.&lt;/p&gt;
&lt;h3 id=&quot;6-3-workers는-슬롯-풀이다--cost-balance의-진짜-공식&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-3)
workers는 슬롯 풀이다 — cost balance의 진짜 공식&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_max_workers&lt;/code&gt;가 3이라고 해서 &amp;quot;언제나 3개가
돈다&amp;quot;는 건 아니다. launcher는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;필요한 DB에만&lt;/strong&gt; worker를
붙이고, 일이 끝나면 슬롯을 반납한다. 다만 매우 큰 테이블 셋이 동시에
threshold를 넘으면 3개가 동시에 돌게 되고, 이때 총 IO는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;cost_limit × 3&lt;/code&gt;이 아니라 — 문서가 정확히 적지만 —
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;cost_limit&lt;/code&gt;을 여러 worker가 나눠 쓰도록
조정&lt;/strong&gt;된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;정확히는 이렇다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_vacuum_cost_limit = -1&lt;/code&gt;(기본)일 때, autovacuum은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;vacuum_cost_limit&lt;/code&gt;(기본 200)을 현재 활성 worker 수로
나누는&lt;/strong&gt; cost balance 동작에 들어간다. worker가 3개면 각자 약
66의 예산을 나눠 갖는다. 그래서 워커 수를 늘린다고 총 처리량이 비례해
늘지 않는다 — 예산 총액은 고정된 채 분할만 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;큰 DB에서 autovacuum이 늦는 이유는 주로 worker 수가 아니라
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;cost_delay 쓰로틀&lt;/strong&gt;과 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;테이블 동결 스캔&lt;/strong&gt;
양쪽이다. 큰 테이블 하나를 빨리 청소해야 한다면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ALTER TABLE tbl SET (autovacuum_vacuum_cost_delay = 0)&lt;/code&gt; 같은
per-table 오버라이드가 더 직접적이다.&lt;/p&gt;
&lt;h3 id=&quot;6-4-관측-pg_stat_progress_vacuum&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-4) 관측:
pg_stat_progress_vacuum&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PG 9.6부터 VACUUM의 진행 상황을 실시간으로 노출하는 뷰가 있다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_progress_vacuum&lt;/code&gt;. &amp;quot;왜 이 VACUUM이 한 시간째 안
끝나는가&amp;quot;를 추적할 때 첫 관측창이 이것이다. phase 컬럼이 일곱 단계를
돈다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;initializing&lt;/code&gt; — 초기화&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;scanning heap&lt;/code&gt; — 힙 페이지 스캔, dead tuple 식별&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;vacuuming indexes&lt;/code&gt; — 인덱스에서 dead tuple 참조
제거&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;vacuuming heap&lt;/code&gt; — 힙에서 dead tuple 공간 회수&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;cleaning up indexes&lt;/code&gt; — 인덱스 cleanup&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;truncating heap&lt;/code&gt; — 빈 페이지 잘라내기&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;final cleanup&lt;/code&gt; — 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;어느 phase에서 정체되는지가 바로 진단이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;scanning heap&lt;/code&gt;에서 오래 머물면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;테이블 크기 + IO
쓰로틀&lt;/strong&gt; 조합, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;vacuuming indexes&lt;/code&gt;에서 막히면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;인덱스 수가 많거나 특정 인덱스가 거대한 경우&lt;/strong&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;truncating heap&lt;/code&gt;에서 멈춘 듯 보이면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AccessExclusiveLock 획득 대기&lt;/strong&gt; 때문일 수 있다. 이 구분이
없으면 &amp;quot;전부 느리다&amp;quot;로만 보이고 다음 액션을 못 고른다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h3 id=&quot;6-5-wraparound와-anti-wraparound&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-5) wraparound와
anti-wraparound&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PostgreSQL의 MVCC는 32비트 XID(트랜잭션 ID)를 쓴다. 2^31 = 약 21억
개의 XID가 한 바퀴를 돌면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;이 튜플이 과거인지 미래인지&amp;quot;&lt;/strong&gt;
를 판단할 수 없게 된다. 이게 유명한 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;wraparound&lt;/strong&gt; 문제다.
PG는 주기적으로 오래된 XID를 특수 값 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FrozenXID&lt;/code&gt;로 교체해서
&amp;quot;영원히 과거&amp;quot;로 표시한다. 이걸 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;freezing&lt;/strong&gt;이라고 부른다.
평소 autovacuum이 일반 청소와 함께 슬슬 해두지만, 미루다가
아슬아슬해지면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;anti-wraparound VACUUM&lt;/strong&gt;이 발동한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;문서가 이 조건을 명시한다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;Tables whose pg_class.relfrozenxid value is more than
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_freeze_max_age&lt;/code&gt; transactions old are always
vacuumed.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://www.postgresql.org/docs/17/routine-vacuuming.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;PostgreSQL
17 §24.1 Routine Vacuuming&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;그리고 한 발 더.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;The system will launch autovacuum processes to prevent
wraparound even when autovacuum is otherwise disabled.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://www.postgresql.org/docs/17/runtime-config-autovacuum.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;PostgreSQL
17 §19.10&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;기본값은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_freeze_max_age=2억&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;vacuum_freeze_table_age=1.5억&lt;/code&gt;. 즉 relfrozenxid가 2억
트랜잭션 이상 뒤처진 테이블은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;반드시&lt;/strong&gt; 청소된다. 그리고
이 anti-wraparound VACUUM은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;cost_delay를 무시&lt;/strong&gt;한다.
쓰로틀이 없다. 큰 테이블이 이 상태에 걸리면 수 시간 동안 IO를 통째로
가져가고, 그동안 OLTP가 체감할 만한 느려짐을 겪는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;autovacuum을 &amp;quot;자동&amp;quot;이라고 믿고 방치하지
말자.&lt;/strong&gt; 평소 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_user_tables.n_dead_tup&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SELECT datname, age(datfrozenxid) FROM pg_database&lt;/code&gt;를
주기적으로 관측해서, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;age가 1억을 넘기 시작한 DB는 선제적으로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;VACUUM FREEZE&lt;/code&gt;를 돌린다&lt;/strong&gt;. 이건 자동이 실패했을 때를
대비한 수동 타임 버퍼다. 특히 autovacuum을 &amp;quot;쓰기 방해된다&amp;quot;는 이유로
끄거나 scale factor를 너무 키우는 경우, 이 버퍼가 없으면 어느 날 서비스
타임에 anti-wraparound가 터진다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-확장의-자리-background-worker와-shared_preload_libraries&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7)
확장의 자리: background worker와 shared_preload_libraries&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PostgreSQL의 확장 모델은 0편에서 얘기한 1986년 설계 선언의 직계다. 그
물리적 구현이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;background worker&lt;/strong&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_preload_libraries&lt;/code&gt;다. 공식 문서가 이렇게
정의한다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;em&gt;&amp;quot;Background workers are modules that can be launched by the
postmaster and run under its control.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;— &lt;a href=&quot;https://www.postgresql.org/docs/17/bgworker.html&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;PostgreSQL 17
§47 Background Worker Processes&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;즉 확장은 자기만의 백그라운드 프로세스를 postmaster의 통제 하에 띄울
수 있다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_cron&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_statements&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;timescaledb&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;citus&lt;/code&gt; 같은 확장이 이 메커니즘으로
동작한다. 이 기능이 쓰는 전역 슬롯 풀이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_worker_processes&lt;/code&gt;(기본 8)다.&lt;/p&gt;
&lt;h3 id=&quot;7-1-shared_preload_libraries--fork-이전에-로드되어야-하는-이유&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-1)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_preload_libraries&lt;/code&gt; — fork 이전에 로드되어야 하는
이유&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;일부 확장은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;공유 메모리 공간을 미리 예약&lt;/strong&gt;해야 한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_statements&lt;/code&gt;가 대표적이다. 이런 확장은 postmaster가
기동하며 공유 메모리를 할당하는 시점에 이미 로드되어 있어야 한다. 그래서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;postgresql.conf&lt;/code&gt;의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_preload_libraries = &amp;#39;pg_stat_statements,pg_cron&amp;#39;&lt;/code&gt;에
넣고 재시작해야 한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CREATE EXTENSION&lt;/code&gt;만으로는 안 되고,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;서버 재시작이 강제&lt;/strong&gt;되는 유일한 이유가 여기다.&lt;/p&gt;
&lt;h3 id=&quot;7-2-max_worker_processes는-슬롯-풀이다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-2)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_worker_processes&lt;/code&gt;는 슬롯 풀이다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 자주 터지는 함정 하나를 짚자.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_worker_processes&lt;/code&gt;(기본 8)는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;병렬 쿼리의 worker,
논리 복제 worker, 그리고 확장이 띄운 background worker가 전부 같이 써야
하는 하나의 풀&lt;/strong&gt;이다. 추가로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_parallel_workers&lt;/code&gt;(기본 8, 단
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_worker_processes&lt;/code&gt; 이하로 제한됨)와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_logical_replication_workers&lt;/code&gt;(기본 4)가 이 풀 위에 얹힌
상한이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;슬롯이 모자라면 에러가 나는 게 아니라
조용히 serial 실행으로 강등된다.&lt;/strong&gt; 병렬로 돌 줄 알았던 쿼리가
단일 worker로 돌면서 성능이 뚝 떨어지고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_activity&lt;/code&gt;에는 worker가 안 보인다. 원인이 &amp;quot;worker
slot 고갈&amp;quot;이라는 걸 눈치채지 못하면 플래너 탓으로 몰고 가기 쉽다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_cron&lt;/code&gt;이나 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;citus&lt;/code&gt;를 올린 뒤 병렬 쿼리가 갑자기
느려졌다면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_worker_processes&lt;/code&gt;를 16/32로 키우는 것이 첫
수다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;연결 수를 늘릴 땐 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_connections&lt;/code&gt;와 함께 proc
array의 의존 4인방&lt;/strong&gt;(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;autovacuum_max_workers&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_wal_senders&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_worker_processes&lt;/code&gt;)을 같이
계산한다. 공유 메모리 크기가 고정이므로 전부 재시작이 필요하다는 사실을
자동화 스크립트에 반영해 둔다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shared_buffers&lt;/code&gt;를 바꿀 땐
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;effective_cache_size&lt;/code&gt;도 같이 움직인다&lt;/strong&gt;. 전자는 할당,
후자는 힌트. 후자를 안 움직이면 플래너가 옛날 세계관에서 돈다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;체크포인트 튜닝은 복제 지연 대응의 첫 단추&lt;/strong&gt;다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;checkpoint_timeout&lt;/code&gt;을 5분 → 15분으로 늘리고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_wal_size&lt;/code&gt;를 함께 올리면 full page image 총량이 줄어
replica lag 스파이크가 평평해진다. 단, recovery 시간이 길어진다는 대가는
받아들여야 한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_stat_bgwriter.buffers_backend&lt;/code&gt;를 대시보드에
띄운다&lt;/strong&gt;. 이 값이 꾸준히 오르면 bgwriter가 못 따라가고 있다는
뜻이고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_lru_maxpages&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;bgwriter_delay&lt;/code&gt;
조정이 필요하다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SELECT datname, age(datfrozenxid) FROM pg_database&lt;/code&gt;를
주 1회 스냅샷&lt;/strong&gt;으로 찍어 둔다. age가 1억을 넘는 DB가 보이면
서비스 저부하 시간에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;VACUUM FREEZE&lt;/code&gt;를 선제적으로 돌린다.
anti-wraparound가 피크 타임에 터지는 것보다 훨씬 낫다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;확장을 올릴 때 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;max_worker_processes&lt;/code&gt;를 반드시
재평가&lt;/strong&gt;한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;pg_cron&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;timescaledb&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;citus&lt;/code&gt;, 논리 복제 subscription이 전부 같은 풀을 판다. 슬롯이
모자라면 병렬 쿼리가 조용히 serial로 떨어진다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;work_mem&lt;/code&gt;은 세션당이 아니라 플랜
노드당&lt;/strong&gt;이다. 한 쿼리가 sort/hash 노드 5개를 가지면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;work_mem × 5&lt;/code&gt;를 먹는다. 연결 수 × &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;work_mem&lt;/code&gt;만
계산하면 실제 메모리의 절반도 안 잡힌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;9-한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;9) 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PostgreSQL은 postmaster 하나가 fork로 자식을 찍어내는
프로세스 DB이고, 그 자식들은 공유 메모리 세그먼트 한 덩어리를 매개로
협력한다&lt;/strong&gt;. 쓰기 경로의 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;3인 1조&lt;/strong&gt;, 공유 메모리
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;5영역&lt;/strong&gt;, worker slot &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한 풀&lt;/strong&gt; — 이 세 축을
머릿속에 띄워두면 대부분의 튜닝 파라미터는 어디에 작용하는지 자명해진다.
다음 편부터는 이 지도 위에서 WAL 포맷·MVCC·락 구조를 하나씩 확대해
들어간다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;태그&lt;/strong&gt;: postgresql, postgres-architecture, postmaster,
shared-buffers, wal-writer, bgwriter, checkpointer, autovacuum,
wraparound, background-worker&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/Database</category>
      <category>autovacuum</category>
      <category>background-worker</category>
      <category>bgwriter</category>
      <category>checkpointer</category>
      <category>postgres-architecture</category>
      <category>PostgreSQL</category>
      <category>postmaster</category>
      <category>shared-buffers</category>
      <category>wal-writer</category>
      <category>wraparound</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/408</guid>
      <comments>https://dding-shark.tistory.com/408#entry408comment</comments>
      <pubDate>Wed, 15 Apr 2026 08:20:49 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot Actuator &amp;mdash; Endpoint SPI와 Health&amp;middot;Metrics&amp;middot;Prometheus 내부</title>
      <link>https://dding-shark.tistory.com/407</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;spring-boot-actuator--endpoint-spi와-healthmetricsprometheus-내부&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;Spring
Boot Actuator — Endpoint SPI와 Health·Metrics·Prometheus 내부&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;운영 중인 서비스를 다루다 보면 결국 세 가지 질문으로 되돌아온다. 지금
살아 있나, 트래픽을 받을 준비가 됐나, 무슨 일이 일어나고 있나. Spring
Boot는 이 세 질문에 답할 수 있는 창구를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Actuator&lt;/strong&gt;라는
이름으로 묶어놓았다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-actuator&lt;/code&gt; 의존성 한
줄이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator&lt;/code&gt; 경로를 열고, 그 아래에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;metrics&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;prometheus&lt;/code&gt; 같은 엔드포인트를
걸어둔다. 다만 이 창구는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본값이 의도적으로 소심하게 잡혀
있다&lt;/strong&gt;. 뭔가 설정해주지 않으면 web에는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt;
하나밖에 안 보인다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Actuator의 설계 중심은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Endpoint&lt;/code&gt;라는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기술 중립
SPI&lt;/strong&gt;다. 한 번 선언하면 web과 JMX 양쪽에 동시에 노출되고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ReadOperation&lt;/code&gt; / &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WriteOperation&lt;/code&gt; /
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@DeleteOperation&lt;/code&gt;이라는 세 가지 HTTP 메서드 대응
어노테이션으로 동작을 정의한다. Health·Info·Metrics 같은 내장
엔드포인트들도 이 SPI 위에 쌓여 있다. 구조를 이해하면 커스텀
엔드포인트를 만드는 일이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@RestController&lt;/code&gt; 짜는 것만큼 가볍게
느껴진다. 그런데 실무에서 Actuator를 들여다보면 거의 항상 다음 지점에서
막힌다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator&lt;/code&gt;에 붙었더니 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt;만
보인다&lt;/strong&gt; — 기본 web 노출이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt; 하나뿐이고,
나머지는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.endpoints.web.exposure.include&lt;/code&gt;로
명시해야 보인다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/health&lt;/code&gt;가 갑자기 느려졌다&lt;/strong&gt; —
커스텀 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HealthIndicator&lt;/code&gt;가 외부 DB/서드파티 호출에서 대기
중이다. 집계 엔드포인트 하나가 전체 헬스 응답을 붙잡는다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;exposure.include=*&lt;/code&gt; 한 줄이 프로덕션에서 자격
증명 노출로 이어진다&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configprops&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;beans&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;threaddump&lt;/code&gt;가
전부 열리면서 프로퍼티 원본이 밖으로 새나간다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/prometheus&lt;/code&gt;가 404다&lt;/strong&gt; —
엔드포인트 노출은 했는데 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;micrometer-registry-prometheus&lt;/code&gt;
의존성이 없어서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PrometheusMeterRegistry&lt;/code&gt; 빈 자체가 안
만들어졌다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;왜 → 구조(Endpoint SPI) → 동작(내장 엔드포인트) →
Health/Metrics/Prometheus → 보안 → 실무&lt;/strong&gt; 순으로 Spring Boot 3.x
/ Spring Framework 6.x / Micrometer 1.x 기준으로 정리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-actuator가-하는-일과-기본-노출-정책&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1) Actuator가 하는
일과 기본 노출 정책&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-managementendpoints-네임스페이스-설정&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2)
management.endpoints 네임스페이스 설정&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-endpoint-spi-기술-중립-엔드포인트-구조&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3) @Endpoint
SPI: 기술 중립 엔드포인트 구조&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-readoperation--writeoperation--selector&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4)
@ReadOperation / @WriteOperation / @Selector&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-내장-엔드포인트-한눈에&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5) 내장 엔드포인트
한눈에&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-health-contributor-기본-등록과-커스텀-작성&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6) Health
Contributor: 기본 등록과 커스텀 작성&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-kubernetes-probes와-applicationavailability&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7)
Kubernetes Probes와 ApplicationAvailability&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-info-contributor와-build-infoproperties&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8) Info
Contributor와 build-info.properties&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#9-micrometer-meterregistry-countertimergaugetag&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;9)
Micrometer MeterRegistry: Counter·Timer·Gauge·Tag&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#10-prometheus-노출&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;10) Prometheus 노출&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#11-spring-security-통합과-포트-분리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;11) Spring Security
통합과 포트 분리&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#12-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;12) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#13-한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;13) 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-actuator가-하는-일과-기본-노출-정책&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1) Actuator가 하는 일과
기본 노출 정책&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Actuator는 **&amp;quot;운영 관점에서 앱을 들여다보는 창구&amp;quot;**를 표준화한
모듈이다. 앱이 살아 있는지, 메트릭이 어떤 값인지, 프로퍼티가 어떻게
바인딩됐는지, 빈이 몇 개 등록돼 있는지 — 이 모든 걸 HTTP(또는 JMX)로
꺼내 볼 수 있게 한다. 핵심은 이 창구들이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;개별 컨트롤러로 일일이
만들어진 게 아니라 공통 SPI 위에 얹혀 있다는 것&lt;/strong&gt;이다. 내장
엔드포인트도 커스텀 엔드포인트도 같은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Endpoint&lt;/code&gt; 인프라를
공유한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-actuator&lt;/code&gt; 의존성만 추가하면 Boot
자동구성이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebEndpointAutoConfiguration&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JmxEndpointAutoConfiguration&lt;/code&gt;을 타면서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator&lt;/code&gt; 경로와 JMX 도메인을 함께 세팅한다. 다만 어떤
엔드포인트가 밖으로 노출되느냐는 별개 문제다. 여기서 틀리기 쉽다 —
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;활성(enabled)과 노출(exposure)은 다른 축&lt;/strong&gt;이다. 활성은
엔드포인트 빈이 실제로 등록되느냐이고, 노출은 그 엔드포인트가 web이나
JMX 채널로 공개되느냐다. 둘 다 켜져야 밖에서 보인다.&lt;/p&gt;
&lt;h3 id=&quot;1-1-기본-노출-정책&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-1) 기본 노출 정책&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;채널&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본 노출 대상&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;web&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;JMX&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;*&lt;/code&gt; (모든 엔드포인트)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;web이 유독 인색하다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt; 하나뿐이고 나머지는 명시해야
보인다. 이 정책은 의도된 것이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env&lt;/code&gt;나 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;beans&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;threaddump&lt;/code&gt; 같은 엔드포인트는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자격 증명·내부 구조
노출 위험&lt;/strong&gt;이 있어서, 개발자가 &amp;quot;나는 이걸 열겠다&amp;quot;고 선언할 때만
나가도록 잠가놨다. JMX는 기본적으로 로컬/내부망용이라 덜 엄격하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-managementendpoints-네임스페이스-설정&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2) management.endpoints
네임스페이스 설정&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;노출·활성·세부 동작은 전부 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.&lt;/code&gt; 네임스페이스로
모여 있다. Boot의 설정 메타데이터도 이 네임스페이스를 별도로 분리해
두었는데, 이유는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;애플리케이션 설정(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.*&lt;/code&gt;)과 운영
설정(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.*&lt;/code&gt;)을 분리&lt;/strong&gt;하려는 것이다.
포트·경로·보안까지 모두 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management&lt;/code&gt;로 나눠 두면, 운영이 앱
설정을 건드리지 않고도 엔드포인트 정책만 조정할 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;2-1-대표-설정&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-1) 대표 설정&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;management&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;endpoints&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-3&quot;&gt;&lt;a href=&quot;#cb1-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-4&quot;&gt;&lt;a href=&quot;#cb1-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;exposure&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-5&quot;&gt;&lt;a href=&quot;#cb1-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; health,info,metrics,prometheus&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-6&quot;&gt;&lt;a href=&quot;#cb1-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;exclude&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; env&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-7&quot;&gt;&lt;a href=&quot;#cb1-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;base-path&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; /actuator&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;       # 기본 /actuator&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-8&quot;&gt;&lt;a href=&quot;#cb1-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;jmx&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-9&quot;&gt;&lt;a href=&quot;#cb1-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;exposure&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-10&quot;&gt;&lt;a href=&quot;#cb1-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;*&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-11&quot;&gt;&lt;a href=&quot;#cb1-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-12&quot;&gt;&lt;a href=&quot;#cb1-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;health&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-13&quot;&gt;&lt;a href=&quot;#cb1-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;show-details&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; when-authorized&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;   # never / always / when-authorized&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-14&quot;&gt;&lt;a href=&quot;#cb1-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;probes&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-15&quot;&gt;&lt;a href=&quot;#cb1-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;                  # liveness/readiness 분리 노출&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-16&quot;&gt;&lt;a href=&quot;#cb1-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-17&quot;&gt;&lt;a href=&quot;#cb1-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;9090&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;                        # 애플리케이션 포트와 분리&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 기억할 세 가지.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;첫째, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;endpoints.web.exposure.include&lt;/code&gt;는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;엔드포인트 id 기반 화이트리스트&lt;/strong&gt;다. 쉼표 구분 목록 또는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;*&lt;/code&gt; 와일드카드가 들어간다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;exclude&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;include&lt;/code&gt;에 걸린 것 중 일부를 빼는 용도다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;include: &amp;quot;*&amp;quot;&lt;/code&gt;에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;exclude: env&lt;/code&gt;로 제외하는
패턴이 흔한데, 이게 안전해 보여도 이후 Boot가 새 엔드포인트를 추가했을
때 자동으로 열리는 문제가 생긴다. 운영에서는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;include를 명시
리스트로 잠그는 쪽&lt;/strong&gt;이 안전하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;둘째, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;endpoint.{id}.&lt;/code&gt; 네임스페이스는 각 엔드포인트의 세부
옵션이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health.show-details&lt;/code&gt;가 대표적이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;never&lt;/code&gt;(기본)는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;{&amp;quot;status&amp;quot;: &amp;quot;UP&amp;quot;}&lt;/code&gt;만 반환하고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;always&lt;/code&gt;는 모든 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HealthIndicator&lt;/code&gt;의 디테일을 다
뱉는다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;when-authorized&lt;/code&gt;는 Spring Security로 인증된 요청에만
디테일을 보여준다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;셋째, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.server.port&lt;/code&gt;를 애플리케이션 포트와
다르게 지정하면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;내부망 전용 포트&lt;/strong&gt;로 Actuator가 분리된다.
LB에서 8080만 외부 공개하고 9090은 쿠버네티스 내부 probe나 Prometheus
scrape 전용으로 두는 게 흔한 구성이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-endpoint-spi-기술-중립-엔드포인트-구조&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3) @Endpoint SPI: 기술
중립 엔드포인트 구조&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Actuator 설계의 핵심은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;엔드포인트 정의를 특정 전송 기술에
묶지 않는다&lt;/strong&gt;는 점이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@RestController&lt;/code&gt;는 HTTP
전용이고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ManagedResource&lt;/code&gt;는 JMX 전용이다. Actuator는 그
중간을 원했다. **&amp;quot;한 번 선언하면 web과 JMX 둘 다에 나간다&amp;quot;**라는 모델을
위해 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Endpoint&lt;/code&gt;라는 추상을 새로 만들었다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Endpoint(id=&amp;quot;custom&amp;quot;)&lt;/code&gt;이 붙은 빈은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;EndpointDiscoverer&lt;/code&gt;가 찾아내
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ExposableWebEndpoint&lt;/code&gt; 또는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ExposableJmxEndpoint&lt;/code&gt;로 변환한다. 변환 과정에서 메서드마다
붙은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ReadOperation&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WriteOperation&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@DeleteOperation&lt;/code&gt;을 HTTP GET/POST/DELETE 또는 JMX
속성/작업에 각각 매핑한다. 디스패처 역할은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcEndpointHandlerMapping&lt;/code&gt;(Servlet 스택)이나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebFluxEndpointHandlerMapping&lt;/code&gt;(Reactive)이 맡는다.&lt;/p&gt;
&lt;h3 id=&quot;3-1-어노테이션-맵&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-1) 어노테이션 맵&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어노테이션&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;용도&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Endpoint(id=&amp;quot;...&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;기술 중립 엔드포인트 선언. web·JMX 동시 노출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WebEndpoint(id=&amp;quot;...&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;web 전용 엔드포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@JmxEndpoint(id=&amp;quot;...&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;JMX 전용 엔드포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ServletEndpoint(id=&amp;quot;...&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;서블릿 레벨 커스텀 엔드포인트 (&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Boot 3.3+에서
deprecated&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ReadOperation&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HTTP GET / JMX 속성 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WriteOperation&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HTTP POST / JMX 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@DeleteOperation&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HTTP DELETE / JMX 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Selector&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;경로 변수 매핑 (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/{id}/{selector}&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;3-2-엔드포인트-발견-흐름&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-2) 엔드포인트 발견 흐름&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;32_spring-boot-actuator-endpoints-01&quot; src=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA6kAAAFgCAMAAABuec/UAAAAYFBMVEUAAAAJCQkQEBAYGBgiIiIsLCwzMzM8PDxAQEBISEhRUVFdXV1nZ2dsbGxycnJ+fn6BgYGMjIyTk5OZmZmmpqaoqKiwsLC8vLzFxcXJycnW1tbe3t7j4+Pv7+/x8fH///9ZqDDkAAAAKnRFWHRjb3B5bGVmdABHZW5lcmF0ZWQgYnkgaHR0cHM6Ly9wbGFudHVtbC5jb212zsofAAABdGlUWHRwbGFudHVtbAABAAAAeJxdkr9OAkEQxvt9igmVFnBKYUFhUG6VEBESL6Fe7gZYvdvd7A1KYnwBO6N2WJCQWNsa3+jwHdw7vPCn2+z3m2++mUwzJWFpmsQsvZPKCCsSSLTS4cTqBIHsFLeUCEdiGtOFVnQtnFzpWyRUkbBRhTHHkAylEYqgcmZMLENBUquWw3FGFRAptHYpriKjpSJfpqG+R4u2oPxdqllicI5CFQTfJQY4LJm2UFGMtiuMkWpcwIPuLt1JZiXNZ0Zb+m/b4Yy1oHoKfgPGSCWTHhwyP//mDdhE8aDZM2iLCWH1vFx9vzEO1XV1bpuKYYwlnhs4qdUALF3XrQbdBlgcy9SFgAcc7usd1xSLkHCbzLZUpQnByvGEQI/yEQHaQdCHSx54/d5N4Pn8igc8W8yd4omQpoK09R5l9ATZ5/L3/YU5M8ht9rzcGgC6+apX8x9XD9nrV7b42OBN98pPhh3X6kf1k1r9D8TKzRCZlBreAAAxgElEQVR42u2dh3rjOM+ooV7c4jiZut9z7v+2/t0pSey4qrdDqjfHPRZtvLMb2aIIAhRhkhIlcP8PEATpPPy1FUAQZA/QUxGEBdBTEYQF0FMRhAXE+G+4jq6tSAkOhlz+xbavrU5NucG5ftyuV+mqmn+MVl068xcxELprIVdq6LtIPHWtd6pvDdfD7KMZDa6tTV230ZkkXa/SbVO/vhJoYKWh7yQxI+qWNXzxK+irJ8i5iG5nk3S9Sle9DiiBBlYa+u5jr63sDro3cumeRoez/5iLUW7QwK57KoIgFPRUBGEB9FQEYQH0VARhAfRUBGGBvT11sz5CeuT5F9V+YV1UfEe4lpUfn/IzaUXFHNW2LkznmpZY++7/lb62HmhFA3iLvrRLaU/w3ultLfHbWRR9denfn9XL7xvQ9lGG5uXEfm+rplcmXLmeoIz4Q6zcYgkRwCkD5VANfuljgJfgByls9SPbaWWrTk6q+4i2p+X6x5Y+gYqx6stbXt0BXV9iTz9sPAedy83iq9Se0mrdIU2rmfkS1D3VBM+Tth0sN3cZ/qg9AcB95cZqaLf9NiW59tlZINDUfWqkokwiVBgFgTn3HrZoemWCqTCQ/c3bk3CAlRVLSlUn9B3b/bGPgIowhw6AwBfBbquhU+r+OEyadUe3dq5zeVKd75f5lKpIqXuqwUXmVqEtCbY3ak8AWMGTAoLUthowybXPzgJeh/2oSEmE0rz96aYnwbmWAp6R6E1+JGdcma3GB1hZsaRUdfxg8OoGwoE6KKuQJ72DK4LTVkOn1P1xBK4M0Q5PPde5PKnO98t8SlWk1DzVCcZLgwqdR8o6UMd8to1T59EjRCszEIY9MCyH6w9XdvSXtLMsQRqp5LO2CnojCG0lG4TVk5Jc0cKKeg/gzdQHiKYyl4jaQSYCooXJ9aGkaamUx1pJiaWj2WYcJ3orm6ieWZJlmwrEUezFM5+oRY6U1/qw/OVCXm7wj+C6oD78HeYOlum/3cqymatq1fHkX1q56Wkq1Vs7Mjiaw4eu7kZKnpfkLs79SXWfGpro0hQTt5FyTYPsWkSlUA7LFqRllT48VivK0ncsEp9H6ioYDN5t6UHO2t1oq3n713khq6VGiD1cucEf2ZhqsweD07WQPr3iWytNtjb5Nsb3yUx7rUwGAv35fVRWgSLyAz1PGMPUJTneZWVNtpDPlupJSa65NexvNiBpGxOWbj8VtZXI9/0oFwELQ3+IH7PJNcxL8aFWUoIGfqLp3HscSbklWTbJoM2CE1K1qFxdrXw50hN3EK0eYPnurRxBdTIrc/23W1k2s1J1/sYecFnlpqepELgFBRxwVdEFh5fyvOQnLT33p9R96BICenSqS1NM7CeVmhYlk4yAZQHKFqRlFR9Khc0Nbex4wY669q2FLq9eg6FXtLtGned67VXnceZCVrNGqD2VBn9kY6r2qZGlcT3DpGK4LwK82MN8mxIYYvJDQjogybJ7YtTLErRH0H6vJxA9qYFly34uu5Gk0FyB2e+DbfVh6MxhMxaEXNSWWv4L8KhnIkJyZkD7BYWmRSmxJaWSMoT0RPqiyuWW5Nn0tdULrYdcLYBnqfLlNIfcbpcgu8Y33iP16edWpvqLO6xMDxuWrPT+gjIsKjc9TXmFbFGCIxNVd8QZka0WeYtzf0rdB69ZIZkuTTEA9ZrWVq5kjZxSrkKbSpNMhAkmVeC/3bX9JDtvwoRzieS03dXrvKTXPnWeZM5lteSh9lQa/HGNqeqpViT7PG9FpCGLpFiZdDPpNut7fUh/DSLLCcqr1eMulBdJg5NVoCrzEG5LSveaDvhELvf48q5+6KSJps/xCCAV4dHZfTKRTzUsSqGUS8oI006+v/o76GeW5NkkyegZoHuZWmRHoSP9ciF8EUwtrl7qramVqf67rGwxUxzbm+lTrnh+mtoqpIS88UIZDNd9KBmdn/tT6l6kl0tXm1KTaYqBRk3rKyuIdKfc0Aptyk0yEVYawH2IJBM7dC4uNm931Tov6bVPnSeZizbcyJO3nBMbU9VTDVjQjbV9FBqldRtNPV21GwlcWJLsbktK9ypqfqZE2AknVHPXr/q3l1LCi9JShuJ64UxSS4ps+tInLlNRq/LlQgQC+FL26XQrOUWR3i0+VbxxmrahbNZk3AubSG0z+hStOHowTa7rUhFTL1SULF/lW3JtsfrIyheb1jXN+7DOi8zijjwnNqaK+MAhfQ2EL0bqqS7PV7YECex4Lmw7416wgOIhMBEcMq7xip82UTQHUntSnEuCdMgWzUR5o2iHPU+mkJ8+KKYlRMOWUgDKQleQddy6/m6GqSVFNn259EaFWom10e7O/kREmw7LXVcFr1HWEVYSBPD6qeL5adpdn2TOI3EW6RKqRpfO/WlaNXSpiGnUNPndfGzmyrWpqSXzTh/Mwyo+b3cfmrefdVVZLXmikxtTxVNN6NNbeqpNft3dlW56A8i3KYJuzvoBp5EDhDXp0MkUQklGCqq1kdfQL6Q9TN9GSuRuvjaTaC4iaalHpHWu/K+CO5eFXFQ7oROf0BROMQR+ldRLomGzlKwkjuSNfNMdprnnqhTwHJ9akmcTFEtQIVcrtbb4ciHICEmZz23V8j01szJrhYdZmVSTHaxByxTPTtNOeIm2KtlRykbn5/6Uui+aTk2XkpiWmtaWnFbNlWtTa5IxqvnC+x91WM1uIGt3tTov6bWPdUnmdSZrS55Kgz+ynZS/GErclfdscwCCtQKNzNqzbcZDZFncQJN1w+hxG1G3pspznDCeL4AblX6i1Of5nBSgtCTFucawXoPSc9ZjESYvsy+FqFaCN/Jnkn8dzpbcwzw+l6mGjVLykmheQXrKrrc5BkhjLrOkyKY7tBZTtZJDK18ug8ibOmdPpHX4zGVW5kYcZGVSTVNOGcqZ4tlp6u9UQ/bI7Exx1LLRUnbuT6n7ooRUlxYxLTUtPnFcOVe/aIn1Jhln511h+PaBp/rF5DI5Km939Tov9NrHujjzMJe1JU+5wR/ZTrj4HfqLxn2ot+hLQGcY2bZEFMSVHZKa9Mkn0j2lCWFYn26Gvsi3J8W5okDgGjspm/xu03J7G/OTzCUNmwqUhZYsCIWKJbVsFbXqOsL64ciqrpNVuvP+9MEVhqOtTBXPTlMFI2/n7fWb5CVZ6+f+NK0auvjlum3UdDVXXlZLk0ww3x+zCyw1Az0nWosHLD1M9DqiZW3P09rgSw19Jx9cyhFq2wwuycOnuYvk5oyGl7clCSVJjRIPVT3Lx7edvxah2VWAzJJatopa3B4Xu05DGb71FDHg2u+gHG1l7TQdRJKX335GjtOqoUvly7aaLnIJW+QSlr7im4LWLgH8BSePD7A/1+TQlrU9T2uDP4RtWeXatrt0X8Nd9BRj4wv6h4Z008rP1EpufCgj+QanbX0np/bPZ1l3uRrZ5qmj2ra7dF/DnYi7beimlZ+p1ajxoYy+99rdy1p3uRrBJ8kRhAXQUxGEBdBTEYQFuu6p/K7HI5DDCbp+1tHAJolJey0N/Tzs4pmggdExV7XP9vDb1So9MIqbqEq3zvydGWgf8BqdZOUDmN7+WS6PWF7HsQ6PlnNx3U7jWpXOl9e5dOvM35mBhzSm1FPPhHPwq7Y+jQ6rdifmMKhyl4w474DeuZYZTKt2J+YwqHKXjLjBqTeC3CDoqQjCAuipCMIC5/XUDl8z6LBqd2IOgyp3yYjzXvtFEOQy4OgXQVgAPRVBWAA9FUFYAFc+sAmD5jCocpeMQE9lEwbNYVDlLhmBo18EYQH0VARhAfRUBGEBXKPEJgyaw6DKXTLiup4aHvKUb3RQiKlTVes4DJrDoMpdMkI4V+yG3axM27ahEtjBeG0WvwrbYz94s948ulgUUwTpNp8zT3VpKNW1GQTB7letmHYtX8qrCr3pHlHLEOQWuXjUlZh38Yn8VSf7HPutkS9mBUNQtPcDwgAhyA1xXk/N3jLjzxxRsX9CMHWEsQ5zx/31kL3c6VUJN8KjStNEOph9lQOTe+hD8G6D+iiQ9FF2TJLPWjrCN3FNXzc3/BscFmWqqdqNwKA5DKrcJSMus0bp1X+euGSk+gLftHeAgaQ+a2SmudkYNO6WO+EW5JjgyyO9oOQvYSLNInj1nibeK/ke5Mck+abSzyfB96lXi3D0OyFvYoUM0+YwqHKXjLjIPNXxRpoyoFs1UgMPRJ6XSUG+aVokVfmq6y6Zgg5VNY5y2Zvoj2CR75o+yCamyTFJPs4NVS6IL0UJHE5UkfvkIvPUIA1OFwDpRCUvu2CrJfNUOY4K7ecB7HgamN6N9yvgy6VjEp6nf7SnLPw7t58GCHJjXMRTFfBk8Gh019HWkOISdcrsBqkHgkDzuG3RY5Wf5nQ1iH04iD7nChiCdI3ztvx0ui3I755HBrqKvBRVjxdAdELSc4Z+EQ4cJGEl+HP6yXbFJacL4lqGjVidsNN8kaGpfCjIdE7rwdGxJm7hcgbb5jCocpeMuIinwvPcUocrOnB9Af5Jg4H976QPpkmGtd9pOh3Djmd/xcGKfnkJuScBvrz9Bql0E4YeQ/P1ljNQRzCaDXhYDA+YV4eWVjr6JtoJ0+YwqHKXjLjcG8/+RtQpozDpRNvuriT7fquPaWLYcn2LJoUc9dpFMDGMg26nOjO1/5nx7RHkclxm3jcNJdt9pp+y0W7bbVCh9oHfckyy/2EF4fNBWiiTN1Ps6/i4EHIDXGbdLw++PNb2PFjZczGvAsqBV35FxQrdtS8cu1oCQTrDjcd6c6YRcBxPOtbOqXaiYeyZw6DKXTLivKPft2uZ8RFRFC6WE/cW2kkBg82eQZW7ZMR5PXU4vJYdW3BmXEAGDvqId08XhiDX47ZXEjgzCEAa7jtjRpDOctOeSmapXG+E136RG+AyKx+6gTOTBnl32i3VToZBcxhUuUtG3HCst+oaJQRhmhse/fK902UgSEfAXgdBWAA9FUFYACNIsQmD5jCocpeMQE9lEwbNYVDlLhmBo18EYQH0VARhAfRUBGEBjPVW5aCYVp9CFIHVDJ3V4ZreBoMqd8mI8z5J3uF1FGXVHMuOY1GdGNPKyt+PmkuEcLUyPGmf379tUim2ED8z7/pBwJmL/u9+Q2CHa3obDKrcJSNuovoOI3yNNG69+CLAmv5E7hHTSikexnGheDPTYtCQ6L9EPc5Yf/349U2xkLLUOi/f4t/uRQjuGN9wjFDuz1PDP/0R2axev58Y0yp0tYbE9+CHCMPfs+8fCoyFfNue7oAde+pzGL5IGDUAodyfpy70UWgE0tAy9XTPwTGtkmymLNQlypZOKpTX146SCcmiaL3KvvnDMF0pE0KlFiUkGiSEs/5KJa66mQsBKL7z+9pVhnSAu1v5EJoP/h83XEHfPjqmVYKlNyR6ECdqZHybCcmiaJHvzwKMfvDLVAiVmpeQahBj/1Em45d5BP3/PQMZ/Mpf2arpXWeAaW5l5UOHT0ammqtyC31CHEryjo5plWBpDYlJnCsyCw0zIXkULdCeNG6ki6pTCClKSKRTot/T0QT6X+3/nGj5Z9AvhR1gpKZ3nQGmuZ4Rdzf69SSwk9mpcHRMqxiLlxsSxWSCGdBqTYToeRQtWtPrZVT+afTzEgrp3FMsVvnuyp795ejgHsitcXeeyoVAvMWNwKvfGts/plVM1qWWJcqcRa8tGeltNyKkEkXLeZ/0V4tCRGsJcr6Vvq7WzyDexH1I5FTuzlMlE7SlYvrG8vnYmFbJJ3PSlMg/zFcDMFYDMRNSRNEC2tcKjhlFXCZEaSkhHo4nRY3ijarC/3AlGXLL71FqVU12vcdV9M0yH8iU8riYVnEf6YVqi8QhN58D9zAqhORRtAi6+sqN3D8/ciHlEtLRr5gPeKl7euv4oyptM4chGFS5S0bc8HuUtmCsvmyNfrFvTCvC0vnSLtFPfv0KIXkULSqQJ+NrrhRQK/zwmp6ddrC6gq+FunfubvQLvfBPXwa/9R3i+8a0Iri9LRLFhrTSxVs+7TuF0o4PUEtXlHiR9Pj45uK75TIRpDqNooeOJ3w0jNknplWvtGCwVeK+gbH2RpQ35to/NIoWciPc3+iXYZxZFIE4wo71Hjlvn+p0dzDdYdX2RpQtLgot2rEyaA6DKnfJiPMW3OFwXp0MQ3ckpsVi6LoONw4WjLiF37m96FwYumOwp+SMRVxfx9B1d8fdeOotYE85IZD78umSEOZAT2UH0qMKpDu9thrIVbi7NUrs4rzrRXfKoDkMqtwlI/AuDSvgGqX7Bke/rICh6+4b/JlGEBZAT0UQFri7t7PcCAyaw6DKXTICPZVNGDSHQZW7ZASOfhGEBdBTEYQF0FMRhAUw1tue0Bdpl7ftfFqouA7X9A2p3CUj7jLWW0pLqLft/A20yhZODhV3bnO6DoMqd8mIm6i+I9kz1Nup+beFikOQA7hnT90z1Nup+beEikOQQ7hrT43x3vpDMFZf+XqIN2vpCN9EWMbx2QCiWRK7DfLwbSlHhopDkEO465UPcag3qTe3/dmAb4R4m0o/6Zu1k/hsAJs0dhvk4dtOCxV3szV9Qyp3yYi79tQk1NtIe3tTaWdZC/HGuaHKkeQ4Plseu42KysK3nRQq7mZr+oZU7pIRdz36TUO9Pf5y/6HbWoi35+kf7YnP47OlsduAhpdJw7edEioOQQ7irj01ZSZ7i/TaUDnEm/LTnK4eyvHZvOTd9+XwbQUHhopDkIO4a09NQr0t3R/2VOnXQ7xFhqbyYR6fLYvdRsjDt50SKg5BDuIe36OUv+ckDvX2uPgq9JyZLNdDvC1noI7y+GwgpLHboAjfdkKouBuu6RtSuUtG3N97lNy1/dRa340QbyEXTy2T+Gzkv6AYyZbCt5U4IFQcghzGnY1+Q3MdwPPWH8ZqiLdiw8X/lXyMEz7Ivk+oOAQ5jLvyVHdtkb5xm6MOzlQX55KDICXup1WR7jSMYLujwuBMBZ1LDoKUuJtYb+uZHd9CGQZvoKxY/99h8Ce2w42DBSPOe0Vp1d0wTau+uQkj4qxP6unCrk+Ha/qGVO6SEfdzd4/vf3vSOAGm9rU1QZDDuR9PBXrv9PtARFdFWOQW5g4HwPf77mb2dBM34ZG74rxXlLrs+JlqgtZ3RfZXy3e4pm9I5Q4ZcX9rlBCERe5qnoogzIKeiiAsgJ6KICxw129nYRgGzWFQ5S4ZgZ7KJgyaw6DKXTICR78IwgLoqQjCAtfw1E8Ls4QgN8M1Yr2Zv5v7VtaFDb2xBYQMmsOgyl0yoitRGc3SsvnsbdYdUa2TMGgOgyp3yYiurMXEMEsI8hGX9tQs2NLBYZbS+E0IglAufUUpC7Z0cJilLH4TgiBw8ZUPebClg8MspfGbLqca2zBoDoMqd8mIC3tqAJvZQqKe2h5myU/DLCX7ymGWnsM/ryfEC9+tGtswaA6DKnfJiAvPBOvBlvYPs5TEb7pavSBIt7iwp+bBllL2DrOUxm9CECTmYp6ahmnKgi1RDgmz1EviNyEIEnPet7M4WefobuxJ+rkebGnfMEtp/Kazq3YbMGgOgyp3yYhLRGUMzY1fBJWoB1vaN8zSme8f3UIzYdscBlXukhHnH/26G4sjA9sdJmGYJQQ5hDM7TBiHlIh2OiqGWUKQgzjv+37tVzuk92BG/rVDLDVCLt3E2Au5X858RUlab4inctykc45xY67KoDkMqtwlI868Rokf/XyWIYpmnVuQ0jmF7s4cBlXukhHnX6GvfPkxgHB6E+cFQbrCJZ6liTvW7vWqCMIwF7pZojyHloRvU0OQc3GxWG+c3LF4ajd2A5dBcxhUuUNGYKw3BGEBHKEiCAugpyIIC6CnIggLYAQpNmHQHAZV7pIR6KlswqA5DKrcJSNw9IsgLNBlT6XvAP4IjESF3A+f6KkHB4l6XX6cfpVIVAhyFT4xglQRJOoiIaLqhdSKuYVHrtg2h0GVu2TEJd6jtIUiSNQFQ0RtjUR1E+2EaXMYVLlLRlxmGeOr7Js/xDR6lD9zRMX+GQeJiuNCreMQUWlqcijJ87c3gKn/jRzyLQs7Fc3iKFMUjESF3DeXmaf6S3gWsuhRr/7zxPWTC0RxXKgkRFSamhxKEA0IDceHjZSHndokUaYoGIkKuW8udEVJe9K4NHqU4400JXvBWRwXKg4RlceWoofGWRz6oJwVWb08KYkyRRO7E4kKQa7ChVY+0OFmGj0qyINGQTkuVB5bKhuZamAZumZZnJonJVGmKkcfHYnqJu67t9Q0OzCocpeMOO8MrvI+qDR6VECDRGV3Pou4UPXYUsArpj12Z6BXkrzkBd6nR6K6ifdtFTBoDoMqd8mIC95PVeSlDV4gyO+Lt3WyK9oENC6U6IRZaul43RQkjQx+iyTbDZecXpKVHysJK8dII1HRYxRx7XktkaiyEhGEcS55VTSNHvU8t9ThKtmVxIWiIaL65dhSMdpcB1721SLslJBEmSrJij9jJCrk/jjvOx9Ww+r3PHrU3+h78iGNCxVHj6rHlqpnjIALhKaslAMjUdVVYxwmzEnj/bGk8i6uZ8RlVz7E0aOmoWS7z+me9MwJeWo7cRIH5YnniZGobmGSVIIJc3jxr6jn1xeYUHkX1zPivG88a/V7Hnx5rB0qal8U6QTV2IUNc0R5Y688mWdI5V0WXa1kfOMZckmcGReAMOifLuneOfNbRBGkgiibYhgVHStyLOftzf+7tjlI96D3yDjLnqjXVoRtzuupz929anAT990ZNMd5I40s4vo6z4zKH5pzNSMuuEapW3RYtVs2x3njhEDuywyp/LE9N+KpCFKB9KgC6U6vrcYtgJ6KXA5npvfl08UggJ6KXJDQ/4bd6bm4WKy3ztFh1W7VnFq8PxZU3gnGekMQ5ANwdIIgLICeiiAsgJ6KICyAEaTYhEFzGFS5S0agp7IJg+YwqHKXjMDRL4KwAIueijHekPujC55qrg88HmO8IXfHJ8Z6q5OHYrPN00veGuPtKNW6D4PmMKhyl4z4xFhvdfJQbI9nKHlrjLejVOs+DJrDoMpdMuIyyxjTyGzeW38Ixuor34jIJvtmLw7FRo+e+89kT2gIT86Se+jluUqSshBvGOMNuVMuM09NI7NJvbntzwZ8MyLbEp6HcSg2SkDjsy3Dp+hlM5EWRa6SpCzEG8Z4Q+6Ui/Q6jteL1LUnwch5E1X6XrreBKTfluhONNILujJoZHzK8+VHF8ke1frGBbOIy3OVJClfwVvSKehEheGsRSIlOUiMBdMYb9euXAQ5GxdZ+VBEZnsM3UlaTCUiW/MHgu4RuCRGW5arJCkN33Z4jLeaarcCg+YwqHKXjLjIe5SKyGwz2VukTvdhRLYqRa7TY7zVVLsVGDSHQZW7ZMRF5ql5ZLal+2Wy2UB7RDYaiq2VPBfGeEOQlMtcHU0jszmLr0LPmclya0S2OOJbS+YiF1RivMUDW4zxhnwu1TBY1+NSsd6qkdm2RGRL970ITx/IPDHGW1O1m4BBcxhUOcaZSaP82uftxXprxHFri8gWh2acis7kI5knxnhrqnYTMGgOgyonek/eXvlBj7+yEZ8Q6y02cGtENi4IxvoRRe0b422namzCoDkMqpwqrliRs3Yk4apG4BvPEGQXzpTeb8g71quAEaQQZD/C5eqKYbDO66n/XM0OBLkcNAwWx/FXjduBEaTYhEFzGFQ50/wN+Ei7chgs9FQ2YdAcBlVOFX8DMetOb8VTEeTm6EgYLPRUBPmIroTBQk9FkI/ge9fWINXjrNI6PBPpsGp3Yg6DKnfJCFz5gCAs0IkhOIIgO0BPRRAWQE9FEBbACFJswqA5DKrcJSNwjRKbMGhOrPLq2locqvRj9fFoXKOE3AmMvfhhMZt05K3ROE9FkO3wT7PgdCln0eTaCiBIl+mMq+IaJTZh0BwGVaZUXfV6RqCnsgmD5jCockzFVW/FUxHk9ujGABg9FUF20AlXxbs0yBVYij2AaKErizTIUJS+Rn24CYFTkmidEK7dQBJ7Iixp3BLVSY8dCgBm1KN740PDlQvygOz0DVtQBovmYcAr9FVliRgtLhzqQjdJYpAGtBfLb6nln6ZXv1mDKx/YhEFzyipvFOqpK1HxI3DCxDHjbbQByVn243e1ey9hX/HWogibSKGPdKfHUt9a9omQ5FDvNepxxvqLAi/QczeDxmGREviL/iOXikkKh7rQNJHG/qRl98dl3XNXvZWVDx1uPx1W7U7MaVX5CWDqPMcf0606gV9G7Knv4XfasVGPU2k8lF5xbODq+aHz4IcIgz/v3y3/m1IVmR72BNF8LY5SMTk1oUmi+i1Om6XHcNmiKmn25br1jqNfpHNIPh0Me/YwHoFyLUdYspAd6lk6acSCvrZ5sJUth3GP9nq/eGJOHEkQ/PRND4M84drrINFTkWvgrSCPg1vHsQbUOz0oXoPtb4ArvyXF1PJD08O0tTdQF864EgQlO4ymr0K+JqYmNPkaBEl3fO1paQP0VOQaBM42TzWtQI67vzDxloDn6LUiEEpOFdkP+aEWxM4pQwBfl8s/k+ZhMTwtriqmJjT7mvTh235GrsalYr11jg6rdifmVFSmk8Lw37bDpKGYvLNTpPHnwf/11KtPMMHm5fxQEWzaqQbUYUe9t6ki1g+L8WnMwJZ5av0rz0/BFXkQvkAb16t39FQ2YdCcPVWWssh/ErfeFgTQ0opDZc6mn41Yvvj41+7XD6MEhgZ7oX4H+L/nre/3vRVPRZCzIQyXswchjD9HfiWQrvlYHMY/zJdDMFYD0fY03ihNbrPDIj/0FtxjIaa6yYi/uvRR8WjDk+IH0CnQU5Gu8sAvNlwk0GV0pkk6z+9ZghuWO8ghN18ANxqB/0662UnRorPDSGZJfRAKMXQj/qwJTb4+0FlqPLvloFvgW0SRz2R12JPkfii2rHddOrVJpAcidazI58SPDvtMzc8OvkeJTRg05xiVRbmtgbr1+askxT0gJ4kfHnYlI84DeiqbMGjO+VR+7p/zsCsZcSj4LA2CsAB6KsImUZh/DD2I6isVzCI5sK6t6zlAT0Wug2mclNX4z4CVmXw3foP336J8wB94CyCyYiL3/dq2ngNc+cAmDJpTU3nZmEU6LVa17YuzzkcDMJX8mpH85S99QhXcF/LnIblJGiWL6iU4I7fydpYOt58Oq3Yn5lRVDppXZufrZqa2fXHWiLTcb6VnSHmIh7sR/POPkg58+Unw9Yv/fNbOCNcoIXdG/EDa0nSlsQbBux2KPxaO++uh97c3IKPab2nSPN7nvzugPgrwKnvWDzHJKvoAr8oI/JkTv54hyJoyV6xZ4L0g8m9lfoeeilyF5IG0kTRbadFLMNI8GFjiSAQ/oC9iyJLifdFbNIHF63fwrd6zkGalS5d80nrfomeOzkM5rryoyP0FtH/lvZC3urbY6Ehu5RcHYYvIpoPfkS4oDrhefyjpnMjzSrEKN0mK97nuQNcHdEWu+qRxSdZ3L1mW67pDTaUfZfm19FYyib65wfrFzZbyxr22recB387CJgyaU1E5eSBtvaTTTb+8qj4jSYrx6eyQPn+aXByKs/JREDddD9LHXjieL3We8cJ7XYfQDkDgzrhW6Xr1jmuU2IRBcyoqxw+k2e8P/xvQ3qJmjZ8nxQjgUpcUylkf5OQOjUQPjqU7T7Qtc/Dvv3beqDf/rV13/XrGq7+3EpURQfYjfiAtBN42o1CWNqLmaJzohDyItmsYQpbE032KuFGitSiXs0KQLHWQhZXg0VcfRWFIHVT+X3xIeuhiQK8OLxYXWFb46eA8FbkCyQNpmvr2qnF/uGdh9t97CIPo3w0MvT/uIE+K93HP4e8/0TNXzgpe2seM3b9L2vmKWedaRjZNzzMNeT+lug32qcgVsFTqd9zXgOeIm0nfA450Gco/vgC6Ruanj3lSvE/+EcRj3x9FVlAsVaDfe3oowBgio/zAmzwms1OyfVqvSM4+YzFb28E1SmzCoDlllbNlD0I6qkvnoLQ1ZndbsiSxlF7K+riykjFt8tYGz34uDQ9FEf6hW36036tDjzPic8EnyZHP5NrPY7OrOc5TEYQF0FMRhAXQUxGEBXDlA5swaA6DKnfJCFxNyCYMmhOrzF07ENPRcIURVwHvpyKfScded80QOE9FEBZAT0UQFsC3s7AJg+YwqHKXjMA1SgjCAjj6RRAWQE9FEBZAT0UQFsA1SmzCoDkMqtwlI3CNEpswaA6DKm8x4lPWWdkTofId1yghyKF8xqOq4fSp4qo4T0WQLsI/TYPK92srhCBIG0LVVXGNEpswaA6DKl/XiKqroqeyCYPmMKjylY2ouCqOfhGkq5RdFT0VQTpLyVXRUxGkuxSuiisf2IRBcxhU+WMjluGAus8qeOCWIQj6R74UBHz8Vv8gjREpplGt7Hmy7Q3Bfk8+DirvxSCumtxXRU9lEwbNYVDlj43Y+NwDgD+HEbcBwZn/3OpMxsIXfX44AvCW8Q6vP05SwjCOh0X9UH6KPy3SPjR/45Qy+5IegSDIURjEU434kzpx/6zHWw6zZxONj5wZjED9Fu+Z5Wmcln1yZ3HfGaS/BUXPmrgseiqCHInikC52oySr9nkac9mfOfyjDkvTlcYavMrRRnhUwdJ7xCXVoTECJxnt+r1Mhvc73ugPEMEk/si1F4aeiiBHormG4vhD6qn+ylB0gFfh2+adbEfSbKWBb6mT5eIbyHOf+FlIo0MGwXOcNVvSq/2TbIl78uI72DIPykNrYRjrjU0YNIdBlXcYwenG2NCSWHNeQIarjteLlLUrjyBU1vToLxydmPbcX5LsuVrsgkmXmcRphvkmkyU9RWQaa9NrVO0TevRUNmHQHAZV3mVE37CMZMiqTeyX4NmHjQGSL6+XUXz7U+YSxxwP3UCXqa/x/BRckQchvkoE4zFEfsgL5Oi1RcXQaOriJ3gqgtwTqvAOmpV+Jh9EGNGgrvb7pL9alA8UsutG6neA/3sugqRH7wYvBoHyKA0kO9mlaK1loaciyNH0VoNkNBt6niODLC0F1eND4G0zCrNVRYadHS7EV5+iDUkRkmu7hvWTjJ6j6eI5DdkMmwg9FUHOzFBSkw+mKehD4L5MX4GfaOobN3L//EwPEtXscD4eDJemqyCFK1kMfIf4rZIMet0tRZ33fb8dvrndYdXuxBwGVd5ixEfhycOIdo0Bz4X7rdR1N17AC3p+1wZmfP22bFIcrlFiEwbNYVDlI4xI3FPYe0W9/FjbMflQMIIg3QY9FUFYAD0VQVgA387CJgyaw6DKXTIC1yixCYPmMKjyFiO4T3k1d05yQwfvpyLIgQxOF3E4OE9FEBZAT0UQFsBYb2zCoDkMqtwlI9BT2YRBcxhUuUtG4OgXQVgAPRVBWAA9FUFYANcosQmD5jCocpeMOO/zqQiCXAYc/SIIC6CnIggLoKciCAvgygc2YdAcBlXukhHJszTROjqLtNA+XQaBGxSxOZzziDyXahXdTuOkSj/JHLW4gnmuM39plbti4HmNOKQxJZ661jo1Cg7X+cvfzKh/bW226nYi16t0O9CvrwQaeFhjSswIu2UNX/wM+uoJci6s24lcr9JVL/8YdevM35mBhzSmLttB+bSxGcMaHc65BvCd5QYN7LqnIghCQU9FEBZAT0UQFkBPRRAWQE9FEBbY21M36yOkR55/Ue0X1kXFd4RrWfnxKT+TVlTMUW3rwnSuadXf9+v/lb62HmhFA3iLvrRLaU/w3ultLfHbWRR9jeNK/qxeft9AS1TYpjI0Lyf2e1s1vTLhyvUEZcQfYuUWS4gAThkc/BTlL30M8BL8IIWtfmQ76SnP6+/ouo9oe1quf2zpE6iYvKDCisGIbOzph43noHO5WXyV2lNarTukaTUzX4K6p5rgedK2g+XmLsMftScAuK/cWA3ttt+mJNc+OwsEmrpPjVSUSYQKoyAw597DFk2vTDAVBrK/eXsSDrCyYkmp6oS+Y7s/9hFQEebQARD4IthtNXRK3R+HSbPu6NbOdS5PqvP9Mp9SFSl1TzW4yNwqtCXB9kbtCQAreFJAkNpeOJ7k2mdnAa/DflSkJEJp3v5005Pg5Ao7P9EbjaEpKLPV+AArK5aUqo4fDF7dQDhQB2UV8qR3cEVw2mrolLo/jsCVIdrhqec6lyfV+X6ZT6mKlJqnOsF4aVCh80hZB+qYz7Zx6jx6hGhlBsKwB4blcP3hyo7+knaWJUgjlXzWVkFvBKGtZIOwelKSK1pYUe8BvJn6ANFU5hJRO8hEQLQwuT6UNC2V8lgrKbF0NNuM40RvZRPVM0uybFOBOIq9eOYTtciR8loflr9cyMsN/hFcF9SHv8PcwTL9t1tZNnNVrTqe/EsrNz1NpXprRwZHc/jQ1d1IyfOS3MW5P6nuU0MTXZpi4jZSrmmQXYuoFMph2YK0rNKHx2pFWfqOOBTzSF0Fg8G7LT3IWbsbbTVv/zovZLXUCLGHKzf4IxtTbfZgcLoWPy7gWytNtjb5Nsb3yUx7rUwGAv35fVRWgSLyAz1PGMPUJTneZWVNtsU7Z+pJSa65NexvNiBpGxOWbj8VtZXI9/0oFwELQ3+In2vINcxL8aFWUoIGfqLp3HscSbklWTbJoM2CE1K1qFxdrXw50hN3EK0eYPnurRxBdTIrc/23W1k2s1J1/sYecFnlpqepELgFBRxwVdEFh5fyvOQnLT33p9R96BICenSqS1NM7CeVmhYlk4yAZQHKFqRlFR9Khc0Nbex4wY669q2FLq9eg6FXtLtGned67VXnceZCVrNGqD2VBn9kY6r2qZGlcT3DpGK4LwK82MN8mxIYYvJDQjogybJ7YtTLErRH0H6vJxA9qYFly34uu5Gk0FyB2e+DbfVh6MxhMxaEXNSWWv4L8KhnIkJyZkD7BYWmRSmxJaWSMoT0RPqiyuWW5Nn0tdULrYdcLYBnqfLlNIfcbpcgu8Y33iP16edWpvqLO6xMDxuWrPT+gjIsKjc9TXmFbFGCIxNVd8QZka0WeYtzf0rdB69ZIZkuTTEA9ZrWVq5kjZxSrkKbSpNMhAkmVeC/3bX9JDtvwoRzieS03dXrvKTXPnWeZM5lteSh9lQa/HGNqeqpViT7PG9FpCGLpFiZdDPpNut7fUh/DSLLCcqr1eMulBdJg5NVoCrzEG5LSveaDvhELvf48q5+6KSJps/xCCAV4dHZfRquLtGwKIVSLikjTDv5/urvoJ9ZkmeTJKNngO5lapEdhY70y4XwRTCTJ7Oot6ZWpvrvsrLFTHFsb6ZPueL5aWqrkBLyxgtlMFz3oWR0fu5PqXuRXi5dbUpNpikGGjWtr6wg0p1yQyu0KTfJRJi/50sDJZnYoXNxsXm7q9Z5Sa996jzJXLThRp685ZzYmKqeasCCbqzto9Aordto6umq3UjgwpJkd1tSuldR8zO1R3BITqjmrl/1by+lhBelpQzF9cKZpJYU2fSlT1ymolbly4UIBPCl7NPpVnKKIr1bfKp44zRtQ9msybgXNpHaZvQpWnH0YJpc16Uipl6oKFm+yrfk2mL1kZUvNq1rmvdhnReZxR15TmxMFfGBQ/oaCF+M1FNdnq9sCRLY8VzYdsa9YAHFQ2AiOGRc4xU/baJoDqT2pDiXBOmQLZqJ8kbRDnueTCE/fVBMS4iGLaUAlIWuIOu4df3dDFNLimz6cumNCrUSa6Pdnf2JiDYdlruuCl6jrCOsJAjg9VPF89O0uz7JnEfiLNIlVI0unfvTtGroUhHTqGnyu/nYzJVrU1NL5p0+mIdVfN7uPjRvP+uqslryRCc3poqnmtCnt/RUm/y6uyvd9AaQb1ME3Zz1A04jBwhr0qGTKYSSjBRUayOvofSGhofp20iJ3M3XZhLNRSQt9Yi0zpX/VXDnspCLaid04hOawimGwCehoVMNm6VkJXEkb+Sb7jDNPVelgOf41JI8m6BYggq5Wqm1xZcLQUZIynxuq5bvqZmVWSs8zMqkmuxgDVqmeHaadsJLtFXJjlI2Oj/3p9R90XRqupTEtNS0tuS0aq5cm1qTjFHNF97/qMNqdgNZu6vVeUmvfaxLMq8zWVvyVBr8ke2k/MVQ4q68Z5sDEKwVaGTWnm0zHiLL4gaarBtGj9uIujVVnuOE8XwB3Kj0E6U+z+ekAKUlKc41hvUalJ6zHosweZl9KUS1EryRP5P863C25B7m8blMNWyUkpdE8wrSU3a9zTFAGnOZJUU23aG1mKqVHFr5chlE3tQ5eyKtw2cuszI34iArk2qacspQzhTPTtPuV9zIHpmdKY5aNlrKzv0pdV+UkOrSIqalpsUnjivn6hctsd4k4+y8KwzfPvBUv5hcJkfl7a5e54Ve+1gXZx7msrbkKTf4I9tJ8g79ReM+1Fv0JaAzjGxbIgriyg5JTfrkE+me0oQwrE83Q1/k25PiXFEgcI2dlE1+t2m5vY35SeaShk0FykJLFoRCxZJatopadR1h/XBkVdfJKt15f/rgCsPRVqaKZ6epgpG38/b6TfKSrPVzf5pWDV38ct02arqaKy+rpUkmmO+P2QWWmoGeE63FA5YeJnod0bK252lt8KWGvpMPLuUItW0Gl+Th09xFcnNGw8vbkoSSpEaJh6qe5ePbzl+L0OwqQGZJLVtFLW6Pi12noQzfeooYcO13UI62snaaDiLJy28/I8dp1dCl8mVbTRe5hC1yCUtf8U1Ba5cA/oKTxwfYn2tyaMvanqe1wR/Ctqxybdtduq/hLnqKsfEF/UNDumnlZ2olNz6UkXyD04bbRr/aP59l3eVqZJunjmrb7tJ9DXci7rahm1Z+plajxocy+t5rdy9r3eVqBJ8kRxAWQE9FEBZAT0UQFkBPPZQbfOkzwgCJp36wjvQahIU3SJ8Xduhg3U7kepVuF9cRO3bm78zAQxpTsvLhEyN+7aXUBWK9XUK307hepV8n1hsa2OCQxpR4KoIg3QbnqQjCAuipCMIC6KkIwgL/Hxag9EZx48o1AAAAAElFTkSuQmCC&quot; alt=&quot;32_spring-boot-actuator-endpoints-01&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 구조 덕분에 한 엔드포인트를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;두 채널이 공유하는 비즈니스
로직&lt;/strong&gt;으로 다룰 수 있다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt;도
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;info&lt;/code&gt;도 이 경로로 나간다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-readoperation--writeoperation--selector&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4) @ReadOperation /
@WriteOperation / @Selector&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실제로 커스텀 엔드포인트를 만들어 보자. 앱의 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;피쳐 토글
상태&lt;/strong&gt;를 읽고 쓰는 엔드포인트라고 가정한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Component&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Endpoint&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;id &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;features&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-3&quot;&gt;&lt;a href=&quot;#cb2-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; FeaturesEndpoint &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-4&quot;&gt;&lt;a href=&quot;#cb2-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-5&quot;&gt;&lt;a href=&quot;#cb2-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Boolean&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; features &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;ConcurrentHashMap&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&amp;gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-6&quot;&gt;&lt;a href=&quot;#cb2-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-7&quot;&gt;&lt;a href=&quot;#cb2-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@ReadOperation&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-8&quot;&gt;&lt;a href=&quot;#cb2-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Boolean&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-9&quot;&gt;&lt;a href=&quot;#cb2-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; features&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-10&quot;&gt;&lt;a href=&quot;#cb2-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-11&quot;&gt;&lt;a href=&quot;#cb2-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-12&quot;&gt;&lt;a href=&quot;#cb2-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@ReadOperation&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-13&quot;&gt;&lt;a href=&quot;#cb2-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; Feature &lt;span class=&quot;fu&quot;&gt;feature&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;@Selector&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-14&quot;&gt;&lt;a href=&quot;#cb2-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;bu&quot;&gt;Boolean&lt;/span&gt; enabled &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; features&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-15&quot;&gt;&lt;a href=&quot;#cb2-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; enabled &lt;span class=&quot;op&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;Feature&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;enabled&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-16&quot;&gt;&lt;a href=&quot;#cb2-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-17&quot;&gt;&lt;a href=&quot;#cb2-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-18&quot;&gt;&lt;a href=&quot;#cb2-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@WriteOperation&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-19&quot;&gt;&lt;a href=&quot;#cb2-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;configureFeature&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;@Selector&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;boolean&lt;/span&gt; enabled&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-20&quot;&gt;&lt;a href=&quot;#cb2-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        features&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; enabled&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-21&quot;&gt;&lt;a href=&quot;#cb2-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-22&quot;&gt;&lt;a href=&quot;#cb2-22&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-23&quot;&gt;&lt;a href=&quot;#cb2-23&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@DeleteOperation&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-24&quot;&gt;&lt;a href=&quot;#cb2-24&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;deleteFeature&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;@Selector&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-25&quot;&gt;&lt;a href=&quot;#cb2-25&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        features&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-26&quot;&gt;&lt;a href=&quot;#cb2-26&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-27&quot;&gt;&lt;a href=&quot;#cb2-27&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-28&quot;&gt;&lt;a href=&quot;#cb2-28&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;Feature&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt;boolean&lt;/span&gt; enabled&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-29&quot;&gt;&lt;a href=&quot;#cb2-29&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 한 클래스만으로 다음이 동시에 노출된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;HTTP&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;경로&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;메서드&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/features&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;features()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/features/{name}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;feature(name)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;POST&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/features/{name}&lt;/code&gt; (body:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;{&amp;quot;enabled&amp;quot;: true}&lt;/code&gt;)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configureFeature(name, enabled)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;DELETE&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/features/{name}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;deleteFeature(name)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 주의할 것이 있다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ReadOperation&lt;/code&gt;은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;반환값이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;null&lt;/code&gt;이면 HTTP 404&lt;/strong&gt;를 자동으로
돌려준다. 명시적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ResponseEntity&lt;/code&gt;를 만들 필요가 없다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WriteOperation&lt;/code&gt;의 파라미터 중 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Selector&lt;/code&gt;가 붙지
않은 것은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;요청 body의 JSON 필드로 매핑&lt;/strong&gt;된다. 위의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;enabled&lt;/code&gt; 파라미터가 그렇다. 이건 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@RequestBody&lt;/code&gt;와
다르다 — 객체 전체가 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;필드 단위로 개별 파라미터에
바인딩&lt;/strong&gt;된다.&lt;/p&gt;
&lt;h3 id=&quot;4-1-selector와-매치-타입&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-1) @Selector와 매치 타입&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Selector&lt;/code&gt;는 두 가지 매치 모드를 지원한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ReadOperation&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Object&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;one&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;@Selector&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;          &lt;span class=&quot;co&quot;&gt;// 단일 경로 세그먼트&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-4&quot;&gt;&lt;a href=&quot;#cb3-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ReadOperation&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-5&quot;&gt;&lt;a href=&quot;#cb3-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Object&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;@Selector&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;match &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; Match&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;ALL_REMAINING&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;[]&lt;/span&gt; parts&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-6&quot;&gt;&lt;a href=&quot;#cb3-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// /actuator/features/a/b/c → parts = [&amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;, &amp;quot;c&amp;quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;두 번째 패턴은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;중첩 키&lt;/strong&gt; 같은 걸 표현할 때 쓴다. 내장
엔드포인트 중 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;logger&lt;/code&gt;가 이런 구조를 쓴다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/loggers/com.example.Foo&lt;/code&gt;처럼 로거 이름에
점(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.&lt;/code&gt;)이 들어가기 때문이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-내장-엔드포인트-한눈에&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5) 내장 엔드포인트 한눈에&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot는 운영에서 쓸 만한 엔드포인트를 이미 내장해 두었다. 의존성만
추가해 두면 활성 상태로 있고, 노출만 해주면 된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;id&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;용도&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;HTTP 메서드&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HealthContributor&lt;/code&gt; 집계 상태&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;빌드/Git/커스텀 info&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;metrics&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Micrometer 메트릭 조회&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;?tag=key:value&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Prometheus scrape 포맷 출력&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;loggers&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;로그 레벨 조회/변경&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET/POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Environment&lt;/code&gt; 프로퍼티 조회&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configprops&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt; 바인딩 결과&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mappings&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;MVC/WebFlux 매핑 목록&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;beans&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;ApplicationContext의 모든 빈&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;conditions&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;자동구성 조건 평가 결과&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;threaddump&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;JVM 스레드 덤프&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;heapdump&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;JVM 힙 덤프 (바이너리)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;scheduledtasks&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;스케줄러 작업 목록&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;caches&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;캐시 목록/조작&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET/DELETE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;httpexchanges&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최근 HTTP 요청 기록 (3.0부터 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;httptrace&lt;/code&gt; →
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;httpexchanges&lt;/code&gt;로 이름 변경)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sessions&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HTTP 세션 조회/만료&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET/DELETE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;flyway&lt;/code&gt; / &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;liquibase&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;마이그레이션 이력&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ApplicationContext.close()&lt;/code&gt; (&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본
비활성&lt;/strong&gt;)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;POST&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 중 운영 필수는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;info&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;metrics&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;prometheus&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;loggers&lt;/code&gt;
정도다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;beans&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configprops&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;threaddump&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;heapdump&lt;/code&gt;는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;디버깅용&lt;/strong&gt;이지 외부에 상시 노출할 것이 아니다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shutdown&lt;/code&gt;은 기본 비활성이고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.endpoint.shutdown.enabled=true&lt;/code&gt; + 노출 +
permitAll 조합이 걸리면 외부에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;curl -X POST&lt;/code&gt;로 앱을 죽일
수 있다. 여기서 틀리기 쉽다 — Actuator 보안을 별도로 걸지 않으면 이
조합이 생각보다 쉽게 발생한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-health-contributor-기본-등록과-커스텀-작성&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6) Health
Contributor: 기본 등록과 커스텀 작성&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/health&lt;/code&gt;는 단일 표시기가 아니라 여러
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HealthContributor&lt;/code&gt;의 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;집계 결과&lt;/strong&gt;다. Boot가
클래스패스와 자동구성을 훑어서 감지된 컴포넌트마다 표시기를 등록한다.
DataSource가 있으면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceHealthIndicator&lt;/code&gt;가, Redis가
있으면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RedisHealthIndicator&lt;/code&gt;가 자동으로 달라붙는다.&lt;/p&gt;
&lt;h3 id=&quot;6-1-자동-등록되는-주요-indicator&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-1) 자동 등록되는 주요
Indicator&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;조건&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Indicator&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;항상&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DiskSpaceHealthIndicator&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PingHealthIndicator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;DataSource 빈 존재&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceHealthIndicator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-data-redis&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RedisHealthIndicator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-data-mongodb&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MongoHealthIndicator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Elasticsearch&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ElasticsearchRestHealthIndicator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;RabbitMQ&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RabbitHealthIndicator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Kafka&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;KafkaHealthIndicator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PingHealthIndicator&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;항상 UP&lt;/strong&gt;을
반환하는 기본 표시기다. 아무 의존성이 없는 상태에서도
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/health&lt;/code&gt;가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UP&lt;/code&gt;을 돌려주는 이유다.&lt;/p&gt;
&lt;h3 id=&quot;6-2-커스텀-healthindicator&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-2) 커스텀 HealthIndicator&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;커스텀 헬스 표시기는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HealthIndicator&lt;/code&gt; 하나만 구현하면
된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Component&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-2&quot;&gt;&lt;a href=&quot;#cb4-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; ExternalApiHealthIndicator &lt;span class=&quot;kw&quot;&gt;implements&lt;/span&gt; HealthIndicator &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-3&quot;&gt;&lt;a href=&quot;#cb4-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-4&quot;&gt;&lt;a href=&quot;#cb4-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;final&lt;/span&gt; ExternalApiClient client&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-5&quot;&gt;&lt;a href=&quot;#cb4-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-6&quot;&gt;&lt;a href=&quot;#cb4-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-7&quot;&gt;&lt;a href=&quot;#cb4-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; Health &lt;span class=&quot;fu&quot;&gt;health&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-8&quot;&gt;&lt;a href=&quot;#cb4-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-9&quot;&gt;&lt;a href=&quot;#cb4-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            ExternalApiClient&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;Status&lt;/span&gt; s &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; client&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;ping&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;   &lt;span class=&quot;co&quot;&gt;// 외부 호출&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-10&quot;&gt;&lt;a href=&quot;#cb4-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;cf&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;s&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;ok&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-11&quot;&gt;&lt;a href=&quot;#cb4-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; Health&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;up&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-12&quot;&gt;&lt;a href=&quot;#cb4-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDetail&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;latencyMs&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; s&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;latencyMs&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-13&quot;&gt;&lt;a href=&quot;#cb4-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-14&quot;&gt;&lt;a href=&quot;#cb4-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-15&quot;&gt;&lt;a href=&quot;#cb4-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; Health&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;down&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDetail&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;reason&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; s&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;reason&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-16&quot;&gt;&lt;a href=&quot;#cb4-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Exception&lt;/span&gt; e&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-17&quot;&gt;&lt;a href=&quot;#cb4-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; Health&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;down&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-18&quot;&gt;&lt;a href=&quot;#cb4-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-19&quot;&gt;&lt;a href=&quot;#cb4-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-20&quot;&gt;&lt;a href=&quot;#cb4-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot는 빈 이름에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HealthIndicator&lt;/code&gt; 접미사를 떼어 id로
사용한다. 위 예시는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;externalApi&lt;/code&gt;라는 id로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/health&lt;/code&gt;에 포함된다.&lt;/p&gt;
&lt;h3 id=&quot;6-3-status-집계-규칙&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-3) Status 집계 규칙&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여러 표시기의 상태가 섞이면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;StatusAggregator&lt;/code&gt;가
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;우선순위&lt;/strong&gt;에 따라 최종 상태를 결정한다. 기본 우선순위는
다음과 같다.&lt;/p&gt;
&lt;pre style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DOWN &amp;gt; OUT_OF_SERVICE &amp;gt; UP &amp;gt; UNKNOWN&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;하나라도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DOWN&lt;/code&gt;이면 전체가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DOWN&lt;/code&gt;이다. 이
규칙이 의미하는 바는 무겁다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;아주 작은 외부 의존성 하나가 전체
헬스를 내린다&lt;/strong&gt;. 전체 헬스가 내려가면 쿠버네티스 liveness probe가
이걸 보고 Pod를 재시작할 수도 있다. 여기서 &amp;quot;이 의존성이 정말 liveness에
연결되어야 하나&amp;quot;를 한번 묻고 가야 한다.&lt;/p&gt;
&lt;h3 id=&quot;6-4-함정-느린-healthindicator&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-4) 함정: 느린
HealthIndicator&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;커스텀 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HealthIndicator&lt;/code&gt;에서 외부 DB/HTTP를 호출하면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;응답이 오기 전까지 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/health&lt;/code&gt; 전체가
대기&lt;/strong&gt;한다. 표시기 하나가 3초 지연되면 헬스 응답도 3초 걸린다.
쿠버네티스는 probe 타임아웃을 보통 1초로 잡기 때문에, 이 조합이 재앙으로
간다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;해결책은 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;그룹 분리&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.endpoint.health.group.*&lt;/code&gt;로 표시기를 그룹화해서,
probe에는 가벼운 그룹만 연결한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본 비활성화 + 선택 활성화&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.health.defaults.enabled=false&lt;/code&gt;로 전체를 끄고
필요한 것만 켠다&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;management&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-3&quot;&gt;&lt;a href=&quot;#cb6-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;health&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-4&quot;&gt;&lt;a href=&quot;#cb6-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-5&quot;&gt;&lt;a href=&quot;#cb6-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;liveness&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-6&quot;&gt;&lt;a href=&quot;#cb6-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;          &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; livenessState&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-7&quot;&gt;&lt;a href=&quot;#cb6-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;readiness&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-8&quot;&gt;&lt;a href=&quot;#cb6-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;          &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; readinessState,db,redis&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 설정이면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/health/liveness&lt;/code&gt;는 앱 자체 상태만
보고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/health/readiness&lt;/code&gt;는 DB·Redis를 포함해서
본다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-kubernetes-probes와-applicationavailability&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7) Kubernetes
Probes와 ApplicationAvailability&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;쿠버네티스는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;liveness&lt;/strong&gt;(살아 있나)와
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;readiness&lt;/strong&gt;(트래픽 받을 준비됐나)를 구분한다. Boot는 이
두 개념을 **&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ApplicationAvailability&lt;/code&gt;**라는 추상으로
제공하고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;probes.enabled=true&lt;/code&gt;면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/health/liveness&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/health/readiness&lt;/code&gt;를 별도로 노출한다.&lt;/p&gt;
&lt;h3 id=&quot;7-1-livenessstate와-readinessstate&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-1) LivenessState와
ReadinessState&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb7&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb7-1&quot;&gt;&lt;a href=&quot;#cb7-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;enum&lt;/span&gt; LivenessState &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-2&quot;&gt;&lt;a href=&quot;#cb7-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    CORRECT&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;   &lt;span class=&quot;co&quot;&gt;// 앱이 정상 동작 중 → UP&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-3&quot;&gt;&lt;a href=&quot;#cb7-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    BROKEN     &lt;span class=&quot;co&quot;&gt;// 내부 오류 → DOWN&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-4&quot;&gt;&lt;a href=&quot;#cb7-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-5&quot;&gt;&lt;a href=&quot;#cb7-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-6&quot;&gt;&lt;a href=&quot;#cb7-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;enum&lt;/span&gt; ReadinessState &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-7&quot;&gt;&lt;a href=&quot;#cb7-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    ACCEPTING_TRAFFIC&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;   &lt;span class=&quot;co&quot;&gt;// 트래픽 받을 준비 완료&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-8&quot;&gt;&lt;a href=&quot;#cb7-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    REFUSING_TRAFFIC     &lt;span class=&quot;co&quot;&gt;// 아직/이제 안 받음&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-9&quot;&gt;&lt;a href=&quot;#cb7-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;LivenessState.BROKEN&lt;/code&gt;은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;앱 재시작이 아니면 회복
불가능한 상태&lt;/strong&gt;를 의미한다. DB 커넥션 실패 같은 일시 장애는
liveness로 간주하지 않는다. readiness는 훨씬 자주 바뀐다 — 앱이 뜨는 중,
스케일아웃·셧다운 직전, 부하 과다 등에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;REFUSING_TRAFFIC&lt;/code&gt;이
될 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;7-2-상태-변경-이벤트&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-2) 상태 변경 이벤트&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;상태는 코드에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AvailabilityChangeEvent.publish&lt;/code&gt;로
바꾼다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb8&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb8-1&quot;&gt;&lt;a href=&quot;#cb8-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Component&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-2&quot;&gt;&lt;a href=&quot;#cb8-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; GracefulShutdownListener &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-3&quot;&gt;&lt;a href=&quot;#cb8-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-4&quot;&gt;&lt;a href=&quot;#cb8-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@EventListener&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ContextClosedEvent&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-5&quot;&gt;&lt;a href=&quot;#cb8-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;onClose&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ContextClosedEvent event&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-6&quot;&gt;&lt;a href=&quot;#cb8-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        AvailabilityChangeEvent&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-7&quot;&gt;&lt;a href=&quot;#cb8-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            event&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getApplicationContext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-8&quot;&gt;&lt;a href=&quot;#cb8-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            ReadinessState&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;REFUSING_TRAFFIC&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-9&quot;&gt;&lt;a href=&quot;#cb8-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-10&quot;&gt;&lt;a href=&quot;#cb8-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-11&quot;&gt;&lt;a href=&quot;#cb8-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 패턴은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;앱이 종료되기 전에 readiness를 먼저 내리는&lt;/strong&gt;
graceful shutdown의 핵심이다. LB가 readiness probe 실패를 보고 Pod를
제외한 다음에야 실제 종료가 진행되도록 순서를 만들 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;7-3-쿠버네티스-probe-설정-예시&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-3) 쿠버네티스 probe 설정
예시&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb9&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb9-1&quot;&gt;&lt;a href=&quot;#cb9-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;livenessProbe&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-2&quot;&gt;&lt;a href=&quot;#cb9-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;httpGet&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-3&quot;&gt;&lt;a href=&quot;#cb9-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; /actuator/health/liveness&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-4&quot;&gt;&lt;a href=&quot;#cb9-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;9090&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;        # management.server.port&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-5&quot;&gt;&lt;a href=&quot;#cb9-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;periodSeconds&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;10&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-6&quot;&gt;&lt;a href=&quot;#cb9-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;readinessProbe&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-7&quot;&gt;&lt;a href=&quot;#cb9-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;httpGet&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-8&quot;&gt;&lt;a href=&quot;#cb9-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; /actuator/health/readiness&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-9&quot;&gt;&lt;a href=&quot;#cb9-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;9090&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-10&quot;&gt;&lt;a href=&quot;#cb9-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;periodSeconds&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;management 포트를 분리해 두면 이 probe 트래픽은 내부망으로만 간다.
애플리케이션 포트에 probe를 걸면 메인 스레드 풀과 경쟁하게 되어 부하
상황에서 probe가 먼저 실패하는 사고가 난다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-info-contributor와-build-infoproperties&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8) Info Contributor와
build-info.properties&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/info&lt;/code&gt;는 기본적으로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;비어 있다&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;{}&lt;/code&gt;가 반환된다. 내용을 채우려면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;InfoContributor&lt;/code&gt;가 필요하다. Boot는 몇 가지 기본
Contributor를 제공한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Contributor&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;활성 조건&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;출처&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;EnvironmentInfoContributor&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.info.env.enabled=true&lt;/code&gt; (3.x 기본 false)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;info.*&lt;/code&gt; 프로퍼티&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GitInfoContributor&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;git.properties&lt;/code&gt; 존재&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Git 메타데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BuildInfoContributor&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;META-INF/build-info.properties&lt;/code&gt; 존재&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;빌드 메타데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JavaInfoContributor&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.info.java.enabled=true&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;JVM 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OsInfoContributor&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.info.os.enabled=true&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;OS 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;8-1-build-infoproperties-생성&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-1) build-info.properties
생성&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-maven-plugin&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;build-info&lt;/code&gt;
goal이 빌드 시점에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;META-INF/build-info.properties&lt;/code&gt;를
만든다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb10&quot;&gt;&lt;pre class=&quot;sourceCode xml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode xml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb10-1&quot;&gt;&lt;a href=&quot;#cb10-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&amp;lt;&lt;span class=&quot;kw&quot;&gt;plugin&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-2&quot;&gt;&lt;a href=&quot;#cb10-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &amp;lt;&lt;span class=&quot;kw&quot;&gt;groupId&lt;/span&gt;&amp;gt;org.springframework.boot&amp;lt;/&lt;span class=&quot;kw&quot;&gt;groupId&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-3&quot;&gt;&lt;a href=&quot;#cb10-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &amp;lt;&lt;span class=&quot;kw&quot;&gt;artifactId&lt;/span&gt;&amp;gt;spring-boot-maven-plugin&amp;lt;/&lt;span class=&quot;kw&quot;&gt;artifactId&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-4&quot;&gt;&lt;a href=&quot;#cb10-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &amp;lt;&lt;span class=&quot;kw&quot;&gt;executions&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-5&quot;&gt;&lt;a href=&quot;#cb10-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &amp;lt;&lt;span class=&quot;kw&quot;&gt;execution&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-6&quot;&gt;&lt;a href=&quot;#cb10-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &amp;lt;&lt;span class=&quot;kw&quot;&gt;goals&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-7&quot;&gt;&lt;a href=&quot;#cb10-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &amp;lt;&lt;span class=&quot;kw&quot;&gt;goal&lt;/span&gt;&amp;gt;build-info&amp;lt;/&lt;span class=&quot;kw&quot;&gt;goal&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-8&quot;&gt;&lt;a href=&quot;#cb10-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &amp;lt;/&lt;span class=&quot;kw&quot;&gt;goals&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-9&quot;&gt;&lt;a href=&quot;#cb10-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &amp;lt;/&lt;span class=&quot;kw&quot;&gt;execution&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-10&quot;&gt;&lt;a href=&quot;#cb10-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &amp;lt;/&lt;span class=&quot;kw&quot;&gt;executions&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb10-11&quot;&gt;&lt;a href=&quot;#cb10-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&amp;lt;/&lt;span class=&quot;kw&quot;&gt;plugin&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Gradle에서는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;springBoot { buildInfo() }&lt;/code&gt;가 같은 역할을
한다. 이 파일이 생기면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/info&lt;/code&gt;에서 빌드
시각·버전·아티팩트 id를 바로 볼 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;8-2-커스텀-infocontributor&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-2) 커스텀 InfoContributor&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb11&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb11-1&quot;&gt;&lt;a href=&quot;#cb11-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Component&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-2&quot;&gt;&lt;a href=&quot;#cb11-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; RevisionInfoContributor &lt;span class=&quot;kw&quot;&gt;implements&lt;/span&gt; InfoContributor &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-3&quot;&gt;&lt;a href=&quot;#cb11-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-4&quot;&gt;&lt;a href=&quot;#cb11-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-5&quot;&gt;&lt;a href=&quot;#cb11-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;contribute&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;Info&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;Builder&lt;/span&gt; builder&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-6&quot;&gt;&lt;a href=&quot;#cb11-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        builder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDetail&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;revision&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-7&quot;&gt;&lt;a href=&quot;#cb11-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;st&quot;&gt;&amp;quot;commit&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getenv&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;GIT_COMMIT&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-8&quot;&gt;&lt;a href=&quot;#cb11-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;st&quot;&gt;&amp;quot;branch&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getenv&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;GIT_BRANCH&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-9&quot;&gt;&lt;a href=&quot;#cb11-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-10&quot;&gt;&lt;a href=&quot;#cb11-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-11&quot;&gt;&lt;a href=&quot;#cb11-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/info&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;revision&lt;/code&gt; 키 아래에 원하는
구조를 주입할 수 있다. 배포 파이프라인이 주입하는 환경 변수를 그대로
노출할 때 편하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;9-micrometer-meterregistry-countertimergaugetag&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;9) Micrometer
MeterRegistry: Counter·Timer·Gauge·Tag&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Actuator의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;metrics&lt;/code&gt; 엔드포인트는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Micrometer&lt;/strong&gt;가 관리하는 메트릭을 그대로 꺼내 보여준다.
Boot 2.x 이후 메트릭 API는 전부 Micrometer로 통일됐고, Actuator는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MeterRegistry&lt;/code&gt;를 DI 받아 쓰는 얇은 인터페이스만
제공한다.&lt;/p&gt;
&lt;h3 id=&quot;9-1-meter-타입&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-1) Meter 타입&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;타입&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;용도&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;예시&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Counter&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;누적 단조 증가&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;요청 수, 에러 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Timer&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;지연 시간 + 호출 수&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HTTP 요청 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Gauge&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;현재 값 스냅샷&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;큐 크기, 커넥션 풀 사용량&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DistributionSummary&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;크기 분포&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;응답 페이로드 바이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;LongTaskTimer&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;장기 작업의 진행 중 시간&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;배치 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;9-2-기본-등록-메트릭&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-2) 기본 등록 메트릭&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;의존성 추가만으로 다음이 자동으로 수집된다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;JVM&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jvm.memory.*&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jvm.gc.*&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jvm.threads.*&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jvm.classes.*&lt;/code&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;시스템&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;system.cpu.usage&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;process.cpu.usage&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;process.files.open&lt;/code&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;HTTP 서버 요청&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;http.server.requests&lt;/code&gt;
(uri·method·status·outcome 태그 포함)&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Tomcat/Jetty&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;tomcat.sessions.*&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;tomcat.threads.*&lt;/code&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Logback&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;logback.events&lt;/code&gt; (level
태그)&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;DataSource&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hikaricp.connections.*&lt;/code&gt;
(HikariCP 사용 시)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;http.server.requests&lt;/code&gt;가 가장 자주 쓰인다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/metrics/http.server.requests?tag=uri:/api/users&amp;amp;tag=status:200&lt;/code&gt;처럼
태그로 필터링해서 볼 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;9-3-커스텀-메트릭과-공통-태그&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-3) 커스텀 메트릭과 공통
태그&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb12&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb12-1&quot;&gt;&lt;a href=&quot;#cb12-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Service&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-2&quot;&gt;&lt;a href=&quot;#cb12-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; OrderService &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-3&quot;&gt;&lt;a href=&quot;#cb12-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-4&quot;&gt;&lt;a href=&quot;#cb12-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;final&lt;/span&gt; Counter orderCounter&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-5&quot;&gt;&lt;a href=&quot;#cb12-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Timer&lt;/span&gt; processTimer&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-6&quot;&gt;&lt;a href=&quot;#cb12-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-7&quot;&gt;&lt;a href=&quot;#cb12-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;OrderService&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;MeterRegistry registry&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-8&quot;&gt;&lt;a href=&quot;#cb12-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;kw&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;orderCounter&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; Counter&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;orders.created&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-9&quot;&gt;&lt;a href=&quot;#cb12-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;생성된 주문 수&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-10&quot;&gt;&lt;a href=&quot;#cb12-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;region&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;kr&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-11&quot;&gt;&lt;a href=&quot;#cb12-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;registry&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-12&quot;&gt;&lt;a href=&quot;#cb12-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;kw&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;processTimer&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; registry&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;timer&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;orders.process&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;sync&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-13&quot;&gt;&lt;a href=&quot;#cb12-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-14&quot;&gt;&lt;a href=&quot;#cb12-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-15&quot;&gt;&lt;a href=&quot;#cb12-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;Order o&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-16&quot;&gt;&lt;a href=&quot;#cb12-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        processTimer&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-17&quot;&gt;&lt;a href=&quot;#cb12-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;co&quot;&gt;// ... 비즈니스 로직&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-18&quot;&gt;&lt;a href=&quot;#cb12-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            orderCounter&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-19&quot;&gt;&lt;a href=&quot;#cb12-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-20&quot;&gt;&lt;a href=&quot;#cb12-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-21&quot;&gt;&lt;a href=&quot;#cb12-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;모든 메트릭에 공통 태그를 주입하고 싶으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MeterRegistryCustomizer&lt;/code&gt;를 쓴다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb13&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb13-1&quot;&gt;&lt;a href=&quot;#cb13-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-2&quot;&gt;&lt;a href=&quot;#cb13-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;MeterRegistryCustomizer&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;MeterRegistry&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;commonTags&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-3&quot;&gt;&lt;a href=&quot;#cb13-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; registry &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; registry&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;commonTags&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-4&quot;&gt;&lt;a href=&quot;#cb13-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;st&quot;&gt;&amp;quot;application&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;order-service&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-5&quot;&gt;&lt;a href=&quot;#cb13-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;st&quot;&gt;&amp;quot;env&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;prod&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-6&quot;&gt;&lt;a href=&quot;#cb13-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-7&quot;&gt;&lt;a href=&quot;#cb13-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이렇게 등록한 태그는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;모든 메트릭에 자동으로 붙는다&lt;/strong&gt;.
Prometheus에서 여러 서비스를 구분하거나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env=prod&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env=dev&lt;/code&gt;를 나눠 볼 때 필수다.&lt;/p&gt;
&lt;h3 id=&quot;9-4-timed--counted--observed&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-4) @Timed / @Counted /
@Observed&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;AOP 기반 어노테이션도 지원한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb14&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb14-1&quot;&gt;&lt;a href=&quot;#cb14-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Timed&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;value &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;order.create.time&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; histogram &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-2&quot;&gt;&lt;a href=&quot;#cb14-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; Order &lt;span class=&quot;fu&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;Command cmd&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Timed&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TimedAspect&lt;/code&gt; 빈이 등록돼 있어야
동작한다. Boot 자동구성이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-aop&lt;/code&gt;가 클래스패스에 있으면
기본 등록한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Observed&lt;/code&gt;(Micrometer 1.10+)는 Timer와
Trace를 동시에 발행해 Micrometer Tracing으로 전파된다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;10-prometheus-노출&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;10) Prometheus 노출&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/prometheus&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Prometheus scrape
포맷&lt;/strong&gt;으로 메트릭을 뱉어주는 엔드포인트다. 핵심은 이 엔드포인트가
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;의존성에 의해 활성화된다&lt;/strong&gt;는 점이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb15&quot;&gt;&lt;pre class=&quot;sourceCode xml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode xml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb15-1&quot;&gt;&lt;a href=&quot;#cb15-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&amp;lt;&lt;span class=&quot;kw&quot;&gt;dependency&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb15-2&quot;&gt;&lt;a href=&quot;#cb15-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &amp;lt;&lt;span class=&quot;kw&quot;&gt;groupId&lt;/span&gt;&amp;gt;io.micrometer&amp;lt;/&lt;span class=&quot;kw&quot;&gt;groupId&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb15-3&quot;&gt;&lt;a href=&quot;#cb15-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &amp;lt;&lt;span class=&quot;kw&quot;&gt;artifactId&lt;/span&gt;&amp;gt;micrometer-registry-prometheus&amp;lt;/&lt;span class=&quot;kw&quot;&gt;artifactId&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span id=&quot;cb15-4&quot;&gt;&lt;a href=&quot;#cb15-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&amp;lt;/&lt;span class=&quot;kw&quot;&gt;dependency&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 의존성이 있으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PrometheusMetricsExportAutoConfiguration&lt;/code&gt;이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PrometheusMeterRegistry&lt;/code&gt;를 등록하고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/prometheus&lt;/code&gt; 엔드포인트가 같이 켜진다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;없으면 엔드포인트 자체가 등록되지 않는다&lt;/strong&gt; — 노출 설정만
해서는 404다. 여기서 틀리기 쉽다.&lt;/p&gt;
&lt;h3 id=&quot;10-1-prometheus-scrape-설정&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-1) Prometheus scrape 설정&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb16&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb16-1&quot;&gt;&lt;a href=&quot;#cb16-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# prometheus.yml&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-2&quot;&gt;&lt;a href=&quot;#cb16-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;scrape_configs&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-3&quot;&gt;&lt;a href=&quot;#cb16-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;job_name&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; order-service&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-4&quot;&gt;&lt;a href=&quot;#cb16-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;metrics_path&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; /actuator/prometheus&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-5&quot;&gt;&lt;a href=&quot;#cb16-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;scrape_interval&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; 15s&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-6&quot;&gt;&lt;a href=&quot;#cb16-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;static_configs&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-7&quot;&gt;&lt;a href=&quot;#cb16-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;targets&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;#39;order-service:9090&amp;#39;&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;scrape_interval&lt;/code&gt;은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;너무 짧게 잡지
않는다&lt;/strong&gt;. 5초 이하로 내리면 매 scrape마다 histogram 계산·라벨
직렬화에 CPU와 GC 부담이 실제로 관찰된다. 15~30초가 기본 권장이다.&lt;/p&gt;
&lt;h3 id=&quot;10-2-히스토그램과-백분위&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-2) 히스토그램과 백분위&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Timer&lt;/code&gt;에서 p95, p99 같은 백분위를 보고 싶으면 두 가지
선택지가 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb17&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb17-1&quot;&gt;&lt;a href=&quot;#cb17-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;registry&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;timer&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;orders.process&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-2&quot;&gt;&lt;a href=&quot;#cb17-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    Tags&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;sync&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-3&quot;&gt;&lt;a href=&quot;#cb17-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 또는 builder로&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-4&quot;&gt;&lt;a href=&quot;#cb17-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-5&quot;&gt;&lt;a href=&quot;#cb17-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;Timer&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;orders.process&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-6&quot;&gt;&lt;a href=&quot;#cb17-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;publishPercentileHistogram&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;   &lt;span class=&quot;co&quot;&gt;// Prometheus에서 histogram_quantile 사용&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-7&quot;&gt;&lt;a href=&quot;#cb17-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;registry&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;publishPercentiles(0.95, 0.99)&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;앱 JVM이
백분위를 계산해서 내보낸다&lt;/strong&gt;. 반면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;publishPercentileHistogram()&lt;/code&gt;은 히스토그램 버킷만 내보내고
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Prometheus 쪽에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;histogram_quantile&lt;/code&gt;로
계산&lt;/strong&gt;한다. 후자가 여러 인스턴스 합산 시 더 정확하다 — 인스턴스별
백분위는 평균을 낼 수 없기 때문이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;11-spring-security-통합과-포트-분리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;11) Spring Security 통합과
포트 분리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Actuator 엔드포인트는 Spring Security가 없으면 그냥 열려 있다.
프로덕션에서는 최소한 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;info&lt;/code&gt;를 제외한
나머지를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;인증 뒤로 숨기는 게&lt;/strong&gt; 기본이다.&lt;/p&gt;
&lt;h3 id=&quot;11-1-endpointrequest-매처&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;11-1) EndpointRequest 매처&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Security의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;EndpointRequest&lt;/code&gt;는 Actuator
엔드포인트에 특화된 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RequestMatcher&lt;/code&gt;를 제공한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb18&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb18-1&quot;&gt;&lt;a href=&quot;#cb18-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-2&quot;&gt;&lt;a href=&quot;#cb18-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;SecurityFilterChain &lt;span class=&quot;fu&quot;&gt;actuator&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;HttpSecurity http&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-3&quot;&gt;&lt;a href=&quot;#cb18-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    http&lt;/span&gt;
&lt;span id=&quot;cb18-4&quot;&gt;&lt;a href=&quot;#cb18-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;securityMatcher&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;EndpointRequest&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;toAnyEndpoint&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-5&quot;&gt;&lt;a href=&quot;#cb18-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;auth &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; auth&lt;/span&gt;
&lt;span id=&quot;cb18-6&quot;&gt;&lt;a href=&quot;#cb18-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;EndpointRequest&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;health&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;info&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;permitAll&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-7&quot;&gt;&lt;a href=&quot;#cb18-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;requestMatchers&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;EndpointRequest&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;prometheus&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;MONITORING&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-8&quot;&gt;&lt;a href=&quot;#cb18-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;hasRole&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-9&quot;&gt;&lt;a href=&quot;#cb18-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-10&quot;&gt;&lt;a href=&quot;#cb18-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;httpBasic&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-11&quot;&gt;&lt;a href=&quot;#cb18-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;csrf&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;csrf &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; csrf&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;disable&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-12&quot;&gt;&lt;a href=&quot;#cb18-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; http&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-13&quot;&gt;&lt;a href=&quot;#cb18-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;EndpointRequest.toAnyEndpoint()&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;현재 등록된
모든 엔드포인트&lt;/strong&gt;를 매치한다. 경로
하드코딩(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/**&lt;/code&gt;)보다 안전하다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.endpoints.web.base-path&lt;/code&gt;를 바꿔도 자동으로
따라간다.&lt;/p&gt;
&lt;h3 id=&quot;11-2-포트-분리--방화벽&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;11-2) 포트 분리 + 방화벽&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb19&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb19-1&quot;&gt;&lt;a href=&quot;#cb19-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;management&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-2&quot;&gt;&lt;a href=&quot;#cb19-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-3&quot;&gt;&lt;a href=&quot;#cb19-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;9090&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-4&quot;&gt;&lt;a href=&quot;#cb19-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;fl&quot;&gt;127.0.0.1&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;   # 외부에서 접근 불가&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.server.address=127.0.0.1&lt;/code&gt;을 주면 9090은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;로컬 루프백으로만 바인딩&lt;/strong&gt;된다. 쿠버네티스에서는 이
설정을 쓰지 않고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;0.0.0.0&lt;/code&gt;으로 두되, NetworkPolicy로 scrape
소스를 제한하는 쪽이 실용적이다. 어느 쪽이든 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Actuator 포트를 LB
뒤로 외부 공개하지 않는 것&lt;/strong&gt;이 원칙이다.&lt;/p&gt;
&lt;h3 id=&quot;11-3-와일드카드-노출의-위험&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;11-3) 와일드카드 노출의 위험&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb20&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb20-1&quot;&gt;&lt;a href=&quot;#cb20-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;management&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-2&quot;&gt;&lt;a href=&quot;#cb20-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;endpoints&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-3&quot;&gt;&lt;a href=&quot;#cb20-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-4&quot;&gt;&lt;a href=&quot;#cb20-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;exposure&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-5&quot;&gt;&lt;a href=&quot;#cb20-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;*&amp;quot;&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;   # 피하기&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;*&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;현재뿐 아니라 미래에 Boot가 추가할
엔드포인트까지 전부 연다&lt;/strong&gt;. 운영에서 이 설정 + permitAll 조합이
나오면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;beans&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configprops&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;threaddump&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;heapdump&lt;/code&gt;가 전부 외부로 노출된다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env&lt;/code&gt;는 특히 위험하다 — 바인딩된 프로퍼티 원본이 그대로
JSON으로 나간다. DB 비밀번호가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.datasource.password&lt;/code&gt;에
바인딩돼 있으면 마스킹이 걸려도 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;키 이름만으로도 스키마가
드러난다&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;선호되는 방식은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;명시 화이트리스트&lt;/strong&gt;다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb21&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb21-1&quot;&gt;&lt;a href=&quot;#cb21-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;management&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-2&quot;&gt;&lt;a href=&quot;#cb21-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;endpoints&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-3&quot;&gt;&lt;a href=&quot;#cb21-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-4&quot;&gt;&lt;a href=&quot;#cb21-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;exposure&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-5&quot;&gt;&lt;a href=&quot;#cb21-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; health,info,metrics,prometheus,loggers&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;12-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;12) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;운영 기본 조합&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;include: health,info,metrics,prometheus&lt;/code&gt;. 여기서 늘리고
싶으면 하나씩 추가. 와일드카드는 쓰지 않는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;포트 분리&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.server.port=9090&lt;/code&gt;. LB는 8080만 외부 공개, 9090은
내부망 전용으로 scrape·probe만 받는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Health 그룹 분리&lt;/strong&gt;: liveness는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;livenessState&lt;/code&gt;만, readiness는 외부 의존성 포함. 느린
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HealthIndicator&lt;/code&gt; 때문에 probe가 꺼지는 사고를 막는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Prometheus 의존성 확인&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/prometheus&lt;/code&gt;가 404면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;micrometer-registry-prometheus&lt;/code&gt;가 빠진 것. 노출 설정만
고쳐선 안 된다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;build-info 활성화&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-maven-plugin&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;build-info&lt;/code&gt; goal을
켜서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/info&lt;/code&gt;에 빌드 메타데이터를 노출. 배포 시 어떤
커밋이 올라가 있는지 확인이 빠르다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;공통 태그&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MeterRegistryCustomizer&lt;/code&gt;로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;region&lt;/code&gt; 같은
태그를 common으로 박아둔다. Prometheus/Grafana에서 여러 서비스 구분이 이
위에서 돈다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;shutdown&lt;/code&gt; 엔드포인트&lt;/strong&gt;: 기본 비활성인
채로 두는 게 맞다. 롤링 업데이트는 쿠버네티스가 SIGTERM으로 처리한다.
Actuator로 종료 트리거를 외부에 열지 않는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;env&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configprops&lt;/code&gt;는
닫아둔다&lt;/strong&gt;: 디버깅이 필요하면 ROLE_ADMIN으로 잠그거나, VPN
뒤에서만 접근 가능한 포트로 분리. 기본 노출 리스트에 넣지 않는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/metrics/http.server.requests&lt;/code&gt;로 즉석
확인&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;?tag=uri:/api/xxx&amp;amp;tag=status:500&lt;/code&gt;으로
특정 엔드포인트의 에러율을 빠르게 볼 수 있다. Grafana 없이도 현장에서
확인 가능하다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;커스텀 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HealthIndicator&lt;/code&gt;에 타임아웃&lt;/strong&gt;:
외부 호출이 들어간다면 반드시 클라이언트 레벨 타임아웃(예:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RestClient&lt;/code&gt; 타임아웃 2~3초)을 걸어둔다. 헬스 응답이 전체
probe를 붙잡는 사고를 막는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;13-한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;13) 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Actuator는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Endpoint&lt;/code&gt; SPI 위에 내장 엔드포인트와
커스텀 엔드포인트가 동일한 방식으로 쌓이는 운영 창구다&lt;/strong&gt;. 기본은
web에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;health&lt;/code&gt;만 노출되고, 나머지는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.endpoints.web.exposure.include&lt;/code&gt;로 명시해야 한다는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;엄격한 기본값&lt;/strong&gt;이 의도적으로 깔려 있다. 운영에서는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;와일드카드 노출을 금지하고, Actuator 포트를 분리하고, Health
표시기의 외부 호출 타임아웃을 걸어두는 세 가지&lt;/strong&gt;가 사고를 막는
최소 방어선이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;태그&lt;/strong&gt;: Spring Boot, Actuator, Endpoint SPI,
HealthIndicator, Micrometer, Prometheus, Kubernetes Probes,
MeterRegistry, Observability, Spring Security&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/Spring</category>
      <category>actuator</category>
      <category>Endpoint SPI</category>
      <category>HealthIndicator</category>
      <category>Kubernetes Probes</category>
      <category>MeterRegistry</category>
      <category>Micrometer</category>
      <category>Observability</category>
      <category>Prometheus</category>
      <category>spring boot</category>
      <category>Spring Security</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/407</guid>
      <comments>https://dding-shark.tistory.com/407#entry407comment</comments>
      <pubDate>Tue, 14 Apr 2026 22:06:20 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot Externalized Configuration &amp;mdash; @ConfigurationProperties 바인딩과 Profile&amp;middot;spring.config.import</title>
      <link>https://dding-shark.tistory.com/406</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;spring-boot-externalized-configuration--configurationproperties-바인딩과-profilespringconfigimport&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;Spring
Boot Externalized Configuration — @ConfigurationProperties 바인딩과
Profile·spring.config.import&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;9편에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Environment&lt;/code&gt;가 어떻게 조립되는가 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PropertySource&lt;/code&gt; 평면 리스트에 누가 먼저 꽂히느냐로
우선순위가 결정된다는 이야기를 했다. 이번 편은 그 이야기의 자매편이다.
9편이 &amp;quot;조립의 원리&amp;quot;였다면, 이번은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Spring Boot가 실무에서 실제로
제공하는 설정 주입 수단 전부를 기능별로 펼쳐 보는&lt;/strong&gt; 쪽이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application-{profile}.yml&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import&lt;/code&gt;, 생성자 바인딩, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration&lt;/code&gt;
단위 파싱, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Validated&lt;/code&gt; 기동 검증 — 이름은 다들 들어봤지만 한
번에 정리해둔 지도가 없어 매번 각 기능의 문서를 따로 찾아다니게
된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;특히 Spring Boot 2.4에서 설정 로딩 구조가 크게 바뀌었다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Config Data API&lt;/strong&gt;라는 새 추상이 도입되면서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles.include&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.additional-location&lt;/code&gt;의 상당 부분이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import&lt;/code&gt;로 흡수됐다. 2.4 이전에 익숙했던 문법
일부가 3.x에서는 쓰지 말아야 할 것으로 바뀌었고, 그 차이를 모르고 쓰면
&amp;quot;예전엔 되던 게 왜 안 되지?&amp;quot;가 된다. 이 글은 Spring Boot 3.x / Spring
Framework 6.x 기준으로 정리한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 주제에서 특히 막히기 쉬운 지점이 있다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;APP_MAIL_HOST&lt;/code&gt; 환경변수를 세팅했는데
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Value(&amp;quot;${app.mail.host}&amp;quot;)&lt;/code&gt;가 못 찾는다&lt;/strong&gt; — Relaxed
Binding은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt; 쪽이 더 관대하다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;yml에 프로파일별 블록을 쓰는 예전
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles: prod&lt;/code&gt; 문법이 안 먹는다&lt;/strong&gt; — 2.4+는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.activate.on-profile&lt;/code&gt;로 바뀌었다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import&lt;/code&gt;를 썼는데 파일이 없어서
기동 실패한다&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;optional:&lt;/code&gt; 접두사를 빼먹은
것이다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration timeout = 30s&lt;/code&gt;를 쓰고 싶은데 바인딩이
깨진다&lt;/strong&gt; — 필드 타입이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;long&lt;/code&gt;이면 단위 표기를 못
받는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;우선순위 → 파일 전략 → 바인딩 → 검증 → 디버깅 →
실무&lt;/strong&gt; 순으로 정리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-externalized-configuration의-큰-그림&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1) Externalized
Configuration의 큰 그림&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-propertysource-우선순위-14계층&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2) PropertySource
우선순위 14계층&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-applicationyml-위치프로파일-우선순위&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3) application.yml
위치·프로파일 우선순위&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-springconfigimport-24의-새-문법&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4)
spring.config.import: 2.4+의 새 문법&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-profile-전략-active--include--group--default&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5) Profile
전략: active / include / group / default&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-configurationproperties-vs-value&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6)
@ConfigurationProperties vs @Value&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-생성자-바인딩과-relaxed-binding&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7) 생성자 바인딩과
Relaxed Binding&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-duration--datasize--period-컨버터&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8) Duration /
DataSize / Period 컨버터&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#9-validated로-기동-시점-검증&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;9) @Validated로 기동 시점
검증&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#10-origin-추적과-actuatorenv-디버깅&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;10) Origin 추적과
/actuator/env 디버깅&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#11-시크릿-configtree와-vault&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;11) 시크릿: configtree와
Vault&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#12-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;12) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#13-한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;13) 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-externalized-configuration의-큰-그림&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1) Externalized
Configuration의 큰 그림&lt;/h2&gt;
&lt;h3 id=&quot;1-1-세-축-소스--바인딩--검증&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-1) 세 축: 소스 / 바인딩 /
검증&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Boot의 설정 시스템은 세 축으로 분해된다. 독립적으로 이해하고
조합할 줄 알아야 전체가 보인다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;축&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;역할&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;대표 기능&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;소스(Source)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;어디서 값이 오는가&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;, 환경변수, CLI 인자, Vault,
ConfigMap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;바인딩(Binding)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;값을 어떻게 객체로 묶는가&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Value&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt;, 생성자
바인딩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;검증(Validation)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;잘못된 값을 언제 실패시키는가&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Validated&lt;/code&gt; + JSR-380, 기동 시점 실패&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;9편에서 본 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PropertySource&lt;/code&gt; 우선순위는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;소스
축&lt;/strong&gt; 이야기였다. 이번 글은 세 축을 모두 걸쳐서 다룬다 —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import&lt;/code&gt;가 소스 축,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt;가 바인딩 축,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Validated&lt;/code&gt;가 검증 축이다.&lt;/p&gt;
&lt;h3 id=&quot;1-2-외부화의-의미&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-2) &amp;quot;외부화&amp;quot;의 의미&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;Externalized&amp;quot;라는 단어가 모호해서 혼동을 부른다. 엄밀히 말하면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;빌드 산출물(jar) 안에 고정되지 않은 모든 설정&lt;/strong&gt;이 외부화
대상이다. 같은 jar를 dev / stage / prod에 그대로 배포하되 값만 바꾸는 게
목표다. 그래서 다음 세 가지가 모두 &amp;quot;외부&amp;quot; 소스다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;빌드 시점에는 알지 못하는 값&lt;/strong&gt; — DB 호스트, API 키,
외부 URL&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;환경마다 달라지는 값&lt;/strong&gt; — 로그 레벨, 커넥션 풀 크기,
기능 플래그&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;비밀 값&lt;/strong&gt; — 토큰, 비밀번호, 서명 키&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;jar 안의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;에 디폴트를 넣고, jar 밖의
프로파일/환경변수/Vault로 오버라이드하는 구조가 기본이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-propertysource-우선순위-14계층&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2) PropertySource 우선순위
14계층&lt;/h2&gt;
&lt;h3 id=&quot;2-1-boot-3x-기준-공식-순서&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-1) Boot 3.x 기준 공식 순서&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Boot가 기동 시 꽂는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PropertySource&lt;/code&gt;의 공식 순서다
(&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;위쪽이 우선&lt;/strong&gt;). 9편에서 다룬 15단계 중 Boot 3.x에서
실질적으로 관찰되는 14개를 공식 순서대로 정리한 판이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;#&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;소스&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;예시&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;DevTools 글로벌 설정&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;~/.config/spring-boot/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@TestPropertySource&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;테스트 전용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootTest(properties=...)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;테스트 전용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CLI 인자&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--app.mail.host=smtp.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SPRING_APPLICATION_JSON&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SPRING_APPLICATION_JSON=&amp;#39;{&amp;quot;app&amp;quot;:{&amp;quot;mail&amp;quot;:{&amp;quot;host&amp;quot;:&amp;quot;x&amp;quot;}}}&amp;#39;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;ServletConfig / ServletContext init 파라미터&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;web.xml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;JNDI&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;java:comp/env&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Java 시스템 프로퍼티&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;-Dapp.mail.host=x&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;OS 환경변수&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;APP_MAIL_HOST=smtp.example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application-{profile}.yml&lt;/code&gt; /
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.properties&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;jar 밖 → jar 안&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;11&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt; /
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.properties&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;jar 밖 → jar 안&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;12&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PropertySource&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PropertySource(&amp;quot;classpath:custom.properties&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;13&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SpringApplication.setDefaultProperties&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;코드에서 주입한 디폴트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;14&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;기본값 (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Value(&amp;quot;${key:default}&amp;quot;)&lt;/code&gt;)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;최종 디폴트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;요점은 이 흐름이다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;테스트 &amp;gt; CLI &amp;gt;
SPRING_APPLICATION_JSON &amp;gt; Servlet init &amp;gt; JNDI &amp;gt; 시스템 프로퍼티
&amp;gt; 환경변수 &amp;gt; profile yml &amp;gt; 공통 yml &amp;gt; @PropertySource &amp;gt;
setDefaultProperties &amp;gt; @Value 기본값&lt;/strong&gt;. 각 지점에서 앞선
레벨이 값을 갖고 있으면 뒤는 보지도 않는다.&lt;/p&gt;
&lt;h3 id=&quot;2-2-함정-cli-vs-환경변수&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-2) 함정: CLI vs 환경변수&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;흔한 오해 한 가지 — &amp;quot;환경변수가 CLI보다 세다&amp;quot;는 감각은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;반대&lt;/strong&gt;다. Boot 3 공식 순서에서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CLI 인자가 OS
환경변수보다 높다&lt;/strong&gt;. 같은 키가 CLI와 환경변수 양쪽에 있으면 CLI가
이긴다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이게 실전에서 가장 흔한 설정 사고를 만든다. 운영자가 환경변수로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;APP_MAIL_HOST&lt;/code&gt;를 오버라이드한다고 믿고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;kubectl set env&lt;/code&gt;를 때렸는데, 실행 스크립트의 엔트리포인트가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;java -jar app.jar --app.mail.host=old-host&lt;/code&gt;를 그대로 들고
있어 환경변수가 완전히 무시된다. CLI는 yml보다도 높고 환경변수보다도
높아서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한 번 박히면 운영 중에 덮을 길이 거의 없다&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실무 기본선: &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CLI는 임시 디버깅/일회성용, 영구 오버라이드는
환경변수 또는 프로파일 yml&lt;/strong&gt;로 분리한다. 엔트리포인트 스크립트와
Dockerfile의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CMD&lt;/code&gt;에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--key=v&lt;/code&gt;를 박아놓지 말 것.
운영 중 오버라이드 경로를 스스로 막는 짓이다.&lt;/p&gt;
&lt;h3 id=&quot;2-3-serverportport8080-관용구&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-3)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;server.port=${PORT:8080}&lt;/code&gt; 관용구&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;운영 환경에서 자주 보이는 한 줄이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; ${PORT:8080}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;환경변수 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PORT&lt;/code&gt;가 있으면 그 값, 없으면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;8080&lt;/code&gt;.
Heroku·Cloud Foundry·Kubernetes에서 포트를 동적으로 주입하는 패턴과
궁합이 맞다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;${KEY:default}&lt;/code&gt; 문법은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Value&lt;/code&gt;와
yml 양쪽 모두에서 동작한다 — yml 안에서 placeholder 문법을 쓸 수 있다는
점이 처음엔 어색하지만, Boot가 yml 로드 시점에 PropertyResolver를 돌리기
때문에 가능하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-applicationyml-위치프로파일-우선순위&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3) application.yml
위치·프로파일 우선순위&lt;/h2&gt;
&lt;h3 id=&quot;3-1-동일-파일명-네-위치&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-1) 동일 파일명, 네 위치&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;같은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;이어도 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어디에
놓이느냐&lt;/strong&gt;에 따라 우선순위가 다르다. 위쪽이 우선.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;#&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;위치&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;용도&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;./config/&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;실행 위치 하위 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;config/&lt;/code&gt; 디렉토리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;./&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;실행 위치 (jar 옆)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;classpath &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/config/&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;jar 안의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;src/main/resources/config/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;classpath &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;jar 안의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;src/main/resources/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실무 패턴은 단순하다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;jar 안(#3, #4)에 디폴트를 두고, jar
밖(#1, #2)으로 운영자가 오버라이드&lt;/strong&gt;한다. 도커 이미지에는
디폴트만 구우면 되고, 컨테이너 런타임에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/app/config/application.yml&lt;/code&gt;을 볼륨 마운트하면 이미지 재빌드
없이 값만 바꾼다.&lt;/p&gt;
&lt;blockquote style=&quot;margin:1.2em 0;padding:0.8em 1.2em;background:#f6f8fa;border-left:4px solid #8c959f;border-radius:0 6px 6px 0;color:#59636e;&quot;&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;참고: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--spring.config.name&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--spring.config.location&lt;/code&gt;으로 파일명/탐색 경로 자체를 바꿀
수 있다. 기본 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application&lt;/code&gt;이 아닌 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;myservice&lt;/code&gt;로
파일명을 바꾸거나, 위 네 위치 대신 임의의 경로를 탐색하게 만들 수
있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;3-2-profile-specific-파일-병합&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-2) Profile-specific 파일
병합&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application-{profile}.yml&lt;/code&gt;은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;같이 로드되어
병합&lt;/strong&gt;된다. 덮어쓰기가 아니다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# application.yml (공통)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-3&quot;&gt;&lt;a href=&quot;#cb2-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-4&quot;&gt;&lt;a href=&quot;#cb2-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; localhost&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-5&quot;&gt;&lt;a href=&quot;#cb2-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;25&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-6&quot;&gt;&lt;a href=&quot;#cb2-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; 10s&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-7&quot;&gt;&lt;a href=&quot;#cb2-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-8&quot;&gt;&lt;a href=&quot;#cb2-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# application-prod.yml (prod 프로파일 활성화 시만)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-9&quot;&gt;&lt;a href=&quot;#cb2-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-10&quot;&gt;&lt;a href=&quot;#cb2-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-11&quot;&gt;&lt;a href=&quot;#cb2-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; smtp.production.example.com&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles.active=prod&lt;/code&gt;로 기동하면 최종 값은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;host=smtp.production.example.com&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;port=25&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;timeout=10s&lt;/code&gt;. 공통에서 디폴트를 잡고 프로파일에서
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;부분만 덮어쓰는&lt;/strong&gt; 구조가 권장된다. 프로파일 yml에 전체를
다시 복사하면 공통 변경이 전파되지 않는다.&lt;/p&gt;
&lt;h3 id=&quot;3-3-같은-프로파일-여러-위치&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-3) 같은 프로파일, 여러 위치&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;프로파일별 파일도 위치 우선순위가 동일하게 적용된다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;./config/application-prod.yml&lt;/code&gt;이 classpath의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application-prod.yml&lt;/code&gt;보다 우선한다. 운영 환경에서 민감
정보만 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;./config/application-prod.yml&lt;/code&gt;로 오버라이드하고,
공개해도 되는 기본 prod 설정은 jar 안에 박아두는 분리가 가능하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-springconfigimport-24의-새-문법&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4) spring.config.import:
2.4+의 새 문법&lt;/h2&gt;
&lt;h3 id=&quot;4-1-왜-생겼나&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-1) 왜 생겼나&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Boot 2.4 이전에는 여러 외부 설정 파일을 끌어올 때
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.additional-location&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles.include&lt;/code&gt;를 조합해야 했다. 로딩 순서가
모호했고, &amp;quot;프로파일 import&amp;quot;와 &amp;quot;파일 import&amp;quot;가 뒤섞여 있어 혼란이
컸다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;2.4에서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Config Data API&lt;/strong&gt;가 도입되면서 단일 엔트리
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import&lt;/code&gt;로 통합됐다. 그리고 이 핵심 메시지가
중요하다 — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import&lt;/code&gt;는 단순 파일 경로가 아니라
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;플러그인 가능한 리졸버&lt;/strong&gt;를 받는다. 파일, 디렉토리, Config
Server, Vault, 커스텀 리졸버까지 같은 문법으로 표현한다.&lt;/p&gt;
&lt;h3 id=&quot;4-2-기본-문법&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-2) 기본 문법&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-4&quot;&gt;&lt;a href=&quot;#cb3-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; optional:file:./override.yml&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-5&quot;&gt;&lt;a href=&quot;#cb3-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; optional:classpath:shared-config.yml&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-6&quot;&gt;&lt;a href=&quot;#cb3-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; optional:configtree:/etc/secrets/&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-7&quot;&gt;&lt;a href=&quot;#cb3-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; optional:configserver:http://config-server:8888&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;각 엔트리는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;[optional:]&amp;lt;scheme&amp;gt;:&amp;lt;location&amp;gt;&lt;/code&gt;
형태다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;scheme&lt;/code&gt; 종류가 의미를 결정한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;scheme&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;의미&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;file:&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;파일 시스템의 yml/properties&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;classpath:&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;classpath 리소스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configtree:&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;디렉토리 구조를 키-값으로 (K8s Secret 마운트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configserver:&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Spring Cloud Config Server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;(커스텀)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ConfigDataLocationResolver&lt;/code&gt; 구현으로 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;4-3-함정-optional-빠뜨리기&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-3) 함정: optional: 빠뜨리기&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import&lt;/code&gt;에 적힌 파일이 존재하지 않으면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기동이 실패&lt;/strong&gt;한다. 이게 설계된 동작이다 — 운영자가
&amp;quot;프로덕션 오버라이드 파일을 꼭 붙여야 한다&amp;quot;고 명시했는데 파일이 없다면
조용히 넘어가는 대신 빨리 터트리는 쪽이 안전하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;문제는 로컬 개발에서다. 로컬에는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;override.yml&lt;/code&gt;이 없는 게
정상인데 기동이 실패하면 난감하다. 해결은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;optional:&lt;/code&gt;
접두사&lt;/strong&gt;다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-2&quot;&gt;&lt;a href=&quot;#cb4-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-3&quot;&gt;&lt;a href=&quot;#cb4-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-4&quot;&gt;&lt;a href=&quot;#cb4-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;      # 피하기: 로컬에서 파일 없으면 기동 실패&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-5&quot;&gt;&lt;a href=&quot;#cb4-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; file:./prod-override.yml&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-6&quot;&gt;&lt;a href=&quot;#cb4-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-7&quot;&gt;&lt;a href=&quot;#cb4-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;      # 선호: 없으면 조용히 스킵&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-8&quot;&gt;&lt;a href=&quot;#cb4-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; optional:file:./prod-override.yml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;없어도 되는 리소스&amp;quot;에는 반드시 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;optional:&lt;/code&gt;을 붙인다.
반대로 &amp;quot;반드시 있어야 하는&amp;quot; 시크릿 파일에는 붙이지 않는다 — 없으면
기동이 터지는 게 올바르다.&lt;/p&gt;
&lt;h3 id=&quot;4-4-configtree-쿠버네티스-시크릿-마운트&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-4) configtree:
쿠버네티스 시크릿 마운트&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configtree:&lt;/code&gt;는 Kubernetes 환경에서 특히 유용하다. K8s가
Secret/ConfigMap을 파일 시스템에 마운트하면 각 키가 파일로 펼쳐진다.&lt;/p&gt;
&lt;pre style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/etc/secrets/
├── app.mail.host       → &amp;quot;smtp.example.com&amp;quot;
├── app.mail.password   → &amp;quot;s3cret&amp;quot;
└── app.mail.port       → &amp;quot;465&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import=configtree:/etc/secrets/&lt;/code&gt;을 적으면
Boot가 이 디렉토리를 스캔해서 각 파일 이름을 키로, 파일 내용을 값으로
읽어들인다. 환경변수로 시크릿을 노출하지 않아도 돼서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ps&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/proc/*/environ&lt;/code&gt; 같은 경로로 샐 위험이 줄어든다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-profile-전략-active--include--group--default&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5) Profile 전략:
active / include / group / default&lt;/h2&gt;
&lt;h3 id=&quot;5-1-네-가지-속성&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-1) 네 가지 속성&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;프로파일은 설정을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;논리 그룹&lt;/strong&gt;으로 묶는 메커니즘이다.
Boot가 제공하는 네 가지 프로파일 관련 속성을 한 표로.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;속성&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;의미&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles.active&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;지금 활성화할 프로파일 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles.include&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;활성 프로파일에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;추가&lt;/strong&gt;로 병합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles.group.&amp;lt;name&amp;gt;&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;논리 그룹 → 여러 프로파일 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles.default&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;아무것도 active 안 하면 이것 사용 (기본값:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;default&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;5-2-include-vs-active&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-2) include vs active&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;흔한 혼동 지점이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;active&lt;/code&gt;는 &amp;quot;외부에서 한 번 결정&amp;quot;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;include&lt;/code&gt;는 &amp;quot;내부에서 항상 따라붙는 것&amp;quot;. 예를 들어 모든
환경에서 공통으로 붙어야 하는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;audit&lt;/code&gt; 프로파일이 있다면.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# application.yml&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-3&quot;&gt;&lt;a href=&quot;#cb6-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;profiles&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-4&quot;&gt;&lt;a href=&quot;#cb6-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; audit&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;   # active가 뭐든 audit은 항상 붙음&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;운영자가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--spring.profiles.active=prod&lt;/code&gt;로 기동해도
실제로는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;default + audit + prod&lt;/code&gt; 세 개가 활성화된다. 이
조합이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application-audit.yml&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application-prod.yml&lt;/code&gt;을 모두 로드한다.&lt;/p&gt;
&lt;h3 id=&quot;5-3-group-이름-하나로-묶어-확장&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-3) group: 이름 하나로 묶어
확장&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;논리 그룹을 만들어 여러 프로파일을 한 번에 활성화할 수 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb7&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb7-1&quot;&gt;&lt;a href=&quot;#cb7-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-2&quot;&gt;&lt;a href=&quot;#cb7-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;profiles&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-3&quot;&gt;&lt;a href=&quot;#cb7-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-4&quot;&gt;&lt;a href=&quot;#cb7-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-5&quot;&gt;&lt;a href=&quot;#cb7-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; prod&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-6&quot;&gt;&lt;a href=&quot;#cb7-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; monitoring&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-7&quot;&gt;&lt;a href=&quot;#cb7-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; audit&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-8&quot;&gt;&lt;a href=&quot;#cb7-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-9&quot;&gt;&lt;a href=&quot;#cb7-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; dev&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-10&quot;&gt;&lt;a href=&quot;#cb7-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; mock-external&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--spring.profiles.active=production&lt;/code&gt;으로 기동하면
실제로는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;prod&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;monitoring&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;audit&lt;/code&gt;
세 개가 활성화된다. 팀에서 &amp;quot;prod 기동&amp;quot;을 말할 때 매번 세 개를 나열하지
않아도 된다.&lt;/p&gt;
&lt;h3 id=&quot;5-4-yaml-문서-분리--on-profile&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-4) YAML 문서 분리 +
on-profile&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 yml 파일 안에서 프로파일별 섹션을 분리하는 문법이 Boot 2.4+에서
바뀌었다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;예전 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles: prod&lt;/code&gt; 문법은 쓰지 말
것&lt;/strong&gt;.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb8&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb8-1&quot;&gt;&lt;a href=&quot;#cb8-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# 피하기 (Boot 2.3 이하 문법)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-2&quot;&gt;&lt;a href=&quot;#cb8-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-3&quot;&gt;&lt;a href=&quot;#cb8-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;profiles&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; prod&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-4&quot;&gt;&lt;a href=&quot;#cb8-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-5&quot;&gt;&lt;a href=&quot;#cb8-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;datasource&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-6&quot;&gt;&lt;a href=&quot;#cb8-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; jdbc:mysql://...&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-7&quot;&gt;&lt;a href=&quot;#cb8-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;pp&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-8&quot;&gt;&lt;a href=&quot;#cb8-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-9&quot;&gt;&lt;a href=&quot;#cb8-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# 선호 (Boot 2.4+)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-10&quot;&gt;&lt;a href=&quot;#cb8-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-11&quot;&gt;&lt;a href=&quot;#cb8-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;datasource&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-12&quot;&gt;&lt;a href=&quot;#cb8-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; jdbc:h2:mem:dev&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-13&quot;&gt;&lt;a href=&quot;#cb8-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;pp&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-14&quot;&gt;&lt;a href=&quot;#cb8-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-15&quot;&gt;&lt;a href=&quot;#cb8-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-16&quot;&gt;&lt;a href=&quot;#cb8-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;activate&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-17&quot;&gt;&lt;a href=&quot;#cb8-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;on-profile&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; prod&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-18&quot;&gt;&lt;a href=&quot;#cb8-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;datasource&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-19&quot;&gt;&lt;a href=&quot;#cb8-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; jdbc:mysql://prod-host:3306/app&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;---&lt;/code&gt;로 분리된 각 문서가 독립 단위이고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.activate.on-profile&lt;/code&gt;이 있는 문서는 해당
프로파일이 활성화됐을 때만 적용된다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;on-profile: &amp;quot;prod &amp;amp; !dev&amp;quot;&lt;/code&gt;처럼 표현식도 지원한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-configurationproperties-vs-value&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6) @ConfigurationProperties
vs @Value&lt;/h2&gt;
&lt;h3 id=&quot;6-1-비교표&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-1) 비교표&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;두 메커니즘은 자주 섞여 쓰이지만 성격이 다르다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;축&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Value&lt;/code&gt;&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt;&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;단위&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;필드 하나&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;접두사 아래 객체 전체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;SpEL (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;#{...}&lt;/code&gt;)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;지원&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Placeholder (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;${...}&lt;/code&gt;)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;지원&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Relaxed Binding&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;제한적&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;완전 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;중첩 객체&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;불가&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;가능 (하위 클래스)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;컬렉션 바인딩&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;불가&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;List/Map/Set 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;검증&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Validated&lt;/code&gt; + JSR-380&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;불변성&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;final&lt;/code&gt; 불가&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;생성자 바인딩으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;final&lt;/code&gt; 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;IDE 자동완성&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-configuration-processor&lt;/code&gt;로 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실무 기본선: &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;여러 키를 묶어 쓰는 모든 설정은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt;&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Value&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Scheduled(fixedDelayString = &amp;quot;${app.delay}&amp;quot;)&lt;/code&gt;처럼
어노테이션 속성에 박아야 해서 객체로 풀 수 없는 경우에만 쓴다.&lt;/p&gt;
&lt;h3 id=&quot;6-2-value가-relaxed-binding에서-약한-이유&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-2) @Value가 Relaxed
Binding에서 약한 이유&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 차이가 환경변수 사용 시 결정적이다. 같은 값을 두 방법으로 받는다고
해보자.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb9&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb9-1&quot;&gt;&lt;a href=&quot;#cb9-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# application.yml&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-2&quot;&gt;&lt;a href=&quot;#cb9-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-3&quot;&gt;&lt;a href=&quot;#cb9-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-4&quot;&gt;&lt;a href=&quot;#cb9-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; localhost&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;APP_MAIL_HOST=smtp.example.com&lt;/code&gt; 환경변수를 세팅한 뒤
기동하면.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb10&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb10-1&quot;&gt;&lt;a href=&quot;#cb10-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// @Value 방식&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-2&quot;&gt;&lt;a href=&quot;#cb10-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Value&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;${app.mail.host}&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-3&quot;&gt;&lt;a href=&quot;#cb10-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; hostA&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-4&quot;&gt;&lt;a href=&quot;#cb10-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-5&quot;&gt;&lt;a href=&quot;#cb10-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// @ConfigurationProperties 방식&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-6&quot;&gt;&lt;a href=&quot;#cb10-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;app.mail&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-7&quot;&gt;&lt;a href=&quot;#cb10-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MailProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; host&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;int&lt;/span&gt; port&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hostA&lt;/code&gt;도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MailProperties.host&lt;/code&gt;도 둘 다
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;smtp.example.com&lt;/code&gt;이 들어간다 — 이 단순 케이스에서는 양쪽
모두 동작한다. 문제는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;키 형태가 애매한 경우&lt;/strong&gt;다. yml에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;app.mail-host&lt;/code&gt;(kebab)로 썼는데
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Value(&amp;quot;${app.mailHost}&amp;quot;)&lt;/code&gt;(camel)로 받으면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Value&lt;/code&gt;는 못 찾는다&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mailHost&lt;/code&gt; 필드가
kebab 키에 자동 매칭된다. §7에서 상세히 본다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-생성자-바인딩과-relaxed-binding&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7) 생성자 바인딩과 Relaxed
Binding&lt;/h2&gt;
&lt;h3 id=&quot;7-1-record로-불변-properties-만들기&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-1) record로 불변
Properties 만들기&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Boot 2.3부터 생성자 바인딩이 공식 지원된다. Java
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;record&lt;/code&gt;와 조합하면 불변 Properties가 한 줄이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb11&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb11-1&quot;&gt;&lt;a href=&quot;#cb11-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;app.mail&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-2&quot;&gt;&lt;a href=&quot;#cb11-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MailProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-3&quot;&gt;&lt;a href=&quot;#cb11-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; host&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-4&quot;&gt;&lt;a href=&quot;#cb11-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;int&lt;/span&gt; port&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-5&quot;&gt;&lt;a href=&quot;#cb11-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; timeout&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-6&quot;&gt;&lt;a href=&quot;#cb11-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;boolean&lt;/span&gt; starttls&lt;/span&gt;
&lt;span id=&quot;cb11-7&quot;&gt;&lt;a href=&quot;#cb11-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb12&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb12-1&quot;&gt;&lt;a href=&quot;#cb12-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@SpringBootApplication&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-2&quot;&gt;&lt;a href=&quot;#cb12-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConfigurationPropertiesScan&lt;/span&gt;   &lt;span class=&quot;co&quot;&gt;// record가 있는 패키지 스캔&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-3&quot;&gt;&lt;a href=&quot;#cb12-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; Application &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;등록 방법은 세 가지다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;방법&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;특징&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt; + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Component&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;컴포넌트 스캔으로 등록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableConfigurationProperties(MailProperties.class)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;명시적 등록, 테스트에서 개별 활성화 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationPropertiesScan&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;지정 패키지에서 어노테이션 붙은 클래스 자동 등록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;record는 생성자가 하나뿐이므로 자동으로 생성자 바인딩이
적용된다&lt;/strong&gt;. 기존 JavaBean 스타일(getter/setter)로 쓰고 싶다면
클래스로 만들고 setter를 제공하면 된다.&lt;/p&gt;
&lt;h3 id=&quot;7-2-relaxed-binding-네-가지-표기를-하나로&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-2) Relaxed Binding:
네 가지 표기를 하나로&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt;의 킬러 기능이다. 다음 네 가지
표기는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;전부 같은 속성&lt;/strong&gt;으로 매핑된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;표기&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;예시&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;주 사용처&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;kebab-case&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;app.mail-host&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;yml/properties 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;camelCase&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;app.mailHost&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;yml 혼용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;snake_case&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;app.mail_host&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;properties&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;SCREAMING_SNAKE (uppercase + underscore)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;APP_MAIL_HOST&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;환경변수 전용&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Kubernetes에서 환경변수로 주입하는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;APP_MAIL_HOST&lt;/code&gt;가 yml의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;app.mail-host&lt;/code&gt;와 같은 키로 해석되는 이유가 바로 이 Relaxed
Binding이다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;규칙&lt;/strong&gt;: 환경변수는 알파벳/숫자/언더스코어만
허용하므로 dot과 dash가 전부 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;_&lt;/code&gt;로 치환된다. 즉
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;app.mail-host&lt;/code&gt; → &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;APP_MAIL_HOST&lt;/code&gt;가 된다.&lt;/p&gt;
&lt;h3 id=&quot;7-3-팀-컨벤션-yml은-kebab-환경변수는-screaming_snake&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-3) 팀
컨벤션: yml은 kebab, 환경변수는 SCREAMING_SNAKE&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 팀에서 두 표기를 섞으면 디버깅 지옥이 된다. 기본선을 정해두자.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb13&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb13-1&quot;&gt;&lt;a href=&quot;#cb13-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# 피하기: 한 파일에 kebab와 camelCase 혼용&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-2&quot;&gt;&lt;a href=&quot;#cb13-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-3&quot;&gt;&lt;a href=&quot;#cb13-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mail-host&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; smtp.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-4&quot;&gt;&lt;a href=&quot;#cb13-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mailPort&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;465&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-5&quot;&gt;&lt;a href=&quot;#cb13-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-6&quot;&gt;&lt;a href=&quot;#cb13-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# 선호: kebab으로 통일&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-7&quot;&gt;&lt;a href=&quot;#cb13-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-8&quot;&gt;&lt;a href=&quot;#cb13-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mail-host&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; smtp.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-9&quot;&gt;&lt;a href=&quot;#cb13-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mail-port&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;465&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;환경변수는 자연스럽게 SCREAMING_SNAKE가 되므로 선택권이 없다. 팀
컨벤션을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;yml=kebab, env=SCREAMING_SNAKE&lt;/strong&gt;로 못 박으면
Relaxed Binding이 둘 사이를 연결한다.&lt;/p&gt;
&lt;h3 id=&quot;7-4-컴파일-옵션--parameters&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-4) 컴파일 옵션: -parameters&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;record 생성자 바인딩은 파라미터 이름을 바이트코드에서 읽어온다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;-parameters&lt;/code&gt; 컴파일 플래그가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;없으면&lt;/strong&gt;
파라미터 이름이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;arg0&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;arg1&lt;/code&gt;으로 박혀서 바인딩이
전부 깨진다. Gradle 설정 예시.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb14&quot;&gt;&lt;pre class=&quot;sourceCode groovy&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode groovy&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb14-1&quot;&gt;&lt;a href=&quot;#cb14-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;tasks&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;JavaCompile&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-2&quot;&gt;&lt;a href=&quot;#cb14-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    options&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;compilerArgs &lt;span class=&quot;op&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;#39;-parameters&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-3&quot;&gt;&lt;a href=&quot;#cb14-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Boot Gradle plugin이 적용된 프로젝트는 이 옵션을 기본으로
넣어준다. 그래도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-parent&lt;/code&gt;를 사용하지 않는
멀티모듈 프로젝트에서는 명시적으로 확인해두는 게 안전하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-duration--datasize--period-컨버터&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8) Duration / DataSize /
Period 컨버터&lt;/h2&gt;
&lt;h3 id=&quot;8-1-단위-접미사-바인딩&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-1) 단위 접미사 바인딩&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot는 자주 쓰는 세 타입에 대해 단위 접미사를 자동으로 파싱한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;타입&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;예시 문자열&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ISO-8601 대안&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;10s&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;5m&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;2h&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;30d&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PT10S&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PT5M&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSize&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;10MB&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;2GB&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;512KB&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Period&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;30d&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;3m&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;1y&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;P30D&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;P1Y&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb15&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb15-1&quot;&gt;&lt;a href=&quot;#cb15-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-2&quot;&gt;&lt;a href=&quot;#cb15-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-3&quot;&gt;&lt;a href=&quot;#cb15-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; 10s&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;           # Duration → 10초&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-4&quot;&gt;&lt;a href=&quot;#cb15-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;max-size&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; 25MB&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;         # DataSize → 25MB&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-5&quot;&gt;&lt;a href=&quot;#cb15-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;retention&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; 90d&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;         # Period → 90일&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb16&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb16-1&quot;&gt;&lt;a href=&quot;#cb16-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;app.mail&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-2&quot;&gt;&lt;a href=&quot;#cb16-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MailProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-3&quot;&gt;&lt;a href=&quot;#cb16-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; timeout&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-4&quot;&gt;&lt;a href=&quot;#cb16-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    DataSize maxSize&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-5&quot;&gt;&lt;a href=&quot;#cb16-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    Period retention&lt;/span&gt;
&lt;span id=&quot;cb16-6&quot;&gt;&lt;a href=&quot;#cb16-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;문자열의 단위를 타입 매칭에 맡긴다는 점이 중요하다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;10s&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration&lt;/code&gt;으로 파싱되고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;10MB&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSize&lt;/code&gt;로 파싱된다. 같은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;10&lt;/code&gt;이라도 타입이 다르면 해석이 다르다.&lt;/p&gt;
&lt;h3 id=&quot;8-2-durationunit--datasizeunit-기본-단위&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-2) @DurationUnit /
@DataSizeUnit: 기본 단위&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;단위를 안 쓰고 숫자만 적고 싶을 때가 있다. &amp;quot;이 필드는 초 단위&amp;quot;로
고정하고 yml에는 숫자만 쓰는 식.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb17&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb17-1&quot;&gt;&lt;a href=&quot;#cb17-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;app.mail&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-2&quot;&gt;&lt;a href=&quot;#cb17-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MailProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-3&quot;&gt;&lt;a href=&quot;#cb17-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@DurationUnit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ChronoUnit&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;SECONDS&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; timeout&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-4&quot;&gt;&lt;a href=&quot;#cb17-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@DataSizeUnit&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;DataUnit&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;MEGABYTES&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; DataSize maxSize&lt;/span&gt;
&lt;span id=&quot;cb17-5&quot;&gt;&lt;a href=&quot;#cb17-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb18&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb18-1&quot;&gt;&lt;a href=&quot;#cb18-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-2&quot;&gt;&lt;a href=&quot;#cb18-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-3&quot;&gt;&lt;a href=&quot;#cb18-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;       # 단위 없음 → 10초로 해석&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-4&quot;&gt;&lt;a href=&quot;#cb18-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;max-size&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;25&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;      # 단위 없음 → 25MB로 해석&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;yml에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;timeout: 10s&lt;/code&gt;처럼 단위를 명시해도 여전히 동작한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@DurationUnit&lt;/code&gt;은 &amp;quot;단위 없을 때의 기본값&amp;quot;만 지정하는
셈이다.&lt;/p&gt;
&lt;h3 id=&quot;8-3-함정-long-필드에-단위-표기&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-3) 함정: long 필드에 단위
표기&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration&lt;/code&gt; 대신 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;long timeoutMillis&lt;/code&gt; 같은 원시
타입을 쓰면 단위 접미사를 못 받는다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb19&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb19-1&quot;&gt;&lt;a href=&quot;#cb19-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-2&quot;&gt;&lt;a href=&quot;#cb19-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-3&quot;&gt;&lt;a href=&quot;#cb19-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; 10s&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;     # long에 바인딩하려 하면 실패&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb20&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb20-1&quot;&gt;&lt;a href=&quot;#cb20-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-2&quot;&gt;&lt;a href=&quot;#cb20-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MailProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dt&quot;&gt;long&lt;/span&gt; timeout&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-3&quot;&gt;&lt;a href=&quot;#cb20-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-4&quot;&gt;&lt;a href=&quot;#cb20-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: 타입을 Duration으로&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-5&quot;&gt;&lt;a href=&quot;#cb20-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MailProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; timeout&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;10초&amp;quot;라는 의미를 값이 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;타입&lt;/strong&gt;이 표현하게 하는
게 핵심이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;long&lt;/code&gt;으로 받으면 단위가 숫자 뒤에 사라지고
호출부에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;10 * 1000&lt;/code&gt;을 일일이 곱해야 한다 — 이게 버그의
온상이다.&lt;/p&gt;
&lt;h3 id=&quot;8-4-커스텀-converter&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-4) 커스텀 Converter&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot가 기본 제공하지 않는 타입은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Converter&amp;lt;String, MyType&amp;gt;&lt;/code&gt;을 빈으로 등록하면 된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb21&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb21-1&quot;&gt;&lt;a href=&quot;#cb21-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Configuration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-2&quot;&gt;&lt;a href=&quot;#cb21-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; ConverterConfig &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-3&quot;&gt;&lt;a href=&quot;#cb21-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-4&quot;&gt;&lt;a href=&quot;#cb21-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@ConfigurationPropertiesBinding&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-5&quot;&gt;&lt;a href=&quot;#cb21-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; Converter&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;stringToUri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-6&quot;&gt;&lt;a href=&quot;#cb21-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;URI&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;::&lt;/span&gt;create&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-7&quot;&gt;&lt;a href=&quot;#cb21-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-8&quot;&gt;&lt;a href=&quot;#cb21-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationPropertiesBinding&lt;/code&gt; 어노테이션이 중요하다.
이게 없으면 일반 Spring MVC 컨버터로만 등록되고 Properties 바인딩 시엔
쓰이지 않는다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;9-validated로-기동-시점-검증&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;9) @Validated로 기동 시점
검증&lt;/h2&gt;
&lt;h3 id=&quot;9-1-틀린-값을-빨리-터트리기&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-1) 틀린 값을 빨리 터트리기&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;설정 오류는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기동 시점&lt;/strong&gt;에 터져야 한다. 런타임에 처음
그 설정을 읽는 요청이 실패하면 유저 한 명이 그 버그의 제물이 된다.
Boot는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Validated&lt;/code&gt; + JSR-380 조합으로 기동 시 검증을
제공한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb22&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb22-1&quot;&gt;&lt;a href=&quot;#cb22-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;app.mail&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-2&quot;&gt;&lt;a href=&quot;#cb22-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Validated&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-3&quot;&gt;&lt;a href=&quot;#cb22-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MailProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-4&quot;&gt;&lt;a href=&quot;#cb22-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@NotBlank&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; host&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-5&quot;&gt;&lt;a href=&quot;#cb22-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Min&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;@Max&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;65535&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;int&lt;/span&gt; port&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-6&quot;&gt;&lt;a href=&quot;#cb22-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@NotNull&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Duration&lt;/span&gt; timeout&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-7&quot;&gt;&lt;a href=&quot;#cb22-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Pattern&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;regexp &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;^(TLS|STARTTLS|NONE)$&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; security&lt;/span&gt;
&lt;span id=&quot;cb22-8&quot;&gt;&lt;a href=&quot;#cb22-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;yml에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;host:&lt;/code&gt;가 비어 있거나 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;port: 99999&lt;/code&gt;가
있으면 기동이 다음처럼 실패한다.&lt;/p&gt;
&lt;pre style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;***************************
APPLICATION FAILED TO START
***************************

Binding to target failed:

    Property: app.mail.port
    Value: 99999
    Reason: 최대 허용 값: 65535&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;프로덕션 배포가 이 단계에서 막힌다. 롤링 업데이트 중 잘못된 설정이
들어가면 헬스체크가 실패해서 이전 버전이 그대로 유지된다 — 이게 의도된
방어선이다.&lt;/p&gt;
&lt;h3 id=&quot;9-2-중첩-객체에-valid-붙이기&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-2) 중첩 객체에 @Valid
붙이기&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt; 안에 중첩 객체가 있으면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;그 필드에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Valid&lt;/code&gt;를 붙여야&lt;/strong&gt; 하위 필드까지
검증된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb24&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb24-1&quot;&gt;&lt;a href=&quot;#cb24-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;app&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-2&quot;&gt;&lt;a href=&quot;#cb24-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Validated&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-3&quot;&gt;&lt;a href=&quot;#cb24-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;AppProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-4&quot;&gt;&lt;a href=&quot;#cb24-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@NotBlank&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-5&quot;&gt;&lt;a href=&quot;#cb24-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Valid&lt;/span&gt; MailProperties mail&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;     &lt;span class=&quot;co&quot;&gt;// @Valid 없으면 하위 검증 스킵&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-6&quot;&gt;&lt;a href=&quot;#cb24-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Valid&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;@Valid&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Endpoint&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; endpoints&lt;/span&gt;
&lt;span id=&quot;cb24-7&quot;&gt;&lt;a href=&quot;#cb24-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-8&quot;&gt;&lt;a href=&quot;#cb24-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MailProperties&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;@NotBlank&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; host&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;@Min&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;int&lt;/span&gt; port&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-9&quot;&gt;&lt;a href=&quot;#cb24-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;record&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Endpoint&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;@NotBlank&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;@Pattern&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;regexp&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;^https?://.+&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; url&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-10&quot;&gt;&lt;a href=&quot;#cb24-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Valid&lt;/code&gt;를 빠뜨리면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mail.host&lt;/code&gt;가 비어 있어도
기동이 성공해버린다. &amp;quot;검증이 돌고 있다는 착각&amp;quot;이 가장 위험하다 — 실제로
검증되는 건 최상위 필드만이다.&lt;/p&gt;
&lt;h3 id=&quot;9-3-의존성-spring-boot-starter-validation&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-3) 의존성:
spring-boot-starter-validation&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;JSR-380 구현체(Hibernate Validator)는 Boot 기본 의존성에 포함되지
않는다. 명시적으로 추가해야 한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb25&quot;&gt;&lt;pre class=&quot;sourceCode groovy&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode groovy&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb25-1&quot;&gt;&lt;a href=&quot;#cb25-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;dependencies &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-2&quot;&gt;&lt;a href=&quot;#cb25-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    implementation &lt;span class=&quot;st&quot;&gt;&amp;#39;org.springframework.boot:spring-boot-starter-validation&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-3&quot;&gt;&lt;a href=&quot;#cb25-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;빠뜨리면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Validated&lt;/code&gt;가 조용히 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;아무 검증도 안
한다&lt;/strong&gt;. 어노테이션만 붙고 돌아가는 validator가 없기 때문.
테스트를 한 번 돌려서 &amp;quot;일부러 틀린 값&amp;quot;이 실제로 기동을 실패시키는지
확인하는 게 안전하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;10-origin-추적과-actuatorenv-디버깅&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;10) Origin 추적과
/actuator/env 디버깅&lt;/h2&gt;
&lt;h3 id=&quot;10-1-origintrackedvalue&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-1) OriginTrackedValue&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Boot는 각 속성 값이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어디서 왔는지&lt;/strong&gt;를 추적한다.
모든 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;PropertySource&lt;/code&gt;가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OriginTrackedValue&lt;/code&gt;로
값을 감싸서 &amp;quot;이 값은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application-prod.yml&lt;/code&gt; 15번째 줄에서
왔음&amp;quot; 같은 메타데이터를 유지한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 정보는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/env&lt;/code&gt; 엔드포인트에서 그대로
노출된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb26&quot;&gt;&lt;pre class=&quot;sourceCode json&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode json&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb26-1&quot;&gt;&lt;a href=&quot;#cb26-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-2&quot;&gt;&lt;a href=&quot;#cb26-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;dt&quot;&gt;&amp;quot;propertySources&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;ot&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-3&quot;&gt;&lt;a href=&quot;#cb26-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-4&quot;&gt;&lt;a href=&quot;#cb26-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;systemEnvironment&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-5&quot;&gt;&lt;a href=&quot;#cb26-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;&amp;quot;properties&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-6&quot;&gt;&lt;a href=&quot;#cb26-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;dt&quot;&gt;&amp;quot;APP_MAIL_HOST&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-7&quot;&gt;&lt;a href=&quot;#cb26-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;          &lt;span class=&quot;dt&quot;&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;smtp.production.example.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-8&quot;&gt;&lt;a href=&quot;#cb26-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;          &lt;span class=&quot;dt&quot;&gt;&amp;quot;origin&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;System Environment Property &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;\&amp;quot;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;APP_MAIL_HOST&lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;\&amp;quot;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-9&quot;&gt;&lt;a href=&quot;#cb26-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-10&quot;&gt;&lt;a href=&quot;#cb26-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-11&quot;&gt;&lt;a href=&quot;#cb26-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;ot&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-12&quot;&gt;&lt;a href=&quot;#cb26-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-13&quot;&gt;&lt;a href=&quot;#cb26-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;Config resource &amp;#39;class path resource [application-prod.yml]&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-14&quot;&gt;&lt;a href=&quot;#cb26-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;dt&quot;&gt;&amp;quot;properties&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-15&quot;&gt;&lt;a href=&quot;#cb26-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;dt&quot;&gt;&amp;quot;app.mail.host&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-16&quot;&gt;&lt;a href=&quot;#cb26-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;          &lt;span class=&quot;dt&quot;&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;localhost&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-17&quot;&gt;&lt;a href=&quot;#cb26-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;          &lt;span class=&quot;dt&quot;&gt;&amp;quot;origin&amp;quot;&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;class path resource [application-prod.yml] - 3:11&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-18&quot;&gt;&lt;a href=&quot;#cb26-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-19&quot;&gt;&lt;a href=&quot;#cb26-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;      &lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-20&quot;&gt;&lt;a href=&quot;#cb26-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-21&quot;&gt;&lt;a href=&quot;#cb26-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;  &lt;span class=&quot;ot&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-22&quot;&gt;&lt;a href=&quot;#cb26-22&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;위 응답에서 보이듯이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;APP_MAIL_HOST&lt;/code&gt;가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;app.mail.host&lt;/code&gt;의 환경변수 형태로 해석&lt;/strong&gt;되고, 실제
적용 값은 환경변수가 이긴다 (systemEnvironment가 yml보다 위). Origin에
&amp;quot;15번째 줄 11번째 칸&amp;quot;까지 찍혀서 &amp;quot;이 값이 어디서 왔지?&amp;quot;를 IDE 없이 답할
수 있다.&lt;/p&gt;
&lt;h3 id=&quot;10-2-활성화&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-2) 활성화&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.endpoint.env.show-values&lt;/code&gt;가 Boot 3부터
도입됐다. 보안상 기본값은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;NEVER&lt;/code&gt; — 값을 가리고 키만
보여준다. 디버깅용으로 열려면 명시.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb27&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb27-1&quot;&gt;&lt;a href=&quot;#cb27-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;management&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-2&quot;&gt;&lt;a href=&quot;#cb27-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-3&quot;&gt;&lt;a href=&quot;#cb27-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-4&quot;&gt;&lt;a href=&quot;#cb27-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;show-values&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; WHEN_AUTHORIZED&lt;/span&gt;&lt;span class=&quot;co&quot;&gt;   # 인증된 요청에만 값 노출&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-5&quot;&gt;&lt;a href=&quot;#cb27-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;endpoints&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-6&quot;&gt;&lt;a href=&quot;#cb27-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-7&quot;&gt;&lt;a href=&quot;#cb27-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;exposure&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-8&quot;&gt;&lt;a href=&quot;#cb27-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;include&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; env,health,info&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ALWAYS&lt;/code&gt;는 시크릿이 그대로 노출되므로 프로덕션에서는 쓰지
않는다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WHEN_AUTHORIZED&lt;/code&gt;는 Spring Security와 조합해 관리자
권한이 있는 경우에만 값을 보여준다.&lt;/p&gt;
&lt;h3 id=&quot;10-3-디버깅-플로우&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-3) 디버깅 플로우&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;왜 이 값이 이렇게 들어갔지?&amp;quot; 상황의 순서.&lt;/p&gt;
&lt;ol type=&quot;1&quot; style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/env/app.mail.host&lt;/code&gt; 호출&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;응답의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;propertySources&lt;/code&gt; 배열 순서가 우선순위 순 (위가
이김)&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;activeValue&lt;/code&gt;가 최종 적용된 값&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;각 소스의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;value&lt;/code&gt; + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;origin&lt;/code&gt;을 훑어서 어디서
덮어썼는지 추적&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Actuator를 못 여는 환경이라면 기동 로그에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--debug&lt;/code&gt;
플래그로 상세 출력을 켠다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ConfigDataEnvironment&lt;/code&gt;가 로드한
모든 파일과 프로파일 해석 결과가 찍힌다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;11-시크릿-configtree와-vault&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;11) 시크릿: configtree와
Vault&lt;/h2&gt;
&lt;h3 id=&quot;11-1-환경변수는-그만--configtree로&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;11-1) 환경변수는 그만 —
configtree로&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;가장 기본적인 시크릿 전달 방식이 환경변수였지만, 이 방식은 보안상
여러 문제가 있다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;프로세스 목록(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ps eef&lt;/code&gt;)이나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/proc/*/environ&lt;/code&gt;에서 노출될 수 있다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;자식 프로세스에 그대로 상속된다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;로그에 찍히는 사고가 빈번하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Kubernetes의 Secret을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파일 마운트&lt;/strong&gt;로 붙이고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configtree:&lt;/code&gt; 스킴으로 읽는 방식이 권장된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb28&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb28-1&quot;&gt;&lt;a href=&quot;#cb28-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# K8s Deployment 발췌&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-2&quot;&gt;&lt;a href=&quot;#cb28-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-3&quot;&gt;&lt;a href=&quot;#cb28-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; app-secrets&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-4&quot;&gt;&lt;a href=&quot;#cb28-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; /etc/secrets&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-5&quot;&gt;&lt;a href=&quot;#cb28-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-6&quot;&gt;&lt;a href=&quot;#cb28-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-7&quot;&gt;&lt;a href=&quot;#cb28-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; app-secrets&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-8&quot;&gt;&lt;a href=&quot;#cb28-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-9&quot;&gt;&lt;a href=&quot;#cb28-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;secretName&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; app-mail-secret&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb29&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb29-1&quot;&gt;&lt;a href=&quot;#cb29-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# application.yml&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb29-2&quot;&gt;&lt;a href=&quot;#cb29-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb29-3&quot;&gt;&lt;a href=&quot;#cb29-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb29-4&quot;&gt;&lt;a href=&quot;#cb29-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; configtree:/etc/secrets/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/etc/secrets/app.mail.password&lt;/code&gt; 파일이 마운트되면 Boot가
그 내용을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;app.mail.password&lt;/code&gt; 속성으로 읽는다. 환경변수보다
누수 경로가 훨씬 적다.&lt;/p&gt;
&lt;h3 id=&quot;11-2-spring-cloud-vault&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;11-2) Spring Cloud Vault&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Vault를 쓰는 경우 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-cloud-starter-vault-config&lt;/code&gt;를
추가하고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import=vault://&lt;/code&gt;로 끌어올 수
있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb30&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb30-1&quot;&gt;&lt;a href=&quot;#cb30-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb30-2&quot;&gt;&lt;a href=&quot;#cb30-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;cloud&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb30-3&quot;&gt;&lt;a href=&quot;#cb30-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;vault&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb30-4&quot;&gt;&lt;a href=&quot;#cb30-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; https://vault.internal:8200&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb30-5&quot;&gt;&lt;a href=&quot;#cb30-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; kubernetes&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb30-6&quot;&gt;&lt;a href=&quot;#cb30-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb30-7&quot;&gt;&lt;a href=&quot;#cb30-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; vault://secret/app/mail&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Vault에 저장된 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;secret/app/mail&lt;/code&gt;의 키들이 Boot 속성 소스로
등록된다. Renewal, lease 관리까지 Vault 클라이언트가 처리한다. 이 글의
주제를 벗어나므로 상세는 Spring Cloud Vault 문서로.&lt;/p&gt;
&lt;h3 id=&quot;11-3-암호화-boot-기본은-없다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;11-3) 암호화: Boot 기본은
없다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&amp;quot;yml에 비밀번호를 암호화해서 넣고 기동 시 복호화&amp;quot;는 Boot 표준 기능이
아니다. Jasypt(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jasypt-spring-boot&lt;/code&gt;) 같은 외부 솔루션을
쓰거나, 애초에 Vault/configtree로 분리하는 쪽이 권장된다. yml 안의
암호화 문자열은 키 관리가 또 다른 문제를 만든다 — 암복호화 키를 어떻게
배포할 것인가가 원래 문제로 돌아간다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;12-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;12) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;모든 설정은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt;로&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Value&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Scheduled&lt;/code&gt;처럼 어노테이션 속성에 박아야 하는 경우에만.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;record + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationPropertiesScan&lt;/code&gt;&lt;/strong&gt;
조합을 기본으로. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;-parameters&lt;/code&gt; 컴파일 옵션이 켜져 있는지 한
번은 확인.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;yml은 kebab-case, 환경변수는 SCREAMING_SNAKE&lt;/strong&gt;로 팀
컨벤션 고정. 혼용이 디버깅 지옥을 만든다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSize&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Period&lt;/code&gt;는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;타입으로 단위를 표현&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;long timeoutMillis&lt;/code&gt;
지양, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Duration timeout&lt;/code&gt; 선호.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;**&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Validated&lt;/code&gt; + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@NotBlank&lt;/code&gt; /
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Min&lt;/code&gt;**으로 기동 시점 방어선을 세움.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-validation&lt;/code&gt; 의존성 확인.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;중첩 Properties는 부모 필드에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Valid&lt;/code&gt;&lt;/strong&gt;
붙이기. 빠뜨리면 검증이 조용히 스킵된다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import&lt;/code&gt;로 외부 파일 끌어올 때는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;optional:&lt;/code&gt; 접두사&lt;/strong&gt;를 기본. 반드시 있어야
하는 파일만 생략.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Boot 2.3 이하의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles: prod&lt;/code&gt; 문법
금지&lt;/strong&gt;. 2.4+의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.activate.on-profile&lt;/code&gt;로
전환.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;시크릿은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;환경변수 대신 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;configtree:&lt;/code&gt;&lt;/strong&gt;
또는 Vault. K8s Secret은 파일 마운트가 기본.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;설정 오작동 디버깅은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/env/{key}&lt;/code&gt;&lt;/strong&gt; 엔드포인트 먼저.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;origin&lt;/code&gt; 필드가 출처를 정확히 찍어준다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;server.port=${PORT:8080}&lt;/code&gt; 같은 관용구는 placeholder +
default 문법으로 통일. 운영자 오버라이드 포인트를 명확히.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;13-한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;13) 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Spring Boot의 Externalized Configuration은 소스·바인딩·검증
세 축을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConfigurationProperties&lt;/code&gt;로 통합한 체계다&lt;/strong&gt;. Boot 2.4
이후 Config Data API가 프로파일 문법과 외부 파일 로딩을 통째로
재정리했기 때문에 예전 관습(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.profiles:&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.additional-location&lt;/code&gt;)을 버리고
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.activate.on-profile&lt;/code&gt;·&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.config.import: optional:...&lt;/code&gt;로
이전&lt;/strong&gt;하는 게 실무 출발선이다. record + Relaxed Binding +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Validated&lt;/code&gt;까지 엮으면 기동 시점에 틀린 설정을 막아주는
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;타입 안전한 설정 파이프라인&lt;/strong&gt;이 완성된다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;태그&lt;/strong&gt;: Spring Boot, Externalized Configuration,
@ConfigurationProperties, spring.config.import, Profile, Relaxed
Binding, Duration, @Validated, configtree, Actuator&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/Spring</category>
      <category>@configurationProperties</category>
      <category>@validated</category>
      <category>actuator</category>
      <category>configtree</category>
      <category>Duration</category>
      <category>Externalized Configuration</category>
      <category>Profile</category>
      <category>relaxed binding</category>
      <category>spring boot</category>
      <category>spring.config.import</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/406</guid>
      <comments>https://dding-shark.tistory.com/406#entry406comment</comments>
      <pubDate>Tue, 14 Apr 2026 22:05:49 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot AutoConfiguration &amp;mdash; @AutoConfiguration과 조건부 조립의 내부</title>
      <link>https://dding-shark.tistory.com/405</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;spring-boot-autoconfiguration--autoconfiguration과-조건부-조립의-내부&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;Spring
Boot AutoConfiguration — @AutoConfiguration과 조건부 조립의 내부&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-web&lt;/code&gt;을 의존성에 추가하고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;main&lt;/code&gt;을 돌리면, 톰캣이 뜨고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DispatcherServlet&lt;/code&gt;이 등록되고, Jackson이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MessageConverter&lt;/code&gt;에 물리고, 정적 리소스가 매핑되고, 에러
페이지가 연결된다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.datasource.url&lt;/code&gt;만 넣으면 HikariCP 커넥션 풀과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSource&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JdbcTemplate&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceTransactionManager&lt;/code&gt;까지 조용히 조립된다. 어떤
클래스도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Bean&lt;/code&gt;으로 직접 등록한 적이 없다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootApplication&lt;/code&gt; 한 줄이 이걸 전부 해낸다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;그런데 이 &amp;quot;한 줄&amp;quot;의 내부는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableAutoConfiguration&lt;/code&gt; →
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AutoConfigurationImportSelector&lt;/code&gt; →
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;META-INF/spring/.../AutoConfiguration.imports&lt;/code&gt; 파일 → 조건부
필터 → 빈 등록으로 이어지는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;정해진 절차&lt;/strong&gt;다. 여기에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;이 깔려 있어 사용자가 빈을 직접
두면 기본값이 양보한다. 겉보기에는 마법이지만 뜯어보면 Spring
Framework의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Conditional&lt;/code&gt;과 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import&lt;/code&gt; 메커니즘을
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파일 하나와 우선순위 규칙&lt;/strong&gt;으로 묶어낸 시스템이다.
5편에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Conditional&lt;/code&gt;의 뿌리를, 14편에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DispatcherServlet&lt;/code&gt;과 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcAutoConfiguration&lt;/code&gt;을
봤다면, 이번 글은 그 둘을 잇는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;조립 파이프라인 자체&lt;/strong&gt;를
다룬다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 주제에서 실무가 자주 막히는 지점은 다음 세 곳이다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.factories&lt;/code&gt;에 자동구성을 등록했는데 Boot
3에서 잡히지 않는다&lt;/strong&gt; — Boot 2.7부터는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports&lt;/code&gt;
파일이 표준이고, Boot 3는 옛 키를 더 이상 읽지 않는다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;사용자가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Bean DataSource&lt;/code&gt;를 직접 정의했더니
Boot의 기본 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSource&lt;/code&gt;가 사라졌다&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceAutoConfiguration&lt;/code&gt;이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean(DataSource.class)&lt;/code&gt;으로 양보하는
의도된 동작이다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--debug&lt;/code&gt; 로그의 &amp;quot;Negative matches&amp;quot;에 내가 기대한
자동구성이 박혀 있는데 이유가 해석이 안 된다&lt;/strong&gt; — CONDITIONS
EVALUATION REPORT의 세 섹션(Positive / Negative / Unconditional)을
구분해서 읽어야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;왜 → 상단 어노테이션 → Selector 흐름 → imports 파일 →
@AutoConfiguration → 조건부 패밀리 → 순서 → 디버깅 → 실무&lt;/strong&gt;
순으로, Spring Boot 3.x / Spring Framework 6.x / Java 17+ 기준으로
정리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-springbootapplication-한-줄-아래에-뭐가-있나&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1)
@SpringBootApplication 한 줄 아래에 뭐가 있나&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-autoconfigurationimportselector-자동구성을-끌어오는-단일-진입점&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2)
AutoConfigurationImportSelector: 자동구성을 끌어오는 단일
진입점&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-autoconfigurationimports-vs-옛-springfactories&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3)
AutoConfiguration.imports vs 옛 spring.factories&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-autoconfiguration-애너테이션과-proxybeanmethodsfalse&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4)
@AutoConfiguration 애너테이션과 proxyBeanMethods=false&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-조건부-어노테이션-패밀리-자동구성에서-쓰이는-여덟-가지&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5)
조건부 어노테이션 패밀리: 자동구성에서 쓰이는 여덟 가지&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-conditionalonmissingbean-자동구성-패턴의-핵심&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6)
@ConditionalOnMissingBean: 자동구성 패턴의 핵심&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-autoconfigurebeforeafter와-순서-의존성&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7)
AutoConfigureBefore/After와 순서 의존성&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-조건-평가-디버깅---debug와-actuator-conditions&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8) 조건
평가 디버깅: --debug와 Actuator /conditions&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#9-자동구성-비활성화-방법&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;9) 자동구성 비활성화
방법&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#10-enablewebmvc-함정과-자동구성-해제&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;10) @EnableWebMvc
함정과 자동구성 해제&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#11-테스트에서-자동구성-선별-로드&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;11) 테스트에서 자동구성
선별 로드&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#12-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;12) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#13-한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;13) 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-springbootapplication-한-줄-아래에-뭐가-있나&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1)
@SpringBootApplication 한 줄 아래에 뭐가 있나&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootApplication&lt;/code&gt;은 하나의 어노테이션처럼 보이지만
실제로는 세 개의 메타 어노테이션을 묶은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;합성
어노테이션&lt;/strong&gt;이다. 이 세 개가 각각 다른 역할을 하고, 그 중
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableAutoConfiguration&lt;/code&gt;이 자동구성 전체의 트리거다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Target&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;ElementType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;TYPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Retention&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;RetentionPolicy&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;RUNTIME&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-3&quot;&gt;&lt;a href=&quot;#cb1-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@SpringBootConfiguration&lt;/span&gt;           &lt;span class=&quot;co&quot;&gt;// = @Configuration + 감지용 마커&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-4&quot;&gt;&lt;a href=&quot;#cb1-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@EnableAutoConfiguration&lt;/span&gt;           &lt;span class=&quot;co&quot;&gt;// = @Import(AutoConfigurationImportSelector.class)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-5&quot;&gt;&lt;a href=&quot;#cb1-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ComponentScan&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;excludeFilters &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-6&quot;&gt;&lt;a href=&quot;#cb1-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;@interface&lt;/span&gt; SpringBootApplication &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;세 어노테이션이 하는 일은 서로 독립적이다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootConfiguration&lt;/code&gt;&lt;/strong&gt;: 사실상
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;이다. 다만 Boot 전용 마커라
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootTest&lt;/code&gt; 같은 테스트 도구가 이 마커로 &amp;quot;앱의 메인
설정 클래스&amp;quot;를 자동으로 찾아낸다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableAutoConfiguration&lt;/code&gt;&lt;/strong&gt;: 자동구성
스캔의 진입점. 내부가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import(AutoConfigurationImportSelector.class)&lt;/code&gt;로 되어
있다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ComponentScan&lt;/code&gt;&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootApplication&lt;/code&gt;이 붙은 클래스의 패키지를 루트로 삼아
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Component&lt;/code&gt; 계열을 스캔한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;1-1-enableautoconfiguration-내부&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-1) @EnableAutoConfiguration
내부&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableAutoConfiguration&lt;/code&gt;을 직접 열어보면 다음과 같은
구조다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Target&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;ElementType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;TYPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Retention&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;RetentionPolicy&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;RUNTIME&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-3&quot;&gt;&lt;a href=&quot;#cb2-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfigurationPackage&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-4&quot;&gt;&lt;a href=&quot;#cb2-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Import&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;AutoConfigurationImportSelector&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-5&quot;&gt;&lt;a href=&quot;#cb2-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;@interface&lt;/span&gt; EnableAutoConfiguration &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-6&quot;&gt;&lt;a href=&quot;#cb2-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; ENABLED_OVERRIDE_PROPERTY &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;spring.boot.enableautoconfiguration&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-7&quot;&gt;&lt;a href=&quot;#cb2-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;?&amp;gt;[]&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;exclude&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-8&quot;&gt;&lt;a href=&quot;#cb2-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;excludeName&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-9&quot;&gt;&lt;a href=&quot;#cb2-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import(AutoConfigurationImportSelector.class)&lt;/code&gt;&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ImportSelector&lt;/code&gt; 구현체를 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import&lt;/code&gt;로 등록하면
Spring은 그 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;selectImports()&lt;/code&gt; 메서드가 반환한 클래스 배열을
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;처럼 취급해 추가 등록한다. 자동구성 로딩은 이
메커니즘을 쓴다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigurationPackage&lt;/code&gt;&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootApplication&lt;/code&gt;이 붙은 클래스의 패키지를 &amp;quot;자동구성
스캔의 루트&amp;quot;로 기억한다. Spring Data JPA의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Entity&lt;/code&gt; 스캔
같은 후속 자동구성이 이 값을 참조한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;1-2-자동의-실체는-import-리턴-배열이다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-2) &amp;quot;자동&amp;quot;의 실체는
@Import 리턴 배열이다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;오해하지 않기 위해 한 번 짚어둔다. 자동구성은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;런타임에
동적으로 클래스를 생성하는 것&lt;/strong&gt;이 아니다. Boot는 미리 빌드된 JAR
안에 자동구성 클래스들과 그 목록 파일을 묶어두고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AutoConfigurationImportSelector&lt;/code&gt;가 그 목록을 읽어
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import&lt;/code&gt;의 반환 배열로 넘긴다. 그 뒤는 Spring의 표준
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt; 처리 흐름이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;즉 자동구성이 하는 일은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;어떤 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;을
추가로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import&lt;/code&gt;할지&amp;quot;를 파일 기반으로 결정&lt;/strong&gt;하는
것뿐이다. 거기에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnClass&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt; 같은 조건부 필터가 걸려 있어
쓸모없는 설정은 걸러진다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-autoconfigurationimportselector-자동구성을-끌어오는-단일-진입점&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2)
AutoConfigurationImportSelector: 자동구성을 끌어오는 단일 진입점&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AutoConfigurationImportSelector&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DeferredImportSelector&lt;/code&gt;를 구현한 클래스다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DeferredImportSelector&lt;/code&gt;는 이름 그대로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;지연
import&lt;/strong&gt;를 뜻한다. Spring은 일반 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;을
먼저 다 처리하고 나서 마지막에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DeferredImportSelector&lt;/code&gt;를
돌린다. 이 순서가 자동구성 설계의 핵심이다 — 사용자
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;이 빈을 등록한 뒤에 자동구성이 돌아야
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;이 &amp;quot;사용자 정의가 있는지&amp;quot; 확인할
수 있기 때문이다.&lt;/p&gt;
&lt;h3 id=&quot;2-1-selectimports의-네-단계&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-1) selectImports의 네 단계&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;getAutoConfigurationEntry&lt;/code&gt;가 아래 네 단계를 호출한다.&lt;/p&gt;
&lt;ol type=&quot;1&quot; style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;getCandidateConfigurations&lt;/code&gt;&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ImportCandidates.load(AutoConfiguration.class, classLoader)&lt;/code&gt;
호출. 클래스패스의 모든 JAR에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports&lt;/code&gt;
파일을 찾아 한 줄씩 읽는다. 이 파일에 나열된 클래스 이름이 곧 자동구성
후보다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;removeDuplicates&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;getExclusions&lt;/code&gt;&lt;/strong&gt;: 중복 제거, 사용자가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;exclude&lt;/code&gt;로 지정한 것 제거&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;filter&lt;/code&gt;&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AutoConfigurationImportFilter&lt;/code&gt; 구현체들이 조건부 필터링을
수행한다. 대표적으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OnClassCondition&lt;/code&gt;이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnClass&lt;/code&gt;를 검사해 클래스패스에 없는 자동구성을
빠르게 제거한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;fireAutoConfigurationImportEvents&lt;/code&gt;&lt;/strong&gt;:
필터링이 끝난 결과를 이벤트로 발행한다. 조건 평가 리포트를 만드는
리스너가 여기서 정보를 수집한다&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;2-2-시퀀스-다이어그램&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-2) 시퀀스 다이어그램&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;30_spring-boot-autoconfiguration-01&quot; src=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA/YAAAJJCAMAAAA3LVxyAAAAYFBMVEUAAAAJCQkXFxcYGBgmJiYsLCwzMzM8PDxCQkJISEhRUVFZWVlnZ2dsbGx2dnZ+fn6Hh4eMjIyWlpacnJympqavr6+ysrK7u7vFxcXMzMzW1tbe3t7j4+Pv7+/29vb///9Y4TNkAAAAKnRFWHRjb3B5bGVmdABHZW5lcmF0ZWQgYnkgaHR0cHM6Ly9wbGFudHVtbC5jb212zsofAAACM2lUWHRwbGFudHVtbAABAAAAeJx1U0+L00AUv8+nePSUIlt2q3joQdqNLRSkrtSDYDyMybQ7mMyEyWRZb3XtwcUFF2RxwQa6ILiKhxY9rOAnaqbfwZnEtM0Wb+G933u/Py/TjCQWMg58FL2iLMQCBxBwxt1DwQMCUsRko+ORAY592eFM9rBuVw4EkYR5WHgVhDRGUpeGmEmoNPuhoGy4z7l0WCsMfepiSTmrAI6gdXBwC91m+KVPWrHkDrM5G9BhLNb4dq9VxhtgCeawbhByIfvEJ67kIhvrtx+Vx3KMjZlHPSxJ5DCrRrNaBMuzM5X8qWaDXft/dAVPh/qS5Cyd7i2WDbd6SJJjmeHsp88Q0s5h54Ex1IBmvsoqNFeRLpuu1t2AKKvmkMiqIl1c9YZEbiXQZlK8XuO6dgN8jj1rC1hzfRxFVdS1YadY2Hli9yD9/i29SsAyN4O7sLiZqS8j2Lu3qz79gMVsUtYgSMCPyMM490oiuAPk2PXjSFNEBVJn04BBlpXlrmKvIsYlAUGHh1LHoNV51CjD/mNmG3GQfpw7TE0v1PQNqKvZYv4L1Hi6vBgvxzOkl66VpyfXKhltGiipHFBBthLIQ20fESb/KS1O0pfmp33+AixNl/4cp+eTbPXqNPqIueL1NiPW0G56UokW/H6ipufmc/l57LCyzX2CWe4yPblMr2/U11FhdPnhdDEbIc20Ikx/vwOTRnIJ6m2ixnPU1K/OvFq0V6vv1u/X6n8BTytyOCYFaLoAAF9uSURBVHja7J0HY+K60rDH3aaHkk2ye+/7/3/Wd8/Z3YTQDe7lk9ywwUAggI09zzkbXCR5NPZYxZKG+T9AEKResEULgCDIvUGzR5DagWaPILUDzR5Bagcf/PVU/8bXYWTpRIjbyxCLAh3mRBDDuI8op5Hlm1+iFJll2tsC6IYCfeHe1wIm6MlfNm5e6htM43iAO8gQ4emd4wE0//bG9kUMaN74CuXIrKd17yLQyXtfD0JT829vcbL9NVHukedT1QqnDIYQIju3vkI5Mpu6+TcV6OS9rwf3a9tj5Qo5AprjPcEuPQSpHWj2CFI70OwRpHag2SNI7UCzR5DawR8+5TssV7R4h1n77cxvUfgOk69Da+Mqt/7mXs6cExaScmDnXkJUXv3fYldn3sqyOalLKgHa3AcY7Y6t+/Sfc1IZW8BI7VPj8M5gKwZJmvBz7/OfHpm7njF75134cVX9nEjQntHhCPxLzplxQ2bytbXPV8PdlLF/juoO55yyBmWbKbpzId9Tfzn0WkZ2zN6dcG3RWX8OOVgKPdD2wov5yXAt07De9o9vnC5cQEoM4GgKX/3or4FtC1cQ4GCC2fSsMfMke4aeE9GEDg8nhyhFCYqnw5WHQOIjOY8RD0b+Osfv5yn1P5Ze70nW7P1PsU8MTZqungy3K+ao7cBNY9vtseXutwkM+xKrS4lBkm6cEXPD+FrmipcJcDjBbHorGErACXnNDA+4g9raE/B7Mt6ZQOIjOY/pHoz8dY7fz1Pqfyy93pOs2W/YPlgWyL33DgtmaG9zX1Jd+Ymd+6La6M79PjmirNwmUam/0JiW3gjVzpL//JXmCl05+V0Z/ju14DNJiRG/SZJrbnSTaQXjqjeBWHTLX+h+s0c2TPdpuaH3esr1wBt3GqEAsTjnEyeYn55nSHHDJj4Uy6mu4YMbUW0lSorTCBUZ5SNMMAgXC5lktQDmvrxy2+2ZIfTE5M6nMkckZ6jEvW3O4xsSix3kN0yrv93JZDe+X/bKiO7lCfXn6z9H/bEUofoho39kS6Yn31/1YDmzVyYnm6Kwmbj0oKOvFFFf09+GDI5Dj8xESSVt7vlGeTLtMNTaaDOwUKUnmFjJr8Sz7XMK630xaKeN4/jba1p8X1rRS9qhWJS53mmt6eaGaSgenb9FxfQdLxIgFud84gTz03Mg6c5IDkVykgey1QyiJUqK0wgVGeUjTJCeSoRMsloAjr5oiKux27HX2zufyhyRPJA4lfP4hsRiLzaNXjCFLshUvJPJbny/5na/KxwT5/j9zFF/LEWo/qz+kS2Z0t7hRGvzwpIWqeDAaGa892lfDPPMwYdBXsqj+Bb5Q9nVDdHVlD4o/9Ij9jtIHXA39MAftRf9DiTev6A3NSMGOO8A/UZ8TSDVfkE3mimxwNVaLTD0Fvi6wjQ3WqpYDwSIxRqcLclegtn0nER920tEcoqC3gw6JFJKSqCKjPKRaCglZJzVQhiK5ic3YCxze+dTolHJqcTa9sFJbkgoNk+sDJTf4Tkv2UlnN7lfDi8f7bQ5cT+VHPVHUnRi9efqH8maPQ+aEpT/xObY4WYxJfYGPKloixsPhOTFLMpByyn1tuWfjPVkGBxgeTv+vVCmjBjAj4I6SXRN8iiYbjBvIxaLyqGZ4LC0V190WFb3d56ly8XJTzBJjwVv7xKxnJnAWQJFJvnYF3I3iXsiiCRXDSbsQ41UvBUteQS2Od9mJBTbph1Csb62O+nsJvertXpvt85Wf6KqZo76v6L/GLMMCw1sObkixRXJmL3LgSPEWwBN8WNzrIqeuhuMJAkznQ0OMZ4f/V4oU1YMJnMX/YndkHdulw+SHEizgQXd1xu75y8VJz/BJD0erN1Dx5R0NB/f1NktyREtlfOdjPjpdmOykwmV3K8Ory7MI3WwE/fzK+o/8g1I84+9cu6P4Z7fHr6UTNued4BzwbJcCL+aCHw829tic4bziawJ2298HNg80La4zce/cNl8yl0xMroxu73tV+BQLAH8ZrPZANdsv76+/iBPS3Dd8LuOT5/RRJzzSCWYmx7Pa1EV4vAlUkpK0sjkwz+VQrEQFe+KRlWQ5Dx7Q0g1nH61dHd3MtmN7xdA40dDP/yiO3U/v6L+nYc0TTkWGthyekWK65HRFKklSfO5IeuOLetWg9OCj6TWqqHZuR2hsvbBOsH71DNcFRRe1teiCq34N2jPSWdPtE+LQZKmrczE/DmwOBWcjFhcQ1s2fLupQYt+55cNlxM13VLpOSrAVpwz2SZ4IL3e5LMr+db6x5FLJEpK0sjkI9bQxULekEjFO6IFEic5396QEEbacOxqdyeT3fh+wVwWXPZIcXzqfn5F/amHFEnImj2rNRhjIKjeiPHXKrBNqkhOX4GS+5nlibW4zifVqDthpI4IT/MFMF0l+YWGPpFGZ8uUEoMk/UkOJTVBsbHZNJk13yIPQCLWE6gqSM2NFLQHmobWbppTbjCFSIBEnDPZJnggPXk0nxN5SaPs8CUSJSVpZPKRaOhSIW9AbCPxnc+KFkic5Hx7QyI60yXTm+/sZLMb3S8wNyA8HTbIk/fzK+pPPaRIQriW3iIqzM3ZcFutdvxg+9N/dpnDM3a0WT/dJPE8PvMLyet8c2IRs+X2XZ0RYxePYWinH/lJieW73M6NTcYOBQIk4gSsT3wUX+V8fjiUnufwbDbHh5WUGc8U5SOlodwUTsn6bVYHv7Wk7nxWtFDiOOdJRmKc9L2Id7LZje6X7yUaUXvHBTqg/y+pP/2QbvW5KtuQ/Zvf6i1ZXUmdz6bEu4yYPXWoX3npSI7GZd6xcR9A0hdwSZ90Roxd2FA0NpP0/nwMLrPBfnum4aH02FjIQ5dIKSmjiygfqaPfF/LqxLJlReMyOU8yEpO7k81udL+Yrz8dB/R/Uv15DymyYy5NabN2uEba3o58PxacDaPcYgXiHDEelpsp6eZUQv0PrP8bsltK8nsVjSM1j8bNvjjw1RlNfTsl3Zpq3IPH1f8NOa9euVbPCn4pvn1gmWhrPtncRYJr5CEzHvROmrsi1nWGELjr1I5pgnmHJXKvp+yLUjr4+JaH3H6Qg7Oc6eT2Q3OYrza3uaxz2M+aj767WoFe8GogZ2NOXoPfS9Xoh289zl20yAO1MBmpy8OGkczV8KsV7rMUHq/LQOU9W9nXfOCPL0RQEnLNfm+Wc5qcFt9V54w//Bz24Or5qxXshysr/jxyUvRVNe7mRg8G2EEniP/JDxl1/ErtvaVurj02Ibx0tC6DmDn4Ra74wB95fHNFKuYpyDX7vVnOaXJOXHXO+MPPYadXP7Bawb6UZUVzouEWX5VxNzcN0WyCzom0ELbdEQ+9vxat/LCtVfPK/WvhpaN1GbqZg1/kig/8kcc3V6RinoI8s49nOSfzrZMNSjw3nOs0bzFn/LI57OFUcP7mc9i/Oh+9Fa9WkCwFkNoOtedmlwIIZ+Hf/wE4gKZwcY77X851Pz2B3pk3QVWCVx8HRgus6BtcSzXO+Jr25UtvIwSb2Yn9J5R7xQd++/juncqIZE/lHvgTkblsRYrvktell0wyj+dbJxvB7Qznhg/a3E3mjF82hz2cCn77OexfnY+erFaQLAWQ2g61t7MUQDgLvyz4ZlwehAssfC3XmQn0HPgQjMaZTtnu4mM8bfPR8XMmvn350pCSF2BnYv8J5V7xgd8+vnunMiIJylqDpdW6bEWKb5NT2m9nOSdT2rdz2wPcDR+8oG4xZ/zSOex0Krh0hznsX5uPHq9WkEwth+2yALH2dpcCGAnfkeu6uDsPxhdznZ5AzxKjD8ye3JV2w/BIaQ3B0Dz+vCUvvnhpsP8N1mWIyE7sP67caz7wyeO7fyorUsecw/qJ4y5akeLb5Jj9dpZzNN+a3W6EOCBH+rr+nPEL57AHU8HvMIf9i/PR49UKkqnlsJ1mHmtvR0KhRFZP7kGmGvjVXKcn0AdmT1OhB0wjnCQbtO7P+jL41UsD95SasRVrN9b+UeVe84FPHt8Dz14iEtP/mMmFDQ/OMfsDs5xT+NHSCbeYM/6Ac9jzLxOsVpBMLYftNHM/JX9ZZ9mzcLJIzhM9NYHe9RjLAV0LK1hCaCsbagXut+tcuVpj5ZxwW+0f4ZoPfPL4HrizGZGKm2W9f2XXpC9sL15hI5lpn5pyL4BB+0cM86npLsLMRImZTfDtb60SwvNaWziRmsiardw57DeQ5xThfPTMZaKr09UK6NTyrdbC7Uh78dTxm0t4CRxzfMTJoVw3GjMtLCNXGrNiRIsLleG5SRjf/d7TfljhWXzIaP8wV33gk8c3786mRfKnvLimfkMK8fC9fwu2s5y3U9p3ptxzDW3acpnbzBl/oDnsh+ejG9FqBcnUctguCxBpT/nWUgA3hqFrPsyMH3kNoyOz8FMT6J+eotDBXzO0KVmk1dzLp8UcufRu0MzE/qNc94FPHt/9UxmRVs4PzpqL3EUrUnybfbPfznLezrTfnXLf83WdaSs3mTP+AHPYT89Hb8arFcRTywNpou1Ye99ZCuDWtMeG7HmZkugLs/APT6BnkywupfMr+V+49G6UzMT+o1z3gU8e3/1TaZFM9YmHwcf0+aIVKb5Ndr59lmS+dc6U+7Cq9sU546F2vz7fvug57F+ciX10Pnq0WkF2KYB4O6ro5iwFcLas3+ZAZrXVj/xC6GiuUxPoI6zxL/J3E4/MfzaWz7nzQE7Ntz956T3SE/vz9Ll/nSs98MlCBHunckVKkipsvv0e3N5GRDhh+kZzxh9nDvvh+eiJYtNLAcTbDL+NXcJZ9pSGc7gNfjjX+xPoRWr10ExMzB5+K7/HFgDIC8qc2ZFwjQc+WYiAzU19V6QiVkk+phVxb6MslGMOdenUclUOVc6+mevON+LeWuHlfeCvzTGz7+5tlIVyzKEunVqqnutbX7q8D/y1KWcNE0GQG4JmjyC1A80eQWoHmn0OhQycqnlm0wOWa5XxQgjN/g6Dwo1TX1LuNzDdO/UJQCiPT0Tj5hN0ypHZ1M2/qUCpe1+OjG+5/a3eEg7X8dRbv/qYk/48PfVO718GTn76M0rzRMi3n4Nfiswy7W290zRu9iBk7n0pMr7lDrd6q4f/uyyeWY7ZIyUR48FEq1JWi1dz8RKcz6Vte7NowUslxoOJVqWsFq/m4iU4H+zSQ5DagWaPILUDzR5BagfXuzBicQsClVGMBxOtSlktXs3FS3A2l/bkIwjysGAlH0FqB5o9gtQONHsEqR04XKeGolUpq8WruXgJzgfNvoaiVSmrxau5eAnOByv5CFI70OwRpHag2SNI7cBRenUUrUpZLV7NxUtwNjhKD0FqR0GV/L3lU5yTjpVrvfYZglyTiyv5W0zdCPy9Lb0vLwZmT5vgrheGmyxM8u6edP849++42BiCVJhvl/be+8wF9S8prNf6V8Jb1PPxWCaGvpa89amAaZqT4z7XEQT5Gt8druP9VV573efm+KsRZyuAFXRAd4a959cTATNIyuygGCWkxKJVKavFq7l4Cc7n0l7IeN3ARaPrbVyho2uBW7qx6G24oblkek2y7WpMrwXuzAC5z5F9R3tTTet3T23R940eOBh0Jyb3FLq0izZJeI//OacB5XRUvvPucgfEKCElFq1KWS1ezcVLcD7frOR7Ws/5a3kraIWLDztLb+h/rAfCgm7DQJj6MLaHA3sc7I+4tiCPBEegZfdiTCvtH/ASl+LR5tjovvWBBpQzUckrqlwrHCPIg/JNs7dkZtEYKACCHR5QhorMvCgNxyeN8UGjD7pldZRGm7bUyTmGZ1nRB9o399wz/mzAtGVfdoPI0aZptTqCAjSgm4kKHIONewS5At8camALYAyCLW6bHseEro3IK0UAK/AWLoEjpq4VfIvrNj8nsgvrDXln0PdAtAmQuAlwdqMW7dEeQSrBpWYftWcYD3wWLB/s/AaODRwHtkiMP9Uq56kh09/+uy5AtxUd5sJNE0wlOZKJ6vr8ATHKSIlFq1JWi1dz8RKcz6WV/CivpHBWlkvN2SxzPPkYlrdkGhKv2vaaj5XDmx4nkiLdWDu+BrIkLg2wg7E60aYkrDee7tOAO1FtkA+IUUZKLFqVslq8mouX4Hy+WckXLbu/8l90rZczlIb58JghB8+ff0B4jg+2jX8G3WmbdaYk9pCH0eQD2GFQvkebz58T4N4YGjAbddEhLylPV3D6EIJ8i++Oyd+snrkDp/7I/eiDm5epVJCDC3dAWu5MeNb3khSizSg8jZ2KutkELwBzKrROjuhDEOQw3x2cK8LE82w9r6Kj8nG5zGS64shB2SER2Pjs9p0QbUbh2WxU8ynY5kVVU10Je/cQ5FIuNXszbh1IDc+0udz2jXR4DP0l7aHY0nnJYE1V54S0GOWjxKJVKavFq7l4Cc7nu6P0SAqtA0Hat5JZGkx5x54yjS5b4hFSJRatSlktXs3FS3A+l5r9anVhxOvgkX/+RhsUKgSCPCiXmn2nU6DQ5sSn/QDtBmt9PzEEqR0P2C6hVs/4SlssWg4EeVC+OUqvCMxP4EhBX7QYpyixaFXKavFqLl6C83m8tfTMqYQFPYJ8h4er5HvOC47SQ5Bv8XBmzzaLlgBBHh0sORGkdqDryxqKVqWsFq/m4iU4HzT7GopWpawWr+biJTgfrOQjSO1As0eQ2oFmjyC1Az3e1lG0KmW1eDUXL8HZPN4ova8QL9zzbXxcrBepIFdwfVlC8h1pnuGbM8KeWuhvE6keNWnbB340z/DNGTGW0d8mUkFqYvazr68Kkgq6gk6ev00EeXC+v6hWoSRi6EuTe+EPOdJsUD+awWD+r/nmbIbJURedOf42H0pD9wAX1Xo0qjJKbyL8HHIHHWlSP5pha/9LvjmVMDmHuui82N9mSTR0D3CU3qNRlUo+Y3kyc9CRJvWjGeX0K7452TA5l7roRH+bSPWoitmPvL9jj3rPnC5C57vRprvrQGvfN2fsYDPd4gmTC1104ic8pGo84FCDXKSf2mTV44450swl1zdnlFybuujM8beJIA/ON11fFk0shr92ZdaDg440edPLiX3IN2eUXOCiM8ff5kNp6B7gWnqPxgMuoZkrxnIKchcOOtKkfjT33Xgc9M3ZDJOjLjoDf5tfJOOWsyQaugdo9o9GZQbnepG3vEOONHM+wx3xzRklt3DlzTN8HVzeE3kIKjM4N/aReciRZk6ZfcQ3Z3REduDpnB49XlxuNgyPnYBIuamM2V+CdHK8vXSmZ11e0j1zbfNXmgmEIDfh2x5vi+VbYpy2+vMhdu+Do5Ei3yqHhu4Berx9NB58cO5n0QLk47uLpVIODd0DHJz7aDzgmypNoR44cwnccgLbbq6LlgRBDvHgZl86AquXO9idj5QZNPurYn7Sgr4qI56RqlKV4TrlwJxuC/qSiXZLcLjOo1GZ4TplIDNKD0FKC1byrwi65UQeAyydEKR2oNkjSO2oyqJa5aPEolUpq8WruXgJzgfNvoaiVSmrxau5eAnOByv5CFI70OwRpHag2SNI7UCPtw8mmulyN1zF40JXn+jx9sHAUXpFYW1ah+f7+yvL4/nm3gPl/QEYmcLRxYBDVjmBVnRxUflIZHvOy42iFYPcHqzk353It+Zytcw5GmL/WXIKqNZeXM379UvSTvjpCZLKC6Rqrut62XAZ0NVnTXjACsqjM+OH5K+nERtmd4/GO+4rrQn4e3GDJXtevnKB3EDyYF+QFCvogDI7Z81Q5DGp9Vp6d8L5nG2seQfcz9maF+aas+ZEUI2BRtfmf/clUOet8Kg7na0smXXm3WB0PwPRARg7+mQj8EvV3YA0duQ4ySg2jO31pMku5zNN4MOkSKDd2KDyQQ0+2o3CkZi6RSr+U0OZtGTgF22sAVYeHK5ze9HGzmhgObEzzsgN57rZ4ugCPA5pb7tOdDRywrl1yZF45VxYA2YBLYUfNWmUOMkodui1E6D7xi6jpOiZndik8bBeb5LdKByJKaseuGvlYlefOFzn0UCzv7lopt1VpHbijDP0rWnabWilGtfB0dgJpxvflq1XTulHo2EBxzEil0pyC/XaCd0GL5uQ+PncjU0MXtN0iHejcCRmG9agcsrFrj7R7B8NbNvfHBfE6JcUtYIddt+roKrkQD8TMnbCyVPne+kDIv1l9pPcEtxHdemzOcltYyth2z6TGInJKeuOSlclRFefNQHN/uZI1LWmnTjjDPC0JrE9bx0634jL19gJp8CojcyBQ0lCOjaYs0FrtdiGOhR7n/Z46rWAR1efdQHv8c3hxJlt64EzTl62WY43PXYDfeple6w1ecNeb7jA4yYr8aoI1AlndzHpcabeSA4cSBLi2AEucKbm+wxNiu7vx/bIK4JJXgNxOFIL4NYNDr7n6hN5ICri8baEbEUbyTrfIcX6iPv4f+8WtP1/1mqwzKbCqdC1/1i0lU6PwrP354/3DNB90n7/b2wzyQFKuvIdJxnHDmjI4w+F+RsmRdmLrf3+/Xuc7CbhAFpAU+muPDjH1WcZ7kLxT0DxEpwPjtK7D+/+KyTOOLNuOOMiNziaOOF0/LATwDv4Zg6TTH/7pzvB8NrkAt7x93oSbmr8pD/nuvpEHhSs5N+eiScY1ohuRRXsbGs7NkwuvZPcGPZEkuxOUkzmAidK7uTtsAmHb/RW3qhobSF3AIfr3B4WHPHpC8Poi0vSYdthE+JcV5/IY4KVfASpHQ/u+rIkYuSKdoZbzl9FC/vNrKLrywcDzf5mon3dlv8tWtbvZhXN/sHAaRcIUjvQ7BGkdqDZI0jtwFF6NRStSlktXs3FS3A+aPY1FK1KWS1ezcVLcD5YyUeQ2oFmfw+8jff9RBDkWuCY/NtjrY0Bvl6REoGLat1YNG/9PtaeHrH9d3ZWa3ftskhwPmj2NxXNmv0l9fvBdefhlA40+0cDK/m3w9PWPuvblbd65OFAs78ZxgToQlcA06IlQZAsaPY3Q35baj5d92JY7ZY98nigx9vbicYoHdH2AHSpxFJeJ6s1vXZZJDgbXGbjxnikyGewvEdKBS6qdWNoke+sK1/eIw8FPo63R5Y9XcDxOkh5QLO/B2yzaAkQJAUO16mhaFXKavFqLl6C80Gzr6FoVcpq8WouXoLzwSYngtQONHsEqR1o9ghSO3CUXh1Fq1JWi1dz8RKcDY7SQ5DagZX82+O4uYc9m/71L0/XNL8R+aR4V0kbKSc4OPf2vLu5E+43Y6J7e9oEsFY8dzC2v1LXps3vvZ+935oma55w+vqr44EOiDf3v5A08phgaV8sY5n8Wa6W6WOWldqx/yw5BVRrL6bm/folacbx5IOkTgXKpzlxilYOcivQ9WWhoq2gQ1fhITacev/O+GFqx32lpe5+lduit+7lRPpBUqcC5SMps+drZvVGFP8EFC/B+aDZ30U0d2aA3OdgqVlCnxTw7sTkqTWrLfJnDYPppk1q2802qJuXuWn97jWjGI7RDeraTJLEWPLWXF9erv3f7c5Y6jpTk5eMn0l0GIuO9rYJLhQmRQLtxo6F8vifdCsSS1+a3Asf/3TeXe6CrBap5ppKcD5Yyb8LY3s4sMdko/vG0gr92H3u2wCOQ2163Wxxawj71lwH2oI8UuIYNsg7STgLa8AsoKXwoyaNMnZGA8vZRgdnCSMuvFCYFD2xEztM0ei+9cPNUKyJ8HPIJT88XNQ4QB4ANPt7YFkdpdEmDe1ug5fNYF+WO3SlPWL2pt2GVqo9z7OsyMYx3PgOJUmA9KPRsIDjGJEWxqbdVaR25mrKUGHCCwVJ5cWmmFarI4S9eZFYjOXJTPLDMdi4ryoPONTgAXFApL7SHFFd+my8H0Da7Cqoqgvrfm4MnkTKJkF/mW1AN0kqgd7T6EJ7AqRiu0lFIg49mvxVhmz8k7kMUinQ9eU9RONIZR1ICW3Oev+hJbNA7JBaPDFq8LSmKCriOuy0c3ZiiIyaPbB/GXLcjvfi6PGFdgWAzDFzJ7T0c2Sskh/X/2qZgK4vH41LS/uS5LUkYpwSTeJVEda8pBHL13yfEbgV58yJ5Yk2bKBP3r3CWGvyhr3e0Ea16bFxDOguJj3O1BvJgR04cWbbOt2Ko1Pc6EI0qbQASbS5N5CEtaCYtDofhYaNIrMe+OFPqluhzHeh+CegeAnOB9v2d+HZ+/PHe4aGPP5QmL8AT9b7gpav3ZWnNuk9UDgVuvYfix5s+/+s4xjQfdJ+/29sM8kBSrr2PZJ1vkMPxNEp8YWCpFICJLENjRzjJv9M/VTo5b//CF2IfxYdfDiqCo7JvxNe+IYlha8f2F30cWzhDtKBIkMLTnrxO9mJxst5B1/S7/5rOnr6QslHuLzYybEotMcEL5TgZ7M59tne0xV8KTwuaPYFs+p8M4GJJxjWqHF1wdTW0R49cyJ2HrF6i1AuHZNvluMTQEnE+IZo3zYdFhzx6QZe9qTj/fi8tNLWvsCckdWbUPwTULwE54Oj9B5dNPmrHW9XRhpNvNVKokU+jtJ7NLCBhlyINCRFvfn5Z+UVLQlyLg9YQXkU/i1agPvgr1R05P1ooNnfjF9FC3Br9Cl5fnym1VgXLQlyJjhcp4aiXQd9ynCu2BJxuM7jgR/wkMsgZT3famDf0EOClXzkIsx5oyV+PxmkENDskUvwnBcs6B8XNHvkEtCH70ODri9rKFqVslq8mouX4HzQ7GsoWpWyWryai5fgfLCBhiC1A80eQWoHmj2C1A70eFtH0aqU1eLVXLwEZ4Oj9BCkdmAl/9G4zBOtd76ri70ouo9ecCvCA1ZQHpgV9SktK+CpJohtusqdv7I8nm/yK4aufrkSgzUzrHkYvNUEMGbhdjtaHdOeP5NUeOXYjdM1m21nZ8Oas5+J/0z2KTroGMZTuNSetemSAsBUvSYdhmPakkCuNEnPIVT15/HPlXz9xbuQAkCzvycqna7lgfPhN5mN+kMEe+y2JFsVeNURiMWvW4HZe1bg+3JFl78XQzeYi9gJ/bhNUuFW8HP/zlmxpwzDb+rjH0FS/v/o337wziAx3GVDTtbddX9zbjcwe//TabNgfjSlideG2UaaDbf2bX2SP+Gbovnxhg9MFcBFte4qmhyskztzifV0/kxf0/5sp2/bxevYoKje0D/WNDBMN1o6K3CRKw+sv+rTXuKJp1xyqvU/I4gR9N38DlfeVcCf8NZTcsu5X857uDUNFshZSkS6RdtW3wR15unxi0b8AfDBBXsHvODiolqPBpr9/UVz9AZRO9tQTS7xZwuSOd+6wwrN0aZltA/hitpRJ0zgIpd6tBG3bnSj39C9bZSCDUKSmhv509rouvy2fhcazSgxLnKis9GeP8iVdCJAY2E4vACNmSDFTQ3yjPiuGDYQ8r3gotk/GlhnuyuOAYxkQ1CYK6rtbR3PNDm1GT8/0hvAn5EQ+K5i+RkYIgtS8KU1dJHrrDYSse+xP/QX49f4t63zveh2rg2js50rM2+FZm5yrzy02/pmZxqNM+0HqdKHgQPHFanbSxBSS+StRAb0fyDwgotzcCoAmv1dMafAvbhhSSyCm/6SMjCmb+GWGvi2WpFTfN/xu6St3ia3yaJFdhjVtt029WI7UEhJb0H0K7JsPAHecTw7KZY32s/w14PIw/UqO1jjs9GiVXiPVjpY8DiL+r/zjK0PXntFqvbK8/9DL7hVAc3+rjRpjZ0UmbRcd4GUoU6yVAU7+IysUqHHusExMOgroEH/cGEbgHYDKAPz3R3FXmwh8Wab0AP333nU0F/PnsMXQODy+rMn7H613Vjw4cOENDc8avuc6Di8znlaUto74+a2HotecKsArqV3f9FERqdGvSFBGHXbY95oLEOb4qm/WUqrAy1BD7fD5fBjv9cSeRNwYIuhF9v4NwUnhYW1PXWeo4aE4Koj8soQ9sQhryLHVHietRpggii03yV92Gian+F5/bOT1A7yveDiWnqPBpr9/UVje/NVGzarHrv1Z0uPD4yogG2G74I5rXozoTmv/cB4qYtcUiTbtiFuvdgmv5F7WzB5zjDDNjgrd5Py2SNxn9JvB88j/7MC/Ua/bJJm/7rNLmUB+i37KfVcCKPtEIB8L7ho9o8GVvLvwY6jyA4znxPjJkV+l1lsgJFDyyTV/PB86IAyrE+LYd09bmh3p20WNI1rkMjPn39AeN7+to1/BkFH/8JgmGbYJsjMufDU4KsgF9Uw3H8B3uG/yWuhZ//L8D+214zgU88IesGtBjgm//ZYqjHcLRJsX1NBoZ/vYn+2+8yY7bf5aTy0LuMiN/ZYG//G3Xiuu7u6pbF4ATtaz55vwwEydXhz9po65bjSvy/mUS+4yMOAZn9jPE11YZRXEfTX2uiCsvPbLnK/wQkvuMijcKnZl2SMQknEOCgaKejBZ4bllfJ6Wa3htcsiwfngKL3biUYKes8nb9aqWz2a/cNxsX/7cuS1JGLkiuaPjWCiatf5BGlV5X9mgSP3in8CipfgfC6t5BfZwiyfGPmi1aa0L/IuFP8EFC/B+eD3mNvBtl5HCjD+5BGXVEaqDK6ld1PRuEaLsz1NKrGY18pqHa9dFgnOBj/g3Z687/YIUiAP+KZ6OMSBpwvYmkLKA5r9PUBHkUipwEIIQWoHur6soWhVymrxai5egvNBs6+haFXKavFqLl6C88FKPoLUDjR7BKkdaPYIUjtwlF4dRatSVotXc/ESnA2O0nscHIb7fiI3TRB5EC4u7ZEbsdIMw84tQd5dBVaecHaKB6EJ7lz8mskjpeUBKygVRwXRddzWIP+sJu1aauLw8hrsJ49UETT70kHdY85Uvpt78mXvSOLwchfLap197ZezYyCPCC6qdXvRxqKjvTETk3tqkG1vww3NJdNrxi4rxyJpaE34nhuGCOgbahfem21QNy8kjqsxvdCGx1KXxvP4n0vNEvoyBA4v5SiqvjS5l/iWWtrW7MO07c9WBzarH2yYQnAmvkgYNUo+8Kg5lrw1R66QSbR0d6H4J6B4Cc4HR+ndXjRnCSPuA16UGd32hv7HeiAsiAnbw4E9BlElhzcKRCFCGq4Hjku91QbxB8LUD9Mix8ZG960P0H1jlwBtQR7JcdSJ8HOY20sXBhCac8OhC+1HKcQJ0ouEUYPkQ7HAWVgDZnEk0VLcheKfgOIlOB/8bn8HlKFi2bIvu3awLTMvSsPxLaujNNqkKu5tQBUkMw4RwIK/jd8cNPqgx3um1eoICnQbvEyeOJ5lRSeOylhe5GrDNgzbNQwjihIF6Cqfn3IrTiFDEjUWC0D60WhY6USRqoBt+zvAUz+X6w0IthCuucNQjzex68qGvG6ue9sQAXb62xp5Nwtgxd7y3NAhlbr02eRIFHU0+asMg6MbHTx3DvCaDdD/bf1KUsiQRHUST5pi6JhnmyhSFdDs7wMH3dbeochlZWuy9FvZEN4mKoxjv9L21rUlB6ZCfdYMWqvFTuLST20SerHu9WCtPe9dfSrai0GUwhYnHZXb9aS5TRSpCpe+xUvSjVESMU6LJolLA2w3c4hXbZu6rGywiyazDeE59uYv06eeb+35hoY0LG/JJK5xJWG98XQXOFPzSUuAN704qr92ZdbLkyUKsLSeB+t1lELYioguso2aiBVxMNFy3IXin4DiJTgf9Hh7J9FGkw9gh5lCNnZZyTTVdiqEpoGg9Mj7uDv5I7c1cob58JhUr9rz5wS4V3nMdK2/b4HDyzjqcgpy7oe/MAC7+ME1zakoBim8BQ32+CLbqLFYFBrkYKKluAvFPwHFS3A+ODj3bvjeXn+4l61t7YUIHVf/kfsutx/Po91+1C7pySiqxxzqfNtJO3XlyDt2KuqOWIcTRR4UbNvfjZwB8OyJEMl5Luc4G7nCDk5GUQ+32XbSZvc22dyTxxNFHhQ0+/LTxpuEXBccrlN+0dplHyePw3UeDRycezPRPr8e9lfRwn4zqzg498HA+uPN+Lot/1u0qEjNwO4aBKkdaPYIUjtwlF4NRatSVotXc/ESnA+afQ1Fq1JWi1dz8RKcD1byEaR2oNkjSO1As78H3sb7fiIIci1wlN7tRbNm73ylX684Su/RQLO/sWje+n2sPT1it8/ZWa3dtcsiwfngKL2bYq113oVB2QfVIzUDzf52eNraZ3wbrR4pG2j2N8OYRBvToiVBkCy4qNbNRJPe1DVdr44ZlljK62S1ptcuiwTng4tq3RRzadXB7pEHAz3e3hS+2QLb1yRsSyFlAs3+xjByR3LWaPdImcDH8fZIz54uVHq8DvJg4HCde4jGNitt9Thc59FAs6+haFXKavFqLl6C86l0KYQgSB5o9ghSO9DsEaR2XPwBrySfAEoixoOJVqWsFq/m4iU4GxylhyC14wHfVAiyy6poAU5hDLjvJ3I90OyRKtApWoATeJNhmeweu/QQ5Pawo4lbtAxpcS6MV5IxCiUR48FEq1JWH0XN5bJ7NPsailalrD6Mmktl91jJR5C7UCa7R7NHkPtQIrtHs0fqh2d/LZztfuf0HuWxe1xLr4aiVSmrO9f2LdMXZYZsearl8kIz9wHfzP4PVnwj3Ek29vmU+yShlQViO/35LU46OH0AYx7+NtOfFtnRZzm+46HZ11C0KmU1e23305dZdTHiwf7wWpKt8gcfcE1qgEmj040j2GO/yWzU5+2FTiZN8bzwlZANVBa7x+E6SHXw/ra75Gf1+Qoz71Ugm/7BsC/k35yXwo0jzN03Htp/Z6/JkZNJBzAZ7whMPI5Qmj4XrSVAs0eqxKLZdTWX72iaYHSpaQIDzswEuc/BWPTXXF8GZ2ry9NRY6s5N63evSTb2Ai01S3gK7NbRG8RGuIZqyNFJO0maEoXUlyb3Qwj/hrLYf4KfRjjXrZ3IWIpxxGj298AEkTwkDpNbv/OZwxE9V8jdPiuR2uBqb867wqittpHU/v1PfwCL8SuxX3mwXLzApz9iZuSEQ8pwne/ydGMvEHSF6Sowewtk+qOothydtLMNiyCkP2kMHS78Gx5WfoW/5bwvl5q9WY6ma0nEOC6aR977DUWBcW4PkD0/UuvTpv9HygdBSbYPMZePNlDvldVir23J7Kz5pJvAr0WIzM+y+g1wZySY9MzYS7I/UKA9C87xLCvlBoIueJIanPIgeNmK4EJ00oP02zsMyTCWS14P4V/KfB0HEE40IgoBzf72omnef+E9aupZ5AHKMm6fSkmT9pzo7SXT/HgrruJWFrO3BTCCNyvDkUI81JBDy+bAaEmFixS99t4NyAkE6tKPPm3zYFBLdqn1hyeTpAOikKPJuzLgwr/08NMT+I7HciX9QI6V/Ntj8dt+oxk/zJ5cnZ48llNc7CUjKbMydBUVC2sDsUHTB0sUGDWs/nBkh9h6UkALGaON2A1kzAat1SLYFBmD/my2Nfsk6XRI6edmunoK/wYn/NmG5V1X6h9pmRUHmv3NWa79321Dol3MEHYiuROTe2rAWHS0N7VFDrszw+N/0t6hvgxjyQu6lUiooO+Jdj4l22GYTDK0H+mF77y7ZfgyVCiCBsrS0J31ash1lpMeZxiKxK8lX+UTUxe5FWdH39SBN72gON4N5AFraH5wju3Nlx3YrNqJpSRJy9uQzEaWWc8P/oahNvpPcj/8yWJUtFbyQLO/OS3LGnHrUNFtne/x8MG9rGcNcPTmyHeCXmWnqzhB79BSTrqVxv4zBG1Qh99uh2EyyYT9SKQy2iw6p0UjWvbTCn4YWk+EHrvYACM1mdHkDwijbc/a0/Sdb0fd6W3jnwF97e4GUuRPpmv9/Um3O8x8AUy3u71MnHQq5OtyCnLPD/6GgQRvJfKuY55swhUCDte5uWgcxyTFCM+yIph205dV0hBVhmDSNqNpdTrktwu+TJ9H6QeEfU8ydEIn2dvtMEwmGcbySLnDMU7xWS342szT5JlUsRtBHbzTcXz6ShXf3KD2/hYcA2g2PA6ewn3pF3lhvu0HYn64LNMOd6HdtoFnUikkSZMDUUj2p0fHuYd/Q7le1muX5Z7K+S5Gs7+/aC6sNyAQsw+U79MDQf9v3I8kBp99nFTf03Z729eUJDOa/FWGbJGfispi9tD0/rZEcKLekvjZ3mn8ZD6jbqvu2UBcerrKfvOc3wsZhk714ImHB+4WD1by7w8H3Va8HXQLc2AqpMzf9iNRgr4nf2c7HSZORvqpTVY918dbSUpmRTdQEadBFd0X2okkiUtettmgfOFEmxiusBYU0wfO1Pxk3I3ArThnvrPtRmFSybAbhfYj2SAXnbcywJezLV02SvpdsbK0/X/WMOI+/t+7FR7orjyAZ27yz1SSxx8K8zc4Sm3/yXpfxA9xvN2IwqSTWf77j9CFRac2t9LbeEWL8Ohcuk5+ScbJlESMc0QLvrP5XtKWXLgDCD4EkT8sqckzO0F3tuMwqWQ8hoHNpsDP9ve+C+ZUbsWdHeG1V5nBD66l5GzusnPqQCTPPJjAZazKsMgvjtK7t2iB+aa6lXrBxyQ2+sPsBd3ZjsOkkqFxvSI/D9/7LkiDT41vNdjda7tBDYqRrdlPNxhkA02yGZ5L5ssxEIybVbjkFCwV8hIhu174Xa9lkzN+sBIHz5pJqCqBbfvCucrbv15NWmk0cVZLpbUz3s5ZAu0y+UE23Q1YjMBsP59N9bBaxPzyyRZoaWPWkoE6pC4VVavsv7T//unKRX1ZQLNHHhBpOPFAN9hWZgKS9AJ/xYGlk0JffKabtmHF54bkrfC7H47NAdCt9IPvWE2d1hTY9r//gX9+cUFBz7wVnckbgmZ/M/4tWoD7Ucwcct/3FstscWzZdtcxnHDT6Vl6eu26ubBUwqLcm7U/LD9uN+n+pus5tJuQY1wmaX+R9wBTVfPA4To3E+1X0RLcjQJ6WMwp69AVbHqZxTDNycB6H/b1WbBpvo+a+iw+58ycH6u/PVre259ypwPxKWM60MYkpAnmGmbg/+GCZpc/J8W/uPIrOdMBzb6Goj1+Vs0pOMB3ley117NesylMRvHm8iOpqHsTqzV0esJ8NWQWRifVn6LOe83G9A9t6/OywjBPHGvTKgRDuwigu31vVImq1mKQKmNOfKbR2xuoIL8KnuF3XEYmm4wDzYbPROMh2ZbCwPtLq6kJnjhIx5R/SMAMXXqIa842wbHKW0XlM4hUD3MqtPO62HnQJqIIutknm+OgWe9BNHUu6vtjmsB1Pxr0w0fUcBdgyvWAC3d7QWiDDn/2P33fdX4UndfbgGaPPBqe83JoROKqScc+LRakjA+HL8V1dJ36yvPXJB4Xf+uUk9HMfrK70KhB+CIxjBHDMBzH6EXn9ibgcJ0aivbgWWXTk1mz1xa1jcDYm5x1s+gYp168YYV1eSV6e1jhsnktciqaLu8zbIFrE94eNPsailalrGav/cSrrs+nPNEwkWUr6UaBbIdleFTcy2b4OcBngDPGweZA2k2gWmAlH6kQTGdnzKP8mhOqm91tbmsP3e5u2NwEHp5KvssQBDkGmj2C1I5Lzb4kDdeSiPFgolUpqzVS8xVBs6+haFXKao3UfEWwSw+pAEwpHEp+TdSiBaCg2SMVoF7LDXwf7NJDkNpxqdmbRQteKjEeTLQqZbV4NRcvwfmg2ddQtCpltXg1Fy/B+WAlH0FqB5o9gtQONHsEqR1c78KIJfnyVxIxHky0KmW1eDUXL8HZXOoVB0GQhwUr+QhSO9DsEaR2oNkXgIEeW5FCwUW17iiasw47UKd9BcBaWS6brAC7ouu8ygp4qgliO/Bmu7I8nm/yK4YOOF+J4QpQVuTxvtUEI1odsl34gPTyLKpVTwnOB83+jqK56va7ifPe7gmuNemHKzqpdAapB86H32Q26g8R7LHbkmxV4FVHIBa/boVm71lD+rOyAcRgCxbuSVEKyGotrl0WCc7nAT8+VIDVZmhxTwCCbJvRQm4yXekZZu4bD50/01ey9UpdrtK1nKdvqcmabFA9oCu/WtPAT5Mrn3VpBEGzLwZFApn5K/GubWb8Lzh6g9wQtqGanNGlVk9nZ0vmvJ8K8k7/2m36RgjeFNg9g5wNmv098aee7zhDECRg33TbZZvD2GgdAxjJhqAsV1Tbg6QQb3JqM6lHSm8Af0YCkIKe5WdgiCxIl464QmoLur68o2jCgGEZnoMFgBas1O4RY4/GSZpT4F5cCIp4Edx0IT4wprELRzWItiLn+I7fBTDa5AZaIhRLvRfVKl6C80Gzv6NobHMVzNNsCcAn7hoi8w68OPFg0GguiAw4iTGzg89FtKnQg8Fa7qxB3wAN+ofrQ7Gg2T8aWMm/J/68S3vnVgIvMrMcr4oio1Ob3pBHiVG33pgajWXUqccb0aJxrY4gRO7ZZOzTQ84Ezf6+dGjhvgb6JS7nLNubr9qwWfVY6C4mPc7UG0FZMkgG+DTDl8GctAKYoCMf1j6aPXImaPb3wNNjL4s6/Q08rPrr4ACfMdoOM58T2yZFfpdZbICRw1KeVPOjAEx4gP4Vw1aABQhyJpfOwCvJGIWSiHFcNGttxK4UI+vtiuBEtXWxtRPP9jUVFPr5zvGFQ4nPmKd4c8o+QcHgcJ1HA0fp3Vg0T1s7MIzFHCWn+UP9cAKI3bVGK/BH7k0q8qDojKLZFy3B+WAl/6ZYa50jVn9m65spfpg9UmnQ7G8HKeg9xj/f6hHkxly6qFZJajYlESNXNH9seL4P0HE/QVpV+d+qwGubze/fqm/e6PI+gwfBLr3bieat1/S7GzMsr5DXymo9r10WCc4H19K7KebKrIPdIw/GxSvnIl+Bb7YY29ck7EJBygSa/Y1hpLbkqmj3SJnAx/H2SCNPF3BWPFIe0PXlPURjm5W2enR9+Wig2ddQtCpltXg1Fy/B+VS6FEIQJA80ewSpHWj2CFI70ONtHUWrUlaLV3PxEpwNjtJDkNqBlXwEqR0PWEF5QEy6FG4J00JqCpr97fH+ADQUZfewtWkdXDQrcXz5xbRyWAn7oZZC4wsxg9iRK06kkuBwnduLpnm/flnGXoDlapnetdJrYdp/lpwC6v7ymAfSyhIkpeWEWutfEDyIrWqu63rHQuRm9f4U/wQUL8H54Fp6txfNIkp+2TvvacSGU2/dGT9M7WwdX2bJT2uHIKmToY7GjlxxHguRl9X7U/wTULwE54OV/JuzXPu/24bUhbHoaG/MxOSeaFV7DYPppg3w3myDunmZm9bvXtOdGSD3OSdxfBkdgLHkrbm+nKTlTE1eMn7GscO0+aVmCX05TGosdXciB9KMRW/DDc0l02uSbVdjei3YXoWk0gxiQxS4BzDhe24odJROmL6+NLkXfHweE+zJvzkthR81HdJWdpYw4j7gRZnRw+tmi6NL5dMzrgNtQR4pMLaHA3sMduL4MjoAzsIaMIttWmNnNLCcJHaYNkD3jV1GSZEzO5EDnKU39D/WA2ERRBoIUz91FZJKJ4gN9nq9AVElxzYKREJH6YTpT4SfQ65o3SKXgWZ/cziOEUP7UIaKZcu+7Nqkami3oZVqJPMsK7KW1VEabcty4/sSHwCQfjQaVpKWaXcVKbO8LkmbgW6Dl80wqZzISUCZeVEajk8d7zX6oKcCkpNCGNvRNB1a3gZUQTJjocN0wvQZy5Pxk8KDgq4v7ygaT71akkJUsAVQQVXJTna1fAdEGtHhY8eX8QGR/qaMzAVxP20Ademz+6llI9OAHBPus3RZfquxDbh9HJTAFae8bq57W6HT6Ywmf5XITTe6vnw00OzvKxoH3cANjqc1iQ1566fAjJzkrC0CKdOF2PFlfGA/cXLcjnbi2GDOBq3VInWt/MgZbHL+cMDWZOm3EqGzIvzUJqvekawWqeZ6SXA+2CdzXyRxycs2y22gT4vasdbkDXu9IRbHmx4r8aoIa17aOr6MD+zAiTPbpl/j4tgBLnCm5vsMTYpe60DkLYbFL5nGTsAwtudQ55oNdtFiEqHjaDSEv1Fk1gPkIUGzvzOjyQewQ0UN1ttROLXZnfyR2xpA2/hn0Hr+/APCc8rxZXyAkm5Jj+a63FmRgFHsgIY8ZrrW37cgKbJ/KHIC8+ExtF8uHTAUBDSSqPjKNNX2Vug4HRqiuZyC3C1am8hl4FScu+N7O7Xp+Ou9S497cW9e7PjSO9jt+u6/pmLHSfnUMt3oEt7RPts/cj83oLtf3d8RmobwGOzRe1RwuM7dRWN2jSo2OC69k9yZfLudeIJhjfbOs1Gxzh2NnCI3YE4jf0doLhMFh+s8Gjg49zFFa0mu/PLVAfaHaF9pzD0Ozn00sG3/mMjXcKeJfnXrCg7XQZDagaX9zfj360F/FS0rUi9wuM7NRPu6LZ/xgiglOFzn0bi0kl+SvJZEjAcTrUpZLV7NxUtwPti2R5DagWaPILUDzR5BagcO16mhaFXKavFqLl6C80Gzr6FoVcpq8WouXoLzwUr+PfA2OEUVKRE4XOf2GCt7iK9XpESg2d8Yb6n5zPARv+0i1QVH6d1UNGNpA1Te6nG4zqOBZn870WhBT+e/V93q0ewfDqzk3wxjEvz48Fm0JAiSBRfVuh2etvZZUsnvf3c5DAS5LlyvaAmqCyO2JNcRQOOF7yeGINcDh+vcVDSx/9pkYfYVR7MPDA7XeTTQ7G8sGtt6eW7MSyzn9bJau2uXRYLzwS692yP2PV3A8TpIeUCzvwdss2gJECQFFkIIUjsu7skvSTWhJGI8mGhVymrxai5egrPB7/YIUjuwko8gtQPNHkFqB5o9gtQOHK5TQ9GqlNXi1Vy8BOdTU7N3bMe17WTXjzc8A45vniJJqSwaugdo9o/GA358OIG70Tkp7ct1Jez7cx47orT2/suEe/b8GWCpiOQWzn66kx/kfJ8nm2A49LRiTyLHViuX/JEV8FQTxDZ18u6vLI/nm/yKoZdciZqM0+2Q0lM9s39nGta6DRaI0QFNynHj/tSG5t94Z0xNVuPDCD4t2g3dohavOvTY1qe0StdU8MD58JvMRv0hgj12W5KtCrzqCCTYutX8eKueSpGqUblnVHdeA/Od8cPoyMupKCvokL+O1TQci+7/IbbPKYHdN+kZcJKg8iBI2yW23fkzfSVbr3RSLa3aT99o3UFSZs9FqwBBTlC5RbVY0KnZz03rd685Fh3tbSZ1YSx5a64vgzM1ecn4mY2itsgf3d/0XDdY1/oN4H9uYPUQNOp3q+2O3iBqYxuqyRndYCo9MXjJnPfpZufd5cqkoXuAi2o9GpXzeCspizGx2LYgjxRwljDiHNIgdxbWgFnQNv1oYDnZGI5DTNecDqQPpRtMmZnP5z7foEm3ZHuZDWsYJtgQtBoUsO1tA6BJ3gL0lwejVBq6B2j2j0b1vts/94w/G+BZViR5U4ZK2G0n/Wg0LDDtrpLp7qO4IMD6o9sc8b9pYc8NBEEYSEpDAZeTBUbgNtuw5nQ6DyIQRHDdlP4G7JT+cIwDCFJuKte2B+g2Pycyt5M9MaiJu0k/XwafvBXIO3voUiNmWhOdlPhAbFtfM4yksdz2q1xzECRq0Dc8SYwBJ0mQHXwugg2maAUgyAkqaPbA99/1Vu4ZCWwR7N3gxHYFmLE94ICl+hgELXxjAa1W+PFv57O9yOhd8rMhyTHqtuHfaCzpm8WvokqRanHpM2qWo0WzL4bhyJxGmty86e03YDhxZtv63kH6IvCDIl36Qf5MzKCuQMtxM2jGS9mPAWxvvmrDZtVjobuY9DhTDzoCYGCQ90XS3C+Jhu5BkVktXs3FS3A+1TF7T1eonTukhS0OeWgb/wyyRT6tfI/mutxZ7STSnbZJTEsNdlokWCts/vtk0wob9lym5t5h5qQR0CRFfpdZbICRw7Okmg+w6LCl0tA9QLN/NC6db7/qFC15VgxLNWLnMw4TNuyjD2l7vPuv8EdpMeYkHqW3cAegRWMsewzMokr9UATVCjefdmsOtq+poNDPd46fXg57s4k/25dEQ/egyKwWr+biJTifSjREPU11ty6n4izlWf3EEwxrRM5tLGmdBOitaMs8CdTfhm/DAQQQu2uNg10NeqOilYEgJ6mA2ZOCHnwYyl8J2zJtuU/eD7QJn1pO7KL3NdPOeSm0z08HQe7No4/SIwW9Rzvjvmb1IH8t2FVEK0glBYDDdR6NS5fQLEktwRkbQQ98y/sEaVWqf/wjPg6XgStoPhqPvoSmp609xkX3kghyBo/u+pK6l/Qc3tPRvSSCfJVHN3uaBaXFOGj3CPJlLjV7sxwtmlAMUuTLviqVQ6SsaLWgyKwWr+biJTifqqylJ/ZfHa9oafJFqz64lt6j8YBvqgOge0kE+SLVm2+PIMgJ0OwRpHagx9s6ilalrBav5uIlOJtHH66DIMjZYCUfQWoHmj2C1I4HbJcgSPlYfT+JG2L2s6tPVGdRrbJRYtGqlNXi1RxJUOo1dhbTQcbuqzJKr3yUWLQqZbV4NRcvwWnY4dTN7BctEIIgN2fH7tHsEaQGZO0ezR5B6kDG7ivn+rI0lFi0KmW1eDUXL8HXSNs9mn0NRatSVotXc54Enn5w18075TuO48KNSdk9VvIR5Iq4mukD2JN436EOkOmuuwkAa5YTUvv9+/fyksudxdbucbgOglwPc8y7wnPKddqCewo3HA0MToBGTkgNmFHwI3FnXexs2OEk/H6PZo8g12PW7nl/13lOUqQR/BUHtm7th6T1fI8hbwDhVmbPxIMIxWngrA1H6dVQtCpltXg1pyVwrSGwip4ye9dPNi3b6dmGsx9yoC1d1oPO7Qb6bQUK7R9H6dVQtCpltXg1pyVwGYGUqKnOOc80Yrs3Pwfdd77fzgk5b/x6+zWa37xTLwEr+QhyNTjf4cFOVdUXIizCxv161msCfLzmhewsDM6zlBu37FOg2SPI1eB4rePr28VcF+qb/+4HLpTlVzBAHDp+azek44pD25U6jMmId5ITzR5BrsfTxLa3LWl1NRLg+VMOHaLPdGrVrtvdCalr5I8hkvY2P7iTmI/u8bZoAR5StCpltXg1ZyRovBqKnHSYNURyUn7joua/Qot9fbYbMnCY/r/BvUp6Cpp9DUWrUlaLV3NWAiHtk40LWutJk92l5m/nhrwzWMlHkKvDZHvnmKBUF+wF/ZFyQ7LMV9K9mny1WjnXBPGu2kVqw6rUq+vsylmh0t60PFGhG/7K8ni+uZc17w/AyBSUL+jmQCAzapc1O2BEm+326eQQpFxUZpSeN/YVRl08c2CP3ZZkq8Je1jTvvwBz6bjZWyCSkAcCuV7Y1UoqZuIw2FocHGJREg3dAxyl92hUxey9vy36XWQ1foWZ+0o7S/y9wBbN7MuJFGf88EggRk4SmwaNMlc+FLQkGroHaPZn47lf69Cz2ZuM4alKJX/R6HobV+jommh0A40y4M4MkPscjCVvzfXl5dr/3e6MpS44U5OXjJ/w3myDunmBsehobxvNEvry3LR+95o00E7s8DL2n+Cn0SNvlbDgx5nLSAbfMn1Rpl1Inmq5vNDMNbHN7P9gxUfT8ZKNfT7lPkloReqg7ZT9Lz3gGkdN15iHv838LoeKmL2n/XQ+ZGbVbBkMxAXw2B/6C1L8O7o8WC5eWpY14sDh6YkRO3Po8ChSXNNfvUnOdIXpUm7rfI+PAmViBykqv8KUyU1l+VkwxEK61IkgUkncT19m1cWIB/vDo41N/qCJaVIDTNqxTzeOYI/9JrNRn7e1ijVw5vznMdv1vGBo4CH7rojZWzKzaDzpJghrMS6ALWugkDLbEkH6AfYSOC4a+2ja5ER7moqukIp9F3x5BTzLirmxKfN1HEEY+qRNYbSJ+qx7DrNASo73tx00Nj9JY9M70NiMoUXJnJdONjzn7hsP7b+z1+0heWD9VZ+ORWKOdmFVZLiOLYAR9baRsjo0RId2zkl0TwzK5wQXdi2VakFd+ukKe17sJ6Jox2M5srumc6Qb9A/XL7WG7gEO10lYNLuu5vIdTROSxqYzM8PmougHzUXaxqSnSFNy26bcDbQkbc6nwHQdndbnuYZqyPFJoI1LkSbE9klFIQqbOptujuZREbNnPCBGa/lgSyKjhlUmDmwRLOByIpET8WCpaPozmLNBa7XYBsqN7c82LO+60oBvCdGSaPKhPr2SaOgeoNnHuNqb864waqttJCf8T38A6ebipz9i6Odf0pQkbcpu0KbcC0TbnKvA7K2w1aqotpycdFYb0jAYcy/rGX3Yw7AHmqN5VKSSL2igLCXN2SxHbHcx6XGm3pB4VYQ1v/9ccOLMtqnV8oa93kSG7ZLmkub7DG96QaGfG1szftGTk/koHl219mVAkAhLZmdN2tjkSWMzerIsi5TI7syUQHpmSHMxaD62w2EfpE0p5QYibU5PUoNTHgS1BpE8ovFJsG23TRqrTV9SSSMzDhufzTRHc1sQFTF70bL7K/9F13oCdJnFBhiZgefPPyA8hwEyb73RXJc7K6LayR+5rYXHGvKY6Vp/39rGP4NgamRebN5diZxnG21yxagLoOicI2WCNjaDRh+TbmxKkdGKDH2SbMjpDdoNlGpz8mDQosWl1h+dBGVgfLgNWG9AIFeJw8Zng+aoT5ujh74zVcTsmafJM2nFNILqfafj+PQFKbx5QffeG4SD6YL7QXf4EcA7CaH8h5Ts/fAY/CDbHfLG/OVywYHd2BTpZa26LP/UPFtApEp4upJvUKxNG5umD5YobBubFm1TJs1FIX4fpNkNZGzbnCJj0J9Ntj0jK3oHuq2dsAlxc7SfPzygKsN1mt7flghOJ5urAy+7iScY1mg3ABuV6hwcji3u9t8dniBdEg3dg9oN12H5v0Inbt2lJQgam4burFdDrrMkjU3DUCR+Lfkqn5i6yK04O/qsDts2ZTYQKXIMzQ/Osb35sgObVTtlq55tm6IoLDnZZvlU2ISN/pM8x/5kMcrNQFXMHtqKYfhfnMrYMm25f+sMlERD96B2Zg/S8HPCNDvsrgSksfm0gh+G1hOhx9LGptRkRhPSXBxtm5lP03e+HS1lG7cpdwMp8idtc/6k2x1mvgCm201dX9O4Rod5noyBHfDpsDGCtxJ51zEPzBi5dAZeSSYclUSMBxOtSlkt6trmxCdWTov8UIJIjs3qOfXxx4kKIjf7Rcj30mG4yNZ3Arks4yUVThv43D55z+f2woZYa9tlucZec7RyM/AQ5H5IQ2L3Fi3yM4fzG5s7H5Ezs/ETC9wJxKVbmYeqsWxO2BDxwHCSnYs+JqvV99O4mWxFC1CPrBarZn69yQ6Hayu64Zfeqh58uM6oJHLkgG37al97sQZesP1ew84e5x9hAYYHN/uSiPFgolUpq8VZvcBaYl98zBuN80YR5HzWa/CV1/6haVj6dvWVlGPrHffXaXbcX6d3jRs4y0GzR5CzMVeN55fWvvH4Xvg7s8g2Bahja98L8K141qcfQ98fBDd2f+1qFGPrDZumdQN/X6XvfECQ0uE5L/nlpTX+T7xp/2GIff+Qg6N0+A08J8Gmeujilvnl02JdSz6528FYeo6jE8QcOofOf/7C0o/nU5nhOqWjxKJVKavFjNJLfw4/KMF/Af4NNqT/wJSO5zTiM0Ni1L/74TAd0iKwEiuUvfWztu7MaaWBJ28Q/5/bLKaPHm9rKFqVslq8mo9K4Dthy9zdbaDPhWV0yJu1P/75jA6zNjgM1w4G/jIMY7K3qY5jJR9Brs+ECWdrGIugte+bkBkz78ycH6u/PVre259yp5O4yOI88Dh7E78kFnTcz2rzdG0zRbNHkOvTl8POPeUpMGiVY5bbNbC8idUaOj1hvhoyC6OTGue3WHv/+IzxI+rSW7h0EIAsXb3jHc0eQa6K7zo8sJyXOqTOX9i/fjJclm0pDLy/tJqa4ImDtE13iJnbLuMEi2/4SzXoORSv36uHw3VqKFqVslq8mjMSkMIaOD4sv/+h3fV0Yz0bSPDjc+vrMlool2kC1/1o0BKdCQ2RJWF5wXfcIYA1gR+38o6JZl9D0aqU1eLVnJZA/JW4sBT+S/8Ge/ILCSO9snHnn043/DUpybl4JG+yJOO8Qxe9nC4UYJudmzlsxEo+glyN9Ny6rc2GS+XvnOrFG9YmOBat1yNoouDbOnkL8F24GWj2CHJ10ia+3WaiZrySbqzLdjgQNyruR+qKfsFLdfPdwgV2vRxdI8iNeJRVVUI5cbhODUWrUlaLV3PxEpwPmn0NRatSVotXc/ESnA/OwEOQ2oFmjyC1A3vyEeQKMA+ydGL4WQCH69RQtCpltXg1hxI8wgp6W/ADHoLUDmzbI0jtQLNHkNqBZo8gtQOH69RQtCpltXg1Fy/B+aDZ11C0KmW1eDUXL8H5YCUfQWoHmj2C1A40ewSpHVzvwoglGdVbEjEeTLQqZbV4NRcvwdlcWtoXPyayTGI8mGhVymrxat6VwDMO7mZ8X/oe6P6BSLcGK/kIchV8lfzRbLAnwa5nBIAZ+Lv0TYOYuB37vnSWJOgnjKkbjDl1eGdOL7rmpTxgBQVByog/a7KgNuI1rollWywfLZFnf/is+ywnYZ31dn3MtXJ/I0SzR5ArsWTASXbEH/BXHNiaRXem8hBWk1/JSdfxWHCDir3n6fLKds691vfA4To1FK1KWS1ezVsJBEFIr3Jr2xvXNgKzdxoAirt1lLNmSIvADVbKXgprR2ndxJ31YdDsayhalbJavJq3EjSazdRK2eZ42P7L9oOJ+I2FYc3FxNimzpu69EXqHXO2fm69O9Kt3N8cACv5CHIlxgBWI95Zz3qNhjB5Dnb6y4XfSFbiWBg/+B/z4AvA3Hjln4TNnQt7NHsEuQ7sG/2bFPfKq+Drfsdhg468btrFTbfLANN2FE9kej3GN0DWGfnMq30PNHsEuQ7CREp5seQ4/ZPU3XWzD2Do4Hu+57XCgAw4n47IOosWeUkYY1EE23y6/LoXgGvp1VC0KmW1eDVvJfCD0TfiS7S7bNLG+2LRApYHlmFZNumvV9n/kL/O77YAy8aQbK6Wd12MD82+hqJVKavFqzklgRF25LfCH1HbkHJ8Q86T8jzAjQMKa1XkbI28DkBSNyJjr8W7yoyVfAS5Eg0rLM790Oz7vOoC10o5x4t9X0KL3aw9VnolAXvc2gGueV8XerhyLoLUDhyTjyC1A4fr1FC0KmW1eDUXL8H5XNq2N4vvSimRGA8mWpWyWryazc+vh/319aA3Bbv0EOR7fN2W/y1a1Bhs2yNI7UCzR5DagYtq1VC0KmW1eDUXL8H5oNnXULQqZbV4NRcvwflgJR9BageaPYLUDjR7BLka3sb7fiJ3AEfp1VC0KmW1eDVvJbCm7/xjlKNo9jUUrUpZLV7NkQSe+nesDx6kfw9H6SHI97FUHYAZPojVo9kjyHfxNmvPfySrR7NHkG9ihN6vwD9jTk7B4KJaNRStSlktXs2S9KauSWHP+Y/SssfVdRDkGphLC1h4GLu/2L89giAJfLMFlq9LD9JoRrNHkGvAyB3JWYuPYfePISWCPADSyNOFhxivg8N1aihalbJavJrTErDNh7B6NPs6ilalrBav5uIlOJ/HeDkhCHJF0OwRpHag2SNI7bj4A15JPgGURIwHE61KWS1ezcVLcDY4Sg9BagdW8hGkdqDZI0jtQLNHkNqBw3VqKFqVslq8mouX4HyiXsiNc2Y8xzgvPNs+GeRsGS4QI0BonAyifn8B1ItE2+ULavs+387sFbKauSfnCHTmtS++zk1zT7nLrY4Je/INV77xddzN04kQt5chuRJ/alr0vMndSZYTnFbb9ylFZg1ue/dvKVD63pci4wn3uNUJYSXfuLnFcc31iRDmvawe5FOvZ7U0zwPXVG99iXUpMitvq8o3FSh178uR8YQ73Ootd+vS4x7Db0CAV57n4fZqK0lm/bsLVJKMJ9zTQrAnPwemaAEQ5Kag2SNI7UCzR5DagWaPILUDzR5BaseRSYO+w5asrzPN2m9nfovCd5h8HVobV2kWKlpROScsJOXAzr2EuI76L3q6jimmJOzK560sm5O6pBKgzX2A0e7Alk//OSeVsQWM1L6ib4CtGCRpws+9znU9uiF65sY478KPq+rnRIL2zKZKfMk5M27ITL629vlquJsy9s9R3eGcU9agbDNFdy7ke+r/sl4PXoY+XYcSOXD8uGJKwo7ZuxOuLTrrzyEHS6EH2l54MT8ZrmUa1tv+8Y3TvUSqlBjA0RS++klNA9sWriDAwQSz6Vlj5kn2DD0nogkdHuzTFwgSFE+HKw+BxEdyHiMejPx1jt/PU+r/sl73LnMiH0du2hGZcvP+3Qf0MrJm73+KfWJo0nT1ZLhdMSdnB2Rk2+2x5e63CQz7kkylxCBJnx5Bv2XD+FrmipcJcDjBbHorGErACXkVQQ+4g9raE7CIO38xgcRHch7TPRj56xy/n6fU/2W97l3mRD6O3LQjMuXm/bsP6GVkzX7D9sGyQO69d1gwQ3ub+5Lqyk/s3BfVRnfu98kRZeU2ibT+QmNaeiPMIkv+81eaK3Tl5Hdl+O/Ugs8kJUb8JkmuudFNptUJQgVi0S1/oftNujqY6T4tN1SNU64H3rjTCAWIxTmfOMH89DxDihs28aFYTnUNH9yIaitRUpxGqMgoH2GCQbhYyCSrBTD35ZXbbs8MoScmdz6VOSI5QyXubXMe35BY7CC/YVr97U4mu/H9sldGdC9PqD9f/znqj6UI1Q8Z/Z++TJLhZCPJB0mf6zRP37StTHunMnm3p3IP/InIXGYh3yXTk++verCc2SuTk01R2ExcetDRV4qor+lvQwbHoUdmoqSSNvd8ozyZdhhqbbQZWKjSE0ys5Ffi2fY5hfW+GLSDxHH87TUtvi+t6CXtUKzgxuid1ppubpiG4tFR11RM3/EiAWJxzidOMD89Z+tvNTkUyUlufqsZREuUFKcRKjLKR5ggPZUImWS1ABx90RBXY7djr7d3PpU5InkgcSrn8Q2JxV5sGr1g4HuQqXgnk934fs3tflc4Js7x+5mj/liKUP1Z/X/hLscZTjaSfKjSoM194aZtZdo7lcm7oKw1WFqtyyzk22RKe4cTrc0LS1qkggOjmfHep30xzDMHHwZ5KY/iW+QPZVc3RFdT+qD8S4/Y7yB1wN3QA3/UXvQ7kHj/gt7UjBjgvAP0G/E1gVT7Bd1opsQCV2u1wNBb4OsK09xoqWI9ECAWa3C2JHsJZtNzEvVtLxHJKQp6M+iQSCkpgSoyykeioZSQcVYLYSian9yAscztnU+JRiWnEmvbBye5IaHYPLEyUH6H57xkJ53d5H45vHy00+bE/VRy1B9J0YnVn6v/g5dJnqntwxWlz/dTeT1y05JHYv9UNu8dcw7rJ467yEK+TdbsedCUoPwnNscON4spsTfgSUVb3HggJC9mUQ5aTqm3Lf9krCfD4ADL2/HvhTJlxAB+FNRJomuSe2S6wbyNWCwqh2aCw9J+V9FhWd3feZYuFyc/wSQ9Fry9S8RyZgJnCRSZ5GNfyN0k7okgklw1mLAPNVLxVrTkEdjmfJuRUGybdgjF+trupLOb3K/W6r3dOlv9iaqaOer/iv4PXybKMLvdiBORs3k9eN1EMQdESvLO9D9mcmEfeDNm73LgCPEWQFP82ByrgKTuBiNJwkxng0OM50e/F8qUFYPJ3EV/Yjd2J876IMmBNBtY0H29sXv+UnHyE0zS48HaPXRMSUfz8U2d3ZIc0VI538mIn243JjuZUMn96vDqwjxSBztxP7+i/q98Azpwmb0rfuWmJTIdECnJexi4KDJte94BzgXLciH8nCHw8YI3FpsznE9kTdh+4+PA5ukCQ77Nx7+Qnk/5dXbFSGOY3d72K3AolgB+s9lsgGu2X19ff5DbGFxXj/QMKXHOI5Vgbno8r0VViMOXSCkpSSOTD/9UCsVCVLwrGlVBkvPsDSHVcPrV0t3dyWQ3vl8AjR8N/fCL7tT9/Ir6dx7SU5eB1KOeeuYFMLJ5PXjTEpnyRErn3Z/yzbUOl1nIt8mIRaok0nxuyLpjy7rV4LTgm6K1amh2bkeorH2wTvDq8gxXBYWX9bWoQiv+DVpC0tnTWNNikKTNQO8RHFjc/2/vypZbxaGgWAw22E6ctW7V/P+XTdXUZHOCzRqWCwIJCcS+BMfqhwRjI/Vp6YAA0ZyAT9GSNOtLi751C2yT+/xrJ5AUy/aga0FCIKfTEXmBFeXdvr3eqJF3fq6pAouEy6DiQAr1JjkhMokL1CBjHHneICkE1ZREo/iBChe1FziuV4FY3T0a27ON/EQnbawm71OFPh8zft8GQptGw5zKX1GxG/6z5B0VqVeGDAad9qKlCc796hQ+CtH5BEQ9YS3ZBtgwb7McRE/avyakgzdB3SvgcPwEws0G/wea/aY+duZE0IiLfo1X4ZGgopmmLpzlbSwipnUApxNQdVOF5wO6Y+109126fwcZAUynI/ICK8pbPx6PMd/4LK66CiwSLoOKAyvUl+QEQN0QtTxNDTLGkecNkmH//iXcHgsf6HCz9gKuCVaH6j7f2J5t5Cc6aWM1eVcv9vnbyLaF3aZFo2FO5a/I2N3TQQb3/78/9cqQ4S0MvfQ+sx2b+/GQD6v9CC6/Rk+BUP3EjvVxR54ThaFM/Qd4d27W3aCN8ZXvGCkaRYSCkFz0i/8RtKJAKjQsnjsECWA6EOeGm+IG41JLVXmhL4t0xNUiUfOZsjgIhZglNHEdDKPyuhLR8jS1lDGKHAeC4JNtgT7Q4WbtFeUONyf8VjY2oQr9W8lPdtJcz3I9OGBGn48CGbRsNCxM6Ssy9oKaYIamzkETU/evuioHgkJ/VXVd+ctXfUui9rHofAifF/W5Jk3RKEJMqYlU0eVnHyRqQRz8pGFVeSIiWVUFIRKlRRYHsXY4ydGBuNHUJCpyHAgC8wMdbtZeQvveUaF/o/ysTtq6GgIp41aNhoURmaUX++pP3LUpUNBV8+xLGplvNfePV74pbPYTnJkwaFwsJhNpcvwK+bvor5QWfiuKR0m5NNCoGXlok80vki9qlnotphNpavyONuig/01p4bdieeNKDg6OidEt7c/zWHlH3xUvyPGOb+YsDMaIgZoGPpNyI8IbZ+ZQQL4fwXWBO8N96ssTe24wL3+O6TrQHUu1ruhkQ1E0KbF/2ASoM9y3P/B/XxmjdK8nBZ/buEN9uoJ6IwNTUF3joe11jk6CIzuWhO/FiT07mGk/outAd1y8dQWsnW1SUv7dUhEdtXQg2FbGYjQ2nPAK9nD7V/lBOL38SfJ9ezLHnpKUVp3ZsSjUSg42mGk/outAd1y8dUVSe4VJSZnlUmH52RySthyL0WiKqwNbUpKD8HfwKIPb/7xk8CNuDX3k2xpp1Zkdyw21sg5isCyryGDG62ystB/RdaA7+llXpA4Q8uTWFW1tKLbIpAQ7gBDLqXoB7QCSmm/M1/ANsDYSiviuddR3pG+Gf9TBaQN3fRJwtsDLblBvT06HqYitq843gIu0nwdT3O0Vv/qSlfamoHmmswa+52qevdrnCwn8CLonbILEdUC3DF1S7WAnofXq+e1JibewtTBr9G4gvROy0lBhqmdvRVjP0dI2Z2hdEaX2C3GFjr62JuBTYOc5W9ewhb1xvstlyYnGJEBSu7Iyg0PSqY7OPjhlM1fRcqqeDFkSDJNtl4LIRaOtRMa2UYNjeBfm87QiAU7Cewf3N5+m4O3kbH2XtG9dNSD4xkj7AFKcLe7hvKTnHcUZs56V9mO6DnQn1Ne6InGAUGewrmhnQ4FMSrCrAsjdQJB6RQeQx9UQXuMiKHSMllGTvhkiCCWY9nGr7DQnjI/WAM5KlYNOVFpWDb7/hXYsGWhPiwpxF/Xc06xgpP2YrgPd0dO6AjpAzGBd0dKGApmUYFcFkDssIPUKDFcLyvq4DagTzbZRk74ZMO2TUpIVrpM+hw7P7jsdY9tWDaQD8aAmUhepvyhxFwBG2o/pOtCH0MVZV7CrgSYlpKsCWo4I/ks11xBB4yGZRZ3wzQhCwfOBbaUDrFW6MzaT/VsweMzFVE1cM35HeFpwECinfeAmO+wQGetUuA4k10cc96AHcBeRuw7oIPoe9JoMWbZ2q4bSFNHdMq0rJuDThNSGgqomqz0xKUlcFXLV0uVMPeQYMTnDPpAEv/b7qqg17cNKB4WGJRiC4kmpGGGAf5M+ytYf1YLTiAClPgeJchOM6zrQHRdkXVFtQ+FkJiXYVQHkbiCZeptBDiATQ1jHx+UP55l1YlRjvkH4ZhyyK1Tp2M1NDyJrJRl59/cUqKm6+FPK04KjgHLaj+s60B0XYF3RbEOhI5MS5CgB2WTLSL0hDiBTY/firMOQOoy2MN+o9s0QcYhfavdBfouqi5tQfh4cBdA2GzRGch3I0MFm46etK4x2HaXWhiIzKaFdFdByNtBlOIB05joYFcFaxjP7pLg2asI3I4P38k/810Qz85+cryfmtJQmm43GqktgelrM6WWxYDScZ43hOtAdl2NdUW1DgYUlXRXQsiDnWy/QXCOB5lefg1dHXfbNUJKsBzpO5e+HQfHW+X6wfrr4d8/+DOpUWa7rwDKsKxYny6ioGpwNjHo/YNvfLficqEv75boOLMO6YnGy/Paor1PwKbDMESYHB8eE4GnPwXF14GnPwXF14GnPwI+8n+jKgyUnLHNMjTTtuz0d0Qtu052U+Samh023AFZOq3LmgDP5MyTLCJZok5U7Tz3XjHS6TnSafBe7bpp3PgMHFPOuqe3dRaRCgkbZhmMRwZJtMiWh5ra/CqRpz8HBcUXg5/YcHFcHnvYcHFcHnvYcHFeHv3JP/FltQrS+AAAAAElFTkSuQmCC&quot; alt=&quot;30_spring-boot-autoconfiguration-01&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 주목할 점은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;두 단계의 필터링&lt;/strong&gt;이다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;filter()&lt;/code&gt; 단계에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnClass&lt;/code&gt; 같은
&amp;quot;클래스패스만 보면 되는&amp;quot; 조건은 일찍 걸러낸다. 그 뒤 실제
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;으로 등록된 다음에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnBean&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;
같은 &amp;quot;다른 빈 정의를 봐야 하는&amp;quot; 조건이 평가된다. 이 분할이 자동구성
100개 이상을 조립하면서도 기동 시간을 유지하는 최적화다.&lt;/p&gt;
&lt;h3 id=&quot;2-3-deferredimportselector라서-의미-있는-것&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-3)
DeferredImportSelector라서 의미 있는 것&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;일반 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ImportSelector&lt;/code&gt;였다면 사용자
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;과 자동구성이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;뒤섞여&lt;/strong&gt;
처리된다. 이 경우 사용자 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Bean DataSource&lt;/code&gt;가 아직 등록되기
전에 자동구성의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean(DataSource.class)&lt;/code&gt;이 평가될 수
있고, 결과는 비결정적이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DeferredImportSelector&lt;/code&gt;는 &amp;quot;모든
일반 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt; 처리 후&amp;quot;라는 순서 계약을 제공한다.
덕분에 자동구성은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;사용자 설정 전체가 정착된 스냅샷&lt;/strong&gt;
위에서 판단을 내릴 수 있다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-autoconfigurationimports-vs-옛-springfactories&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3)
AutoConfiguration.imports vs 옛 spring.factories&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;자동구성 후보 목록이 어디서 오는가를 정확히 아는 게 Boot 버전
업그레이드에서 가장 큰 함정을 피하는 길이다.&lt;/p&gt;
&lt;h3 id=&quot;3-1-boot-27-이전-springfactories-키-방식&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-1) Boot 2.7 이전:
spring.factories 키 방식&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot 2.6 이하에서는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;META-INF/spring.factories&lt;/code&gt;의 특정
키에 자동구성 클래스를 나열했다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;# META-INF/spring.factories (Boot 2.6 이하)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration,\
com.example.OtherAutoConfiguration&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 키에 콤마로 여러 값을 이어 붙이고, 줄 끝은 백슬래시로 이어야 한다.
이 파일은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;여러 종류의 메타 등록&lt;/strong&gt;(ApplicationListener,
FailureAnalyzer, EnvironmentPostProcessor 등)을 한 곳에서 관리하는 만능
창고였다. 그런데 항목이 늘면서 &amp;quot;자동구성 클래스 하나 추가할 때마다
줄바꿈·이스케이프·콤마를 신경 써야 한다&amp;quot;는 불편이 쌓였다.&lt;/p&gt;
&lt;h3 id=&quot;3-2-boot-27--3x-imports-한-줄-한-클래스&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-2) Boot 2.7+ / 3.x:
.imports 한 줄 한 클래스&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot 2.7에서 자동구성 전용 파일이 새로 도입됐고, Boot 3에서는 이
경로가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;유일한 표준&lt;/strong&gt;이 됐다.&lt;/p&gt;
&lt;pre style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration
com.example.OtherAutoConfiguration
# 주석도 지원됨&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 줄에 한 클래스 FQCN, 주석은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;#&lt;/code&gt;. 이스케이프나 콤마가
필요 없다. 파일 이름이 바로 키라 편집이 단순하다.&lt;/p&gt;
&lt;h3 id=&quot;3-3-두-방식-비교&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-3) 두 방식 비교&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;항목&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;spring.factories (구)&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AutoConfiguration.imports (신)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;경로&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;META-INF/spring.factories&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;포맷&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;key=cls1,cls2,\&lt;/code&gt; 연쇄&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;한 줄에 한 FQCN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;주석&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Properties 주석&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;#&lt;/code&gt; 주석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Boot 2.7&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;유효 (호환)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;유효 (권장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Boot 3.x&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;자동구성 키 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;무시됨&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;다른 용도&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;ApplicationListener 등 계속 유지&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;자동구성 전용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. spring.factories 자체가 사라진 것은 아니다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ApplicationListener&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;EnvironmentPostProcessor&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FailureAnalyzer&lt;/code&gt; 등은 Boot 3에서도 여전히
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.factories&lt;/code&gt;를 쓴다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;자동구성 키만&amp;quot; Boot 3에서
버려졌다.&lt;/strong&gt; 직접 만든 starter를 2.x에서 3.x로 올리면서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.factories&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;EnableAutoConfiguration=&lt;/code&gt;
블록만 남겨두면 해당 자동구성이 통째로 로딩되지 않는다. 마이그레이션
가이드는 이 파일 분리를 첫 번째 체크 항목으로 언급한다.&lt;/p&gt;
&lt;h3 id=&quot;3-4-마이그레이션-체크리스트&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-4) 마이그레이션 체크리스트&lt;/h3&gt;
&lt;pre class=&quot;text&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;# 구조 예시
src/main/resources/
├── META-INF/
│   ├── spring.factories                         # Listener, Processor 등은 유지
│   └── spring/
│       └── org.springframework.boot.autoconfigure.AutoConfiguration.imports&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;마이그레이션 시 해야 할 일은 간단하다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.factories&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;EnableAutoConfiguration=&lt;/code&gt;
블록을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;통째로 옮겨 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.imports&lt;/code&gt; 파일로 만들되&lt;/strong&gt;,
한 줄에 한 FQCN으로 정리하고, 이어붙임 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;\&lt;/code&gt;와 콤마를 제거한다.
IDE 플러그인이 자동으로 변환해주기도 한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-autoconfiguration-애너테이션과-proxybeanmethodsfalse&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4)
@AutoConfiguration 애너테이션과 proxyBeanMethods=false&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot 2.7에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfiguration&lt;/code&gt;이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;과 별도 어노테이션으로 분리됐다. 이전에는
자동구성도 그냥 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;으로 선언하고 그 위에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnClass&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter&lt;/code&gt;를
수동으로 쌓아야 했다. 지금은 전용 어노테이션이 이 패턴을 묶어준다.&lt;/p&gt;
&lt;h3 id=&quot;4-1-어노테이션-구조&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-1) 어노테이션 구조&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 개략화 — 실제 정의는 before/after가 메타의 value에 @AliasFor로 연결된다&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Target&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;ElementType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;TYPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-3&quot;&gt;&lt;a href=&quot;#cb6-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Retention&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;RetentionPolicy&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;RUNTIME&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-4&quot;&gt;&lt;a href=&quot;#cb6-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Configuration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;proxyBeanMethods &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-5&quot;&gt;&lt;a href=&quot;#cb6-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfigureBefore&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-6&quot;&gt;&lt;a href=&quot;#cb6-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfigureAfter&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-7&quot;&gt;&lt;a href=&quot;#cb6-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;@interface&lt;/span&gt; AutoConfiguration &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-8&quot;&gt;&lt;a href=&quot;#cb6-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@AliasFor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;annotation &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Configuration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-9&quot;&gt;&lt;a href=&quot;#cb6-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-10&quot;&gt;&lt;a href=&quot;#cb6-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-11&quot;&gt;&lt;a href=&quot;#cb6-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;?&amp;gt;[]&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;before&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-12&quot;&gt;&lt;a href=&quot;#cb6-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;beforeName&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-13&quot;&gt;&lt;a href=&quot;#cb6-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;?&amp;gt;[]&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;after&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-14&quot;&gt;&lt;a href=&quot;#cb6-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;afterName&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-15&quot;&gt;&lt;a href=&quot;#cb6-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureBefore&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter&lt;/code&gt;가 메타로 달려 있고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfiguration&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;before&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;after&lt;/code&gt;
속성은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AliasFor&lt;/code&gt;로 각 메타 어노테이션의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;value&lt;/code&gt;에 연결된다. 즉
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfiguration(after = X.class)&lt;/code&gt; 한 줄이 실제로는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter(X.class)&lt;/code&gt;와 동일한 메타데이터를 주입하는
셈이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;세 가지 요점이 있다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration(proxyBeanMethods = false)&lt;/code&gt;&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;을 메타로 달고 있지만 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;프록시를
끈다&lt;/strong&gt;. 일반 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;은 CGLIB 프록시를 생성해
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Bean&lt;/code&gt; 메서드끼리의 참조를 싱글턴으로 유지하는데, 자동구성은
이런 교차 참조를 거의 쓰지 않으므로 프록시를 끄는 편이 기동 속도에
이득이다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;before&lt;/code&gt; / &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;after&lt;/code&gt; 속성&lt;/strong&gt;:
예전에는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter&lt;/code&gt; 별도 어노테이션을 달아야 했던
것을, 이제는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfiguration(after = X.class)&lt;/code&gt; 한 줄에
녹일 수 있다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Lite 모드로 동작&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;proxyBeanMethods = false&lt;/code&gt;인 설정 클래스는 &amp;quot;Lite
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;&amp;quot;으로 분류돼, CGLIB 생성 비용이 없고 final
클래스도 허용된다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;4-2-proxybeanmethodsfalse가-의미하는-것&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-2)
proxyBeanMethods=false가 의미하는 것&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;일반 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;에서 메서드 간 참조가 어떻게
동작하는지 떠올려보자.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb7&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb7-1&quot;&gt;&lt;a href=&quot;#cb7-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Configuration&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// proxyBeanMethods = true (기본)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-2&quot;&gt;&lt;a href=&quot;#cb7-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; Normal &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-3&quot;&gt;&lt;a href=&quot;#cb7-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt; A &lt;span class=&quot;fu&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-4&quot;&gt;&lt;a href=&quot;#cb7-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt; B &lt;span class=&quot;fu&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// a()를 호출하지만 싱글턴 보장됨 (프록시가 가로챔)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-5&quot;&gt;&lt;a href=&quot;#cb7-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Normal&lt;/code&gt; 클래스는 런타임에 CGLIB 서브클래스로 감싸져
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;a()&lt;/code&gt; 호출이 컨테이너 조회로 리다이렉트된다. 그래서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;b()&lt;/code&gt; 안의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;a()&lt;/code&gt;는 매번 새 인스턴스를 만들지 않고
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;같은 싱글턴&lt;/strong&gt;을 돌려받는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;자동구성에서는 이 패턴을 거의 쓰지 않는다. 빈끼리의 의존은 생성자
파라미터로 주입되는 게 일반적이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb8&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb8-1&quot;&gt;&lt;a href=&quot;#cb8-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfiguration&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// proxyBeanMethods = false&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-2&quot;&gt;&lt;a href=&quot;#cb8-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; MyAuto &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-3&quot;&gt;&lt;a href=&quot;#cb8-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt; A &lt;span class=&quot;fu&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-4&quot;&gt;&lt;a href=&quot;#cb8-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt; B &lt;span class=&quot;fu&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;A a&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// 컨테이너가 a를 주입&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-5&quot;&gt;&lt;a href=&quot;#cb8-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 경우 프록시가 불필요하고, CGLIB 서브클래스 생성 비용이 사라진다.
기동 시간 최적화가 일차 목적이고, AOT/네이티브 이미지에서도
유리하다.&lt;/p&gt;
&lt;h3 id=&quot;4-3-주의-bean-메서드-간-직접-호출-금지&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-3) 주의: @Bean 메서드
간 직접 호출 금지&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfiguration&lt;/code&gt;이나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration(proxyBeanMethods = false)&lt;/code&gt;에서 실수로 메서드
직접 호출을 쓰면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;싱글턴 보장이 깨진다&lt;/strong&gt;.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb9&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb9-1&quot;&gt;&lt;a href=&quot;#cb9-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기: @Bean 메서드를 직접 호출&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-2&quot;&gt;&lt;a href=&quot;#cb9-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfiguration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-3&quot;&gt;&lt;a href=&quot;#cb9-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; Risky &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-4&quot;&gt;&lt;a href=&quot;#cb9-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt; A &lt;span class=&quot;fu&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-5&quot;&gt;&lt;a href=&quot;#cb9-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt; B &lt;span class=&quot;fu&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// ← a()가 매번 new A()를 반환. 싱글턴 깨짐&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-6&quot;&gt;&lt;a href=&quot;#cb9-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-7&quot;&gt;&lt;a href=&quot;#cb9-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-8&quot;&gt;&lt;a href=&quot;#cb9-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: 생성자 주입&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-9&quot;&gt;&lt;a href=&quot;#cb9-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfiguration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-10&quot;&gt;&lt;a href=&quot;#cb9-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; Safe &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-11&quot;&gt;&lt;a href=&quot;#cb9-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt; A &lt;span class=&quot;fu&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-12&quot;&gt;&lt;a href=&quot;#cb9-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt; B &lt;span class=&quot;fu&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;A a&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-13&quot;&gt;&lt;a href=&quot;#cb9-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;proxyBeanMethods = false&lt;/code&gt;는 &amp;quot;메서드 호출을 가로채지
않겠다&amp;quot;는 선언이다. 그 전제를 지키려면 메서드 간 참조는 파라미터
주입으로 써야 한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-조건부-어노테이션-패밀리-자동구성에서-쓰이는-여덟-가지&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5)
조건부 어노테이션 패밀리: 자동구성에서 쓰이는 여덟 가지&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;5편에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Conditional&lt;/code&gt;의 뿌리를 다뤘다. 이 섹션은 그 중
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동구성에서 특히 자주 쓰이는 여덟 가지&lt;/strong&gt;를 자동구성
관점에서 다시 정리한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어노테이션&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;평가 대상&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동구성 용도&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnClass&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;클래스패스&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;라이브러리가 있어야만 동작 (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RedisTemplate&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSource&lt;/code&gt; 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingClass&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;클래스패스&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;특정 라이브러리가 없을 때 대체 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnBean&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;이미 등록된 빈 정의&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;다른 자동구성이 등록한 빈에 이어 붙이기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;이미 등록된 빈 정의&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;사용자 정의 우선&lt;/strong&gt; — 자동구성 패턴의 핵심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnProperty&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Environment&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;기능 토글, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;matchIfMissing&lt;/code&gt; 기본 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnWebApplication&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;컨텍스트 타입&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SERVLET&lt;/code&gt; / &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;REACTIVE&lt;/code&gt; / &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ANY&lt;/code&gt;
구분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnResource&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;ResourceLoader&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;설정 파일 존재 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnJava&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;JVM 버전&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;특정 Java 버전 이상에서만&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;5-1-평가-순서와-단계-분리&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-1) 평가 순서와 단계 분리&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;앞서 §2에서 언급한 &amp;quot;두 단계 필터&amp;quot;를 다시 보자.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AutoConfigurationImportFilter&lt;/code&gt;가 걸러내는 건
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;클래스패스만 보면 되는&lt;/strong&gt;
조건(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnClass&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnWebApplication&lt;/code&gt;)이다. 이 단계에서 떨어진
자동구성은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt; 등록조차 되지 않는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;그 다음 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;으로 등록된 자동구성에 대해
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;빈 정의를 봐야 하는&lt;/strong&gt;
조건(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnBean&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;)이 평가된다. 이 지점이 함정이다.
이 순서 때문에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnBean(X.class)&lt;/code&gt;이 성공하려면
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;X를 등록하는 자동구성이 먼저 처리되어야&lt;/strong&gt; 하고, 이게 &lt;a href=&quot;#7-autoconfigurebeforeafter와-순서-의존성&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;§7 순서&lt;/a&gt;에서 다시
나올 핵심 이슈다.&lt;/p&gt;
&lt;h3 id=&quot;5-2-conditionalonproperty의-matchifmissing&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-2)
@ConditionalOnProperty의 matchIfMissing&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnProperty(name = &amp;quot;x.y.z&amp;quot;, havingValue = &amp;quot;true&amp;quot;)&lt;/code&gt;는
프로퍼티가 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;없으면&lt;/strong&gt; 기본적으로 조건 불성립(빈
미등록)이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;matchIfMissing = true&lt;/code&gt;를 명시해야 &amp;quot;프로퍼티
없을 때 기본으로 켠다&amp;quot;가 된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb10&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb10-1&quot;&gt;&lt;a href=&quot;#cb10-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 환경변수가 사라지면 조용히 기능 꺼짐&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-2&quot;&gt;&lt;a href=&quot;#cb10-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConditionalOnProperty&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;app.feature.enabled&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; havingValue &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-3&quot;&gt;&lt;a href=&quot;#cb10-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-4&quot;&gt;&lt;a href=&quot;#cb10-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 기본 켜짐, 명시적으로 끄려면 &amp;quot;false&amp;quot; 설정&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-5&quot;&gt;&lt;a href=&quot;#cb10-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConditionalOnProperty&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;app.feature.enabled&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; havingValue &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; matchIfMissing &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot가 기본으로 켜 두고 싶은 기능(예:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.jmx.enabled&lt;/code&gt;는 Boot 2.2+에서 기본 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;false&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.aop.auto&lt;/code&gt; 기본 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;true&lt;/code&gt;)은 이 두 속성의
조합으로 방향을 잡는다. 내 자동구성을 만들 때도 디폴트 상태를 명확히
문서화하려면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;matchIfMissing&lt;/code&gt;을 꼭 의식적으로 설정한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-conditionalonmissingbean-자동구성-패턴의-핵심&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6)
@ConditionalOnMissingBean: 자동구성 패턴의 핵심&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;자동구성 설계의 중심은 이 한 어노테이션이다. &amp;quot;사용자가 자기 빈을
등록했으면 내 기본값은 양보한다&amp;quot;는 규칙이 모든 자동구성에 박혀 있다.&lt;/p&gt;
&lt;h3 id=&quot;6-1-datasourceautoconfiguration의-실제-패턴&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-1)
DataSourceAutoConfiguration의 실제 패턴&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot 3의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceAutoConfiguration&lt;/code&gt; 내부 한 조각을
개략화해서 보자.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb11&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb11-1&quot;&gt;&lt;a href=&quot;#cb11-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfiguration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-2&quot;&gt;&lt;a href=&quot;#cb11-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConditionalOnClass&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;DataSource&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; EmbeddedDatabaseType&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-3&quot;&gt;&lt;a href=&quot;#cb11-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; DataSourceAutoConfiguration &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-4&quot;&gt;&lt;a href=&quot;#cb11-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-5&quot;&gt;&lt;a href=&quot;#cb11-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Configuration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;proxyBeanMethods &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-6&quot;&gt;&lt;a href=&quot;#cb11-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Conditional&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;PooledDataSourceCondition&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-7&quot;&gt;&lt;a href=&quot;#cb11-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@ConditionalOnMissingBean&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;DataSource&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;XADataSource&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// ← 핵심&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-8&quot;&gt;&lt;a href=&quot;#cb11-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Import&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;({&lt;/span&gt;DataSourceConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;Hikari&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-9&quot;&gt;&lt;a href=&quot;#cb11-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; PooledDataSourceConfiguration &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-10&quot;&gt;&lt;a href=&quot;#cb11-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean({DataSource.class, XADataSource.class})&lt;/code&gt;
한 줄이 전체 방향을 결정한다. 사용자 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Bean DataSource&lt;/code&gt;가 없을 때만 Boot가 HikariCP 기반
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSource&lt;/code&gt;를 만들고, 사용자가 직접 정의하면 이 블록 전체가
건너뛰어진다.&lt;/p&gt;
&lt;h3 id=&quot;6-2-사용자-입장에서의-동작-시나리오&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-2) 사용자 입장에서의 동작
시나리오&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;세 가지 시나리오로 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;시나리오 A&lt;/strong&gt;: 사용자가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Bean DataSource&lt;/code&gt;를 두지 않음 → Boot가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.datasource.url&lt;/code&gt;을 읽어
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HikariDataSource&lt;/code&gt;를 생성해 등록&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;시나리오 B&lt;/strong&gt;: 사용자가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Bean public DataSource myDs() { ... }&lt;/code&gt;를 정의 → Boot의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceAutoConfiguration&lt;/code&gt; 내부 분기가
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;통째로&lt;/strong&gt; 스킵됨. 사용자 빈만 존재&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;시나리오 C&lt;/strong&gt;: 사용자가 두 개의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Bean DataSource&lt;/code&gt;를 정의 → 이건
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;의 문제가 아니라 일반 빈 충돌.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Primary&lt;/code&gt;나 이름 지정으로 해결&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;시나리오 B에서 &amp;quot;Boot가 조용히 꺼진다&amp;quot;가 포인트다. 에러도 경고도 없다.
이걸 의도한 동작이라는 점을 모르면 &amp;quot;Boot가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HikariDataSource&lt;/code&gt;를 안 쓴다&amp;quot;는 버그 리포트처럼 보일 수
있다.&lt;/p&gt;
&lt;h3 id=&quot;6-3-자동구성-밖에서-쓰면-비결정적&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-3) 자동구성 밖에서 쓰면
비결정적&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;5편에서도 짚었지만 다시 강조한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동구성
클래스에서만&lt;/strong&gt; 안전하게 동작한다. 사용자
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt;에서 쓰면 같은 레벨의 사용자
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Configuration&lt;/code&gt; 간 처리 순서가 보장되지 않기 때문에 &amp;quot;오늘은
되고 내일은 안 되는&amp;quot; 버그의 근원이 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 자주 걸린다. 내 앱 코드 안에서 &amp;quot;사용자가 안 두면 기본값&amp;quot;을
구현하고 싶다면 방법은 두 가지다. 하나는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동구성으로
승격&lt;/strong&gt;해 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AutoConfiguration.imports&lt;/code&gt;에 등록하는 것,
다른 하나는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt; 대신
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ObjectProvider&lt;/strong&gt;나 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본 인스턴스 생성자
주입&lt;/strong&gt;으로 바꾸는 것이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb12&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb12-1&quot;&gt;&lt;a href=&quot;#cb12-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기: 사용자 @Configuration에서 @ConditionalOnMissingBean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-2&quot;&gt;&lt;a href=&quot;#cb12-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Configuration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-3&quot;&gt;&lt;a href=&quot;#cb12-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; MyConfig &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-4&quot;&gt;&lt;a href=&quot;#cb12-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-5&quot;&gt;&lt;a href=&quot;#cb12-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@ConditionalOnMissingBean&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;Clock&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// ← 순서 비결정적&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-6&quot;&gt;&lt;a href=&quot;#cb12-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; Clock &lt;span class=&quot;fu&quot;&gt;clock&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; Clock&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;systemUTC&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-7&quot;&gt;&lt;a href=&quot;#cb12-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-8&quot;&gt;&lt;a href=&quot;#cb12-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-9&quot;&gt;&lt;a href=&quot;#cb12-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: 기본값 주입 패턴&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-10&quot;&gt;&lt;a href=&quot;#cb12-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Configuration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-11&quot;&gt;&lt;a href=&quot;#cb12-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; MyConfig &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-12&quot;&gt;&lt;a href=&quot;#cb12-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-13&quot;&gt;&lt;a href=&quot;#cb12-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; MyService &lt;span class=&quot;fu&quot;&gt;myService&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ObjectProvider&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;Clock&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; clockProvider&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-14&quot;&gt;&lt;a href=&quot;#cb12-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        Clock clock &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; clockProvider&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getIfAvailable&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;Clock&lt;span class=&quot;op&quot;&gt;::&lt;/span&gt;systemUTC&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-15&quot;&gt;&lt;a href=&quot;#cb12-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MyService&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;clock&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-16&quot;&gt;&lt;a href=&quot;#cb12-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-17&quot;&gt;&lt;a href=&quot;#cb12-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ObjectProvider&lt;/code&gt;는 해당 타입 빈이 없으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;getIfAvailable&lt;/code&gt;의 폴백을 돌려준다. 순서 의존성이 없고,
컨텍스트 조립이 끝난 뒤에 결정되므로 안전하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-autoconfigurebeforeafter와-순서-의존성&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7)
AutoConfigureBefore/After와 순서 의존성&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;자동구성 여러 개가 얽힐 때 누가 먼저 처리되는지가 문제로 돌아온다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnBean(X.class)&lt;/code&gt;이 통과하려면 X를 등록하는
자동구성이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;먼저&lt;/strong&gt; 처리되어야 한다.&lt;/p&gt;
&lt;h3 id=&quot;7-1-세-가지-순서-제어-수단&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-1) 세 가지 순서 제어 수단&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;수단&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;용도&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;예시&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureBefore&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;대상 자동구성보다 먼저 처리&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureBefore(WebMvcAutoConfiguration.class)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;대상 자동구성 뒤에 처리&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter(DataSourceAutoConfiguration.class)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureOrder&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Boot 내부 순서 상수&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfiguration&lt;/code&gt;으로 선언하면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;before&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;after&lt;/code&gt; 속성으로 직접 지정할 수 있어
어노테이션을 겹겹이 쌓을 필요가 없다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb13&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb13-1&quot;&gt;&lt;a href=&quot;#cb13-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfiguration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;after &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; DataSourceAutoConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-2&quot;&gt;&lt;a href=&quot;#cb13-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConditionalOnBean&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;DataSource&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-3&quot;&gt;&lt;a href=&quot;#cb13-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; MyJdbcAutoConfiguration &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-4&quot;&gt;&lt;a href=&quot;#cb13-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-5&quot;&gt;&lt;a href=&quot;#cb13-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; MyJdbcHelper &lt;span class=&quot;fu&quot;&gt;myJdbcHelper&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;DataSource&lt;/span&gt; ds&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-6&quot;&gt;&lt;a href=&quot;#cb13-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;7-2-왜-순서가-필요한가&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-2) 왜 순서가 필요한가&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MyJdbcAutoConfiguration&lt;/code&gt;이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnBean(DataSource.class)&lt;/code&gt;을 쓴다면,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceAutoConfiguration&lt;/code&gt;이 먼저 돌면서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSource&lt;/code&gt; 빈을 등록해야 조건이 통과한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter&lt;/code&gt;를 안 적으면 어느 쪽이 먼저 처리될지
보장이 없어서, 내 자동구성이 &amp;quot;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSource&lt;/code&gt;가 안 보인다&amp;quot;며
조용히 꺼져버릴 수 있다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;주의할 점이 하나 있다. 같은 자동구성 안에서 정의된 빈끼리는 순서가
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동으로&lt;/strong&gt; 맞춰지지만, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;다른 자동구성 간에는
명시&lt;/strong&gt;가 필요하다. 내 starter가 제공하는 자동구성이 Boot의 공식
자동구성 뒤에 와야 하는 경우가 대부분이라
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter&lt;/code&gt; 선언은 거의 반사적으로 들어간다.&lt;/p&gt;
&lt;h3 id=&quot;7-3-순서는-빈-주입-순서가-아니다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-3) 순서는 빈 주입 순서가
아니다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter&lt;/code&gt;가 보장하는 건 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동구성
클래스가 처리되는 순서&lt;/strong&gt;지, 빈이 생성되는 순서가 아니다. 빈 생성
순서는 의존 그래프로 결정된다. 이 둘을 혼동하면
&amp;quot;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter&lt;/code&gt;를 붙였는데도 내 빈이 다른 빈보다 먼저
생성된다&amp;quot;는 오해가 생긴다. 빈 생성 순서를 제어하려면 생성자 주입으로
의존을 명시하거나 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@DependsOn&lt;/code&gt;을 쓴다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-조건-평가-디버깅---debug와-actuator-conditions&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8) 조건 평가
디버깅: --debug와 Actuator /conditions&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;자동구성이 기대대로 동작하지 않을 때 가장 먼저 켜야 할 것이
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CONDITIONS EVALUATION REPORT&lt;/strong&gt;다.&lt;/p&gt;
&lt;h3 id=&quot;8-1---debug-플래그&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-1) --debug 플래그&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;JVM 옵션 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;-Ddebug=true&lt;/code&gt; 또는 명령줄 인자
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--debug&lt;/code&gt;를 주고 앱을 띄우면, 기동 로그 끝에 긴 리포트가
찍힌다. 로그 레벨을 다 올리는 게 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;조건 평가 정보만
덤프&lt;/strong&gt;한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb14&quot;&gt;&lt;pre class=&quot;sourceCode bash&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode bash&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb14-1&quot;&gt;&lt;a href=&quot;#cb14-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;java&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;-jar&lt;/span&gt; myapp.jar &lt;span class=&quot;at&quot;&gt;--debug&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-2&quot;&gt;&lt;a href=&quot;#cb14-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;# 또는&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-3&quot;&gt;&lt;a href=&quot;#cb14-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;java&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;-Ddebug&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;true &lt;span class=&quot;at&quot;&gt;-jar&lt;/span&gt; myapp.jar&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;리포트는 세 섹션으로 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Positive matches&lt;/strong&gt;: 조건을 모두 통과해 등록된
자동구성 + 어떤 조건으로 통과했는지&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Negative matches&lt;/strong&gt;: 조건에서 탈락한 자동구성 +
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어느 조건에서 왜&lt;/strong&gt; 탈락했는지&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Exclusions / Unconditional classes&lt;/strong&gt;: 사용자가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;exclude&lt;/code&gt;로 지정한 것, 조건 없이 늘 적용되는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;8-2-리포트-해석-예&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-2) 리포트 해석 예&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceAutoConfiguration&lt;/code&gt;이 Positive에 있는 경우의
전형적 출력이다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceAutoConfiguration matched:
  - @ConditionalOnClass found required classes
    &amp;#39;javax.sql.DataSource&amp;#39;, &amp;#39;org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType&amp;#39;
    (OnClassCondition)

DataSourceAutoConfiguration.PooledDataSourceConfiguration matched:
  - AnyNestedCondition on DataSourceAutoConfiguration.PooledDataSourceCondition
  - @ConditionalOnMissingBean (types: javax.sql.DataSource,javax.sql.XADataSource;
    SearchStrategy: all) did not find any beans (OnBeanCondition)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;같은 자동구성이 Negative에 나오는 경우는 이렇다.&lt;/p&gt;
&lt;pre class=&quot;text&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RedisAutoConfiguration:
  Did not match:
    - @ConditionalOnClass did not find required class
      &amp;#39;org.springframework.data.redis.connection.RedisConnectionFactory&amp;#39;
      (OnClassCondition)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;**&amp;quot;did not find required class&amp;quot;**는 클래스패스에 라이브러리가 없다는
뜻이고, **&amp;quot;found ... beans&amp;quot;**는 사용자가 이미 빈을 등록했다는 뜻이다. 이
두 문구만 읽어도 자동구성이 꺼진 이유의 90%가 해결된다.&lt;/p&gt;
&lt;h3 id=&quot;8-3-actuator-actuatorconditions&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-3) Actuator
/actuator/conditions&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;기동 중인 앱의 조건 평가를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;런타임에 JSON으로&lt;/strong&gt; 조회할
수 있다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-actuator&lt;/code&gt;를 추가하고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;management.endpoints.web.exposure.include=conditions&lt;/code&gt;를 켜면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/conditions&lt;/code&gt;로 같은 정보가 나온다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb17&quot;&gt;&lt;pre class=&quot;sourceCode bash&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode bash&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb17-1&quot;&gt;&lt;a href=&quot;#cb17-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;ex&quot;&gt;curl&lt;/span&gt; http://localhost:8080/actuator/conditions &lt;span class=&quot;kw&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;ex&quot;&gt;jq&lt;/span&gt; .&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;로그를 뒤지는 것보다 빠르고, 운영 환경에서 &amp;quot;이 배포본이 어떤
자동구성을 켰는지&amp;quot; 확인할 때 유용하다. 민감 정보는 아니지만 내부 구조를
드러내므로 외부 노출은 피하는 게 일반적이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;9-자동구성-비활성화-방법&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;9) 자동구성 비활성화 방법&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;의도적으로 특정 자동구성을 끄고 싶은 경우가 있다. 프로퍼티 바인딩이
다른 DB를 쓰고 있는데 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceAutoConfiguration&lt;/code&gt;이 기동 시
에러를 내거나, 보안 자동구성이 테스트에서 방해가 되는 식이다.&lt;/p&gt;
&lt;h3 id=&quot;9-1-두-가지-방법&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-1) 두 가지 방법&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;방법 A:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootApplication(exclude = ...)&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb18&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb18-1&quot;&gt;&lt;a href=&quot;#cb18-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@SpringBootApplication&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;exclude &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; DataSourceAutoConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-2&quot;&gt;&lt;a href=&quot;#cb18-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; MyApp &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;코드로 명시. 해당 클래스를 import해야 하므로 클래스패스에 실제로 있는
자동구성에만 쓴다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;방법 B: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.autoconfigure.exclude&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb19&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb19-1&quot;&gt;&lt;a href=&quot;#cb19-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-2&quot;&gt;&lt;a href=&quot;#cb19-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;autoconfigure&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-3&quot;&gt;&lt;a href=&quot;#cb19-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;exclude&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-4&quot;&gt;&lt;a href=&quot;#cb19-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-5&quot;&gt;&lt;a href=&quot;#cb19-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;문자열이므로 클래스패스에 없어도 지정 가능. 환경별로 껐다 켰다 할 수
있어 운영에서 더 유연하다.&lt;/p&gt;
&lt;h3 id=&quot;9-2-비활성화는-최후-수단&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-2) 비활성화는 최후 수단&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;자동구성을 끄기 전에 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;프로퍼티로 기능을 끌 수 있는지&lt;/strong&gt;
먼저 확인한다. 대부분의 자동구성은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;xxx.enabled=false&lt;/code&gt; 같은
스위치를 제공한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb20&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb20-1&quot;&gt;&lt;a href=&quot;#cb20-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-2&quot;&gt;&lt;a href=&quot;#cb20-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;jmx&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-3&quot;&gt;&lt;a href=&quot;#cb20-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-4&quot;&gt;&lt;a href=&quot;#cb20-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;sql&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-5&quot;&gt;&lt;a href=&quot;#cb20-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-6&quot;&gt;&lt;a href=&quot;#cb20-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; never&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-7&quot;&gt;&lt;a href=&quot;#cb20-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;management&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-8&quot;&gt;&lt;a href=&quot;#cb20-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-9&quot;&gt;&lt;a href=&quot;#cb20-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;shutdown&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-10&quot;&gt;&lt;a href=&quot;#cb20-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;ch&quot;&gt;false&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;프로퍼티 스위치가 있는데도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;exclude&lt;/code&gt;로 끊어버리면
자동구성이 제공하는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;다른 필요한 빈&lt;/strong&gt;까지 사라진다.
자동구성 하나는 보통 여러 빈을 같이 등록하므로, 필요한 건 남기고 필요
없는 것만 끄는 프로퍼티 방식이 안전하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;10-enablewebmvc-함정과-자동구성-해제&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;10) @EnableWebMvc 함정과
자동구성 해제&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;14편에서 이미 짚은 주제지만 자동구성 관점에서 한 번 더 정리한다. 이
함정은 자동구성 조건부 설계의 대표 사례다.&lt;/p&gt;
&lt;h3 id=&quot;10-1-webmvcautoconfiguration의-게이트&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-1)
WebMvcAutoConfiguration의 게이트&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcAutoConfiguration&lt;/code&gt;은 다음 조건을 건다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb21&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb21-1&quot;&gt;&lt;a href=&quot;#cb21-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfiguration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;after &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; DispatcherServletAutoConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-2&quot;&gt;&lt;a href=&quot;#cb21-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConditionalOnWebApplication&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;type &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;SERVLET&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-3&quot;&gt;&lt;a href=&quot;#cb21-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConditionalOnClass&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;({&lt;/span&gt; Servlet&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; DispatcherServlet&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; WebMvcConfigurationSupport&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-4&quot;&gt;&lt;a href=&quot;#cb21-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ConditionalOnMissingBean&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;WebMvcConfigurationSupport&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// ← 이 줄&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-5&quot;&gt;&lt;a href=&quot;#cb21-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfigureOrder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;Ordered&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;HIGHEST_PRECEDENCE&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dv&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-6&quot;&gt;&lt;a href=&quot;#cb21-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; WebMvcAutoConfiguration &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;주목할 건
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)&lt;/code&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcConfigurationSupport&lt;/code&gt; 타입 빈이 이미 있으면 이
자동구성이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;통째로&lt;/strong&gt; 꺼진다.&lt;/p&gt;
&lt;h3 id=&quot;10-2-enablewebmvc의-내부&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-2) @EnableWebMvc의 내부&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;사용자가 무심코 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableWebMvc&lt;/code&gt;를 붙이면 내부에서 다음이
일어난다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb22&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb22-1&quot;&gt;&lt;a href=&quot;#cb22-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Target&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;ElementType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;TYPE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-2&quot;&gt;&lt;a href=&quot;#cb22-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Retention&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;RetentionPolicy&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;RUNTIME&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-3&quot;&gt;&lt;a href=&quot;#cb22-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Import&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;DelegatingWebMvcConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-4&quot;&gt;&lt;a href=&quot;#cb22-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;@interface&lt;/span&gt; EnableWebMvc &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableWebMvc&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DelegatingWebMvcConfiguration&lt;/code&gt;을 import하고, 이 클래스는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcConfigurationSupport&lt;/code&gt;를 상속한 서브클래스다. 즉
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableWebMvc&lt;/code&gt;를 붙이면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;컨텍스트에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcConfigurationSupport&lt;/code&gt; 빈이 등록&lt;/strong&gt;되고, 위에서
본 조건이 깨져 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcAutoConfiguration&lt;/code&gt;이 전면 해제된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;그 결과로 사라지는 것은 적지 않다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;Jackson/JSON &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MessageConverter&lt;/code&gt; 자동 등록&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;정적 리소스 매핑 (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/static/**&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/public/**&lt;/code&gt;)&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DefaultErrorAttributes&lt;/code&gt;와 기본 에러 페이지&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ContentNegotiationManager&lt;/code&gt; 기본 구성&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcProperties&lt;/code&gt; 바인딩&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;10-3-함정-회피&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-3) 함정 회피&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;MVC 설정을 커스터마이즈하고 싶을 때 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableWebMvc&lt;/code&gt;를 쓰면
안 된다. 대신 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcConfigurer&lt;/code&gt; 빈을 추가해 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동구성
위에 얹는다&lt;/strong&gt;.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb23&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb23-1&quot;&gt;&lt;a href=&quot;#cb23-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 피하기: @EnableWebMvc로 자동구성 통째 해제&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-2&quot;&gt;&lt;a href=&quot;#cb23-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Configuration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-3&quot;&gt;&lt;a href=&quot;#cb23-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@EnableWebMvc&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-4&quot;&gt;&lt;a href=&quot;#cb23-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; BadMvcConfig &lt;span class=&quot;kw&quot;&gt;implements&lt;/span&gt; WebMvcConfigurer &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-5&quot;&gt;&lt;a href=&quot;#cb23-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-6&quot;&gt;&lt;a href=&quot;#cb23-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;addInterceptors&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;InterceptorRegistry registry&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-7&quot;&gt;&lt;a href=&quot;#cb23-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        registry&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;addInterceptor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MyInterceptor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-8&quot;&gt;&lt;a href=&quot;#cb23-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-9&quot;&gt;&lt;a href=&quot;#cb23-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-10&quot;&gt;&lt;a href=&quot;#cb23-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-11&quot;&gt;&lt;a href=&quot;#cb23-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 선호: @EnableWebMvc 제거, Configurer만&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-12&quot;&gt;&lt;a href=&quot;#cb23-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Configuration&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-13&quot;&gt;&lt;a href=&quot;#cb23-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; GoodMvcConfig &lt;span class=&quot;kw&quot;&gt;implements&lt;/span&gt; WebMvcConfigurer &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-14&quot;&gt;&lt;a href=&quot;#cb23-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-15&quot;&gt;&lt;a href=&quot;#cb23-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;addInterceptors&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;InterceptorRegistry registry&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-16&quot;&gt;&lt;a href=&quot;#cb23-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        registry&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;addInterceptor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;MyInterceptor&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-17&quot;&gt;&lt;a href=&quot;#cb23-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-18&quot;&gt;&lt;a href=&quot;#cb23-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;후자가 Boot의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcAutoConfiguration&lt;/code&gt;이 제공하는 모든
기본값을 유지한 채 인터셉터만 추가한다. 이 패턴이 Boot 자동구성을
확장하는 공식 방식이다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Configurer 빈을 등록해 얹는다&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebSecurityCustomizer&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RestTemplateCustomizer&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JpaVendorAdapter&lt;/code&gt; 등도 같은 철학이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;11-테스트에서-자동구성-선별-로드&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;11) 테스트에서 자동구성 선별
로드&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;전체 앱 컨텍스트(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootTest&lt;/code&gt;)는 무겁다. 자동구성 몇
개만 로드해서 테스트하고 싶을 때가 있다.&lt;/p&gt;
&lt;h3 id=&quot;11-1-importautoconfiguration&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;11-1)
@ImportAutoConfiguration&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb24&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb24-1&quot;&gt;&lt;a href=&quot;#cb24-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@SpringJUnitConfig&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-2&quot;&gt;&lt;a href=&quot;#cb24-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ImportAutoConfiguration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-3&quot;&gt;&lt;a href=&quot;#cb24-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    DataSourceAutoConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-4&quot;&gt;&lt;a href=&quot;#cb24-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    JdbcTemplateAutoConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-5&quot;&gt;&lt;a href=&quot;#cb24-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-6&quot;&gt;&lt;a href=&quot;#cb24-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; JdbcSliceTest &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-7&quot;&gt;&lt;a href=&quot;#cb24-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Autowired&lt;/span&gt; JdbcTemplate jdbcTemplate&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-8&quot;&gt;&lt;a href=&quot;#cb24-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// ...&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-9&quot;&gt;&lt;a href=&quot;#cb24-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;지정한 자동구성만 로드한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AutoConfiguration.imports&lt;/code&gt;
전체를 읽지 않고, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;명시 리스트만&lt;/strong&gt; 처리한다. 슬라이스
테스트가 빠른 이유다.&lt;/p&gt;
&lt;h3 id=&quot;11-2-boot-슬라이스-테스트-어노테이션&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;11-2) Boot 슬라이스 테스트
어노테이션&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot가 제공하는 슬라이스 테스트는 내부적으로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ImportAutoConfiguration&lt;/code&gt;을 미리 세팅해둔 합성
어노테이션이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어노테이션&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;로드하는 자동구성 (요지)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WebMvcTest&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcAutoConfiguration&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebFluxAutoConfiguration&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JacksonAutoConfiguration&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HttpMessageConvertersAutoConfiguration&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ValidationAutoConfiguration&lt;/code&gt; 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@DataJpaTest&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceAutoConfiguration&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HibernateJpaAutoConfiguration&lt;/code&gt; 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@JdbcTest&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DataSourceAutoConfiguration&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JdbcTemplateAutoConfiguration&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@JsonTest&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JacksonAutoConfiguration&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GsonAutoConfiguration&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@RestClientTest&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RestTemplateAutoConfiguration&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JacksonAutoConfiguration&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;각각 필요한 자동구성만 선별 로드하고, 나머지는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.autoconfigure.exclude&lt;/code&gt; 상당의 처리로 빼놓는다.
슬라이스 테스트가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootTest&lt;/code&gt;보다 훨씬 빠른 이유가 이
선별 로딩이다.&lt;/p&gt;
&lt;h3 id=&quot;11-3-내-starter-테스트에서의-패턴&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;11-3) 내 starter 테스트에서의
패턴&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;커스텀 starter를 개발 중이라면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ApplicationContextRunner&lt;/code&gt;를 쓴다. 자동구성 조건을 단위
테스트처럼 확인할 수 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb25&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb25-1&quot;&gt;&lt;a href=&quot;#cb25-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-2&quot;&gt;&lt;a href=&quot;#cb25-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;dataSource가_없으면_자동구성이_꺼진다&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-3&quot;&gt;&lt;a href=&quot;#cb25-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;ApplicationContextRunner&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-4&quot;&gt;&lt;a href=&quot;#cb25-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withConfiguration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;AutoConfigurations&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;MyJdbcAutoConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-5&quot;&gt;&lt;a href=&quot;#cb25-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;context &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-6&quot;&gt;&lt;a href=&quot;#cb25-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;fu&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;doesNotHaveBean&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;MyJdbcHelper&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-7&quot;&gt;&lt;a href=&quot;#cb25-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-8&quot;&gt;&lt;a href=&quot;#cb25-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-9&quot;&gt;&lt;a href=&quot;#cb25-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-10&quot;&gt;&lt;a href=&quot;#cb25-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-11&quot;&gt;&lt;a href=&quot;#cb25-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;dataSource가_있으면_helper가_등록된다&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-12&quot;&gt;&lt;a href=&quot;#cb25-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;ApplicationContextRunner&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-13&quot;&gt;&lt;a href=&quot;#cb25-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withConfiguration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;AutoConfigurations&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-14&quot;&gt;&lt;a href=&quot;#cb25-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            DataSourceAutoConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-15&quot;&gt;&lt;a href=&quot;#cb25-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            MyJdbcAutoConfiguration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-16&quot;&gt;&lt;a href=&quot;#cb25-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withPropertyValues&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;spring.datasource.url=jdbc:h2:mem:test&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-17&quot;&gt;&lt;a href=&quot;#cb25-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;context &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-18&quot;&gt;&lt;a href=&quot;#cb25-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;fu&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;hasSingleBean&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;MyJdbcHelper&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-19&quot;&gt;&lt;a href=&quot;#cb25-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-20&quot;&gt;&lt;a href=&quot;#cb25-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ApplicationContextRunner&lt;/code&gt;는 매 테스트마다 가벼운
컨텍스트를 새로 띄우고 닫는다. 자동구성의 조건 분기를 하나씩 검증하기에
가장 적합한 도구다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;12-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;12) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기동 시 자동구성 의심이 들면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--debug&lt;/code&gt;부터&lt;/strong&gt; 붙인다. Positive/Negative 섹션의 &amp;quot;did
not find required class&amp;quot; / &amp;quot;found beans&amp;quot; 문구 두 개로 80% 원인이
해결된다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Boot 버전 업그레이드(특히 2.x → 3.x)에서 자동구성이 안
잡히면&lt;/strong&gt; &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.factories&lt;/code&gt;의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;EnableAutoConfiguration=&lt;/code&gt; 블록을
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports&lt;/code&gt;로
옮긴다. 나머지 키(ApplicationListener, FailureAnalyzer 등)는 그대로
둔다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;내 자동구성은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfiguration(after = ...)&lt;/code&gt;으로 순서를
명시&lt;/strong&gt;한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnBean&lt;/code&gt;을 쓰는 순간 after
선언이 반사적으로 필요하다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;사용자 앱 코드에서는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;을 쓰지 않는다.&lt;/strong&gt; 필요하면
자동구성으로 승격하거나, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ObjectProvider.getIfAvailable&lt;/code&gt;로
대체한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동구성 커스터마이즈는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebMvcConfigurer&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebSecurityCustomizer&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RestTemplateCustomizer&lt;/code&gt;
같은 Configurer 빈으로 얹는다.&lt;/strong&gt; &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableWebMvc&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableWebSecurity&lt;/code&gt; 같은 스위치는 자동구성을 통째로
해제한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동구성을 끄고 싶으면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;xxx.enabled=false&lt;/code&gt;
프로퍼티가 있는지 먼저 확인&lt;/strong&gt;한다. 없을 때만
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring.autoconfigure.exclude&lt;/code&gt;로 간다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;운영 중 조건 평가가 궁금하면 Actuator
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/actuator/conditions&lt;/code&gt;&lt;/strong&gt;. 같은 정보가 JSON으로
나온다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;starter를 개발한다면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ApplicationContextRunner&lt;/code&gt;로
조건 분기를 단위 테스트&lt;/strong&gt;한다. &amp;quot;있을 때 / 없을 때&amp;quot;를 한 파일에서
확인하면 리뷰가 쉽다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfiguration&lt;/code&gt;의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;proxyBeanMethods = false&lt;/code&gt;를 기억&lt;/strong&gt;한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Bean&lt;/code&gt; 메서드끼리 직접 호출하면 싱글턴이 깨진다. 생성자
파라미터 주입을 쓴다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnProperty&lt;/code&gt;의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;matchIfMissing&lt;/code&gt;을 반드시 의식&lt;/strong&gt;한다. 기본
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;false&lt;/code&gt;라 프로퍼티가 사라지면 기능이 조용히 꺼진다. 기본
켜짐을 원하면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;true&lt;/code&gt; 명시&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;13-한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;13) 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Boot 자동구성은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AutoConfiguration.imports&lt;/code&gt; 파일의
FQCN 목록을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AutoConfigurationImportSelector&lt;/code&gt;가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import&lt;/code&gt;로 주입하고, 그 위에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ConditionalOnMissingBean&lt;/code&gt;으로 사용자 정의를 우선시하는 조립
파이프라인이다&lt;/strong&gt;. 마법이 아니라 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Conditional&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import&lt;/code&gt; + 정해진 순서
계약(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DeferredImportSelector&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureAfter&lt;/code&gt;)의 조합으로 돌아간다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;--debug&lt;/code&gt;의 CONDITIONS EVALUATION REPORT 두 줄이
자동구성 장애의 90%를 설명&lt;/strong&gt;하므로 로그부터 읽는 습관이 가장 큰
무기다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;태그&lt;/strong&gt;: spring-boot, autoconfiguration,
conditional-on-missing-bean, import-selector,
auto-configuration-imports, enable-auto-configuration,
proxy-bean-methods, spring-boot-3, starter,
conditions-evaluation-report&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/Spring</category>
      <category>auto-configuration-imports</category>
      <category>autoconfiguration</category>
      <category>conditional-on-missing-bean</category>
      <category>conditions-evaluation-report</category>
      <category>enable-auto-configuration</category>
      <category>import-selector</category>
      <category>proxy-bean-methods</category>
      <category>spring-boot</category>
      <category>spring-boot-3</category>
      <category>starter</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/405</guid>
      <comments>https://dding-shark.tistory.com/405#entry405comment</comments>
      <pubDate>Tue, 14 Apr 2026 22:05:18 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security Testing &amp;mdash; @WithMockUser&amp;middot;MockMvc&amp;middot;Reactive 테스트 전략</title>
      <link>https://dding-shark.tistory.com/404</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;spring-security-testing--withmockusermockmvcreactive-테스트-전략&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;Spring
Security Testing — @WithMockUser·MockMvc·Reactive 테스트 전략&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Security를 써본 사람은 인증·인가를 한 번쯤 붙여본다. 그런데
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;그걸 테스트로 덮는 순간 두 번째 벽이 생긴다&lt;/strong&gt;. 로컬에서
브라우저로는 멀쩡히 로그인되는 컨트롤러가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WebMvcTest&lt;/code&gt;에서는
401이나 403으로 돌아오고, POST 요청은 전부 CSRF로 튕기고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize&lt;/code&gt;를 단위 테스트로 덮었는데 권한 검증이 아예
돌지 않는다. 테스트만 통과하면 배포되는 환경에서는 이게 그대로 &amp;quot;가짜
안전망&amp;quot;이 된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Security 6.x / Spring Boot 3.x / Spring Test 6.x 기준으로
테스트에서 막히는 지점은 거의 정해져 있다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;의 차이, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MockMvc&lt;/code&gt;에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf())&lt;/code&gt;를 빼먹었을 때의 403,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@InjectMocks&lt;/code&gt; 단위 테스트에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize&lt;/code&gt;가
통째로 무력화되는 상황, 그리고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;roles&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authorities&lt;/code&gt; 옵션의 접두사 차이까지. 한 번 정리해두면 다시
같은 실수를 하지 않는다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;POST 테스트가 403인데 CSRF 토큰을 까먹었다&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf())&lt;/code&gt;가 빠진 신호다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;를 붙였는데
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetailsService&lt;/code&gt;가 안 불린다&lt;/strong&gt; — 그건
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;가 할 일이다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;단위 테스트로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize&lt;/code&gt;를 검증하려다
무력화를 몰랐다&lt;/strong&gt; — Spring 컨텍스트 없이는 AOP 프록시가 없다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser(roles=&amp;quot;ADMIN&amp;quot;)&lt;/code&gt;가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasAuthority(&amp;quot;ADMIN&amp;quot;)&lt;/code&gt;은 실패시킨다&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;roles&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ROLE_&lt;/code&gt; 접두사가 붙지만
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authorities&lt;/code&gt;는 안 붙는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;왜 → 어노테이션 내부 → MockMvc 포스트프로세서 →
메서드 시큐리티 → Reactive → 실무&lt;/strong&gt; 순으로 정리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-왜-security-testing은-따로-배워야-하나&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1) 왜 Security
Testing은 따로 배워야 하나&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-withmockuser-내부-testsecuritycontextholder&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2)
@WithMockUser 내부: TestSecurityContextHolder&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-withanonymoususer-withuserdetails-커스텀-withsecuritycontext&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3)
@WithAnonymousUser, @WithUserDetails, 커스텀
@WithSecurityContext&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-securitymockmvcrequestpostprocessors-총정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4)
SecurityMockMvcRequestPostProcessors 총정리&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-csrf-필수-postputdelete-테스트-함정&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5) csrf() 필수:
POST/PUT/DELETE 테스트 함정&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-formlogin-logout-authenticated-matcher&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6) formLogin(),
logout(), authenticated() matcher&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-webmvctest-vs-springboottest--autoconfiguremockmvc&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7)
@WebMvcTest vs @SpringBootTest + @AutoConfigureMockMvc&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-method-security-테스트-aop-프록시-조건&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8) Method
Security 테스트: AOP 프록시 조건&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#9-jwt--oauth2-mock-jwtclaimauthorities&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;9) JWT / OAuth2
mock: jwt().claim().authorities()&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#10-webflux-webtestclient--mockuser--mockjwt&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;10) WebFlux:
WebTestClient + mockUser / mockJwt&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#11-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;11) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#12-한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;12) 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-왜-security-testing은-따로-배워야-하나&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1) 왜 Security
Testing은 따로 배워야 하나&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Security를 붙인 애플리케이션은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;컨트롤러에 도달하기
전에&lt;/strong&gt; 필터 체인이 먼저 움직인다. 인증 여부 확인, CSRF 검증, 권한
평가, 세션 관리가 전부 필터 단계에서 끝난다. 이 말은 컨트롤러 단위
테스트에서 필터 체인이 빠져 있으면 &amp;quot;보안 없는 컨트롤러&amp;quot;를 테스트하는
셈이 된다는 뜻이다. 반대로 필터 체인이 전부 배치되면, 이번엔 테스트에서
인증을 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어떻게 주입할 것인가&lt;/strong&gt;라는 문제가 생긴다.&lt;/p&gt;
&lt;h3 id=&quot;1-1-세-가지-테스트-축&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-1) 세 가지 테스트 축&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Security의 테스트 지원은 크게 세 축이다. 이 셋을 섞어 쓰는 게
실무 패턴이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;축&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;도구&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;용도&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;컨텍스트 주입&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithSecurityContext&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;테스트 메서드 실행 전에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContext&lt;/code&gt;를 선제
주입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;요청별 주입&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityMockMvcRequestPostProcessors&lt;/code&gt;
(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;user()&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jwt()&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;csrf()&lt;/code&gt; 등)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MockMvc.perform(...).with(...)&lt;/code&gt; 체인으로 요청마다 다른
인증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;응답 검증&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityMockMvcResultMatchers&lt;/code&gt;
(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authenticated()&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;unauthenticated()&lt;/code&gt;)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;로그인 결과·세션 상태 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;어노테이션 기반은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;테스트 메서드 전체에 영향&lt;/strong&gt;을 주고,
포스트프로세서 기반은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;한 요청에만 적용&lt;/strong&gt;된다. 같은 테스트
클래스 안에서 섞어 쓸 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;1-2-spring-security-test-의존성&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;1-2)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-security-test&lt;/code&gt; 의존성&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 모든 API는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;org.springframework.security:spring-security-test&lt;/code&gt;
아티팩트에 들어 있다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-test&lt;/code&gt;에는 자동
포함되지 않으므로 별도로 추가해야 한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre class=&quot;sourceCode kotlin&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode kotlin&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// build.gradle.kts&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;testImplementation&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;org.springframework.security:spring-security-test&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이게 없으면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt; 어노테이션은 존재하지만
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TestExecutionListener&lt;/code&gt;가 등록되지 않아 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;아무 일도 안
일어난다&lt;/strong&gt;. 여기서 한 번 막히면 로그에도 특별한 힌트가 없어서
오래 헤맨다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-withmockuser-내부-testsecuritycontextholder&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2) @WithMockUser
내부: TestSecurityContextHolder&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;는 Spring Security Test의 대표
어노테이션이다. 메서드나 클래스 위에 붙이면 테스트 실행 직전에
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;가짜 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt;이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContext&lt;/code&gt;에 주입&lt;/strong&gt;된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-3&quot;&gt;&lt;a href=&quot;#cb2-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 기본_사용자로_실행된다&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-4&quot;&gt;&lt;a href=&quot;#cb2-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// username=user, password=password, roles=USER&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-5&quot;&gt;&lt;a href=&quot;#cb2-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    Authentication auth &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; SecurityContextHolder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getAuthentication&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-6&quot;&gt;&lt;a href=&quot;#cb2-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;fu&quot;&gt;assertThat&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getName&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isEqualTo&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-7&quot;&gt;&lt;a href=&quot;#cb2-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;기본값은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;username=&amp;quot;user&amp;quot;&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;password=&amp;quot;password&amp;quot;&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;roles={&amp;quot;USER&amp;quot;}&lt;/code&gt;다.
옵션으로 전부 바꿀 수 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;username &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; roles &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 관리자_테스트&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-4&quot;&gt;&lt;a href=&quot;#cb3-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-5&quot;&gt;&lt;a href=&quot;#cb3-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-6&quot;&gt;&lt;a href=&quot;#cb3-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;authorities &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;SCOPE_read&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;SCOPE_write&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-7&quot;&gt;&lt;a href=&quot;#cb3-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;OAuth_스코프_테스트&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;2-1-동작-원리&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-1) 동작 원리&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;내부는 단순하다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WithSecurityContextTestExecutionListener&lt;/code&gt;라는 JUnit 확장이
테스트 메서드 실행 전에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;를 읽어서
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UsernamePasswordAuthenticationToken&lt;/code&gt;을 만든 뒤
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TestSecurityContextHolder&lt;/code&gt;에 넣는다&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TestSecurityContextHolder&lt;/code&gt;는 테스트 범위용
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContextHolder&lt;/code&gt; 대체물이고, 각 테스트 메서드 시작
시점에 값이 전역 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContextHolder&lt;/code&gt;로 복사된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;29_spring-security-testing-01&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;29_spring-security-testing-01&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;테스트 격리는 여기서 보장된다. 각 테스트 메서드 끝날 때마다
컨텍스트가 리셋되므로 테스트 간 인증 상태가 새어나가지 않는다.&lt;/p&gt;
&lt;h3 id=&quot;2-2-roles-vs-authorities의-role_-접두사&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-2) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;roles&lt;/code&gt;
vs &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authorities&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ROLE_&lt;/code&gt; 접두사&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. 두 옵션은 이름만 다른 게 아니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;옵션&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ROLE_&lt;/code&gt; 접두사&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;예시 입력&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;실제 권한&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;roles = {&amp;quot;ADMIN&amp;quot;}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자동으로 붙음&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ROLE_ADMIN&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authorities = {&amp;quot;ADMIN&amp;quot;}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;안 붙음&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ADMIN&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이게 왜 중요하냐면, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize(&amp;quot;hasRole(&amp;#39;ADMIN&amp;#39;)&amp;quot;)&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize(&amp;quot;hasAuthority(&amp;#39;ADMIN&amp;#39;)&amp;quot;)&lt;/code&gt;이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;완전히
다른 조건을 검사하기&lt;/strong&gt; 때문이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasRole(&amp;#39;ADMIN&amp;#39;)&lt;/code&gt;은
내부에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ROLE_ADMIN&lt;/code&gt;을 찾고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasAuthority(&amp;#39;ADMIN&amp;#39;)&lt;/code&gt;은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;접두사 없이&lt;/strong&gt;
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ADMIN&lt;/code&gt;을 찾는다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-2&quot;&gt;&lt;a href=&quot;#cb4-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;roles &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;co&quot;&gt;// 내부적으로 ROLE_ADMIN&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-3&quot;&gt;&lt;a href=&quot;#cb4-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;hasRole은_통과&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-4&quot;&gt;&lt;a href=&quot;#cb4-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// @PreAuthorize(&amp;quot;hasRole(&amp;#39;ADMIN&amp;#39;)&amp;quot;) 통과&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-5&quot;&gt;&lt;a href=&quot;#cb4-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// @PreAuthorize(&amp;quot;hasAuthority(&amp;#39;ADMIN&amp;#39;)&amp;quot;) 실패 — ROLE_ADMIN != ADMIN&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-6&quot;&gt;&lt;a href=&quot;#cb4-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-7&quot;&gt;&lt;a href=&quot;#cb4-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-8&quot;&gt;&lt;a href=&quot;#cb4-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-9&quot;&gt;&lt;a href=&quot;#cb4-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;authorities &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-10&quot;&gt;&lt;a href=&quot;#cb4-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;hasAuthority만_통과&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-11&quot;&gt;&lt;a href=&quot;#cb4-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// @PreAuthorize(&amp;quot;hasAuthority(&amp;#39;ADMIN&amp;#39;)&amp;quot;) 통과&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-12&quot;&gt;&lt;a href=&quot;#cb4-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// @PreAuthorize(&amp;quot;hasRole(&amp;#39;ADMIN&amp;#39;)&amp;quot;) 실패 — 접두사 ROLE_ 부재&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-13&quot;&gt;&lt;a href=&quot;#cb4-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;OAuth2 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SCOPE_*&lt;/code&gt; 패턴을 쓸 때는 반드시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authorities&lt;/code&gt;를 쓴다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;roles = &amp;quot;SCOPE_read&amp;quot;&lt;/code&gt;로
주면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ROLE_SCOPE_read&lt;/code&gt;가 되어 의미가 망가진다.&lt;/p&gt;
&lt;h3 id=&quot;2-3-어디에-붙일-수-있나&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;2-3) 어디에 붙일 수 있나&lt;/h3&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;테스트 메서드 위&lt;/strong&gt;: 해당 메서드만 적용&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;테스트 클래스 위&lt;/strong&gt;: 모든 메서드에 기본 적용,
메서드별로 재정의 가능&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;메타 어노테이션&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;를 감싼
커스텀 어노테이션 가능 (&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithAdmin&lt;/code&gt; 같은 축약형)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-withanonymoususer-withuserdetails-커스텀-withsecuritycontext&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3)
@WithAnonymousUser, @WithUserDetails, 커스텀 @WithSecurityContext&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt; 하나로 대부분 커버되지만, 상황에 따라 세
가지 형제가 필요하다.&lt;/p&gt;
&lt;h3 id=&quot;3-1-withanonymoususer&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-1) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithAnonymousUser&lt;/code&gt;&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;익명 사용자 상태를 강제한다. 클래스 위에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;가
걸려 있을 때 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;특정 메서드만&lt;/strong&gt; 익명으로 테스트하고 싶을 때
쓴다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb5&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb5-1&quot;&gt;&lt;a href=&quot;#cb5-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@SpringBootTest&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-2&quot;&gt;&lt;a href=&quot;#cb5-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;roles &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-3&quot;&gt;&lt;a href=&quot;#cb5-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; OrderControllerTest &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-4&quot;&gt;&lt;a href=&quot;#cb5-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-5&quot;&gt;&lt;a href=&quot;#cb5-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-6&quot;&gt;&lt;a href=&quot;#cb5-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 로그인_사용자는_주문조회&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-7&quot;&gt;&lt;a href=&quot;#cb5-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-8&quot;&gt;&lt;a href=&quot;#cb5-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-9&quot;&gt;&lt;a href=&quot;#cb5-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@WithAnonymousUser&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-10&quot;&gt;&lt;a href=&quot;#cb5-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 익명사용자는_401&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-11&quot;&gt;&lt;a href=&quot;#cb5-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;3-2-withuserdetails&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-2) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;와 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;자주 혼동되는
지점&lt;/strong&gt;이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;는 아무 빈도 건드리지 않고
토큰을 만들어 박는다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetailsService&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;호출되지
않는다&lt;/strong&gt;. 반면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;는 스프링
컨텍스트에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetailsService&lt;/code&gt; 빈을 찾아
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;loadUserByUsername(value)&lt;/code&gt;를 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;실제로
호출&lt;/strong&gt;한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithUserDetails&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;admin@example.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;co&quot;&gt;// 실제 DB 또는 InMemory 조회&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-3&quot;&gt;&lt;a href=&quot;#cb6-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 실제_사용자_데이터로_테스트&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;실제 DB 기반 통합 테스트나, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetails&lt;/code&gt; 커스텀 구현체의
필드(예: tenantId, organizationId)를 써야 하는 인가 로직을 검증할 때
쓴다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어노테이션&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetailsService&lt;/code&gt; 호출&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;용도&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;안 함&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;단순한 권한 조합만 필요한 테스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;함&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;실제 사용자 데이터·커스텀 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetails&lt;/code&gt; 필드 의존
로직&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;컨텍스트가 없는 컨트롤러 단위 테스트(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WebMvcTest&lt;/code&gt; 없이
Mockito만)에서는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;는 동작하지 않는다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetailsService&lt;/code&gt; 빈이 없기 때문이다.&lt;/p&gt;
&lt;h3 id=&quot;3-3-커스텀-withsecuritycontext&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-3) 커스텀
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithSecurityContext&lt;/code&gt;&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt; 타입 자체를 바꾸고 싶을 때 쓴다. 예를
들어 JWT 토큰 기반 커스텀 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Principal&lt;/code&gt;을 주입하려면 팩토리를
만들어야 한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb7&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb7-1&quot;&gt;&lt;a href=&quot;#cb7-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Retention&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;RetentionPolicy&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;RUNTIME&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-2&quot;&gt;&lt;a href=&quot;#cb7-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithSecurityContext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;factory &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; WithMockJwtSecurityContextFactory&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-3&quot;&gt;&lt;a href=&quot;#cb7-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;at&quot;&gt;@interface&lt;/span&gt; WithMockJwt &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-4&quot;&gt;&lt;a href=&quot;#cb7-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;subject&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-5&quot;&gt;&lt;a href=&quot;#cb7-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;scopes&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{};&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-6&quot;&gt;&lt;a href=&quot;#cb7-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-7&quot;&gt;&lt;a href=&quot;#cb7-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-8&quot;&gt;&lt;a href=&quot;#cb7-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; WithMockJwtSecurityContextFactory&lt;/span&gt;
&lt;span id=&quot;cb7-9&quot;&gt;&lt;a href=&quot;#cb7-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;kw&quot;&gt;implements&lt;/span&gt; WithSecurityContextFactory&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;WithMockJwt&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-10&quot;&gt;&lt;a href=&quot;#cb7-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-11&quot;&gt;&lt;a href=&quot;#cb7-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Override&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-12&quot;&gt;&lt;a href=&quot;#cb7-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; SecurityContext &lt;span class=&quot;fu&quot;&gt;createSecurityContext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;WithMockJwt annotation&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-13&quot;&gt;&lt;a href=&quot;#cb7-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        Jwt jwt &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; Jwt&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withTokenValue&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;token&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-14&quot;&gt;&lt;a href=&quot;#cb7-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;header&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alg&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;none&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-15&quot;&gt;&lt;a href=&quot;#cb7-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;claim&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;sub&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; annotation&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;subject&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-16&quot;&gt;&lt;a href=&quot;#cb7-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-17&quot;&gt;&lt;a href=&quot;#cb7-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;dt&quot;&gt;var&lt;/span&gt; authorities &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Arrays&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;annotation&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;scopes&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-18&quot;&gt;&lt;a href=&quot;#cb7-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;s &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;SimpleGrantedAuthority&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;SCOPE_&amp;quot;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;+&lt;/span&gt; s&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-19&quot;&gt;&lt;a href=&quot;#cb7-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;toList&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-20&quot;&gt;&lt;a href=&quot;#cb7-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;dt&quot;&gt;var&lt;/span&gt; auth &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;JwtAuthenticationToken&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;jwt&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; authorities&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-21&quot;&gt;&lt;a href=&quot;#cb7-21&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        SecurityContext ctx &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; SecurityContextHolder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;createEmptyContext&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-22&quot;&gt;&lt;a href=&quot;#cb7-22&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        ctx&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;setAuthentication&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;auth&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-23&quot;&gt;&lt;a href=&quot;#cb7-23&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; ctx&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-24&quot;&gt;&lt;a href=&quot;#cb7-24&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-25&quot;&gt;&lt;a href=&quot;#cb7-25&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb8&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb8-1&quot;&gt;&lt;a href=&quot;#cb8-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-2&quot;&gt;&lt;a href=&quot;#cb8-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockJwt&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;subject &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; scopes &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;read&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;write&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-3&quot;&gt;&lt;a href=&quot;#cb8-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;JWT_스코프_테스트&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;팀 내에서 같은 패턴이 10개 이상 반복되면 커스텀 어노테이션으로 빼는
게 테스트 가독성이 훨씬 좋아진다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-securitymockmvcrequestpostprocessors-총정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4)
SecurityMockMvcRequestPostProcessors 총정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;어노테이션 기반이 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;요청별로&lt;/strong&gt; 인증 상태를 바꾸고
싶을 때는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MockMvc&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(...)&lt;/code&gt; 체인을 쓴다.
정적 import로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityMockMvcRequestPostProcessors.*&lt;/code&gt;를
가져오면 코드가 짧아진다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb9&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb9-1&quot;&gt;&lt;a href=&quot;#cb9-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;static&lt;/span&gt;&lt;span class=&quot;im&quot;&gt; org&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;springframework&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;servlet&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;SecurityMockMvcRequestPostProcessors&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.*;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-2&quot;&gt;&lt;a href=&quot;#cb9-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-3&quot;&gt;&lt;a href=&quot;#cb9-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/api/orders&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-4&quot;&gt;&lt;a href=&quot;#cb9-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;admin&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-5&quot;&gt;&lt;a href=&quot;#cb9-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;csrf&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-6&quot;&gt;&lt;a href=&quot;#cb9-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;MediaType&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-7&quot;&gt;&lt;a href=&quot;#cb9-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;{&lt;/span&gt;&lt;span class=&quot;sc&quot;&gt;\&amp;quot;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;itemId&lt;/span&gt;&lt;span class=&quot;sc&quot;&gt;\&amp;quot;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;:1}&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-8&quot;&gt;&lt;a href=&quot;#cb9-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isCreated&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;4-1-주요-포스트프로세서-매트릭스&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-1) 주요 포스트프로세서
매트릭스&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;메서드&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;용도&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;주요 옵션&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;user(&amp;quot;name&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;임시 사용자 주입&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.roles(...)&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.authorities(...)&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.password(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;anonymous()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;익명 사용자&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authentication(auth)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;커스텀 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt; 직접 주입&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;httpBasic(&amp;quot;u&amp;quot;, &amp;quot;p&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HTTP Basic 헤더 추가&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;csrf()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;CSRF 토큰 자동 생성·첨부&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.asHeader()&lt;/code&gt; (헤더로 보내기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jwt()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JwtAuthenticationToken&lt;/code&gt; mock&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.jwt(j -&amp;gt; j.claim(&amp;quot;sub&amp;quot;, ...).authorities(...))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;opaqueToken()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Opaque 토큰 인증 mock&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.attributes(a -&amp;gt; a.put(&amp;quot;sub&amp;quot;, ...))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Login()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;OAuth2 Client 로그인 상태 mock&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.authorities(...)&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.attributes(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Client()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClient&lt;/code&gt; 주입&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.principalName(...)&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.accessToken(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;4-2-user-vs-withmockuser&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-2) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;user()&lt;/code&gt; vs
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;둘은 거의 같은 일을 한다. 차이는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;적용 범위&lt;/strong&gt;다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb10&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb10-1&quot;&gt;&lt;a href=&quot;#cb10-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 어노테이션: 메서드 전체&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-2&quot;&gt;&lt;a href=&quot;#cb10-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-3&quot;&gt;&lt;a href=&quot;#cb10-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;roles &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-4&quot;&gt;&lt;a href=&quot;#cb10-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;admin_전용&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-5&quot;&gt;&lt;a href=&quot;#cb10-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/admin/dashboard&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-6&quot;&gt;&lt;a href=&quot;#cb10-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-7&quot;&gt;&lt;a href=&quot;#cb10-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-8&quot;&gt;&lt;a href=&quot;#cb10-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-9&quot;&gt;&lt;a href=&quot;#cb10-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 포스트프로세서: 이 요청만&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-10&quot;&gt;&lt;a href=&quot;#cb10-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-11&quot;&gt;&lt;a href=&quot;#cb10-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 한_테스트_안에_역할_두_번&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-12&quot;&gt;&lt;a href=&quot;#cb10-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/me&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-13&quot;&gt;&lt;a href=&quot;#cb10-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-14&quot;&gt;&lt;a href=&quot;#cb10-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/admin&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;bob&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-15&quot;&gt;&lt;a href=&quot;#cb10-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-16&quot;&gt;&lt;a href=&quot;#cb10-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 테스트 메서드 안에서 여러 사용자로 여러 요청을 날려야 하면
포스트프로세서가 답이다.&lt;/p&gt;
&lt;h3 id=&quot;4-3-authenticationauth로-완전-커스텀-주입&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;4-3)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authentication(auth)&lt;/code&gt;로 완전 커스텀 주입&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;특수한 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt; 타입(멀티테넌트 토큰, 커스텀
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Principal&lt;/code&gt; 등)을 넣을 때 쓴다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb11&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb11-1&quot;&gt;&lt;a href=&quot;#cb11-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;var&lt;/span&gt; tenantAuth &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;TenantAwareAuthenticationToken&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-2&quot;&gt;&lt;a href=&quot;#cb11-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;tenant-42&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;SimpleGrantedAuthority&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;ROLE_USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-3&quot;&gt;&lt;a href=&quot;#cb11-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-4&quot;&gt;&lt;a href=&quot;#cb11-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/api/me&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authentication&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;tenantAuth&lt;span class=&quot;op&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-5&quot;&gt;&lt;a href=&quot;#cb11-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-csrf-필수-postputdelete-테스트-함정&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5) csrf() 필수:
POST/PUT/DELETE 테스트 함정&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이게 실무에서 가장 자주 마주치는 함정이다. Spring Security는 기본
설정에서 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;상태 변경 HTTP 메서드(POST, PUT, DELETE, PATCH)에 CSRF
토큰을 요구&lt;/strong&gt;한다. 테스트에서도 똑같이 요구한다.&lt;/p&gt;
&lt;h3 id=&quot;5-1-잘못된-코드와-증상&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-1) 잘못된 코드와 증상&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;피해야 할 패턴:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb12&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb12-1&quot;&gt;&lt;a href=&quot;#cb12-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-2&quot;&gt;&lt;a href=&quot;#cb12-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;roles &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-3&quot;&gt;&lt;a href=&quot;#cb12-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 주문_생성&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-4&quot;&gt;&lt;a href=&quot;#cb12-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/api/orders&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-5&quot;&gt;&lt;a href=&quot;#cb12-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;MediaType&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-6&quot;&gt;&lt;a href=&quot;#cb12-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;{&lt;/span&gt;&lt;span class=&quot;sc&quot;&gt;\&amp;quot;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;itemId&lt;/span&gt;&lt;span class=&quot;sc&quot;&gt;\&amp;quot;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;:1}&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-7&quot;&gt;&lt;a href=&quot;#cb12-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isCreated&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt; &lt;span class=&quot;co&quot;&gt;// 실패: 403 Forbidden&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-8&quot;&gt;&lt;a href=&quot;#cb12-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 테스트는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;403 Forbidden&lt;/strong&gt;으로 실패한다. 로그를 보면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Invalid CSRF token found for ...&lt;/code&gt;. 분명 인증은 됐는데 왜
거부당할까. 필터 체인의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CsrfFilter&lt;/code&gt;가 POST에 CSRF 토큰이
없는 걸 보고 튕긴 것이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;선호 패턴:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb13&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb13-1&quot;&gt;&lt;a href=&quot;#cb13-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-2&quot;&gt;&lt;a href=&quot;#cb13-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;roles &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-3&quot;&gt;&lt;a href=&quot;#cb13-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 주문_생성&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-4&quot;&gt;&lt;a href=&quot;#cb13-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/api/orders&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-5&quot;&gt;&lt;a href=&quot;#cb13-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;csrf&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;  &lt;span class=&quot;co&quot;&gt;// ← 이거 한 줄&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-6&quot;&gt;&lt;a href=&quot;#cb13-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;MediaType&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-7&quot;&gt;&lt;a href=&quot;#cb13-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;{&lt;/span&gt;&lt;span class=&quot;sc&quot;&gt;\&amp;quot;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;itemId&lt;/span&gt;&lt;span class=&quot;sc&quot;&gt;\&amp;quot;&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;:1}&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-8&quot;&gt;&lt;a href=&quot;#cb13-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isCreated&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-9&quot;&gt;&lt;a href=&quot;#cb13-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf())&lt;/code&gt;는 유효한 CSRF 토큰을 자동 생성해서
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;파라미터로&lt;/strong&gt; 첨부한다. JSON API처럼 헤더로 보내고 싶으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf().asHeader())&lt;/code&gt;를 쓴다.&lt;/p&gt;
&lt;h3 id=&quot;5-2-rest-api는-csrf를-꺼두는-게-표준-아닌가&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-2) REST API는
CSRF를 꺼두는 게 표준 아닌가&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;맞다. stateless JWT API는 보통
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.csrf(csrf -&amp;gt; csrf.disable())&lt;/code&gt;로 꺼둔다. 이 설정을 테스트
컨텍스트에도 그대로 로드한다면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf())&lt;/code&gt;는 필요 없다.
그런데 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;테스트용 보안 설정이 따로 기본값으로 로드되고
있다면&lt;/strong&gt; CSRF가 켜져 있을 수 있다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WebMvcTest&lt;/code&gt;에서
커스텀 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityConfig&lt;/code&gt;를 import하지 않았다면 Spring Security
기본 설정(CSRF on)이 자동 적용된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;증상이 403이면 체크 순서는 이렇다.&lt;/p&gt;
&lt;ol type=&quot;1&quot; style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;컨트롤러가 실제로 실행되는가&lt;/strong&gt; — 브레이크포인트나
로그로 확인. 실행 안 되면 필터에서 튕긴 것&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CSRF 토큰이 있는가&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf())&lt;/code&gt;
추가&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;사용자 역할이 맞는가&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize&lt;/code&gt;나
HttpSecurity 매처 조건 확인&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;CORS가 원인인가&lt;/strong&gt; — 이건 거의 없지만 통합 테스트에서
가끔 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;5-3-get에는-csrf-불필요&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-3) GET에는 CSRF 불필요&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;GET은 기본적으로 상태 변경이 아닌 걸로 간주되므로 CSRF 검사를 안
한다. GET 테스트에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf())&lt;/code&gt;를 붙이면 경고는 안 나지만
의미 없는 노이즈다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-formlogin-logout-authenticated-matcher&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6) formLogin(),
logout(), authenticated() matcher&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;폼 기반 로그인 플로우를 통합 테스트하는 전용 API가 있다.&lt;/p&gt;
&lt;h3 id=&quot;6-1-formlogin--로그인-시도&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-1) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;formLogin()&lt;/code&gt; —
로그인 시도&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb14&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb14-1&quot;&gt;&lt;a href=&quot;#cb14-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;static&lt;/span&gt;&lt;span class=&quot;im&quot;&gt; org&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;springframework&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;servlet&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;SecurityMockMvcRequestBuilders&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.*;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-2&quot;&gt;&lt;a href=&quot;#cb14-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;static&lt;/span&gt;&lt;span class=&quot;im&quot;&gt; org&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;springframework&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;servlet&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;SecurityMockMvcResultMatchers&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.*;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-3&quot;&gt;&lt;a href=&quot;#cb14-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-4&quot;&gt;&lt;a href=&quot;#cb14-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-5&quot;&gt;&lt;a href=&quot;#cb14-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 로그인_성공&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-6&quot;&gt;&lt;a href=&quot;#cb14-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;secret&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-7&quot;&gt;&lt;a href=&quot;#cb14-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withUsername&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-8&quot;&gt;&lt;a href=&quot;#cb14-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;redirectedUrl&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/home&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-9&quot;&gt;&lt;a href=&quot;#cb14-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-10&quot;&gt;&lt;a href=&quot;#cb14-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-11&quot;&gt;&lt;a href=&quot;#cb14-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-12&quot;&gt;&lt;a href=&quot;#cb14-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 로그인_실패&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-13&quot;&gt;&lt;a href=&quot;#cb14-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;formLogin&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;wrong&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-14&quot;&gt;&lt;a href=&quot;#cb14-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;unauthenticated&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-15&quot;&gt;&lt;a href=&quot;#cb14-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;redirectedUrl&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/login?error&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb14-16&quot;&gt;&lt;a href=&quot;#cb14-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;formLogin()&lt;/code&gt;은 내부에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;POST /login&lt;/code&gt;에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;username&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;password&lt;/code&gt; 파라미터와 CSRF 토큰을
자동으로 넣어준다. 직접 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;post(&amp;quot;/login&amp;quot;)&lt;/code&gt;을 쓰는 것보다 훨씬
짧다.&lt;/p&gt;
&lt;h3 id=&quot;6-2-logout--로그아웃-플로우&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-2) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;logout()&lt;/code&gt; —
로그아웃 플로우&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb15&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb15-1&quot;&gt;&lt;a href=&quot;#cb15-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-2&quot;&gt;&lt;a href=&quot;#cb15-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-3&quot;&gt;&lt;a href=&quot;#cb15-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 로그아웃&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-4&quot;&gt;&lt;a href=&quot;#cb15-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-5&quot;&gt;&lt;a href=&quot;#cb15-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;unauthenticated&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-6&quot;&gt;&lt;a href=&quot;#cb15-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;redirectedUrl&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/login?logout&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb15-7&quot;&gt;&lt;a href=&quot;#cb15-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;logout()&lt;/code&gt;도 내부에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;POST /logout&lt;/code&gt; + CSRF를
처리한다.&lt;/p&gt;
&lt;h3 id=&quot;6-3-result-matcher&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-3) Result Matcher&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;matcher&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;검증 대상&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authenticated()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContext&lt;/code&gt;에 인증된 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt;이
있는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;unauthenticated()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;없거나 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Anonymous&lt;/code&gt;인가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.withUsername(&amp;quot;x&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;principal 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.withRoles(&amp;quot;ADMIN&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;권한에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ROLE_ADMIN&lt;/code&gt; 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.withAuthentication(m)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Hamcrest matcher로 세밀한 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb16&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb16-1&quot;&gt;&lt;a href=&quot;#cb16-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-2&quot;&gt;&lt;a href=&quot;#cb16-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withUsername&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb16-3&quot;&gt;&lt;a href=&quot;#cb16-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withRoles&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-webmvctest-vs-springboottest--autoconfiguremockmvc&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7)
@WebMvcTest vs @SpringBootTest + @AutoConfigureMockMvc&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;두 어노테이션의 선택은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;보안 설정을 포함해서 테스트할 것인가,
로직만 볼 것인가&lt;/strong&gt;로 갈린다.&lt;/p&gt;
&lt;h3 id=&quot;7-1-webmvctest&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-1) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WebMvcTest&lt;/code&gt;&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;컨트롤러 한 개 + MVC 관련 빈만 로드한다. 빠르지만 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;커스텀
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityConfig&lt;/code&gt;를 자동으로 안 불러온다&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;피해야 할 패턴:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb17&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb17-1&quot;&gt;&lt;a href=&quot;#cb17-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WebMvcTest&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;OrderController&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-2&quot;&gt;&lt;a href=&quot;#cb17-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; OrderControllerTest &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-3&quot;&gt;&lt;a href=&quot;#cb17-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// SecurityConfig가 로드되지 않아 Spring Security 기본값 적용&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-4&quot;&gt;&lt;a href=&quot;#cb17-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// — 기본값: 모든 요청 인증 필요 + CSRF on&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb17-5&quot;&gt;&lt;a href=&quot;#cb17-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;선호 패턴:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb18&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb18-1&quot;&gt;&lt;a href=&quot;#cb18-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@WebMvcTest&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;OrderController&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-2&quot;&gt;&lt;a href=&quot;#cb18-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Import&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;SecurityConfig&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;co&quot;&gt;// 실제 보안 설정 포함&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-3&quot;&gt;&lt;a href=&quot;#cb18-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; OrderControllerTest &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-4&quot;&gt;&lt;a href=&quot;#cb18-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Autowired&lt;/span&gt; MockMvc mockMvc&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-5&quot;&gt;&lt;a href=&quot;#cb18-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@MockBean&lt;/span&gt; OrderService orderService&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-6&quot;&gt;&lt;a href=&quot;#cb18-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb18-7&quot;&gt;&lt;a href=&quot;#cb18-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@MockBean UserDetailsService&lt;/code&gt;를 선언하고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;를 쓰면 헷갈린다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetailsService&lt;/code&gt;를
호출하지 않는다&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@MockBean&lt;/code&gt;은 빈 주입을
만족시키려고 필요한 것뿐이고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;는 아예 그 빈을
건드리지 않고 토큰을 만들어 박는다. 이 구분을 알면 &amp;quot;왜
when(...).thenReturn(...)을 해놨는데 안 불리지&amp;quot;라는 혼란이 없어진다.&lt;/p&gt;
&lt;h3 id=&quot;7-2-springboottest--autoconfiguremockmvc&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-2)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootTest&lt;/code&gt; + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureMockMvc&lt;/code&gt;&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;전체 컨텍스트를 로드한다. 실제 빈들이 전부 올라오고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityConfig&lt;/code&gt;도 자동 포함된다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb19&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb19-1&quot;&gt;&lt;a href=&quot;#cb19-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@SpringBootTest&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-2&quot;&gt;&lt;a href=&quot;#cb19-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@AutoConfigureMockMvc&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-3&quot;&gt;&lt;a href=&quot;#cb19-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; OrderIntegrationTest &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-4&quot;&gt;&lt;a href=&quot;#cb19-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Autowired&lt;/span&gt; MockMvc mockMvc&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-5&quot;&gt;&lt;a href=&quot;#cb19-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// 실제 UserDetailsService, 실제 PasswordEncoder, 실제 DB까지&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb19-6&quot;&gt;&lt;a href=&quot;#cb19-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;느리지만 완전한 통합 테스트다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;가 제대로
동작하는 것도 여기다.&lt;/p&gt;
&lt;h3 id=&quot;7-3-mockmvc-자동-security-통합&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-3) &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MockMvc&lt;/code&gt; 자동
Security 통합&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Boot 3.x의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AutoConfigureMockMvc&lt;/code&gt;는 Security 필터
체인을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MockMvc&lt;/code&gt;에 자동 배치한다. 따라서 별도 설정이 필요
없다. 반면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Boot 없이&lt;/strong&gt; raw Spring Test를 쓸 때는
명시적으로 붙여야 한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb20&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb20-1&quot;&gt;&lt;a href=&quot;#cb20-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// Spring Boot 없는 경우에만 필요&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-2&quot;&gt;&lt;a href=&quot;#cb20-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;mockMvc &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; MockMvcBuilders&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;webAppContextSetup&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ctx&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-3&quot;&gt;&lt;a href=&quot;#cb20-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;SecurityMockMvcConfigurers&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;springSecurity&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb20-4&quot;&gt;&lt;a href=&quot;#cb20-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이게 빠진 상태에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;를 쓰면 **컨트롤러의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Principal&lt;/code&gt;이 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;null&lt;/code&gt;**로 들어온다. 컨텍스트에는
auth가 있는데 필터를 안 거치니 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HttpServletRequest&lt;/code&gt;에
principal이 박히지 않는 것이다. 증상이 &amp;quot;auth는 있는데 컨트롤러에서는
null&amp;quot;이면 이걸 의심한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-method-security-테스트-aop-프록시-조건&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8) Method Security
테스트: AOP 프록시 조건&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PostAuthorize&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Secured&lt;/code&gt;는 전부 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AOP 프록시&lt;/strong&gt; 위에서
동작한다. 메서드 호출 시점에 프록시가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt;을 꺼내
조건을 평가하고, 실패하면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AccessDeniedException&lt;/code&gt;을 던진다.
프록시가 없으면 그냥 평범한 메서드 호출이 되어 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;어노테이션이
완전히 무시된다&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;8-1-단위-테스트에서-흔히-틀리는-패턴&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-1) 단위 테스트에서 흔히
틀리는 패턴&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;피해야 할 패턴:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb21&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb21-1&quot;&gt;&lt;a href=&quot;#cb21-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ExtendWith&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;MockitoExtension&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-2&quot;&gt;&lt;a href=&quot;#cb21-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; OrderServiceTest &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-3&quot;&gt;&lt;a href=&quot;#cb21-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-4&quot;&gt;&lt;a href=&quot;#cb21-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@InjectMocks&lt;/span&gt; OrderService service&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;co&quot;&gt;// ← 프록시 없음&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-5&quot;&gt;&lt;a href=&quot;#cb21-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Mock&lt;/span&gt; OrderRepository repo&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-6&quot;&gt;&lt;a href=&quot;#cb21-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-7&quot;&gt;&lt;a href=&quot;#cb21-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-8&quot;&gt;&lt;a href=&quot;#cb21-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;roles &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-9&quot;&gt;&lt;a href=&quot;#cb21-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 권한없는_사용자_거부&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-10&quot;&gt;&lt;a href=&quot;#cb21-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;co&quot;&gt;// 기대: AccessDeniedException&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-11&quot;&gt;&lt;a href=&quot;#cb21-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;co&quot;&gt;// 실제: 그냥 통과. @PreAuthorize가 아예 평가 안 됨&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-12&quot;&gt;&lt;a href=&quot;#cb21-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;fu&quot;&gt;assertThatThrownBy&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; service&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;deleteOrder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;1L&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-13&quot;&gt;&lt;a href=&quot;#cb21-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isInstanceOf&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;AccessDeniedException&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;co&quot;&gt;// 실패&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-14&quot;&gt;&lt;a href=&quot;#cb21-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb21-15&quot;&gt;&lt;a href=&quot;#cb21-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@InjectMocks&lt;/code&gt;는 순수 자바 객체를 만든다. Spring AOP
프록시로 감싸지 않는다. 그래서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize(&amp;quot;hasRole(&amp;#39;ADMIN&amp;#39;)&amp;quot;)&lt;/code&gt;이 붙어 있어도 무시된다.
게다가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ExtendWith(MockitoExtension.class)&lt;/code&gt;는 Spring
컨텍스트를 띄우지 않으므로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WithSecurityContextTestExecutionListener&lt;/code&gt;도 등록되지 않아서
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;이중으로 무력화&lt;/strong&gt;된다.&lt;/p&gt;
&lt;h3 id=&quot;8-2-제대로-검증하는-방법&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-2) 제대로 검증하는 방법&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;메서드 시큐리티는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Spring 컨텍스트 위에서&lt;/strong&gt; 테스트해야
한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;선호 패턴:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb22&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb22-1&quot;&gt;&lt;a href=&quot;#cb22-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@SpringBootTest&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-2&quot;&gt;&lt;a href=&quot;#cb22-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; OrderServiceMethodSecurityTest &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-3&quot;&gt;&lt;a href=&quot;#cb22-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-4&quot;&gt;&lt;a href=&quot;#cb22-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Autowired&lt;/span&gt; OrderService service&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-5&quot;&gt;&lt;a href=&quot;#cb22-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@MockBean&lt;/span&gt; OrderRepository repo&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-6&quot;&gt;&lt;a href=&quot;#cb22-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-7&quot;&gt;&lt;a href=&quot;#cb22-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-8&quot;&gt;&lt;a href=&quot;#cb22-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;roles &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-9&quot;&gt;&lt;a href=&quot;#cb22-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 일반사용자는_주문삭제_불가&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-10&quot;&gt;&lt;a href=&quot;#cb22-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;fu&quot;&gt;assertThatThrownBy&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; service&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;deleteOrder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;1L&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-11&quot;&gt;&lt;a href=&quot;#cb22-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isInstanceOf&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;AccessDeniedException&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-12&quot;&gt;&lt;a href=&quot;#cb22-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-13&quot;&gt;&lt;a href=&quot;#cb22-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-14&quot;&gt;&lt;a href=&quot;#cb22-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-15&quot;&gt;&lt;a href=&quot;#cb22-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@WithMockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;roles &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-16&quot;&gt;&lt;a href=&quot;#cb22-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; 관리자는_주문삭제_가능&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-17&quot;&gt;&lt;a href=&quot;#cb22-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;fu&quot;&gt;assertThatCode&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; service&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;deleteOrder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dv&quot;&gt;1L&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-18&quot;&gt;&lt;a href=&quot;#cb22-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;doesNotThrowAnyException&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-19&quot;&gt;&lt;a href=&quot;#cb22-19&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb22-20&quot;&gt;&lt;a href=&quot;#cb22-20&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;풀 컨텍스트가 무거우면 슬림하게 줄일 수도 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb23&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb23-1&quot;&gt;&lt;a href=&quot;#cb23-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@ExtendWith&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;SpringExtension&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-2&quot;&gt;&lt;a href=&quot;#cb23-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Import&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;OrderService&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-3&quot;&gt;&lt;a href=&quot;#cb23-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@EnableMethodSecurity&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-4&quot;&gt;&lt;a href=&quot;#cb23-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;class&lt;/span&gt; OrderServiceSlimTest &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-5&quot;&gt;&lt;a href=&quot;#cb23-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;at&quot;&gt;@Autowired&lt;/span&gt; OrderService service&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-6&quot;&gt;&lt;a href=&quot;#cb23-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb23-7&quot;&gt;&lt;a href=&quot;#cb23-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import&lt;/code&gt;로 대상 빈만 올리고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableMethodSecurity&lt;/code&gt;로 AOP 프록시만 활성화한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootTest&lt;/code&gt;보다 빠르면서 메서드 시큐리티는 살아
있다.&lt;/p&gt;
&lt;h3 id=&quot;8-3-요약-표&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-3) 요약 표&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;테스트 스타일&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AOP 프록시&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize&lt;/code&gt; 동작&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ExtendWith(MockitoExtension)&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@InjectMocks&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;없음&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;무시됨&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootTest&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;있음&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@ExtendWith(SpringExtension)&lt;/code&gt; + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@EnableMethodSecurity&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;있음&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WebMvcTest&lt;/code&gt; (서비스 계층)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;없음 (컨트롤러만)&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;무시됨&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;9-jwt--oauth2-mock-jwtclaimauthorities&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;9) JWT / OAuth2 mock:
jwt().claim().authorities()&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Resource Server로 JWT 검증을 하는 API는 테스트에서 실제 토큰을
발급받기 어렵다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JwtDecoder&lt;/code&gt;를 mock하는 것도 가능하지만,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityMockMvcRequestPostProcessors.jwt()&lt;/code&gt;가 훨씬
간단하다.&lt;/p&gt;
&lt;h3 id=&quot;9-1-jwt-기본&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-1) JWT 기본&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb24&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb24-1&quot;&gt;&lt;a href=&quot;#cb24-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/api/profile&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-2&quot;&gt;&lt;a href=&quot;#cb24-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;jwt&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;jwt&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;builder &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; builder&lt;/span&gt;
&lt;span id=&quot;cb24-3&quot;&gt;&lt;a href=&quot;#cb24-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;subject&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-4&quot;&gt;&lt;a href=&quot;#cb24-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;claim&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;email&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;alice@example.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-5&quot;&gt;&lt;a href=&quot;#cb24-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb24-6&quot;&gt;&lt;a href=&quot;#cb24-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;jsonPath&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;$.username&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jwt()&lt;/code&gt;는 내부에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JwtAuthenticationToken&lt;/code&gt;을
만들어 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContext&lt;/code&gt;에 박는다. 실제 서명 검증은 일어나지
않는다.&lt;/p&gt;
&lt;h3 id=&quot;9-2-스코프-권한-주입&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-2) 스코프 권한 주입&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;JWT는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SCOPE_*&lt;/code&gt; 권한으로 변환되는 게 기본이다.
포스트프로세서에서 직접 지정할 수 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb25&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb25-1&quot;&gt;&lt;a href=&quot;#cb25-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/api/orders&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-2&quot;&gt;&lt;a href=&quot;#cb25-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;jwt&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorities&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-3&quot;&gt;&lt;a href=&quot;#cb25-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;SimpleGrantedAuthority&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;SCOPE_orders:write&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-4&quot;&gt;&lt;a href=&quot;#cb25-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;csrf&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-5&quot;&gt;&lt;a href=&quot;#cb25-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;MediaType&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-6&quot;&gt;&lt;a href=&quot;#cb25-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;{...}&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb25-7&quot;&gt;&lt;a href=&quot;#cb25-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isCreated&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize(&amp;quot;hasAuthority(&amp;#39;SCOPE_orders:write&amp;#39;)&amp;quot;)&lt;/code&gt;를 이
조합으로 검증한다.&lt;/p&gt;
&lt;h3 id=&quot;9-3-opaque-token과-oauth2-login&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-3) Opaque token과 OAuth2
Login&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Introspection 기반 Opaque 토큰:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb26&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb26-1&quot;&gt;&lt;a href=&quot;#cb26-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/api/me&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb26-2&quot;&gt;&lt;a href=&quot;#cb26-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;opaqueToken&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;attributes&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;a &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; a&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;sub&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;OAuth2 Client로 로그인한
상태(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/oauth2/authorization/google&lt;/code&gt; 흐름을 지난 상태)
mock:&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb27&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb27-1&quot;&gt;&lt;a href=&quot;#cb27-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;mockMvc&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-2&quot;&gt;&lt;a href=&quot;#cb27-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;oauth2Login&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-3&quot;&gt;&lt;a href=&quot;#cb27-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorities&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;SimpleGrantedAuthority&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;ROLE_USER&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb27-4&quot;&gt;&lt;a href=&quot;#cb27-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;attributes&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;a &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; a&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;email&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;alice@gmail.com&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;OAuth2 Login은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2User&lt;/code&gt;를 principal로 만드는 흐름이라,
컨트롤러에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AuthenticationPrincipal OAuth2User&lt;/code&gt;로 받을 때
필드 값이 필요한 경우 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;attributes&lt;/code&gt;로 채워준다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;10-webflux-webtestclient--mockuser--mockjwt&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;10) WebFlux:
WebTestClient + mockUser / mockJwt&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Reactive 스택은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MockMvc&lt;/code&gt;가 아니라
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebTestClient&lt;/code&gt;를 쓴다. 포스트프로세서도 이름이 다르다.&lt;/p&gt;
&lt;h3 id=&quot;10-1-securitymockserverconfigurers&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-1)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityMockServerConfigurers&lt;/code&gt;&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb28&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb28-1&quot;&gt;&lt;a href=&quot;#cb28-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;static&lt;/span&gt;&lt;span class=&quot;im&quot;&gt; org&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;springframework&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;reactive&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;im&quot;&gt;SecurityMockServerConfigurers&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.*;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-2&quot;&gt;&lt;a href=&quot;#cb28-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-3&quot;&gt;&lt;a href=&quot;#cb28-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Test&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-4&quot;&gt;&lt;a href=&quot;#cb28-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;dt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;reactive_admin_엔드포인트&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-5&quot;&gt;&lt;a href=&quot;#cb28-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    webTestClient&lt;/span&gt;
&lt;span id=&quot;cb28-6&quot;&gt;&lt;a href=&quot;#cb28-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mutateWith&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mockUser&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;roles&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;ADMIN&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-7&quot;&gt;&lt;a href=&quot;#cb28-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/admin/stats&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-8&quot;&gt;&lt;a href=&quot;#cb28-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-9&quot;&gt;&lt;a href=&quot;#cb28-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb28-10&quot;&gt;&lt;a href=&quot;#cb28-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mutateWith&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebTestClient&lt;/code&gt;의 설정을
테스트별로 변경하는 진입점이다. 내부적으로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ServerWebExchange&lt;/code&gt;에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt;을
주입한다.&lt;/p&gt;
&lt;h3 id=&quot;10-2-주요-configurer&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-2) 주요 configurer&lt;/h3&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;메서드&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;용도&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mockUser(...)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;기본 사용자 주입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mockAuthentication(auth)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;커스텀 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mockJwt()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JwtAuthenticationToken&lt;/code&gt; mock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mockOpaqueToken()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Opaque 토큰 mock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mockOAuth2Login()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;OAuth2 로그인 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;csrf()&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;CSRF 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb29&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb29-1&quot;&gt;&lt;a href=&quot;#cb29-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;webTestClient&lt;/span&gt;
&lt;span id=&quot;cb29-2&quot;&gt;&lt;a href=&quot;#cb29-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mutateWith&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;mockJwt&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb29-3&quot;&gt;&lt;a href=&quot;#cb29-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;jwt&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;j &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; j&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;subject&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb29-4&quot;&gt;&lt;a href=&quot;#cb29-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorities&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;SimpleGrantedAuthority&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;SCOPE_read&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb29-5&quot;&gt;&lt;a href=&quot;#cb29-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/api/data&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb29-6&quot;&gt;&lt;a href=&quot;#cb29-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;exchange&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb29-7&quot;&gt;&lt;a href=&quot;#cb29-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;expectStatus&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;isOk&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;10-3-withmockuser는-reactive에서도-동작&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-3)
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;는 Reactive에서도 동작&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;어노테이션 기반은 MVC/Reactive 공통이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ReactiveSecurityContextHolder&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContextHolder&lt;/code&gt; 양쪽 모두에 맞춰진
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;TestSecurityContextHolder&lt;/code&gt;를 경유해 주입된다. 어노테이션을
쓰면 스택에 관계없이 같은 방식이 되고, 포스트프로세서(혹은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebTestClientConfigurer&lt;/code&gt;)를 쓸 때만 서블릿·리액티브 API가
갈린다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;11-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;11) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;컨트롤러 단위 테스트는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WebMvcTest + @Import(SecurityConfig.class)&lt;/code&gt;&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityConfig&lt;/code&gt;를 안 불러오면 테스트용 기본값이 적용돼 실제
동작과 달라진다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;POST/PUT/DELETE 테스트 실패하면 403부터 확인&lt;/strong&gt;. 99%가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf())&lt;/code&gt; 빠진 케이스다. JSON API라면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf().asHeader())&lt;/code&gt;까지 고려&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt; vs &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;
선택 기준은 단 하나&lt;/strong&gt;: &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetailsService&lt;/code&gt;를
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;실제로 호출할 필요&lt;/strong&gt;가 있는가. 커스텀
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetails&lt;/code&gt; 필드(tenantId 등)를 로직이 쓴다면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@MockBean UserDetailsService&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;를 섞어 쓰는 실수&lt;/strong&gt;:
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;when(...).thenReturn(...)&lt;/code&gt;이 호출 안 된 것처럼 보이는 이유.
애초에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;는 그 빈을 안 부른다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;roles&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ROLE_&lt;/code&gt; 접두사 붙고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authorities&lt;/code&gt;는 안 붙는다&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasRole&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;hasAuthority&lt;/code&gt;의 차이를 모르면 테스트가 설정 따라 들쭉날쭉
통과한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize&lt;/code&gt; 단위 테스트는 Spring 컨텍스트
필수&lt;/strong&gt;. Mockito &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@InjectMocks&lt;/code&gt;로는 AOP 프록시가 없어
검증 자체가 무력화된다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@SpringBootTest&lt;/code&gt; 또는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@Import + @EnableMethodSecurity&lt;/code&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;JWT/OAuth2 Resource Server는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jwt()&lt;/code&gt;
포스트프로세서&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JwtDecoder&lt;/code&gt;를 직접 mock하지 말고
Security Test API를 써라. 서명 검증·시간 검증을 건너뛰면서도 권한은
그대로 주입된다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;WebFlux는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;mutateWith(mockUser(...))&lt;/code&gt;&lt;/strong&gt;.
MVC의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(user(...))&lt;/code&gt;와 이름이 다르다. 혼동하면 컴파일은
되지만 주입이 안 된다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;통합 테스트에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;formLogin()&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;logout()&lt;/code&gt; 유틸을 써라&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;post(&amp;quot;/login&amp;quot;)&lt;/code&gt;을 직접 쓰면 CSRF·파라미터 이름·경로를 매번
직접 관리하게 된다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;디버깅 팁&lt;/strong&gt;: 401/403이 나면 먼저
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.andDo(print())&lt;/code&gt;로 필터 체인 응답 헤더와 바디를 찍어본다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WWW-Authenticate&lt;/code&gt;나 에러 메시지에 거의 원인이 나온다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;12-한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;12) 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Spring Security Testing은 &amp;quot;컨텍스트 주입 + 요청별 주입 + 응답
검증&amp;quot; 세 축을 섞어 쓰는 작업이다&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithMockUser&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@WithUserDetails&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetailsService&lt;/code&gt; 호출
여부에서 갈리고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;MockMvc&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.with(csrf())&lt;/code&gt;는
상태 변경 요청에 거의 항상 필요하며, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@PreAuthorize&lt;/code&gt; 검증은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;AOP 프록시가 있는 컨텍스트&lt;/strong&gt;에서만 의미를 가진다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;roles&lt;/code&gt;와 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authorities&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ROLE_&lt;/code&gt;
접두사 차이를 놓치면 같은 코드가 설정 따라 통과·실패를 왕복하니 처음부터
구분해서 쓴다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;태그&lt;/strong&gt;: spring-security, testing, mockmvc,
webtestclient, withmockuser, withuserdetails, method-security, csrf,
jwt, webflux&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/Spring</category>
      <category>CSRF</category>
      <category>JWT</category>
      <category>method-security</category>
      <category>MockMvc</category>
      <category>spring-security</category>
      <category>Testing</category>
      <category>webflux</category>
      <category>WebTestClient</category>
      <category>withmockuser</category>
      <category>WithUserDetails</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/404</guid>
      <comments>https://dding-shark.tistory.com/404#entry404comment</comments>
      <pubDate>Tue, 14 Apr 2026 22:04:45 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security OAuth2 Client &amp;mdash; Authorization Code Flow와 ClientRegistration 내부</title>
      <link>https://dding-shark.tistory.com/403</link>
      <description>&lt;div style=&quot;font-family:Pretendard,-apple-system,BlinkMacSystemFont,&quot;Apple SD Gothic Neo&quot;,&quot;Noto Sans KR&quot;,&quot;Segoe UI&quot;,sans-serif;font-size:16px;line-height:1.75;color:#1f2328;max-width:820px;margin:0 auto;padding:20px 0;word-break:keep-all;overflow-wrap:break-word;&quot;&gt;
&lt;h1 id=&quot;spring-security-oauth2-client--authorization-code-flow와-clientregistration-내부&quot; style=&quot;font-size:2.1rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.4em;border-bottom:2px solid #d1d9e0;margin-top:0;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;Spring
Security OAuth2 Client — Authorization Code Flow와 ClientRegistration
내부&lt;/h1&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;들어가며&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;바로 앞 글(&lt;a href=&quot;./27_spring-security-oauth2-resource-server.md&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;27편 Resource
Server&lt;/a&gt;)이 &amp;quot;이미 발급된 토큰을 받기만 하는 쪽&amp;quot;이었다면, 이번 글은
정반대다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;토큰을 발급받아 오는 쪽&lt;/strong&gt; — 사용자를 IdP로
리다이렉트하고, 돌아온 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code&lt;/code&gt;를 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;token endpoint&lt;/code&gt;에
교환하고, 받은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;access_token&lt;/code&gt;을 저장해 뒀다가 API 호출할 때
붙여 주는 전 과정을 Spring Security가 알아서 해 준다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-oauth2-client&lt;/code&gt; 하나 추가하면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt; 여덟 줄로 구글 로그인이 동작한다. 그
아래에서 필터 두 개가 리다이렉트를 주고받고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientRegistration&lt;/code&gt;이 설정을 들고 있고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientManager&lt;/code&gt;가 토큰 수명을 관리한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;문제는 이 흐름이 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;브라우저 리다이렉트를 3번 거친다&lt;/strong&gt;는
점이다. 디버거를 찍어도 세션이 끊어졌다 붙었다 하니 한 번에 따라가기
어렵고, 실패하면 어느 단계에서 어긋났는지 증상만 봐서는 모른다. 다음 네
지점이 거의 항상 한 번씩은 걸린다.&lt;/p&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Authorization Code Flow를 머릿속에 못 그리면 디버깅이 안
잡힌다&lt;/strong&gt; — 리다이렉트가 3번 일어나고, 필터는 그 중 2개만
관여한다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;redirect-uri&lt;/code&gt;를 하드코딩했다가 배포 환경별로
매번 고친다&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;{baseUrl}&lt;/code&gt; placeholder를 쓰면
자동으로 치환된다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;로그아웃을 눌렀는데 다시 로그인 버튼을 누르면 바로
통과된다&lt;/strong&gt; — IdP 쪽 세션이 살아 있어서다. OIDC Logout이
답이다&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;멀티 인스턴스로 띄웠더니 OAuth 콜백이 랜덤하게
튕긴다&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HttpSession&lt;/code&gt; 기반 리포지토리가 기본이라
sticky session 또는 JDBC/Spring Session이 필요하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 글은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;왜 → 플로우 → 필터/리포지토리 → 토큰 관리 → API 호출
→ PKCE/OIDC → 실무&lt;/strong&gt; 순으로 Spring Security 6.x / Spring Boot 3.x
기준으로 정리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#1-oauth2-client와-resource-server의-역할-분리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;1) OAuth2
Client와 Resource Server의 역할 분리&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#2-authorization-code-flow-전체-시퀀스&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;2) Authorization
Code Flow 전체 시퀀스&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#3-clientregistration과-boot-자동구성&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;3)
ClientRegistration과 Boot 자동구성&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#4-oauth2authorizationrequestredirectfilter-내부&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;4)
OAuth2AuthorizationRequestRedirectFilter 내부&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#5-oauth2loginauthenticationfilter와-토큰-교환&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;5)
OAuth2LoginAuthenticationFilter와 토큰 교환&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#6-oauth2user와-oidcuser-userinfo-조회&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;6) OAuth2User와
OidcUser: UserInfo 조회&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#7-authorizedclientrepository-vs-service-어디에-저장하나&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;7)
AuthorizedClientRepository vs Service: 어디에 저장하나&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#8-oauth2authorizedclientmanager와-자동-refresh&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;8)
OAuth2AuthorizedClientManager와 자동 refresh&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#9-registeredoauth2authorizedclient와-webclient로-api-호출&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;9)
@RegisteredOAuth2AuthorizedClient와 WebClient로 API 호출&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#10-pkce-public-client와-spring-security-6의-권장&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;10)
PKCE: public client와 Spring Security 6의 권장&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#11-oidc-logout과-oidcclientinitiatedlogoutsuccesshandler&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;11)
OIDC Logout과 OidcClientInitiatedLogoutSuccessHandler&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#12-실무에서-이렇게-읽고-쓴다&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;12) 실무에서 이렇게 읽고
쓴다&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;a href=&quot;#13-한-줄-정리&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;13) 한 줄 정리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;1-oauth2-client와-resource-server의-역할-분리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;1) OAuth2
Client와 Resource Server의 역할 분리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;OAuth2의 세 축은 Authorization Server, Client, Resource Server다.
Resource Server는 &amp;quot;토큰을 받기만 하는 쪽&amp;quot;이고, **Client는 &amp;quot;토큰을
발급받는 쪽&amp;quot;**이다. 사용자를 IdP로 리다이렉트하고, IdP에서 돌아온
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code&lt;/code&gt;를 토큰으로 교환하고, 그 토큰을 저장해 두었다가 외부
API를 호출할 때 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authorization: Bearer&lt;/code&gt;로 붙인다. Spring
Security가 이 모든 단계를 필터와 서비스로 쪼개 놓았다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 차이를 못 잡으면 스타터부터 잘못 고른다. 백엔드 API만 있는
서비스가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2-client&lt;/code&gt;를 추가하면 브라우저가 IdP 로그인
페이지로 리다이렉트되기 시작하고, 반대로 로그인 기능이 필요한 서비스가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2-resource-server&lt;/code&gt;만 추가하면 콜백 처리는커녕
리다이렉트도 일어나지 않는다. 두 스타터는 서로의 대체재가 아니라
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;다른 일을 하는 서로 다른 모듈&lt;/strong&gt;이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;구분&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;OAuth2 Client&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;OAuth2 Resource Server&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;역할&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;토큰을 발급받아 저장/사용&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;토큰을 받아 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;스타터&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-oauth2-client&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;spring-boot-starter-oauth2-resource-server&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;주도 필터&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationRequestRedirectFilter&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2LoginAuthenticationFilter&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;BearerTokenAuthenticationFilter&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;DSL&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;http.oauth2Login(...)&lt;/code&gt; /
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;http.oauth2Client(...)&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;http.oauth2ResourceServer(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;세션 필요?&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;예 — 리다이렉트 상태 보관&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;아니오 — STATELESS 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Login&lt;/code&gt;과 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Client&lt;/code&gt;도 서로 다르다.
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Login&lt;/code&gt;은 &amp;quot;OAuth2로 사용자를
로그인시킨다&amp;quot;&lt;/strong&gt;(= 내 앱의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt; 주체를
IdP가 제공한 OAuth2 사용자로 세팅). &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Client&lt;/code&gt;는
&amp;quot;OAuth2를 외부 API 호출용 자격증명으로 쓴다&amp;quot;&lt;/strong&gt;(= 로그인은 별도로
하고, 토큰은 외부 서비스 호출용). 대부분의 웹 앱은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Login&lt;/code&gt;만 켜면 두 일이 한 번에 된다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;2-authorization-code-flow-전체-시퀀스&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;2) Authorization Code
Flow 전체 시퀀스&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Authorization Code Flow는 OAuth2에서 가장 흔하고 동시에 가장 디버깅이
까다로운 그랜트다. 브라우저가 세 번 리다이렉트되는 동안 세션은 유지되고,
민감한 자격증명(클라이언트 시크릿)은 브라우저를 거치지 않는다. 일단
시퀀스부터 그리고 본문을 읽는 게 훨씬 빠르다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;img role=&quot;img&quot; aria-label=&quot;28_spring-security-oauth2-client-01&quot; src=&quot;data:image/png;base64,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&quot; alt=&quot;28_spring-security-oauth2-client-01&quot; style=&quot;max-width:100%;height:auto;display:block;margin:1.5em auto;border-radius:6px;background:#fff;padding:8px;border:1px solid #d1d9e0;&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;시퀀스에서 눈여겨볼 세 지점. 첫째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;클라이언트 시크릿은
브라우저를 거치지 않는다&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code&lt;/code&gt;가 브라우저로 돌아온
뒤, 앱 서버가 직접 IdP의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;token endpoint&lt;/code&gt;에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code + client_secret&lt;/code&gt;(또는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_verifier&lt;/code&gt;)을
POST로 교환한다. 이 교환이 백엔드 간 통신이라 시크릿이 노출되지 않는다.
둘째, &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;state&lt;/code&gt; 파라미터가 CSRF 방어&lt;/strong&gt;를 한다.
리다이렉트 시작 시 앱이 세션에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;state&lt;/code&gt;를 저장하고, 콜백에서
돌아온 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;state&lt;/code&gt;와 비교한다. 안 맞으면 거부. 셋째,
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code&lt;/code&gt;는 일회용 + 단명&lt;/strong&gt;이다. 보통 수십 초
안에 만료되고 한 번 교환하면 소멸한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이 시퀀스가 머릿속에 안 들어가 있으면 &amp;quot;왜 내 앱이 방금 IdP로
튕겼지?&amp;quot;에서 멈춘다. 리다이렉트가 3번이라는 걸 기억해 두면 네트워크
탭에서 단계별로 끊어 볼 수 있다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;3-clientregistration과-boot-자동구성&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;3) ClientRegistration과
Boot 자동구성&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientRegistration&lt;/code&gt;은 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&amp;quot;한 IdP를 어떻게 부르면
되는가&amp;quot;의 메타데이터&lt;/strong&gt;다. client id, client secret, authorization
uri, token uri, scope, redirect uri, 사용자 이름 클레임 — IdP 연동에
필요한 모든 설정이 이 한 객체에 들어 있다. Spring Security는 이걸
빌더로도, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;로도, OIDC Discovery로도 만들 수
있다.&lt;/p&gt;
&lt;h3 id=&quot;3-1-빌더로-직접&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-1) 빌더로 직접&lt;/h3&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb1&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb1-1&quot;&gt;&lt;a href=&quot;#cb1-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;ClientRegistration google &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; ClientRegistration&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withRegistrationId&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;google&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-2&quot;&gt;&lt;a href=&quot;#cb1-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;clientId&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;xxx&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-3&quot;&gt;&lt;a href=&quot;#cb1-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;clientSecret&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;yyy&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-4&quot;&gt;&lt;a href=&quot;#cb1-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;clientAuthenticationMethod&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ClientAuthenticationMethod&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;CLIENT_SECRET_BASIC&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-5&quot;&gt;&lt;a href=&quot;#cb1-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorizationGrantType&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;AuthorizationGrantType&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;AUTHORIZATION_CODE&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-6&quot;&gt;&lt;a href=&quot;#cb1-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;redirectUri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;{baseUrl}/login/oauth2/code/{registrationId}&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-7&quot;&gt;&lt;a href=&quot;#cb1-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;openid&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;profile&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;email&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-8&quot;&gt;&lt;a href=&quot;#cb1-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorizationUri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;https://accounts.google.com/o/oauth2/v2/auth&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-9&quot;&gt;&lt;a href=&quot;#cb1-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;tokenUri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;https://oauth2.googleapis.com/token&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-10&quot;&gt;&lt;a href=&quot;#cb1-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;userInfoUri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;https://openidconnect.googleapis.com/v1/userinfo&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-11&quot;&gt;&lt;a href=&quot;#cb1-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;userNameAttributeName&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;IdTokenClaimNames&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;SUB&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-12&quot;&gt;&lt;a href=&quot;#cb1-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;jwkSetUri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;https://www.googleapis.com/oauth2/v3/certs&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-13&quot;&gt;&lt;a href=&quot;#cb1-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;clientName&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;Google&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb1-14&quot;&gt;&lt;a href=&quot;#cb1-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;대부분의 항목은 IdP 문서에서 옮겨 적는 값이다. 여기서 놓치면 안 되는
게 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;redirectUri&lt;/code&gt;의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;{baseUrl}&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;{registrationId}&lt;/code&gt; placeholder다. Spring Security가 런타임에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;request&lt;/code&gt;의 scheme/host/port와 registration id로 치환하므로
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;로컬, 스테이징, 프로덕션에서 각각 값을 바꿀 필요가
없다&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;http://localhost:8080/login/oauth2/code/google&lt;/code&gt;이나
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;https://app.example.com/login/oauth2/code/google&lt;/code&gt;이 자동으로
만들어진다. 하드코딩하지 말 것. 들어가며의 두 번째 함정이다.&lt;/p&gt;
&lt;h3 id=&quot;3-2-oidc-discovery로-자동-채우기&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-2) OIDC Discovery로 자동
채우기&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;OIDC를 지원하는 IdP는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/.well-known/openid-configuration&lt;/code&gt;
엔드포인트에서 위 URL들을 전부 JSON으로 내려준다. Spring Security는 이걸
한 줄로 읽어서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientRegistration&lt;/code&gt;을 만들 수 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb2&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb2-1&quot;&gt;&lt;a href=&quot;#cb2-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;ClientRegistration keycloak &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; ClientRegistrations&lt;/span&gt;
&lt;span id=&quot;cb2-2&quot;&gt;&lt;a href=&quot;#cb2-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;fromIssuerLocation&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;https://idp.example.com/realms/app&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-3&quot;&gt;&lt;a href=&quot;#cb2-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;registrationId&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;keycloak&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-4&quot;&gt;&lt;a href=&quot;#cb2-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;clientId&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;my-client&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-5&quot;&gt;&lt;a href=&quot;#cb2-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;clientSecret&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;secret&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb2-6&quot;&gt;&lt;a href=&quot;#cb2-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;fromIssuerLocation&lt;/code&gt;이 discovery 문서를 GET해서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authorization_endpoint&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;token_endpoint&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;userinfo_endpoint&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;jwks_uri&lt;/code&gt;를 전부 채운다.
빌더로 채운 값들과 merge되므로 예외(값을 바꾸고 싶은 경우)만
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.xxx(...)&lt;/code&gt;로 덮으면 된다.&lt;/p&gt;
&lt;h3 id=&quot;3-3-spring-boot-자동구성&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;3-3) Spring Boot 자동구성&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;프로덕션에서 실제로 가장 많이 쓰는 방식은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;application.yml&lt;/code&gt;이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb3&quot;&gt;&lt;pre class=&quot;sourceCode yaml&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode yaml&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb3-1&quot;&gt;&lt;a href=&quot;#cb3-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;fu&quot;&gt;spring&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-2&quot;&gt;&lt;a href=&quot;#cb3-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;security&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-3&quot;&gt;&lt;a href=&quot;#cb3-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;oauth2&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-4&quot;&gt;&lt;a href=&quot;#cb3-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;      &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-5&quot;&gt;&lt;a href=&quot;#cb3-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;registration&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-6&quot;&gt;&lt;a href=&quot;#cb3-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;          &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;google&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-7&quot;&gt;&lt;a href=&quot;#cb3-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;client-id&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; ${GOOGLE_CLIENT_ID}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-8&quot;&gt;&lt;a href=&quot;#cb3-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;client-secret&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; ${GOOGLE_CLIENT_SECRET}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-9&quot;&gt;&lt;a href=&quot;#cb3-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; openid, profile, email&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-10&quot;&gt;&lt;a href=&quot;#cb3-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;          &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;keycloak&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-11&quot;&gt;&lt;a href=&quot;#cb3-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;client-id&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; my-client&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-12&quot;&gt;&lt;a href=&quot;#cb3-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;client-secret&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; ${KEYCLOAK_SECRET}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-13&quot;&gt;&lt;a href=&quot;#cb3-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorization-grant-type&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; authorization_code&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-14&quot;&gt;&lt;a href=&quot;#cb3-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;redirect-uri&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; &lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;{baseUrl}/login/oauth2/code/{registrationId}&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-15&quot;&gt;&lt;a href=&quot;#cb3-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; openid, profile&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-16&quot;&gt;&lt;a href=&quot;#cb3-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;        &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-17&quot;&gt;&lt;a href=&quot;#cb3-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;          &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;keycloak&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb3-18&quot;&gt;&lt;a href=&quot;#cb3-18&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;            &lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;issuer-uri&lt;/span&gt;&lt;span class=&quot;kw&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;at&quot;&gt; https://idp.example.com/realms/app&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Boot가 하는 일은 두 가지다. 첫째, 등록된 provider에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;issuer-uri&lt;/code&gt;가 있으면 discovery를 호출해 엔드포인트를 채운다.
둘째, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;CommonOAuth2Provider&lt;/code&gt;
enum(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GOOGLE&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GITHUB&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;FACEBOOK&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OKTA&lt;/code&gt;)에
해당하는 registration은 provider 블록 없이도 기본 URL이 내장돼 있다.
그래서 구글은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;client-id&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;client-secret&lt;/code&gt;만 있으면
끝난다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;빈으로는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;InMemoryClientRegistrationRepository&lt;/code&gt;가 기본으로
뜬다. 멀티 테넌시나 런타임 추가가 필요하면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JdbcClientRegistrationRepository&lt;/code&gt;로 바꿔서 DB에
저장한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;4-oauth2authorizationrequestredirectfilter-내부&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;4)
OAuth2AuthorizationRequestRedirectFilter 내부&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Flow의 첫 번째 리다이렉트(앱 → IdP)를 만드는 필터다. 이름이 긴 만큼
이름이 곧 역할이다 — &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Authorization Request를 만들어서 Redirect로
내려보낸다&lt;/strong&gt;.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;트리거는 두 가지다. 사용자가 직접
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/oauth2/authorization/{registrationId}&lt;/code&gt; URL을 치거나(로그인
버튼이 보통 이 URL로 링크), 인증 필요한 엔드포인트에서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;LoginUrlAuthenticationEntryPoint&lt;/code&gt;가 해당 URL로
리다이렉트하거나. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Login&lt;/code&gt;이 등록되면 기본 entry point가
이 리다이렉트로 설정된다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;필터가 하는 일 순서.&lt;/p&gt;
&lt;ol type=&quot;1&quot; style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;matcher 체크&lt;/strong&gt; — URL이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/oauth2/authorization/{registrationId}&lt;/code&gt;에 매칭되는지
확인한다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationRequestResolver&lt;/code&gt;가
담당한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientRegistration&lt;/code&gt; 조회&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientRegistrationRepository&lt;/code&gt;에서 registration id로 찾는다.
없으면 그냥 다음 필터로 넘긴다(이 필터가 처리할 일이 아님).&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationRequest&lt;/code&gt; 생성&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;state&lt;/code&gt;(랜덤 32바이트 base64), PKCE 활성 시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_verifier&lt;/code&gt; + &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_challenge&lt;/code&gt;, 그리고
authorization uri + scope + redirect uri + client id를 조합한 full URL을
만든다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;세션에 저장&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthorizationRequestRepository&lt;/code&gt;(기본
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HttpSessionOAuth2AuthorizationRequestRepository&lt;/code&gt;)에 이
요청을 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;state&lt;/code&gt;를 키로 저장한다. 콜백 검증 시 꺼내 쓸
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_verifier&lt;/code&gt;와 원래 요청 URL을 여기에 같이 넣는다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;302 리다이렉트&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RedirectStrategy&lt;/code&gt;로
브라우저에 full authorization URL로 리다이렉트 응답을 보낸다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 중요한 게 세션이다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;state&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_verifier&lt;/code&gt;는 콜백에서 다시 꺼내 검증/교환에 써야 하는데,
브라우저는 IdP를 경유해 돌아오므로 앱 측에서 기억할 곳은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;세션뿐&lt;/strong&gt;이다(기본 구현 기준). 세션이 없거나 콜백 시점에
다른 노드로 라우팅되면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthorizationRequest&lt;/code&gt;가 사라져서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;[invalid_state_parameter]&lt;/code&gt; 오류가 난다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 틀리기 쉽다. 무상태(STATELESS) 설정을 전면 적용한 뒤
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Login&lt;/code&gt;을 켜면 이 필터가 세션을 못 만들어 바로 깨진다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2Login&lt;/code&gt;을 쓴다면 세션은 필요하다. 필요하면 OAuth 플로우
동안만 세션을 허용하고 나머지는 상태 없이 가도록 설계한다(이 글 범위 밖,
별도 튜닝).&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb4&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb4-1&quot;&gt;&lt;a href=&quot;#cb4-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;http&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;oauth2Login&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;oauth2 &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; oauth2&lt;/span&gt;
&lt;span id=&quot;cb4-2&quot;&gt;&lt;a href=&quot;#cb4-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorizationEndpoint&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;endpoint &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; endpoint&lt;/span&gt;
&lt;span id=&quot;cb4-3&quot;&gt;&lt;a href=&quot;#cb4-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;baseUri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/oauth2/authorization&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;             &lt;span class=&quot;co&quot;&gt;// 기본값&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-4&quot;&gt;&lt;a href=&quot;#cb4-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorizationRequestRepository&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;repo&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;         &lt;span class=&quot;co&quot;&gt;// 커스터마이즈&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-5&quot;&gt;&lt;a href=&quot;#cb4-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorizationRequestResolver&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;resolver&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;       &lt;span class=&quot;co&quot;&gt;// PKCE 활성 등&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-6&quot;&gt;&lt;a href=&quot;#cb4-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb4-7&quot;&gt;&lt;a href=&quot;#cb4-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;5-oauth2loginauthenticationfilter와-토큰-교환&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;5)
OAuth2LoginAuthenticationFilter와 토큰 교환&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;두 번째 리다이렉트로 브라우저가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/login/oauth2/code/{registrationId}?code=...&amp;amp;state=...&lt;/code&gt;에
도착하면 이 필터가 받는다. 이번엔 토큰 교환과 사용자 로딩, 인증 주체
세팅까지 한꺼번에 처리한다.&lt;/p&gt;
&lt;h3 id=&quot;5-1-다섯-단계&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-1) 다섯 단계&lt;/h3&gt;
&lt;ol type=&quot;1&quot; style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;state 검증&lt;/strong&gt; — 쿼리의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;state&lt;/code&gt;와 세션에
저장된 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationRequest.state&lt;/code&gt;를 비교한다. 불일치
시 즉시 거부. CSRF 방어의 핵심이다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;토큰 교환&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationCodeGrantRequest&lt;/code&gt;를 만들어
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AccessTokenResponseClient&lt;/code&gt;에 넘긴다. 기본 구현은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RestClientAuthorizationCodeTokenResponseClient&lt;/code&gt;(6.3+) 또는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DefaultAuthorizationCodeTokenResponseClient&lt;/code&gt;로, IdP의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;token endpoint&lt;/code&gt;에 POST로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code + client_secret + redirect_uri + code_verifier&lt;/code&gt;(PKCE)를
보낸다. 응답은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AccessTokenResponse&lt;/code&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;access_token&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;token_type&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;expires_in&lt;/code&gt;, 선택적 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;refresh_token&lt;/code&gt;, OIDC면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;id_token&lt;/code&gt;.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;ID Token 검증 (OIDC)&lt;/strong&gt; — OIDC면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcAuthorizationCodeAuthenticationProvider&lt;/code&gt;가 나서서
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;id_token&lt;/code&gt;의 서명(JWKS), &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;iss&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;aud&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;exp&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;nonce&lt;/code&gt;를 검증한다. 여기서 실패하면 로그인
자체가 거부된다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;UserInfo 조회&lt;/strong&gt; — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2UserService&lt;/code&gt;(기본
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DefaultOAuth2UserService&lt;/code&gt;) 또는 OIDC면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcUserService&lt;/code&gt;가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;userinfo endpoint&lt;/code&gt;를 호출해
사용자 프로필을 가져온다(OIDC는 id token만으로도 충분한 경우 스킵 가능).
결과는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2User&lt;/code&gt; 또는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcUser&lt;/code&gt;.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Authentication 생성 + 저장&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2LoginAuthenticationToken&lt;/code&gt;(authenticated=true)을 만들어
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContextHolder&lt;/code&gt;에 꽂고, 토큰들을
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClient&lt;/code&gt;로 묶어
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientRepository&lt;/code&gt;에 저장한다. 이후
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthenticationSuccessHandler&lt;/code&gt;가 원래 요청 URL로
리다이렉트.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;5-2-두-종류의-authentication이-나뉜다&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;5-2) 두 종류의
Authentication이 나뉜다&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;여기서 혼동 포인트가 하나 있다. **로그인 결과로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContextHolder&lt;/code&gt;에 들어가는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authentication&lt;/code&gt;**과 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthorizedClient&lt;/code&gt;에
저장되는 토큰&lt;/strong&gt;은 역할이 다르다. 전자는 &amp;quot;내 앱의 로그인 주체가
누구인가&amp;quot;를 표현하고, 후자는 &amp;quot;이 사용자의 IdP 발급 access token이
뭐인가&amp;quot;를 들고 있다. 앱 안에서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/me&lt;/code&gt; 같은 엔드포인트의 권한
체크는 전자가 처리하고, 외부 API(구글 Calendar 등) 호출은 후자의 토큰을
꺼내 Bearer로 붙인다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2LoginAuthenticationToken&lt;/code&gt;의 principal 타입은
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2User&lt;/code&gt;&lt;/strong&gt; (또는 OIDC면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcUser&lt;/code&gt;)다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;@AuthenticationPrincipal OAuth2User user&lt;/code&gt;로 주입받을 수
있고, 거기서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;getAttributes()&lt;/code&gt;로 IdP가 내려준 클레임을
꺼낸다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;6-oauth2user와-oidcuser-userinfo-조회&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;6) OAuth2User와 OidcUser:
UserInfo 조회&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2User&lt;/code&gt;는 OAuth2 로그인이 끝나고 난 뒤 Spring
Security가 쓰는 &amp;quot;사용자&amp;quot; 추상이다. UserDetails와는 별개다 — 폼
로그인에서 쓰는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;UserDetails&lt;/code&gt;는 로컬 사용자 DB의 표현이고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2User&lt;/code&gt;는 IdP가 내려준 프로필의 표현이다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb5&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb5-1&quot;&gt;&lt;a href=&quot;#cb5-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;interface&lt;/span&gt; OAuth2User &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-2&quot;&gt;&lt;a href=&quot;#cb5-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;getAttributes&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-3&quot;&gt;&lt;a href=&quot;#cb5-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;Collection&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;&amp;lt;?&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;extends&lt;/span&gt; GrantedAuthority&lt;span class=&quot;op&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;getAuthorities&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-4&quot;&gt;&lt;a href=&quot;#cb5-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;getName&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb5-5&quot;&gt;&lt;a href=&quot;#cb5-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;기본 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DefaultOAuth2UserService&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()&lt;/code&gt;로
GET을 때려 JSON을 가져온다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;userNameAttributeName&lt;/code&gt;으로
지정된 클레임(구글은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sub&lt;/code&gt;, 깃허브는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;id&lt;/code&gt;,
네이버는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;response&lt;/code&gt;처럼 중첩인 경우도)을
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;getName()&lt;/code&gt; 값으로 쓴다. 이걸 잘못 설정하면 Principal name이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;null&lt;/code&gt;이 되거나 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;IllegalArgumentException&lt;/code&gt;이
난다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;OIDC면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcUser&lt;/code&gt;가 대신 들어가고, 이건
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2User&lt;/code&gt;를 확장해서 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcIdToken&lt;/code&gt;과
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcUserInfo&lt;/code&gt;를 추가로 들고 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb6&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb6-1&quot;&gt;&lt;a href=&quot;#cb6-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;kw&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;interface&lt;/span&gt; OidcUser &lt;span class=&quot;kw&quot;&gt;extends&lt;/span&gt; OAuth2User&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; IdTokenClaimAccessor &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-2&quot;&gt;&lt;a href=&quot;#cb6-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    OidcUserInfo &lt;span class=&quot;fu&quot;&gt;getUserInfo&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-3&quot;&gt;&lt;a href=&quot;#cb6-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    OidcIdToken &lt;span class=&quot;fu&quot;&gt;getIdToken&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb6-4&quot;&gt;&lt;a href=&quot;#cb6-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcIdToken&lt;/code&gt;에는 IdP가 서명한 JWT가 들어 있어서 별도
userinfo 호출 없이도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;sub&lt;/code&gt;, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;email&lt;/code&gt;,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;name&lt;/code&gt;을 꺼낼 수 있다. 많은 경우 userinfo는 호출하지 않아도
ID Token만으로 충분하다.&lt;/p&gt;
&lt;h3 id=&quot;6-1-권한-매핑&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;6-1) 권한 매핑&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;IdP가 내려준 scope이나 roles 클레임을 앱의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrantedAuthority&lt;/code&gt;로 바꾸고 싶으면 두 방법이 있다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb7&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb7-1&quot;&gt;&lt;a href=&quot;#cb7-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;http&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;oauth2Login&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;oauth2 &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; oauth2&lt;/span&gt;
&lt;span id=&quot;cb7-2&quot;&gt;&lt;a href=&quot;#cb7-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;userInfoEndpoint&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;info &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; info&lt;/span&gt;
&lt;span id=&quot;cb7-3&quot;&gt;&lt;a href=&quot;#cb7-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;userAuthoritiesMapper&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;authorities &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-4&quot;&gt;&lt;a href=&quot;#cb7-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;co&quot;&gt;// 기본 SCOPE_xxx들을 ROLE_xxx로 바꾸거나,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-5&quot;&gt;&lt;a href=&quot;#cb7-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;co&quot;&gt;// 커스텀 클레임(roles)에서 추출해 권한을 재구성&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-6&quot;&gt;&lt;a href=&quot;#cb7-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; remapped&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-7&quot;&gt;&lt;a href=&quot;#cb7-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-8&quot;&gt;&lt;a href=&quot;#cb7-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;oidcUserService&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;customOidcUserService&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-9&quot;&gt;&lt;a href=&quot;#cb7-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb7-10&quot;&gt;&lt;a href=&quot;#cb7-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;GrantedAuthoritiesMapper&lt;/code&gt;는 이미 생성된 authority를 한 번
더 가공하는 자리이고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcUserService&lt;/code&gt;(또는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2UserService&lt;/code&gt;) 커스텀 빈은 애초에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcUser&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2User&lt;/code&gt;를 만들 때 권한을 직접
세팅한다. 복잡한 매핑은 후자가 낫다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;7-authorizedclientrepository-vs-service-어디에-저장하나&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;7)
AuthorizedClientRepository vs Service: 어디에 저장하나&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;토큰 저장은 Spring Security가 두 계층으로 추상화해 놓았다. 이름이
비슷해서 헷갈리기 쉽다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;margin:1.4em 0;font-size:0.94rem;border:1px solid #d1d9e0;&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;타입&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;역할&lt;/strong&gt;&lt;/th&gt;
&lt;th style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;font-weight:700;color:#0a0a0a;background:#f6f8fa;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;기본 구현&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientRepository&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;HTTP 레벨 조회/저장 — request/response/principal 문맥 필요&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HttpSessionOAuth2AuthorizedClientRepository&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientService&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;Principal 레벨 영속 — HTTP 문맥 없이도 동작&lt;/td&gt;
&lt;td style=&quot;padding:10px 14px;text-align:left;border:1px solid #d1d9e0;vertical-align:top;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;InMemoryOAuth2AuthorizedClientService&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;**&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientRepository&lt;/code&gt;**는 컨트롤러 필터
체인에서 &amp;quot;지금 이 요청의 주체가 가진 토큰&amp;quot;을 가져올 때 쓴다. 기본 구현은
세션을 저장소로 쓰고, 사용자별 토큰을 HTTP 세션에 넣는다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;**&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientService&lt;/code&gt;**는 HTTP 밖(예:
스케줄러, 배치 잡)에서 토큰을 꺼낼 때 쓴다. principal name만 알면
저장/조회가 가능하다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HttpSessionOAuth2AuthorizedClientRepository&lt;/code&gt;는 내부적으로
세션에 직접 저장하고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthenticatedPrincipalOAuth2AuthorizedClientRepository&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientService&lt;/code&gt;에 저장 책임을 위임한다.
후자를 쓰면 세션에 토큰을 안 넣고도 원리 상 동작한다.&lt;/p&gt;
&lt;h3 id=&quot;7-1-멀티-인스턴스-함정&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;7-1) 멀티 인스턴스 함정&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;기본이 세션 저장이라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;앱을 여러 인스턴스로 띄웠는데 세션
공유를 안 하면 OAuth 콜백이 랜덤하게 실패&lt;/strong&gt;한다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/oauth2/authorization/...&lt;/code&gt;로 리다이렉트 만든 인스턴스와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/login/oauth2/code/...&lt;/code&gt;로 콜백 받은 인스턴스가 다르면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthorizationRequestRepository&lt;/code&gt;가 값을 못 찾는다. 들어가며의
네 번째 함정이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;해결은 세 가지.&lt;/p&gt;
&lt;ol type=&quot;1&quot; style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;sticky session&lt;/strong&gt; — 로드밸런서에서 같은 사용자는 같은
인스턴스로 고정. 가장 단순하지만 장애 시 불편.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;Spring Session + Redis&lt;/strong&gt; — 세션 자체를 공유
스토리지에 저장.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;HttpSessionOAuth2AuthorizedClientRepository&lt;/code&gt;가 그대로
동작한다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;JDBC 리포지토리로 교체&lt;/strong&gt; —
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JdbcOAuth2AuthorizedClientService&lt;/code&gt;를 써서 DB 테이블에 저장.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthenticatedPrincipalOAuth2AuthorizedClientRepository&lt;/code&gt;와
조합한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb8&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb8-1&quot;&gt;&lt;a href=&quot;#cb8-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-2&quot;&gt;&lt;a href=&quot;#cb8-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;OAuth2AuthorizedClientService &lt;span class=&quot;fu&quot;&gt;authorizedClientService&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-3&quot;&gt;&lt;a href=&quot;#cb8-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        JdbcOperations jdbc&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-4&quot;&gt;&lt;a href=&quot;#cb8-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        ClientRegistrationRepository clients&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-5&quot;&gt;&lt;a href=&quot;#cb8-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;JdbcOAuth2AuthorizedClientService&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;jdbc&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; clients&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-6&quot;&gt;&lt;a href=&quot;#cb8-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-7&quot;&gt;&lt;a href=&quot;#cb8-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-8&quot;&gt;&lt;a href=&quot;#cb8-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-9&quot;&gt;&lt;a href=&quot;#cb8-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;OAuth2AuthorizedClientRepository &lt;span class=&quot;fu&quot;&gt;authorizedClientRepository&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-10&quot;&gt;&lt;a href=&quot;#cb8-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        OAuth2AuthorizedClientService service&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-11&quot;&gt;&lt;a href=&quot;#cb8-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;AuthenticatedPrincipalOAuth2AuthorizedClientRepository&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;service&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb8-12&quot;&gt;&lt;a href=&quot;#cb8-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;스키마는 Spring Security가 기본 DDL
파일(&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2-client-schema.sql&lt;/code&gt;)을 제공한다.
Flyway/Liquibase에 얹어 초기 마이그레이션으로 관리한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;8-oauth2authorizedclientmanager와-자동-refresh&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;8)
OAuth2AuthorizedClientManager와 자동 refresh&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClient&lt;/code&gt;에 저장된
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;access_token&lt;/code&gt;은 언젠가 만료된다. 만료 시
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;refresh_token&lt;/code&gt;으로 새 access token을 받아오는 절차를 매번
직접 짜는 건 고역이다. Spring Security는 이걸
&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientManager&lt;/code&gt;&lt;/strong&gt; 한 개로
추상화한다.&lt;/p&gt;
&lt;h3 id=&quot;8-1-역할&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-1) 역할&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientManager.authorize(OAuth2AuthorizeRequest)&lt;/code&gt;를
부르면 Manager가 한다.&lt;/p&gt;
&lt;ol type=&quot;1&quot; style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;저장소(Repository 또는 Service)에서 해당 principal + registration의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClient&lt;/code&gt;를 조회.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;access token이 살아 있으면 그대로 반환.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;만료됐고 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;refresh_token&lt;/code&gt;이 있으면 refresh grant로 새
token을 받아 저장소 갱신.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;아직 없으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;authorization_code&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;client_credentials&lt;/code&gt; 등
설정된 grant로 처음 획득.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;8-2-provider-체인&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-2) Provider 체인&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Manager 내부는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientProvider&lt;/code&gt; 체인이다.
각 provider가 한 그랜트 타입을 책임진다. 기본 조합은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientProviderBuilder&lt;/code&gt;로 만든다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb9&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb9-1&quot;&gt;&lt;a href=&quot;#cb9-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-2&quot;&gt;&lt;a href=&quot;#cb9-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;OAuth2AuthorizedClientManager &lt;span class=&quot;fu&quot;&gt;authorizedClientManager&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-3&quot;&gt;&lt;a href=&quot;#cb9-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        ClientRegistrationRepository clients&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-4&quot;&gt;&lt;a href=&quot;#cb9-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        OAuth2AuthorizedClientRepository authorizedClients&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-5&quot;&gt;&lt;a href=&quot;#cb9-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-6&quot;&gt;&lt;a href=&quot;#cb9-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    OAuth2AuthorizedClientProvider provider &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; OAuth2AuthorizedClientProviderBuilder&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-7&quot;&gt;&lt;a href=&quot;#cb9-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorizationCode&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-8&quot;&gt;&lt;a href=&quot;#cb9-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;refreshToken&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-9&quot;&gt;&lt;a href=&quot;#cb9-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;clientCredentials&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-10&quot;&gt;&lt;a href=&quot;#cb9-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-11&quot;&gt;&lt;a href=&quot;#cb9-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-12&quot;&gt;&lt;a href=&quot;#cb9-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    DefaultOAuth2AuthorizedClientManager manager &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-13&quot;&gt;&lt;a href=&quot;#cb9-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;DefaultOAuth2AuthorizedClientManager&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;clients&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; authorizedClients&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-14&quot;&gt;&lt;a href=&quot;#cb9-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    manager&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;setAuthorizedClientProvider&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;provider&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-15&quot;&gt;&lt;a href=&quot;#cb9-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; manager&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb9-16&quot;&gt;&lt;a href=&quot;#cb9-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;체인 순서대로 각 provider가 &amp;quot;내가 처리할 수 있는 상태인가?&amp;quot;를
확인하고 맞는 것이 처리한다. 예를 들어 만료된 access token + 유효한
refresh token 상태면 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;refreshToken()&lt;/code&gt; provider가 나서서
refresh grant를 수행한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;서블릿 스택의 기본 구현은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DefaultOAuth2AuthorizedClientManager&lt;/code&gt;, HTTP 문맥이 없을
때(배치, 스케줄러)는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthorizedClientServiceOAuth2AuthorizedClientManager&lt;/code&gt;를
쓴다. 후자는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientService&lt;/code&gt; 기반이라
request/response가 필요 없다.&lt;/p&gt;
&lt;h3 id=&quot;8-3-만료-임계치&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;8-3) 만료 임계치&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;refresh 시점을 결정하는 건 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;expiresAt&lt;/code&gt;이지만, 정확히
만료된 순간에 refresh를 시도하면 이미 만료된 상태라 401이 날 수도 있다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;DefaultOAuth2AuthorizedClientManager&lt;/code&gt;는 내부적으로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;clockSkew&lt;/code&gt;(기본 60초)만큼 일찍 만료로 판단한다. 시계가 조금
어긋나 있어도 안전하게 돈다. 이 값은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientProviderBuilder.refreshToken(cfg -&amp;gt; cfg.clockSkew(Duration.ofSeconds(30)))&lt;/code&gt;로
조정 가능하다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;9-registeredoauth2authorizedclient와-webclient로-api-호출&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;9)
@RegisteredOAuth2AuthorizedClient와 WebClient로 API 호출&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;저장된 토큰을 외부 API 호출에 꽂는 표준 통로는 두 가지다.&lt;/p&gt;
&lt;h3 id=&quot;9-1-registeredoauth2authorizedclient&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-1)
@RegisteredOAuth2AuthorizedClient&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;컨트롤러 메서드 인자로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClient&lt;/code&gt;를
주입받는다. 내부적으로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientArgumentResolver&lt;/code&gt;가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientManager.authorize(...)&lt;/code&gt;를 호출해서
만료 시 자동 refresh까지 처리한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb10&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb10-1&quot;&gt;&lt;a href=&quot;#cb10-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@GetMapping&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;/calendar&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-2&quot;&gt;&lt;a href=&quot;#cb10-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;calendar&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;at&quot;&gt;@RegisteredOAuth2AuthorizedClient&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;google&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; OAuth2AuthorizedClient client&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-3&quot;&gt;&lt;a href=&quot;#cb10-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; token &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; client&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getAccessToken&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;getTokenValue&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-4&quot;&gt;&lt;a href=&quot;#cb10-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;co&quot;&gt;// 이 token으로 구글 Calendar API 호출&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-5&quot;&gt;&lt;a href=&quot;#cb10-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb10-6&quot;&gt;&lt;a href=&quot;#cb10-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;한 메서드 안에서 일회성으로 토큰을 꺼낼 때 쓴다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;client.getRefreshToken()&lt;/code&gt;까지 꺼낼 수 있고, Manager가 만료를
확인해서 이미 갱신된 상태로 준다.&lt;/p&gt;
&lt;h3 id=&quot;9-2-oauth2authorizedclientexchangefilterfunction&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;9-2)
OAuth2AuthorizedClientExchangeFilterFunction&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;WebClient(또는 Spring Security 6.3+에서는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;RestClient&lt;/code&gt;)에
필터로 꽂아 두면, 매 요청마다 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;토큰을 자동으로
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;Authorization: Bearer&lt;/code&gt; 헤더로 붙이고 만료되면
refresh&lt;/strong&gt;한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb11&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb11-1&quot;&gt;&lt;a href=&quot;#cb11-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-2&quot;&gt;&lt;a href=&quot;#cb11-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;WebClient &lt;span class=&quot;fu&quot;&gt;googleApi&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;OAuth2AuthorizedClientManager manager&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-3&quot;&gt;&lt;a href=&quot;#cb11-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-4&quot;&gt;&lt;a href=&quot;#cb11-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;ServletOAuth2AuthorizedClientExchangeFilterFunction&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;manager&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-5&quot;&gt;&lt;a href=&quot;#cb11-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    oauth2&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;setDefaultClientRegistrationId&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;google&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-6&quot;&gt;&lt;a href=&quot;#cb11-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-7&quot;&gt;&lt;a href=&quot;#cb11-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; WebClient&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-8&quot;&gt;&lt;a href=&quot;#cb11-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;oauth2&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;oauth2Configuration&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-9&quot;&gt;&lt;a href=&quot;#cb11-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-10&quot;&gt;&lt;a href=&quot;#cb11-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-11&quot;&gt;&lt;a href=&quot;#cb11-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-12&quot;&gt;&lt;a href=&quot;#cb11-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// 사용처&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-13&quot;&gt;&lt;a href=&quot;#cb11-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt; resp &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt; googleApi&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-14&quot;&gt;&lt;a href=&quot;#cb11-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;https://www.googleapis.com/calendar/v3/...&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-15&quot;&gt;&lt;a href=&quot;#cb11-15&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;retrieve&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-16&quot;&gt;&lt;a href=&quot;#cb11-16&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;bodyToMono&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb11-17&quot;&gt;&lt;a href=&quot;#cb11-17&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;필터가 현재 요청의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContext&lt;/code&gt;에서 principal을 찾아
해당 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClient&lt;/code&gt;를 자동으로 꺼낸다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;setDefaultClientRegistrationId(&amp;quot;google&amp;quot;)&lt;/code&gt;로 기본
registration을 지정하거나, 호출마다
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;.attributes(clientRegistrationId(&amp;quot;google&amp;quot;))&lt;/code&gt;로 지정한다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;배치/스케줄러처럼 HTTP 문맥이 없는 곳에서 이 필터를 쓰려면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthorizedClientServiceOAuth2AuthorizedClientExchangeFilterFunction&lt;/code&gt;을
쓰고 Manager도 service 기반으로 붙여야 한다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;10-pkce-public-client와-spring-security-6의-권장&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;10) PKCE:
public client와 Spring Security 6의 권장&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Authorization Code Flow의 변형으로 **PKCE (Proof Key for Code
Exchange, RFC 7636)**가 있다. 원래는 public client(모바일 앱, SPA)가
client secret을 안전하게 못 들고 있어서 생긴 해결책이다.&lt;/p&gt;
&lt;h3 id=&quot;10-1-원리&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-1) 원리&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;브라우저/앱이 리다이렉트 직전에 랜덤 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_verifier&lt;/code&gt;를
생성하고, 이걸 SHA-256으로 해시한 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_challenge&lt;/code&gt;를
authorization request에 포함시킨다. IdP가 이
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_challenge&lt;/code&gt;를 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code&lt;/code&gt;에 묶어 기억. 나중에 토큰
교환 시점에 원본 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_verifier&lt;/code&gt;를 같이 보내면 IdP가 해시를
다시 계산해서 초기 값과 맞는지 확인한다. 중간에 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code&lt;/code&gt;를
탈취해도 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_verifier&lt;/code&gt;가 없으면 토큰으로 못 바꾼다.&lt;/p&gt;
&lt;h3 id=&quot;10-2-spring-security에서-활성화&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-2) Spring Security에서
활성화&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Security 6부터는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;confidential client(서버 측 앱)에도
PKCE가 권장&lt;/strong&gt;된다.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ClientAuthenticationMethod.NONE&lt;/code&gt;으로 설정하면 자동으로
PKCE가 켜지고, confidential 클라이언트로 켜고 싶으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationRequestResolver&lt;/code&gt;를 커스터마이즈한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb12&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb12-1&quot;&gt;&lt;a href=&quot;#cb12-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-2&quot;&gt;&lt;a href=&quot;#cb12-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;OAuth2AuthorizationRequestResolver &lt;span class=&quot;fu&quot;&gt;pkceResolver&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;ClientRegistrationRepository clients&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-3&quot;&gt;&lt;a href=&quot;#cb12-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    DefaultOAuth2AuthorizationRequestResolver resolver &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-4&quot;&gt;&lt;a href=&quot;#cb12-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;DefaultOAuth2AuthorizationRequestResolver&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;clients&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;st&quot;&gt;&amp;quot;/oauth2/authorization&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-5&quot;&gt;&lt;a href=&quot;#cb12-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    resolver&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;setAuthorizationRequestCustomizer&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-6&quot;&gt;&lt;a href=&quot;#cb12-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        OAuth2AuthorizationRequestCustomizers&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withPkce&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-7&quot;&gt;&lt;a href=&quot;#cb12-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; resolver&lt;span class=&quot;op&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-8&quot;&gt;&lt;a href=&quot;#cb12-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-9&quot;&gt;&lt;a href=&quot;#cb12-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb12-10&quot;&gt;&lt;a href=&quot;#cb12-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;co&quot;&gt;// http.oauth2Login(...).authorizationEndpoint(e -&amp;gt; e.authorizationRequestResolver(resolver))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationRequestCustomizers.withPkce()&lt;/code&gt;가
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_verifier&lt;/code&gt;를 생성해
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationRequest.additionalParameters&lt;/code&gt;에 넣고,
토큰 교환 시에 그대로 전달되도록 붙여준다. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code_verifier&lt;/code&gt;는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthorizationRequestRepository&lt;/code&gt;(세션)에 요청과 함께 저장돼
있다가 콜백 때 꺼내진다.&lt;/p&gt;
&lt;h3 id=&quot;10-3-왜-confidential에도-권장인가&quot; style=&quot;font-size:1.25rem;font-weight:700;line-height:1.3;color:#1f2328;margin-top:1.8em;margin-bottom:0.6em;&quot;&gt;10-3) 왜 confidential에도
권장인가&lt;/h3&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;PKCE가 client secret을 대체하는 게 아니라 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;code 탈취 공격에
대한 추가 방어층&lt;/strong&gt;이기 때문이다. 서버 측 앱이라도 내부망 프록시나
로그를 통해 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;code&lt;/code&gt;가 유출될 수 있고, PKCE는 그 상황에서도
교환을 무력화시킨다. 설정 비용이 거의 없으므로 기본으로 켜는 것이 현재
업계 권장이다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;11-oidc-logout과-oidcclientinitiatedlogoutsuccesshandler&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;11)
OIDC Logout과 OidcClientInitiatedLogoutSuccessHandler&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;Spring Security의 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/logout&lt;/code&gt;은 기본적으로 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;내 앱의
세션만 끊는다&lt;/strong&gt;. &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContextHolder&lt;/code&gt;를 비우고,
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JSESSIONID&lt;/code&gt; 쿠키를 만료시킨다. 그런데 사용자가 IdP에서도
로그인돼 있는 상태라면 IdP 세션은 여전히 살아있다. 직후에 로그인 버튼을
다시 누르면 IdP는 &amp;quot;이미 로그인되어 있으므로 동의 화면 없이 바로 code
발급&amp;quot;을 한다. 체감상 로그아웃이 안 된 것처럼 보인다. 들어가며의 세 번째
함정이다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;이걸 해결하려면 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;OIDC RP-Initiated Logout&lt;/strong&gt;(IdP의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;end_session_endpoint&lt;/code&gt;를 호출)을 써야 한다. Spring Security는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcClientInitiatedLogoutSuccessHandler&lt;/code&gt;로 이걸
지원한다.&lt;/p&gt;
&lt;div class=&quot;sourceCode&quot; id=&quot;cb13&quot;&gt;&lt;pre class=&quot;sourceCode java&quot; style=&quot;background:#f6f8fa;border:1px solid #d1d9e0;border-radius:6px;padding:16px 20px;overflow-x:auto;line-height:1.55;margin:1.2em 0;&quot;&gt;&lt;code style=&quot;background:none;padding:0;border:0;font-size:0.88rem;color:#1f2328; class=&quot;sourceCode java&quot; style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;&lt;span id=&quot;cb13-1&quot;&gt;&lt;a href=&quot;#cb13-1&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;at&quot;&gt;@Bean&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-2&quot;&gt;&lt;a href=&quot;#cb13-2&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;SecurityFilterChain &lt;span class=&quot;fu&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;HttpSecurity http&lt;span class=&quot;op&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-3&quot;&gt;&lt;a href=&quot;#cb13-3&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;                         ClientRegistrationRepository clients&lt;span class=&quot;op&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kw&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;bu&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;op&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-4&quot;&gt;&lt;a href=&quot;#cb13-4&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    OidcClientInitiatedLogoutSuccessHandler handler &lt;span class=&quot;op&quot;&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-5&quot;&gt;&lt;a href=&quot;#cb13-5&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;kw&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;fu&quot;&gt;OidcClientInitiatedLogoutSuccessHandler&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;clients&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-6&quot;&gt;&lt;a href=&quot;#cb13-6&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    handler&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;setPostLogoutRedirectUri&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;st&quot;&gt;&amp;quot;{baseUrl}/&amp;quot;&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-7&quot;&gt;&lt;a href=&quot;#cb13-7&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-8&quot;&gt;&lt;a href=&quot;#cb13-8&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;    &lt;span class=&quot;cf&quot;&gt;return&lt;/span&gt; http&lt;/span&gt;
&lt;span id=&quot;cb13-9&quot;&gt;&lt;a href=&quot;#cb13-9&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;auth &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; auth&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;anyRequest&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;authenticated&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-10&quot;&gt;&lt;a href=&quot;#cb13-10&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;oauth2Login&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bu&quot;&gt;Customizer&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;withDefaults&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-11&quot;&gt;&lt;a href=&quot;#cb13-11&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;logout&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;logout &lt;span class=&quot;op&quot;&gt;-&amp;gt;&lt;/span&gt; logout&lt;/span&gt;
&lt;span id=&quot;cb13-12&quot;&gt;&lt;a href=&quot;#cb13-12&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;            &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;logoutSuccessHandler&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;(&lt;/span&gt;handler&lt;span class=&quot;op&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-13&quot;&gt;&lt;a href=&quot;#cb13-13&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;        &lt;span class=&quot;op&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;fu&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;op&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span id=&quot;cb13-14&quot;&gt;&lt;a href=&quot;#cb13-14&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; style=&quot;color:#0969da;text-decoration:none;&quot;&gt;&lt;/a&gt;&lt;span class=&quot;op&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;동작은 이렇게 간다. 사용자가 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/logout&lt;/code&gt;을 누르면 Spring
Security가 로컬 세션을 비우고, success handler가 IdP의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;end_session_endpoint&lt;/code&gt;로 리다이렉트를 만든다. URL에는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;id_token_hint&lt;/code&gt;(방금 로그인에 쓰였던 ID Token)와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;post_logout_redirect_uri&lt;/code&gt;가 붙는다. IdP가 자기 세션을 비우고
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;post_logout_redirect_uri&lt;/code&gt;로 다시 리다이렉트. 여기서도
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;{baseUrl}&lt;/code&gt; placeholder를 쓰면 환경별 하드코딩이
사라진다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;주의할 점 — &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;post_logout_redirect_uri&lt;/code&gt;는 &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;IdP 측에
사전 등록&lt;/strong&gt;되어 있어야 한다(대부분의 IdP가 화이트리스트를 요구).
안 맞으면 IdP가 리다이렉트를 거부한다. 로컬/스테이징/프로덕션 각 URL을
전부 IdP 콘솔에 등록해 둔다.&lt;/p&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;SSO 환경에서 &amp;quot;로그아웃이 덜 된 것 같다&amp;quot;는 리포트가 올라오면 90% 이
handler를 안 붙인 경우다.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;12-실무에서-이렇게-읽고-쓴다&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;12) 실무에서 이렇게 읽고 쓴다&lt;/h2&gt;
&lt;ul style=&quot;margin:0.8em 0 1.1em;padding-left:1.8em;line-height:1.75;&quot;&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;redirect-uri&lt;/code&gt;는 무조건
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;{baseUrl}/login/oauth2/code/{registrationId}&lt;/code&gt;로
쓴다&lt;/strong&gt;. 환경별 yml 분기 없이 로컬/스테이징/프로덕션이 자동으로
맞춰진다. 반대로 IdP 콘솔에는 전부 등록해 둔다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;issuer-uri&lt;/code&gt; 한 줄로 시작&lt;/strong&gt;한다. OIDC
IdP(Keycloak, Auth0, 구글)는 discovery가 되므로 provider 블록에
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;issuer-uri&lt;/code&gt;만 넣으면 authorization/token/userinfo/jwks 전부
채워진다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;멀티 인스턴스면 세션 저장을 분리&lt;/strong&gt;한다. Spring
Session + Redis가 가장 단순하다. 그게 싫으면
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;JdbcOAuth2AuthorizedClientService&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;AuthenticatedPrincipalOAuth2AuthorizedClientRepository&lt;/code&gt;로
DB에 저장한다. sticky session은 임시방편이다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;/logout&lt;/code&gt;에는
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcClientInitiatedLogoutSuccessHandler&lt;/code&gt;를 붙인다&lt;/strong&gt;.
안 붙이면 IdP 세션이 남아서 재로그인 시 동의 없이 통과된다. SSO 환경에서
반드시.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;외부 API 호출은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebClient&lt;/code&gt; +
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;ServletOAuth2AuthorizedClientExchangeFilterFunction&lt;/code&gt;&lt;/strong&gt;.
수동으로 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;client.getAccessToken().getTokenValue()&lt;/code&gt;를 붙이는
패턴은 refresh를 직접 처리해야 해서 잔버그가 난다. 필터를 쓰면 만료 시
Manager가 자동으로 refresh.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;PKCE를 confidential 클라이언트에도 켠다&lt;/strong&gt;.
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationRequestCustomizers.withPkce()&lt;/code&gt; 한 줄이다.
보안상의 추가층이고 비용이 거의 없다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;권한은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;userAuthoritiesMapper&lt;/code&gt;보다
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oidcUserService&lt;/code&gt;/&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;oauth2UserService&lt;/code&gt;를 커스텀하는
쪽이 디버깅하기 좋다&lt;/strong&gt;. 권한 원천을 한 군데로 모으고, 테스트에서
user service를 mock하면 권한 분기까지 단번에 검증된다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;디버깅은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;org.springframework.security.oauth2.client&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;org.springframework.web.client&lt;/code&gt;를 DEBUG&lt;/strong&gt;.
authorization request 생성, state 검증, token endpoint 호출까지 로그에
남는다. 네트워크 탭과 교차로 보면 3번의 리다이렉트가 투명하게
그려진다.&lt;/li&gt;
&lt;li style=&quot;margin:0.25em 0;line-height:1.75;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;refresh token은 보통 DB에 저장되므로 암호화 또는 별도 보호를
고려&lt;/strong&gt;한다. 탈취 시 access token을 계속 재발급할 수 있으니 무기한
쿠키처럼 다뤄선 안 된다. DB 컬럼 레벨 암호화 또는 KMS 연동이
일반적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;h2 id=&quot;13-한-줄-정리&quot; style=&quot;font-size:1.55rem;font-weight:700;line-height:1.3;color:#1f2328;padding-bottom:0.3em;border-bottom:1px solid #d1d9e0;margin-top:2.5em;margin-bottom:0.6em;letter-spacing:-0.01em;&quot;&gt;13) 한 줄 정리&lt;/h2&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;OAuth2 Client의 전체 동작은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizationRequestRedirectFilter&lt;/code&gt;(앱→IdP)와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2LoginAuthenticationFilter&lt;/code&gt;(콜백→토큰 교환) 두 필터의
합주로 끝나고, 결과물은 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;SecurityContextHolder&lt;/code&gt;의
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2User&lt;/code&gt;와
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientRepository&lt;/code&gt;의 토큰으로 분리
저장된다&lt;/strong&gt;. 외부 API 호출은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OAuth2AuthorizedClientManager&lt;/code&gt; 한 개가 만료 확인 +
refresh까지 맡고, &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;WebClient&lt;/code&gt; 필터에 꽂으면 자동으로 Bearer가
붙는다. &lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;redirect-uri&lt;/code&gt;는 &lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;{baseUrl}&lt;/code&gt;
placeholder, 멀티 인스턴스는 세션 공유, 로그아웃은
&lt;code style=&quot;font-family:&quot;JetBrains Mono&quot;,&quot;SF Mono&quot;,Menlo,Consolas,&quot;D2Coding&quot;,monospace;font-size:0.88em;&quot;&gt;OidcClientInitiatedLogoutSuccessHandler&lt;/code&gt; — 이 세 가지가
실무에서 90% 이상의 함정을 없앤다&lt;/strong&gt;.&lt;/p&gt;
&lt;hr style=&quot;border:0;height:1px;background:#d1d9e0;margin:2.5em 0;&quot; /&gt;
&lt;p style=&quot;margin:0.6em 0 1.4em;text-indent:1em;line-height:1.75;color:#1f2328;&quot;&gt;&lt;strong style=&quot;color:#0a0a0a;font-weight:700;&quot;&gt;태그&lt;/strong&gt;: spring-security, oauth2, oauth2-client,
authorization-code, oidc, pkce, client-registration, authorized-client,
webclient, spring-boot&lt;/p&gt;
&lt;/div&gt;</description>
      <category>CS/Spring</category>
      <category>authorization-code</category>
      <category>authorized-client</category>
      <category>client-registration</category>
      <category>oauth2</category>
      <category>oauth2-client</category>
      <category>OIDC</category>
      <category>pkce</category>
      <category>spring-boot</category>
      <category>spring-security</category>
      <category>WebClient</category>
      <author>dding-shark</author>
      <guid isPermaLink="true">https://dding-shark.tistory.com/403</guid>
      <comments>https://dding-shark.tistory.com/403#entry403comment</comments>
      <pubDate>Tue, 14 Apr 2026 22:04:12 +0900</pubDate>
    </item>
  </channel>
</rss>