<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>신입개발자</title>
    <link>https://yurizzy.tistory.com/</link>
    <description>LazyInitializationException은 갑자기 온다. . . .  </description>
    <language>ko</language>
    <pubDate>Thu, 2 Jul 2026 19:10:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>김춘덕⸝ဗီူ⸜</managingEditor>
    <image>
      <title>신입개발자</title>
      <url>https://tistory1.daumcdn.net/tistory/5282374/attach/4c1645ba443741eab2b02e80c8fcf8a4</url>
      <link>https://yurizzy.tistory.com</link>
    </image>
    <item>
      <title> &amp;zwj;  Springframwork Mig 기록 - GitLab CI/CD 빌드 환경 맞추기 (feat. Gradle 프로필 빌드)</title>
      <link>https://yurizzy.tistory.com/268</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YBgIL/dJMcabYBqnZ/GB0o0ImovTVCidP6UTKJ4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YBgIL/dJMcabYBqnZ/GB0o0ImovTVCidP6UTKJ4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YBgIL/dJMcabYBqnZ/GB0o0ImovTVCidP6UTKJ4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYBgIL%2FdJMcabYBqnZ%2FGB0o0ImovTVCidP6UTKJ4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;955&quot; height=&quot;556&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;레거시 Spring Framework 기반 프로젝트를 Spring Boot 3와 Gradle 기반으로 고도화하면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;GitLab CI/CD 빌드 환경을 맞춰야 하는 일이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;처음에는 단순히 &amp;ldquo;JDK 버전과 Gradle 버전만 알려주면 되는 일&amp;rdquo;이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;하지만 메일을 다시 읽고 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;을 따라가다 보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;내가 제대로 이해해야 하는 것은 빌드 도구 버전이 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;소스가 어떤 서버에서 빌드되고, 어떤 프로필로 WAR가 만들어지고, 어떤 경로로 운영 서버까지 이동하는지&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 글은 사내 프로젝트의 실제 값은 모두 익명화하고, GitLab CI/CD와 Gradle 프로필 빌드를&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이해해 간 과정을 공개 가능한 수준으로 정리한 기록이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발환경&lt;/h2&gt;
&lt;table style=&quot;height: 256px;&quot; width=&quot;741&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;width: 143px; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;구분&lt;/span&gt;&lt;/th&gt;
&lt;th style=&quot;width: 270px; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;AS-IS&lt;/span&gt;&lt;/th&gt;
&lt;th style=&quot;width: 366px; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;TO-BE&lt;/span&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 143px; text-align: left; height: 20px;&quot;&gt;Java&lt;/td&gt;
&lt;td style=&quot;width: 270px; text-align: left; height: 20px;&quot;&gt;Java 1.7 계열&lt;/td&gt;
&lt;td style=&quot;width: 366px; text-align: left; height: 20px;&quot;&gt;Java 17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 143px; text-align: left; height: 20px;&quot;&gt;프레임워크&lt;/td&gt;
&lt;td style=&quot;width: 270px; text-align: left; height: 20px;&quot;&gt;Spring Framework 4 계열&lt;/td&gt;
&lt;td style=&quot;width: 366px; text-align: left; height: 20px;&quot;&gt;Spring Boot 3 계열&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 143px; text-align: left; height: 20px;&quot;&gt;빌드 도구&lt;/td&gt;
&lt;td style=&quot;width: 270px; text-align: left; height: 20px;&quot;&gt;Maven&lt;/td&gt;
&lt;td style=&quot;width: 366px; text-align: left; height: 20px;&quot;&gt;Gradle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 143px; text-align: left; height: 20px;&quot;&gt;배포 단위&lt;/td&gt;
&lt;td style=&quot;width: 270px; text-align: left; height: 20px;&quot;&gt;WAR&lt;/td&gt;
&lt;td style=&quot;width: 366px; text-align: left; height: 20px;&quot;&gt;WAR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 143px; text-align: left; height: 20px;&quot;&gt;CI/CD&lt;/td&gt;
&lt;td style=&quot;width: 270px; text-align: left; height: 20px;&quot;&gt;일부 수동 빌드/배포&lt;/td&gt;
&lt;td style=&quot;width: 366px; text-align: left; height: 20px;&quot;&gt;GitLab CI/CD 기반 자동화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 143px; text-align: left; height: 20px;&quot;&gt;운영 방식&lt;/td&gt;
&lt;td style=&quot;width: 270px; text-align: left; height: 20px;&quot;&gt;운영 서버 직접 배포&lt;/td&gt;
&lt;td style=&quot;width: 366px; text-align: left; height: 20px;&quot;&gt;빌드 산출물 생성 후 운영 공유 저장소를 거쳐 배포&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;CI/CD 담당자로부터 빌드 환경을 맞추기 위한 요청을 받았다. 요청의 핵심은 다음과 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발용 빌드 실행 서버에서 WAR 파일을 생성한 뒤, &lt;br /&gt;운영 공유 저장소를 거쳐 운영 서버의 배포 경로로 복사하는 구조다. &lt;br /&gt;기존 Maven 빌드 때처럼, 이번 Gradle 빌드도 &lt;br /&gt;개발자 로컬 환경과 빌드 실행 서버 환경을 맞추고 싶다. &lt;br /&gt;Gradle 빌드에 필요한 환경 정보를 공유해 달라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;처음에는 이 문장을 보고 세 가지가 헷갈렸다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;ldquo;개발용 빌드 실행 서버&amp;rdquo;가 내가 평소 접속하는 DEV 서버와 같은 의미인지 헷갈렸다.&lt;/li&gt;
&lt;li&gt;예시로 받은 &lt;code&gt;java -version&lt;/code&gt;, &lt;code&gt;mvn -version&lt;/code&gt; 결과가 빌드 서버의 값인지 담당자 로컬 PC의 값인지 헷갈렸다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt; 안에 &lt;code&gt;git pull&lt;/code&gt;이 없는데, GitLab Runner가 어떻게 최신 소스를 받아 빌드하는지 몰랐다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이때 내가 놓친 핵심은 &lt;b&gt;GitLab CI/CD에서는 개발자 PC, &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;GitLab 서버, GitLab Runner가 설치된 서버, &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;운영 서버의 역할이 서로 다르다&lt;/b&gt;는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;[개발자 PC]
  - git push

[GitLab 서버]
  - push 이벤트 감지
  - .gitlab-ci.yml 해석
  - Runner에 job 할당

[빌드 실행 서버]
  - GitLab Runner가 job 실행
  - 소스 checkout
  - Gradle 빌드
  - WAR artifact 생성

[운영 서버 A/B]
  - WAR 교체
  - 애플리케이션 재기동
  - 헬스체크&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;즉 &amp;ldquo;빌드 환경을 맞춘다&amp;rdquo;는 말은 단순히 내 PC 정보를 알려주는 일이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;내 PC에서 성공하는 빌드가 GitLab Runner가 실행되는 서버에서도&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt; 같은 방식으로 성공하도록 보장하는 작업&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1번째 시도: 담당자 로컬 PC 환경을 빌드 서버 환경으로 이해하기 (실패)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;b&gt;시도한 이유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;처음에는 요청 메일에 포함된 명령 결과를 보고, 그것이 빌드 서버의 환경이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;C:\Users\[계정]&amp;gt;java -version
openjdk version &quot;17&quot;

C:\Users\[계정]&amp;gt;mvn -version
Apache Maven 3.x.x&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그래서 &amp;ldquo;Java 17은 이미 맞춰져 있으니 Gradle만 확인하면 되겠다&amp;rdquo;고 답하려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;b&gt;문제점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;다시 보니 프롬프트가 Windows 형식이었다.&lt;/p&gt;
&lt;pre class=&quot;taggerscript&quot;&gt;&lt;code&gt;C:\Users\[계정]&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;반면 실제 빌드와 배포가 일어나는 서버는 Linux 계열 서버였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 값은 빌드 실행 서버의 값이 아니라, 담당자 로컬 PC에서 확인한 값이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 상태로 답했다면 &amp;ldquo;빌드 서버에는 Java 17이 있다&amp;rdquo;고 잘못 전제했을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;빌드 환경 확인에서 가장 위험한 실수는 &lt;b&gt;어디에서 실행한 명령인지 확인하지 않고 결과만 보는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;br /&gt;버전 확인 결과를 공유받으면, 먼저 실행 위치를 확인해야 한다. &lt;br /&gt;java -version보다 중요한 질문은 &amp;ldquo;이 명령을 어느 서버에서 실행했는가&amp;rdquo;다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2번째 시도: GitLab Runner가 하는 일을 직접 따라가기 (성공)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;b&gt;시도한 이유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;에는 &lt;code&gt;git pull&lt;/code&gt; 명령이 보이지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그런데도 GitLab CI/CD에서는 push한 코드가 빌드 대상이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 부분을 이해하지 못하면, 빌드 실행 서버의 작업 디렉터리와 최신 소스 반영 방식을 계속 오해할 수밖에 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;b&gt;확인한 내용&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;GitLab Runner는 job을 실행하기 전에 저장소 준비 단계를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;프로젝트 설정과 Runner 설정에 따라 &lt;code&gt;clone&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;empty&lt;/code&gt; 같은 Git 전략을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;우리 프로젝트에서는 기존 작업 디렉터리를 재사용하면서 변경분을 가져오는 방식으로 동작하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;공개용으로 단순화하면 흐름은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. 개발자가 main 또는 develop 브랜치에 push한다.
2. GitLab 서버가 .gitlab-ci.yml의 rules를 평가한다.
3. 조건이 맞으면 build-runner 태그를 가진 Runner에 job을 할당한다.
4. Runner가 빌드 실행 서버의 작업 디렉터리에서 소스를 준비한다.
5. Runner가 script 블록에 적힌 Gradle 명령을 실행한다.
6. 생성된 WAR 파일을 GitLab artifact로 업로드한다.
7. deploy job이 artifact를 내려받아 운영 배포를 진행한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이제 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;에 &lt;code&gt;git pull&lt;/code&gt;이 없어도 최신 소스로 빌드되는 이유가 이해됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;code&gt;script&lt;/code&gt; 블록은 전체 CI 작업 중 &amp;ldquo;내가 직접 적은 명령&amp;rdquo;에 해당하고, 그 앞뒤로 Runner가 수행하는 준비 단계와 정리 단계가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;CI/CD가 더 이상 &amp;ldquo;GitLab 안에서 알아서 되는 일&amp;rdquo;처럼 보이지 않았다. GitLab Runner는 GitLab 서버의 지시를 받아, 정해진 서버에서 정해진 명령을 실행하는 실행자였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;br /&gt;.gitlab-ci.yml을 읽을 때는 script 블록만 보면 안 된다. &lt;br /&gt;Runner가 job을 받기 전후로 수행하는 저장소 준비, artifact 처리, cache 처리까지 함께 봐야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3번째 시도: 운영 프로필을 하나로 묶어 빌드하기 (실패 직전)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;b&gt;시도한 이유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;처음 CI 설정을 봤을 때 운영 빌드 프로필이 하나로 잡혀 있었다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;variables:
  APP_MODULE: &quot;sample-web&quot;
  PROFILE_PROD: &quot;prod&quot;
  GRADLE_OPTS: &quot;-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs=-Xmx2g&quot;
  GRADLE_USER_HOME: &quot;$CI_PROJECT_DIR/.gradle&quot;
  ARTIFACT_GLOB: &quot;sample-web/build/libs/*.war&quot;

build:sample-web:prod:
  stage: build
  tags:
    - build-runner
  script:
    - chmod +x ./gradlew
    - ./gradlew :${APP_MODULE}:clean :${APP_MODULE}:build -Pprofile=${PROFILE_PROD}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;겉으로 보기에는 자연스러웠다. 운영 환경이니 &lt;code&gt;prod&lt;/code&gt; 프로필로 빌드하면 될 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;b&gt;문제점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;문제는 Gradle에서 프로필별 리소스를 가져오는 방식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;프로젝트의 &lt;code&gt;build.gradle&lt;/code&gt;은 공개용으로 단순화하면 다음 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;def profile = project.hasProperty(&quot;profile&quot;) ? project.getProperty(&quot;profile&quot;) : &quot;local&quot;

sourceSets {
    main {
        resources {
            srcDir &quot;src/main/resources&quot;
            srcDir &quot;src/main/resources-${profile}&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 구조에서는 &lt;code&gt;-Pprofile=prod&lt;/code&gt;로 빌드하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Gradle이 다음 디렉터리를 리소스 대상으로 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;src/main/resources
src/main/resources-prod&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그런데 실제 프로젝트에는 운영 서버별 리소스 디렉터리가 따로 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;sample-web/src/main/
├── resources
├── resources-local
├── resources-dev
├── resources-prod-a
└── resources-prod-b&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;code&gt;resources-prod&lt;/code&gt; 디렉터리가 없다면 빌드가 바로 실패한다고 단정할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Gradle은 존재하지 않는 리소스 디렉터리를 조용히 지나갈 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;더 위험한 점은 &lt;b&gt;빌드는 성공했는데 운영에 필요한 설정 파일이 WAR 안에 빠질 수 있다&lt;/b&gt;는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;예를 들어 운영 서버 A와 운영 서버 B가 서로 다른 외부 연동 주소나 서버별 설정을 사용한다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;하나의 &lt;code&gt;prod&lt;/code&gt; 프로필로 만든 WAR를 두 서버에 그대로 배포하는 방식은 맞지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;br /&gt;Gradle의 -Pprofile=prod는 Spring의 spring.profiles.active=prod와 같은 개념으로 자동 연결되는 값이 아니다. build.gradle에서 그 값을 어떻게 사용하는지 반드시 확인해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 해결: 운영 서버별 Gradle 프로필 빌드로 분리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;최종적으로는 운영 서버별 리소스 디렉터리 구조에 맞춰&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;빌드 job도 분리하는 방향이 가장 안전하다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;공개용으로 단순화한 예시는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;stages:
  - build
  - deploy

variables:
  APP_MODULE: &quot;sample-web&quot;
  GRADLE_OPTS: &quot;-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs=-Xmx2g&quot;
  GRADLE_USER_HOME: &quot;$CI_PROJECT_DIR/.gradle&quot;

cache:
  key: &quot;${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}&quot;
  paths:
    - .gradle/wrapper
    - .gradle/caches

.build_template: &amp;amp;build_template
  stage: build
  tags:
    - build-runner
  script:
    - chmod +x ./gradlew
    - ./gradlew :${APP_MODULE}:clean :${APP_MODULE}:build -Pprofile=${BUILD_PROFILE}
  artifacts:
    name: &quot;${BUILD_PROFILE}-${CI_PROJECT_NAME}-${CI_COMMIT_SHORT_SHA}&quot;
    paths:
      - ${APP_MODULE}/build/libs/*.war
    expire_in: 30 days

build:sample-web:prod-a:
  &amp;lt;&amp;lt;: *build_template
  variables:
    BUILD_PROFILE: &quot;prod-a&quot;
  rules:
    - if: '$CI_COMMIT_BRANCH == &quot;main&quot;'
      changes:
        - sample-web/**/*
      when: on_success
    - when: never

build:sample-web:prod-b:
  &amp;lt;&amp;lt;: *build_template
  variables:
    BUILD_PROFILE: &quot;prod-b&quot;
  rules:
    - if: '$CI_COMMIT_BRANCH == &quot;main&quot;'
      changes:
        - sample-web/**/*
      when: on_success
    - when: never&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;배포 job도 각 서버가 자신에게 맞는 artifact를 사용하도록 분리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;.deploy_template: &amp;amp;deploy_template
  stage: deploy
  tags:
    - build-runner
  resource_group: &quot;production-deploy-lock&quot;
  environment:
    name: production
  script:
    - test -n &quot;$TARGET_HOST&quot;
    - test -n &quot;$DEPLOY_USER&quot;
    - WAR_FILE=$(find &quot;${APP_MODULE}/build/libs&quot; -name &quot;*.war&quot; | head -n 1)
    - test -n &quot;$WAR_FILE&quot;
    - scp &quot;$WAR_FILE&quot; &quot;${DEPLOY_USER}@${TARGET_HOST}:/deployments/sample-web/new/ROOT.war&quot;
    - ssh &quot;${DEPLOY_USER}@${TARGET_HOST}&quot; &quot;/opt/sample-web/bin/deploy_app.sh&quot;

deploy:sample-web:prod-a:
  &amp;lt;&amp;lt;: *deploy_template
  needs:
    - job: build:sample-web:prod-a
      artifacts: true
  variables:
    TARGET_HOST: &quot;$PROD_SSH_HOST_A&quot;
  when: manual

deploy:sample-web:prod-b:
  &amp;lt;&amp;lt;: *deploy_template
  needs:
    - job: build:sample-web:prod-b
      artifacts: true
    - job: deploy:sample-web:prod-a
  variables:
    TARGET_HOST: &quot;$PROD_SSH_HOST_B&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;여기서 중요한 점은 IP, 계정, 비밀번호, SSH 키 같은 민감 정보가&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt;에 직접 들어가지 않는다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이런 값은 GitLab CI/CD Variables에 저장하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;yml에서는 &lt;code&gt;$PROD_SSH_HOST_A&lt;/code&gt;처럼 변수명만 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;또 하나의 안전장치는 &lt;code&gt;resource_group&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;운영 배포처럼 동시에 실행되면 위험한 job에는 같은 resource group을 지정해,&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;같은 환경으로 향하는 배포가 겹치지 않도록 제한할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;다만 이 구조만으로 완전한 무중단 배포가 보장된다고 말할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;운영 서버 A를 재기동하는 동안 운영 서버 B가 트래픽을 정상 처리하려면&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;로드밸런서, 헬스체크, 세션 처리 방식까지 함께 맞아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그래서 이 글에서는 &amp;ldquo;무중단 배포&amp;rdquo;라고 단정하기보다&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;운영 서버 A/B를 순차적으로 배포하는 구조&lt;/b&gt;라고 정리하는 것이 정확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Maven 프로필과 Gradle 프로필이 달랐던 지점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이번에 가장 크게 배운 부분은 Maven과 Gradle의 프로필 처리 방식 차이였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Maven에서는 &lt;code&gt;pom.xml&lt;/code&gt; 안에 &lt;code&gt;&amp;lt;profiles&amp;gt;&lt;/code&gt;를 정의하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;특정 프로필이 활성화되면 해당 설정이 빌드 모델에 반영된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;profiles&amp;gt;
  &amp;lt;profile&amp;gt;
    &amp;lt;id&amp;gt;prod-a&amp;lt;/id&amp;gt;
    &amp;lt;properties&amp;gt;
      &amp;lt;db.url&amp;gt;jdbc:mariadb://[DB_HOST_A]:3306/app&amp;lt;/db.url&amp;gt;
    &amp;lt;/properties&amp;gt;
  &amp;lt;/profile&amp;gt;

  &amp;lt;profile&amp;gt;
    &amp;lt;id&amp;gt;prod-b&amp;lt;/id&amp;gt;
    &amp;lt;properties&amp;gt;
      &amp;lt;db.url&amp;gt;jdbc:mariadb://[DB_HOST_B]:3306/app&amp;lt;/db.url&amp;gt;
    &amp;lt;/properties&amp;gt;
  &amp;lt;/profile&amp;gt;
&amp;lt;/profiles&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;반면 이번 Gradle 프로젝트에서는 &lt;code&gt;-Pprofile=prod-a&lt;/code&gt; 값을&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;code&gt;build.gradle&lt;/code&gt;에서 직접 읽고, 그 값으로 리소스 디렉터리를 조립하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;./gradlew :sample-web:build -Pprofile=prod-a

포함 대상:
- src/main/resources
- src/main/resources-prod-a&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그래서 Maven을 오래 사용한 사람에게는 &amp;ldquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;프로필별 빌드 파일이 어디 있느냐&amp;rdquo;는 질문이 자연스러울 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;하지만 이 프로젝트의 Gradle 구조에서는 프로필별 빌드 파일이 따로 있는 것이 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;프로필 값에 맞는 리소스 디렉터리를 동적으로 포함하는 방식&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;height: 186px;&quot; width=&quot;734&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 146px;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구분&lt;/p&gt;
&lt;/th&gt;
&lt;th style=&quot;width: 219px;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven&lt;/p&gt;
&lt;/th&gt;
&lt;th style=&quot;width: 361px;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle&lt;/p&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 146px;&quot;&gt;프로필 활성화&lt;/td&gt;
&lt;td style=&quot;width: 219px;&quot;&gt;&lt;code&gt;mvn -Pprod-a&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 361px;&quot;&gt;&lt;code&gt;./gradlew -Pprofile=prod-a&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 146px;&quot;&gt;설정 위치&lt;/td&gt;
&lt;td style=&quot;width: 219px;&quot;&gt;&lt;code&gt;pom.xml&lt;/code&gt;의 &lt;code&gt;&amp;lt;profiles&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 361px;&quot;&gt;&lt;code&gt;build.gradle&lt;/code&gt;의 로직과 리소스 디렉터리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 146px;&quot;&gt;환경별 리소스&lt;/td&gt;
&lt;td style=&quot;width: 219px;&quot;&gt;filtering으로 값 치환&lt;/td&gt;
&lt;td style=&quot;width: 361px;&quot;&gt;&lt;code&gt;resources-${profile}&lt;/code&gt; 디렉터리 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 146px;&quot;&gt;확인 포인트&lt;/td&gt;
&lt;td style=&quot;width: 219px;&quot;&gt;활성화된 profile의 properties&lt;/td&gt;
&lt;td style=&quot;width: 361px;&quot;&gt;&lt;code&gt;sourceSets&lt;/code&gt;와 &lt;code&gt;processResources&lt;/code&gt; 대상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 차이를 이해하고 나니, CI/CD 담당자에게 공유해야 할 정보도 명확해졌다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;필요한 것은 &amp;ldquo;prod 빌드 명령어&amp;rdquo; 하나가 아니라,
운영 서버별로 어떤 Gradle profile 값을 사용해야 하는지다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빌드 실행 서버를 DEV 서버와 함께 쓰는 구조에서 주의할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이번 환경에서는 별도 빌드 전용 서버가 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;개발용 서버와 같은 장비에서 GitLab Runner가 함께 동작하는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 방식은 현실적인 장점이 있지만, 주의할 점도 분명했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;height: 201px;&quot; width=&quot;725&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px; width: 155px;&quot;&gt;항목&lt;/th&gt;
&lt;th style=&quot;height: 20px; width: 587px;&quot;&gt;주의할 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 26px;&quot;&gt;
&lt;td style=&quot;height: 26px; width: 155px;&quot;&gt;CPU/메모리&lt;/td&gt;
&lt;td style=&quot;height: 26px; width: 587px;&quot;&gt;Gradle 빌드 중 애플리케이션 서버 성능에 영향을 줄 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px; width: 155px;&quot;&gt;디스크&lt;/td&gt;
&lt;td style=&quot;height: 25px; width: 587px;&quot;&gt;Gradle cache, GitLab 작업 디렉터리, WAR artifact가 누적될 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px; width: 155px;&quot;&gt;권한&lt;/td&gt;
&lt;td style=&quot;height: 25px; width: 587px;&quot;&gt;Runner 계정이 필요한 배포 경로에 접근할 수 있어야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;height: 25px; width: 155px;&quot;&gt;장애 영향&lt;/td&gt;
&lt;td style=&quot;height: 25px; width: 587px;&quot;&gt;개발용 서버 장애가 CI/CD 장애로 이어질 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그래서 CI 설정에는 최소한의 안전장치가 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;variables:
  GRADLE_OPTS: &quot;-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs=-Xmx2g&quot;
  GRADLE_USER_HOME: &quot;$CI_PROJECT_DIR/.gradle&quot;

resource_group: &quot;production-deploy-lock&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;code&gt;-Dorg.gradle.daemon=false&lt;/code&gt;는 CI처럼 일회성 빌드가 반복되는 환경에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Gradle 데몬을 계속 남기지 않기 위한 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;code&gt;-Xmx2g&lt;/code&gt;는 빌드 JVM의 메모리 상한을 두기 위한 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;code&gt;GRADLE_USER_HOME&lt;/code&gt;을 프로젝트 디렉터리 아래로 잡으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;GitLab cache 설정과 함께 관리하기 쉬워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;다만 cache는 빌드 속도를 높이는 최적화 수단이지, 항상 존재한다고 가정하면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;첫째&lt;/b&gt;, CI/CD에서 &amp;ldquo;환경을 맞춘다&amp;rdquo;는 말은 JDK 버전 하나를 맞춘다는 뜻이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;개발자 로컬, GitLab Runner, 빌드 명령어, 리소스 프로필, artifact 경로가 모두 같은 전제 위에서 움직이도록 맞추는 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;둘째&lt;/b&gt;, &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;은 배포팀만 보는 파일이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;애플리케이션을 개발하는 사람도 최소한 빌드 job과 deploy job의 흐름은 읽을 수 있어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;특히 어떤 프로필로 빌드되는지는 런타임 장애와 바로 연결될 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;셋째&lt;/b&gt;, Gradle의 &lt;code&gt;-Pprofile&lt;/code&gt; 값은 프로젝트마다 의미가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Spring profile과 같은 이름을 쓴다고 해서 자동으로 같은 동작을 한다고 보면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;code&gt;build.gradle&lt;/code&gt;에서 그 값이 어디에 쓰이는지 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;넷째&lt;/b&gt;, &amp;ldquo;잘 모르겠다&amp;rdquo;고 멈춰서 확인한 덕분에 더 큰 실수를 피할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;담당자 로컬 PC의 버전 정보를 빌드 서버 정보로 착각한 것도, &lt;code&gt;prod&lt;/code&gt; 프로필 하나로&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;운영 서버 A/B를 모두 빌드할 뻔한 것도, 다시 확인하지 않았다면 그냥 지나쳤을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이번 일을 통해 CI/CD는 더 이상 &amp;ldquo;운영 쪽에서 알아서 돌아가는 영역&amp;rdquo;이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;내가 읽고 검증해야 하는 코드라는 감각이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;결국 파이프라인도 코드이고, 빌드도 코드이고, 배포도 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/runner/&quot;&gt;GitLab Docs - GitLab Runner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ci/runners/configure_runners/#git-strategy&quot;&gt;GitLab Docs - Configure runners: Git strategy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ee/ci/yaml/&quot;&gt;GitLab Docs - CI/CD YAML syntax reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ci/variables/&quot;&gt;GitLab Docs - CI/CD variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ci/jobs/job_artifacts/&quot;&gt;GitLab Docs - Job artifacts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ci/caching/&quot;&gt;GitLab Docs - Caching in GitLab CI/CD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ci/resource_groups/&quot;&gt;GitLab Docs - Resource group&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gradle.org/current/dsl/org.gradle.api.tasks.SourceSet.html&quot;&gt;Gradle Docs - SourceSet DSL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gradle.org/current/userguide/directory_layout.html&quot;&gt;Gradle Docs - Gradle-managed directories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://maven.apache.org/guides/introduction/introduction-to-profiles.html&quot;&gt;Maven Docs - Introduction to build profiles&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category> ️ DevOpѕ</category>
      <category>CICD</category>
      <category>cicd배포</category>
      <category>deploy</category>
      <category>GitLab</category>
      <category>gradle</category>
      <category>리눅스배포</category>
      <category>배포</category>
      <category>백엔드</category>
      <category>서버배포</category>
      <category>주니어개발자</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/268</guid>
      <comments>https://yurizzy.tistory.com/268#entry268comment</comments>
      <pubDate>Wed, 20 May 2026 21:08:52 +0900</pubDate>
    </item>
    <item>
      <title>  주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 : 신입이 알아야 할 DB 성능&amp;middot;풀스캔&amp;middot;인덱스 9가지</title>
      <link>https://yurizzy.tistory.com/267</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;인트로&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9ZQbT/dJMcadaUrGT/IKmniVyXQGMTATYfrxjqa1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9ZQbT/dJMcadaUrGT/IKmniVyXQGMTATYfrxjqa1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9ZQbT/dJMcadaUrGT/IKmniVyXQGMTATYfrxjqa1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9ZQbT%2FdJMcadaUrGT%2FIKmniVyXQGMTATYfrxjqa1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;360&quot; height=&quot;480&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;최근 &lt;code&gt;주니어 백엔드 개발자가 반드시 알아야 할 실무 지식&lt;/code&gt;을 읽으면서&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DB 성능과 인덱스에 대한 내용을 다시 정리하게 됐다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 DB 성능 문제가 생기면 &amp;ldquo;인덱스를 추가하면 되지 않을까?&amp;rdquo;라고 단순하게 생각했다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 책을 읽고 실무 상황에 대입해보니, 인덱스는 정답이라기보다&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조회 패턴에 맞춰 설계해야 하는 도구&lt;/b&gt;에 가까웠다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;특히 풀스캔, &lt;code&gt;LIKE&lt;/code&gt; 검색, &lt;code&gt;COUNT&lt;/code&gt;, 정규화와 비정규화,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;오래된 데이터 분리, 캐시, 장비 확장은 각각 따로 떨어진 주제가 아니었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;결국 하나의 질문으로 이어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 이 API는 DB에서 읽을 필요가 있는 데이터만 읽고 있는가?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글은 풀스캔을 줄이기 위해 내가 먼저 확인해야 할 &lt;br /&gt;DB 성능 항목 9가지를 정리한 기록이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;특정 회사 시스템을 그대로 설명한 글은 아니고, &lt;br /&gt;실무에서 자주 만나는 조회 API를 기준으로 재구성한 예시다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;수집 대상&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 ORM이나 SQL 매퍼 사용법보다는&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;순수 DB 레벨에서 조회 성능을 떨어뜨리는 패턴과 대응 기준&lt;/b&gt;을 정리한 글이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;내가 특히 기억해두려는 항목은 아래 9가지다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;번호&lt;/th&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;먼저 볼 질문&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;풀스캔&lt;/td&gt;
&lt;td&gt;실행 계획에서 읽는 row가 과하게 많지 않은가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;단일 인덱스와 복합 인덱스&lt;/td&gt;
&lt;td&gt;조회 조건이 어떤 컬럼 조합으로 반복되는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LIKE&lt;/code&gt; 검색&lt;/td&gt;
&lt;td&gt;일반 인덱스로 처리할 검색인가, 전문 검색이 필요한가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;커버링 인덱스&lt;/td&gt;
&lt;td&gt;테이블 row 접근 없이 인덱스만으로 끝낼 수 있는가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;인덱스 개수&lt;/td&gt;
&lt;td&gt;읽기 성능 때문에 쓰기 비용을 과하게 늘리고 있지 않은가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;정규화와 비정규화&lt;/td&gt;
&lt;td&gt;조인 비용과 정합성 비용 중 무엇이 더 큰가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;시간 조건&lt;/td&gt;
&lt;td&gt;기본 조회 범위가 너무 넓지 않은가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COUNT&lt;/code&gt; 집계&lt;/td&gt;
&lt;td&gt;전체 개수가 정말 필요한가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;데이터 분리, 캐시, 장비 확장&lt;/td&gt;
&lt;td&gt;쿼리 튜닝 이후에도 구조적 한계가 남는가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;개발환경&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예시는 아래 환경을 기준으로 작성했다.&lt;/p&gt;
&lt;table style=&quot;width: 815px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 109px;&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;width: 706px;&quot;&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 109px; text-align: center;&quot;&gt;언어&lt;/td&gt;
&lt;td style=&quot;width: 706px;&quot;&gt;Java 17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 109px; text-align: center;&quot;&gt;프레임워크&lt;/td&gt;
&lt;td style=&quot;width: 706px;&quot;&gt;Spring Boot 3 계열&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 109px; text-align: center;&quot;&gt;DB&lt;/td&gt;
&lt;td style=&quot;width: 706px;&quot;&gt;MySQL 8.x 기준 예시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 109px; text-align: center;&quot;&gt;조회 방식&lt;/td&gt;
&lt;td style=&quot;width: 706px;&quot;&gt;게시글 목록, 검색, 상세 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 109px; text-align: center;&quot;&gt;확인 도구&lt;/td&gt;
&lt;td style=&quot;width: 706px;&quot;&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;, 실행 시간 로그, slow query log&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DBMS마다 옵티마이저와 인덱스 동작은 다를 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이 글의 SQL은 개념을 설명하기 위한 예시이며, 실제 적용 전에는&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;반드시 운영 DB와 유사한 데이터 분포에서 실행 계획을 확인해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d06obx/dJMcacwomZF/ohmLlgwZuHpkEg9oXvywzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d06obx/dJMcacwomZF/ohmLlgwZuHpkEg9oXvywzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d06obx/dJMcacwomZF/ohmLlgwZuHpkEg9oXvywzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd06obx%2FdJMcacwomZF%2FohmLlgwZuHpkEg9oXvywzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1066&quot; height=&quot;273&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;게시글 목록 API가 있다고 가정한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 아래처럼 검색어, 상태, 작성 기간을 모두 받을 수 있는 API를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT id, title, writer_name, status, created_at
FROM article
WHERE status = 'PUBLISHED'
  AND title LIKE '%spring%'
  AND created_at &amp;gt;= '2026-05-01 00:00:00'
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;기능만 보면 자연스러운 쿼리다. 하지만 데이터가 많아지면 문제가 생길 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;status&lt;/code&gt;나 &lt;code&gt;created_at&lt;/code&gt;에 인덱스가 있어도 &lt;code&gt;title LIKE '%spring%'&lt;/code&gt; 조건은&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;일반적인 B-Tree 인덱스로 효율적인 검색을 하기 어렵다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;앞에 &lt;code&gt;%&lt;/code&gt;가 붙으면 문자열의 시작점을 알 수 없기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;결국 DB는 많은 row를 읽고, 그중에서 조건에 맞는 데이터를 골라내야 할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이때 실행 계획에서 &lt;code&gt;type=ALL&lt;/code&gt;이 보이면 전체 테이블을 훑는 풀 테이블 스캔 가능성을 의심해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;703&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6py7p/dJMcadvcUj9/SXGekH7zU7W0ioEXKBKY5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6py7p/dJMcadvcUj9/SXGekH7zU7W0ioEXKBKY5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6py7p/dJMcadvcUj9/SXGekH7zU7W0ioEXKBKY5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6py7p%2FdJMcadvcUj9%2FSXGekH7zU7W0ioEXKBKY5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;751&quot; height=&quot;703&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;703&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;내가 처음 헷갈렸던 부분은&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;컬럼에 인덱스가 있으면 무조건 빠르다&amp;rdquo;는 생각이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 인덱스가 있어도 조건 형태, 컬럼 순서, 조회 범위,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;데이터 분포에 따라 옵티마이저는 인덱스를 쓰지 않을 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;1번째 시도: 검색 조건마다 인덱스 추가하기&amp;nbsp;&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 떠올린 방법은 조건에 등장하는 컬럼마다 인덱스를 추가하는 것이었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE INDEX idx_article_status ON article(status);
CREATE INDEX idx_article_title ON article(title);
CREATE INDEX idx_article_created_at ON article(created_at);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;겉으로 보면 괜찮아 보인다. &lt;code&gt;WHERE&lt;/code&gt;에 등장하는 컬럼마다 인덱스가 있기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 방식은 문제를 정확히 해결하지 못할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째&lt;/b&gt;, 단일 인덱스 여러 개가 항상 복합 인덱스 하나와 같은 효과를 내지는 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DB 옵티마이저가 index merge를 선택할 수도 있지만,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;자주 실행되는 핵심 조회라면 조회 패턴에 맞춘 복합 인덱스가 더 명확할 때가 많다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘째&lt;/b&gt;, &lt;code&gt;LIKE '%keyword%'&lt;/code&gt; 검색은 일반적인 B-Tree 인덱스와 잘 맞지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LIKE 'spring%'&lt;/code&gt;처럼 앞부분이 고정된 검색은&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;범위 검색으로 인덱스를 활용할 가능성이 있지만,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;%spring%&lt;/code&gt;처럼 앞뒤를 모두 열어둔 검색은 일반 인덱스로 시작 위치를 잡기 어렵다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;셋째&lt;/b&gt;, 인덱스가 많아질수록 쓰기 비용이 늘어난다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;가 발생할 때 테이블 데이터뿐 아니라&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스도 함께 갱신해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 인덱스는 많이 만들수록 좋은 것이 아니라 &lt;b&gt;필요한 만큼만&lt;/b&gt; 만들어야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 이미 &lt;code&gt;(status, created_at)&lt;/code&gt; 복합 인덱스가 있는데&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;status&lt;/code&gt; 단일 인덱스를 또 추가하는 경우를 생각할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL에서는 복합 인덱스의 선행 컬럼을 활용할 수 있기 때문에 단일 인덱스가 중복일 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;물론 인덱스 크기, 조회 빈도, 옵티마이저 선택에 따라 예외가 있을 수 있으므로&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 계획과 실제 사용량을 보고 판단해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;width: 847px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 325px;&quot;&gt;접근&lt;/th&gt;
&lt;th style=&quot;width: 188px;&quot;&gt;장점&lt;/th&gt;
&lt;th style=&quot;width: 334px;&quot;&gt;한계&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; width: 325px;&quot;&gt;조건 컬럼마다 단일 인덱스 추가&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 188px;&quot;&gt;만들기 쉽다&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 334px;&quot;&gt;실제 조회 패턴과 맞지 않을 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; width: 325px;&quot;&gt;무조건 복합 인덱스 추가&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 188px;&quot;&gt;특정 조회에는 빠를 수 있다&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 334px;&quot;&gt;컬럼 순서가 틀리면 효과가 작다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; width: 325px;&quot;&gt;전문 검색 없이 &lt;code&gt;LIKE '%keyword%'&lt;/code&gt; 유지&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 188px;&quot;&gt;구현이 단순하다&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 334px;&quot;&gt;데이터가 많아지면 풀스캔 가능성이 커진다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 시도에서 배운 점은 단순했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 컬럼 기준이 아니라 조회 패턴 기준으로 설계해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;2번째 시도: 조회 패턴 기준으로 복합 인덱스 설계하기&amp;nbsp;&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다음으로는 실제 API가 어떤 방식으로 조회되는지 먼저 정리했다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 게시글 목록 조회의 핵심 패턴이 아래와 같다고 가정한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;공개 상태의 게시글만 조회한다.&lt;/li&gt;
&lt;li&gt;최신순으로 정렬한다.&lt;/li&gt;
&lt;li&gt;대부분 최근 3개월 데이터만 조회한다.&lt;/li&gt;
&lt;li&gt;한 페이지에 20개씩 가져온다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우에는 단순히 &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt; 각각에 인덱스를 거는 것보다&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아래처럼 자주 함께 쓰이는 조건과 정렬을 고려한 복합 인덱스를 검토할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE INDEX idx_article_status_created_at
ON article(status, created_at DESC);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 조회 쿼리도 범위를 제한한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT id, title, writer_name, status, created_at
FROM article
WHERE status = 'PUBLISHED'
  AND created_at &amp;gt;= '2026-02-01 00:00:00'
  AND created_at &amp;lt;  '2026-05-01 00:00:00'
ORDER BY created_at DESC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;복합 인덱스에서는 컬럼 순서가 중요하다. &lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL 기준으로 &lt;code&gt;(status, created_at)&lt;/code&gt; 인덱스는 &lt;code&gt;status&lt;/code&gt;만 쓰는 조회나&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;status + created_at&lt;/code&gt;을 함께 쓰는 조회에 활용될 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;created_at&lt;/code&gt;만 조건으로 쓰는 조회에는 같은 방식으로 잘 활용되지 않을 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;선행 컬럼을 고를 때는 &lt;b&gt;카디널리티&lt;/b&gt;도 함께 본다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;카디널리티는 값의 종류가 얼마나 다양한지를 의미한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;보통 후보 row를 많이 줄일 수 있는 컬럼이 앞에 오면 유리하지만,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;실제 인덱스 순서는 동등 조건, 범위 조건, 정렬 조건, 데이터 분포를 함께 보고 결정해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D9Jex/dJMcaiwzv3T/vJbR8U1ixFKtZVsgh0Mc5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D9Jex/dJMcaiwzv3T/vJbR8U1ixFKtZVsgh0Mc5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D9Jex/dJMcaiwzv3T/vJbR8U1ixFKtZVsgh0Mc5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD9Jex%2FdJMcaiwzv3T%2FvJbR8U1ixFKtZVsgh0Mc5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;492&quot; height=&quot;584&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이때 중요한 것은 &lt;code&gt;EXPLAIN&lt;/code&gt;으로 확인하는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT id, title, writer_name, status, created_at
FROM article
WHERE status = 'PUBLISHED'
  AND created_at &amp;gt;= '2026-02-01 00:00:00'
  AND created_at &amp;lt;  '2026-05-01 00:00:00'
ORDER BY created_at DESC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;실행 계획에서 특히 아래 항목을 먼저 본다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;width: 851px; height: 100px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;width: 147px; height: 20px;&quot;&gt;항목&lt;/th&gt;
&lt;th style=&quot;width: 704px; height: 20px;&quot;&gt;확인할 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 147px; text-align: center; height: 20px;&quot;&gt;&lt;code&gt;type&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 704px; text-align: left; height: 20px;&quot;&gt;&lt;code&gt;ALL&lt;/code&gt;이면 풀 테이블 스캔 가능성을 의심한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 147px; text-align: center; height: 20px;&quot;&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 704px; text-align: left; height: 20px;&quot;&gt;실제 선택된 인덱스가 무엇인지 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 147px; text-align: center; height: 20px;&quot;&gt;&lt;code&gt;rows&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 704px; text-align: left; height: 20px;&quot;&gt;DB가 읽을 것으로 예상하는 row 수를 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 147px; text-align: center; height: 20px;&quot;&gt;&lt;code&gt;Extra&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 704px; text-align: left; height: 20px;&quot;&gt;&lt;code&gt;Using index&lt;/code&gt;, &lt;code&gt;Using filesort&lt;/code&gt;, &lt;code&gt;Using temporary&lt;/code&gt; 등을 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;Using index&lt;/code&gt;는 &amp;ldquo;인덱스를 아예 안 쓴다&amp;rdquo;는 뜻이 아니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL에서는 쿼리에 필요한 컬럼을 인덱스만으로 가져올 수 있을 때&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Using index&lt;/code&gt;가 표시될 수 있다. 흔히 말하는 &lt;b&gt;커버링 인덱스&lt;/b&gt; 상황이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 아래 쿼리는 필요한 컬럼이 모두 인덱스에 포함되어 있다면&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;테이블 row까지 추가로 접근하지 않고 인덱스에서 값을 가져올 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE INDEX idx_article_status_created_id
ON article(status, created_at DESC, id);

SELECT id, status, created_at
FROM article
WHERE status = 'PUBLISHED'
ORDER BY created_at DESC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 인덱스에 없는 컬럼을 함께 조회하면&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DB는 인덱스로 후보 row를 찾은 뒤 실제 테이블 row에 접근해야 할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT id, title, content, status, created_at
FROM article
WHERE status = 'PUBLISHED'
ORDER BY created_at DESC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;content&lt;/code&gt;가 인덱스에 없다면 이 값을 읽기 위해 테이블 row 접근이 필요하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다고 모든 조회 컬럼을 인덱스에 넣어야 한다는 뜻은 아니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스 크기와 쓰기 비용이 커지기 때문에, 목록 화면처럼 정말 자주 호출되고&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;필요한 컬럼이 적은 조회에만 신중하게 검토해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 목록 조회에서는 &lt;code&gt;SELECT *&lt;/code&gt;를 습관적으로 쓰지 않는 것이 중요하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;화면에 필요한 컬럼만 고르면 네트워크 전송량도 줄고, 커버링 인덱스를 검토할 여지도 생긴다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;3번째 시도: &lt;code&gt;LIKE&lt;/code&gt; 검색을 전문 검색으로 분리하기&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;검색어가 제목이나 본문 중간에 포함되는지를 찾아야 한다면&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LIKE '%keyword%'&lt;/code&gt;는 기능 요구사항에는 맞지만&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;성능 요구사항에는 불리할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이때 선택지는 크게 세 가지다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;width: 864px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 197px;&quot;&gt;선택지&lt;/th&gt;
&lt;th style=&quot;width: 320px;&quot;&gt;적합한 상황&lt;/th&gt;
&lt;th style=&quot;width: 347px;&quot;&gt;주의할 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 197px; text-align: center;&quot;&gt;&lt;code&gt;LIKE 'keyword%'&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 320px; text-align: center;&quot;&gt;접두어 검색이면 충분한 경우&lt;/td&gt;
&lt;td style=&quot;width: 347px;&quot;&gt;중간 단어 검색에는 맞지 않는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 197px; text-align: center;&quot;&gt;DB 전문 검색 인덱스&lt;/td&gt;
&lt;td style=&quot;width: 320px; text-align: center;&quot;&gt;DB 안에서 검색을 처리하고 싶은 경우&lt;/td&gt;
&lt;td style=&quot;width: 347px;&quot;&gt;형태소, 언어, stopword, 정렬 품질을 확인해야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 197px; text-align: center;&quot;&gt;Elasticsearch 같은 검색 엔진&lt;/td&gt;
&lt;td style=&quot;width: 320px; text-align: center;&quot;&gt;검색 품질, 랭킹, 다중 필드 검색이 중요한 경우&lt;/td&gt;
&lt;td style=&quot;width: 347px;&quot;&gt;동기화 구조와 운영 복잡도가 늘어난다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL에서는 &lt;code&gt;FULLTEXT&lt;/code&gt; 인덱스와&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MATCH() AGAINST()&lt;/code&gt; 문법을 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE FULLTEXT INDEX ft_article_title_content
ON article(title, content);

SELECT id, title, created_at
FROM article
WHERE MATCH(title, content) AGAINST ('spring boot' IN NATURAL LANGUAGE MODE)
ORDER BY created_at DESC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Oracle을 사용한다면 Oracle Text 같은 기능을 검토할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;검색 품질과 확장성이 더 중요하다면 &lt;b&gt;Elasticsearch&lt;/b&gt; 같은 별도 검색 엔진을 두는 방식도 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다만 검색 엔진을 붙인다고 모든 문제가 끝나는 것은 아니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DB와 검색 엔진 사이의 데이터 동기화, 장애 시 재처리,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;검색 결과와 원본 데이터의 정합성, 색인 지연을 함께 설계해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CQzaU/dJMcacpCxlw/wLFIT7MjGxk2koiTfbiCQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CQzaU/dJMcacpCxlw/wLFIT7MjGxk2koiTfbiCQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CQzaU/dJMcacpCxlw/wLFIT7MjGxk2koiTfbiCQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCQzaU%2FdJMcacpCxlw%2FwLFIT7MjGxk2koiTfbiCQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;541&quot; data-origin-width=&quot;693&quot; data-origin-height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;내가 정리한 기준은 이렇다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;단순 관리자 화면에서 가끔 쓰는 검색이면 &lt;code&gt;LIKE&lt;/code&gt;를 유지할 수도 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 사용자가 자주 검색하고 데이터가 계속 늘어나는 핵심 기능이라면&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;일반 인덱스가 아니라 전문 검색 구조를 검토해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;정규화와 비정규화는 언제 고민해야 할까?&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;정규화는 데이터 중복을 줄이고 변경 정합성을 지키기 위한 설계다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자 이름을 &lt;code&gt;user&lt;/code&gt; 테이블에만 두고 게시글에서는&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;user_id&lt;/code&gt;만 참조하면, 사용자 이름이 바뀌어도 한 곳만 수정하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 조회 API가 매번 여러 테이블을 조인해야 하고,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그 조회가 매우 자주 호출된다면 비정규화를 검토할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 게시글 목록에서 항상 작성자 이름이 필요하다고 가정한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT a.id, a.title, u.name, a.created_at
FROM article a
JOIN users u ON a.writer_id = u.id
WHERE a.status = 'PUBLISHED'
ORDER BY a.created_at DESC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 조인이 성능 병목이 되고 작성자 이름 변경이 드문 요구사항이라면,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;article.writer_name&lt;/code&gt;처럼 조회용 컬럼을 중복 저장하는 방법을 검토할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;width: 853px; height: 100px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;width: 100px; height: 20px;&quot;&gt;구분&lt;/th&gt;
&lt;th style=&quot;width: 281px; height: 20px;&quot;&gt;정규화&lt;/th&gt;
&lt;th style=&quot;width: 472px; height: 20px;&quot;&gt;비정규화&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 100px; text-align: center; height: 20px;&quot;&gt;목적&lt;/td&gt;
&lt;td style=&quot;width: 281px; text-align: center; height: 20px;&quot;&gt;중복 제거, 정합성 유지&lt;/td&gt;
&lt;td style=&quot;width: 472px; height: 20px;&quot;&gt;조회 성능, 쿼리 단순화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 100px; text-align: center; height: 20px;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 281px; text-align: center; height: 20px;&quot;&gt;수정 지점이 적다&lt;/td&gt;
&lt;td style=&quot;width: 472px; height: 20px;&quot;&gt;조인을 줄일 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 100px; text-align: center; height: 20px;&quot;&gt;단점&lt;/td&gt;
&lt;td style=&quot;width: 281px; text-align: center; height: 20px;&quot;&gt;조회 시 조인이 늘 수 있다&lt;/td&gt;
&lt;td style=&quot;width: 472px; height: 20px;&quot;&gt;데이터 불일치 가능성이 생긴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 100px; text-align: center; height: 20px;&quot;&gt;적용 기준&lt;/td&gt;
&lt;td style=&quot;width: 281px; text-align: center; height: 20px;&quot;&gt;변경이 잦고 정합성이 중요한 데이터&lt;/td&gt;
&lt;td style=&quot;width: 472px; height: 20px;&quot;&gt;조회가 매우 많고 변경이 적은 데이터&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;비정규화는 성능 최적화 수단이지만, 동시에 정합성 비용을 만드는 선택이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 &amp;ldquo;조인이 싫다&amp;rdquo;가 아니라 &amp;ldquo;이 조회가 병목이고,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;중복 데이터의 동기화 규칙을 관리할 수 있다&amp;rdquo;는 근거가 있을 때 적용해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;831&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KUH6K/dJMcafmlUue/96dQmSGP4ixjgHKKhEyts1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KUH6K/dJMcafmlUue/96dQmSGP4ixjgHKKhEyts1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KUH6K/dJMcafmlUue/96dQmSGP4ixjgHKKhEyts1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKUH6K%2FdJMcafmlUue%2F96dQmSGP4ixjgHKKhEyts1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;831&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;831&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;조회 범위를 줄이는 것도 성능 개선이다&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스를 잘 설계해도 조회 범위가 너무 넓으면&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DB가 읽어야 할 데이터가 많아진다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;실무에서는 &amp;ldquo;전체 기간 조회&amp;rdquo;가 생각보다 위험하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;사용자는 단순히 검색 버튼을 눌렀지만,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;서버 입장에서는 몇 년치 데이터를 대상으로 정렬과 필터링을 수행할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 목록 API에는 기간 조건을 기본값으로 두는 것이 도움이 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT id, title, status, created_at
FROM article
WHERE status = 'PUBLISHED'
  AND created_at &amp;gt;= CURRENT_DATE - INTERVAL 3 MONTH
ORDER BY created_at DESC
LIMIT 20;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;조회 범위를 제한하면 인덱스가 후보 row를 줄이기 쉬워진다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;또한 사용자도 실제로 필요한 최근 데이터부터 확인하게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;물론 모든 화면에 임의로 기간 제한을 걸면 안 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;감사, 정산, 법적 보관 데이터처럼 전체 기간 조회가 필요한 기능은&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;별도 화면이나 배치성 조회로 분리하는 편이 낫다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;전체 개수를 세지 않는 것도 방법이다&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;페이징을 만들 때 습관적으로 전체 개수를 함께 조회하는 경우가 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT COUNT(*)
FROM article
WHERE status = 'PUBLISHED'
  AND title LIKE '%spring%';&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;문제는 &lt;code&gt;COUNT&lt;/code&gt;도 공짜가 아니라는 점이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;특히 검색 조건이 복잡하고 필터링 대상이 많으면 DB는 개수를 구하기 위해 많은 row를 확인해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;사용자에게 반드시 &amp;ldquo;총 12,345건&amp;rdquo;이 필요한지 먼저 확인해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;필요하지 않다면 &lt;code&gt;hasNext&lt;/code&gt; 방식으로 바꿀 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT id, title, created_at
FROM article
WHERE status = 'PUBLISHED'
ORDER BY created_at DESC
LIMIT 21;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;한 페이지 크기가 20개라면 21개를 조회해서 다음 페이지 존재 여부만 판단한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 전체 개수를 세지 않아도 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;381&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZ8GAx/dJMb990KHEu/F7twHkcmS3kv9xsFJFAPj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZ8GAx/dJMb990KHEu/F7twHkcmS3kv9xsFJFAPj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZ8GAx/dJMb990KHEu/F7twHkcmS3kv9xsFJFAPj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZ8GAx%2FdJMb990KHEu%2FF7twHkcmS3kv9xsFJFAPj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;953&quot; height=&quot;381&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;381&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 무한 스크롤이나 &amp;ldquo;더보기&amp;rdquo; UI와 잘 맞는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;반대로 정확한 전체 페이지 수가 꼭 필요한 관리자 화면에서는&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;COUNT&lt;/code&gt;를 유지하되, 검색 조건과 인덱스를 더 신중하게 봐야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;정확한 숫자가 중요하지 않은 화면이라면 &lt;b&gt;추정값&lt;/b&gt;이나 &lt;b&gt;캐시&lt;/b&gt;를 쓰는 방법도 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 통계 테이블에 일정 주기로 집계한 값을 저장하거나,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;자주 반복되는 카운트 결과를 짧은 시간 동안 캐싱할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다만 이 경우에는 사용자에게 보여주는 숫자가&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;실시간 정확값이 아닐 수 있다는 점을 화면 요구사항과 맞춰야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;오래된 데이터는 삭제하거나 분리 보관해야 한다&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;성능 문제는 쿼리만의 문제가 아닐 때가 많다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;테이블이 너무 커져서 어떤 조회를 해도 부담이 되는 상황이 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 로그인 이력, 알림 이력, API 호출 로그처럼 계속 쌓이는 데이터는&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;시간이 지나면 조회 빈도가 낮아진다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이런 데이터는 보관 정책을 정하고 삭제하거나 별도 보관 테이블로 분리할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;width: 854px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 189px;&quot;&gt;데이터&lt;/th&gt;
&lt;th style=&quot;width: 172px;&quot;&gt;운영 테이블&lt;/th&gt;
&lt;th style=&quot;width: 493px;&quot;&gt;보관 테이블&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 189px; text-align: center;&quot;&gt;최근 3개월 로그인 이력&lt;/td&gt;
&lt;td style=&quot;width: 172px; text-align: center;&quot;&gt;자주 조회&lt;/td&gt;
&lt;td style=&quot;width: 493px;&quot;&gt;유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 189px; text-align: center;&quot;&gt;3개월 이전 로그인 이력&lt;/td&gt;
&lt;td style=&quot;width: 172px; text-align: center;&quot;&gt;거의 조회하지 않음&lt;/td&gt;
&lt;td style=&quot;width: 493px;&quot;&gt;archive 테이블 또는 별도 저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 189px; text-align: center;&quot;&gt;법적 보관 대상&lt;/td&gt;
&lt;td style=&quot;width: 172px; text-align: center;&quot;&gt;삭제 불가&lt;/td&gt;
&lt;td style=&quot;width: 493px;&quot;&gt;접근 빈도에 맞춰 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nx2uc/dJMcahYJMuS/GwYRZduVSvoQp2A55ncGxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nx2uc/dJMcahYJMuS/GwYRZduVSvoQp2A55ncGxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nx2uc/dJMcahYJMuS/GwYRZduVSvoQp2A55ncGxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNx2uc%2FdJMcahYJMuS%2FGwYRZduVSvoQp2A55ncGxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;479&quot; height=&quot;654&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;삭제나 분리는 기능팀 혼자 결정할 수 없다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;보관 기간, 감사 요건, 장애 분석 필요성, 개인정보 정책을 함께 확인해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;DB 장비 확장과 캐시는 마지막 카드가 아니다&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;DB 성능을 이야기하면 장비 확장이나 캐시 서버도 자주 나온다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;장비 확장은 분명 효과가 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;CPU, 메모리, 디스크 I/O가 부족한 상황에서는 스케일업이나&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;읽기 전용 복제 DB 구성이 필요할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;캐시도 강력하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;자주 조회되지만 자주 바뀌지 않는 데이터는 Redis 같은&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;별도 캐시 서버에 두면 DB 부하를 줄일 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 둘 다 쿼리와 데이터 구조를 보지 않고&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;바로 적용하면 문제를 늦게 발견하게 만들 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;width: 849px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 152px;&quot;&gt;방법&lt;/th&gt;
&lt;th style=&quot;width: 222px;&quot;&gt;효과&lt;/th&gt;
&lt;th style=&quot;width: 475px;&quot;&gt;먼저 확인할 점&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 152px; text-align: center;&quot;&gt;DB 스케일업&lt;/td&gt;
&lt;td style=&quot;width: 222px; text-align: center;&quot;&gt;CPU, 메모리, I/O 여유 증가&lt;/td&gt;
&lt;td style=&quot;width: 475px;&quot;&gt;비효율 쿼리가 그대로 남는지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 152px; text-align: center;&quot;&gt;Read replica&lt;/td&gt;
&lt;td style=&quot;width: 222px; text-align: center;&quot;&gt;읽기 트래픽 분산&lt;/td&gt;
&lt;td style=&quot;width: 475px;&quot;&gt;복제 지연과 읽기 정합성 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 152px; text-align: center;&quot;&gt;Redis 캐시&lt;/td&gt;
&lt;td style=&quot;width: 222px; text-align: center;&quot;&gt;반복 조회 부하 감소&lt;/td&gt;
&lt;td style=&quot;width: 475px;&quot;&gt;만료 정책과 데이터 무효화 전략 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 152px; text-align: center;&quot;&gt;검색 엔진&lt;/td&gt;
&lt;td style=&quot;width: 222px; text-align: center;&quot;&gt;검색 부하 분리&lt;/td&gt;
&lt;td style=&quot;width: 475px;&quot;&gt;색인 동기화와 장애 복구 전략 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;캐시는 특히 무효화 전략이 중요하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 수정했는데 캐시를 지우지 않으면 사용자는 오래된 값을 보게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 캐시를 넣을 때는 &amp;ldquo;언제 저장할지&amp;rdquo;보다 &amp;ldquo;언제 지울지&amp;rdquo;를 먼저 설계해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;내가 기억하려는 우선순위는&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;&lt;b&gt;쿼리 확인 &amp;rarr; 인덱스 설계 &amp;rarr; 데이터 분리 &amp;rarr; 캐시 &amp;rarr; 장비 확장&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스도 확인하지 않은 상태에서 캐시나 장비 확장부터 적용하면,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;느린 원인을 가린 채 운영 비용만 키울 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;최종 정리: DB 성능을 볼 때의 순서&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;내가 이번에 정리한 순서는 아래와 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;1090&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t60Re/dJMcabxrnV9/HCHKB2GckIVocKPzfiPJDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t60Re/dJMcabxrnV9/HCHKB2GckIVocKPzfiPJDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t60Re/dJMcabxrnV9/HCHKB2GckIVocKPzfiPJDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft60Re%2FdJMcabxrnV9%2FHCHKB2GckIVocKPzfiPJDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;1090&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;1090&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 인덱스를 먼저 만들기 전에 조회 패턴을 먼저 보는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;내가 기억하려는 기준은 아래와 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;풀스캔을 줄이려면 &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;LIMIT&lt;/code&gt;을 함께 보고 인덱스를 설계한다.&lt;/li&gt;
&lt;li&gt;복합 인덱스는 컬럼 순서가 중요하며, leftmost prefix를 이해해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LIKE '%keyword%'&lt;/code&gt;는 일반 인덱스보다 전문 검색 구조가 필요한 신호일 수 있다.&lt;/li&gt;
&lt;li&gt;인덱스에 포함된 컬럼만 조회하면 커버링 인덱스로 테이블 접근을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;인덱스에 없는 컬럼까지 조회하면 후보 row를 찾은 뒤 실제 row 접근이 필요할 수 있다.&lt;/li&gt;
&lt;li&gt;인덱스는 읽기 성능을 돕지만 쓰기 비용과 저장 공간 비용을 만든다.&lt;/li&gt;
&lt;li&gt;비정규화는 조인을 줄일 수 있지만 데이터 불일치 비용을 만든다.&lt;/li&gt;
&lt;li&gt;전체 개수가 꼭 필요하지 않다면 &lt;code&gt;COUNT&lt;/code&gt; 대신 &lt;code&gt;hasNext&lt;/code&gt; 방식을 검토한다.&lt;/li&gt;
&lt;li&gt;오래된 데이터는 운영 테이블에서 계속 들고 있을수록 조회와 관리 비용이 커진다.&lt;/li&gt;
&lt;li&gt;캐시와 장비 확장은 효과가 있지만, 비효율 쿼리를 덮는 방식으로 쓰면 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이번 내용을 정리하면서 DB 성능 개선은&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;빠른 쿼리 하나 만들기&amp;rdquo;가 아니라는 생각이 들었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;성능 문제는 보통 여러 층이 겹쳐서 생긴다. 조회 범위가 넓고,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;검색 조건은 인덱스를 타기 어렵고, 전체 개수까지 세고,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;오래된 데이터도 같은 테이블에 계속 쌓이면 느려질 수밖에 없다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 내가 실무에서 먼저 할 일은 인덱스를 추가하는 것이 아니라,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;느린 API의 조회 패턴을 문장으로 설명하는 일이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 API는 어떤 사용자가, 어떤 조건으로, 어느 기간의 데이터를, 몇 건이나 필요로 하는가?&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 질문에 답할 수 있어야 인덱스도 설계할 수 있고,&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;전문 검색도 검토할 수 있고, 캐시나 장비 확장도 근거 있게 이야기할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/mysql-indexes.html&quot;&gt;MySQL 8.4 Reference Manual - How MySQL Uses Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/multiple-column-indexes.html&quot;&gt;MySQL 8.4 Reference Manual - Multiple-Column Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/explain-output.html&quot;&gt;MySQL 8.4 Reference Manual - EXPLAIN Output Format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/fulltext-search.html&quot;&gt;MySQL 8.4 Reference Manual - Full-Text Search Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-fulltext-index.html&quot;&gt;MySQL 8.4 Reference Manual - InnoDB Full-Text Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/26/ccapp/index.html&quot;&gt;Oracle Text Application Developer's Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/docs/reference/query-languages/query-dsl/full-text-queries&quot;&gt;Elasticsearch Reference - Full text queries&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category> ️ DevOpѕ</category>
      <category>boot세션</category>
      <category>java</category>
      <category>NPE</category>
      <category>nullpointexception</category>
      <category>session</category>
      <category>springboot</category>
      <category>백엔드</category>
      <category>사용자 식별값</category>
      <category>세션</category>
      <category>주니어개발자</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/267</guid>
      <comments>https://yurizzy.tistory.com/267#entry267comment</comments>
      <pubDate>Sat, 16 May 2026 23:10:15 +0900</pubDate>
    </item>
    <item>
      <title> &amp;zwj;  Springframwork Mig 기록 : 화면에는 없는데 서버에는 필요한 값들 (feat. 세션 기반 파라미터)</title>
      <link>https://yurizzy.tistory.com/266</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DwU2A/dJMcadIyvjg/TTKB1fK1qKVYbZqQV5oa2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DwU2A/dJMcadIyvjg/TTKB1fK1qKVYbZqQV5oa2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DwU2A/dJMcadIyvjg/TTKB1fK1qKVYbZqQV5oa2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDwU2A%2FdJMcadIyvjg%2FTTKB1fK1qKVYbZqQV5oa2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;559&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 시스템을 Spring Boot 3 기반으로 옮기면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller 응답 방식과 세션 접근 방식을 함께 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 요청 파라미터만 잘 넘기면 기능이 그대로 동작할 거라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 일부 기능에서 &lt;code&gt;NullPointerException&lt;/code&gt;이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상했던 점은 화면에서 넘기는 값은 빠진 게 없어 보였다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에는 없지만 서버 로직에는 꼭 필요한 값이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 &lt;b&gt;세션에서 꺼내 쓰던 사용자 식별값&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 레거시 코드에서 암묵적으로 사용하던 세션 기반 값을 어떻게 추적했고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 후에는 어떻게 NPE를 방어했는지 정리한 기록이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발환경&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AS-IS&lt;/th&gt;
&lt;th&gt;TO-BE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java 1.7&lt;/td&gt;
&lt;td&gt;Java 17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Framework 4 계열&lt;/td&gt;
&lt;td&gt;Spring Boot 3 계열&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSP + Spring MVC&lt;/td&gt;
&lt;td&gt;JSP + Spring MVC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ModelAndView&lt;/code&gt; + jsonView&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt;&lt;/code&gt; 직접 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;레거시 세션 핸들러 직접 호출&lt;/td&gt;
&lt;td&gt;세션 유틸리티 기반 접근&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글의 예제 코드는 회사 소스코드가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 업무에서 겪은 문제 흐름만 비슷하게 재구성한 독립 실행 예제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2320&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kGcrg/dJMcahc94CP/zJAToHhYZ1aK72fo1BRpnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kGcrg/dJMcahc94CP/zJAToHhYZ1aK72fo1BRpnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kGcrg/dJMcahc94CP/zJAToHhYZ1aK72fo1BRpnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGcrg%2FdJMcahc94CP%2FzJAToHhYZ1aK72fo1BRpnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2320&quot; height=&quot;486&quot; data-origin-width=&quot;2320&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qqicM/dJMcagFl344/NfKazD6VWhVMwkhh6TLk5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qqicM/dJMcagFl344/NfKazD6VWhVMwkhh6TLk5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qqicM/dJMcagFl344/NfKazD6VWhVMwkhh6TLk5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqqicM%2FdJMcagFl344%2FNfKazD6VWhVMwkhh6TLk5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;844&quot; height=&quot;514&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 중 특정 등록 API를 테스트했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청에는 &lt;code&gt;deviceName&lt;/code&gt; 같은 화면 입력값이 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 서비스 로직에서는 요청값뿐 아니라&lt;br /&gt;&lt;code&gt;userSeq&lt;/code&gt;, &lt;code&gt;empId&lt;/code&gt; 같은 사용자 식별값도 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 프론트에서 파라미터를 누락한 줄 알았다. &lt;br /&gt;하지만 JSP와 JavaScript를 확인해도 해당 값은 화면에서 보내는 값이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 구조에서는 Controller가 세션에서 사용자 정보를 꺼내 DTO에 직접 채우고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 값들은 요청 파라미터가 아니라 &lt;b&gt;세션 기반 파라미터&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 헷갈렸던 지점은 여기였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;height: 203px;&quot; width=&quot;774&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;화면에서 보이는가&lt;/th&gt;
&lt;th&gt;서버에서 필요한가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deviceName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;보인다&lt;/td&gt;
&lt;td&gt;필요하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;userSeq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;보이지 않는다&lt;/td&gt;
&lt;td&gt;필요하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;empId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;보이지 않는다&lt;/td&gt;
&lt;td&gt;필요하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;companyCode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;보이지 않는다&lt;/td&gt;
&lt;td&gt;일부 로직에서 필요하다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 문제는 &amp;ldquo;파라미터가 안 넘어왔다&amp;rdquo;가 아니라 &lt;br /&gt;&amp;ldquo;세션에서 채워야 하는 값을 마이그레이션 과정에서 놓쳤다&amp;rdquo;에 가까웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJhY2z/dJMcabcUKsT/2PLv4gQi8pRpibix24arkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJhY2z/dJMcabcUKsT/2PLv4gQi8pRpibix24arkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJhY2z/dJMcabcUKsT/2PLv4gQi8pRpibix24arkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJhY2z%2FdJMcabcUKsT%2F2PLv4gQi8pRpibix24arkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1158&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1번째 시도: 요청 파라미터만 확인하기 (실패)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clccse/dJMcacCWA3A/XyKlyFoD7d0KJPaQK8bgK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clccse/dJMcacCWA3A/XyKlyFoD7d0KJPaQK8bgK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clccse/dJMcacCWA3A/XyKlyFoD7d0KJPaQK8bgK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fclccse%2FdJMcacCWA3A%2FXyKlyFoD7d0KJPaQK8bgK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;980&quot; height=&quot;702&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시도한 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API가 실패하면 가장 먼저 요청값을 의심하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 브라우저 네트워크 탭과 서버 로그에서 요청 파라미터를 먼저 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에서 입력한 값은 정상적으로 넘어오고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음에는 &lt;code&gt;name&lt;/code&gt; 속성 오타나 JavaScript 객체 생성 문제를 찾으려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 파라미터를 기준으로만 보면 코드는 대략 이런 흐름이었다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;Map&amp;lt;String, String&amp;gt; requestParams = Map.of(&quot;deviceName&quot;, &quot;phone&quot;);

String deviceName = requestParams.get(&quot;deviceName&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 정상적으로 존재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점/한계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 핵심은 요청 파라미터가 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 로직은 &lt;code&gt;deviceName&lt;/code&gt;만으로 동작하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록 로직에는 &amp;ldquo;누가 등록했는가&amp;rdquo;가 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정보는 화면에서 보내지 않고 로그인 이후 서버 세션에 저장된 값을 사용하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 파라미터만 보면 원인을 찾을 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;1650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c63AgW/dJMcahYwKi2/bokuaKubY3CQ78qLbzn0DK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c63AgW/dJMcahYwKi2/bokuaKubY3CQ78qLbzn0DK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c63AgW/dJMcahYwKi2/bokuaKubY3CQ78qLbzn0DK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc63AgW%2FdJMcahYwKi2%2FbokuaKubY3CQ78qLbzn0DK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;604&quot; height=&quot;1650&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;1650&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배운 점&lt;br /&gt;마이그레이션 QA에서 입력값을 확인할 때는 request parameter만 보면 안 된다. session attribute, request attribute, interceptor에서 주입하는 값까지 함께 봐야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2번째 시도: 세션에서 암묵적으로 채우던 값 추적하기 (성공)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2DWqj/dJMcah5jRbR/h1ZBOfyfcb5QklzkR7ToV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2DWqj/dJMcah5jRbR/h1ZBOfyfcb5QklzkR7ToV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2DWqj/dJMcah5jRbR/h1ZBOfyfcb5QklzkR7ToV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2DWqj%2FdJMcah5jRbR%2Fh1ZBOfyfcb5QklzkR7ToV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;506&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시도한 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 Controller를 다시 보니 요청값을 DTO에 넣은 뒤&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 사용자 정보도 함께 DTO에 넣고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 화면에서는 보이지 않지만 Controller 안에서 조립되는 값이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 구조를 공개용 예제로 단순화하면 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;SessionUser sessionUser = LegacySessionHandler.getLoginInfo(request);

RegisterCommand command = new RegisterCommand(
        sessionUser.userSeq(),
        sessionUser.empId(),
        requestParams.get(&quot;deviceName&quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 세션이 항상 있다고 가정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 상태에서 정상 흐름만 테스트하면 문제가 없어 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 세션이 없거나 만료된 상태에서 접근하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sessionUser&lt;/code&gt;가 &lt;code&gt;null&lt;/code&gt;이 되고, 바로 NPE가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;Cannot invoke &quot;SessionUser.userSeq()&quot; because sessionUser is null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 업무에서도 이와 비슷하게&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;화면에서 보이지 않는 값&amp;rdquo;이 세션에서 채워지고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MJcEV/dJMcabKPcL0/eD3n9WP7kQGWeyC0zbrs81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MJcEV/dJMcabKPcL0/eD3n9WP7kQGWeyC0zbrs81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MJcEV/dJMcabKPcL0/eD3n9WP7kQGWeyC0zbrs81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMJcEV%2FdJMcabKPcL0%2FeD3n9WP7kQGWeyC0zbrs81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2304&quot; height=&quot;1086&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;1086&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 프론트 파라미터 누락이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션하면서 세션 접근 방식을 바꾸는 과정에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 사용자 정보가 필요한 기능과 그렇지 않은 기능을 구분해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;배운 점&lt;br /&gt;레거시 코드는 Controller 안에서 요청값과 세션값을 섞어 DTO를 만드는 경우가 많다. 이때 세션값은 화면에 보이지 않기 때문에 QA 목록에 따로 적어두지 않으면 놓치기 쉽다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 해결: 세션 유틸리티로 접근을 통일하고 NPE를 먼저 막기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eCHsAT/dJMcadPi48k/kBlUf0SZVsxvmrFXEMOrV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eCHsAT/dJMcadPi48k/kBlUf0SZVsxvmrFXEMOrV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eCHsAT/dJMcadPi48k/kBlUf0SZVsxvmrFXEMOrV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeCHsAT%2FdJMcadPi48k%2FkBlUf0SZVsxvmrFXEMOrV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1354&quot; height=&quot;894&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;894&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로는 세션 접근을 한 곳으로 모으고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 없을 때 바로 명확한 응답을 반환하는 방식으로 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 두 가지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Controller마다 세션을 직접 꺼내지 않고 &lt;code&gt;SessionUtils&lt;/code&gt; 같은 유틸리티를 통해 접근한다.&lt;/li&gt;
&lt;li&gt;세션이 없으면 서비스 로직으로 내려가기 전에 &lt;code&gt;LOGIN_REQUIRED&lt;/code&gt; 같은 명확한 응답을 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 회사 소스코드가 아니라 실행 가능한 예제다. Java 17 기준으로 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 방법&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;javac SessionHiddenParameterExample.java
java -ea SessionHiddenParameterExample&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-ea&lt;/code&gt; 옵션은 Java assertion을 활성화하는 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 가능한 예제 코드&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class SessionHiddenParameterExample {
    public static void main(String[] args) {
        legacyControllerThrowsNullPointerExceptionWhenSessionIsMissing();
        migratedControllerReturnsUnauthorizedWhenSessionIsMissing();
        migratedControllerMergesRequestParameterAndSessionValue();
        System.out.println(&quot;All tests passed&quot;);
    }

    static void legacyControllerThrowsNullPointerExceptionWhenSessionIsMissing() {
        LegacyController controller = new LegacyController(new RegisterService());

        try {
            controller.registerDevice(Map.of(&quot;deviceName&quot;, &quot;phone&quot;), RequestContext.empty());
            throw new AssertionError(&quot;NullPointerException이 발생해야 한다&quot;);
        } catch (NullPointerException expected) {
            assert expected.getMessage().contains(&quot;null&quot;) : expected.getMessage();
        }
    }

    static void migratedControllerReturnsUnauthorizedWhenSessionIsMissing() {
        MigratedController controller = new MigratedController(new RegisterService());

        ApiResponse response = controller.registerDevice(
                Map.of(&quot;deviceName&quot;, &quot;phone&quot;),
                RequestContext.empty()
        );

        assert response.status() == 401 : response;
        assert &quot;LOGIN_REQUIRED&quot;.equals(response.body().get(&quot;message&quot;)) : response;
    }

    static void migratedControllerMergesRequestParameterAndSessionValue() {
        MigratedController controller = new MigratedController(new RegisterService());
        RequestContext request = RequestContext.withSession(
                new SessionUser(10L, &quot;E1001&quot;, &quot;C01&quot;)
        );

        ApiResponse response = controller.registerDevice(
                Map.of(&quot;deviceName&quot;, &quot;phone&quot;),
                request
        );

        assert response.status() == 200 : response;
        assert Long.valueOf(10L).equals(response.body().get(&quot;userSeq&quot;)) : response;
        assert &quot;E1001&quot;.equals(response.body().get(&quot;empId&quot;)) : response;
        assert &quot;phone&quot;.equals(response.body().get(&quot;deviceName&quot;)) : response;
    }

    static class LegacyController {
        private final RegisterService registerService;

        LegacyController(RegisterService registerService) {
            this.registerService = registerService;
        }

        ApiResponse registerDevice(Map&amp;lt;String, String&amp;gt; requestParams, RequestContext request) {
            SessionUser sessionUser = LegacySessionHandler.getLoginInfo(request);

            RegisterCommand command = new RegisterCommand(
                    sessionUser.userSeq(),
                    sessionUser.empId(),
                    requestParams.get(&quot;deviceName&quot;)
            );

            return ApiResponse.ok(registerService.register(command));
        }
    }

    static class MigratedController {
        private final RegisterService registerService;

        MigratedController(RegisterService registerService) {
            this.registerService = registerService;
        }

        ApiResponse registerDevice(Map&amp;lt;String, String&amp;gt; requestParams, RequestContext request) {
            Optional&amp;lt;SessionUser&amp;gt; sessionUser = SessionUtils.currentUser(request);
            if (sessionUser.isEmpty()) {
                return ApiResponse.of(401, Map.of(&quot;message&quot;, &quot;LOGIN_REQUIRED&quot;));
            }

            String deviceName = requestParams.get(&quot;deviceName&quot;);
            if (deviceName == null || deviceName.isBlank()) {
                return ApiResponse.of(400, Map.of(&quot;message&quot;, &quot;DEVICE_NAME_REQUIRED&quot;));
            }

            RegisterCommand command = RegisterCommand.from(requestParams, sessionUser.get());
            return ApiResponse.ok(registerService.register(command));
        }
    }

    static class LegacySessionHandler {
        static SessionUser getLoginInfo(RequestContext request) {
            return request.sessionUser();
        }
    }

    static class SessionUtils {
        static Optional&amp;lt;SessionUser&amp;gt; currentUser(RequestContext request) {
            return Optional.ofNullable(request.sessionUser());
        }
    }

    static class RegisterService {
        Map&amp;lt;String, Object&amp;gt; register(RegisterCommand command) {
            Map&amp;lt;String, Object&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
            result.put(&quot;userSeq&quot;, command.userSeq());
            result.put(&quot;empId&quot;, command.empId());
            result.put(&quot;deviceName&quot;, command.deviceName());
            return result;
        }
    }

    record RegisterCommand(Long userSeq, String empId, String deviceName) {
        static RegisterCommand from(Map&amp;lt;String, String&amp;gt; requestParams, SessionUser sessionUser) {
            return new RegisterCommand(
                    sessionUser.userSeq(),
                    sessionUser.empId(),
                    requestParams.get(&quot;deviceName&quot;)
            );
        }
    }

    record ApiResponse(int status, Map&amp;lt;String, Object&amp;gt; body) {
        static ApiResponse ok(Map&amp;lt;String, Object&amp;gt; body) {
            return new ApiResponse(200, body);
        }

        static ApiResponse of(int status, Map&amp;lt;String, Object&amp;gt; body) {
            return new ApiResponse(status, body);
        }
    }

    record SessionUser(Long userSeq, String empId, String companyCode) {
    }

    record RequestContext(SessionUser sessionUser) {
        static RequestContext empty() {
            return new RequestContext(null);
        }

        static RequestContext withSession(SessionUser sessionUser) {
            return new RequestContext(sessionUser);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;All tests passed&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서 중요한 부분은 &lt;code&gt;MigratedController&lt;/code&gt;다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;1622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpE5QJ/dJMcagSRUw5/n7HKAf7eSe0NkU2lgFW5h0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpE5QJ/dJMcagSRUw5/n7HKAf7eSe0NkU2lgFW5h0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpE5QJ/dJMcagSRUw5/n7HKAf7eSe0NkU2lgFW5h0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpE5QJ%2FdJMcagSRUw5%2Fn7HKAf7eSe0NkU2lgFW5h0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1140&quot; height=&quot;1622&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;1622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Optional&amp;lt;SessionUser&amp;gt; sessionUser = SessionUtils.currentUser(request);
if (sessionUser.isEmpty()) {
    return ApiResponse.of(401, Map.of(&quot;message&quot;, &quot;LOGIN_REQUIRED&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 없으면 DTO를 만들지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스도 호출하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 인증 상태를 확인하고, 실패 응답을 명확하게 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 요청 파라미터와 세션값을 합쳐 Command 객체를 만든다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;RegisterCommand command = RegisterCommand.from(requestParams, sessionUser.get());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정리하면 &amp;ldquo;화면에서 넘어온 값&amp;rdquo;과 &amp;ldquo;서버 세션에서 채운 값&amp;rdquo;의 경계가 분명해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마이그레이션 QA에서 추가한 체크 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 일을 겪고 나서 QA 시나리오에 세션 기반 값을 별도로 적었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/muLVp/dJMcac3X5OU/Pm8YoyNwR7NUi6IkOykhK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/muLVp/dJMcac3X5OU/Pm8YoyNwR7NUi6IkOykhK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/muLVp/dJMcac3X5OU/Pm8YoyNwR7NUi6IkOykhK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmuLVp%2FdJMcac3X5OU%2FPm8YoyNwR7NUi6IkOykhK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1300&quot; height=&quot;1536&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;height: 253px;&quot; width=&quot;858&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;검증 항목&lt;/th&gt;
&lt;th&gt;확인 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;정상 세션&lt;/td&gt;
&lt;td&gt;로그인 후 &lt;code&gt;userSeq&lt;/code&gt;, &lt;code&gt;empId&lt;/code&gt; 같은 사용자 값이 정상 추출되는지 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;세션 없음&lt;/td&gt;
&lt;td&gt;NPE가 아니라 명확한 인증 실패 응답이 내려오는지 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;세션 만료&lt;/td&gt;
&lt;td&gt;만료된 세션으로 접근했을 때 로그인 페이지 또는 인증 실패 응답으로 흐르는지 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;요청값 누락&lt;/td&gt;
&lt;td&gt;화면 입력값이 없을 때 400 계열 응답 또는 명확한 메시지가 내려오는지 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;세션값 + 요청값 조합&lt;/td&gt;
&lt;td&gt;DTO나 Command 객체에 두 종류의 값이 모두 들어가는지 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 체크리스트를 만들고 나니 &amp;ldquo;기능이 된다&amp;rdquo;와&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;기능이 같은 방식으로 된다&amp;rdquo;를 더 구분해서 볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 마이그레이션에서는 이 차이가 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에서 같은 버튼을 눌렀을 때 결과가 같아 보여도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부에서 필요한 값이 누락되면 특정 예외 케이스에서 바로 깨질 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 파라미터에 보이지 않는 값도 서버 로직에서는 필수값일 수 있다.&lt;/li&gt;
&lt;li&gt;레거시 Controller는 요청값, 세션값, 공통 유틸 값을 한 번에 조립하는 경우가 많다.&lt;/li&gt;
&lt;li&gt;마이그레이션할 때는 &lt;code&gt;request parameter&lt;/code&gt;뿐 아니라 &lt;code&gt;session attribute&lt;/code&gt;도 QA 항목으로 분리해야 한다.&lt;/li&gt;
&lt;li&gt;세션이 없을 때 NPE가 나는 코드는 정상 케이스 테스트만으로는 발견하기 어렵다.&lt;/li&gt;
&lt;li&gt;세션 접근을 유틸리티로 통일하면 null 방어와 응답 정책을 한 곳에서 맞추기 쉬워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 하면서 &amp;ldquo;화면에 없는데 서버에는 필요한 값&amp;rdquo;을 보는 눈이 조금 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 프론트에서 값을 안 넘긴 줄 알고 한참을 요청 파라미터만 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 실제 원인은 세션에서 암묵적으로 채우던 값을 놓친 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 마이그레이션은 코드를 새 문법으로 바꾸는 작업만은 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드가 어디에서 값을 가져오고 있었는지, 그 값이 어떤 전제 위에서 동작했는지 다시 확인하는 작업이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/arguments.html&quot;&gt;Spring Framework 공식 문서 - MVC Controller Method Arguments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/sessionattribute.html&quot;&gt;Spring Framework 공식 문서 - @SessionAttribute&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/7/docs/technotes/guides/language/assert.html&quot;&gt;Oracle Java 공식 문서 - Programming With Assertions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  Fr&amp;alpha;мeworĸ</category>
      <category>boot세션</category>
      <category>java</category>
      <category>NPE</category>
      <category>nullpointexception</category>
      <category>session</category>
      <category>springboot</category>
      <category>백엔드</category>
      <category>사용자 식별값</category>
      <category>세션</category>
      <category>주니어개발자</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/266</guid>
      <comments>https://yurizzy.tistory.com/266#entry266comment</comments>
      <pubDate>Tue, 28 Apr 2026 23:51:50 +0900</pubDate>
    </item>
    <item>
      <title>⚡️개발자가 자주 사용하는 NANO명령어 (알쓸나잡) : Vim 유저도 당황하지 않는 Nano 편집기 완벽 가이드 (설정부터 커스텀까지)</title>
      <link>https://yurizzy.tistory.com/265</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/suCKV/dJMcacv7G8e/nV4GtLSRgJkv0hGGLs7LK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/suCKV/dJMcacv7G8e/nV4GtLSRgJkv0hGGLs7LK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/suCKV/dJMcacv7G8e/nV4GtLSRgJkv0hGGLs7LK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsuCKV%2FdJMcacv7G8e%2FnV4GtLSRgJkv0hGGLs7LK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;559&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작에 앞서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 리눅스 서버를 운영하다 보면 Vim이 손에 익어있음에도 불구하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 배포판 환경이나 인프라 설정 때문에 &lt;b&gt;Nano 편집기&lt;/b&gt;를 써야 하는 상황이 생기곤 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Vim은 되는데 Nano는 왜 안 되지?&quot;라고 답답해하셨던 분들을 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nano를 Vim처럼 강력하게 만드는 설정법과 필수 명령어들을 정리했습니다.  &amp;zwj;♀️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  한눈에 보는 에디터 비교: Nano vs Vim&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 에디터의 가장 큰 차이는 &lt;b&gt;'모드(Mode)'의 존재 여부&lt;/b&gt;입니다. 아래 표를 통해 나에게 맞는 도구를 확인해 보세요.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;구분&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;&lt;b&gt;Nano (직관적인 메모장)&lt;/b&gt;&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;&lt;b&gt;Vim (강력한 모달 편집기)&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;핵심 컨셉&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Modeless&lt;/b&gt;: 보이는 대로 입력&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;Modal&lt;/b&gt;: 모드 전환을 통한 조작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;진입 장벽&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;매우 낮음 (메모장 수준)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;높음 (학습 곡선 존재)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;주요 장점&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;하단 단축키 도움말 상시 표시&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;키보드만으로 압도적인 편집 속도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;추천 상황&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;간단한 설정 파일 수정, 초보자&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;대규모 코드 수정, 서버 상주 작업&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;결정적 차이&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;입력과 명령이 동시에 가능&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;입력(Insert)&lt;/b&gt;과 &lt;b&gt;명령(Normal)&lt;/b&gt; 모드 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;Vim이 '운전면허가 필요한 스포츠카'라면, Nano는 '누구나 탈 수 있는 자전거'와 같습니다.&quot;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Nano의 첫인상을 바꾸는 &lt;code&gt;~/.nanorc&lt;/code&gt; 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nano가 불편하게 느껴지는 가장 큰 이유는 '익숙한 개발 환경' 세팅이 안 되어 있기 때문입니다. 홈 디렉토리에 &lt;code&gt;.nanorc&lt;/code&gt; 파일을 수정하여 Nano를 현대적인 에디터로 변환해 보세요.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 설정 파일 열기 (또는 생성)
nano ~/.nanorc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필수 추천 설정값&lt;/h3&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;## 1. 시각적 요소 설정
set linenumbers        # 줄 번호 표시
set constantshow       # 하단에 항상 커서 위치(줄/열) 표시
set mouse              # 마우스 클릭으로 커서 이동 및 스크롤 활성화

## 2. 편집 효율 설정
set tabsize 4          # 탭 크기를 4칸으로 설정
set tabstospaces       # 탭을 스페이스로 자동 변환
set softwrap           # 화면 너비를 넘어가는 긴 줄 자동 줄바꿈
set autoindent         # 자동 들여쓰기 활성화

## 3. 구문 강조 (Syntax Highlighting)
include &quot;/usr/share/nano/*.nanorc&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Vim 유저를 위한 &quot;Nano에서 Vim 맛내기&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vim의 단축키가 손가락에 박혀있는 분들을 위해, Nano에서도 유사한 경험을 할 수 있도록 &lt;code&gt;bind&lt;/code&gt; 설정을 추가할 수 있습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;목표 동작&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Nano 바인딩 설정 (in .nanorc)&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;저장 (Vim의 :w)&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;bind ^S savefile main&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Ctrl+S로 즉시 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;종료 (Vim의 :q)&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;bind ^Q exit main&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Ctrl+Q로 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;b&gt;줄 삭제 (Vim의 dd)&lt;/b&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code&gt;bind ^D cut main&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Ctrl+D로 현재 줄 전체 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Nano 핵심 명령어 Cheat Sheet&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(표기: &lt;code&gt;^&lt;/code&gt; = Ctrl, &lt;code&gt;M-&lt;/code&gt; = Alt)&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✂️ 편집 및 검색 (가장 중요!)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;M-A&lt;/code&gt; (Mark)&lt;/b&gt;: &lt;b&gt;블록 선택 시작&lt;/b&gt; (Vim의 visual mode와 유사)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;^K&lt;/code&gt; / &lt;code&gt;^U&lt;/code&gt;&lt;/b&gt;: 잘라내기 / 붙여넣기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;M-U&lt;/code&gt; / &lt;code&gt;M-E&lt;/code&gt;&lt;/b&gt;: 실행 취소(Undo) / 재실행(Redo)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;^\&lt;/code&gt; (M-R)&lt;/b&gt;: &lt;b&gt;검색 및 치환 (Replace)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  파일 관리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;^O&lt;/code&gt;&lt;/b&gt;: 파일 저장 (파일명 확인 가능)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;^X&lt;/code&gt;&lt;/b&gt;: 편집기 종료&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;M-G&lt;/code&gt;&lt;/b&gt;: 특정 줄 번호로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실전 팁: 멀티 버퍼 활용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vim에서 &lt;code&gt;:tabnew&lt;/code&gt;를 쓰듯 Nano에서도 여러 파일을 동시에 열어두고 전환할 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;nano -F 파일1 파일2&lt;/code&gt; 처럼 여러 파일을 함께 엽니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;M-.&lt;/code&gt;&lt;/b&gt; (Alt + 마침표): 다음 파일로 전환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;M-,&lt;/code&gt;&lt;/b&gt; (Alt + 쉼표): 이전 파일로 전환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에디터 하나에 종속되지 않고 &lt;b&gt;Vim과 Nano를 자유자재로 오가는 유연함&lt;/b&gt;은 숙련된 개발자의 증거이기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가이드가 여러분의 터미널 작업을 조금 더 쾌적하게 만들어주길 바랍니다!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>⚠️ Iɴғr&amp;alpha;/KeyM&amp;alpha;p &amp;zwj; </category>
      <category>linux</category>
      <category>linux에디터</category>
      <category>linux에디터커스텀</category>
      <category>linux편집기</category>
      <category>nano</category>
      <category>nano단축키</category>
      <category>nano를vim</category>
      <category>nano에디터</category>
      <category>nano커스텀</category>
      <category>서버개발자</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/265</guid>
      <comments>https://yurizzy.tistory.com/265#entry265comment</comments>
      <pubDate>Fri, 24 Apr 2026 00:56:39 +0900</pubDate>
    </item>
    <item>
      <title>⚡️개발자가 자주 사용하는 VIM 명령어 모음 : 알아두면 쓸데있는 Vim 잡학지식(알쓸빔잡)</title>
      <link>https://yurizzy.tistory.com/264</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZUAC9/dJMcafTTlGT/UGge4k2mu9cPCqP1wOxPc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZUAC9/dJMcafTTlGT/UGge4k2mu9cPCqP1wOxPc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZUAC9/dJMcafTTlGT/UGge4k2mu9cPCqP1wOxPc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZUAC9%2FdJMcafTTlGT%2FUGge4k2mu9cPCqP1wOxPc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;559&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;559&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 모드 전환 (Mode Switching)&lt;/h2&gt;
&lt;table style=&quot;width: 636px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 192px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 444px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;i&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;커서 앞 삽입 모드 진입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;I&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;줄 맨 앞에서 삽입 모드 진입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;커서 뒤 삽입 모드 진입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;줄 맨 끝에서 삽입 모드 진입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;o&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;아래 줄 새로 추가 후 삽입 모드 진입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;O&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;위 줄 새로 추가 후 삽입 모드 진입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;v&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;비주얼 모드 (문자 단위 선택)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;V&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;비주얼 라인 모드 (줄 단위 선택)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;Ctrl+v&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;비주얼 블록 모드 (블록 단위 선택)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 192px;&quot;&gt;&lt;code&gt;Esc&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 444px;&quot;&gt;노멀 모드로 복귀&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 커서 이동 - 기본 (Basic Movement)&lt;/h2&gt;
&lt;table style=&quot;height: 414px;&quot; width=&quot;773&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;왼쪽으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;j&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;아래로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;위로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;오른쪽으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄 맨 앞으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄 첫 번째 비공백 문자로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;줄 맨 끝으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 맨 처음으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일 맨 끝으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{숫자}G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특정 줄 번호로 이동 (예: &lt;code&gt;10G&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 커서 이동 - 단어 (Word Movement)&lt;/h2&gt;
&lt;table style=&quot;height: 409px;&quot; width=&quot;775&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 212px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 557px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;w&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;다음 단어 첫 글자로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;W&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;공백 기준 다음 단어 첫 글자로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;b&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;이전 단어 첫 글자로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;B&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;공백 기준 이전 단어 첫 글자로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;e&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;현재/다음 단어 끝 글자로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;E&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;공백 기준 현재/다음 단어 끝 글자로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;ge&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;이전 단어 끝 글자로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;f{문자}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;현재 줄에서 해당 문자 위치로 이동 (앞 방향)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;F{문자}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;현재 줄에서 해당 문자 위치로 이동 (뒤 방향)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 212px;&quot;&gt;&lt;code&gt;t{문자}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 557px;&quot;&gt;현재 줄에서 해당 문자 바로 앞으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 커서 이동 - 화면/파일 (Screen/File Movement)&lt;/h2&gt;
&lt;table style=&quot;height: 396px;&quot; width=&quot;783&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;Ctrl+d&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;반 페이지 아래로 스크롤&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;Ctrl+u&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;반 페이지 위로 스크롤&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;Ctrl+f&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;한 페이지 아래로 스크롤&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;Ctrl+b&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;한 페이지 위로 스크롤&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;H&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;화면 맨 위 줄로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;M&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;화면 중간 줄로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;L&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;화면 맨 아래 줄로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;zz&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;현재 줄을 화면 중앙에 위치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;zt&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;현재 줄을 화면 맨 위에 위치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;code&gt;zb&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;현재 줄을 화면 맨 아래에 위치&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 삭제 (Delete)&lt;/h2&gt;
&lt;table style=&quot;height: 390px;&quot; width=&quot;786&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 문자 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서 앞 문자 삭제 (백스페이스)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 줄 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{숫자}dd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N줄 삭제 (예: &lt;code&gt;3dd&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 다음 단어 전까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 이전 단어 전까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 줄 맨 앞까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d$&lt;/code&gt; / &lt;code&gt;D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 줄 끝까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dgg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 파일 처음까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 파일 끝까지 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 복사 &amp;amp; 붙여넣기 (Yank &amp;amp; Paste)&lt;/h2&gt;
&lt;table style=&quot;height: 391px;&quot; width=&quot;778&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yy&lt;/code&gt; / &lt;code&gt;Y&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 줄 복사 (yank)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{숫자}yy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N줄 복사 (예: &lt;code&gt;3yy&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 단어 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;y$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 줄 끝까지 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;y0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서부터 줄 맨 앞까지 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서 아래/뒤에 붙여넣기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;P&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커서 위/앞에 붙여넣기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot;ayy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;레지스터 a에 현재 줄 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot;ap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;레지스터 a 내용 붙여넣기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&quot;+p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;시스템 클립보드에서 붙여넣기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 변경 (Change)&lt;/h2&gt;
&lt;table style=&quot;height: 401px;&quot; width=&quot;781&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 227px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 548px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;r{문자}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;현재 문자를 다른 문자로 교체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;R&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;교체 모드 진입 (타이핑한 내용으로 덮어쓰기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;cw&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;현재 단어 변경 (삭제 후 삽입 모드)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;cc&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;현재 줄 내용 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;c$&lt;/code&gt; / &lt;code&gt;C&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;커서부터 줄 끝까지 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;ci{&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;&lt;code&gt;{}&lt;/code&gt; 내부 내용 변경 (Change Inside)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;ci(&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;&lt;code&gt;()&lt;/code&gt; 내부 내용 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;ci&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;&lt;code&gt;&quot;&quot;&lt;/code&gt; 내부 내용 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;ca{&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;&lt;code&gt;{}&lt;/code&gt; 포함 내용 변경 (Change Around)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 227px;&quot;&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 548px;&quot;&gt;현재 문자 대/소문자 전환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실행 취소 &amp;amp; 반복 (Undo &amp;amp; Repeat)&lt;/h2&gt;
&lt;table style=&quot;height: 207px;&quot; width=&quot;775&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령어&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실행 취소 (undo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;재실행 (redo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;마지막 변경 명령 반복&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{숫자}.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;마지막 변경 명령 N번 반복&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;마지막 Ex 명령 반복&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 검색 (Search)&lt;/h2&gt;
&lt;table style=&quot;height: 311px;&quot; width=&quot;782&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 234px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 542px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 234px;&quot;&gt;&lt;code&gt;/{패턴}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 542px;&quot;&gt;아래 방향으로 패턴 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 234px;&quot;&gt;&lt;code&gt;?{패턴}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 542px;&quot;&gt;위 방향으로 패턴 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 234px;&quot;&gt;&lt;code&gt;n&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 542px;&quot;&gt;다음 검색 결과로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 234px;&quot;&gt;&lt;code&gt;N&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 542px;&quot;&gt;이전 검색 결과로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 234px;&quot;&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 542px;&quot;&gt;현재 단어 아래 방향으로 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 234px;&quot;&gt;&lt;code&gt;#&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 542px;&quot;&gt;현재 단어 위 방향으로 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 234px;&quot;&gt;&lt;code&gt;:noh&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 542px;&quot;&gt;검색 하이라이트 해제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 234px;&quot;&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 542px;&quot;&gt;매칭되는 괄호로 이동 (&lt;code&gt;{&lt;/code&gt;, &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;[&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 치환 (Substitute)&lt;/h2&gt;
&lt;table style=&quot;height: 233px;&quot; width=&quot;773&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 228px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 539px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 228px;&quot;&gt;&lt;code&gt;:s/old/new/&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 539px;&quot;&gt;현재 줄 첫 번째 old를 new로 치환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 228px;&quot;&gt;&lt;code&gt;:s/old/new/g&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 539px;&quot;&gt;현재 줄 전체 old를 new로 치환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 228px;&quot;&gt;&lt;code&gt;:%s/old/new/g&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 539px;&quot;&gt;파일 전체 old를 new로 치환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 228px;&quot;&gt;&lt;code&gt;:%s/old/new/gc&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 539px;&quot;&gt;파일 전체 치환 (각 항목 확인 요청)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 228px;&quot;&gt;&lt;code&gt;:%s/old/new/gi&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 539px;&quot;&gt;파일 전체 대소문자 무시하고 치환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 228px;&quot;&gt;&lt;code&gt;:{범위}s/old/new/g&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 539px;&quot;&gt;특정 줄 범위 치환 (예: &lt;code&gt;:1,10s/old/new/g&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 파일 &amp;amp; 버퍼 (File &amp;amp; Buffer)&lt;/h2&gt;
&lt;table style=&quot;width: 785px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 229px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 556px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:w&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;현재 파일 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:w {파일명}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;다른 이름으로 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:q&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:q!&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;저장하지 않고 강제 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:wq&lt;/code&gt; / &lt;code&gt;ZZ&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;저장 후 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:e {파일명}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;다른 파일 열기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:e!&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;현재 파일 다시 불러오기 (변경사항 버림)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:bn&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;다음 버퍼로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:bp&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;이전 버퍼로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 229px;&quot;&gt;&lt;code&gt;:ls&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 556px;&quot;&gt;열린 버퍼 목록 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 창 분할 (Window Split)&lt;/h2&gt;
&lt;table style=&quot;height: 281px;&quot; width=&quot;779&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 249px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 524px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;:sp&lt;/code&gt; / &lt;code&gt;Ctrl+w s&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 524px;&quot;&gt;수평 분할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;:vsp&lt;/code&gt; / &lt;code&gt;Ctrl+w v&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 524px;&quot;&gt;수직 분할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;Ctrl+w w&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 524px;&quot;&gt;다음 창으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;Ctrl+w h/j/k/l&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 524px;&quot;&gt;방향키로 창 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;Ctrl+w =&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 524px;&quot;&gt;모든 창 크기 균등하게 조정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;:close&lt;/code&gt; / &lt;code&gt;Ctrl+w c&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 524px;&quot;&gt;현재 창 닫기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;:only&lt;/code&gt; / &lt;code&gt;Ctrl+w o&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 524px;&quot;&gt;현재 창만 남기고 모두 닫기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. 탭 (Tab)&lt;/h2&gt;
&lt;table style=&quot;height: 192px;&quot; width=&quot;783&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 249px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 528px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;:tabnew {파일명}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot;&gt;새 탭에서 파일 열기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;gt&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot;&gt;다음 탭으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;gT&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot;&gt;이전 탭으로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;:tabclose&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot;&gt;현재 탭 닫기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 249px;&quot;&gt;&lt;code&gt;:tabs&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 528px;&quot;&gt;열린 탭 목록 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;14. 들여쓰기 &amp;amp; 포매팅 (Indent &amp;amp; Format)&lt;/h2&gt;
&lt;table style=&quot;height: 247px;&quot; width=&quot;779&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 246px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 527px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 246px;&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 527px;&quot;&gt;현재 줄 들여쓰기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 246px;&quot;&gt;&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 527px;&quot;&gt;현재 줄 내어쓰기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 246px;&quot;&gt;&lt;code&gt;{숫자}&amp;gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 527px;&quot;&gt;N줄 들여쓰기 (예: &lt;code&gt;3&amp;gt;&amp;gt;&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 246px;&quot;&gt;&lt;code&gt;=G&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 527px;&quot;&gt;커서부터 파일 끝까지 자동 들여쓰기 정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 246px;&quot;&gt;&lt;code&gt;gg=G&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 527px;&quot;&gt;파일 전체 자동 들여쓰기 정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 246px;&quot;&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; (비주얼)&lt;/td&gt;
&lt;td style=&quot;width: 527px;&quot;&gt;선택 영역 들여쓰기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 246px;&quot;&gt;&lt;code&gt;&amp;lt;&lt;/code&gt; (비주얼)&lt;/td&gt;
&lt;td style=&quot;width: 527px;&quot;&gt;선택 영역 내어쓰기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;15. 매크로 &amp;amp; 기타 (Macro &amp;amp; Others)&lt;/h2&gt;
&lt;table style=&quot;height: 379px;&quot; width=&quot;777&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;width: 253px;&quot;&gt;명령어&lt;/th&gt;
&lt;th style=&quot;width: 518px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;q{문자}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;매크로 녹화 시작 (예: &lt;code&gt;qa&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;q&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;매크로 녹화 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;@{문자}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;매크로 실행 (예: &lt;code&gt;@a&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;@@&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;마지막 매크로 재실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;{숫자}@{문자}&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;매크로 N번 반복 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;:set number&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;줄 번호 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;:set nonumber&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;줄 번호 숨기기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;:set hlsearch&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;검색 하이라이트 활성화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;Ctrl+a&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;현재 커서의 숫자 1 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 253px;&quot;&gt;&lt;code&gt;Ctrl+x&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;width: 518px;&quot;&gt;현재 커서의 숫자 1 감소&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팁&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;숫자 조합&lt;/b&gt;: 대부분의 명령어 앞에 숫자를 붙여 반복 실행 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;5j&lt;/code&gt; &amp;rarr; 5줄 아래 이동&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3dd&lt;/code&gt; &amp;rarr; 3줄 삭제&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2yy&lt;/code&gt; &amp;rarr; 2줄 복사&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;텍스트 오브젝트&lt;/b&gt;: &lt;code&gt;ci(&lt;/code&gt;, &lt;code&gt;da&quot;&lt;/code&gt;, &lt;code&gt;yi{&lt;/code&gt; 등 조합으로 강력한 편집 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt; = inner (내부만), &lt;code&gt;a&lt;/code&gt; = around (포함)&lt;/li&gt;
&lt;li&gt;대상: &lt;code&gt;w&lt;/code&gt;(단어), &lt;code&gt;s&lt;/code&gt;(문장), &lt;code&gt;p&lt;/code&gt;(단락), &lt;code&gt;&quot;&lt;/code&gt;, &lt;code&gt;'&lt;/code&gt;, &lt;code&gt;`&lt;/code&gt;, &lt;code&gt;(&lt;/code&gt;, &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;[&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;레지스터&lt;/b&gt;: &lt;code&gt;&quot;a&lt;/code&gt; ~ &lt;code&gt;&quot;z&lt;/code&gt; 26개의 레지스터로 여러 내용 관리 가능&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>⚠️ Iɴғr&amp;alpha;/KeyM&amp;alpha;p &amp;zwj; </category>
      <category>CLI</category>
      <category>keymap</category>
      <category>vim</category>
      <category>vimkeymap</category>
      <category>Vim단축키</category>
      <category>vim명령어</category>
      <category>vim사용법</category>
      <category>vim조합</category>
      <category>vim텍스트</category>
      <category>단축키</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/264</guid>
      <comments>https://yurizzy.tistory.com/264#entry264comment</comments>
      <pubDate>Thu, 23 Apr 2026 03:14:30 +0900</pubDate>
    </item>
    <item>
      <title> ️ 실무 : StrictHttpFirewall이 동작하는 원리 (feat. PROPFIND, WebDAV) | PROPFIND가 뭐예요 ?</title>
      <link>https://yurizzy.tistory.com/263</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JPnPE/dJMcaaZiCUL/KK3uikeKmgF12psGdIdw5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JPnPE/dJMcaaZiCUL/KK3uikeKmgF12psGdIdw5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JPnPE/dJMcaaZiCUL/KK3uikeKmgF12psGdIdw5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJPnPE%2FdJMcaaZiCUL%2FKK3uikeKmgF12psGdIdw5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;279&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;작년 10월 말부터 진행했던 고도화 프로젝트가 개발 서버에서 통합 QA를 진행하고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 우리 프로젝트는 네이티브 개발자 분들이 WebView 사용과 API호출 등 연결을 해주고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 개발서버의 QA중인 고도화 서비스는 네이티브 개발자 분들 , 그리고 관계사 관리자 분들이 요즘 사용해 주고 있어서&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;매일 개발서버 로그를 검수하며 문제는 없는 지 체크를 해보고 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;오늘도 로그를 보면서 Exception 걸린 부분이 있는지 검토하고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그러다 &lt;code&gt;PROPFIND&lt;/code&gt; 라는 Exception 문구가 있는 게 아닌가?&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이건 또 뭐다냐 &amp;nbsp; 하면서 구글에 검색해 보았다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;내용을 알게 되고 서비스 운영 하는 개발자 분들이 알고 있으면 좋은 내용인 것 같아 오늘도 포스팅을 써본다 &amp;zwj;♀️&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;주제 정의&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security의 &lt;code&gt;StrictHttpFirewall&lt;/code&gt;이 비표준 HTTP 메서드를 차단하는 원리와, 폐쇄망 서버에서 &lt;code&gt;PROPFIND&lt;/code&gt; 요청이 간헐적으로 발생하는 이유를 분석한다.&lt;br /&gt;단순히 에러를 끄는 방법이 아니라, &lt;b&gt;왜 이 에러가 발생하는지&lt;/b&gt;, &lt;b&gt;Spring Security 내부에서 어떤 흐름으로 차단이 일어나는지&lt;/b&gt;를 중심으로 정리한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UkRAQ/dJMcah5dyhg/ZwLcP5sCMV1hnTlLKmz8m1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UkRAQ/dJMcah5dyhg/ZwLcP5sCMV1hnTlLKmz8m1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UkRAQ/dJMcah5dyhg/ZwLcP5sCMV1hnTlLKmz8m1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUkRAQ%2FdJMcah5dyhg%2FZwLcP5sCMV1hnTlLKmz8m1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1406&quot; height=&quot;762&quot; data-origin-width=&quot;1406&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;원전 / 공식 스펙 요약&lt;/h2&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;HttpFirewall 인터페이스&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security는 &lt;code&gt;HttpFirewall&lt;/code&gt;이라는 인터페이스를 통해 요청 유효성 검사를 추상화한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public interface HttpFirewall {
    FirewalledRequest getFirewalledRequest(HttpServletRequest request)
        throws RequestRejectedException;

    HttpServletResponse getFirewalledResponse(HttpServletResponse response);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;구현체는 두 가지다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구현체&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DefaultHttpFirewall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최소한의 검사만 수행. 레거시 호환용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StrictHttpFirewall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spring Security 4.2.4+부터 기본값. 엄격한 검사 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/exploits/firewall.html&quot;&gt;  공식 문서 - HttpFirewall&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/exploits/firewall.html&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;StrictHttpFirewall의 검사 항목&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StrictHttpFirewall&lt;/code&gt;은 요청이 필터 체인에 진입하기 전, 아래 항목을 순서대로 검사한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;1014&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbu78Y/dJMcaciwVCQ/xycExLTLKISwv0dWa4udkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbu78Y/dJMcaciwVCQ/xycExLTLKISwv0dWa4udkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbu78Y/dJMcaciwVCQ/xycExLTLKISwv0dWa4udkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcbu78Y%2FdJMcaciwVCQ%2FxycExLTLKISwv0dWa4udkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;1014&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;1014&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. HTTP 메서드 검사&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;허용 목록에 없는 메서드는 즉시 차단한다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;// StrictHttpFirewall 내부 기본 허용 목록
private static final Set&amp;lt;String&amp;gt; ALLOW_ANY_HTTP_METHOD = Collections.unmodifiableSet(
    new HashSet&amp;lt;&amp;gt;(Arrays.asList(
        &quot;DELETE&quot;, &quot;GET&quot;, &quot;HEAD&quot;, &quot;OPTIONS&quot;, &quot;PATCH&quot;, &quot;POST&quot;, &quot;PUT&quot;
    ))
);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PROPFIND&lt;/code&gt;, &lt;code&gt;MKCOL&lt;/code&gt;, &lt;code&gt;COPY&lt;/code&gt; 등 WebDAV 전용 메서드는 이 목록에 포함되지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. URL 정규화 검사&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;비정규화된 경로를 차단한다. 디렉터리 트래버설 공격 방지가 목적이다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;/normal/path       &amp;rarr; 허용
/path/../secret    &amp;rarr; 차단 (Path Traversal)
//double/slash     &amp;rarr; 차단
/path/%2F/encoded  &amp;rarr; 차단 (URL 인코딩 우회 시도)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 헤더 / 파라미터 CR&amp;middot;LF 검사&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP Response Splitting 공격을 방지하기 위해 헤더와 파라미터 값에 &lt;code&gt;\r&lt;/code&gt;, &lt;code&gt;\n&lt;/code&gt;이 포함되면 차단한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 호스트 헤더 검사&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;허용된 호스트 목록을 설정한 경우, 목록 외 호스트로의 요청을 차단한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/firewall/StrictHttpFirewall.html&quot;&gt;  StrictHttpFirewall JavaDoc&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/firewall/StrictHttpFirewall.html&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;PROPFIND와 WebDAV&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;code&gt;PROPFIND&lt;/code&gt;는 HTTP/1.1을 확장한 &lt;b&gt;WebDAV 프로토콜&lt;/b&gt;에서 정의하는 메서드다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;WebDAV는 웹 서버를 파일 시스템처럼 다룰 수 있게 하는 프로토콜로&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP를 기반으로 파일 생성&amp;middot;수정&amp;middot;삭제&amp;middot;이동이 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;WebDAV가 추가한 주요 메서드는 아래와 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메서드&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROPFIND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;리소스의 속성(메타데이터) 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROPPATCH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;리소스 속성 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MKCOL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;컬렉션(디렉터리) 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COPY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;리소스 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MOVE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;리소스 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOCK&lt;/code&gt; / &lt;code&gt;UNLOCK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;리소스 잠금&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 중 &lt;code&gt;PROPFIND&lt;/code&gt;는 클라이언트가 서버에 접근할 때 &lt;b&gt;가장 먼저 보내는 탐색 요청&lt;/b&gt;이기 때문에&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;WebDAV를 사용하는 클라이언트라면 반드시 발생한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc4918&quot;&gt;  공식 스펙 - RFC 4918 (WebDAV)&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;실무 해석 및 분석&lt;/h2&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;요청 차단 흐름 상세 분석&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;실제 에러 스택을 기준으로 흐름을 따라가 본다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;at StrictHttpFirewall.rejectForbiddenHttpMethod(StrictHttpFirewall.java:531)
at StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:508)
at FilterChainProxy.doFilterInternal(FilterChainProxy.java:211)
at FilterChainProxy.doFilter(FilterChainProxy.java:191)
at CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
at HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;흐름을 단계별로 정리하면 다음과 같다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;HTTP 요청 수신
      &amp;darr;
FilterChainProxy.doFilter()
      &amp;darr;
FilterChainProxy.doFilterInternal()
      &amp;darr;
StrictHttpFirewall.getFirewalledRequest()   &amp;larr; 여기서 검사 시작
      &amp;darr;
rejectForbiddenHttpMethod()                 &amp;larr; PROPFIND 감지
      &amp;darr;
RequestRejectedException 발생
      &amp;darr;
클라이언트 &amp;rarr; 400 Bad Request 응답&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vZIRv/dJMcagL00gz/mvuIb5NKJgt83KpYryZdu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vZIRv/dJMcagL00gz/mvuIb5NKJgt83KpYryZdu1/img.png&quot; style=&quot;width: 31.4471%; margin-right: 10px;&quot; data-widthpercent=&quot;32.2&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;770&quot; data-origin-width=&quot;500&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vZIRv/dJMcagL00gz/mvuIb5NKJgt83KpYryZdu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvZIRv%2FdJMcagL00gz%2FmvuIb5NKJgt83KpYryZdu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Sfl4K/dJMcag6iFUu/QO8VGuAGAWa79jwNkCT7PK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Sfl4K/dJMcag6iFUu/QO8VGuAGAWa79jwNkCT7PK/img.png&quot; style=&quot;width: 23.1798%; margin-right: 10px;&quot; data-widthpercent=&quot;23.73&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;1147&quot; data-origin-width=&quot;549&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Sfl4K/dJMcag6iFUu/QO8VGuAGAWa79jwNkCT7PK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSfl4K%2FdJMcag6iFUu%2FQO8VGuAGAWa79jwNkCT7PK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;549&quot; height=&quot;1147&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LYtDS/dJMcaiC1Evq/YgVmzXAagMItdfon1FkmlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LYtDS/dJMcaiC1Evq/YgVmzXAagMItdfon1FkmlK/img.png&quot; style=&quot;width: 43.0475%;&quot; data-widthpercent=&quot;44.07&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;594&quot; data-origin-width=&quot;528&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LYtDS/dJMcaiC1Evq/YgVmzXAagMItdfon1FkmlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLYtDS%2FdJMcaiC1Evq%2FYgVmzXAagMItdfon1FkmlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;528&quot; height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;mermaid를 사용중이 신 분은&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아래 소스를 복사하여 사용하시면 흐름도를 깔끔하게 볼 수 있습니다.❤️&lt;/p&gt;
&lt;pre id=&quot;code_1776704481063&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;```mermaid
flowchart TD
    A([  HTTP 요청 수신]) --&amp;gt; B[FilterChainProxy.doFilter]
    B --&amp;gt; C[FilterChainProxy.doFilterInternal]
    C --&amp;gt; D[StrictHttpFirewall.getFirewalledRequest]

    D --&amp;gt; E{HTTP 메서드\n허용 목록 검사}

    E -- &quot;❌ 비허용 메서드\nPROPFIND 등&quot; --&amp;gt; F[rejectForbiddenHttpMethod]
    F --&amp;gt; G[RequestRejectedException 발생]
    G --&amp;gt; H([  400 Bad Request 응답])

    E -- &quot;✅ 허용 메서드\nGET / POST 등&quot; --&amp;gt; I{URL 정규화\n검사}

    I -- &quot;❌ 비정규화 경로\n/../ // 등&quot; --&amp;gt; G
    I -- &quot;✅ 정상 경로&quot; --&amp;gt; J{헤더&amp;middot;파라미터\nCR&amp;middot;LF 검사}

    J -- &quot;❌ CR&amp;middot;LF 포함&quot; --&amp;gt; G
    J -- &quot;✅ 정상&quot; --&amp;gt; K{호스트 헤더\n검사}

    K -- &quot;❌ 비허용 호스트&quot; --&amp;gt; G
    K -- &quot;✅ 정상&quot; --&amp;gt; L[FilterChain 진입]

    L --&amp;gt; M[Security Filter들 순차 실행]
    M --&amp;gt; N[DispatcherServlet]
    N --&amp;gt; O[HandlerMapping &amp;rarr; Controller]
    O --&amp;gt; P([  정상 응답])

    style A fill:#4A90D9,color:#fff,stroke:none
    style H fill:#E05C5C,color:#fff,stroke:none
    style P fill:#5BAD72,color:#fff,stroke:none
    style F fill:#E8A838,color:#fff,stroke:none
    style G fill:#E05C5C,color:#fff,stroke:none
```&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;컨트롤러는커녕 서블릿 필터 체인에도 진입하지 못한 채 차단&lt;/b&gt;된다는 점이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DispatcherServlet&lt;/code&gt;, &lt;code&gt;HandlerMapping&lt;/code&gt;, &lt;code&gt;@RequestMapping&lt;/code&gt; 어디에도 도달하지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;왜 폐쇄망 서버에서 간헐적으로 발생하는가?&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;외부 인터넷이 차단된 폐쇄망이라도, 내부 네트워크에는 WebDAV를 사용하는 클라이언트가 다수 존재한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;간헐적&lt;/b&gt;으로 발생하는 이유는, 이 클라이언트들이 사용자의 명시적 행동 없이 자동으로 요청을 보내기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyvEsl/dJMcacW62ih/J3iAsJ2QNioPc3LMPl28Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyvEsl/dJMcacW62ih/J3iAsJ2QNioPc3LMPl28Yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyvEsl/dJMcacW62ih/J3iAsJ2QNioPc3LMPl28Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyvEsl%2FdJMcacW62ih%2FJ3iAsJ2QNioPc3LMPl28Yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1570&quot; height=&quot;348&quot; data-origin-width=&quot;1570&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;발생 주체&lt;/th&gt;
&lt;th&gt;발생 조건&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Windows 탐색기&lt;/td&gt;
&lt;td&gt;네트워크 드라이브 연결 시&lt;/td&gt;
&lt;td&gt;서버 URL을 탐색기에 입력하면 자동으로 &lt;code&gt;PROPFIND&lt;/code&gt; 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Office&lt;/td&gt;
&lt;td&gt;파일 열기/저장 경로에 서버 URL 사용 시&lt;/td&gt;
&lt;td&gt;Word, Excel이 서버를 WebDAV 스토리지로 인식하고 탐색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;보안 장비 (IDS/IPS)&lt;/td&gt;
&lt;td&gt;주기적 헬스체크 / 포트 스캐닝 시&lt;/td&gt;
&lt;td&gt;비표준 메서드로 서버 응답 유형을 판별&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내부 모니터링 도구&lt;/td&gt;
&lt;td&gt;서버 상태 점검 시&lt;/td&gt;
&lt;td&gt;일부 도구가 WebDAV 프로토콜 기반 점검을 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개발자 로컬 환경&lt;/td&gt;
&lt;td&gt;IDE나 REST 클라이언트 설정 오류 시&lt;/td&gt;
&lt;td&gt;IntelliJ HTTP Client, Postman 설정 실수로 발생하기도 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBU3Q1/dJMcafTRCag/gsv2im82lbjHd9rruoCS9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBU3Q1/dJMcafTRCag/gsv2im82lbjHd9rruoCS9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBU3Q1/dJMcafTRCag/gsv2im82lbjHd9rruoCS9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBU3Q1%2FdJMcafTRCag%2Fgsv2im82lbjHd9rruoCS9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;712&quot; height=&quot;479&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;이 로그는 위험한가?&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론부터 말하면, 위험하지 않다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StrictHttpFirewall&lt;/code&gt;이 설계된 대로 동작하고 있다는 증거다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;서버는 허용하지 않은 메서드를 정상적으로 거부했고, 실제 비즈니스 로직에는 전혀 영향을 주지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다만 두 가지 상황에서는 대응이 필요하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그가 과도하게 쌓여 실제 장애 로그를 가리는 경우&lt;/li&gt;
&lt;li&gt;내부 서비스가 WebDAV를 실제로 사용해야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;DefaultHttpFirewall과의 차이&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;레거시 프로젝트에서 &lt;code&gt;DefaultHttpFirewall&lt;/code&gt;을 사용 중이라면 이 에러가 발생하지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DefaultHttpFirewall&lt;/code&gt;은 메서드 허용 목록 검사를 수행하지 않기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 3, Spring Security 6으로 마이그레이션하는 과정에서&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StrictHttpFirewall&lt;/code&gt;이 기본값으로 바뀌면서 이런 로그가 새로 등장하는 경우가 많다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;고도화 프로젝트 진행 중이라면 이 지점을 눈여겨볼 필요가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2301&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SH4PJ/dJMcafzzaVZ/0FcxItB1YXjwjbPMrXN7g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SH4PJ/dJMcafzzaVZ/0FcxItB1YXjwjbPMrXN7g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SH4PJ/dJMcafzzaVZ/0FcxItB1YXjwjbPMrXN7g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSH4PJ%2FdJMcafzzaVZ%2F0FcxItB1YXjwjbPMrXN7g0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2301&quot; height=&quot;253&quot; data-origin-width=&quot;2301&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;결론 및 실무 팁&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;상황에 따라 세 가지 기준으로 대응을 선택한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;1089&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0k8wh/dJMcahD6R5e/rfa0f4H4AohwzqMhxjrjbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0k8wh/dJMcahD6R5e/rfa0f4H4AohwzqMhxjrjbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0k8wh/dJMcahD6R5e/rfa0f4H4AohwzqMhxjrjbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0k8wh%2FdJMcahD6R5e%2Frfa0f4H4AohwzqMhxjrjbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;942&quot; height=&quot;1089&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;1089&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Case 1. WebDAV가 전혀 필요 없고, 로그 양도 감당 가능한 경우 &amp;rarr; 방치&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;보안 측면에서 정상 동작 중이다. 별도 조치 없이 모니터링만 유지한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Case 2. 로그만 억제하고 싶은 경우 &amp;rarr; &lt;code&gt;RequestRejectedHandler&lt;/code&gt; 등록&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Spring Security 5.7부터 &lt;code&gt;RequestRejectedHandler&lt;/code&gt;를 빈으로 등록하면 처리 방식을 커스텀할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;기본 구현체인 &lt;code&gt;HttpStatusRequestRejectedHandler&lt;/code&gt;는 400 응답만 내리고 스택 트레이스를 남기지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class SecurityConfig {

    /**
     * RequestRejectedException 발생 시 스택 트레이스 없이 400만 응답한다.
     * Spring Security 5.7+ 기준
     */
    @Bean
    public RequestRejectedHandler requestRejectedHandler() {
        return new HttpStatusRequestRejectedHandler();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;로그 레벨만 조정하는 방법도 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# application.properties
logging.level.org.springframework.security.web.firewall=ERROR&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;단, 이 방법은 다른 방화벽 관련 경고도 함께 억제하므로 주의한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Case 3. 내부 서비스가 WebDAV를 실제로 사용하는 경우 &amp;rarr; 허용 메서드 명시적 추가&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;StrictHttpFirewall&lt;/code&gt;에 허용 메서드를 추가하고, &lt;code&gt;WebSecurityCustomizer&lt;/code&gt;에 등록한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;@Configuration
public class SecurityConfig {

    /**
     * PROPFIND 메서드를 허용 목록에 추가한다.
     * WebDAV를 실제로 사용하는 경우에만 적용한다.
     */
    @Bean
    public HttpFirewall allowWebDavFirewall() {
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        firewall.setAllowedHttpMethods(
            Arrays.asList(
                &quot;HEAD&quot;, &quot;DELETE&quot;, &quot;POST&quot;, &quot;GET&quot;,
                &quot;OPTIONS&quot;, &quot;PATCH&quot;, &quot;PUT&quot;, &quot;PROPFIND&quot;
            )
        );
        return firewall;
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -&amp;gt; web.httpFirewall(allowWebDavFirewall());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;허용 메서드를 넓히면 공격 표면이 넓어진다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PROPFIND&lt;/code&gt; 요청의 출처를 먼저 파악하고, 실제로 필요한 경우에만 적용한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  참고 링크&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/exploits/firewall.html&quot;&gt;Spring Security 공식 문서 - HTTP Firewall&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/firewall/StrictHttpFirewall.html&quot;&gt;StrictHttpFirewall JavaDoc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc4918&quot;&gt;RFC 4918 - WebDAV 스펙&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-security/blob/main/web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java&quot;&gt;Spring Security GitHub - StrictHttpFirewall.java&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category> ️ DevOpѕ</category>
      <category>HTTP Firewall</category>
      <category>http보안</category>
      <category>PROPFIND</category>
      <category>RFC</category>
      <category>security</category>
      <category>spring</category>
      <category>StricHttpFirewall</category>
      <category>WebDAV</category>
      <category>서버개발</category>
      <category>정보보안</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/263</guid>
      <comments>https://yurizzy.tistory.com/263#entry263comment</comments>
      <pubDate>Tue, 21 Apr 2026 02:05:36 +0900</pubDate>
    </item>
    <item>
      <title> &amp;zwj;  Springframwork Mig 기록 : OKTA SAML 로그인 무한 루프 디버깅기 (feat. 백엔드&amp;middot;프론트 환장의 콜라보)</title>
      <link>https://yurizzy.tistory.com/262</link>
      <description>&lt;h2 data-heading=&quot;INTRO&quot; data-ke-size=&quot;size26&quot;&gt;INTRO&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework 4에서 Spring Boot 3으로 마이그레이션하는 고도화 프로젝트 중&lt;br /&gt;OKTA SAML 인증 연동을 붙이고 테스트 서버에 배포했다.&lt;br /&gt;설레는 마음으로 로그인을 시도했는데, 화면이 계속 새로고침되며 멈추지 않았다.&lt;br /&gt;처음에는 &quot;배포를 잘못했나?&quot; 싶어서 서버를 재시작해봤고, 로그를 다시 확인해봤다. 그런데 아무리 봐도 배포 자체는 정상이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fiddler 네트워크 로그를 열어서 트랜잭션 흐름을 직접 추적해보고 나서야 원인이 보였다.&lt;br /&gt;백엔드에서 터진 HTTP 500 에러와, 그 에러 상황을 전혀 고려하지 않은 프론트엔드 리다이렉트 로직이 서로 맞물려 만들어낸&lt;br /&gt;그야말로 &lt;b&gt;환장의 콜라보&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 백엔드 문제라고 확신했는데, 백엔드를 고쳐도 루프는 계속됐다.&lt;br /&gt;두 레이어를 동시에 수정하고 나서야 루프를 끊을 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mT5ld/dJMb99MNkn3/s13CgKFXKb1IASQ5QhMf2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mT5ld/dJMb99MNkn3/s13CgKFXKb1IASQ5QhMf2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mT5ld/dJMb99MNkn3/s13CgKFXKb1IASQ5QhMf2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmT5ld%2FdJMb99MNkn3%2Fs13CgKFXKb1IASQ5QhMf2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;521&quot; height=&quot;344&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-heading=&quot;문제 상황&quot; data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 Fiddler 로그를 펼쳐놨을 때는 솔직히 어디서부터 봐야 할지 막막했다.&lt;br /&gt;요청이 수십 개 쌓여 있었고, 다 비슷하게 생긴 리다이렉트가 반복되고 있었다.&lt;br /&gt;하나씩 따라가다 보니 아래처럼 5단계가 계속 순환하는 패턴이 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FzEV2/dJMcacW4rzJ/V44bF9SFCy1bczX5o5zusk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FzEV2/dJMcacW4rzJ/V44bF9SFCy1bczX5o5zusk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FzEV2/dJMcacW4rzJ/V44bF9SFCy1bczX5o5zusk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFzEV2%2FdJMcacW4rzJ%2FV44bF9SFCy1bczX5o5zusk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;517&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;517&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;무한 루프 흐름도&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로그인 페이지 진입 &amp;rarr; 로그인 페이지 JS가 &lt;b&gt;무조건&lt;/b&gt; OKTA SAML 인증 URL로 리다이렉트&lt;/li&gt;
&lt;li&gt;OKTA 서버에서 아이디/비밀번호 인증 성공 &amp;rarr; SAML 콜백 엔드포인트로 POST&lt;/li&gt;
&lt;li&gt;LoginController가 콜백을 수신해 처리하던 중 &lt;b&gt;HTTP 500 에러 발생&lt;/b&gt; (SAML 파싱 오류)&lt;/li&gt;
&lt;li&gt;Tomcat 에러 페이지가 렌더링되고, 해당 에러 페이지에 포함된 &lt;b&gt;공통 스크립트&lt;/b&gt;가 로드되면서 인덱스 페이지로 302 리다이렉트&lt;/li&gt;
&lt;li&gt;인덱스 페이지는 세션이 없으므로 다시 로그인 페이지로 이동 &amp;rarr; 1번으로 복귀&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 정리하고 나서 처음 든 생각은 &quot;3번에서 500이 나는 게 문제다. 저걸 잡으면 되겠다&quot;였다.&lt;br /&gt;백엔드 개발자니까 서버 에러부터 고치면 된다는 생각이 자연스럽게 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 그게 아니었다. &lt;b&gt;1번 단계&lt;/b&gt;의 자바스크립트는 에러 여부를 따지지 않고 무조건 OKTA로 튕겨냈다.&lt;br /&gt;백엔드에서 500이 나든 302가 나든, 결국 다시 로그인 페이지로 돌아오면 JS가 또 OKTA로 보내버리는 구조였다.&lt;br /&gt;백엔드 에러를 아무리 잡아도 프론트엔드가 루프를 계속 만들고 있었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더불어, 기존 코드를 보다가 예외 처리 리다이렉트 경로가 이렇게 되어 있다는 것도 눈에 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 기존 코드: .jsp 확장자를 직접 지정
return &quot;redirect:/login.jsp?error=true&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC를 공부하면서 ViewResolver가 뷰 이름을 실제 경로로 변환해준다는 건 알고 있었는데&lt;br /&gt;이렇게 직접 .jsp로 리다이렉트하면 ViewResolver를 아예 우회한다는 것은 그때 처음 제대로 인식했다.&lt;br /&gt;Model에 세팅해야 할 기본값들이 전부 날아가버려서 빈 화면이나 404가 뜰 수 있는 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-heading=&quot;1번째 시도: 백엔드 try-catch만 적용 (실패)&quot; data-ke-size=&quot;size26&quot;&gt;1번째 시도: 백엔드 try-catch만 적용 (실패)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;505&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccadGS/dJMcabKGg3r/o658MQbh8DKZmK0BQCV7QK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccadGS/dJMcabKGg3r/o658MQbh8DKZmK0BQCV7QK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccadGS/dJMcabKGg3r/o658MQbh8DKZmK0BQCV7QK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccadGS%2FdJMcabKGg3r%2Fo658MQbh8DKZmK0BQCV7QK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;505&quot; height=&quot;396&quot; data-origin-width=&quot;505&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시도한 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;서버에서 500이 안 나면, 에러 페이지가 렌더링되지 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 페이지 안에 있는 공통 스크립트도 실행되지 않을 것이다.&lt;br /&gt;그러면 루프가 끊기지 않을까?&quot; 라는 논리였다. 콜백 핸들러에 try-catch를 감싸서 예외를 잡고&lt;br /&gt;에러 코드를 파라미터로 담아 명시적으로 로그인 페이지로 리다이렉트했다.&lt;br /&gt;백엔드 개발자로서 할 수 있는 가장 직관적인 대응이었고&lt;br /&gt;이 정도면 해결되지 않을까 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적용 방법&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/saml-callback&quot;)
public String samlCallback(HttpServletRequest request, Model model) {
    try {
        // SAML Assertion 파싱 및 사용자 검증 로직
        return &quot;redirect:/main&quot;;
    } catch (Exception e) {
        logger.error(&quot;[OKTA_ERROR] SAML 인증 콜백 처리 중 오류 발생:&quot;, e);
        return &quot;redirect:/login?error=SAML_PARSE_FAILED&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점/한계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포하고 다시 테스트했는데 루프는 그대로였다. 로그를 보니 이번엔 500 대신 302가 찍혔는데&lt;br /&gt;결국 로그인 페이지로 이동하자마자 JS가 다시 OKTA로 튕겨버렸다.&lt;br /&gt;&quot;분명히 에러를 잡았는데 왜 여전히 루프가 도는 거지?&quot;라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 다시 Fiddler 로그를 열어서 흐름을 처음부터 다시 봤다.&lt;br /&gt;서버는 분명 302를 잘 보내고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &lt;b&gt;로그인 페이지 JS가 error 파라미터가 있는지 없는지 확인 자체를 하지 않는다&lt;/b&gt;는 것이었다.&lt;br /&gt;에러가 붙어서 돌아오든, 정상으로 돌아오든 무조건 OKTA로 리다이렉트하는 코드가 거기 있었다.&lt;br /&gt;백엔드를 아무리 고쳐봤자 소용없는 구조였다. 이 순간 &quot;아, 이건 내가 담당하는 레이어만의 문제가 아니구나&quot;를 체감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배운 점: 서버에서 에러를 우아하게 처리하는 것과, 사용자 흐름의 루프를 실제로 차단하는 것은 다른 문제다.&lt;br /&gt;백엔드만 고치면 된다는 생각이 얼마나 단편적이었는지 느꼈다.&lt;br /&gt;버그를 분석할 때는 내가 담당하는 레이어가 아니라, 요청이 흘러가는 전체 경로를 먼저 그려봐야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-heading=&quot;최종 해결: 백엔드 예외 처리 + 프론트엔드 루프 차단 (2중 방어)&quot; data-ke-size=&quot;size26&quot;&gt;최종 해결: 백엔드 예외 처리 + 프론트엔드 루프 차단 (2중 방어)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;739&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wM6rD/dJMcajhy4hT/TkyUUzuToh0nnQIP8Q0pWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wM6rD/dJMcajhy4hT/TkyUUzuToh0nnQIP8Q0pWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wM6rD/dJMcajhy4hT/TkyUUzuToh0nnQIP8Q0pWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwM6rD%2FdJMcajhy4hT%2FTkyUUzuToh0nnQIP8Q0pWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1298&quot; height=&quot;739&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;739&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루프를 끊으려면 두 레이어를 반드시 함께 수정해야 한다는 것을 파악했다.&lt;br /&gt;그런데 막상 수정 방향을 잡고 나니 또 다른 걱정이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;내가 고치려는 방식이 혹시 다른 곳을 망가뜨리지 않을까?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;신입으로서 가장 무서운 순간이 이런 때다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 공통 스크립트나 에러 처리 로직은 전체 시스템에 영향을 줄 수 있어서 섣불리 건드리기가 꺼려졌다.&lt;br /&gt;그래서 수정 전에 예상되는 사이드 이펙트를 먼저 정리해보고, 각각에 대한 대응책을 같이 준비했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;해결책 1: 백엔드 예외 방어 (에러 은폐 방지 포함)&quot; data-ke-size=&quot;size23&quot;&gt;해결책 1: 백엔드 예외 방어 (에러 은폐 방지 포함)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 처리 중 예외가 발생하면 에러 코드를 담아 로그인 페이지로 리다이렉트한다.&lt;br /&gt;그런데 여기서 처음에는 단순하게 catch로 잡고 302로 보내면 된다고 생각했다.&lt;br /&gt;그러다가 &quot;이렇게 하면 운영 중에 에러가 나도 500 알람이 안 뜨는 거 아닌가?&quot; 하는 의문이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 그랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try-catch로 모든 예외를 잡아버리면 APM 모니터링 도구 입장에서는 정상 응답처럼 보인다.&lt;br /&gt;운영 중 SAML 파싱이 계속 실패하고 있어도, 담당자가 아무도 인지를 못하게 되는 것이다.&lt;br /&gt;화면을 깔끔하게 처리하려다가 오히려 장애 대응 체계를 무력화할 뻔했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그래서 화면 흐름은 302로 처리하되, 서버 로그에는 반드시 전체 스택 트레이스를 남기는 방식으로 보완했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// LoginController.java
@PostMapping(&quot;/samlCallback&quot;)
public String samlCallback(HttpServletRequest request, Model model) {
    try {
        // SAML Assertion 파싱 및 사용자 검증 로직
        return &quot;redirect:/main&quot;;
    } catch (Exception e) {
        // [핵심] 화면 흐름은 302로 제어하되, 서버 로그에는 스택 트레이스를 반드시 남긴다
        // &amp;rarr; APM 모니터링 체계를 무력화하지 않기 위한 방어 코딩
        logger.error(&quot;[OKTA_ERROR] SAML 인증 콜백 처리 중 치명적 오류 발생:&quot;, e);
        return &quot;redirect:/login?error=SAML_PARSE_FAILED&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 .jsp 직접 리다이렉트도 이 기회에 함께 수정했다.&lt;br /&gt;앞서 문제 상황에서 발견했던 부분인데, 단독으로는 루프와 직접 연결된 버그는 아니었지만&lt;br /&gt;언제든 빈 화면이 뜰 수 있는 잠재적인 결함이었다.&lt;br /&gt;발견한 김에 같이 고치는 게 맞다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// AS-IS: ViewResolver를 우회, Model 데이터 누락 위험
return &quot;redirect:/login.jsp?error=true&quot;;

// TO-BE: ViewResolver를 정상 경유
return &quot;redirect:/login?error=true&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-heading=&quot;해결책 2: 프론트엔드 루프 차단 (화이트 스크린 방지 포함)&quot; data-ke-size=&quot;size23&quot;&gt;해결책 2: 프론트엔드 루프 차단 (화이트 스크린 방지 포함)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 페이지 JSP에서 OKTA 자동 리다이렉트를 실행하기 전&lt;br /&gt;URL에 error 파라미터가 있는지 먼저 확인하도록 분기를 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그런데 여기서도 단순하게 &quot;에러 있으면 리다이렉트 스크립트 멈추면 되겠네&quot; 라고 생각했다가&lt;br /&gt;한 가지를 놓쳤다는 걸 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OKTA 전용 로그인 페이지에는 ID/PW 입력 폼 자체가 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그러니 리다이렉트 스크립트만 멈춰버리면 사용자는 에러 알림을 확인하고 나서&lt;br /&gt;아무것도 없는 &lt;b&gt;빈 화면에 갇혀버리게&lt;/b&gt; 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3A2w0/dJMcaadUHEa/sQuf9iboMGk2YqybEpNVg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3A2w0/dJMcaadUHEa/sQuf9iboMGk2YqybEpNVg1/img.png&quot; data-alt=&quot;사용자 반응 예상&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3A2w0/dJMcaadUHEa/sQuf9iboMGk2YqybEpNVg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3A2w0%2FdJMcaadUHEa%2FsQuf9iboMGk2YqybEpNVg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;869&quot; height=&quot;645&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사용자 반응 예상&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 입장에서는 &quot;내가 뭘 해야 하지?&quot; 가 되는 상황이다.&lt;br /&gt;기능 구현에만 집중하다 보면 이런 UX적인 부분을 놓치기 쉬운데&lt;br /&gt;수정하기 전에 직접 시나리오를 따라가 본 게 도움이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 발생 시 안내 팝업을 띄운 뒤&lt;br /&gt;지정된 서비스 안내 페이지로 Fallback 처리하는 방식으로 대응했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// login.jsp: OKTA 자동 리다이렉트 스크립트 (수정 후)
$(document).ready(function() {
    var errorParam = new URLSearchParams(window.location.search).get('error');

    if (errorParam) {
        // [핵심] 루프를 멈추되, 사용자가 빈 화면에 갇히지 않도록 Fallback 처리
        alert(&quot;로그인 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.&quot;);
        location.href = '/notice';
        return; // OKTA 리다이렉트 중단
    }

    // 정상 흐름: OKTA SAML 인증 URL로 리다이렉트
    location.href = &quot;${oktaSamlAuthUrl}&quot;;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-heading=&quot;최종 흐름 정리&quot; data-ke-size=&quot;size26&quot;&gt;최종 흐름 정리&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckrwum/dJMcahxlaI4/O6iBuX4a9HWqBAw4X2H3pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckrwum/dJMcahxlaI4/O6iBuX4a9HWqBAw4X2H3pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckrwum/dJMcahxlaI4/O6iBuX4a9HWqBAw4X2H3pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fckrwum%2FdJMcahxlaI4%2FO6iBuX4a9HWqBAw4X2H3pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;318&quot; data-origin-width=&quot;637&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[정상 흐름]
로그인 페이지 &amp;rarr; OKTA 인증 &amp;rarr; SAML 콜백 처리 &amp;rarr; 메인 페이지

[에러 발생 시 흐름 (수정 후)]
로그인 페이지 &amp;rarr; OKTA 인증 &amp;rarr; SAML 콜백 처리
    &amp;rarr; (Exception 발생) &amp;rarr; 서버 로그 기록 &amp;rarr; 로그인 페이지?error=SAML_PARSE_FAILED
    &amp;rarr; (error 파라미터 감지) &amp;rarr; 안내 팝업 &amp;rarr; 서비스 안내 페이지
    [루프 차단됨]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-heading=&quot;배운 점&quot; data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lmIfM/dJMcaflY4sq/oqd3Ra2ONUQTe8eQnJ1HGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lmIfM/dJMcaflY4sq/oqd3Ra2ONUQTe8eQnJ1HGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lmIfM/dJMcaflY4sq/oqd3Ra2ONUQTe8eQnJ1HGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlmIfM%2FdJMcaflY4sq%2Foqd3Ra2ONUQTe8eQnJ1HGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;888&quot; height=&quot;495&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복합 버그는 레이어 하나만 고쳐서는 해결되지 않는다.&lt;br /&gt;백엔드 500 에러와 프론트엔드 무조건 리다이렉트 로직이 맞물리면, 각각은 정상처럼 보여도 합쳐지면 무한 루프를 만든다.&lt;br /&gt;버그를 분석할 때 &quot;내 담당 레이어&quot;에만 집중하는 것이 얼마나 좁은 시야인지 이번에 실감했다.&lt;br /&gt;요청 흐름 전체를 먼저 그리는 습관을 들여야 한다.&lt;/li&gt;
&lt;li&gt;try-catch로 예외를 잡아 사용자 화면을 깔끔하게 처리하는 것과, 서버 로그에 에러를 남기는 것은 반드시 동시에 해야 한다.&lt;br /&gt;처음에는 &quot;어차피 에러 페이지 보여주면 되지&quot;라고 단순하게 생각했는데, 그 판단이 운영 중 장애를 아무도 인지하지 못하게 만드는 함정이었다.&lt;/li&gt;
&lt;li&gt;기능이 동작하는지 확인하는 것과, 사용자가 에러 상황에서 갇히지 않는지 확인하는 것은 다른 테스트다.&lt;br /&gt;수정 후 반드시 에러 시나리오를 직접 따라가 보는 것이 중요하다.&lt;br /&gt;빈 화면에 방치되는 사용자를 만들지 않으려면, 정상 흐름만큼 에러 흐름도 꼼꼼하게 설계해야 한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  Fr&amp;alpha;мeworĸ</category>
      <category>500error</category>
      <category>backend</category>
      <category>debug</category>
      <category>error</category>
      <category>exception</category>
      <category>http</category>
      <category>loop</category>
      <category>okta</category>
      <category>saml</category>
      <category>springboot</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/262</guid>
      <comments>https://yurizzy.tistory.com/262#entry262comment</comments>
      <pubDate>Fri, 17 Apr 2026 04:13:30 +0900</pubDate>
    </item>
    <item>
      <title> ️ 실무 : GCP to OCI 이전 구축기 및 504 Gateway Time-out 해결기 (feat. Swap Memory)</title>
      <link>https://yurizzy.tistory.com/261</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;INTRO&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여느 때와 다름없는 평화로운 금요일&lt;br /&gt;결혼식을 한다고 2년 가까이 길었던 머리를&lt;br /&gt;단발로 싹-둑 자르고 파마를 하러 미용실에 방문했다.&lt;br /&gt;꾸벅꾸벅 졸고 있던 찰나, 휴대폰이 '징- 징-' 울렸다.&lt;br /&gt;무심코 화면을 켰는데 순간 내 눈을 의심했다.&lt;br /&gt;&lt;b&gt;구글 클라우드 ~~~ &lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;52,150원 결제 완료&lt;/b&gt;&amp;nbsp;카드사 결제 알림이 떠 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MK6Nj/dJMcahcRuzy/UHuzbmrk886EsdEmTQ0Zuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MK6Nj/dJMcahcRuzy/UHuzbmrk886EsdEmTQ0Zuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MK6Nj/dJMcahcRuzy/UHuzbmrk886EsdEmTQ0Zuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMK6Nj%2FdJMcahcRuzy%2FUHuzbmrk886EsdEmTQ0Zuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;293&quot; height=&quot;292&quot; data-origin-width=&quot;814&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;대체 내가 뭘 돌렸길래 5만 원이나 결제가 된 거지?&quot;&lt;br /&gt;머리에 약을 바른 채 구글 클라우드 콘솔 앱을 켜고 한참을 뒤적거리며 찾아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 트래픽 과금인가? 무료 티어 기준을 초과한 사용량이 무엇 때문인지 분명치 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;안 그래도 기존에 사용하던 GCP 무료 티어 서버는 너무 버벅거리고&lt;br /&gt;접근할 때마다 느려서 개발하고 테스트하기가 여간 불편한 게 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 요금 과금 문자를 시작으로 평소 자주 떠들떠들 하는 개발자 오톡방에 클라우드 추천을 받았고&lt;br /&gt;사람들이 추천한 오라클 클라우드(OCI)로 서버를 아예 이전하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무사히(&lt;s&gt;?&lt;/s&gt;) OCI에 인프라 환경을 처음부터 새롭게 구축하고&lt;br /&gt;모든 설정을 보란 듯이 마쳤지만, 기쁨도 잠시... &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Admin 로그인 페이지에 접속하자마자 서버 전체가 완전히 뻗어버리는 치명적인 문제를&amp;nbsp;마주하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온갖 삽질 끝에 서버 가상 메모리(Swap Space) 할당이라는 인프라적 조치를 통해 이 사태를 해결해냈다.&lt;br /&gt;Gemini랑 새벽 3시까지 작업한 인프라 이전 과정과 트러블슈팅의 전체 과정을 생생하게 기록해두려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; background-color: #f6e199;&quot;&gt;&lt;s&gt;(또 언제 할 줄 모르잖아 ?)&lt;/s&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발환경 및 클라우드 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 마이그레이션을 진행하며 기존의 AS-IS 환경과 새로운 TO-BE 환경의 차이는 꽤 컸다.&lt;br /&gt;특히 단순 반복 배포에서 벗어나 최신 트렌드에 맞춘 Docker 환경을 추가 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;구분&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;AS-IS (GCP 환경)&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;TO-BE (OCI 환경)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;운영체제 및 인프라&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Google Cloud Platform (무료 티어)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Oracle Cloud Infrastructure (무료 티어)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;배포 패러다임&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;jar 파일 수동 빌드 &amp;gt; 업로드 등 단순 배포&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Docker Compose 기반 다중 컨테이너 자동화 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;웹 프론트 엔드&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;웹 서버 부재&lt;br /&gt;8080 등 개별 포트 직접 접근&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Nginx 리버스 프록시 적용 &lt;br /&gt;(80/443 포트 브릿지)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;서버 시스템 자원&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;물리 RAM 1GB &lt;br /&gt;(빈번한 스와핑 발생)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;물리 RAM 1GB + Swap Memory 2GB &lt;br /&gt;강제 할당 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 기술: Java 17, Spring Boot 3.3.2, PostgreSQL 15, RabbitMQ, Nginx, Certbot&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  무료 티어 최강자 비교: GCP vs OCI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나처럼 토이 프로젝트나 레거시 개선 연습용 개인 서버를 찾는 개발자에게 무료 클라우드는 사막의 오아시스다.❤️&lt;br /&gt;하지만 벤더사마다 제공하는 자원에는 명확한 차이가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;GCP (e2-micro) 무료 티어&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;OCI (VM.Standard.E2.1.Micro) 무료 티어&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;RAM (메모리)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1GB&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1GB (ARM 기반 Ampere는 24GB까지 주기도함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;CPU 코어&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;2 vCPU &lt;br /&gt;(0.25 코어 제한을 둔 버스팅)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1/8 OCPU &lt;br /&gt;(AMD 마이크로 인스턴스 기준)&lt;br /&gt;(ARM 선택 시 최대 4 OCPU)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;네트워크 트래픽&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;북미 리전만 무료 &lt;br /&gt;(아시아는 과금)&lt;br /&gt;월 1GB 무료&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;서울/춘천 리전 선택 가능&lt;br /&gt;월 10TB까지 무료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;디스크(스토리지)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;30GB 고정 &lt;br /&gt;(Standard HDD 수준 속도)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;최대 200GB 무료 제공 &lt;br /&gt;(Boot Volume 성능 압도적)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;총평&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;간단한 Node.js나 파이썬 연습용. &lt;br /&gt;무거운 Java(Spring) + DB 버티기 힘듦.&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;한국 리전을 무료로 주어 체감 접속 속도 빠름&lt;br /&gt;디스크가 넉넉함. &lt;br /&gt;Java 프로젝트의 새로운 은신처로 각광.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교표를 보면 알겠지만, 한국 리전이 무료로 제공되면서&lt;br /&gt;트래픽이 10TB나 허용되는 OCI는 개인 서버 용도로는 현재 적수가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OCI 환경 구축: 밑바닥부터 Nginx까지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 맞이한 과제는 그야말로 아무것도 깔려있지 않은&lt;br /&gt;텅 빈 OCI 서버 깡통을, 내 프로젝트가 살아 숨 쉬며 운영 가능한 인프라로 탈바꿈시키는 것이었다.&lt;br /&gt;이 과정은 다음 순서로 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;809&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7QbXp/dJMcahxbCO8/Mx4zyGMTJE4CtoSiYbUVYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7QbXp/dJMcahxbCO8/Mx4zyGMTJE4CtoSiYbUVYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7QbXp/dJMcahxbCO8/Mx4zyGMTJE4CtoSiYbUVYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7QbXp%2FdJMcahxbCO8%2FMx4zyGMTJE4CtoSiYbUVYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;308&quot; height=&quot;308&quot; data-origin-width=&quot;809&quot; data-origin-height=&quot;809&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;OCI 회원가입 및 우분투 인스턴스 생성 &lt;br /&gt;OCI에 가입하는 것부터가 난관이라고들 하지만 운 좋게 한 번에 통과했다.&lt;br /&gt;이후 무료 티어(Always Free)가 제공되는&lt;code&gt;VM.Standard.E2.1.Micro&lt;/code&gt; 인스턴스를 하나 파냈다.&lt;br /&gt;운영체제는 패키지 관리가 제일 편하고 레퍼런스가 방대한 Ubuntu 22.04 LTS 버전을 선택했다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;고정 IP 할당 및 도메인 연결&lt;br /&gt;클라우드 인스턴스는 재부팅을 하거나 중지했다가 켜면 가상 IP가 바뀌어버리는 현상이 발생한다.&lt;br /&gt;이렇게 되면 도메인과 연결이 끊어지기 때문에 OCI 콘솔 메뉴에서 &lt;br /&gt;&lt;code&gt;예약된 공용 IP(Reserved Public IP)&lt;/code&gt;를 하나 발급받아&lt;br /&gt;내 인스턴스의 가상 네트워크(VCN)에 고정으로 물려주었다.&lt;br /&gt;그 후 내가 소유한 호스팅케이알(내 도메인 관리처) 관리자 페이지에 로그인해&lt;br /&gt;이 새로운 고정 IP 주소로 도메인(&lt;code&gt;http://[내부도메인]&lt;/code&gt;)의 A 레코드를 매핑 연결해주었다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;도커(Docker) 및 인프라 필수 패키지 설치&lt;br /&gt;서버 내부의 폴더들이 파편화되고 의존성 라이브러리가 꼬이는 현상을 막고자&lt;br /&gt;모든 컴포넌트를 Docker 컨테이너 위에서 깔끔하게 통제하기로 했다.&lt;br /&gt;맥북 터미널을 키고 SSH로 우분투에 진입 &lt;code&gt;sudo apt-get update&lt;/code&gt;와 함께 &lt;br /&gt;Docker 데몬 및 여러 컨테이너를 한방에 띄워줄 수 있는 Docker Compose 패키지를 순차적으로 설치했다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;OCI 방화벽(보안 목록) 포트 개방&lt;br /&gt;내부적으로 애플리케이션의 8080 포트와 웹 서버의 80 포트가 돌고 있어도&lt;br /&gt;OCI 자체가 지닌 방화벽(Security List)이 꽉 막고 있다면 외부 인터넷에서는 절대 접속할 수가 없다.&lt;br /&gt;OCI VCN의 서브넷 설정에 들어가서, HTTP 통신을 위한 80 포트와&lt;br /&gt;HTTPS를 위한 443 포트, 원격 접속을 위한 22 포트에 대한 Ingress(수신) 규칙을&lt;br /&gt;모두 &lt;code&gt;0.0.0.0/0&lt;/code&gt; (전 세계 누구든) 상태로 허용 포트를 개방해주었다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Nginx 리버스 프록시 설정 및 서버 적용&lt;br /&gt;기존에는 프론트에 WAS가 직접 노출되어 있어서&lt;br /&gt;&lt;code&gt;http://[도메인]:8080&lt;/code&gt; 처럼 주소 뒤에 포트 번호를 지저분하게 붙여야만 했다.&lt;br /&gt;이를 개선하고자, 사용자는 깔끔하게 80 포트로 들어오고 그 트래픽을 백엔드의 8080 포트로 자연스럽게 토스해주는&lt;br /&gt;'리버스 프록시(Reverse Proxy)' 설정을 위해 Nginx 웹 서버를 도입했다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스코드 레포지토리 루트에 &lt;code&gt;nginx&lt;/code&gt; 폴더를 파고&lt;br /&gt;&lt;code&gt;default.conf&lt;/code&gt; 설정 파일을 만들어 아래와 같은 설정을 부여했다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server {
    listen 80;
    listen [::]:80;
    server_name [내부도메인];

    location / {
        # app은 Docker Compose에서 명명한 Spring Boot 컨테이너 이름
        proxy_pass http://app:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;이후 루트의 &lt;code&gt;docker-compose.yml&lt;/code&gt; 파일에&lt;br /&gt;Nginx 이미지를 받아오는 컨테이너 정의를 추가하고&lt;br /&gt;호스트의 &lt;code&gt;./nginx&lt;/code&gt; 디렉토리를 컨테이너 속 &lt;code&gt;/etc/nginx/conf.d&lt;/code&gt; 로 마운트하여&lt;br /&gt;이 설정 파일을 자동으로 읽어오도록 구성했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;6&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GitHub Actions 자동 배포 세팅 완료&lt;br /&gt;손으로 매번 코드를 빌드하고 옮기고 재시작하는 수동 배포의 번거로움을 덜기 위해&lt;br /&gt;&lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt; 파일을 작성해 CI/CD 파이프라인을 구축했다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;appleboy/ssh-action&lt;/code&gt; 플러그인을 활용해 OCI 서버에 은밀하게 접속한 다음&lt;br /&gt;&lt;code&gt;docker-compose down -v&lt;/code&gt; 로 기존 컨테이너를 내리고&lt;br /&gt;변경된 코드들과 함께 &lt;code&gt;docker-compose up -d --build&lt;/code&gt; 명령어가&lt;br /&gt;자동으로 실행되도록 스크립트를 조율했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정한 모든 세팅을 마치고 떨리는 마음으로 크롬 브라우저에 &lt;code&gt;http://[내부도메인]&lt;/code&gt; 주소를 쳐서 엔터를 눌렀다.&lt;br /&gt;드디어 로컬 컴퓨터에서만 보던 우리 프로젝트의 반가운 웰컴 페이지가 OCI 망을 타고 아름답게 나타났다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh1DFd/dJMcacQanyJ/Wq9CUNZEdy2UdLXFLjEi8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh1DFd/dJMcacQanyJ/Wq9CUNZEdy2UdLXFLjEi8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh1DFd/dJMcacQanyJ/Wq9CUNZEdy2UdLXFLjEi8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh1DFd%2FdJMcacQanyJ%2FWq9CUNZEdy2UdLXFLjEi8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;339&quot; height=&quot;289&quot; data-origin-width=&quot;506&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황 통보: 죽음의 무한 로딩&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx 경로 세팅이며 Docker 배포 자동화까지 모든 것이 너무도 교과서처럼 완벽하게 끝났다고 생각했다.&lt;br /&gt;아주 흐뭇한 마음으로 개발 서버 대시보드를 둘러보기 위해 Admin 계정으로 로그인을 시도하는 버튼을 클릭했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 화면 중앙의 로그인 로딩 스피너 창이 빙글빙글 돌기 시작했다.&lt;br /&gt;&lt;br /&gt;처음엔 클라우드 데이터베이스 초기 로딩이 유독 느린가 싶어 기다려 보았다.&lt;br /&gt;개발자 도구 네트워크 탭을 열어보니 &lt;code&gt;pending&lt;/code&gt; 상태로 응답을 애타게 기다리고 있었다.&lt;br /&gt;요상함을 느끼고 요청을 &lt;code&gt;Esc&lt;/code&gt; 키로 강제 취소한 뒤 다시 가벼운 웰컴 페이지로 접속을 시도했으나&lt;br /&gt;이제는 아예 빈 화면만 뜨고 브라우저가 아무런 반응조차 하지 않았다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3aMoT/dJMb996YFIo/bbKYAdlRQkmusTCdEKPQfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3aMoT/dJMb996YFIo/bbKYAdlRQkmusTCdEKPQfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3aMoT/dJMb996YFIo/bbKYAdlRQkmusTCdEKPQfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3aMoT%2FdJMb996YFIo%2FbbKYAdlRQkmusTCdEKPQfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;616&quot; height=&quot;414&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 하염없이 1분여를 기다리자&lt;br /&gt;브라우저에는 &lt;code&gt;504 Gateway Time-out&lt;/code&gt; 에러 화면이 출력되었다.&lt;br /&gt;&lt;br /&gt;Nginx는 살아서 어떻게든 앞단을 지키고 있었지만&amp;nbsp;뒤에서 답을 줘야 할 Spring Boot가 기절했다는 확실한 표식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설상가상으로 구체적인 원인을 파악하기 위해 서버에 SSH 접속 툴(Mac의 iTerm)을 켜고 터미널 접근을 시도했지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웬걸...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PJFjN/dJMcaax3OLb/jw2W0KhiAk2e06f2F9KV20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PJFjN/dJMcaax3OLb/jw2W0KhiAk2e06f2F9KV20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PJFjN/dJMcaax3OLb/jw2W0KhiAk2e06f2F9KV20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPJFjN%2FdJMcaax3OLb%2Fjw2W0KhiAk2e06f2F9KV20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;285&quot; height=&quot;290&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Connection timed out&lt;/code&gt; 에러 문구만 덩그러니 내뱉으며 심지어 터미널은 내 키보드 입력을 모두 무시했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어플리케이션(Spring Boot)만 죽은 게 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCI 리눅스(Ubuntu) 운영체제 껍데기 전체가 완전히 동태가 되버린 Kernel Freeze 상태인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;첫 번째 시도: 코드 변경 로그 확인 및 재배포 모니터링 (실패)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z6C2d/dJMcah41iGl/MKLUPFsdnIC3UnlMt6NZ4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z6C2d/dJMcah41iGl/MKLUPFsdnIC3UnlMt6NZ4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z6C2d/dJMcah41iGl/MKLUPFsdnIC3UnlMt6NZ4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz6C2d%2FdJMcah41iGl%2FMKLUPFsdnIC3UnlMt6NZ4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;957&quot; height=&quot;534&quot; data-origin-width=&quot;957&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;시도한 이유 ⁇&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;가장 처음 드는 합리적 의심은 방금 내가 직접 작성한 Nginx의 라우팅 설정 파일(&lt;code&gt;default.conf&lt;/code&gt;)이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오타나 논리 오류로 인해 내부에서 통신 무한 루프 상태에 빠졌을 가능성이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 JWT 인증이나 Spring Security 로직에서 토큰을 빙빙 돌리며&lt;br /&gt;자원을 독식하는 결함이 배포 과정에서 함께 숨어 들어갔다고 의심했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;적용 방법&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;터미널조차 연결되지 않는 이 깡통 상태를 풀기 위해 OCI 클라우드 콘솔 홈페이지에 직접 접속했다.&lt;br /&gt;인스턴스 전원을 차단(Force Stop)하는 메뉴를 눌러 가상머신을 물리적으로 재부팅하고 다시 시작(Start)시켰다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;서버가 간신히 켜지자마자, 다음 다운이 오기 전에&lt;br /&gt;도커 컨테이너들의 로그 스트림(&lt;code&gt;docker-compose logs -f&lt;/code&gt;) 명령어를 띄웠다.&lt;br /&gt;그리고 Nginx &lt;code&gt;proxy_pass&lt;/code&gt; 설정에 잘못된 헤더나 Loop 옵션이 있는지 두 눈에 불을 켜고 확인했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;문제점 및 한계&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 까보면 깔수록 Nginx 설정은 문법적 오류 하나 없이 너무나 완벽했다.&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;(Gemini가 써준거라 완벽한 줄 알았다 )&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;에러 로그 한 줄 보이지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 막 재부팅된 직후에는 웰컴 페이지 접속이 아주 평온하고 빠르게 잘 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그러나, 다시 브라우저 창을 열고 Admin 인증을 거쳐 수많은 회원 테이블과 가입 통계를 조인해서 불러오는&lt;br /&gt;'강력한 DB 내부 로직'이 담긴 API를 호출하는 순간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또다시 어김없이 터미널의 커서가 정지해버리며 서버 전체가 기절해버렸다.&lt;br /&gt;예외(Exception) 추적 로그를 살펴보고 자시고 할 여유조차 주지 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다시   빵- 하고 OCI 서버가 마비된 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mi1SO/dJMcaakvNpo/Y5x7PqHPKAyQQSRqiwGcl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mi1SO/dJMcaakvNpo/Y5x7PqHPKAyQQSRqiwGcl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mi1SO/dJMcaakvNpo/Y5x7PqHPKAyQQSRqiwGcl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmi1SO%2FdJMcaakvNpo%2FY5x7PqHPKAyQQSRqiwGcl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;956&quot; height=&quot;533&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;533&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배운 점&lt;br /&gt;Nginx 컨테이너가 끝끝내 사용자 브라우저에 에러 응답(504)을 잘 던져주고&lt;br /&gt;뒷단 시스템 전체가 통제 불능이 된다는 것은&lt;br /&gt;단순한 프록시 규칙 에러나 코드의 무한 연산 문제가 아니라는 걸 깨달았다.&lt;br /&gt;이는 그 뒤에서 구동 중인 대형 WAS(Spring Boot) 및 다른 요소들이 서로 경합하다가&lt;br /&gt;물리적인 서버의 핵심 리소스를 아예 고갈시켜버린 인프라 레벨의 치명상이라는 뜻이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 해결: 인공호흡기 가상 메모리(Swap File) 할당&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ssJAL/dJMcagLOLef/nhUmL2K2tfSc2UbLTnPx2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ssJAL/dJMcagLOLef/nhUmL2K2tfSc2UbLTnPx2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ssJAL/dJMcagLOLef/nhUmL2K2tfSc2UbLTnPx2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FssJAL%2FdJMcagLOLef%2FnhUmL2K2tfSc2UbLTnPx2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;346&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;근본적 원인 분석 (OOM 패닉 현상)&lt;/span&gt;&lt;br /&gt;문제의 본질은 OCI 무료 티어 인스턴스가 가진 치명적인 사양적 한계 때문이었다.&lt;br /&gt;CPU 아키텍처나 엄청난 디스크 용량이 전부가 아니다.&lt;br /&gt;가장 중요한 워크스페이스인 물리적 메모리(RAM)가 단 1GB에 불과하다는 점이 재앙의 씨앗이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 돌리고 있는 우리 프로젝트의 사양을 계산해보면 답이 나온다.&lt;br /&gt;무거운 JVM 기반의 거대한 Spring Boot WAS 컨테이너 하나가 기본 400MB 이상을 점유하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 데이터베이스가 200MB, 비동기 통신을 제어하는 RabbitMQ가 또 200MB...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;게다가 트래픽 앞단을 지키는 Nginx와 GitHub 배포를 위한 데몬 서비스들까지...&lt;br /&gt;무려 5마리의 덩치 큰 프로세스들이 좁디좁은 1GB RAM 공간(자취방) 안에서 숨 막히게 공존하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평온하게 대기 중일 때는 이들이 어떻게든 구겨져서 아슬아슬하게 적재되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;(대만의 구룡성채나 다름없는 듯  )&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하지만, 내가 '로그인'을 진행한 순간 Spring Data JPA 구현체가 대량의 테이블을 읽어&lt;br /&gt;영속성 컨텍스트(메모리 캐시) 공간에 엔티티들을 대거 적재하려 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 RAM 사용량이 순식간에 100% 임계점을 뚫고 나갈 기세로 되는 것 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux 운영체제(커널)는 물리 메모리가 극도로 부족해 시스템이 위험해지면&lt;br /&gt;가용한 공간을 확보하기 위해 가장 RAM을 많이 먹고 있는 녀석을 골라&lt;br /&gt;강제로 사살해버리는 OOM(Out of Memory) 킬러 메커니즘을 발동시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 보통 Java(+Spring Boot) 프로세스가 제일 첫 번째 타깃  이 되어 죽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 심각한 문제는 OCI 서버의 초기 우분투 세팅에는 스토리지 디스크(SSD)의 빈 공간을 잠시 빌려 쓰는&lt;br /&gt;가상 메모리 영역인 Swap 메모리가 단 1Byte도 존재하지 않는 0B로 세팅되어 있었다는 점이다.&lt;br /&gt;RAM 용량이 초과되었을 때 잠시 피난 갈 수 있는 최후의 보류(인공호흡기) 공간이 아예 없어서&lt;br /&gt;OOM 킬러마저도 프로세스를 정리하지 못하고 커널 패닉에 빠져&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 전체가 쇼크사(System Freeze) 해버린 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;최종 적용 방법 및 구현 코드&lt;/span&gt;&lt;br /&gt;이를 해결하기 위한 조치는 간단명료하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 돈을 쓰면 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 미니PC를 살까 고민도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 일단 , 무료 티어로 버텨보자 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서&amp;nbsp;하드디스크(SSD)의 넉넉한 200GB 무료 공간 중 자투리 2GB를 강제로 빼앗아 잘라내고&lt;br /&gt;이를 가상 RAM처럼 작동하게 만들어주는 리눅스 Swap 영역 할당 설정을 수동으로 진행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OCI 콘솔에서 서버 강제 재부팅을 또다시 먹인 직후 도커 컨테이너들이 모두 구동되며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리를 잡아먹기 전의 골든 타임(약 2분) 안에 터미널을 열고 재빨리 아래 명령어들을 순차적으로 쏟아부었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 1. 시스템 루트 파티션에 2GB 크기의 커다란 빈 파일(스왑용) 생성
# fallocate는 dd 명령어보다 할당 속도가 훨씬 빠르다
sudo fallocate -l 2G /swapfile

# 2. 스왑 파일의 권한 변경
# 관리자(root) 계정만 읽고 쓰기가 가능하도록 권한을 600으로 막아야 보안 경고가 뜨지 않는다.
sudo chmod 600 /swapfile

# 3. 방금 만든 일반 빈 파일을 스왑 공간 구조로 내부 포맷
sudo mkswap /swapfile

# 4. 포맷된 해당 공간을 가상 메모리로 리눅스 커널에 활성화 명령!
sudo swapon /swapfile

# 5. 마운트 테이블(/etc/fstab)에 영구 등록
# 이 설정을 잊으면 다음 재부팅 때 스왑 메모리가 날아간다.
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# 6. 최종 할당 상태 확인
free -h&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 명령어들을 실행한 후 마지막으로 &lt;code&gt;free -h&lt;/code&gt;를 입력하여 상태표를 보았을 때&lt;br /&gt;눈앞에 나타난 글자는 이제 잘 수 있구나를 외쳤다 !&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PhrDg/dJMcaco6f0E/aQ85nKbfk3xWSMVZhRHes0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PhrDg/dJMcaco6f0E/aQ85nKbfk3xWSMVZhRHes0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PhrDg/dJMcaco6f0E/aQ85nKbfk3xWSMVZhRHes0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPhrDg%2FdJMcaco6f0E%2FaQ85nKbfk3xWSMVZhRHes0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;910&quot; height=&quot;453&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;기존 &lt;code&gt;Swap: 0&lt;/code&gt; 자리에 &lt;code&gt;Swap: 2.0Gi&lt;/code&gt; 라는 수치가 뚜렷하게 잡혀&lt;br /&gt;시스템에 든든한 보조 메모리 공간이 확보된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다시 Admin 환경에 접속하여 대략 2만건의&amp;nbsp;데이터를 긁어오는 복잡한 조인 쿼리 로직 버튼을 과감히 눌러 보았다.&lt;br /&gt;터미널 모니터링 창에서 RAM 활용도가 1GB의 한계치인 95%를 뛰어넘으려 할 때마다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈치 빠른 시스템이 즉각 백그라운드 프로세스들을 방금 만든 2GB의 Swap 영역으로 밀어내주기(Paging) 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과, 단 한 번의 에러 스파이크나 타임아웃 멈춤 현상 없이&lt;br /&gt;Spring Boot 서비스가 마치 16GB 메모리 서버에 있는 양 아주 쾌적하게 끝까지 동작을 무사히 마쳤다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한눈에 보는 OCI 서버 인프라 전체 설정 순서표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 인프라 환경 이전 및 요금 청구 사태로 시작된 OCI 마이그레이션과 장애 대응 전체 이력 타임라인을 적어보았다.&lt;br /&gt;다음에 또 내가 서버를 구성할 때 참고하실 수 있도록 하나의 테이블으로 정리해 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;진행 순서&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;핵심 작업 항목&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;작업 상세 내용 요약&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Step 1&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;OCI 회원가입 및 서버 세팅&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;오라클 클라우드 가입 후, Ubuntu 22.04 LTS 이미지를 사용&lt;br /&gt;무료 제공 규격인 &lt;code&gt;VM.Standard.E2.1.Micro&lt;/code&gt; 인스턴스 신규 생성함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Step 2&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;네트워크 및 고정 IP 설정&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;재부팅 시 IP 유실 방지로 Reserved Public IP 발급&lt;br /&gt;인스턴스에 고정 할당함. &lt;br /&gt;발급받은 IP는 구매한 도메인 DNS에 A 레코드로 직접 매핑 연결.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Step 3&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;서버 필수 의존 패키지 설치&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;SSH 원격 접속 후 인스턴스 내부에 패키지 관리자를 통해 &lt;br /&gt;Docker 데몬 &lt;br /&gt;Docker Compose 오케스트레이션 툴&lt;br /&gt;Git을 설치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Step 4&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;OCI 보안 정책 및 방화벽 개방&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;OCI VCN의 Security List 수정 메뉴에 접근 &lt;br /&gt;HTTP(80), 443 포트&amp;amp; SSH(22) 수신 룰 완전히 오픈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Step 5&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;앞단 Nginx 웹 프록시 구성&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;루트 &lt;code&gt;nginx/default.conf&lt;/code&gt; 파일을 작성&lt;br /&gt;사용자의 80 포트 요청을 백엔드 스프링부트 컨테이너 8080 포트로 &lt;br /&gt;내부에서 패스하도록 브릿지 생성.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Step 6&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;인프라 CI/CD 배포 파이프라인&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Github Actions의 deploy 스크립트를 작성 메인 브랜치 푸시 시 자동 &lt;br /&gt;서버 SSH 접속 및 &lt;code&gt;docker-compose up&lt;/code&gt; 갱신 구동 파이프라인 구축 완료.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Step 7&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;치명적 메모리 부족 장애 발생&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;OCI 환경 구축 완료 후 첫 관리자 로그인 시도 시 화면 무한 로딩, &lt;br /&gt;504 Time-out 발생 및 서버 OOM 판정&lt;br /&gt;우분투 전체가 다운되는 등 완전 먹통 현상 마주.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Step 8&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Swap 메모리 영역 강제 추가&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;장애 해결의 핵심! &lt;br /&gt;터미널 접속 후 &lt;code&gt;fallocate&lt;/code&gt;로 2GB 공간 스왑 영역 지정 권한 부여 &lt;br /&gt;및 &lt;code&gt;fstab&lt;/code&gt; 영구 등록 , 이후 모든 프로세스 정상 동작 확인!&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;739&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/trKCb/dJMcacikMdu/yzmkMVQm7JmzuELfCEAVB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/trKCb/dJMcacikMdu/yzmkMVQm7JmzuELfCEAVB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/trKCb/dJMcacikMdu/yzmkMVQm7JmzuELfCEAVB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtrKCb%2FdJMcacikMdu%2FyzmkMVQm7JmzuELfCEAVB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;739&quot; height=&quot;409&quot; data-origin-width=&quot;739&quot; data-origin-height=&quot;409&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인프라 관리는 코드 너머의 숲을 보는 것이다.&lt;br /&gt;백엔드 개발자라 할지라도 단순히 자바 코드와 어플리케이션 아키텍처 결함만 볼 게 아니다.&lt;br /&gt;배포되는 클라우드 인스턴스의 베어메탈 급 물리적 시스템 리소스(RAM 사용률, CPU Idle 상태, Disk I/O)도&lt;br /&gt;반드시 htop이나 top 명령어로 직접 들여다보는 모니터링 습관을 길러야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;504 에러의 숨은 함정&lt;br /&gt;Nginx 등 리버스 프록시 앞단을 세웠을 때 클라이언트 브라우저에서 맞는 &lt;code&gt;504 Gateway Time-out&lt;/code&gt;은&lt;br /&gt;주로 백엔드 서버(WAS) 환경 시스템 자체가 아예 기절해서 응답을 줄 컴퓨팅 리소스가 없기 때문에 흔히 발생한다.&lt;br /&gt;에러를 보자마자 다짜고짜 애먼 네트워크 인증 통신이나 라우팅 구성만 의심하고&lt;br /&gt;파싱 코드를 뒤지면 하루 종일 답도 없는 삽질을 하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무료 티어 클라우드의 현실적인 생존 필수값&lt;br /&gt;해외 클라우드 벤더사(GCP, AWS, OCI 등)가 홍보성으로 제공하는 제일 저렴한 무료 마이크로 1GB 인스턴스는&lt;br /&gt;RAM이 지독하게 쪼들린다. 만약 로컬에선 잘 되던 프로젝트가 이곳에 배포했을 때 수시로 죽는다면&lt;br /&gt;Docker 여러 개와 무거운 JVM 기반 프레임워크를 올릴 땐 Swap 가상 메모리 우회 세팅을 하는 것이&lt;br /&gt;선택 옵션이 아니라 그냥 생존을 위한 필수 전제조건임을 아주 뼈아프게 극복해내며 배웠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나의 삽질 및 실수 모음집 &amp;amp; 해결 팁&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuE5H7/dJMcaa5TlPs/4oAmJoSPNazwlyzgRqarM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuE5H7/dJMcaa5TlPs/4oAmJoSPNazwlyzgRqarM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuE5H7/dJMcaa5TlPs/4oAmJoSPNazwlyzgRqarM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuE5H7%2FdJMcaa5TlPs%2F4oAmJoSPNazwlyzgRqarM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;486&quot; height=&quot;286&quot; data-origin-width=&quot;486&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 인프라 구축 과정에서 정말 자잘하고 어이없는 실수들을 많이 저질렀다.&lt;br /&gt;나름 잘 설정했다고 생각했는데 안 되어서 당황했던 포인트들을 모아 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1️⃣ Nginx 443 포트 매핑 누락&lt;br /&gt;Nginx 설정 파일(default.conf)에는 listen 443 ssl; 이라고 적었다.&lt;br /&gt;Nginx 자체가 HTTPS 포트를 열어두도록 완벽하게 잘 만들었다.&lt;br /&gt;그런데 서버에 제일 바깥에서 컨테이너를 띄우는 docker-compose.yml 파일에서&lt;br /&gt;정작 443:443 포트를 외부로 개방(매핑)하는 설정을 빼먹었다.&lt;br /&gt;&lt;br /&gt;  해결 팁&lt;br /&gt;도어락은 바꿨는데 현관문을 안 열어준 격이다. &lt;br /&gt;docker-compose.yml 파일의 ports 설정에 꼭 443 포트를 추가하자.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;2️⃣ 도커 빌드 실패(Build failed) - .jar 파일 2개 오지랖&lt;br /&gt;로컬에서 잘 빌드되던 도커가 서버에서 자꾸 죽길래 봤더니&lt;br /&gt;빌드 결과물 폴더에 기본 .jar 파일과 함께 -plain.jar 파일까지 2개가 생성되어 있었다.&lt;br /&gt;도커 데몬 입장에서는 어떤 jar 파일을 카피해서 구동해야 할지 몰라 빌드가 실패한 것이다.&lt;br /&gt;&lt;br /&gt;  해결 팁&lt;br /&gt;로컬 프로젝트의 build.gradle 파일에 -plain.jar 파일은 아예 생성되지 않도록 설정&lt;br /&gt;(jar { enabled = false })을 딱 한 줄 추가해 두어 깔끔하게 해결했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;3️⃣ docker compose vs docker-compose 버전 충돌&lt;br /&gt;어떤 가이드 문서에는 띄어쓰기가 있고 어떤 건 하이픈(-)이 있어서 헷갈렸는데,&lt;br /&gt;결국 내가 설치한 서버 환경(구버전 호환)에서는 띄어쓰기를 한 docker compose 명령어가 작동하지 않았다.&lt;br /&gt;&lt;br /&gt;  해결 팁&lt;br /&gt;배포 스크립트의 명령어를 모두 docker-compose 구문으로 바꿔서 구동시켰다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;4️⃣ GitHub Actions Nginx 폴더 누락&lt;br /&gt;CI/CD를 통해 배포가 쫙쫙 잘 된 줄 알았는데, Nginx 컨테이너가 켜지자마자 죽어버렸다.&lt;br /&gt;로그를 까보니 호스트 서버에 nginx/default.conf 파일이 존재하지 않는다는 에러였다.&lt;br /&gt;&lt;br /&gt;  해결 팁&lt;br /&gt;deploy.yml 배포 스크립트 안에서 scp 커맨드로 파일을 클라우드 서버에 복사해 넘길 때&lt;br /&gt;docker-compose.yml만 넘기고 nginx 디렉터리는 안 넘기고 있었다.&lt;br /&gt;scp -r에 ./nginx 폴더를 포함시켜서 해결했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;5️⃣ .env 파일 무자비한 덮어쓰기 대면책&lt;br /&gt;서버 DB 비밀번호 같은 환경변수를 GitHub Actions의 Secrets로 주입할 때 초보적인 실수를 저질렀다.&lt;br /&gt;echo &quot;${{ secrets.DB_PASS }}&quot; &amp;gt; .env 형식처럼 꺾쇠 화살표 한 개(&amp;gt;)를 쓰는 바람에,&lt;br /&gt;기존에 만들어 둔 수많은 .env 속 변수들을 모두 싹둑 날려버리고 저거 단 한 줄만 덮어써 버린 것이다.&lt;br /&gt;&lt;br /&gt;  해결 팁&lt;br /&gt;파일 내용을 덮어쓰는 게 아니라 맨 밑에 '내용 추가' 만 하려면 꼭 화살표 두 개(&amp;gt;&amp;gt;)를 &lt;br /&gt;붙여서 써야 한다는 리눅스 커맨드의 무서움을 깨달았다.&lt;/blockquote&gt;</description>
      <category> ️ DevOpѕ</category>
      <category>Action</category>
      <category>cloud</category>
      <category>Docker</category>
      <category>GCP</category>
      <category>instance</category>
      <category>nginx</category>
      <category>oci</category>
      <category>무료서버</category>
      <category>무료티어</category>
      <category>인프라</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/261</guid>
      <comments>https://yurizzy.tistory.com/261#entry261comment</comments>
      <pubDate>Sat, 4 Apr 2026 03:41:53 +0900</pubDate>
    </item>
    <item>
      <title>  인프런(인프랩) 방문기 | 공약은 반드시 지킨다 (feat. 카파도키아 파우치)</title>
      <link>https://yurizzy.tistory.com/260</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공약의 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2월 인프런 챌린지가 한창 진행 중이던 어느 날&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 생각나는 데로 공약을 걸었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;만약 제가 상금을 받게 되면, 3월 신혼여행에서 도라님 선물 사올게요.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챌린지를 열심히 하다 보면 그런 기분이 드는 순간이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 해낼 수 있을 것 같다는 근거 없는 자신감.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 던진 공약이었는데, 상금도 받고 꿈도 꾸지 못한&amp;nbsp;&lt;span style=&quot;background-color: #ee2323;&quot;&gt;&lt;b&gt;MVP&lt;/b&gt;&lt;/span&gt;가 되어버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVP가 되면 인프런 수강권 6개월권을 받는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 받아버렸다 !!!!!!!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기쁘기도 했지만 솔직히 공약이 더 머릿속에 남았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;뭘 사다드려야 하지?! &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVS2hD/dJMcajn6XFH/Z7VDyt7OBgiSMWHuAI9Bfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVS2hD/dJMcajn6XFH/Z7VDyt7OBgiSMWHuAI9Bfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVS2hD/dJMcajn6XFH/Z7VDyt7OBgiSMWHuAI9Bfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVS2hD%2FdJMcajn6XFH%2FZ7VDyt7OBgiSMWHuAI9Bfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;457&quot; height=&quot;598&quot; data-origin-width=&quot;457&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;카파도키아에서 파우치를 샀다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신혼여행지는 튀르키예였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인천 ➡️ 이스탄불&amp;nbsp;➡️&amp;nbsp;카파도키아&amp;nbsp;➡️&amp;nbsp;이스탄불&amp;nbsp;➡️&amp;nbsp;인천&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열기구 타고 하늘 뜨는 곳, 드라마 세트장 같은 절벽 마을들.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정신없이 다니면서도 머릿속 한켠에는 계속 생각이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도라님 선물 사야 하는데  . .&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 카파도키아 기념품 가게를 기웃거리다가 파우치를 하나 집어들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튀르키예 특유의 알록달록한 풍선 그림이 자수로 들어간 파우치.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;387&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbjJOl/dJMcafe3KxF/KIVSn729iA5PLGyfnhEqYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbjJOl/dJMcafe3KxF/KIVSn729iA5PLGyfnhEqYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbjJOl/dJMcafe3KxF/KIVSn729iA5PLGyfnhEqYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbjJOl%2FdJMcafe3KxF%2FKIVSn729iA5PLGyfnhEqYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;156&quot; height=&quot;158&quot; data-origin-width=&quot;387&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보자마자 오! 딱 좋겠다 싶었다❤️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파우치만 가져가면 좀 허전하니까, 튀르키예 차 티팩도 몇 개 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현지에서 마셨는데 맛이 좋았거든.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기에 결혼 답례품도 챙겨서 같이 드리기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공약 하나가 나를 카파도키아까지 생각나게 한 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vGUQy/dJMcafTDmGg/V5sxktU6ery96N3ceuowV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vGUQy/dJMcafTDmGg/V5sxktU6ery96N3ceuowV0/img.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vGUQy/dJMcafTDmGg/V5sxktU6ery96N3ceuowV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvGUQy%2FdJMcafTDmGg%2FV5sxktU6ery96N3ceuowV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqMj4i/dJMcahqnJDw/zmnfH9nDmEkFC2zFTi5Pe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqMj4i/dJMcahqnJDw/zmnfH9nDmEkFC2zFTi5Pe1/img.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqMj4i/dJMcahqnJDw/zmnfH9nDmEkFC2zFTi5Pe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqMj4i%2FdJMcahqnJDw%2FzmnfH9nDmEkFC2zFTi5Pe1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JkR79/dJMcafMS3Db/Ivya01d7QlIig4kMkKtEUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JkR79/dJMcafMS3Db/Ivya01d7QlIig4kMkKtEUk/img.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JkR79/dJMcafMS3Db/Ivya01d7QlIig4kMkKtEUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJkR79%2FdJMcafMS3Db%2FIvya01d7QlIig4kMkKtEUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;신행 후, 드디어 인프랩으로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튀르키예에서 돌아오고 나서 며칠은 재택을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시차 적응도 해야 하고, 몸도 좀 쉬어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 오랜만에 회사에 출근하는 날&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도라님 선물과 회사 분들 선물짐을 챙겨서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출근을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가슴이 콩닥콩닥 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퇴근시간이 되서 바로 인프랩으로 향했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 버스를타고 약 30분. 생각보다 멀지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사실 집에 가는 길목이기도 함ㅋㅋ)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/85Zkk/dJMcahjDDWM/RAP8ScnddLq38eIkLlBuk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/85Zkk/dJMcahjDDWM/RAP8ScnddLq38eIkLlBuk1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;765&quot; data-filename=&quot;blob&quot; style=&quot;width: 82.0894%; margin-right: 10px;&quot; data-widthpercent=&quot;83.06&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/85Zkk/dJMcahjDDWM/RAP8ScnddLq38eIkLlBuk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F85Zkk%2FdJMcahjDDWM%2FRAP8ScnddLq38eIkLlBuk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2250&quot; height=&quot;765&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PBnMf/dJMcacbwKJj/cbUOWtkNMTKrcIm5Hs7kVK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PBnMf/dJMcacbwKJj/cbUOWtkNMTKrcIm5Hs7kVK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;3543&quot; data-filename=&quot;20260331_165951.jpg&quot; width=&quot;469&quot; height=&quot;782&quot; data-widthpercent=&quot;16.94&quot; style=&quot;width: 16.7478%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PBnMf/dJMcacbwKJj/cbUOWtkNMTKrcIm5Hs7kVK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPBnMf%2FdJMcacbwKJj%2FcbUOWtkNMTKrcIm5Hs7kVK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2126&quot; height=&quot;3543&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프랩 문 앞에 섰을 땐, 기분이 좀 묘했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프런 마크는 모니터에서만 보던 것 인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 실제로 여기 와 있구나 하는 감각.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 팬 미팅에 온 것 같기도 하고. .&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니면 취업 준비할 때 가보고 싶던 회사에 처음 들어가는 느낌 같기도 했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;향로님,시몬님,도라님을 실물로 뵀다❤️&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crOsVV/dJMcagkGdIr/jSJ3eq1ClSSvuDSxfhb7oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crOsVV/dJMcagkGdIr/jSJ3eq1ClSSvuDSxfhb7oK/img.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot; data-is-animation=&quot;false&quot; style=&quot;width: 35.5814%; margin-right: 10px;&quot; data-widthpercent=&quot;36&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crOsVV/dJMcagkGdIr/jSJ3eq1ClSSvuDSxfhb7oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrOsVV%2FdJMcagkGdIr%2FjSJ3eq1ClSSvuDSxfhb7oK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o1Tjc/dJMcaiW4B1G/BkkwaYlDPkKt193HiczPzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o1Tjc/dJMcaiW4B1G/BkkwaYlDPkKt193HiczPzK/img.png&quot; data-origin-width=&quot;4000&quot; data-origin-height=&quot;3000&quot; data-is-animation=&quot;false&quot; style=&quot;width: 63.2558%;&quot; data-widthpercent=&quot;64&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o1Tjc/dJMcaiW4B1G/BkkwaYlDPkKt193HiczPzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo1Tjc%2FdJMcaiW4B1G%2FBkkwaYlDPkKt193HiczPzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4000&quot; height=&quot;3000&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챌린지 때마다 화면 너머로 뵙던 도라님을 드디어 실물로 뵀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜로 있는 분이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(당연히 있는 분이지만, 그 감격이 있잖아.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시몬님도 함께 나와주셔서 인프랩 내부를 구경시켜 주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기저기 돌아보면서 &quot;와, 여기서 근무하시는 거구나&quot; 하는 생각이 계속 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중에 챌린지 때 방송하시던 방도 보았는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 당시엔 집에서 하시는 줄 알았다. 알고 보니 사무실이었다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구경하다 보니 직원들을 세심하게 챙겨주는 부분들이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;곳곳에 눈에 들어왔다. 솔직히 부러웠다.   . . . (우리 회사도 . . 크흡. .)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 놀란 건, 도라님이 갑자기 대표님을 소개시켜 주셨던 순간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누가 봐도 그냥 자기 자리에서 조용히 일하고 계신 일반 직원분처럼 보이셨는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그분이 대표님이라고 하셔서 깜짝 놀랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐주얼한 분위기도 놀라웠지만, 전혀 티가 안 나신 것도 놀라웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수평하고 자유로운 분위기, 정말 페어한 곳이구나 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠시 휴게실에 앉아 도라님께 카파도키아에서 사온 파우치와 티팩, 결혼 답례품도 드렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 거지만 공약을 지켰다는 뿌듯함이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;그리고 개발계의 아이돌, 향.로.님❤️&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4000&quot; data-origin-height=&quot;3000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s8wE0/dJMcaiJxvMD/ZOqjHCU8k7k1hhcQAgthXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s8wE0/dJMcaiJxvMD/ZOqjHCU8k7k1hhcQAgthXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s8wE0/dJMcaiJxvMD/ZOqjHCU8k7k1hhcQAgthXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs8wE0%2FdJMcaiJxvMD%2FZOqjHCU8k7k1hhcQAgthXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4000&quot; height=&quot;3000&quot; data-origin-width=&quot;4000&quot; data-origin-height=&quot;3000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 인프랩에 들어갔을 때부터 회의 중이셔서 못 뵙고 갈 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퇴근 시간도 임박해오고, 아쉬움을 달래며 슬슬 돌아서려던&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 순 ‼ 간 ‼ &lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;향로님이 나타나셨다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 악수도 하고 사진도 찍었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 좋았다. (이만하면 성덕 인정인가?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;lv_0_20260331201855.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZdfxe/dJMcaiJxvNM/MnXnjs8Gd0tK9iQm7gKaj0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZdfxe/dJMcaiJxvNM/MnXnjs8Gd0tK9iQm7gKaj0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZdfxe/dJMcaiJxvNM/MnXnjs8Gd0tK9iQm7gKaj0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZdfxe%2FdJMcaiJxvNM%2FMnXnjs8Gd0tK9iQm7gKaj0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;560&quot; height=&quot;747&quot; data-filename=&quot;lv_0_20260331201855.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;4000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도라님이 챙겨주신 인프런 굿즈도 너무 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짱짱한 티셔츠에, 컴팩트해서 소지하기 좋은 우산, 그리고 인프런 스티커까지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;(오늘 출근할 때 인프런 티셔츠 입은 건 비밀이다.)&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인프런과 나의 인연&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해 보면 인프런이랑 인연이 꽤 길다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;늦은 나이에 개발자 공부를 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직장을 다니면서 사이버대도 병행하면서. 솔직히 힘들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 포기하고 싶은 순간도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 개발바닥 유튜브를 보면서 많이 버텼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나만 힘든 게 아니구나, 다들 이렇게 시작하는 거구나&quot; 하는 안도감 같은 게 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발바닥을 보다가 김영한 선생님의 존재를 알게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 어느 날 인스타에서 인프런 광고가 떴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발바닥에서 보던 향로님이 계신 곳이라는 걸 알고 있었기 때문에 자연스럽게 가입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 나도코딩님의 무료 강의 몇 개를 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 김영한 선생님의 자바 기초 강의가 한번 무료로 풀린 적이 있었는데, 그게 시작이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의가 너무 좋았다. 설명이 너무 명확하고, 왜 이렇게 하는지를 알려주는 방식이 마음에 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 강의, 또 그다음 강의.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 돌이켜보면 영한 선생님 강의는 안 들어도 이미 좋은 가격이고 좋은 내용이면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 구매 가 나의 기준이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 총 13개를 구매했고, 그중 완강은 7개 정도 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지도 서서히 다 들을 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제로초님, 토비님 강의도 결제해서 보고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프런이 내 개발 공부의 거의 대부분을 차지하게 됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HG3Py/dJMcafMS3Rm/ppdSzq7wxP7bAHpZUKN1AK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HG3Py/dJMcafMS3Rm/ppdSzq7wxP7bAHpZUKN1AK/img.png&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;740&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.6787%; margin-right: 10px;&quot; data-widthpercent=&quot;50.26&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HG3Py/dJMcafMS3Rm/ppdSzq7wxP7bAHpZUKN1AK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHG3Py%2FdJMcafMS3Rm%2FppdSzq7wxP7bAHpZUKN1AK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;955&quot; height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R35He/dJMcahqnJTe/gnm8NQOZwe3KZObCSB7M61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R35He/dJMcahqnJTe/gnm8NQOZwe3KZObCSB7M61/img.png&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;740&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.1585%;&quot; data-widthpercent=&quot;49.74&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R35He/dJMcahqnJTe/gnm8NQOZwe3KZObCSB7M61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR35He%2FdJMcahqnJTe%2Fgnm8NQOZwe3KZObCSB7M61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;945&quot; height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5vfqK/dJMcahqnJTq/8AQ04iV1lAmxwGeUwB1NOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5vfqK/dJMcahqnJTq/8AQ04iV1lAmxwGeUwB1NOK/img.png&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;735&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.6247%; margin-right: 10px; margin-top: 10px;&quot; data-widthpercent=&quot;50.21&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5vfqK/dJMcahqnJTq/8AQ04iV1lAmxwGeUwB1NOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5vfqK%2FdJMcahqnJTq%2F8AQ04iV1lAmxwGeUwB1NOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;932&quot; height=&quot;735&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xId5V/dJMcagZhDMb/au5h1Iss6x5Y0UlEThphBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xId5V/dJMcagZhDMb/au5h1Iss6x5Y0UlEThphBk/img.png&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;734&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.2125%; margin-top: 10px;&quot; data-widthpercent=&quot;49.79&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xId5V/dJMcagZhDMb/au5h1Iss6x5Y0UlEThphBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxId5V%2FdJMcagZhDMb%2Fau5h1Iss6x5Y0UlEThphBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;923&quot; height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zQatq/dJMcagZhDMj/3gD4cpk8RBj7239hhWXuTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zQatq/dJMcagZhDMj/3gD4cpk8RBj7239hhWXuTK/img.png&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;671&quot; data-is-animation=&quot;false&quot; style=&quot;width: 33.474%; margin-right: 10px;&quot; data-widthpercent=&quot;33.87&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zQatq/dJMcagZhDMj/3gD4cpk8RBj7239hhWXuTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzQatq%2FdJMcagZhDMj%2F3gD4cpk8RBj7239hhWXuTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;939&quot; height=&quot;671&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RVEzH/dJMcaibLzKP/adofna8LDX2KPKJqcES1Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RVEzH/dJMcaibLzKP/adofna8LDX2KPKJqcES1Ck/img.png&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;344&quot; data-is-animation=&quot;false&quot; style=&quot;width: 65.3632%;&quot; data-widthpercent=&quot;66.13&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RVEzH/dJMcaibLzKP/adofna8LDX2KPKJqcES1Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRVEzH%2FdJMcaibLzKP%2Fadofna8LDX2KPKJqcES1Ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;챌린지가 나를 다시 일으켰다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자로 취업한 이후, 한동안 성장하고 있다는 느낌이 안 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 출퇴근하고 주어진 일만 하다 보니까 어느 순간 공부도 손에서 놓게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 다시 정신 차리고 공부를 재개할 무렵, 자극이 필요하다는 걸 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자 하는 공부의 한계 같은 거.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 인프런에서 처음 챌린지를 접하게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향로님이 진행하던 챌린지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해내고 나서의 그 희열감이 지금도 기억난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;내가 뭔가를 완수했다는 감각.&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람들의 글을 보고, 서로 응원하고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자 공부할 때는 느끼기 어려운 감각이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 좋아서 계속 챌린지에 참여하게 됐고, 그 끝에 2월 MVP가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챌린지를 거치면서 공부에 다시 재미를 붙이게 되니, 이것저것 덩달아 다시 살아나기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미뤄두기만 했던 개인 프로젝트도 시작했고, 손 놓고 있던 티스토리도 다시 글을 올리기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;246&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LcuN9/dJMcafF6dC3/k0Detau7QDAdGt12iZquJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LcuN9/dJMcafF6dC3/k0Detau7QDAdGt12iZquJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LcuN9/dJMcafF6dC3/k0Detau7QDAdGt12iZquJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLcuN9%2FdJMcafF6dC3%2Fk0Detau7QDAdGt12iZquJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;246&quot; height=&quot;122&quot; data-origin-width=&quot;246&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 한동안 황량했던 깃 잔디가 조금씩 푸릇푸릇해지고 있다.ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1775047705950&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;gayulz - Overview&quot; data-og-description=&quot;rebase했다가 과거를 바꿨습니다. gayulz has 8 repositories available. Follow their code on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/gayulz&quot; data-og-url=&quot;https://github.com/gayulz&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fVeBO/dJMb88F5bBw/ktUXMGUNvE16qcpPgX7zkK/img.png?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460,https://scrap.kakaocdn.net/dn/A5Sbv/dJMb83Si53v/jvxSdvYa9DXYiXxX8xMoK0/img.png?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460&quot;&gt;&lt;a href=&quot;https://github.com/gayulz&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/gayulz&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fVeBO/dJMb88F5bBw/ktUXMGUNvE16qcpPgX7zkK/img.png?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460,https://scrap.kakaocdn.net/dn/A5Sbv/dJMb83Si53v/jvxSdvYa9DXYiXxX8xMoK0/img.png?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;gayulz - Overview&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;rebase했다가 과거를 바꿨습니다. gayulz has 8 repositories available. Follow their code on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 글을 통해 막간 홍보를 하자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4월 챌린지가 지금 시작했다❤️&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내일부터 시작이니 빨리 같이 시작할 사람들은&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크를 타고 들어가서 빨리들 가입하시라 . .  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1775047774922&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;[인프런 챌린지] 4월 봄 맞이 인프런 운동회 챌린지 | 인프런 - 인프런&quot; data-og-description=&quot;789명이 수강했던 챌린지, 지금 바로 살펴보세요. 혼자 하는 공부는 오래가기 어렵습니다! 그렇다고 단순한 인증 챌린지도 오래 남지 않죠. 4월, 인프런에서 팀전으로 참여할 수 있는 &amp;lsquo;봄 맞이 &quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://inf.run/sqaXf&quot; data-og-url=&quot;https://www.inflearn.com/challenge/inf-challenge-202604-%ED%8C%80%EC%A0%84%EC%B1%8C%EB%A6%B0%EC%A7%80&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/crToOL/dJMb9kT3fRl/Pj5o6ERukQxfwB7w9I9jG1/img.gif?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bUlHeh/dJMb9b3Sktb/mic2SIzzwqiWQNMOWua2gK/img.gif?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781&quot;&gt;&lt;a href=&quot;https://inf.run/sqaXf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inf.run/sqaXf&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/crToOL/dJMb9kT3fRl/Pj5o6ERukQxfwB7w9I9jG1/img.gif?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781,https://scrap.kakaocdn.net/dn/bUlHeh/dJMb9b3Sktb/mic2SIzzwqiWQNMOWua2gK/img.gif?width=1200&amp;amp;height=781&amp;amp;face=0_0_1200_781');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[인프런 챌린지] 4월 봄 맞이 인프런 운동회 챌린지 | 인프런 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;789명이 수강했던 챌린지, 지금 바로 살펴보세요. 혼자 하는 공부는 오래가기 어렵습니다! 그렇다고 단순한 인증 챌린지도 오래 남지 않죠. 4월, 인프런에서 팀전으로 참여할 수 있는 &amp;lsquo;봄 맞이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-size: 1.62em; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;마무리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프랩에서 나오면서 드는 생각은 하나였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참 감사하다. 인프런이란 플랫폼에서 나는 정말 많은 걸 얻고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부도 하고, 랠릿을 통해 이력서도 작성하고 업데이트하고, 완강한 수강증을 하나씩 모으면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트로피를 수집하는 것 같은 뿌듯함도 얻는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 강의 플랫폼이라고만 생각했는데, 챌린지를 통해 공부를 이어갈 동력을 얻고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기서 만난 분들 덕분에 이런 방문까지 하게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 계속 인프런을 이용할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 영한 선생님 강의들도 꾸준히 들을 예정이고, 챌린지도 기회가 되면 또 참여해 볼 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공약 하나가 카파도키아 파우치가 되고, 인프랩 방문이 되고, 이 글이 됐다. . .&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 좋은 인연이 계속 이어지고 있는 것 같아서 기분이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 언젠가, 더 성장해서 인프런에 꼭 들어가고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>☃︎ D&amp;iota;&amp;alpha;ry</category>
      <category>inflearn</category>
      <category>개발바닥</category>
      <category>백엔드개발자</category>
      <category>성덕</category>
      <category>신입개발자</category>
      <category>인프랩</category>
      <category>인프런</category>
      <category>인프런강의</category>
      <category>인프런챌린지</category>
      <category>향로</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/260</guid>
      <comments>https://yurizzy.tistory.com/260#entry260comment</comments>
      <pubDate>Wed, 1 Apr 2026 22:00:55 +0900</pubDate>
    </item>
    <item>
      <title> ️ 실무 : Referer로 망분리를 시도했다가 배운 것들 (feat. Stateless vs Stateful)</title>
      <link>https://yurizzy.tistory.com/259</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgVWwx/dJMcajn5bkW/ELlwyHbD1FPLMvL2n4bYD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgVWwx/dJMcajn5bkW/ELlwyHbD1FPLMvL2n4bYD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgVWwx/dJMcajn5bkW/ELlwyHbD1FPLMvL2n4bYD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgVWwx%2FdJMcajn5bkW%2FELlwyHbD1FPLMvL2n4bYD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;948&quot; height=&quot;630&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⟡ 인트로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OKTA로 인증 방식을 전환하면서 기존 SSO 인증서가 담당하던 내/외부망 구분 기능이 사라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 HTTP Referer 헤더를 이용해 내부망 사용자를 판별하려 했으나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즐겨찾기 접속&amp;middot;OKTA 리다이렉트&amp;middot;forward 등 실제 운영 환경에서 Referer가 소실되는 경우가 너무 많아&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 내부망 사용자까지 차단되는 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Stateless한 Referer를 버리고, 서버가 상태를 기억하는 &lt;b&gt;세션 플래그(Session Flag)&lt;/b&gt; 방식으로 전환하여 문제를 해결했다. 이 글은 그 과정에서 Referer가 무엇이고 왜 접근 제어의 신뢰 기반이 될 수 없는지를 정리한 기록이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⟡ 개발환경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AS-IS TO-BE&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSO PKI 인증서 기반 내/외부 구분&lt;/td&gt;
&lt;td&gt;OKTA SAML 인증 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증서 유무로 VDI 여부 판단&lt;/td&gt;
&lt;td&gt;세션 플래그(INTERNAL_VERIFIED)로 판단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UserInterceptor (비활성, 데드코드)&lt;/td&gt;
&lt;td&gt;AuthenticationInterceptor (활성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 1.7 / Spring Framework 4.x&lt;/td&gt;
&lt;td&gt;Java 17 / Spring Boot 3.x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⟡ 문제 상황&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC02tK/dJMcabXX0cY/x1HoH2TkR1xxyTaKF37mPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC02tK/dJMcabXX0cY/x1HoH2TkR1xxyTaKF37mPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC02tK/dJMcabXX0cY/x1HoH2TkR1xxyTaKF37mPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC02tK%2FdJMcabXX0cY%2Fx1HoH2TkR1xxyTaKF37mPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;333&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;333&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 방식이 사라진 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레거시 코드의 인터셉터에는 SSO 인증서를 검증하는 로직이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 PC(또는 VDI)에 설치된 사내 인증서 모듈을 호출해서 VDI 여부를 판별했던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;VDI 사용자  &amp;rarr; 인증서 모듈 실행 성공 &amp;rarr; SSO 통과 ✅
외부 사용자 &amp;rarr; PC에 사내 인증서 없음 &amp;rarr; 모듈 로드 실패 &amp;rarr; 차단 ❌
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 보내는 헤더(텍스트)를 믿은 게 아니라, PC 자체에 설치된 암호화된 인증서라는 물리적 증거를 검사했기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;망 분리가 가능했다. 하지만 OKTA로 전환되면서 이 인증서 검증 로직이 사라졌고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 입장에서는 &quot;이 사람이 VDI 안에 있는가&quot;를 판단할 방법이 없어진 상태가 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Referer를 먼저 시도했는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 내에서 최초로 나온 아이디어는 HTTP Referer 헤더를 이용하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항은 아래 두 조건 중 하나를 만족하면 내부 사용자로 보자는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Referer 헤더에 내부망 포털 도메인([내부망 포털])이 포함되어 있음&lt;/li&gt;
&lt;li&gt;특정 관문 경로(/main.do)로 직접 접근함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로 표현하면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;private boolean isInternalAccess(String referer, String requestPath) {
    // 조건 A: Referer 체크
    if (referer != null &amp;amp;&amp;amp; referer.contains(&quot;[내부망 포털]&quot;)) {
        return true;
    }
    // 조건 B: 관문 경로 체크
    if (requestPath != null &amp;amp;&amp;amp; requestPath.startsWith(&quot;/main&quot;)) {
        return true;
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기에는 합리적인 것 같았다. 하지만 실제 배포 테스트를 해보니 두 가지 구조적 문제가 드러났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⟡ 1번째 시도: Referer 헤더 기반 내/외부 판단 (실패)&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1cSde/dJMcafF4olG/DtuStdyHvhDVWj9DDsjYy1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1cSde/dJMcafF4olG/DtuStdyHvhDVWj9DDsjYy1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1cSde/dJMcafF4olG/DtuStdyHvhDVWj9DDsjYy1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1cSde%2FdJMcafF4olG%2FDtuStdyHvhDVWj9DDsjYy1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;395&quot; height=&quot;296&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시도한 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부망 포털에서 링크를 클릭해 들어오는 경우, 브라우저가 자동으로 출발지 URL을 Referer 헤더에 담아 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 헤더를 인터셉터에서 읽어서 내부망 도메인 포함 여부를 판별하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점 1: 즐겨찾기&amp;middot;주소창 직접 입력 시 Referer는 null&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VDI 사용자라도 크롬을 새로 켜고 주소창에 직접 URL을 입력하거나 즐겨찾기로 접속하면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 Referer 헤더를 아예 보내지 않는다. 서버에 도착하는 HTTP 요청을 보면 이렇다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /main.do HTTP/1.1
Host: [내부서버]
User-Agent: Mozilla/5.0 ...
Cookie: JSESSIONID=...
(Referer 헤더 없음)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉터 입장에서는 Referer가 null이므로 &quot;출발지가 없다 &amp;rarr; 외부망이다&quot;라고 오판하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 VDI 사용자가 차단되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점 2: forward 또는 OKTA 리다이렉트 시 Referer 증발&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/main.do 조건을 통과해 세션 검증 없이 인터셉터를 통과했다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 forward:/view/p/login.do로 내부 포워딩되는 순간, 서버 내부에서 새로운 요청이 생성되기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Referer가 비어버린다. OKTA 302 리다이렉트 이후에도 동일한 현상이 발생한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;/main.do (통과) &amp;rarr; forward &amp;rarr; /view/p/login.do
&amp;rarr; 인터셉터 재진입
&amp;rarr; Referer: null
&amp;rarr; 차단 ❌
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 레이어에서 검사해도 결과는 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검사 위치 실행 코드 결과&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filter&lt;/td&gt;
&lt;td&gt;request.getHeader(&quot;Referer&quot;)&lt;/td&gt;
&lt;td&gt;null &amp;rarr; 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interceptor&lt;/td&gt;
&lt;td&gt;request.getHeader(&quot;Referer&quot;)&lt;/td&gt;
&lt;td&gt;null &amp;rarr; 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller&lt;/td&gt;
&lt;td&gt;request.getHeader(&quot;Referer&quot;)&lt;/td&gt;
&lt;td&gt;null &amp;rarr; 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSP/JS&lt;/td&gt;
&lt;td&gt;document.referrer&lt;/td&gt;
&lt;td&gt;&quot;&quot; &amp;rarr; 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심은 코드의 위치 문제가 아니라, 데이터의 부재 문제다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 제공하지 않는 정보는 서버의 어떤 레이어에서도 만들어낼 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Referer는 Stateless 방식이다. &quot;지금 이 순간의 요청&quot;이 어디서 왔는지만 알 뿐&quot;&lt;br /&gt;이 사람이 아까 어떤 경로로 들어왔는지&quot;는 기억하지 못한다. &lt;br /&gt;접근 제어처럼 연속적인 상태를 기억해야 하는 로직에는 태생적으로 부적합하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⟡ 최종 해결: 세션 플래그(Session Flag) 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPNepz/dJMcahw7KqQ/nxtPWHPWvHq6W6NkjyKkN0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPNepz/dJMcahw7KqQ/nxtPWHPWvHq6W6NkjyKkN0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPNepz/dJMcahw7KqQ/nxtPWHPWvHq6W6NkjyKkN0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPNepz%2FdJMcahw7KqQ%2FnxtPWHPWvHq6W6NkjyKkN0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;510&quot; height=&quot;265&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Referer를 버리고 세션(Session)으로 관점을 바꿨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션은 브라우저를 닫기 전까지 &lt;b&gt;상태를 기억&lt;/b&gt;하는 Stateful한 저장소다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디어는 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;/main.do 관문을 통과한 사람의 세션에 &quot;내부망 인증 완료&quot; 도장을 찍어두고, &lt;br /&gt;이후 모든 요청에서는 Referer를 보지 않고 이 도장의 유무만 확인한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 상수 정의&lt;/b&gt; &amp;mdash; 관문 URL 변경 시 이 한 곳만 수정하면 된다.&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;public final static class NetworkAccess {
    public static final String GATEWAY_PATH                  = &quot;/main.do&quot;;
    public static final String OKTA_SAML_CALLBACK_PATH       = &quot;/login/okta-saml-check.do&quot;;
    public static final String SESSION_KEY_INTERNAL_VERIFIED = &quot;INTERNAL_VERIFIED&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 관문 컨트롤러&lt;/b&gt; &amp;mdash; /main.do 진입 시 세션에 플래그를 세팅한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;

@Controller
public class MainRedirectController {

    @RequestMapping(NetworkAccess.GATEWAY_PATH)
    public ModelAndView redirectMainToLogin(HttpServletRequest request) {
        // 내부망 진입 도장 찍기
        request.getSession().setAttribute(
            NetworkAccess.SESSION_KEY_INTERNAL_VERIFIED,
            Boolean.TRUE
        );
        return new ModelAndView(&quot;forward:/view/p/login.do&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 인터셉터 판단 로직&lt;/b&gt; &amp;mdash; Referer 대신 세션 플래그만 본다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

private boolean isInternalAccess(HttpServletRequest request, String requestPath) {

    // 1. /main.do 관문 진입: 무조건 통과 (세팅은 컨트롤러에서)
    if (requestPath != null
            &amp;amp;&amp;amp; requestPath.indexOf(NetworkAccess.GATEWAY_PATH) &amp;gt; -1) {
        return true;
    }

    // 2. OKTA SAML 콜백: 세션이 재생성되므로 이 시점에 플래그 재세팅
    if (requestPath != null
            &amp;amp;&amp;amp; requestPath.indexOf(NetworkAccess.OKTA_SAML_CALLBACK_PATH) &amp;gt; -1) {
        request.getSession().setAttribute(
            NetworkAccess.SESSION_KEY_INTERNAL_VERIFIED,
            Boolean.TRUE
        );
        return true;
    }

    // 3. 그 외 모든 요청: 세션 플래그 유무만 확인
    HttpSession session = request.getSession(false);
    return session != null
        &amp;amp;&amp;amp; session.getAttribute(NetworkAccess.SESSION_KEY_INTERNAL_VERIFIED) != null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리 흐름&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;VDI 사용자 &amp;rarr; /main.do 접속
  &amp;rarr; 세션에 INTERNAL_VERIFIED=true 세팅
  &amp;rarr; OKTA 인증 완료 (세션 재생성 시 콜백에서 재세팅)
  &amp;rarr; 이후 모든 요청: 세션 플래그 확인 &amp;rarr; 통과 ✅

외부 사용자 &amp;rarr; /view/p/login.do 직접 접속
  &amp;rarr; 세션 플래그 없음
  &amp;rarr; external-notice.jsp 차단 화면 ❌
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 OKTA 콜백에서 플래그를 재세팅해야 하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OKTA SAML 인증이 완료되면 Session Fixation 공격 방지를 위해 &lt;b&gt;세션 ID가 교체&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 세션에 저장해둔 INTERNAL_VERIFIED 플래그도 함께 사라지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방치하면 로그인 직후 AJAX 요청이 전부 차단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/login/okta-saml-check.do 콜백 진입 시점에 플래그를 재세팅하면 이 문제를 방어할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;403&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VcouA/dJMcaiW2O7X/5HApEXegkJWUZkmddpenK0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VcouA/dJMcaiW2O7X/5HApEXegkJWUZkmddpenK0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VcouA/dJMcaiW2O7X/5HApEXegkJWUZkmddpenK0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVcouA%2FdJMcaiW2O7X%2F5HApEXegkJWUZkmddpenK0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;547&quot; height=&quot;403&quot; data-origin-width=&quot;547&quot; data-origin-height=&quot;403&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;⟡ 배운 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Referer는 &quot;지금&quot;만 알고 세션은 &quot;흐름 전체&quot;를 기억한다.&lt;/b&gt; &lt;br /&gt;접근 제어처럼 요청 간의 맥락이 필요한 로직은 Stateless한 헤더가 아니라 &lt;br /&gt;Stateful한 세션을 신뢰 기반으로 삼아야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터의 부재는 코드의 위치로 해결할 수 없다.&lt;/b&gt; &lt;br /&gt;Referer 검사를 Filter&amp;middot;Interceptor&amp;middot;Controller&amp;middot;JSP 어디로 옮겨도 결과는 동일하다. &lt;br /&gt;&quot;어디서 검사하는가&quot;가 아니라 &quot;검사할 값이 존재하는가&quot;의 문제이기 때문이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;완벽한 망분리를 원한다면 클라이언트 IP 화이트리스트나 인프라 레벨 정책이 유일한 해답이다.&lt;/b&gt; &lt;br /&gt;브라우저가 선택적으로 전송하는 헤더(Referer)에 의존하는 방식은 언제든 우회될 수 있다. &lt;br /&gt;세션 플래그 방식은 현실적인 절충안이지, 완벽한 보안 솔루션이 아니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category> ️ DevOpѕ</category>
      <category>network</category>
      <category>okta</category>
      <category>Referer</category>
      <category>stateful</category>
      <category>개발자</category>
      <category>망분리</category>
      <category>백엔드개발자</category>
      <category>보안</category>
      <category>인프라</category>
      <category>주니어개발자</category>
      <author>김춘덕⸝ဗီူ⸜</author>
      <guid isPermaLink="true">https://yurizzy.tistory.com/259</guid>
      <comments>https://yurizzy.tistory.com/259#entry259comment</comments>
      <pubDate>Mon, 30 Mar 2026 22:59:01 +0900</pubDate>
    </item>
  </channel>
</rss>