<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>HS_Developer</title>
    <link>https://winwin0219.tistory.com/</link>
    <description>✧ Daily Backend Log</description>
    <language>ko</language>
    <pubDate>Wed, 3 Jun 2026 03:59:27 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>HS0601</managingEditor>
    <image>
      <title>HS_Developer</title>
      <url>https://tistory1.daumcdn.net/tistory/7292339/attach/02a413e36a484656811f905d3f234555</url>
      <link>https://winwin0219.tistory.com</link>
    </image>
    <item>
      <title>[코드트리 후기] 북마크로 나만의 복습 루틴 만들기</title>
      <link>https://winwin0219.tistory.com/336</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vvwIV/dJMcabj6WJ9/OtL6yvzhNsGGq0sA8u76l1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vvwIV/dJMcabj6WJ9/OtL6yvzhNsGGq0sA8u76l1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vvwIV/dJMcabj6WJ9/OtL6yvzhNsGGq0sA8u76l1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvvwIV%2FdJMcabj6WJ9%2FOtL6yvzhNsGGq0sA8u76l1%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;272&quot; height=&quot;191&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.codetree.ai/ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;코드트리&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&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;p data-ke-size=&quot;size16&quot;&gt;이번에 느낀 점은 코딩테스트 훈련은 문제를 N개 푸는 만큼 내가 헷갈린 문제를 다시 풀어보는 과정도 중요하다는 것이다&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 다시 풀어본 문제 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;북마크 해두고 다시 풀어본 문제는&lt;b&gt; 배열의 값을 3배로!&lt;/b&gt; 문제다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략하게, 3행 3열의 배열을 입력 받고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 값을 3배로 만들어서 출력하는 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 풀면서 2차원 배열에서는 바깥 반복문이 행을 담당하고&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안쪽 반복문이 열을 담당한다는 점을 다시 확인할 수 있었다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 배열에 값을 저장한 뒤 출력할 수 있지만 이 문제처럼 입력받은 값을 바로 3배로 계산해서 출력하는 방식도 가능하다는 점이다&lt;/p&gt;
&lt;pre id=&quot;code_1780016474203&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        // Please write your code here.
    Scanner sc = new Scanner(System.in);

     int[][] array = new int[3][3];

     for(int i = 0; i &amp;lt; 3; i++){
        for(int j = 0; j &amp;lt; 3; j++){
            array[i][j] = sc.nextInt();

             System.out.print(array[i][j] * 3 + &quot; &quot;);
        }
        System.out.println();
     }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>공부습관</category>
      <category>오답노트</category>
      <category>코드트리</category>
      <category>코딩테스트</category>
      <category>코테공부</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/336</guid>
      <comments>https://winwin0219.tistory.com/336#entry336comment</comments>
      <pubDate>Fri, 29 May 2026 10:02:02 +0900</pubDate>
    </item>
    <item>
      <title>[Redis] DB 부하를 줄이기 위한 Redis</title>
      <link>https://winwin0219.tistory.com/335</link>
      <description>&lt;div class=&quot;toc-box&quot;&gt;
&lt;p class=&quot;toc-title&quot; 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;&lt;a href=&quot;#redis-intro&quot;&gt;Redis란 무엇인가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#redis-why&quot;&gt;왜 Redis를 쓰는가 (기존 DB와 차이)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#redis-data-types&quot;&gt;Redis 자료구조 6가지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#redis-ttl&quot;&gt;TTL (Time To Live)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#redis-persistence&quot;&gt;영속성 (Persistence) RDB vs AOF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#redis-single-thread&quot;&gt;싱글 스레드인데 왜 빠른가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#redis-cache-strategy&quot;&gt;캐싱 전략 4가지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#redis-eviction&quot;&gt;메모리 관리 (Eviction Policy)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#redis-spring-boot&quot;&gt;Spring Boot에서 Redis 사용하기&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h2 id=&quot;redis-intro&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Redis란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Re&lt;/b&gt;&lt;/span&gt;mote &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Di&lt;/b&gt;&lt;/span&gt;ctionary &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;S&lt;/b&gt;&lt;/span&gt;erver의 약자다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2009년 이탈리아 개발자 Salvatore Sanfilippo 가 MySQL 기반 실시간 로그 분석 시스템이 너무 느린 걸 해결하고자 만들었다 처음엔 개인 프로젝트였는데 오픈소스로 공개되면서 전세계적으로 퍼졌다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한줄로 정의하면 RAM에 데이터를 저장하는 Key-Value 기반 데이터 저장소&lt;/p&gt;
&lt;pre id=&quot;code_1779433612757&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Key              Value
post:views:42    200
user:session:abc {&quot;id&quot;: 1, &quot;name&quot;: &quot;민수&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Key로 데이터를 저장하고 Key로 데이터를 꺼낸다 그래서 구조가 단순하기 때문에 빠르다&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;redis-why&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 Redis를 쓰는가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 같은 일반 DB와 비교&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 데이터를 디스크(HDD/SSD)에 저장한다 Redis는 데이터를 RAM에 저장한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM이 디스크보다 얼마나 빠르냐면&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 접근 속도 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;HDD&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;~10ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SSD&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;~0.1ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;RAM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;~0.0001ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자로 보면 RAM이 SSD보다 1000배 빠르다 이게 Redis가 빠른 근본적인 이유다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 MySQL이 있는데 왜 Redis를 쓰냐면 MySQL 혼자로도 물론 동작은 한다 근데 트래픽이 많아지면 문제가 생긴다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 게시글 조회수를 MySQL에 바로 쓴다고 가정해보자&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;사용자 1000명이 동시에 같은 게시글을 조회한다&amp;nbsp;&lt;br /&gt;&amp;rarr; MySQL에 UPDATE 쿼리 1000개 동시 발생&lt;br /&gt;&amp;rarr; MySQL 락 경쟁&lt;br /&gt;&amp;rarr; 처리 지연&lt;br /&gt;&amp;rarr; 서비스 느려짐&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;즉 Redis는 RAM에 저장하고 원자적으로 처리하니까 이 1000개를 빠르게 소화할 수 있다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, Redis는 MySQL을 대체하는 게 아니라 MySQL의 부하를 줄이기 위해 앞단에 두는 것이다&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;[Spring Boot 서버] &amp;rarr; [Redis 서버] (별도 프로세스, 보통 도커) &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;rarr; [MySQL 서버] (별도 프로세스)&lt;/blockquote&gt;
&lt;h2 id=&quot;redis-data-types&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Redis 자료구조 6가지&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis가 단순 캐시랑 다른 점이 여기서 나온다 Key - Value저장이 아니라 Value에 다양한 자료구조를 지원한다&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. String 단순한 값 하나 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본 자료구조로 문자열, 숫자 모두 저장이 가능하다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779433940369&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET  name  &quot;민수&quot;
GET  name           &amp;rarr; &quot;민수&quot;

SET  count  0
INCR count          &amp;rarr; 1   (원자적 +1)
INCRBY count 5      &amp;rarr; 6   (원자적 +N)
DECR count          &amp;rarr; 5   (원자적 -1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INCR이 중요한 이유는 원자적으로 실행된다는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 1000명이&amp;nbsp; INCR을 쳐도 누락 없이 정확히 1000개 증가한다 MySQL에서 UPDATE count = count +1 동시에 치면 Lost Udpate 발생하는 것과 대조된다&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. List 순서가 있는 목록, 양쪽 끝에서 넣고 뺄 수 있음 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서가 있는 문자열 목록으로 양쪽 끝에서 push/pop 가능하다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434017972&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RPUSH  queue  &quot;작업1&quot;      &amp;rarr; [작업1]
RPUSH  queue  &quot;작업2&quot;      &amp;rarr; [작업1, 작업2]
LPUSH  queue  &quot;긴급작업&quot;   &amp;rarr; [긴급작업, 작업1, 작업2]

LPOP   queue              &amp;rarr; &quot;긴급작업&quot;
RPOP   queue              &amp;rarr; &quot;작업2&quot;

LRANGE queue 0 -1         &amp;rarr; [&quot;작업1&quot;]  (전체 조회)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L은 Left의 앞, R은 Right 뒤&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. Hash 하나의 키 안에 필드-값 쌍 여러 개 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 Key 안에 필드 - 값 쌍을 여러 개 저장하는 것으로 Java의 Map이랑 비슷하다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434183656&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HSET  user:1  name  &quot;민수&quot;
HSET  user:1  age   &quot;30&quot;
HSET  user:1  role  &quot;backend&quot;

HGET   user:1  name          &amp;rarr; &quot;민수&quot;
HGETALL user:1               &amp;rarr; {name: 민수, age: 30, role: backend}
HDEL   user:1  role          &amp;rarr; role 필드 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String으로 하면 user:1:name, user:1:age 키를 여러 개 만들어야 하는데 Hash는 하나의 키로 묶을 수 있다&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. Set 중복 없는 집합, 순서 없음 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복을 허용하지 않는 문자열 집합으로 순서가 없다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434240968&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SADD  tags  &quot;java&quot;
SADD  tags  &quot;spring&quot;
SADD  tags  &quot;java&quot;      &amp;rarr; 이미 있어서 무시됨

SMEMBERS tags            &amp;rarr; {&quot;java&quot;, &quot;spring&quot;}
SISMEMBER tags &quot;java&quot;   &amp;rarr; 1 (있음)
SISMEMBER tags &quot;python&quot; &amp;rarr; 0 (없음)

SINTER tags1 tags2       &amp;rarr; 교집합
SUNION tags1 tags2       &amp;rarr; 합집합
SDIFF  tags1 tags2       &amp;rarr; 차집합&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주요 사용처 : 좋아요 누른 사용자 목록(중복방지), 팔로워 목록, 태그&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. Sorted Set (ZSet) 점수 있는 집합, 점수 기준 자동 정렬 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Set인데 각 요소에 score(점수)가 있고 score 기준으로 자동 정렬된다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434309766&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ZADD   ranking  100  &quot;post:42&quot;
ZADD   ranking  200  &quot;post:7&quot;
ZADD   ranking  50   &quot;post:1&quot;

ZINCRBY  ranking  1  &quot;post:42&quot;     &amp;rarr; post:42 score = 101

ZREVRANGE ranking 0 2 WITHSCORES
&amp;rarr; [post:7(200), post:42(101), post:1(50)]  (높은 순)

ZRANGE ranking 0 2 WITHSCORES
&amp;rarr; [post:1(50), post:42(101), post:7(200)]  (낮은 순)

ZSCORE  ranking  &quot;post:42&quot;         &amp;rarr; 101
ZRANK   ranking  &quot;post:42&quot;         &amp;rarr; 1  (0부터 시작, 낮은 순 기준 순위)
ZREVRANK ranking &quot;post:42&quot;         &amp;rarr; 1  (높은 순 기준 순위)&lt;/code&gt;&lt;/pre&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;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6. HyperLogLog 고유 방문자 수 추정 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 값은 아니지만 메모리 극소로 고유 방문자 수를 추정하는 특수자료구조&lt;/p&gt;
&lt;pre id=&quot;code_1779434353654&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PFADD  visitors  &quot;user:1&quot;
PFADD  visitors  &quot;user:2&quot;
PFADD  visitors  &quot;user:1&quot;   &amp;rarr; 중복 무시

PFCOUNT visitors             &amp;rarr; 2 (&amp;plusmn;0.81% 오차)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확도는 99.19%인데 메모리는 항상 최대 12KB만 쓴다 정확한 숫자가 필요 없고 대략적인 UV(순방문자)만 필요할 때 쓴다&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;/ul&gt;
&lt;h2 id=&quot;redis-ttl&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;TTL (Time To Live)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL은 데이터의 유효 기간이다 설정한 시간이 지나면 Redis가 해당 키를 자동으로 삭제한다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434424684&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET  token  &quot;abc123&quot;
EXPIRE  token  3600         &amp;rarr; 3600초(1시간) 후 삭제

# 또는 처음부터 같이 설정
SET  token  &quot;abc123&quot;  EX  3600

TTL  token              &amp;rarr; 3595  (남은 시간 초 단위)
TTL  token              &amp;rarr; -1    (TTL 없음, 영구)
TTL  token              &amp;rarr; -2    (키 존재하지 않음)

PERSIST  token          &amp;rarr; TTL 제거 (영구 저장으로 변경)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주요 사용처 : JWT 리프레시 토큰, 이메일 인증코드 , 임시 캐시&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;redis-persistence&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;영속성 (Persistence) - RDB vs AOF&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis가 RAM에 저장하면 서버 재시작할 때 데이터 날아가는 거 아닌가싶은데 맞다 근데 Redis는 이걸 보완하는 두가지 방식을 제공하고 있다&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. RDB (Redis Database Snapshot)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 시점의 데이터를 통째로 스냅샷을 찍어서 디스크에 저장하는 방식이다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434513457&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# redis.conf 설정
save 900 1      &amp;rarr; 900초 안에 1번 이상 변경되면 저장
save 300 10     &amp;rarr; 300초 안에 10번 이상 변경되면 저장
save 60 10000   &amp;rarr; 60초 안에 10000번 이상 변경되면 저장&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점 : 파일 하나로 전체 복원가능, 성능 영향 적음&lt;/li&gt;
&lt;li&gt;단점 : 마지막 스냅샷 이후 데이터 유실 가능 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. AOF&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 쓰기 명령어를 로그 파일에&amp;nbsp; 순서대로 기록하는 방식이다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434547935&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# redis.conf 설정
appendonly yes
appendfsync everysec   &amp;rarr; 1초마다 디스크에 sync&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점 : 데이터 유실이 최대 1초치로 줄어든다&amp;nbsp;&lt;/li&gt;
&lt;li&gt;단점 : 파일이 계속 커진다, RDB보다 복원 느림&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 어떻게 쓰냐&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; RDB &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; AOF &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;데이터 유실&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;최대 수 분&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;최대 1초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;성능 영향&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;적음&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;약간 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;복원 속도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;빠름&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;느림&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 &lt;b&gt;RDB + AOF 둘 다 켜는 경우&lt;/b&gt;가 많다&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;redis-single-thread&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;싱글스레드인데 왜 빠른가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 명령어 처리를 싱글 스레드로 한다 빠른 이유는 3가지&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;RAM 접근 자체가 빠르다 아무리 싱글 스레드여도 RAM에서 읽고 쓰는 속도가 디스크보다 압도적으로 빨라서다&lt;/li&gt;
&lt;li&gt;I/O Multiplexing 네트워크 연결 자체는 멀티 플렉싱으로 여러 클라이언트가 동시에 처리한다&amp;nbsp;&lt;br /&gt;명령어 실행만 싱글 스레드(INCR, GET, ZADD 같은 실제 Redis 명령어를 실행하는 건 싱글스레드로 순서대로 하나씩 처리)&lt;/li&gt;
&lt;li&gt;싱글 스레드가 오히려 장점이다 멀티 스레드면 락, 컨텍스트 스위칭 오버헤드가 생긴다 싱글스레드는 이 오버헤드가 없고 명령어가 순차처리되니까 원자성이 자동으로 보장된다&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1779434777346&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;동시에 1000명이 INCR post:views:42 실행
&amp;rarr; Redis 내부: 1번, 2번, 3번 ... 순서대로 처리
&amp;rarr; 결과: 정확히 1000 증가, Lost Update 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 이걸 하려면 락이 필요한데 Redis는 싱글스레드 덕분에 락 없이 원자성을 보장한다&lt;/p&gt;
&lt;h2 id=&quot;redis-cache-strategy&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;캐싱 전략 4가지&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 캐시로 쓸 때 어떤 패턴으로 읽고 쓸 건지 결정해야 한다&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Cache Aside (Look Aside)&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 직접 Redis와 DB를 관리하는 패턴이다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434845667&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;읽기:
1. Redis에서 먼저 조회
2. 있으면 (Cache Hit) &amp;rarr; Redis 값 반환
3. 없으면 (Cache Miss) &amp;rarr; DB 조회 &amp;rarr; Redis에 저장 &amp;rarr; 반환

쓰기:
1. DB에 바로 씀
2. Redis 캐시 삭제 (다음 읽기 때 DB에서 다시 로드)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점 : Redis 장애 시 DB로 fallback 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점 : Cache Miss 첫 요청은 느리며 DB와 Redis 일관성을 직접 관리해야 한다&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. Read Through &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시가 DB 앞에서 프록시처럼 동작한다 앱은 항상 캐시에만 요청한다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434941600&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;앱 &amp;rarr; Redis 요청
        Cache Hit  &amp;rarr; 반환
        Cache Miss &amp;rarr; Redis가 알아서 DB 조회 &amp;rarr; 저장 &amp;rarr; 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache Aside 랑 비슷한데 DB 조회 책임이 앱이 아니라 캐시 레이어에 있다&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. Write Through &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 쓸 때 Redis에도 동시에 쓴다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779434982966&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;앱 &amp;rarr; Redis 쓰기 &amp;rarr; DB 쓰기 (동기)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점 : 항상 Redis = DB 일관성 보장한다&lt;/li&gt;
&lt;li&gt;단점 : 쓸 때마다 두 곳에 쓰니까 느리다 안 읽히는데 데이터도 캐시에 쌓인다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. Write Back (Write Behind) &amp;mdash; 인기수에 쓰는 패턴 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에 먼저 쓰고 나중에 배치로 DB에 반영한다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779435039955&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;앱 &amp;rarr; Redis만 씀 (즉시)
        &amp;darr;
스케줄러 (주기적으로)
        &amp;rarr; Redis 값 읽어서 DB 반영&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장점 : DB 쓰기 횟수 대폭 감소, 고빈도 쓰기에 최적&lt;/li&gt;
&lt;li&gt;단점 : Redis 장애 시 미반영 데이터 유실 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;redis-eviction&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;메모리 관리 (Eviction Policy)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 RAM을 쓴다 RAM은 무한하지 않으니까 꽉 차면 어떻게 할 건지 정책을 설정해야 한다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779435113260&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# redis.conf
maxmemory 256mb              &amp;rarr; 최대 256MB 사용
maxmemory-policy allkeys-lru &amp;rarr; 꽉 차면 LRU 기준으로 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Eviction Policy 종류&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 정책 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 설명 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;noeviction&lt;/td&gt;
&lt;td&gt;꽉 차면 에러 반환 (기본값)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;allkeys-lru&lt;/td&gt;
&lt;td&gt;전체 키 중 최근에 안 쓴 거 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;volatile-lru&lt;/td&gt;
&lt;td&gt;TTL 있는 키 중 최근에 안 쓴 거 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;allkeys-random&lt;/td&gt;
&lt;td&gt;전체 키 중 랜덤 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;volatile-ttl&lt;/td&gt;
&lt;td&gt;TTL 가장 짧은 거 먼저 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 용도면 allkeys-lru 가 일반적이며 자주 쓰는 데이터는 남고, 안 쓰는 데이터는 알아서 지워진다&lt;/p&gt;
&lt;h2 id=&quot;redis-spring-boot&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Boot에서 Redis 사용하기&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1779435164932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-data-redis'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 의존성만 추가해도 자동으로 RedisTemplate 빈을 만들어준다 (Auto Configuration)&lt;/p&gt;
&lt;pre id=&quot;code_1779435170726&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  data:
    redis:
      host: localhost
      port: 6379&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring Boot에서는 Redis 의존성과 접속 정보를 설정한 뒤 RedisTemplate 등을 통해 Redis를 사용할 수 있다&lt;/span&gt;&lt;br /&gt;&lt;span&gt;중요한 것은 Redis를 단순 캐시로만 보는 것이 아니라, 조회수 카운터, 세션 저장, 인기글 랭킹처럼 목적에 맞는 &lt;b&gt;자료구조와 캐싱 전략을 선택&lt;/b&gt;하는 것이다&lt;/span&gt;&lt;/p&gt;</description>
      <category>Backend/  JPA &amp;middot; DB</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/335</guid>
      <comments>https://winwin0219.tistory.com/335#entry335comment</comments>
      <pubDate>Fri, 22 May 2026 18:11:12 +0900</pubDate>
    </item>
    <item>
      <title>[Troubleshooting]FULLTEXT 인덱스 누락</title>
      <link>https://winwin0219.tistory.com/334</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;부하테스트 시나리오 B &lt;/span&gt;개선하여 merge 후, 운영 환경에서 게시글 검색 API 호출 시 500이 떴다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/hansolChoi29/CoreBoard/blob/main/docs/performance/performance/scenario-b-search/bottleneck-analysis.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt; (시나리오 B 분석 내용은 여기서 확인할 수 있다) &lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;GET &amp;lt;https://api.coreboard-api.xyz/posts/search?keyword=image&amp;amp;page=0&amp;amp;size=10&amp;gt; 500 (Internal Server Error)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글 전체 조회는 정상이었기 때문에 프론트 요청 문제는 아니었다 검색 API 내부에서 터지는 거라고 판단했다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 분석 - 원인 좁히기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 쿼리를 먼저 확인했다&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;MATCH(p.title, p.content) AGAINST(:keyword IN BOOLEAN MODE)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MATCH ... AGAINST 구문은 대응하는 FULLTEXT 인덱스가 있어야 동작한다 운영 DB 인덱스를 확인했다&lt;/p&gt;
&lt;pre id=&quot;code_1779363673266&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW INDEX FROM post;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;PRIMARY
uk_post_title
FK7ky67sgi7k0ayf22652f7763r
idx_post_board_status_created_at&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FULLTEXT 인덱스가 없었다 원인은 여기였다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 원인&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서는 title, content를 대상으로 전문 검색을 하도록 짜여 있었는데, 운영 DB에는 그에 대응하는 인덱스가 없었다&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;: MATCH(title, content) AGAINST(...)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 DB&lt;/b&gt;: FULLTEXT(title, content) 인덱스 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과&lt;/b&gt;: 쿼리 실행 실패 &amp;rarr; 500&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 DB에서 먼저 검증했다&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE FULLTEXT INDEX ft_post_title_content
ON post (title, content);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779363775057&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -i &quot;http://localhost:8080/posts/search?keyword=test&amp;amp;page=0&amp;amp;size=10&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779363779248&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 200&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방향이 맞는 걸 확인했으니, 운영 DB를 직접 수정하는 대신 Flyway migration 파일로 반영했다&lt;/p&gt;
&lt;pre id=&quot;code_1779363789466&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src/main/resources/db/migration/V20260521_02__add_fulltext_index_post_title_content.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 파일(Flyway)을 생성하여 아래 코드를 넣어주었다&lt;/p&gt;
&lt;pre id=&quot;code_1779363794839&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE FULLTEXT INDEX ft_post_title_content
ON post (title, content);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 후 migration이 정상 적용됐는지 확인했다&lt;/p&gt;
&lt;pre id=&quot;code_1779363804690&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT installed_rank, version, description, success
FROM flyway_schema_history
ORDER BY installed_rank DESC
LIMIT 5;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779363813023&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;installed_rank: 3
version: 20260521.02
description: add fulltext index post title content
success: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스도 확인했다&lt;/p&gt;
&lt;pre id=&quot;code_1779363821391&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW INDEX FROM post WHERE Key_name = 'ft_post_title_content';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부랑 외부 둘 다 확인했다&lt;/p&gt;
&lt;pre id=&quot;code_1779363829692&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -i &quot;http://localhost:8080/posts/search?keyword=test&amp;amp;page=0&amp;amp;size=10&quot;
curl -i &quot;https://api.coreboard-api.xyz/posts/search?keyword=test&amp;amp;page=0&amp;amp;size=10&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779363839073&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;둘 다 
HTTP/1.1 200&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 성과 및 배운 점&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div data-test-render-count=&quot;1&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-is-streaming=&quot;false&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 코드가 멀쩡해도 운영 DB 스키마가 따라오지 않으면 500이 난다 로컬에서는 FULLTEXT 인덱스를 직접 만들어서 쓰고 있었으니 당연히 로컬에선 안 터졌다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 운영 DB를 직접 건드리는 대신 Flyway migration으로 남겨두면, 나중에 새 환경에 배포해도 동일한 스키마가 자동으로 재현된다. 이번에 Flyway를 도입한 이유가 바로 이거다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;검색 500 확인
&amp;rarr; 검색 쿼리 확인
&amp;rarr; 운영 DB 인덱스 확인
&amp;rarr; FULLTEXT 인덱스 누락 확인
&amp;rarr; 로컬 검증
&amp;rarr; Flyway migration으로 반영
&amp;rarr; 배포 후 migration 적용 확인
&amp;rarr; 내부/외부 200 확인&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-state=&quot;closed&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-state=&quot;closed&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div data-state=&quot;closed&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div data-state=&quot;closed&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/334</guid>
      <comments>https://winwin0219.tistory.com/334#entry334comment</comments>
      <pubDate>Thu, 21 May 2026 20:49:34 +0900</pubDate>
    </item>
    <item>
      <title>[Troubleshooting]Flyway 도입 후 기존 운영 DB에서 배포 실패 해결</title>
      <link>https://winwin0219.tistory.com/333</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/332&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://winwin0219.tistory.com/332&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1779362236670&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[DB] Flyway &amp;mdash; ddl-auto=update와 무엇이 다른가&quot; data-og-description=&quot;Flyway란?Spring Boot 프로젝트를 운영하다 보면 DB 스키마가 변할 일이 생긴다 post 테이블에 view_count 추가users 테이블에 인덱스 추가 새로운 attachment 테이블 생성이걸 개발자가 직접 운영 DB에 접속해&quot; data-og-host=&quot;winwin0219.tistory.com&quot; data-og-source-url=&quot;https://winwin0219.tistory.com/332&quot; data-og-url=&quot;https://winwin0219.tistory.com/332&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bi4Zjz/dJMb9kT8MPC/ZS0xrxihyFhsjfH3iXlgZ1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/c0Ewpa/dJMb9llcXy9/BZwKQEAsPDcx1gWTmGAIZ0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/332&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://winwin0219.tistory.com/332&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bi4Zjz/dJMb9kT8MPC/ZS0xrxihyFhsjfH3iXlgZ1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/c0Ewpa/dJMb9llcXy9/BZwKQEAsPDcx1gWTmGAIZ0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[DB] Flyway &amp;mdash; ddl-auto=update와 무엇이 다른가&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Flyway란?Spring Boot 프로젝트를 운영하다 보면 DB 스키마가 변할 일이 생긴다 post 테이블에 view_count 추가users 테이블에 인덱스 추가 새로운 attachment 테이블 생성이걸 개발자가 직접 운영 DB에 접속해&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;winwin0219.tistory.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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 문제&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions를 통해 CoreBoard를 GCP 서버에 배포하던 중, 배포 마지막 단계에서 실패가 발생했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드와 jar 업로드는 완료되었지만, 서버 재시작 이후 healthcheck 단계에서 localhost:8080에 연결하지 못했다&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;curl: (7) Failed to connect to localhost port 8080 after 0 ms: Couldn't connect to server
Waiting for CoreBoard... (1/60)
Waiting for CoreBoard... (2/60)
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Spring Boot가 아직 8080 포트를 열지 못한 문제처럼 보였다&amp;nbsp;하지만 localhost:8080 연결 실패는 원인이 아니라 결과였다&amp;nbsp;&lt;/p&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 Boot 애플리케이션이 정상 기동하지 못하고 있던 것이었다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 분석 - 원인 좁히기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP 서버에 접속해 로그를 확인했다&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;sudo systemctl status coreboard --no-pager
sudo journalctl -u coreboard -n 100 --no-pager&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot가 Flyway 초기화 단계에서 죽고 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;Found non-empty schema(s) `coreboard` but no schema history table.
Use baseline() or set baselineOnMigrate to true to initialize the schema history table.&lt;/code&gt;&lt;/pre&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;coreboard DB에는 이미 테이블이 있다&lt;/li&gt;
&lt;li&gt;근데 flyway_schema_history는 없다&lt;/li&gt;
&lt;li&gt;Flyway 입장에서는 이 DB가 어떤 상태인지 알 수 없다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;[실제 흐름]&lt;/b&gt;&lt;br /&gt;기존 운영 DB에 이미 테이블 존재 &lt;br /&gt;&amp;rarr; Flyway 신규 도입 &lt;br /&gt;&amp;rarr; flyway_schema_history 없음 &lt;br /&gt;&amp;rarr; Flyway가 DB 상태 판단 불가 &amp;rarr; 초기화 실패 &lt;br /&gt;&amp;rarr; Spring Boot 기동 실패 &lt;br /&gt;&amp;rarr; 8080 포트가 열리지 않음 &lt;br /&gt;&amp;rarr; GitHub Actions healthcheck 실패&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한&amp;nbsp;VM 사양이 작다 보니 가끔 프로세스가 죽는 일이 있어서 Restart=always를&amp;nbsp;걸어뒀는데,&amp;nbsp;이번엔&amp;nbsp;Flyway&amp;nbsp;오류로&amp;nbsp;죽는&amp;nbsp;거라 &lt;br /&gt;5초마다 재시작 &amp;rarr; 같은 오류 &amp;rarr; 또 죽는 루프가 반복됐다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;로그가 계속 새로 쌓인 이유가 그것이었다&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;Restart=always
RestartSec=5&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 원인&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 운영 DB에 Flyway를 뒤늦게 도입하면서 기준점이 없었던 것이 문제였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 DB에는 이미 테이블이 있었다&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;board
post
users
comments
attachment
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway를 처음 도입했으니 당연히 flyway_schema_history는 없었으며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway가 이 상태에서 임의로 migration을 실행하면 기존 테이블과 충돌하기 때문에, 안전하게 실행을 멈춘 것이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/etc/coreboard.env에 baseline 설정을 추가했다&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;SPRING_FLYWAY_BASELINE_ON_MIGRATE=true
SPRING_FLYWAY_BASELINE_VERSION=0&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 설정 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;의미&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;SPRING_FLYWAY_BASELINE_ON_MIGRATE=true&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;기존 테이블이 있는 DB를 Flyway 관리 대상으로 등록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;SPRING_FLYWAY_BASELINE_VERSION=0&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;현재 운영 DB 상태를 0번 기준점으로 간주&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;baseline은 이미 테이블이 있는 DB에 Flyway를 처음 도입할 때만 필요한 설정이다 로컬은 Flyway 도입 시점에 새로 구성했기 때문에 해당하지 않아서 운영 서버에서만 따로 관리하기로 판단했다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;sudo systemctl restart coreboard&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이후 Flyway 문제는 해결되었지만, 2차 문제가 있었다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP 저사양 VM에서 Spring Boot 기동 시간이 너무 길었다&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;Started CoreBoardApplication in 323.497 seconds&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 5분이 걸렸다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 healthcheck는 최대 10분을 기다리도록 짜여 있었는데, ssh-action 자체 실행 제한 시간과 맞물리면 실패 로그조차 제대로 못 보고 끊길 수 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;command_timeout과 healthcheck 횟수를 함께 늘렸다&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;command_timeout: 20m&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;for i in {1..90}; do
  if curl -fsS &amp;lt;http://localhost:8080/actuator/health&amp;gt; &amp;gt; /dev/null; then
    echo &quot;CoreBoard is healthy&quot;
    exit 0
  fi

  echo &quot;Waiting for CoreBoard... ($i/90)&quot;
  sleep 10
done&lt;/code&gt;&lt;/pre&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;healthcheck 최대 대기 시간: 90회 &amp;times; 10초 = 15분&lt;/li&gt;
&lt;li&gt;ssh-action&amp;nbsp;최대&amp;nbsp;실행&amp;nbsp;시간:&amp;nbsp;20분&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 200 반환. 배포 성공.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 성과 및 배운 점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로는 healthcheck 실패처럼 보였지만 &lt;b&gt;실제 원인&lt;/b&gt;은 &lt;b&gt;Flyway baseline 설정 누락&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;이번에 확인한 것들&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기존 운영 DB에 &lt;b&gt;Flyway를 뒤늦게 도입할 때는 baseline 설정이 필요하다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub Actions healthcheck 실패는 원인이 아니라 결과일 수 있다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 실패 시&lt;/b&gt; Actions 로그만 보면 안 된다 &lt;b&gt;서버의 journalctl까지&lt;/b&gt; 봐야 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Restart=always&lt;/b&gt;는 운영 복구엔 유용하지만 반복 실패 상황에서는 &lt;b&gt;로그 분석을 어렵게 만든다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;healthcheck 대기 시간은&lt;/b&gt; 로컬 기준이 아니라 &lt;b&gt;실제 운영 서버 기준으로 잡아야 한다&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;shell&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;배포 실패
&amp;rarr; journalctl 확인
&amp;rarr; Flyway 초기화 실패
&amp;rarr; baseline 설정 추가
&amp;rarr; healthcheck 대기 시간 조정
&amp;rarr; 배포 성공&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/333</guid>
      <comments>https://winwin0219.tistory.com/333#entry333comment</comments>
      <pubDate>Thu, 21 May 2026 20:35:09 +0900</pubDate>
    </item>
    <item>
      <title>[DB] Flyway &amp;mdash; ddl-auto=update와 무엇이 다른가</title>
      <link>https://winwin0219.tistory.com/332</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Flyway란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트를 운영하다 보면 DB 스키마가 변할 일이 생긴다&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;post 테이블에 view_count 추가&lt;/li&gt;
&lt;li&gt;users 테이블에 인덱스 추가&amp;nbsp;&lt;/li&gt;
&lt;li&gt;새로운 attachment 테이블 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 개발자가 직접 운영 DB에 접속해서 SQL 날리면 어떻게 될까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;누가 언제 어떤 SQL을 실행했는지 기록이 없다&amp;nbsp;&lt;/li&gt;
&lt;li&gt;로컬 DB랑 운영 DB랑 구조가 달라지는 시점도 모른다&amp;nbsp;&lt;/li&gt;
&lt;li&gt;팀이면 더 심각해진다 (누군가 이미 실행한 걸 또 실행할 수도 있다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway는 이 문제를 해결하는 도구이며 SQL 파일을 버전 번호로 관리한다&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;V1__create_post_table .sql&lt;/li&gt;
&lt;li&gt;V2__add_view_count_column.sql&lt;/li&gt;
&lt;li&gt;V3__add_post_title_index.sql&lt;/li&gt;
&lt;/ul&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;b&gt;ddl-auto = update가 하는 일&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate가 엔티티 클래스를 보고 이 컬럼이 없네? 추가해줄게하고 자동으로 ALTER TABLE을 날린다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779360350043&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 엔티티에 필드 추가
private String thumbnailUrl;

// Hibernate가 알아서 
ALTER TABLE post ADD COLUMN tumbnail_url VARCHAR(255);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제가 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ddl-auto = update의 치명적인 한계&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 컬럼 삭제를 못한다&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1779360404971&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이 필드를 지우면
private String thumbnailUrl; // 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate는 컬럼을 절대 DROP하지 않는다 안전 때문에 설계 자체가 그러한데, 그래서 엔티티에서 필드를 지워도 DB 컬럼은 그대로 남는다 운영 DB가 서서히 엔티티랑 달라지기 시작한다&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 데이터 변환 로직을 못 쓴다&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 이런 작업이 필요하다고 가정해보자&lt;/p&gt;
&lt;pre id=&quot;code_1779360516042&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- status 컬럼 타입을 VARCHAR -&amp;gt; INT로 바꾸면서 
-- 기존 데이터도 변환해야 한다 
UPDATE post SET status_code = CASE
	WHEN status = 'ACTIVE' THEN 1
	WHEN status = 'DELETED' THEN 0
END;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate는 이런 데이터 마이그레이션 로직을 실행할 수가 없다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 타입 바꿨다고 기존 데이터를 알아서 변환해주진 않는다&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 어떤 변경이 언제 실행됐는지 기록이 없다&amp;nbsp;&lt;/b&gt;&lt;/h4&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;A가 배포함 &amp;rarr; 컬럼 추가됨&lt;/li&gt;
&lt;li&gt;B가 배포함 &amp;rarr; 또 다른 컬럼 추가됨&lt;/li&gt;
&lt;li&gt;근데 로컬 DB에는 어떤 게 적용됐는지 아무도 모른다&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway는 flyway_schema_history에 전부 기록되니까 지금 DB가 어떤 상태인지 명확하게 알 수 있다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 운영 환경에서 udpate 자체가 위험하다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예기치 않는 DDL이 운영 DB에 자동으로 날아가는 구조다 그래서 실무에서는 운영 환경 ddl-auto는 무조건 none 또는 validate로 설정하고 스키마 변경은 반드시 직접 검토한 SQL로만 한다&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;ddl-auto = update&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; Flyway &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;컬럼 추가&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;자동&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;SQL로 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;컬럼 삭제&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;안 됨&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;SQL로 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;데이터 변환&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;안 됨&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;SQL로 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;변경 이력 관리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;없음&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;history 테이블&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;운영 안전성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;위험&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&amp;nbsp;안전&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 ddl-auto = update는 로컬 개발 편의용이며 Flyway는 운영 DB 변경을 안전하게 관리하는 도구다&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;b&gt; flyway_schema_history 테이블&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway가 어디까지 실행했는지 기억하는 방법이 있어야한다 그 기억 장소가 바로 flyway_schema_history 테이블.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway가 처음 실행되면 이 테이블을 DB에 만들고 SQL 파일을 실행될 때마다 아래처럼 기록한다&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; version &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; description &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; success &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;create post table&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;add view count column&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에 애플리케이션을 재시작하면 Flyway가 이 테이블을 보고 V1, V2는 이미 실행됐으니까 V3부터 실행하면 되겠다~고 판단한다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;만일 기존 운영 DB에 Flyway를 뒤늦게 도입하면 어떻게 될까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제가 생긴다 (참고로 내가 겪은 문제다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 Flyway를 쓴 게 아니라 중간에 도입하면 운영 DB 안에는 이미 board, post, users같은 테이블이 있다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 flyway_schema_history 테이블은 없다 (한 번도 Flyway를 쓴 적이 없으니까)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway 입장에서 생각해보면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB이 이미 있네?&lt;/li&gt;
&lt;li&gt;근데 내가 관리한 기록이 없잖아?&lt;/li&gt;
&lt;li&gt;아니 대체 이게 V1 이전 상태야? V3 이후 상태야? 나는 알 수가 없어!&lt;/li&gt;
&lt;li&gt;만약 내가 V1 SQL을 실행하면 이미 존재하는 테이블에 또 CREATE TABLE하는 건데 그럼 에러 터지잖아!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Flyway는 나는 모르겠으니가 실행 안 할게~하고 멈춰버린다&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;baseline이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황을 해결하는 Flyway옵션이 바로 baseline이다&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;지금 이 DB 상태를 Flyway의 시작점 (0번)으로 인정해줘&lt;/li&gt;
&lt;li&gt;여기서부터 내가 관리할게&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1779361065356&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SPRING_FLYWAY_BASELINE_ON_MIGRATE=true   &amp;rarr; 기존 DB를 시작점으로 인정해줘
SPRING_FLYWAY_BASELINE_VERSION=0         &amp;rarr; 그 시작점을 0번으로 등록해줘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게하면 Flyway는 flyway_schema_history 테이블을 생성하고 현재 상태를 0번으로 기록한 뒤 V1부터 관리를 시작한다&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;파일 네이밍 규칙과 위치&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway가 SQL 파일을 찾는 기본 경로는 아래와 같다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779361458349&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src/
└── main/
    └── resources/
        └── db/
            └── migration/        &amp;larr; 여기
                ├── V1__create_users_table.sql
                ├── V2__create_post_table.sql
                └── V3__add_view_count_column.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경로는 Flyway가 약속으로 정해놓은 기본 값이다 별도 설정 없이 이 경로에 파일을 넣으면&amp;nbsp; 자동으로 인식한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 이름 규칙도 정해져 있다 대표적으로 두 가지 방식이 있는데&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 순번 방식&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;V1__create_users_table.sql&lt;/li&gt;
&lt;li&gt;V2__create_post_table.sql&lt;/li&gt;
&lt;li&gt;V3__add_view_count_column.sql&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 날짜 기반 방식 &amp;nbsp;&amp;larr; 이게 내가 사용한 방식이다&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;V20260521_01__create_users_table.sql&lt;/li&gt;
&lt;li&gt;V20260521_02__add_fulltext_index_post_title_content.sql&lt;/li&gt;
&lt;li&gt;V20260522_01__add_attachment_table.sql&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 버전 번호는 V1, V2처럼 순번으로 쓸 수 있고 V20260521_01처럼 날짜 기반으로 쓸 수도 있다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 프로젝트에서는 날짜기반이 버전 충돌을 피하기 더 유리하다&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[순번 방식에서 충돌이 생기는 상황]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A랑 B가 같은 프로젝트에서 동시에 작업 중이라고 가정해보자&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779361841804&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;현재 최신 마이그레이션 : V4__...sql&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;A : 새 기능 개발 중 &amp;rarr; V5__add_alarm_table.sql 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B : 다른 기능 개발 중 &amp;rarr; V5__add_report_table.sql 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 V5로 만든 것. 각자 로컬에서는 문제 없이 돌아간다 근데 둘 다 머지하면&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779361855650&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;V5__add_alarm_table.sql
V5__add_report_table.sql
&amp;larr; 버전 번호가 같은 파일이 두 개&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flyway는 같은 버전 번호가 두 개면 오류 내고 실행 자체를 거부한다&amp;nbsp;&lt;/p&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;A:&amp;nbsp;V20260521_01__add_alarm_table.sql &lt;br /&gt;B:&amp;nbsp;V20260521_02__add_report_table.sql&lt;/p&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;. 물론 같은 날 같은 순번을 동시에 만드는 극단적인 경우는 있을 수 있는데, 순번 방식보다 충돌 가능성이 훨씬 낮다&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/  JPA &amp;middot; DB</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/332</guid>
      <comments>https://winwin0219.tistory.com/332#entry332comment</comments>
      <pubDate>Thu, 21 May 2026 20:11:22 +0900</pubDate>
    </item>
    <item>
      <title>[코테 공부] 잔디 심기로 코딩테스트 1일 1문제 습관 형성하기 (feat. 코드트리)</title>
      <link>https://winwin0219.tistory.com/331</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코테 때문에 훈련한다기 보단, 사고력을 기르려고 꾸준히 노력하고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 그 꾸준히가 잘 안 되는 성격이라서 &lt;a href=&quot;https://www.codetree.ai/ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;코드트리의 청약 챌린지&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;에 합류하게 되었다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;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 alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260520_203650.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I6E07/dJMcaciPRWp/CNyDpsrZC4IOVsYXs0146k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I6E07/dJMcaciPRWp/CNyDpsrZC4IOVsYXs0146k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I6E07/dJMcaciPRWp/CNyDpsrZC4IOVsYXs0146k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI6E07%2FdJMcaciPRWp%2FCNyDpsrZC4IOVsYXs0146k%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;860&quot; height=&quot;483&quot; data-filename=&quot;20260520_203650.png&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;483&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;예전에는 코딩테스트 공부를 할 때 문제 하나를 오래 붙잡다가 지치거나 며칠 쉬면 다시 시작하기 어려웠다 그런데 코드트리에서는 난리도와 챕터가 나뉘어 있어서 오늘은 1문제만 풀자처럼 작게 시작하기 좋다 문제를 많이 푸는 것보다 중요한 건 끊기지 않는 흐름이라고 느꼈다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>1일1코테</category>
      <category>개발자루틴</category>
      <category>코드트리</category>
      <category>코딩테스트</category>
      <category>코테공부</category>
      <category>코테독학</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/331</guid>
      <comments>https://winwin0219.tistory.com/331#entry331comment</comments>
      <pubDate>Wed, 20 May 2026 20:41:35 +0900</pubDate>
    </item>
    <item>
      <title>Bottleneck Analysis</title>
      <link>https://winwin0219.tistory.com/330</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;&lt;b&gt;서버 전체 &amp;rarr; 특정 API &amp;rarr; 특정SQL &amp;rarr; 특정 SQL 연산비용&lt;/b&gt; 순서로 좁혀가야 한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: right;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인 좁히는 순서&lt;/b&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1 단계 : 전체 서버 문제인지 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 전체가 느린가, 특정 API만 느린가?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 결과&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색목록 조회 API 평균 19초, p95 20초&lt;/li&gt;
&lt;li&gt;상세 조회 API p95 12ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 전체 서버 장애나 네트워크 문제라기 보다 검색 목록 조회 API에 병목이 집중되어있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 첫번째 관찰&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2단계 : 서버 자원 부족인지 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버자원 (CPU, JVM Memory, DB Connection) 부족인지 확인해야 한다&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;CPU 지속적으로 높지 않았음&lt;/li&gt;
&lt;li&gt;JVM Memory 급격한 증가없음&lt;/li&gt;
&lt;li&gt;DBPending Connection 0 커넥션 기다리는 요청 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(만약 CPU가 높은 수준으로 지속되지 않았고 Heap Memory 급증/OOM 징후도 없고 DB Pending Connection도 0이라면)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서버 자원 포화나 커넥션 풀 부족 보다는 특정 요청 내부의 처리 비용이 큰 상황으로 본다&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;b&gt;3단계 : 애플리케이션 로직 문제인지 DB 쿼리 문제인지 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 API 1번 호출 할 때 실제로 어떤 SQL이 나가는지 확인했다 Page 기반 조회라서 SQL이 2개 나간다&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;목록 조회 SQL(실제 게시글 데이터)&lt;/li&gt;
&lt;li&gt;count query (전체 몇 건인지 세는 SQL)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두개를 EXPLAIN으로 실행 계획을 봐야 한다&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 118px;&quot; border=&quot;1&quot; data-end=&quot;1554&quot; data-start=&quot;1373&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 확인 대상 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 보는 이유&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;1440&quot; data-start=&quot;1401&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1413&quot; data-start=&quot;1401&quot;&gt;목록 조회 SQL&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1440&quot; data-start=&quot;1413&quot;&gt;검색 조건이 어떻게 SQL로 나가는지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;1487&quot; data-start=&quot;1441&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1455&quot; data-start=&quot;1441&quot;&gt;count query&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1487&quot; data-start=&quot;1455&quot;&gt;Page 조회 때문에 전체 개수 계산이 붙는지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;1521&quot; data-start=&quot;1488&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1497&quot; data-start=&quot;1488&quot;&gt;SQL 개수&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1521&quot; data-start=&quot;1497&quot;&gt;N+1 또는 불필요한 반복 쿼리 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;1554&quot; data-start=&quot;1522&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1534&quot; data-start=&quot;1522&quot;&gt;SQL 실행 시간&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1554&quot; data-start=&quot;1534&quot;&gt;진짜 오래 걸리는 SQL 특정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4단계 : 실행 계획으로 DB 내부 비용 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL을 확인했으면 바로 EXPLAIN을 봐야 한다&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1873&quot; data-start=&quot;1702&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; EXPLAIN 항목 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 의미 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1753&quot; data-start=&quot;1732&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1738&quot; data-start=&quot;1732&quot;&gt;key&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1753&quot; data-start=&quot;1738&quot;&gt;어떤 인덱스를 썼는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1776&quot; data-start=&quot;1754&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1761&quot; data-start=&quot;1754&quot;&gt;type&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1776&quot; data-start=&quot;1761&quot;&gt;접근 방식이 괜찮은지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1800&quot; data-start=&quot;1777&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1784&quot; data-start=&quot;1777&quot;&gt;rows&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1800&quot; data-start=&quot;1784&quot;&gt;대략 몇 건을 검사할지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1846&quot; data-start=&quot;1801&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1809&quot; data-start=&quot;1801&quot;&gt;Extra&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1846&quot; data-start=&quot;1809&quot;&gt;filesort, temporary 같은 추가 비용이 있는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1873&quot; data-start=&quot;1847&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1858&quot; data-start=&quot;1847&quot;&gt;filtered&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1873&quot; data-start=&quot;1858&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;만약 검색 목록 조회 시 SQL 인덱스를 탔으나 row가 많고 Using temporary; Using filesort라면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr;&lt;span&gt; 인덱스 미사용 문제가 아니라 인덱스를 사용했음에도 검색 조건과 정렬 조건을 처리하기 위해 너무 많은 후보 행을 검사하고 정렬하는 문제다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;인덱스를 탔다 = 찾기 시작할 위치를 잡았다정도다 근데 다음부터가 문제다&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;인덱스가 board_id, status, created_at 순으로 되어있는데&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;board_id = 2&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;status = 'PUBLISHED'까지는 인덱스로 어느정도 범위를 잡았음 근데 그 결과가 70만건&lt;/span&gt;&lt;/span&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;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;5단계 : count qeury 비용도 같이 볼 것&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;Page 조회면 보통 목록 조회 쿼리만 나가는 게 아니라 count query도 나간다&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 응답 지연은 목록 조회 sql 하나만의 문제가 아니라 목록 조회 sql과 count query가 함께 많은 후보 행을 검사하면서 발생하는 것으로 판단한다 &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779247018675&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LOWER(title) LIKE '%test%' OR content LIKE '%test%'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건으로 걸러내야 하는데 LIKE '%test%'는 앞에 %가 붙어있어서 인덱스로 범위를 줄이는 게 불가능하다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 B-tree 인덱스는 앞에서부터 일치하는 거만 빠르게 찾을 수 있기 때문이다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test로 시작하는 거라면 인덱스로 찾겠는데 %test%는 어디서든 나올 수 있으니까 결국 다 뒤져야 한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록 조회는 ORDER BY created_at DESC 정렬이 있다 근데 인덱스 구조상 keyword 조건 처리 때문에 인덱스 순서 그대로 정렬 결과를 뽑는 게 안 된다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 DB가 따로 정렬 작업을 수행해야 한다 이게 &lt;b&gt;Using filesort&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;이름이 file이라서 파일에 저장하는 것처럼 보이는데 그냥 인덱스 순서만으로 정렬을 못 끝내고 별도 정렬 처리를 했다는 뜻이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 202px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 용어 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt; 뜻 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;병목&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;전체 흐름을 느리게 만드는 가장 느린 구간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;p95&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;요청 100개 중 느린 쪽 5개를 제외했을 때의 상위 응답 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Pending Connection&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;DB 커넥션을 못 받아서 기다리는 요청 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Count Query&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;전체 데이터 개수를 세는 쿼리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;EXPLAIN&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;DB가 쿼리를 어떻게 실행할지 보여주는 실행 계획&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;rows&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;DB가 검사할 것으로 예상하는 행 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;filesort&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;인덱스 순서만으로 정렬하지 못해 별도 정렬 작업을 수행하는 것&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;temporary&lt;/td&gt;
&lt;td style=&quot;height: 21px;&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;</description>
      <category>Backend</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/330</guid>
      <comments>https://winwin0219.tistory.com/330#entry330comment</comments>
      <pubDate>Wed, 20 May 2026 11:15:30 +0900</pubDate>
    </item>
    <item>
      <title>[코드트리] 반복문을 반복하며 중첩 반복문 약점 극복하기</title>
      <link>https://winwin0219.tistory.com/329</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 중첩반복문에 대해 한문제 풀 예정이다&lt;/p&gt;
&lt;h2 id=&quot;약수가-세-개인-수&quot; style=&quot;background-color: #ffffff; color: #2c2c2c; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;https://www.codetree.ai/ko/trails/complete/curated-cards/nl-pre-1d-loop-repetition-2/submissions?page=1&amp;amp;page_size=20&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;약수가 세 개인 수&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;[문제]&lt;/b&gt;&lt;br /&gt;두 정수 start와 end가 주어집니다.&lt;br /&gt;정수의 약수란, 그 수를 나누었을 때 나머지가 없이 떨어지는 양의 정수를 뜻합니다. 예를 들어,&amp;nbsp;6의 약수는&amp;nbsp;1,&amp;nbsp;2,&amp;nbsp;3,&amp;nbsp;6으로 총 네 개입니다.&lt;br /&gt;start&amp;nbsp;이상&amp;nbsp;end&amp;nbsp;이하인 정수 중에서, 약수가 정확하게 세 개인 수의 개수를 구하는 프로그램을 작성해보세요.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;[입력]&lt;/b&gt;&lt;br /&gt;첫 줄에 두 정수 start와 end가 공백으로 구분되어 주어집니다&lt;br /&gt;제한조건 : 1 &amp;lt;= start &amp;lt;= end &amp;lt;= 1000&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;[출력]&lt;/b&gt;&lt;br /&gt;첫 줄에 조건을 만족하는 정수의 개수를 출력합니다&amp;nbsp;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;[예제 설명]&lt;/b&gt;&lt;br /&gt;&lt;b&gt;예제 1&lt;/b&gt;에서, 조건을 만족하는 수는 4가 유일합니다&amp;nbsp;&lt;br /&gt;3의 약수 : {1, 3}&lt;br /&gt;4의 약수 : {1, 2, 4}&lt;br /&gt;5의 약수 : {1, 5}&lt;br /&gt;6의 약수 : {1, 2, 3, 6}&lt;br /&gt;7의 약수 : {1, 7}&lt;br /&gt;&lt;br /&gt;&lt;b&gt;예제 3&lt;/b&gt;에서, 조건을 만족하는 수는 4, 9, 25, 49로 총 4개가 있습니다&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;[입력 예제 1]&lt;/b&gt;&lt;br /&gt;입력 : 3 7&lt;br /&gt;출력 : 1&lt;br /&gt;&lt;b&gt;[입력 예제 2]&lt;/b&gt;&lt;br /&gt;입력 : 9 16&lt;br /&gt;출력 : 1&lt;br /&gt;&lt;b&gt;[입력 예제 3]&lt;/b&gt;&lt;br /&gt;입력 : 1 50&lt;br /&gt;출력 : 4&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[내가 한 풀이]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소수제곱 카운트하면되는 문제라는 거까지는 알아냈는데 아직은 간단한 for과 if문 정도만 할 줄 알아서 반복문으로 풀었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*소수제곱&lt;/b&gt; 즉, 2의 제곱, 3의 제곱, 5의 제곱 이런 식인 셈.&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;num을 start ~ end까지 하나씩 검사하기&lt;/li&gt;
&lt;li&gt;각 num마다 약수 개수를 저장할 divisorCount를 0으로 초기화하기&lt;/li&gt;
&lt;li&gt;i를 1~num까지 나눠보기&lt;/li&gt;
&lt;li&gt;num % i == 0이면 i는 num의 약수&lt;/li&gt;
&lt;li&gt;약수가 3개면 answer 증가&amp;nbsp;&lt;/li&gt;
&lt;li&gt;마지막에 answer 출력&lt;/li&gt;
&lt;/ol&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;1. 바깥 반복문 : start~end까지 숫자를 하나씩 꺼내기 위한 반복문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 안쪽 반복문 : 현재 숫자 num의 약수가 몇 개인지 세기 위한 반복문&lt;/p&gt;
&lt;pre id=&quot;code_1779019562298&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Scanner;

public class Main{
	public static void main(String[] args){
    	Scanner sc = new Scanner(System.in);
        int start = sc.nextInt();
        int end = sc.nextInt();
        int answer = 0;
        
        for(int num = start; num &amp;lt;= end; num++){
        	int divisorCount = 0;
            for(int i = 1; i &amp;lt;= num; i++){
            	if(num % i == 0){
                	divisorCount++;
                }
            }
            if(divisorCount == 3){
            	answer++;
            }
        }
    	System.out.println(answer);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;풀이하면서 헷갈렸던 점&lt;/b&gt;&lt;/h4&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;예를 들어 3의 약수 개수와 4의 약수 개수는 서로 다른 값이다 그래서 divisorCount는 바깥 반복문 안에서 매번 0으로 초기화해야 했다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;학습하면서 느낀 점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩 반복문 문제는 for문을 두 번 쓰는 문법이 아니라 반복해야 하는 일이 두 단계로 나뉘어 있을 때 사용하는 구조라는 것을 알게되었다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. start~end까지 숫자를 하나씩 확인하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 각 숫자마다 1부터 자기자신까지 나누어보며 약수 개수를 세는 것&lt;/p&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;&lt;a href=&quot;https://www.codetree.ai/ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;코드트리&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;의 커리큘럼은 기본 개념을 먼저 보고, 그 다음 비슷한 유형의 문제를 풀면서 직접 적용해 보는 구조라서 내가 어디서 막히는지 확인하기 좋았다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제를 통해 반복문 문제를 풀 때 바로 코드를 쓰기 보다 무엇을 반복해야 하는가를 나누어 생각해야 한다는 점을 배웠다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;앞으로 반복문 문제를 풀 때 다음 순서로 할 것&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 입값이 무엇인지 확인한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 반복해야 하는 범위가 어디인지 찾는다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 반복문 안에서 매번 초기화해야 하는 값이 있는지 확인한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 조건을 만족하면 어떤 값을 증가시킬지 정한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 출력 위치가 반복문 안인지 밖인지 확인한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>알고리즘 기초</category>
      <category>중첩반복문</category>
      <category>코드트리</category>
      <category>코딩테스트</category>
      <category>코테공부</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/329</guid>
      <comments>https://winwin0219.tistory.com/329#entry329comment</comments>
      <pubDate>Sun, 17 May 2026 21:15:45 +0900</pubDate>
    </item>
    <item>
      <title>[Transactional] 전파(Propagation) &amp;amp; 격리 수준(Isolation Level)</title>
      <link>https://winwin0219.tistory.com/327</link>
      <description>&lt;div class=&quot;toc-box&quot;&gt; 
 &lt;p class=&quot;toc-title&quot; 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;&lt;a href=&quot;#why-1&quot;&gt;트랜잭션의 기본 개념과 Spring 동작 방식&lt;/a&gt;&lt;/li&gt; 
  &lt;li&gt;&lt;a href=&quot;#why-2&quot;&gt;트랜잭션 전파: Propagation&lt;/a&gt;&lt;/li&gt; 
  &lt;li&gt;&lt;a href=&quot;#why-3&quot;&gt;주요 전파 옵션별 동작과 실무 사용 기준&lt;/a&gt;&lt;/li&gt; 
  &lt;li&gt;&lt;a href=&quot;#why-4&quot;&gt;트랜잭션 격리 수준: Isolation Level&lt;/a&gt;&lt;/li&gt; 
  &lt;li&gt;&lt;a href=&quot;#why-5&quot;&gt;실무 설계 기준&lt;/a&gt;&lt;/li&gt; 
  &lt;li&gt;&lt;a href=&quot;#why-6&quot;&gt;자주 하는 실수&lt;/a&gt;&lt;/li&gt; 
 &lt;/ol&gt; 
&lt;/div&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 트랜잭션의 기본 개념과 Spring 동작 방식&lt;/b&gt;&lt;/h2&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;트랜잭션이 뭔뒈!&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 안전벨트다 DB 작업 여러 개를 하나의 작업처럼 묶는 것이다&amp;nbsp;&lt;br&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;A 계좌에 만원 차감&lt;/li&gt;&lt;li&gt;B 계좌에 만원 추가&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이건 사실 DB입장에서는 작업이 2개다&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;A 계좌 잔액 update&lt;/li&gt;&lt;li&gt;B 계좌 잔액 update&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 1번은 성공하고 2번에서 서버가 터지면 A 돈은 빠졌는데 B 돈은 안 들어가는 대참사가 생긴다 그래서 트랜잭션이 필요하다&amp;nbsp;&lt;br&gt;둘 다 성공하면 commit, 하나라도 실패하면 rollback&lt;br&gt;&amp;nbsp;&lt;br&gt;@Transactional을 붙이면 Spring은 대충 이런 식으로 움직인다&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void transfer() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;accountA.minus(10000);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;accountB.plus(10000);
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;transfer() 호출 → Spring이 트랜잭션 시작 → A 계좌 차감 → B 계좌 증가 → 문제 없으면 commit. 문제 생기면 rollback&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 @Transactional은 DB 변경 작업의 생명줄이다&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;그렇다면 @Transactional 붙이면 무조건 안전할까?&lt;/b&gt;&lt;br&gt;정확히는 트랜잭션 범위 안에 들어온 DB 작업만 안전하게 묶인다 그리고 Spring은 보통 프록시 방식으로 @Transactional을 적용한다 Spring 공식문서는 프록시 모드에서는 프록시를 통해 들어오는 외부 메서드 호출만 가로챈다고 설명한다&amp;nbsp;&lt;br&gt;그래서 같은 클래스 안에서 자기 메서드를 직접 호출하면 트랜잭션이 기대대로 안 걸릴 수 있다&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class OrderService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void order() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pay(); // 같은 클래스 내부 호출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@Transactional
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public void pay() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 트랜잭션 걸릴 것 같지만 기대대로 안 걸릴 수 있음
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 조금 위험하다 왜냐하면 order()에서 pay()를 부를 때 Spring 프록시를 거치지 않고 그냥 자기 내부 메서드를 바로 호출하기 때문이다&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring 프록시 = 입구 보안요원 &lt;br&gt;외부에서 들어오면 보안요원이 체크함 &lt;br&gt;근데 건물 안에서 옆방으로 바로 가면 보안요원을 안 만남&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 @Transactional은 보통 public 서비스 메서드의 외부 호출 경계에 붙인다고 이해하면 된다&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 트랜잭션 전파: Propagation&lt;/b&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전파. 영어로 &lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;Propagation.&lt;/b&gt;&lt;/span&gt;&lt;br&gt;Propagation - 이미 트랜잭션이 있는데 너도 같이 탈래?&amp;nbsp;&lt;br&gt;어떤 메서드가 실행될 때, 이미 진행 중인 트랜잭션이 있다면 이 메서드는 그 트랜잭션에 같이 들어갈까?&lt;br&gt;아니면 자기만의 새 트랜잭션을 만들까?&lt;br&gt;아니면 트랜잭션 없이 실행할까?라고 이해하면 된다&lt;br&gt;예를들어&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void order() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;saveOrder();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pay();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendMessage();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 order()는 이미 트랜잭션을 시작했다 그런데 안에서 pay()를 호출한다&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void pay() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;paymentRepository.save(...);
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이때 질문은 이렇다 pay()는 order()가 만든 트랜션에 같이 들어갈까? 아니면 pay()만 따로 새 트랜잭션을 만들까?&lt;br&gt;이거를 결정하는 게 Propagation이다&amp;nbsp;&lt;br&gt;Spring 공식문서는 transactional propagation을 설명하면서 REQUIRED, REQUIRES_NEW, NESTED 같은 전파 옵션을 구분해서 설명한다&amp;nbsp;&lt;br&gt;특히 REQUIRES_NEW는 항상 독립적인 물리 트랜잭션을 사용하고 NESTED는 하나의 물리 트랜잭션 안에서 savepoint를 사용한다고 설명한다&amp;nbsp;&lt;/p&gt;&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;
 &lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt; 
 &lt;div class=&quot;moreless-content&quot;&gt;
  &lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt; 
  &lt;div class=&quot;moreless-content&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;실제 DB connection에서 돌아가는 진짜 트랜잭션&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;Spring 메서드 단위로 보이는 트랜잭션 범위&lt;/p&gt; 
   &lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; suspend란?&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;b&gt; savepoint란?&lt;/b&gt;&lt;/p&gt; 
   &lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 중간에 찍어두는 저장 지점&lt;/p&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Propagation 옵션&lt;/b&gt;을 여러 개 있지만 4개정도만 제대로 알면 된다&amp;nbsp;&lt;br&gt;1. &lt;b&gt;REQUIRED&lt;/b&gt; : 기본값 있으면 같이 타고, 없으면 새로 만든다&lt;br&gt;2. &lt;b&gt;REQUIRES_NEW&lt;/b&gt; : 무조건 새 트랜잭션을 만든다&lt;br&gt;3. &lt;b&gt;MANDATORY&lt;/b&gt; : 기존 트랜잭션이 반드시 있어야 한다&amp;nbsp;&lt;br&gt;4. &lt;b&gt;NESTED&lt;/b&gt; : 기존 트랜잭션 안에 중간 저장점 savepoint를 만든다&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 주요 전파 옵션별 동작과 실무 사용 기준&lt;/b&gt;&lt;/h2&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. REQUIRED는 기본값이다&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void order() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;saveOrder();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pay();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 사실&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRED)
public void order() {
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이거랑 같다고 보면 된다 이미 트랜잭션이 있으면 거기에 참여한다 없으면 새로 만든다&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 같이 한 배 타기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. REQUIRES_NEW 이름 그대로, 새 트랜잭션이 필요하다&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;logRepository.save(...);
}&lt;/code&gt;&lt;/pre&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;b&gt;[흐름]&lt;/b&gt;&lt;br&gt;order() 시작 &lt;br&gt;→ 트랜잭션 A &lt;br&gt;&lt;br&gt;시작 saveLog() 호출 &lt;br&gt;→ 트랜잭션 A 잠깐 멈춤 &lt;br&gt;→ 트랜잭션 B 새로 시작 &lt;br&gt;→ 로그 저장 &lt;br&gt;→ 트랜잭션 B commit &lt;br&gt;&lt;br&gt;order()로 복귀 &lt;br&gt;→ 트랜잭션 A 계속 진행&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;언제 쓸까?&lt;/b&gt;&lt;br&gt;대표적으로 로그 저장.&lt;br&gt;예를 들어 주문은 실패해서 rollback되어도 주문 실패 로그는 남기고 싶을 수 있다&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void order() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;paymentService.pay();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (Exception e) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;logService.saveFailLog(); // 이건 남기고 싶음
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw e;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이때 로그 저장이 REQUIRED면 주문 트랜잭션이 rollback될 때 로그도 같이 날아갈 수 있다&amp;nbsp;&lt;br&gt;그래서 로그 쪽을 이렇게 분리한다&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveFailLog() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;logRepository.save(...);
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 주문이 실패해서 rollback되어도 로그는 별도 트랜잭션에서 commit될 수 있다&lt;br&gt;하지만 조심해야 한다&amp;nbsp;&lt;br&gt;Spring 공식문서는 REQUIRES_NEW가 독립적인 물리 트랜잭션을 사용하며 내부 트랜잭션이 별도 DB connection을 얻기 때문에 connection pool 크기에 주의해야 한다고 설명한다&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;쉽게말하면, 바깥 트랜잭션이 DB 연결 1개 잡고 있음, 안쪽 REQUIRES_NEW가 DB 연결 1개 더 필요함&amp;nbsp;&lt;br&gt;그래서 동시 요청이 많으면 DB 연결이 부족해질 수 있다&lt;br&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 혼자 새 배 타기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. MANDATORY는 필수라는 뜻이다&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.MANDATORY)
public void someInnerWork() {
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;기존 트랜잭션이 반드시 있어야 하며 없으면 예외를 터뜨린다 즉, 이 메서드는 혼자 실행되면 안 된다는 뜻이다&lt;br&gt;비유해보자면, 이 작업은 반드시 큰 작업의 일부로만 실행되어야 한다 단독 실행 금지&lt;br&gt;&amp;nbsp;&lt;br&gt;예를 들어 어떤 내부 작업이 반드시 바깥 트랜잭션과 한 묶음이어야 한다면 MANDATORY를 고려할 수 있다&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 혼자 못 감. 반드시 누군가의 트랜잭션 안에서만 감&lt;/b&gt;&lt;/span&gt;&lt;br&gt;4. NESTED는 중첩이라는 뜻이다&amp;nbsp;&lt;br&gt;NESTED는 REQUIRES_NEW처럼 완전히 새 트랜잭션을 만드는 게 아니다&lt;br&gt;공식문서에 따르면 PROPAGATION_NESTED는 하나의 물리 트랜잭션 안에서 여러 savepoint를 사용하고 안쪽 범위만 부분 rollback 할 수 있게 한다&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;쉽게말하면, 전체 트랜잭션 안에 중간 저장 지점을 찍어둔다 안쪽 작업이 실패하면 그 저장 지점까지만 되돌린다&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;게임으로 비유하면&lt;br&gt;큰 미션 시작 → 중간 저장 → 작은 미션 실패 → 중간 저장 지점으로 돌아감 → 큰 미션은 계속 진행&lt;/blockquote&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void order() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;saveOrder();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;try {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;couponService.useCoupon();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} catch (Exception e) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 쿠폰 실패해도 주문은 계속 진행하고 싶음
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;saveResult();
}
@Transactional(propagation = Propagation.NESTED)
public void useCoupon() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;couponRepository.save(...);
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;order() 트랜잭션 A 시작&lt;br&gt;주문 저장 &lt;br&gt;&lt;br&gt;useCoupon() 호출 &lt;br&gt;→ 트랜잭션 A 안에 savepoint 생성 &lt;br&gt;→ 쿠폰 처리 실패 &lt;br&gt;→ savepoint까지만 rollback &lt;br&gt;&lt;br&gt;주문 작업 계속 &lt;br&gt;트랜잭션 A commit&lt;/blockquote&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;REQUIRES_NEW : 진짜 새 트랜잭션&lt;/li&gt;&lt;li&gt;NESTED : 같은 트랜잭션 안의 중간 저장점&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 바깥 트랜잭션이 최종 rollback되면? NESTED 안쪽 작업도 결국 같이 rollback된다 왜냐하면 같은 물리 트랜잭션 안에 있었기 때문이다&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 같은 배 안에서 중간 저장 지점 만들기&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 트랜잭션 격리 수준: Isolation Level&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Isolation Level : 동시에 DB를 볼 때, 어디까지 보여줄 거야? - 격리 수준&lt;br&gt;Propagation은 메서드끼리 트랜잭션을 어떻게 나눌지였다 Isolation은 완전히 다른 문제다&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;동시에 여러 사람이 DB를 읽고 쓰면 한 사람이 아직 확정하지 않은 변경을 다른 사람이 봐도 될까? &lt;br&gt;같은 데이터를 두 번 읽었는데 중간에 값이 바뀌어도 될까?&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Spring 공식문서도isolation을 이 트랜잭션이 다른 트랜잭션의 작업으로부터 얼마나 격리되는가?의 정도라고 설명한다&amp;nbsp;&lt;br&gt;예를 들어 다른 트랜잭션의 commit되지 않은 쓰기를 볼 수 있는가?같은 문제다&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;격리 수준을 이해하려면 먼저 동시에 일어나면 생기는 이상한 현상 3개를 봐야 한다&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. Dirty Read&amp;nbsp;&lt;/b&gt;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;트랜잭션 A: 상품 가격을 10,000원에서 1,000원으로 바꿈 &lt;br&gt;트랜잭션 A: 아직 commit 안 함 &lt;br&gt;&lt;br&gt;트랜잭션 B: 상품 가격을 읽음 &lt;br&gt;트랜잭션 B: 1,000원이라고 봄 &lt;br&gt;&lt;br&gt;트랜잭션 A: 아 실수였다. rollback&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 B는 실제로 확정된 적 없는 가격을 읽은 거다 이게 DirtyRead&lt;br&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 아직 확정 안 된 남의 임시 작업을 읽어버림&lt;/b&gt;&lt;/span&gt;&amp;nbsp;&lt;br&gt;이건 거의 허용하면 안 된다&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2. Non-repeatable Read&lt;/b&gt;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;트랜잭션 A 시작 &lt;br&gt;A: 상품 id=1 조회 → 가격 10,000원 &lt;br&gt;&lt;br&gt;트랜잭션 B 시작 &lt;br&gt;B: 상품 id=1 가격을 12,000원으로 수정 &lt;br&gt;B: commit &lt;br&gt;&lt;br&gt;A: 상품 id=1 다시 조회 → 가격 12,000원&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;A입장에서는 같은 트랜잭션 안에서 같은 상품을 읽었는데 값이 바뀌었다&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 같은 row를 다시 읽었는데 값이 달라짐&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;3. Phantom Read &lt;/b&gt;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;트랜잭션 A 시작 &lt;br&gt;A: 가격 10,000원 이상 상품 조회 → 5개 &lt;br&gt;&lt;br&gt;트랜잭션 B 시작 &lt;br&gt;B: 가격 15,000원 상품 추가 &lt;br&gt;B: commit &lt;br&gt;&lt;br&gt;A: 가격 10,000원 이상 상품 다시 조회 → 6개&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;A 입장에서는 처음에 없던 row가 갑자기 유령처럼 나타났다&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 같은 조건으로 조회했는데 행 개수가 달라짐&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; Isolation Level 옵션 &lt;/b&gt;&lt;/h4&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;READ_UNCOMMITTED&lt;/li&gt;&lt;li&gt;READ_COMMITTED&lt;/li&gt;&lt;li&gt;REPEATABLE_READ&lt;/li&gt;&lt;li&gt;SERIALIZABLE&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;아래 순서로 갈수록 격리 수준이 강해진다&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;READ_UNCOMMITTED &amp;lt; READ_COMMITTED &amp;lt; REPEATABLE_READ &amp;lt; SERIALIZABLE&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;격리 수준이 낮으면&lt;/b&gt; 동시 처리는 잘 되지만 이상한 읽기가 생길 수 있다&amp;nbsp;&lt;br&gt;반대로 &lt;b&gt;격리 수준이 높으면&lt;/b&gt; 데이터는 더 안전하지만 대신 대기, 충돌, 성능 비용이 커질 수 있다&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. READ_UNCOMMITTED &lt;/b&gt;&lt;br&gt;다른 트랜잭션이 commit 안 한 데이터도 읽을 수 있다 즉, DirtyRead가 가능하다&amp;nbsp;&lt;br&gt;남이 연필로 적어둔 임시 메모를 최종 결재 문서인 줄 알고 읽는것&lt;br&gt;근데 거의 안씀&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;2. READ_COMMITTED&lt;/b&gt;&lt;br&gt;다른 트랜잭션이 commit 한 데이터만 읽을 수 있다 DirtyRead는 막는다&amp;nbsp;&lt;br&gt;PostgreSQL 공식문서는 Read Committed가 PostgreSQL의 기본 격리 수준이며 이 수준에서 SELECT는 쿼리가 시작되기 전에 commit된 데이터만 보고 commit 되지 않는 데이터는 보지 않는다고 설명한다&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 Non-repeatable Read는 가능하다&amp;nbsp;&lt;br&gt;왜냐하면 같은 트랜잭션 안에서도 두 번째 SELECT를 실행할 때 그 사이에 다른 트랜잭션이 commit한 데이터는 보일 수 있기 때문이다&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;첫 번째 조회 시점에는 가격 10,000원 &lt;br&gt;그 후 누가 commit함 &lt;br&gt;두 번째 조회 시점에는 가격 12,000원&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 확정된 것만 읽긴 하는데 다시 읽으면 바뀔 수 있음&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;3. REPEATABLE_READ &lt;/b&gt;&lt;br&gt;같은 트랜잭션 안에서 같은 row를 다시 읽으면 같은 값이 보이도록 함&lt;br&gt;&amp;nbsp;&lt;br&gt;MySQL InnoDB 공식문서는 InnoDB가 4가지 격리 수준을 제공하며 기본 격리 수준은 REPEATABLE READ라고 설명한다&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;트랜잭션 A 시작 &lt;br&gt;A: 상품 id=1 조회 → 가격 10,000원 &lt;br&gt;&lt;br&gt;트랜잭션 B: &lt;br&gt;상품 id=1 가격 12,000원으로 수정 &lt;br&gt;commit &lt;br&gt;&lt;br&gt;A: 상품 id=1 다시 조회&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;REPEATABLE_READ에는 A가 여전히 10,000원을 볼 수 있다&amp;nbsp;&lt;br&gt;비유해보자면, 트랜잭션을 시작할 때 DB 사진을 한 장 찍어둠 근데 그 트랜잭션 안에서는 그 사진 기준으로 계속 봄&lt;br&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, 내 트랜잭션 안에서는 같은 row가 흔들리지 않게 함&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;4. SERIALIZABLE &lt;/b&gt;&lt;br&gt;이건 가장 강하다 동시에 실행되더라도 결과가 마치 하나씩 줄 세워 실행한 것처럼 보이게 한다&amp;nbsp;&lt;br&gt;비유해보자면, 사람들이 동시에 계산대에 몰려와도 직원이 한 명씩 줄 세워 처리하는 것&lt;br&gt;&amp;nbsp;&lt;br&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;대기 증가&amp;nbsp;&lt;/li&gt;&lt;li&gt;충돌 증가&amp;nbsp;&lt;/li&gt;&lt;li&gt;성능 저하 가능성&lt;/li&gt;&lt;li&gt;재시도 필요 가능성&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 그럼 가장 높은 SERIALIZABLE 쓰면 끝 아냐? &lt;/b&gt;&lt;br&gt;그건 아니다 격리 수준은 &lt;b&gt;약&lt;/b&gt; 같은 것&lt;br&gt;증상에 맞는 약을 써야지 무조건 제일 센 약을 매일 먹으면 몸이 망가진다 DB도 똑같다 데이터 정합성은 좋아질 수 있지만 동시에 처리할 수 있는 양이 줄고 대기가 늘 수 있다&amp;nbsp;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 136px;&quot; border=&quot;1&quot; data-ke-style=&quot;style9&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 14px;&quot;&gt;&lt;td style=&quot;height: 14px; text-align: center;&quot;&gt;&lt;b&gt; 구분 &lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 14px; text-align: center;&quot;&gt;&lt;b&gt; Propagation&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;b&gt;트랜잭션의 경계 문제 &lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 14px; text-align: center;&quot;&gt;&lt;b&gt; Isolation&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;b&gt;트랜잭션끼리의 거리두기 문제 &lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 21px;&quot;&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;한국말 느낌&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;전파&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;격리&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 21px;&quot;&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;핵심 질문&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;트랜잭션을 같이 쓸까, 새로 만들까?&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;동시에 실행될 때 남의 변경이 보일까?&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 21px;&quot;&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;보는 대상&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;메서드 호출 관계&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;DB 동시성 상황&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 21px;&quot;&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;대표 옵션&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;REQUIRED, REQUIRES_NEW, NESTED&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 21px;&quot;&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;초보자 비유&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;같은 배 탈지, 새 배 탈지&lt;/td&gt;&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;다른 사람이 작업 중인 종이를 볼 수 있는지&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 실무 설계 기준&lt;/b&gt;&lt;/h2&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void writeSomething() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 저장, 수정, 삭제
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;대부분은 이 경우 이정도로 시작한다 조회는 보통 이렇게 한다&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
public Something getSomething() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 조회
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 특별한 이유가 있을 때에만 propagation이나 isolation을 명시한다&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본값이 뭐지?&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;대략 이렇게 이해하면 된다&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;propagation = Propagation.REQUIRED,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;isolation = Isolation.DEFAULT
)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;REQUIRED는 Spring 기본값이고 DEFAULT는 DB 기본 격리 수준을 따르겠 다는 뜻이다&amp;nbsp;&lt;br&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;Spring 공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;의 @Transactional 설정 표에서도 isolation 기본값 DEFAULT,&lt;br&gt;propagation의 기본값은&amp;nbsp; REQUIRED로 정리되어있다&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;즉, 같은 Spring 코드라도 DB가 다르면 격리 동작이 다를 수 있다&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;언제 REQUIRED를 쓰는가?&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 작업 하나를 하나의 비즈니스 작업으로 묶고 싶을 때 사용한다&amp;nbsp;&lt;br&gt;아무 데나 막 붙여라가 아니라 하나의 비즈니스 작업 단위에 붙이라는 뜻이다&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 언제 REQUIRES_NEW를 쓰는가?&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;정말 독립적으로 commit되어야 하는 작업일 때 쓴다&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;감사 로그&lt;/li&gt;&lt;li&gt;알림 발송 기록&lt;/li&gt;&lt;li&gt;별도 이력 저장&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;근데 남발하면 안됨&lt;br&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;DB connection을 추가로 씀&lt;/li&gt;&lt;li&gt;장애 상황에서 일부 데이터만 남을 수 있음&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉 바깥 작업이 실패해서 rollback돼도 이 데이터는 반드시 남아야 하나?&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;그렇다면 REQUIRES_NEW 후보 아니면 대부분 REQUIRED&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;언제 Isolation을 직접 지정하나?&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
@Transactional(readOnly = true)&lt;/code&gt;&lt;/pre&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;/li&gt;&lt;li&gt;재고, 포인트, 잔액, 예약 수량처럼 틀리면 안 됨&lt;/li&gt;&lt;li&gt;같은 트랜잭션 안에서 반복 조회 결과가 흔들리면 안 됨&lt;/li&gt;&lt;/ul&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;[재고 차감 상황]&lt;/b&gt;&lt;br&gt;현재 재고 1개 &lt;br&gt;&lt;br&gt;사용자 A 구매 &lt;br&gt;사용자 B 구매 &lt;br&gt;&lt;br&gt;둘 다 동시에 재고 있네?라고 판단하면 문제&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이런 건 단순히 @Transational만 붙인다고 끝나지 않을 수 있다 격리 수준, 락, update 쿼리, 재시도 정책까지 같이 봐야 한다&amp;nbsp;&lt;br&gt;&lt;b&gt;즉, Isolation은 모든 동시성 문제를 마법처럼 해결하지 않는다&amp;nbsp;&lt;/b&gt;&lt;br&gt;특히 조회수 증가, 재고 차감, 쿠폰 선착순 같은 건 isolation만 볼 게 아니라 락/ 원자적/ update/ 중복 방지 까지 같이 봐야 한다&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;자주하는 실수&lt;/b&gt;&lt;/h2&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실수 2. private 메서드에 붙이기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
private void saveSomething() {
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 기반에서는 기대대로 안 먹는다고 보면 된다&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실수 2. 같은 클래스 내부 호출&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void outer() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;inner();
}

@Transactional
public void inner() {
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이것 또한 프록시를 안 거칠 수 있다&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실수 3. REQUIRES_NEW를 안전장치처럼 남발&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이건 더 안전함이 아니라 트랜잭션을 분리해야 한다 분리하면 일부만 commit 될 수 있다&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실수 4. 격리 수준을 무조건 높이기&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional(isolation = Isolation.SERIALIZABLE)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;제일 센 격리지만 제일 좋은 기본값은 아니다&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style3&quot;&gt;정합성 ↑ &lt;br&gt;동시성 ↓ &lt;br&gt;대기/충돌 가능성 ↑&lt;/blockquote&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실수 5. 외부 API 호출을 트랜잭션 안에서 오래 붙잡기&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void process() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;repository.save(...);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;externalApi.call(); // 오래 걸릴 수 있음
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이러면 DB 트랜잭션이 열린 상태로 외부 API응답을 기다린다&amp;nbsp;&lt;br&gt;문제는&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;DB connection 오래 점유&lt;/li&gt;&lt;li&gt;lock 오래 유지&lt;/li&gt;&lt;li&gt;응답 지연&lt;/li&gt;&lt;li&gt;장애 전파&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 외부 시스템 호출은 트랜잭션 안에 넣을지 진짜 신중해야 한다&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/  JPA &amp;middot; DB</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/327</guid>
      <comments>https://winwin0219.tistory.com/327#entry327comment</comments>
      <pubDate>Fri, 15 May 2026 16:51:06 +0900</pubDate>
    </item>
    <item>
      <title>[Devlog] CoreBoard 성능 테스트 계획 및 결과 정리</title>
      <link>https://winwin0219.tistory.com/326</link>
      <description>&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;테스트 개요 (Overview)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 테스트 목적&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;152&quot; data-ke-size=&quot;size16&quot;&gt;CoreBoard의 주요 사용 흐름에서 API 응답 지연과 병목 구간을 확인한다.&lt;/p&gt;
&lt;p data-end=&quot;279&quot; data-start=&quot;199&quot; data-ke-size=&quot;size16&quot;&gt;API를 하나씩 단독 호출하는 방식이 아니라, &lt;b&gt;프론트 화면에서 사용자가 실제로 이동하는 순서&lt;/b&gt;를 기준으로 JMeter 시나리오를 구성한다.&lt;/p&gt;
&lt;p data-end=&quot;279&quot; data-start=&quot;199&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;279&quot; data-start=&quot;199&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 data-end=&quot;279&quot; data-start=&quot;199&quot;&gt;주요 조회 API의 응답 시간&lt;/li&gt;
&lt;li data-end=&quot;279&quot; data-start=&quot;199&quot;&gt;검색 조건이 들어갔을 때의 성능 변화&lt;/li&gt;
&lt;li data-end=&quot;279&quot; data-start=&quot;199&quot;&gt;읽기/쓰기 요청이 섞였을 때의 DB connection 상태&lt;/li&gt;
&lt;li data-end=&quot;279&quot; data-start=&quot;199&quot;&gt;첨부파일 메타데이터 조회가 상세 조회 성능에 미치는 영향&lt;/li&gt;
&lt;li data-end=&quot;279&quot; data-start=&quot;199&quot;&gt;부하 상황에서 CPU, JVM memory, DB connection, Swap 사용량 변화&lt;/li&gt;
&lt;li data-end=&quot;279&quot; data-start=&quot;199&quot;&gt;&lt;span&gt;Redis 기반 조회수 기록이 게시글 상세 조회 응답 시간에 미치는 영향&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;279&quot; data-start=&quot;199&quot;&gt;&lt;span&gt;Redis ZSET 기반 인기글 조회 성능&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;279&quot; data-start=&quot;199&quot;&gt;&lt;span&gt;조회수 delta 적립 후 DB 동기화 흐름의 안정성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 테스트 대상 시스템&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot API 서버&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;Nginx&lt;/li&gt;
&lt;li&gt;게시판/게시글/댓글/첨부파일/조회수/인기글 관련 API&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 테스트 환경&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1차 테스트 : 로컬 또는 테스트 환경&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 또는 별도 테스트 환경에서는 비교적 강한 부하를 주어 병목 후보를 확인한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JMeter로 Step Load 방식의 부하 테스트 수행&lt;/li&gt;
&lt;li&gt;Prometheus / Grafana로 서버 지표 확인&lt;/li&gt;
&lt;li&gt;API 응답 시간, JVM memory, CPU, DB connection 상태 관찰&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2차 검증 : GCP 배포 서버&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP 배포 서버에서는 낮은 부하로만 정상 동작을 확인한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTPS &amp;rarr; Nginx &amp;rarr; Spring Boot &amp;rarr; MySQL 경로 검증&lt;/li&gt;
&lt;li&gt;운영 서버 장애 방지&lt;/li&gt;
&lt;li&gt;GCP 비용 증가 방지&lt;/li&gt;
&lt;li&gt;Stress Load는 수행하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 성능 지표 및 목표 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 처리량&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 목표 : 10 ~ 30 RPS 구간에서 안정 동작 확인&lt;br /&gt;해당 수치는 최대 처리량을 증명하기 위한 기준이 아니라, CoreBoard의 현재 서버 규모와 테스트 목적을 고려한 &lt;b&gt;초기 관찰 기준선&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 응답 시간&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1193&quot; data-start=&quot;1165&quot; data-ke-size=&quot;size16&quot;&gt;응답 시간은 시나리오 성격에 따라 다르게 판단한다.&lt;/p&gt;
&lt;p data-end=&quot;1193&quot; data-start=&quot;1165&quot; data-ke-size=&quot;size16&quot;&gt;평균 응답 시간은 전체적인 경향을 보기 위한 지표로 사용하고,&lt;br /&gt;p95 응답 시간은 일부 요청이 지연되는 구간을 확인하기 위한 핵심 지표로 본다.&lt;/p&gt;
&lt;p data-end=&quot;1379&quot; data-start=&quot;1279&quot; data-ke-size=&quot;size16&quot;&gt;응답 시간 목표는 절대적인 성능 보장 수치가 아니라, API 성격과 서버 규모를 고려한 초기 기준선이다.&lt;br /&gt;테스트 결과가 누적되면 실제 측정값을 기준으로 목표치를 다시 조정한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 105px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 21px; width: 40.6977%;&quot;&gt;&lt;b&gt; 시나리오 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px; width: 59.186%;&quot;&gt;&lt;b&gt; 기준 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px; width: 40.6977%;&quot;&gt;시나리오 A - 비로그인 게시판 탐색&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px; width: 59.186%;&quot;&gt;평균 500ms 이하 / p95 1초 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px; width: 40.6977%;&quot;&gt;시나리오 B - 검색 흐름&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px; width: 59.186%;&quot;&gt;평균 800ms 이하 / p95 1.5초 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px; width: 40.6977%;&quot;&gt;시나리오 C - 로그인 후 글 작성 및 댓글 작성&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px; width: 59.186%;&quot;&gt;평균 1초 이하 / p95 2초 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px; width: 40.6977%;&quot;&gt;시나리오 D - 첨부파일이 있는 게시글 조회&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px; width: 59.186%;&quot;&gt;평균 800ms 이하 / p95 1.5초 이하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; width: 40.6977%;&quot;&gt;시나리오 E - Redis 조회수 및 인기글 흐름&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 59.186%;&quot;&gt;평균 800ms 이하 / p95 1.5초 이하&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;공통 중단 기준 : p95&amp;nbsp;응답&amp;nbsp;시간이&amp;nbsp;3초를&amp;nbsp;초과하는&amp;nbsp;상태가&amp;nbsp;지속되면&amp;nbsp;테스트를&amp;nbsp;중단하고&amp;nbsp;병목&amp;nbsp;분석으로&amp;nbsp;전환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류율&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;5xx 에러율&lt;/b&gt; : 0%&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예상된 4xx&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증 없는 쓰기 요청 &amp;rarr; 401&lt;/li&gt;
&lt;li&gt;권한 없는 삭제/관리자 접근 &amp;rarr; 403&lt;/li&gt;
&lt;li&gt;잘못된 요청 데이터 &amp;rarr; 400&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확인 대상&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 80% 이상 지속&lt;/li&gt;
&lt;li&gt;Memory available 급감&lt;/li&gt;
&lt;li&gt;Swap 사용량 급증&lt;/li&gt;
&lt;li&gt;HikariCP pending connection 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자원 사용률&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 상황이 지속되면 테스트를 중단한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;CPU 80% 이상 지속 시 중단&lt;/li&gt;
&lt;li&gt;Memory available 급감 시 중단&lt;/li&gt;
&lt;li&gt;Swap 사용량 급증 시 중단&lt;/li&gt;
&lt;li&gt;HikariCP pending connection 발생 시 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5. 부하 시나리오 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;1. 시나리오 A &amp;mdash; 비로그인 사용자의 게시판 탐색 &lt;/b&gt;&lt;/span&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;게시글 목록 조회는 Page 기반 조회라 count query, 정렬, 작성자 조회에서 병목이 생길 수 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자 행동 흐름&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;메인 접속&lt;br /&gt;&amp;rarr; 게시판 목록 조회&lt;br /&gt;&amp;rarr; 특정 게시판 진입&lt;br /&gt;&amp;rarr; 게시글 목록 조회&lt;br /&gt;&amp;rarr; 페이지 이동&lt;br /&gt;&amp;rarr; 게시글 상세 조회&lt;br /&gt;&amp;rarr; 댓글 목록 조회&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대상 API&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GET /boards &lt;br /&gt;GET /boards/{boardId}/posts?page=0&amp;amp;size=10&amp;amp;direction=DESC &lt;br /&gt;GET /boards/{boardId}/posts?page=1&amp;amp;size=10&amp;amp;direction=DESC &lt;br /&gt;GET /posts/{id} &lt;br /&gt;GET /posts/{postId}/comments&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; 성능 목표&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균 응답 시간 : 500ms 이하&lt;/li&gt;
&lt;li&gt;p95 응답시간 : 1초 이하&lt;/li&gt;
&lt;li&gt;p95 3초 초과가 지속되면 테스트 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;2. 시나리오 B &amp;mdash; 검색 사용자의 흐름 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적&lt;/b&gt; : 검색 조건이 들어갔을 때 게시글 목록 조회 성능이 어떻게 달라지는지 확인한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색은 일반 목록 조회보다 조건 필터링 비용이 커질 수 있으므로 응답 시간 변화를 비교한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자 행동 흐름&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;게시판 진입&amp;nbsp;&lt;br /&gt;&amp;rarr; 검색어 입력&lt;br /&gt;&amp;rarr; 검색 결과&amp;nbsp; 목록 조회&lt;br /&gt;&amp;rarr; 검색 결과 게시글 상세 조회&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대상 API&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GET /boards/{boardId}/posts/ search? keyword=test&amp;amp;page=0&amp;amp;size=10&lt;br /&gt;GET /posts/{id}&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; 성능 목표&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균 응답 시간 : 800ms 이하&lt;/li&gt;
&lt;li&gt;p95 응답시간 : 1.5초 이하&lt;/li&gt;
&lt;li&gt;p95 3초 초과가 지속되면 테스트 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;3. 시나리오 C &amp;mdash; 로그인 사용자의 글 작성 및 댓글 작성 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적&lt;/b&gt; : 읽기와 쓰기가 섞인 상황에서 API 응답 시간, DB connection, 트랜잭션 처리 상태를 확인한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 API는 인증, 검증, 트랜잭션, INSERT가 포함되므로 DB connection 점유 시간이 길어질 수 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자 행동 흐름&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;로그인&lt;br /&gt;&amp;rarr; 게시판 목록 조회&lt;br /&gt;&amp;rarr; 게시글 작성&lt;br /&gt;&amp;rarr; 작성한 게시글 상세 조회&lt;br /&gt;&amp;rarr; 댓글 작성&lt;br /&gt;&amp;rarr; 댓글 목록 조회&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대상 API&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;POST /auth/token&lt;br /&gt;GET&amp;nbsp;/boards&lt;br /&gt;POST&amp;nbsp;/boards/{boardId}/posts&lt;br /&gt;GET&amp;nbsp;/posts/{id}&lt;br /&gt;POST&amp;nbsp;/posts/{postId}/comments&lt;br /&gt;GET&amp;nbsp;/posts/{postId}/comments&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; 성능 목표&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균 응답 시간 : 1초 이하&lt;/li&gt;
&lt;li&gt;p95 응답시간 : 2초 이하&lt;/li&gt;
&lt;li&gt;p95 3초 초과가 지속되면 테스트 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&amp;nbsp;&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;쓰기 API는 DB 상태를 계속 변경하므로 로컬/테스트 환경에서만 강하게 실행한다&lt;/li&gt;
&lt;li&gt;GCP 배포 서버에서는 낮은 부하로만 검증한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;4. 시나리오 D &amp;mdash; 첨부파일이 있는 게시글 조회 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적&lt;/b&gt; : 게시글 상세 조회 시 첨부파일 조회가 응답 시간에 미치는 영향을 확인한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첨부파일이 있는 게시글 상세 조회는 게시글, 댓글 외에 첨부파일 메타데이터 조회가 추가되므로 일반 상세 조회와 비교한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자 행동 흐름&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;첨부파일이 있는 게시판 진입&lt;br /&gt;&amp;rarr;&amp;nbsp;게시글 목록 조회&lt;br /&gt;&amp;rarr;&amp;nbsp;첨부파일이 있는 게시글 상세 조회&lt;br /&gt;&amp;rarr;&amp;nbsp;댓글 목록 조회&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대상 API&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GET /boards/{boardId}/posts&lt;br /&gt;GET&amp;nbsp;/posts/{id}&lt;br /&gt;GET&amp;nbsp;/posts/{postId}/comments&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; 성능 목표 &lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균 응답 시간 : 800ms 이하&lt;/li&gt;
&lt;li&gt;p95 응답시간 : 1.5초 이하&lt;/li&gt;
&lt;li&gt;p95 3초 초과가 지속되면 테스트 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;5. 시나리오 E &amp;mdash; Redis 조회수 및 인기글 흐름&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적&lt;/b&gt; : 게시글 상세 조회 시 Redis 기반 조회수 기록이 응답 시간에 미치는 영향과, Redis ZSET 기반 인기글 조회 성능을 확인한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;게시글 상세 조회는 기존에는 DB 조회 후 응답하는 흐름이었지만, 조회수 기능 추가 이후 Redis 중복 조회 방지 키 저장, 조회수 delta 증가, 인기글 점수 증가가 함께 수행된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 상세 조회 응답 시간이 Redis 기록 로직 때문에 유의미하게 증가하는지 확인한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;사용자 행동 흐름&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;게시글 상세 조회&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;rarr;&amp;nbsp;동일&amp;nbsp;게시글&amp;nbsp;반복&amp;nbsp;조회&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;rarr;&amp;nbsp;여러&amp;nbsp;게시글&amp;nbsp;상세&amp;nbsp;조회&amp;nbsp;&amp;nbsp; &lt;br /&gt;&amp;rarr; 인기글 목록 조회&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; 대상 API &lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GET&amp;nbsp;/posts/{id} &lt;br /&gt;GET&amp;nbsp;/posts/popular?size=10&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1170&quot; data-start=&quot;1160&quot;&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 data-end=&quot;1193&quot; data-start=&quot;1172&quot;&gt;평균 응답 시간 : 800ms 이하&lt;/li&gt;
&lt;li data-end=&quot;1215&quot; data-start=&quot;1194&quot;&gt;p95 응답 시간 : 1.5초 이하&lt;/li&gt;
&lt;li data-end=&quot;1240&quot; data-start=&quot;1216&quot;&gt;p95 3초 초과가 지속되면 테스트 중단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 부하 형태&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 로컬/테스트 환경&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 또는 테스트 환경에서는 Step Load 방식으로 부하를 증가시킨다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시&amp;nbsp;사용자&amp;nbsp;5명&amp;nbsp;&amp;rarr;&amp;nbsp;10명&amp;nbsp;&amp;rarr;&amp;nbsp;20명&amp;nbsp;&amp;rarr;&amp;nbsp;30명&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계는 다음 기준으로 실행한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Ramp-up: 60초&lt;/li&gt;
&lt;li&gt;Duration:&amp;nbsp;3분&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적은 최대 트래픽을 자랑하는 것이 아니라, 부하가 증가할 때 어떤 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;b&gt;2. GCP 배포 서버&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP 배포 서버에서는 낮은 부하로만 검증한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시 사용자 5명 &amp;rarr; 10명 &amp;rarr; 20명&lt;/li&gt;
&lt;li&gt;각&amp;nbsp;1~3분&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 서버에서는 Stress Load를 수행하지 않는다 &lt;b&gt;저사양 서버이므로 장애와 비용 증가 위험이 크기 때문&lt;/b&gt;이다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7. 시나리오 비중&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JMeter 테스트 시나리오의 비중은 실제 사용 흐름을 기준으로 다음과 같이 설정한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 101px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; text-align: center; width: 50.3488%;&quot;&gt;&lt;b&gt; 시나리오 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center; width: 49.5349%;&quot;&gt;&lt;b&gt; 비중 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 50.3488%;&quot;&gt;게시판 탐색&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 49.5349%;&quot;&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 50.3488%;&quot;&gt;검색&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 49.5349%;&quot;&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 50.3488%;&quot;&gt;게시글 상세 + 댓글 조회&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 49.5349%;&quot;&gt;15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 50.3488%;&quot;&gt;인기글 조회&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.5349%;&quot;&gt;15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 50.3488%;&quot;&gt;로그인 / 글 작성 / 댓글 작성&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center; width: 49.5349%;&quot;&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 요청이 대부분의 사용 흐름에서 더 자주 발생하므로 게시판 탐색, 검색, 상세 조회, 인기글 조회의 비중을 높게 둔다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인기글 조회는 Redis 기반 조회수 기능 추가 이후 새로 생긴 읽기 흐름이므로 별도 비중으로 분리한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 요청은 DB 상태를 변경하므로 낮은 비중으로 제한한다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8. 테스트 도구 및 인프라 &lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;부하테스트 도구&lt;/b&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;JMeter&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모니터링 도구&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot Actuator&lt;/li&gt;
&lt;li&gt;Prometheus&lt;/li&gt;
&lt;li&gt;Grafana&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;9. 최종 테스트 전략&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreBoard 프론트 화면의 주요 사용 흐름을 기준으로 JMeter 부하 시나리오를 설계한다.&lt;br /&gt;Prometheus와 Grafana를 통해 HTTP 응답 시간, JVM memory, CPU 사용률, DB connection 상태를 관찰한다.&lt;br /&gt;Repository 실행 시간은 직접 계측이 필요하므로, 초기 테스트에서는 API 응답 시간과 DB connection 지표를 통해 병목 후보를 추정한다.&lt;br /&gt;운영 서버에는 직접 고부하를 주지 않고, 테스트 환경에서 병목을 재현한 뒤 배포 서버에서는 제한된 부하로 정상 동작을 검증한다.&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;b&gt;10. 시나리오별 테스트 완료 결과&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 105px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignCenter&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;시나리오&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;&lt;b&gt; 확인한 병목 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;&lt;b&gt; &amp;nbsp;개선 내용 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;&lt;b&gt; &amp;nbsp;최종 결과 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;Scenario A - 비로그인 게시판 탐색&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;게시글 목록 조회 지연&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;목록 조회에서 user 조인 제거&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;&lt;b&gt;통과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;Scenario B - 검색 흐름&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;검색 목록 조회 지연&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;검색 조회 최적화&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;&lt;b&gt;통과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;Scenario C - 글 작성 및 댓글 작성&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;게시글 작성 지연&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;post.title UNIQUE INDEX 적용&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 21px;&quot;&gt;&lt;b&gt;통과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;Scenario D - 첨부파일 게시글 조회&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;게시글 목록 조회 지연&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;Page &amp;rarr; Slice, 복합 인덱스 적용&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;통과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;11. 최종 판단&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scenario A, B, C, D 테스트를 모두 완료했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 시나리오에서 발견된 병목은 별도 Bottleneck Analysis 문서로 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 후 재측정 결과, 모든 시나리오는 정의한 중단 기준을 초과하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 CoreBoard의 주요 사용자 흐름은 로컬 테스트 기준에서 안정적으로 동작한다고 판단한다.&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/326</guid>
      <comments>https://winwin0219.tistory.com/326#entry326comment</comments>
      <pubDate>Wed, 13 May 2026 17:51:52 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble Shooting] 배포 서버 API 응답 지연</title>
      <link>https://winwin0219.tistory.com/325</link>
      <description>&lt;div class=&quot;toc-box&quot;&gt;
&lt;p class=&quot;toc-title&quot; 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;&lt;a href=&quot;#why-pro&quot;&gt;문제 &amp;mdash; 배포 서버 API 응답이 갑자기 8초 이상 지연됨&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dns-dns&quot;&gt;원인 분석 &amp;mdash; 내부 호출과 외부 호출을 비교하고 서버 자원 상태를 확인함&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dns-a&quot;&gt;원인 &amp;mdash; apt 자동 점검과 swap 부재로 인한 메모리 압박 발생&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dns-an&quot;&gt;해결 &amp;mdash; apt timer 비활성화, swap 1GB 추가, 애플리케이션 재시작&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dns-sum&quot;&gt;배운 점 &amp;mdash; Health Check만으로는 운영 상태를 판단할 수 없음&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h2 id=&quot;why-pro&quot; data-end=&quot;30&quot; data-start=&quot;25&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;76&quot; data-start=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CoreBoard 배포 서버의 API 응답이 갑자기 전반적으로 느려졌다&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;129&quot; data-start=&quot;78&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 프론트 문제처럼 보였지만, 서버 내부에서 직접 API를 호출해도 약 8초가 걸렸다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;time curl -s -o /dev/null -w &quot;status=%{http_code} total=%{time_total}\n&quot; \
http://127.0.0.1:8080/boards?page=0\&amp;amp;size=10\&amp;amp;direction=DESC&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;283&quot; data-start=&quot;280&quot;&gt;응답&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;status=200 total=8.242320&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;343&quot; data-start=&quot;324&quot; data-ke-size=&quot;size16&quot;&gt;외부 도메인으로 호출해도 비슷했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;status=200 total=8.241890&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;439&quot; data-start=&quot;384&quot; data-ke-size=&quot;size16&quot;&gt;따라서 Nginx나 HTTPS만의 문제가 아니라, 서버 내부 자원 문제일 가능성이 높다고 판단했다&lt;/p&gt;
&lt;h2 id=&quot;dns-dns&quot; data-end=&quot;454&quot; data-start=&quot;446&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인 분석&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;496&quot; data-start=&quot;456&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;애플리케이션은 살아 있었지만, 서버 자원이 비정상적으로 부족했다&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;538&quot; data-start=&quot;498&quot; data-ke-size=&quot;size16&quot;&gt;health 확인 결과 애플리케이션과 DB 연결은 UP 상태였다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl -i http://127.0.0.1:8080/actuator/health&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;640&quot; data-start=&quot;599&quot; data-ke-size=&quot;size16&quot;&gt;하지만 top, free -h 확인 결과 서버 상태가 좋지 않았다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;load average: 15.22
CPU wa: 68.3%
Mem: 969Mi total, 938Mi used
Swap: 0B&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;760&quot; data-start=&quot;727&quot; data-ke-size=&quot;size16&quot;&gt;또한 apt-get 자동 점검 프로세스가 실행 중이었다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;ps -ef | grep apt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;apt-get --just-print -qq full-upgrade
apt-get check -qq
apt.systemd.daily&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;dns-a&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;971&quot; data-start=&quot;892&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1GB 메모리 서버에서 apt 자동 점검 작업이 실행되면서 Spring Boot, MariaDB와 자원 경쟁이 발생한 것이 원인이었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;986&quot; data-start=&quot;973&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 다음 흐름이다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;1GB RAM 서버
&amp;rarr; swap 없음
&amp;rarr; apt-daily 자동 점검 실행
&amp;rarr; Java, MariaDB와 자원 경쟁
&amp;rarr; 메모리 압박 및 IO wait 증가
&amp;rarr; 전체 API 응답 지연&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1143&quot; data-start=&quot;1103&quot; data-ke-size=&quot;size16&quot;&gt;health가 UP이어도 응답 속도까지 정상이라는 뜻은 아니었다&lt;/p&gt;
&lt;h2 id=&quot;dns-an&quot; data-end=&quot;1155&quot; data-start=&quot;1150&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1193&quot; data-start=&quot;1157&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;apt 자동 작업을 중지하고, swap 1GB를 추가했다&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1216&quot; data-start=&quot;1195&quot; data-ke-size=&quot;size16&quot;&gt;실행 중인 apt 프로세스를 종료했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;sudo kill &amp;lt;apt-get PID&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1268&quot; data-start=&quot;1255&quot; data-ke-size=&quot;size16&quot;&gt;apt 상태를 복구했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;sudo dpkg --configure -a
sudo apt-get -f install&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1353&quot; data-start=&quot;1332&quot; data-ke-size=&quot;size16&quot;&gt;자동 apt timer를 비활성화했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;sudo systemctl disable --now apt-daily.service apt-daily.timer 
apt-daily-upgrade.service apt-daily-upgrade.timer&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1496&quot; data-start=&quot;1481&quot; data-ke-size=&quot;size16&quot;&gt;swap 1GB를 추가했다&lt;/p&gt;
&lt;blockquote data-end=&quot;1496&quot; data-start=&quot;1481&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; swap이란?&lt;/b&gt;&lt;br /&gt;RAM이 부족할 때 디스크 일부를 임시 메모리처럼 쓰는 공간이다&amp;nbsp;&lt;br /&gt;&lt;br /&gt;RAM = 작업대 &lt;br /&gt;디스크 = 창고 &lt;br /&gt;swap = 작업대가 꽉 찼을 때 잠깐 창고 일부를 작업대처럼 쓰는 공간&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1630&quot; data-start=&quot;1611&quot; data-ke-size=&quot;size16&quot;&gt;재부팅 후에도 적용되도록 등록했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1721&quot; data-start=&quot;1704&quot; data-ke-size=&quot;size16&quot;&gt;이후 애플리케이션을 재시작했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo systemctl restart coreboard&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; font-size: 1.62em; letter-spacing: -1px;&quot;&gt;결과&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1823&quot; data-start=&quot;1781&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1823&quot; data-start=&quot;1781&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내부 API 응답 시간이 약 8초에서 0.03초 수준으로 개선되었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1830&quot; data-start=&quot;1825&quot; data-ke-size=&quot;size16&quot;&gt;조치 전:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;status=200 total=8.242320&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1876&quot; data-start=&quot;1871&quot; data-ke-size=&quot;size16&quot;&gt;조치 후:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;status=200 total=0.033635&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1948&quot; data-start=&quot;1917&quot; data-ke-size=&quot;size16&quot;&gt;외부 HTTPS 호출도 약 0.3초 수준으로 회복되었다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;total=0.29791&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;1989&quot; data-start=&quot;1982&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2028&quot; data-start=&quot;1991&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서버가 살아 있는 것과 정상 성능으로 동작하는 것은 다르다&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2085&quot; data-start=&quot;2030&quot; data-ke-size=&quot;size16&quot;&gt;health가 UP이어도 CPU, 메모리, IO wait, 응답 시간을 함께 확인해야 한다&lt;/p&gt;
&lt;p data-end=&quot;2135&quot; data-start=&quot;2087&quot; data-ke-size=&quot;size16&quot;&gt;또한 저사양 서버에서는 운영체제의 자동 작업도 애플리케이션 성능에 영향을 줄 수 있다&lt;/p&gt;
&lt;p data-end=&quot;2181&quot; data-start=&quot;2137&quot; data-ke-size=&quot;size16&quot;&gt;이번 문제를 통해 서버 운영 시 다음 항목을 함께 확인해야 한다는 점을 배웠다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;애플리케이션 health
내부 API 응답 시간
외부 도메인 응답 시간
CPU / Memory / IO wait
자동 백그라운드 작업 여부&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2318&quot; data-start=&quot;2274&quot; data-ke-size=&quot;size16&quot;&gt;앞으로는 자동 apt 업데이트를 비활성화하고, 주기적으로 수동 점검하기로 했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;sudo apt update
sudo apt list --upgradable&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2397&quot; data-start=&quot;2376&quot; data-ke-size=&quot;size16&quot;&gt;필요할 때만 직접 업데이트를 수행한다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo apt upgrade&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;top란?&lt;/b&gt;&lt;br /&gt;서버가 지금 얼마나 바쁜지 보는 명령어&lt;br /&gt;CPU, 메모리, 실행 중인 프로세스를 본다&amp;nbsp;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;free -h란?&lt;/b&gt;&lt;br /&gt;서버 메모리 상태를 보는 명령어&lt;br /&gt;Mem&amp;nbsp;&amp;nbsp;=&amp;nbsp;실제&amp;nbsp;RAM&lt;br /&gt;Swap&amp;nbsp;=&amp;nbsp;보조&amp;nbsp;메모리&amp;nbsp;공간&lt;br /&gt;available = 지금 새 작업에 쓸 수 있는 메모리&lt;/blockquote&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Load Average란?&lt;/b&gt;&lt;br /&gt;서버에 일이 얼마나 밀려있는지 보여주는 값&lt;/blockquote&gt;
&lt;pre id=&quot;code_1778659963027&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;load average: 15.22, 14.16, 12.55
// 최근 1분 / 5분 / 15분 동안 CPU를 쓰려고 기다리는 작업이 얼마나 많았는가&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; IO wait / wa란?&lt;/b&gt;&lt;br /&gt;CPU가 디스크나 메모리 작업이 끝나길 기다리는 시간&lt;/blockquote&gt;
&lt;pre id=&quot;code_1778660013533&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;wa 68.3%
// CPU 시간의 68.3%가 계산이 아니라 대기 상태&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; Swap이란?&lt;/b&gt;&lt;br /&gt;RAM이 부족할 때 디스크 일부를 임시 메모리처럼 쓰는 공간&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RAM : 빠른 작업대&lt;/li&gt;
&lt;li&gt;디스크 : 창고&amp;nbsp;&lt;/li&gt;
&lt;li&gt;swap : 작업대가 꽉 찼을 때 임시로 쓰는 창고 공간&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swap은 RAM보다 훨씬 느리다 그래서 swap은 성능 향상용이 아니라 서버가 메모리 부족으로 흔들리는 걸 막는 완충장치다&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;&lt;b&gt; kswapd란?&lt;/b&gt;&lt;br /&gt;리눅스가 메모리를 정리할 때 사용하는 커널 프로세스다&lt;br /&gt;메모리가 부족하면 kswapd가 바빠진다&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;apt란?&lt;/b&gt;&lt;br /&gt;리눅스에서 프로그램을 설치하거나 업데이트하는 도구다&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apt 가 관리하는 것들은 대개 이런 것이다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nginx&lt;/li&gt;
&lt;li&gt;mariadb&lt;/li&gt;
&lt;li&gt;openssl&lt;/li&gt;
&lt;li&gt;curl&lt;/li&gt;
&lt;li&gt;보안 패치&lt;/li&gt;
&lt;li&gt;시스템&amp;nbsp;라이브러리&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; apt-daily란?&lt;/b&gt;&lt;br /&gt;서버가 자동으로 apt 점검을 하도록 예약된 작업&lt;br /&gt;저사양 서버에서 이 자동 작업도 CPU와 메모리를 꽤 먹을 수 있다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; systemd timer란?&lt;/b&gt;&lt;br /&gt;리눅스의 예약 실행 기능&lt;br /&gt;윈도우의 작업 스케줄러처럼 생각하면 된다&amp;nbsp;&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;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/325</guid>
      <comments>https://winwin0219.tistory.com/325#entry325comment</comments>
      <pubDate>Wed, 13 May 2026 17:00:57 +0900</pubDate>
    </item>
    <item>
      <title>[Network] 브라우저가 도메인을 IP로 찾는 과정 &amp;mdash; DNS 동작 원리</title>
      <link>https://winwin0219.tistory.com/324</link>
      <description>&lt;div class=&quot;toc-box&quot;&gt;
  &lt;p class=&quot;toc-title&quot; 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;&lt;a href=&quot;#why-dns&quot;&gt;DNS가 왜 필요한가?&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#dns-hierarchy&quot;&gt;DNS의 계층 구조&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#dns-lookup-flow&quot;&gt;브라우저가 도메인을 IP로 찾는 전체 흐름&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#dns-cache-ttl&quot;&gt;TTL과 캐시 — DNS 전파 지연이 왜 생기는가&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#dns-records&quot;&gt;주요 DNS 레코드 종류&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;#dns-summary&quot;&gt;전체 흐름 정리&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;

&lt;h2 id=&quot;why-dns&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DNS가 왜 필요한가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷에서 컴퓨터끼리 통신할 때 IP주소를 사용한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;IP( Internet Protocol)주소란?&lt;/b&gt;&lt;br /&gt;인터넷에서 장치나 서버를 찾기 위해 사용하는 숫자 기반 주소다&lt;br /&gt;IP 주소는 그 프로토콜에서 목적지를 식별하기 위해 사용하는 주소값이다&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;그런데 매번 35.202.68.192 같은 숫자를 외워서 접속하기는 불가능하다 그래서 &lt;b&gt;사람이 읽기 쉬운 이름 즉, 도메인&lt;/b&gt;을 만들었다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; 도메인(Domain) 이란?&lt;/b&gt;&lt;br /&gt;사람이 기억하기 쉬운 인터넷 이름이다&lt;br /&gt;DNS는 이 도메인 이름을 실제 접속에 필요한 IP 주소로 변환해준다&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;google.com, naver.com 같은 형식이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 주소창에 google.com 을 입력하면 브라우저는 이걸 어딘가에서 IP주소로 변환해야 한다 그 변환을 담당하는 세스템이 바로 DNS다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; DNS(Domain Name System)란?&lt;br /&gt;The Domain Name System (DNS) is a simple query-response protocol whose messages in both directions have the same format. - &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8499&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;공식문서링크&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;&lt;br /&gt;&lt;/b&gt;도메인 이름을 IP주소로 변환해주는 시스템이다 인터넷의 전화번호부라고 생각하면 된다&amp;nbsp;&lt;br /&gt;구글이요? &amp;rarr; 5.190.27.1 로 전화하세요 같은 역할을 한다&amp;nbsp;&lt;/blockquote&gt;
&lt;h2 id=&quot;dns-hierarchy&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DNS의 계층 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS는 한 서버가 모든 도메인을 다 알고 있는 게 아니다 &lt;b&gt;계층적으로 분산&lt;/b&gt;되어 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778645950319&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;루트 네임서버 (.)
    &amp;darr;
TLD 네임서버 (.com / .kr / .xyz 등)
    &amp;darr;
권한 네임서버 (google.com / coreboard-api.xyz 등)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 어떤 역할을 하는지 알아보자&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 루트 네임서버 (Root Name Server)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 계층의 최상위 서버다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 세계에 &lt;a href=&quot;https://www.iana.org/domains/root/servers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;13그룹(A~M)&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;이 있고 실제 서버는 수백 대가 분산 운영된다&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The authoritative name servers that serve the DNS root zone, often known simply as the &amp;ldquo;root servers&amp;rdquo;, are a network of hundreds of servers in many countries around the world. &lt;br /&gt;They are configured in the DNS root zone as 13 named authorities.&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;루트 네임서버는 모든 도메인의 IP를 알고 있는 게 아니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.com은 이쪽 서버에 물어봐&lt;/li&gt;
&lt;li&gt;.xyz는 저쪽 서버에 물어봐&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLD 네임서버의 위치만 알려준다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. TLD 네임서버 (Top Level Domain Name Server)&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;TLD(Top Level Domain)이란?&lt;/b&gt;&lt;br /&gt;도메인의 맨 끝 부분이다 google.com에서 .com, naver.co.kr에서 .kr이 TLD다&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;.com, .kr, .xyz같은 최상위도메인 별로 서버가 따로 있다 TLD네임서버는 해당 TLD 아래에 있는 도메인들의 권한 네임서버 위치를 알고 있다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;google.com? 그건 구글 권한 네임서버에 물어봐~&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 권한 네임서버(Authoritative Name Server)&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;권한 네임서버(Authoritative Name Server)란?&lt;br /&gt;&lt;/b&gt;특정 도메인의 &lt;b&gt;실제 DNS 정보를 가지고 있는 서버&lt;/b&gt;다 가비아에서 도메인을 샀다면 &lt;b&gt;가비아의 네임서버가 권한네임 서버&lt;/b&gt;다&amp;nbsp;&lt;br /&gt;여기서 A&amp;nbsp; 레코드를 등록하면 이 서버가 이 도메인의 IP는 이거야라고 확실하게 답해준다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서버가 최종 정답을 가지고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 네임서버에서 정상 조회되면 DNS 설정 자체는 맞는 것이다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778646335462&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nslookup api.coreboard-api.xyz ns.gabia.co.kr&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;dns-lookup-flow&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;브라우저가 도메인을 IP로 찾는 전체 흐름&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;google.com을 입력했을 때 무슨 일이 일어나는지 단계별로 본다.&lt;/p&gt;
&lt;pre id=&quot;code_1778646352846&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;브라우저 주소창에 google.com 입력
          &amp;darr;
  1. 브라우저 캐시 확인
          &amp;darr; 없으면
  2. OS 캐시 확인 (hosts 파일 포함)
          &amp;darr; 없으면
  3. 로컬 DNS 리졸버에 질의 (ISP 서버)
          &amp;darr; 모르면
  4. 루트 네임서버에 질의
          &amp;darr;
  5. TLD 네임서버에 질의 (.com)
          &amp;darr;
  6. 권한 네임서버에 질의 (google.com)
          &amp;darr;
  7. IP 반환 &amp;rarr; 캐시 저장 &amp;rarr; 접속&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 1단계: 브라우저 캐시 확인 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 한 번 조회한 도메인의 IP를 &lt;b&gt;자체 캐시&lt;/b&gt;에 저장한다 google.com에 오늘 이미 접속했다면, 브라우저는 저장해둔 IP를 바로 꺼내 쓴다. DNS 조회를 생략하는 것이다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;캐시(Cache) 란?&lt;/b&gt;&lt;br /&gt;자주 쓰는 데이터를 빠르게 꺼내 쓰려고 임시로 저장해두는 공간이다 매번 처음부터 찾는 것보다 훨씬 빠르다&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 2단계: OS 캐시 확인 + hosts 파일 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 캐시에 없으면 운영체제(OS) 레벨 캐시를 확인한다 그리고 &lt;b&gt;hosts 파일&lt;/b&gt;도 함께 확인한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; hosts 파일이란?&lt;/b&gt; &lt;br /&gt;컴퓨터에 직접 이 도메인은 이 IP야~라고 수동으로 적어두는 파일이다&lt;br /&gt;Windows 기준 C:\Windows\System32\drivers\etc\hosts 에 있다 &lt;br /&gt;DNS 서버보다 먼저 확인하기 때문에, 여기에 적힌 내용이 우선이다&lt;br /&gt;로컬 개발할 때 127.0.0.1 myapp.local 같은 식으로 쓰기도 한다 &lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 3단계: 로컬 DNS 리졸버에 질의 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS 캐시에도 없으면 &lt;b&gt;로컬 DNS 리졸버&lt;/b&gt;에 질의한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; 로컬 DNS 리졸버(Local DNS Resolver)란?&lt;/b&gt;&lt;br /&gt;ISP(인터넷 서비스 제공자, 즉 KT, SKT, LGU+ 등)가 운영하는 DNS 서버다&lt;br /&gt;내 컴퓨터가 인터넷에 연결되면 자동으로 이 서버를 사용하도록 설정된다&lt;br /&gt;나 대신 DNS 조회를 대행해주는 서버라고 보면 된다&lt;br /&gt;Google의 8.8.8.8, Cloudflare의 1.1.1.1도 이 역할을 한다.&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;로컬 DNS 리졸버도 캐시를 가지고 있다&amp;nbsp; 다른 사용자가 최근에 같은 도메인을 조회했다면 캐시에서 바로 응답한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시가 없으면 이 서버가 아래 4~6단계를 대신 수행해준다이 과정을 &lt;b&gt;재귀 쿼리&lt;/b&gt;라고 부른다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; 재귀 쿼리(Recursive Query)란?&lt;/b&gt; &lt;br /&gt;클라이언트가 로컬 DNS 리졸버에 google.com의 IP 알아와줘~라고 부탁하면,&lt;br /&gt;리졸버가 루트 &amp;rarr; TLD &amp;rarr; 권한 네임서버까지 직접 돌아다니며 &lt;br /&gt;결과를 가져오는 방식이다 클라이언트는 기다리기만 하면 된다&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 4단계: 루트 네임서버에 질의 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 DNS 리졸버가 루트 네임서버에 google.com의 IP가 뭐야? 라고 물어본다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 네임서버는 IP는 모른다. 대신 이렇게 답한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.com 담당 TLD 네임서버는 여기야 &amp;rarr; TLD 서버 주소 반환&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 5단계: TLD 네임서버에 질의 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.com TLD 네임서버에 google.com의 IP가 뭐야? 라고 물어본다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLD 네임서버도 IP는 모른다&amp;nbsp;대신 이렇게 답한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;google.com 담당 권한 네임서버는 여기야 &amp;rarr; 구글 권한 네임서버 주소 반환&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 6단계: 권한 네임서버에 질의 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글의 권한 네임서버에 google.com의 IP가 뭐야? 라고 물어본다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 최종 답이 나온다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;google.com &amp;rarr; 142.250.196.110 &amp;rarr; IP 반환&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 7단계: IP 반환 후 캐시 저장 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 DNS 리졸버가 받은 IP를 클라이언트(내 컴퓨터)에게 돌려준다 동시에 이 결과를 &lt;b&gt;TTL 동안 캐시에 저장&lt;/b&gt;해둔다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 받은 IP로 서버에 접속한다&lt;/p&gt;
&lt;h2 id=&quot;dns-cache-ttl&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;TTL과 캐시 — DNS 전파 지연이 왜 생기는가&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 트러블슈팅에서 겪은 &lt;b&gt;DNS 전파 지연&lt;/b&gt;이 왜 생기는지가 보인다 가비아에서 A 레코드를 새로 등록하거나 수정하면, 권한 네임서버(가비아)에는 즉시 반영된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 전 세계 로컬 DNS 리졸버들은 각자 &lt;b&gt;이전에 조회한 결과를 TTL만큼 캐시에 들고 있다&lt;/b&gt;&amp;nbsp;그 TTL이 만료되기 전까지는 변경된 값을 알 수가 없다&lt;/p&gt;
&lt;pre id=&quot;code_1778646753643&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;가비아 권한 네임서버: IP 변경 완료 
KT DNS 리졸버: 이전 캐시 TTL 남음, 아직 모름 
Google DNS (8.8.8.8): 이전 캐시 TTL 남음, 아직 모름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 리졸버의 TTL이 제각각이라서, 사람마다 다른 시점에 변경된 DNS를 보게 된다 이게 DNS 전파 지연이다. 최대 48시간이 걸리는 이유도 이거다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;dns-records&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;주요 DNS 레코드 종류&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 네임서버 안에는 도메인에 대한 여러 정보가 레코드로 저장된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; DNS 레코드란?&lt;br /&gt;&lt;/b&gt; 권한 네임서버에 저장되는 도메인 관련 정보다. 종류마다 역할이 다르다&lt;/blockquote&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 164px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt; 레코드 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt; 역할 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt; 예시 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;A&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;도메인 &amp;rarr; IPv4 주소&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;api.coreboard-api.xyz &amp;rarr; 35.202.68.192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;AAAA&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;도메인 &amp;rarr; IPv6 주소&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;IPv6 환경용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;CNAME&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;도메인 &amp;rarr; 다른 도메인&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;www.example.com &amp;rarr; example.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;MX&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;도메인 &amp;rarr; 메일 서버 주소&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;이메일 수신 담당&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;NS&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;도메인 &amp;rarr; 권한 네임서버 주소&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;ns.gabia.co.kr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;&lt;b&gt;TXT&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;도메인에 텍스트 정보 첨부&lt;/td&gt;
&lt;td style=&quot;height: 21px; text-align: center;&quot;&gt;도메인 소유 인증, SPF 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 레코드는 도메인을 IP로 직접 연결하는 가장 기본적인 레코드다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CNAME이란?&lt;/b&gt; &lt;br /&gt;도메인을 다른 도메인으로 연결하는 레코드다&lt;br /&gt;&lt;a href=&quot;http://www.coreboard.com&quot;&gt;www.coreboard.com&lt;/a&gt; &amp;rarr; &lt;a href=&quot;http://coreboard.com&quot;&gt;coreboard.com&lt;/a&gt; 같은 식으로, 별칭을 만들 때 쓴다&lt;br /&gt;Vercel 배포할 때 CNAME &amp;rarr; &lt;a href=&quot;http://cname.vercel-dns.com&quot;&gt;cname.vercel-dns.com&lt;/a&gt; 처럼 설정하는 것도 이거다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;dns-summary&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;전체 흐름 정리&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1778646869085&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;브라우저에 google.com 입력
          &amp;darr;
  [브라우저 캐시] &amp;rarr; 있으면 바로 사용
          &amp;darr; 없으면
  [OS 캐시 / hosts 파일] &amp;rarr; 있으면 바로 사용
          &amp;darr; 없으면
  [로컬 DNS 리졸버] &amp;rarr; 캐시 있으면 바로 응답
          &amp;darr; 없으면 (재귀 쿼리 시작)
  [루트 네임서버] &amp;rarr; .com TLD 서버 주소 알려줌
          &amp;darr;
  [.com TLD 네임서버] &amp;rarr; google.com 권한 네임서버 주소 알려줌
          &amp;darr;
  [google.com 권한 네임서버] &amp;rarr; 142.250.196.110 반환
          &amp;darr;
  리졸버가 결과 캐시 (TTL만큼)
          &amp;darr;
  브라우저에 IP 전달 &amp;rarr; 서버 접속&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/  Network</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/324</guid>
      <comments>https://winwin0219.tistory.com/324#entry324comment</comments>
      <pubDate>Wed, 13 May 2026 13:47:18 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble Shooting] HTTPS로 연결하며 마주한 배포 트러블슈팅</title>
      <link>https://winwin0219.tistory.com/323</link>
      <description>&lt;div class=&quot;toc-box&quot;&gt;
&lt;p class=&quot;toc-title&quot; 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;&lt;a href=&quot;#mixed-content&quot;&gt;문제 상황: Mixed Content 차단&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#https-direction&quot;&gt;해결 방향: 백엔드도 HTTPS로&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dns-nxdomain&quot;&gt;트러블슈팅 1: DNS 전파 지연&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#swagger-timeout&quot;&gt;트러블슈팅 2: Swagger API 문서 요청에서 Gateway Time-out 발생&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#certbot-apt-lock&quot;&gt;트러블슈팅 3: Certbot 설치 중 apt lock&amp;nbsp;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#ssh-failed&quot;&gt;트러블슈팅 4: SSH 접속 불가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#https-502&quot;&gt;트러블슈팅 5: SSL 적용 후 502 Bad Gateway&amp;nbsp;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#result-review&quot;&gt;결론과 회고&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;h2 id=&quot;https-direction&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 문제 상황 &amp;nbsp;: Mixed Content 차단 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreBoard는 Spring Boot 백엔드와 React 프론트엔드로 구성된 프로젝트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드는 GCP Compute Engine에 먼저 배포해 두었고, 이후 프론트엔드를 Vercel에 배포하면서 두 서버를 연결하는 작업을 시작했다.&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;그런데 Vercel 배포 환경에서 브라우저를 열자마자 모든 API 요청이 차단되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1778639593028&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Mixed Content: The page at 'https://coreboard-web-drht.vercel.app/' 
was loaded over HTTPS, but requested an insecure XMLHttpRequest 
endpoint 'http://35.202.68.192:8080/boards?page=0&amp;amp;size=10&amp;amp;direction=DESC'. 
This request has been blocked; the content must be served over HTTPS.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;에러 직역&lt;br /&gt;혼합 콘텐츠: '&lt;a href=&quot;https://coreboard-web-drht.vercel.app/&quot;&gt;https://coreboard-web-drht.vercel.app/&lt;/a&gt;' 페이지는 HTTPS로 로드되었는데,&lt;br /&gt;보안되지 않은 XMLHttpRequest 엔드포인트 '&lt;a href=&quot;http://35.202.68.192:8080/&quot;&gt;http://35.202.68.192:8080/&lt;/a&gt;...'를 요청했습니다.&lt;br /&gt;이 요청은 차단되었습니다. 콘텐츠는 HTTPS로 제공되어야 합니다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 왜 이런 에러가 발생하는가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 &lt;span data-token-index=&quot;1&quot;&gt;Mixed Content(혼합 콘텐츠)&lt;/span&gt; 를 차단한다&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;1&quot;&gt;Mixed Content란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS로 로드된 페이지에서 HTTP 리소스(API, 이미지 등)를 요청하는 상황을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 페이지는 보안된 연결을 보장하는데, 거기서 HTTP 요청이 나가면 그 보안이 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 브라우저는 이를 허용하지 않고 강제로 차단한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Vercel 환경변수 설정 문제를 의심했다. API 주소가 제대로 들어갔는지, 빌드에 반영됐는지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 에러 메시지를 자세히 보면, 문제는 API 주소가 &lt;b&gt;비어있는 게 아니었다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소는 잘 들어갔다. 다만 그 주소 자체가 http:// 로 시작하는 것이 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 해결책은 axios 설정을 고치는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;백엔드 API 자체를 HTTPS로 접근 가능하게 만들어야 했다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방향: 백엔드 API도 HTTPS로&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 API를 HTTPS로 제공하기 위한 구조를 설계했다.&lt;/p&gt;
&lt;pre id=&quot;code_1778642827385&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;클라이언트 (Vercel, HTTPS)
    &amp;darr;  https://api.coreboard-api.xyz
  Nginx (443 포트, SSL 처리)
    &amp;darr;  http://localhost:8080
  Spring Boot 애플리케이션&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1891&quot; data-origin-height=&quot;871&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK75YK/dJMcaja22OC/NjfB9zAQomKNNjGTI0TNn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK75YK/dJMcaja22OC/NjfB9zAQomKNNjGTI0TNn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK75YK/dJMcaja22OC/NjfB9zAQomKNNjGTI0TNn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK75YK%2FdJMcaja22OC%2FNjfB9zAQomKNNjGTI0TNn1%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;1891&quot; height=&quot;871&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1891&quot; data-origin-height=&quot;871&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1778639839242&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;api.coreboard-api.xyz -&amp;gt; 35.202.68.192&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체적인 계획은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GCP VM에 고정 IP를 연결하고, api.coreboard-api.xyz 도메인의 A 레코드를 해당 IP로 설정한다.&lt;/li&gt;
&lt;li&gt;Spring Boot에 SSL을 직접 붙이지 않고, 앞단에 &lt;b&gt;Nginx&lt;/b&gt; 를 두어 HTTPS를 처리하게 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Certbot&lt;/b&gt; 으로 무료 SSL 인증서를 발급받아 Nginx에 적용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;A 레코드란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS(도메인 네임 시스템)에서 도메인 이름을 IP 주소로 연결하는 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;api.coreboard-api.xyz &amp;rarr; 35.202.68.192 같은 식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 도메인을 입력받으면 DNS에서 A 레코드를 조회해서 실제 IP를 찾아낸다.&lt;/p&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;span data-token-index=&quot;0&quot;&gt;Nginx란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서버 소프트웨어다. 아파치(Apache)와 함께 가장 많이 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단독으로 정적 파일을 서빙하거나, 다른 서버 앞에서 중계 역할(Reverse Proxy)을 할 수 있다.&lt;/p&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;Reverse Proxy란?&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;클라이언트는 Nginx하고만 통신하고, Spring Boot가 뒤에 있다는 사실을 모른다.&lt;/p&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;SSL Termination이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 암호화/복호화 처리를 Nginx 같은 앞단 서버에서 끝내고,&amp;nbsp;내부 애플리케이션 서버로는 평문 HTTP로 전달하는 방식이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Upstream이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx뒤쪽에 있는 실젲 애플리케이션 서버. 이번 경우에는 SpringBoot가 Upstream이다&lt;/p&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;span data-token-index=&quot;0&quot;&gt;Certbot이란?&lt;/span&gt; &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let's Encrypt라는 무료 인증기관에서 SSL 인증서를 자동으로 발급받고 갱신해주는 도구다&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; 최종 목표: 프론트엔드가 &lt;a style=&quot;color: #ee2323;&quot; href=&quot;https://api.coreboard-api.xyz&quot;&gt;https://api.coreboard-api.xyz&lt;/a&gt; 주소로 API를 호출하도록 만드는 것. &lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h2 id=&quot;dns-nxdomain&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 트러블슈팅 1: DNS 전파 지연 &lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 발생&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인의 A 레코드를 GCP VM 고정 IP로 설정한 뒤, 바로 api.coreboard-api.xyz 로 접근을 시도했다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;설정을 저장했으니 바로 될 것이라고 생각했지만, 도메인이 정상 조회되지 않았다.&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;br /&gt;&lt;/span&gt;이때 확인한 증상:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;NXDOMAIN&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;NXDOMAIN이란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS에서 이런 도메인은 없다(Non-Existent Domain)는 응답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 자체가 등록되지 않았거나, DNS에 아직 반영되지 않은 경우에 발생한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;원인 분석 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 A 레코드를 잘못 입력했거나, 저장이 안 된 줄 알았다.&amp;nbsp;그래서 &lt;b&gt;권한 네임서버&lt;/b&gt; 를 직접 지정해서 조회해봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1778640338259&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nslookup api.coreboard-api.xyz ns.gabia.co.kr&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;3289&quot; data-start=&quot;3158&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;nslookup이란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인이 어떤 IP로 연결되는지 확인하는 명령어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 인자로 특정 DNS 서버를 지정할 수 있다.&lt;/p&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;span data-token-index=&quot;0&quot;&gt;권한 네임서버(Authoritative Name Server)란?&lt;/span&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;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과: 권한 네임서버에서는 api.coreboard-api.xyz &amp;rarr; 35.202.68.192 로 정상 조회됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Google DNS(8.8.8.8)나 Cloudflare DNS(1.1.1.1) 에서는 아직 같은 결과가 나오지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 보고 문제 범위를 좁혔다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도메인 설정 자체가 틀렸다면, 권한 네임서버에서도 조회되지 않아야 한다.&lt;/li&gt;
&lt;li&gt;권한 네임서버에서는 정상 조회됐다. 따라서 설정 자체는 맞다.&lt;/li&gt;
&lt;li&gt;문제는 외부 DNS 서버들이 아직 변경 내용을 모른다는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;3289&quot; data-start=&quot;3158&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 원인 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;3395&quot; data-start=&quot;3314&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;DNS 전파 지연(DNS Propagation Delay)&lt;/span&gt; 이었다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;3395&quot; data-start=&quot;3314&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;DNS 전파란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 설정 변경이 전 세계 DNS 서버들에 반영되기까지 걸리는 시간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 네임서버에 설정이 반영되더라도, 각 ISP나 구글&amp;middot;클라우드플레어 같은 외부 DNS 서버들은&amp;nbsp;자신이 캐시에 저장해둔 이전 정보를 일정 시간 동안 그대로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 수 분에서 최대 48시간까지 걸릴 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;3395&quot; data-start=&quot;3314&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 해결 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;3540&quot; data-start=&quot;3474&quot; data-ke-size=&quot;size16&quot;&gt;A 레코드 설정을 다시 건드리지 않고, 권한 네임서버 기준으로 정상 등록된 것을 확인한 뒤 기다렸다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 문제를 확인할 때는 &lt;b&gt;조회 기준을 분리해야 한다.&amp;nbsp;&lt;/b&gt;내 PC에서 조회가 안 된다고 바로 설정을 바꾸면 안 된다.&lt;/p&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;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;권한 네임서버에서 정상이면 &amp;rarr; 설정은 맞다. 전파를 기다린다.&lt;/li&gt;
&lt;li&gt;권한 네임서버에서도 안 되면 &amp;rarr; 설정 자체를 다시 확인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;swagger-timeout&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 트러블슈팅 2: Gateway Time-out &lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 문제 발생 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx를 설정한 뒤 Swagger UI 페이지 자체는 열렸다. 그래서 Nginx와 Spring Boot 연결이 어느 정도 된다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Swagger UI에서 API 명세를 가져오는 /v3/api-docs 요청이 응답하지 않았다.&lt;/p&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 data-token-index=&quot;1&quot;&gt;504 Gateway Time-out&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span data-token-index=&quot;0&quot;&gt;504 Gateway Time-out이란?&lt;/span&gt; &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞단 서버(Nginx)가 뒤쪽 서버(Spring Boot)로부터 정해진 시간 안에 응답을 받지 못했을 때 발생한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 원인 분석 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Nginx reverse proxy 설정 문제를 의심했다.&amp;nbsp;Nginx를 막 설정한 상태였고, timeout 값이 너무 짧을 수도 있다고 생각했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 바로 Nginx 문제로 단정하지 않았다. &lt;b&gt;외부 요청에서만 느린 건지, Spring Boot 자체가 느린 건지 먼저 분리해야 했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 서버 내부에서 Spring Boot를 직접 호출해봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1778640668864&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -i -m 120 http://localhost:8080/v3/api-docs
# 결과: 120초 동안 응답 없음&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;curl이란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 HTTP 요청을 보낼 수 있는 명령어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 없이 서버가 제대로 응답하는지 확인할 때 자주 쓴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;i는 응답 헤더를 같이 보여달라는 옵션, m 120은 최대 120초 기다리라는 옵션이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제 범위가 좁혀졌다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nginx를 통할 때만 느렸다면 &amp;rarr; Nginx 설정, 방화벽, proxy timeout 의심&lt;/li&gt;
&lt;li&gt;&lt;b&gt;localhost:8080 직접 호출도 120초 응답 없음&lt;/b&gt; &amp;rarr; Spring Boot 내부 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Spring Boot 로그를 확인했다.&lt;/p&gt;
&lt;pre id=&quot;code_1778640777014&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Init duration for springdoc-openapi is: 390994 ms
# 390994ms = 약 6분 31초&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;springdoc-openapi란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 자동으로 Swagger API 명세를 생성해주는 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 시작된 뒤, 첫 /v3/api-docs 요청이 들어오면 전체 API를 스캔해서 문서를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 초기화 과정이 오래 걸릴 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 원인 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;springdoc-openapi 초기화 지연&lt;/b&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx는 요청을 Spring Boot로 잘 넘겼지만, Spring Boot가 /v3/api-docs 를 처리하는 데 6분 이상 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx의 기본 timeout 설정 안에 응답이 오지 않으니 Gateway Time-out이 발생한 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 해결 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx를 건드리지 않고, 일단 springdoc-openapi 초기화가 완료될 때까지 기다렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화가 끝난 뒤에는 /v3/api-docs 요청이 정상적으로 응답했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Gateway Time-out을 만났을 때 확인 순서:&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;서버 내부에서 curl localhost:8080/{해당 경로} 로 직접 호출해본다.&lt;/li&gt;
&lt;li&gt;내부 직접 호출도 느리다 &amp;rarr; Spring Boot 내부 문제 (로그 확인)&lt;/li&gt;
&lt;li&gt;내부 직접 호출은 빠른데 외부만 느리다 &amp;rarr; Nginx, 방화벽, proxy 설정 의심&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메시지만 보고 Nginx 설정부터 수정하면 안 된다.&lt;/p&gt;
&lt;h2 id=&quot;certbot-apt-lock&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 트러블슈팅 3: Certbot 설치 중 apt lock 발생 &lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-end=&quot;5511&quot; data-start=&quot;5506&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 발생&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;5640&quot; data-start=&quot;5624&quot; data-ke-size=&quot;size16&quot;&gt;HTTPS를 위해 Certbot을 설치하려는데 다음 에러가 발생했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;/var/lib/dpkg/lock-frontend held by PID 381771 apt-get check -qq&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 직역&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/var/lib/dpkg/lock-frontend 파일이 PID 381771번 프로세스(apt-get check -qq)에 의해 점유되어 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;5722&quot; data-start=&quot;5720&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 원인 분석 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Certbot 설치 명령어를 잘못 입력한 줄 알았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 에러 메시지를 보면 문제는 Certbot이 아니었다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;apt란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ubuntu/Debian 계열 Linux에서 프로그램을 설치&amp;middot;업데이트&amp;middot;삭제하는 패키지 관리자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apt install 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;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;apt lock이란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux에서는 apt 작업이 동시에 여러 개 실행되면 충돌이 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 한 apt 프로세스가 실행 중이면 lock 파일을 잡고, 다른 apt 작업이 시작되지 못하게 막는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/var/lib/dpkg/lock-frontend가 그 lock 파일이다.&lt;/p&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;span data-token-index=&quot;1&quot;&gt;PID란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Process ID. Linux에서 실행 중인 모든 프로세스에 붙는 고유 번호다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PID 381771이라고 하면 381771번 프로세스라는 뜻이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; 즉, Certbot 설치가 실패한 이유는 &lt;b&gt;이미 다른 apt 프로세스가 실행 중&lt;/b&gt;이었기 때문이다.&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;6146&quot; data-start=&quot;6144&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;6249&quot; data-start=&quot;6195&quot; data-ke-size=&quot;size16&quot;&gt;기존에 실행 중이던 apt 관련 프로세스(자동 업데이트 등)가 lock을 점유하고 있었다.&lt;/p&gt;
&lt;h4 data-end=&quot;6253&quot; data-start=&quot;6251&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock을 잡고 있는 프로세스를 확인하고 종료했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 dpkg 상태를 복구한 뒤 Certbot 설치를 다시 진행했다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;dpkg란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apt 아래에서 실제 패키지 설치/관리를 담당하는 하위 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;apt가 고장나거나 중간에 끊기면 dpkg 상태도 같이 깨질 수 있어서 복구가 필요하다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSL 인증서 설정이라고 해서 Nginx나 도메인만 보면 되는 게 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Certbot을 설치하려면 &lt;b&gt;서버의 패키지 관리자 상태도 정상&lt;/b&gt;이어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 트러블슈팅에서는 애플리케이션, 프록시, 인증서뿐 아니라 &lt;b&gt;OS 레벨 상태&lt;/b&gt;까지 확인해야 한다.&lt;/p&gt;
&lt;h2 id=&quot;ssh-failed&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 트러블슈팅 4: SSH 접속 불가 &lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-end=&quot;6576&quot; data-start=&quot;6571&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 발생&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 설정 작업 중 서버에 SSH로 접속이 되지 않는 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PowerShell에서 직접 SSH 접속 시도&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ssh: connect to host 35.202.68.192 port 22: Connection timed out&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;에러 직역&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;35.202.68.192의 22번 포트로 연결 시도: 연결 시간 초과&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6775&quot; data-start=&quot;6745&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Cloud Identity-Aware Proxy를 통한 연결 실패 코드: 4003
이유: failed to connect to backend&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 직역&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 연결 실패&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;Cloud Shell에서 포트 상태 확인&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6919&quot; data-start=&quot;6869&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Connection refused
22 closed&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;1&quot;&gt;SSH란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secure Shell. 원격 서버에 터미널로 접속하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버를 직접 앞에 두지 않고도 인터넷을 통해 명령어를 실행할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 포트는 22번이다.&lt;/p&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;span data-token-index=&quot;0&quot;&gt;GCP IAP SSH란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Cloud Platform의 Identity-Aware Proxy를 통한 SSH 접속 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;22번 포트를 직접 열지 않아도 GCP 내부 네트워크를 통해 서버에 접근할 수 있는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이마저 안 된다면 서버 접근 경로 자체에 문제가 있다는 신호다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;6965&quot; data-start=&quot;6963&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 원인 분석 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 Nginx나 Spring Boot가 응답을 못 하는 문제가 아니었다.&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;일반 SSH 접속과 IAP SSH 접속이 모두 실패했고, 포트 확인 결과도 22 closed였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 클라이언트 문제로 볼 수 없었다.&lt;/p&gt;
&lt;h4 data-end=&quot;7333&quot; data-start=&quot;7331&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;7566&quot; data-start=&quot;7494&quot; data-ke-size=&quot;size16&quot;&gt;IAP SSH 접근에 필요한 &lt;span data-token-index=&quot;1&quot;&gt;GCP 방화벽 규칙이 열려 있지 않았고&lt;/span&gt;, 서버 내부 SSH 상태도 복구가 필요한 상황이었다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;7566&quot; data-start=&quot;7494&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;방화벽 규칙(Firewall Rule)이란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 들어오는 트래픽을 어떤 포트, 어떤 IP로부터 허용할지 결정하는 규칙이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP에서는 Compute Engine 인스턴스마다 방화벽 규칙을 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAP SSH도 GCP 내부 IP 대역(35.235.240.0/20)에서 22번 포트로 들어오는 트래픽을 허용해야 동작한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;7570&quot; data-start=&quot;7568&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 해결 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAP SSH 방화벽 규칙을 추가하고, &lt;span data-token-index=&quot;1&quot;&gt;startup-script&lt;/span&gt; 를 통해 SSH 접근을 복구했다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;startup-script란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP VM이 부팅될 때 자동으로 실행되는 스크립트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH가 막혀서 직접 들어갈 수 없는 상황에서도, 인스턴스를 재시작하면서 스크립트를 주입해 복구할 수 있다.&lt;/p&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;span data-token-index=&quot;1&quot;&gt;systemd란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Linux에서 서비스(백그라운드 프로세스)를 관리하는 시스템이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot를 서버에서 서비스로 등록해두면, 서버가 재시작되거나 프로세스가 죽어도 자동으로 재실행된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 정상인지 확인하려면 로그를 봐야 하고,&amp;nbsp;Nginx 설정을 고치려면 서버에 들어가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SSH가 막히면 원인 분석 자체가 불가능&lt;/b&gt;해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 접근 경로도 운영의 일부로 관리해야 한다.&lt;/p&gt;
&lt;h2 id=&quot;https-502&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 트러블슈팅 5: 502 Bad Gateway &lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-end=&quot;7822&quot; data-start=&quot;7817&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제 발생&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;7897&quot; data-start=&quot;7880&quot; data-ke-size=&quot;size16&quot;&gt;Certbot으로 SSL 인증서를 발급한 뒤, HTTPS API 호출이 정상 동작하는지 확인했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl -i https://api.coreboard-api.xyz/actuator/health&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;7981&quot; data-start=&quot;7966&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;HTTP/1.1 502 Bad Gateway
Server: nginx/1.22.1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 직역&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;502 Bad Gateway: Nginx가 뒤쪽 서버로부터 유효한 응답을 받지 못했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &lt;span data-token-index=&quot;0&quot;&gt;502 Bad Gateway란?&lt;/span&gt; &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞단 서버(Nginx)가 뒤쪽 서버(Spring Boot)로 요청을 전달하려 했으나, 연결 자체가 실패했을 때 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;504는 응답이 너무 늦게 왔다이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;502는 연결 자체가 안 됐다에 가깝다.&lt;/p&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;span data-token-index=&quot;1&quot;&gt;/actuator/health란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot Actuator가 제공하는 엔드포인트로, 애플리케이션이 정상 동작 중인지 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;{&quot;status&quot;:&quot;UP&quot;} 같은 응답이 오면 정상이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 원인 분석 &lt;/b&gt;&lt;/h4&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Nginx 설정이 잘못됐거나 Spring Boot가 죽은 것이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 서버 내부에서 Spring Boot를 직접 호출해봤다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl -i http://localhost:8080/actuator/health&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;curl: (7) Failed to connect to localhost port 8080 after 0 ms: Couldn't connect to server&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 직역&lt;br /&gt;localhost의 8080 포트로 연결하는 데 실패했습니다. 서버에 연결할 수 없습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과가 중요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 도메인으로 호출했을 때 502가 발생하면 Nginx 설정 문제처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;localhost:8080 직접 호출도 연결이 안 된다면&lt;/b&gt;, Spring Boot가 아직 8080 포트에서 요청을 받을 준비가 안 된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, HTTPS 요청은 Nginx까지 도달했는데 Nginx가 뒤쪽 Spring Boot로 연결하려는 순간 8080 포트에 아무것도 없었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 journalctl로 서비스 로그를 확인했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Starting CoreBoardApplication&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;8733&quot; data-start=&quot;8717&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;journalctl이란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd로 관리되는 서비스의 로그를 확인하는 명령어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;journalctl -u coreboard -f 처럼 사용하면 특정 서비스의 로그를 실시간으로 볼 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;8733&quot; data-start=&quot;8717&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot가 아직 시작 중이었다. 서버 메모리 상태도 확인했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;free -h&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;8777&quot; data-start=&quot;8756&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Mem: 969Mi total, used 913Mi, available 56Mi
Swap: 0B&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 부족 + Swap 없음 + springdoc-openapi 초기화 지연까지 겹쳐서 기동이 오래 걸리고 있었다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;8996&quot; data-start=&quot;8846&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;8996&quot; data-start=&quot;8846&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;Swap이란?&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 메모리(RAM)가 부족할 때 임시로 사용하는 디스크 공간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swap: 0B는 Swap이 설정되지 않은 상태를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 여유가 56MB밖에 없는데 Swap도 없으면, Spring Boot 기동이 크게 느려질 수 있다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;9000&quot; data-start=&quot;8998&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSL 인증서 발급 문제가 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx는 HTTPS 요청을 정상적으로 받았지만, upstream인 Spring Boot가 아직 8080 포트에서 요청을 받을 수 없는 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 502는 &lt;b&gt;Spring Boot 기동 지연&lt;/b&gt; 때문에 발생한 문제였다.&lt;/p&gt;
&lt;h4 data-end=&quot;9188&quot; data-start=&quot;9186&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;curl localhost:8080/actuator/health 로 Spring Boot가 실제로 응답 가능한 상태인지 확인&lt;/li&gt;
&lt;li&gt;journalctl 로 애플리케이션 시작 로그 확인&lt;/li&gt;
&lt;li&gt;free -h 로 서버 메모리 상태 확인&lt;/li&gt;
&lt;li&gt;Spring Boot가 완전히 기동된 뒤 다시 HTTPS health check 요청&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종응답&lt;/p&gt;
&lt;pre id=&quot;code_1778643787423&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 200
Server: nginx/1.22.1
Content-Type: application/vnd.spring-boot.actuator.v3+json&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;배운 점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;502 Bad Gateway를 만났을 때 &lt;b&gt;바로 Nginx 설정을 보면 안 된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 upstream인 Spring Boot가 실제로 포트를 열고 응답 가능한 상태인지 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;502 확인 순서&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;curl localhost:8080/{경로} &amp;rarr; Spring Boot 응답 가능 여부 확인&lt;/li&gt;
&lt;li&gt;안 된다면 journalctl -u {서비스명} 로 기동 로그 확인&lt;/li&gt;
&lt;li&gt;free -h 로 메모리 여유 확인&lt;/li&gt;
&lt;li&gt;그래도 원인을 모르겠다면 그때 Nginx 설정(/etc/nginx/sites-available/) 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;result-review&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 결론과 회고 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 HTTPS 백엔드 API 호출이 정상화되었다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778640935434&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -i https://api.coreboard-api.xyz/actuator/health&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778640939686&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 200
Server: nginx/1.22.1
Content-Type: application/vnd.spring-boot.actuator.v3+json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actuator health 응답에서도 애플리케이션 상태가 UP으로 확인되었다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결한 문제들 요약&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 트러블슈팅 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;에러&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;실제&lt;span&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;원인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; &amp;nbsp;핵심 확인 방법 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;NXDOMAIN&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;DNS 전파 지연&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;권한 네임서버로 먼저 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;504 Gateway Time-out&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;springdoc-openapi 초기화 지연&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;localhost 직접 curl로 분리 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;apt lock&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;기존 apt 프로세스 충돌&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;lock 잡은 PID 확인 후 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;SSH 접속 불가&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;IAP 방화벽 규칙 미설정&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;startup-script로 복구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;502 Bad Gateway&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Spring Boot 기동 지연&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;localhost curl + journalctl&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가장 크게 배운 것&lt;/b&gt;&lt;/h4&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;routeros&quot;&gt;&lt;code&gt;브라우저
  &amp;darr;
DNS
  &amp;darr;
Nginx (Reverse Proxy)
  &amp;darr;
Spring Boot
  &amp;darr;
OS (메모리, 프로세스, 방화벽)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메시지는 항상 가장 앞단에서 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실제 원인은 훨씬 아래 계층에 있는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway Time-out이 발생했을 때 Nginx 문제처럼 보였지만 실제로는 Spring Boot 내부 지연이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;502가 발생했을 때 SSL 문제처럼 보였지만 실제로는 Spring Boot가 아직 켜지지도 않은 상태였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;확인 방법은 항상 계층을 분리해서 각각 확인이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 안 되면 내부에서 직접 호출해본다. 내부에서도 안 되면 더 아래 계층을 본다.&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/323</guid>
      <comments>https://winwin0219.tistory.com/323#entry323comment</comments>
      <pubDate>Wed, 13 May 2026 12:21:16 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble Shooting]운영 배포 후 첨부파일 엔티티 변경으로 JPA 스키마 검증 실패</title>
      <link>https://winwin0219.tistory.com/321</link>
      <description>&lt;h2 data-end=&quot;138&quot; data-start=&quot;133&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;226&quot; data-start=&quot;140&quot; data-ke-size=&quot;size16&quot;&gt;CoreBoard에 게시글 첨부파일 생명주기 기능과 검색 기능을 추가한 뒤, dev 브랜치의 변경사항을 main으로 병합하고 운영 서버에 배포했다&lt;/p&gt;
&lt;p data-end=&quot;289&quot; data-start=&quot;228&quot; data-ke-size=&quot;size16&quot;&gt;배포 자체는 진행된 것처럼 보였지만, 브라우저에서 Swagger 주소로 접속하자 다음과 같은 화면이 나타났다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;사이트에 연결할 수 없음
ERR_CONNECTION_REFUSED&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;411&quot; data-start=&quot;341&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 8080 포트나 GCP 방화벽 문제처럼 보였다&lt;br /&gt;하지만 서버 내부에서 직접 확인해보니 외부 접속 문제가 아니었다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl -i http://localhost:8080/actuator/health&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;484&quot; data-start=&quot;472&quot; data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같았다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;curl: (7) Failed to connect to localhost port 8080 after 0 ms: Couldn't connect to server&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;665&quot; data-start=&quot;589&quot; data-ke-size=&quot;size16&quot;&gt;즉, 외부 방화벽 문제가 아니라 &lt;b&gt;서버 내부에서도 Spring Boot 애플리케이션이 8080 포트로 정상 기동되지 않은 상태&lt;/b&gt;였다&lt;/p&gt;
&lt;h2 data-end=&quot;677&quot; data-start=&quot;672&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;분석&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;699&quot; data-start=&quot;679&quot; data-ke-size=&quot;size16&quot;&gt;먼저 systemd 상태를 확인했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo systemctl status coreboard&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;785&quot; data-start=&quot;746&quot; data-ke-size=&quot;size16&quot;&gt;결과만 보면 서비스는 active (running) 상태로 보였다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;Active: active (running)
ExecStart=/usr/bin/java -jar /home/ggksthf29/apps/coreboard/app.jar&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;969&quot; data-start=&quot;893&quot; data-ke-size=&quot;size16&quot;&gt;하지만 systemctl에서 프로세스가 살아 있다고 해서 애플리케이션이 정상적으로 HTTP 요청을 받을 준비가 끝났다는 뜻은 아니다&lt;/p&gt;
&lt;p data-end=&quot;984&quot; data-start=&quot;971&quot; data-ke-size=&quot;size16&quot;&gt;그래서 로그를 확인했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;sudo journalctl -u coreboard -n 300 --no-pager&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1116&quot; data-start=&quot;1046&quot; data-ke-size=&quot;size16&quot;&gt;로그에는 Spring Boot가 시작되다가 JPA EntityManagerFactory 초기화 단계에서 실패한 흔적이 있었다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;Failed to initialize JPA EntityManagerFactory
Unable to build Hibernate SessionFactory
Schema-validation: missing column [deleted_at] in table [attachment]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1319&quot; data-start=&quot;1287&quot; data-ke-size=&quot;size16&quot;&gt;이후 HikariCP가 종료되고 Tomcat도 중지되었다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;HikariPool-1 - Shutdown initiated
HikariPool-1 - Shutdown completed
Stopping service [Tomcat]
Application run failed&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1578&quot; data-start=&quot;1451&quot; data-ke-size=&quot;size16&quot;&gt;즉, 애플리케이션은 실행을 시도했지만, JPA 스키마 검증 단계에서 실패했고 그 결과 Tomcat이 완전히 뜨지 못했다 그래서 8080 포트가 열리지 않았다&lt;/p&gt;
&lt;h2 data-end=&quot;1590&quot; data-start=&quot;1585&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1645&quot; data-start=&quot;1592&quot; data-ke-size=&quot;size16&quot;&gt;원인은 &lt;b&gt;엔티티 변경사항이 운영 DB 스키마에 반영되지 않은 상태에서 배포했기 때문&lt;/b&gt;이었다&lt;/p&gt;
&lt;p data-end=&quot;1694&quot; data-start=&quot;1647&quot; data-ke-size=&quot;size16&quot;&gt;이번 작업에서 Attachment 엔티티에 deletedAt 필드를 추가했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Column
private LocalDateTime deletedAt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1779&quot; data-start=&quot;1750&quot; data-ke-size=&quot;size16&quot;&gt;또한 첨부파일 상태에도 DELETED를 추가했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;public enum AttachmentStatus {
    TEMP,
    CONFIRMED,
    DELETED
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2082&quot; data-start=&quot;1864&quot; data-ke-size=&quot;size16&quot;&gt;코드 기준으로는 게시글 삭제 또는 수정 중 제거된 첨부파일을 바로 물리 삭제하지 않고 DELETED 상태로 전환한 뒤, 일정 기간이 지나면 스케줄러가 정리하는 구조로 바뀌었다 실제 변경사항에도 deletedAt 필드 추가, DELETED 상태 추가, markDeleted() 메서드 추가가 포함되어 있었다&lt;/p&gt;
&lt;p data-end=&quot;2138&quot; data-start=&quot;2084&quot; data-ke-size=&quot;size16&quot;&gt;하지만 운영 DB의 attachment 테이블에는 아직 deleted_at 컬럼이 없었다&lt;/p&gt;
&lt;p data-end=&quot;2251&quot; data-start=&quot;2140&quot; data-ke-size=&quot;size16&quot;&gt;운영 환경의 JPA 설정은 ddl-auto: validate 방식이었다. 이 방식은 엔티티를 기준으로 DB 테이블을 자동 수정하지 않고, &lt;b&gt;엔티티와 실제 DB 스키마가 일치하는지만 검사&lt;/b&gt;한다&lt;/p&gt;
&lt;p data-end=&quot;2292&quot; data-start=&quot;2253&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Hibernate가 애플리케이션 기동 중 다음 검증을 수행했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Attachment 엔티티에는 deletedAt 필드가 있음
&amp;rarr; attachment 테이블에는 deleted_at 컬럼이 없음
&amp;rarr; 스키마 검증 실패
&amp;rarr; EntityManagerFactory 생성 실패
&amp;rarr; Spring Boot 기동 실패&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2482&quot; data-start=&quot;2439&quot; data-ke-size=&quot;size16&quot;&gt;추가로, DB의 status 컬럼도 기존에는 다음 상태만 허용하고 있었다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum('CONFIRMED','TEMP')&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2576&quot; data-start=&quot;2522&quot; data-ke-size=&quot;size16&quot;&gt;하지만 코드에는 DELETED 상태가 추가되었기 때문에, 이 역시 운영 DB에 반영해야 했다&lt;/p&gt;
&lt;p data-end=&quot;2594&quot; data-start=&quot;2578&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 원인은 두 가지였다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;1. attachment 테이블에 deleted_at 컬럼이 없었다.
2. attachment.status enum에 DELETED 값이 없었다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;2701&quot; data-start=&quot;2696&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2745&quot; data-start=&quot;2703&quot; data-ke-size=&quot;size16&quot;&gt;운영 DB에 접속해서 attachment 테이블 스키마를 직접 수정했다&lt;/p&gt;
&lt;p data-end=&quot;2759&quot; data-start=&quot;2747&quot; data-ke-size=&quot;size16&quot;&gt;먼저 DB에 접속했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;mysql -u root -p coreboard&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2826&quot; data-start=&quot;2801&quot; data-ke-size=&quot;size16&quot;&gt;이후 deleted_at 컬럼을 추가했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE attachment
ADD COLUMN deleted_at datetime(6) NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2921&quot; data-start=&quot;2903&quot; data-ke-size=&quot;size16&quot;&gt;적용 후 테이블 구조를 확인했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;DESC attachment;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2981&quot; data-start=&quot;2952&quot; data-ke-size=&quot;size16&quot;&gt;확인 결과 deleted_at 컬럼이 추가되었다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;| deleted_at | datetime(6) | YES | | NULL |&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3077&quot; data-start=&quot;3040&quot; data-ke-size=&quot;size16&quot;&gt;그 다음 status 컬럼에 DELETED 상태를 추가했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;ALTER TABLE attachment
MODIFY COLUMN status enum('TEMP','CONFIRMED','DELETED') NOT NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3212&quot; data-start=&quot;3180&quot; data-ke-size=&quot;size16&quot;&gt;다시 확인하니 status 컬럼이 다음처럼 변경되었다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;enum('TEMP','CONFIRMED','DELETED')&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3290&quot; data-start=&quot;3262&quot; data-ke-size=&quot;size16&quot;&gt;DB 스키마를 수정한 뒤 애플리케이션을 재시작했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;sudo systemctl restart coreboard&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3373&quot; data-start=&quot;3338&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 바로 curl을 날렸을 때 여전히 연결되지 않았다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl -i http://localhost:8080/actuator/health&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3581&quot; data-start=&quot;3434&quot; data-ke-size=&quot;size16&quot;&gt;하지만 로그를 보니 애플리케이션이 죽은 것이 아니라, 기동 시간이 오래 걸리고 있었다 실제로 로그에는 Tomcat 초기화, JPA 초기화, HikariCP 연결 생성이 순서대로 진행되고 있었다&lt;/p&gt;
&lt;p data-end=&quot;3606&quot; data-start=&quot;3583&quot; data-ke-size=&quot;size16&quot;&gt;조금 더 기다린 뒤 최신 로그를 확인했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;sudo journalctl -u coreboard --since &quot;2026-05-11 14:14:30&quot; --no-pager&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3703&quot; data-start=&quot;3691&quot; data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같았다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Initialized JPA EntityManagerFactory for persistence unit 'default'
Tomcat started on port 8080 (http) with context path '/'
Started CoreBoardApplication&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3959&quot; data-start=&quot;3872&quot; data-ke-size=&quot;size16&quot;&gt;즉, JPA 스키마 검증을 통과했고 Spring Boot 애플리케이션이 정상 기동되었다&lt;/p&gt;
&lt;h2 data-end=&quot;3971&quot; data-start=&quot;3966&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;4052&quot; data-start=&quot;3973&quot; data-ke-size=&quot;size16&quot;&gt;이번 문제를 통해 서버가 안 뜬다가 아니라, &lt;b&gt;운영 배포에서 코드 변경과 DB 스키마 변경의 순서가 중요하다&lt;/b&gt;는 점을 확인했다&lt;/p&gt;
&lt;p data-end=&quot;4271&quot; data-start=&quot;4054&quot; data-ke-size=&quot;size16&quot;&gt;특히 운영 환경에서 ddl-auto: validate를 사용하는 경우, Hibernate는 DB를 자동으로 바꿔주지 않는다 엔티티와 DB 스키마가 다르면 애플리케이션 기동을 막는다 이 덕분에 잘못된 스키마 상태로 서버가 떠서 런타임 중 더 큰 문제가 발생하는 것은 막을 수 있었지만, 배포 전에 스키마 변경을 먼저 반영하지 않으면 서비스 자체가 기동되지 않는다는 것도 알게 되었다&lt;/p&gt;
&lt;p data-end=&quot;4306&quot; data-start=&quot;4273&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 data-end=&quot;4306&quot; data-start=&quot;4273&quot;&gt;엔티티에 필드가 추가되면 운영 DB에도 컬럼 추가가 필요하다&lt;/li&gt;
&lt;li data-end=&quot;4306&quot; data-start=&quot;4273&quot;&gt;enum 값이 추가되면 DB enum 컬럼도 함께 수정해야 한다&lt;/li&gt;
&lt;li data-end=&quot;4306&quot; data-start=&quot;4273&quot;&gt;ddl-auto: validate 환경에서는 DB 스키마 변경을 자동으로 기대하면 안 된다&lt;/li&gt;
&lt;li data-end=&quot;4306&quot; data-start=&quot;4273&quot;&gt;배포 후 ERR_CONNECTION_REFUSED가 보이면 방화벽보다 먼저 서버 내부 curl과 애플리케이션 로그를 확인해야 한다&lt;/li&gt;
&lt;li data-end=&quot;4306&quot; data-start=&quot;4273&quot;&gt;systemctl active 상태만으로 애플리케이션 정상 기동을 확정하면 안 된다&lt;/li&gt;
&lt;li data-end=&quot;4306&quot; data-start=&quot;4273&quot;&gt;Tomcat started, Started Application 로그까지 확인해야 실제 기동 완료로 볼 수 있다&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4676&quot; data-start=&quot;4649&quot; data-ke-size=&quot;size16&quot;&gt;최종적으로 다음 SQL을 반영해 문제를 해결했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE attachment
ADD COLUMN deleted_at datetime(6) NULL;

ALTER TABLE attachment
MODIFY COLUMN status enum('TEMP','CONFIRMED','DELETED') NOT NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4884&quot; data-start=&quot;4843&quot; data-ke-size=&quot;size16&quot;&gt;그리고 애플리케이션 로그에서 다음 메시지를 확인하며 정상 기동을 검증했다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Tomcat started on port 8080
Started CoreBoardApplication&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;5098&quot; data-start=&quot;4956&quot; data-ke-size=&quot;size16&quot;&gt;이번 장애는 첨부파일 생명주기 기능을 운영에 반영하면서 발생한 DB 스키마 불일치 문제였다&lt;br /&gt;앞으로는 엔티티 변경이 포함된 배포에서는 코드 배포 전에 운영 DB 변경사항을 먼저 정리하고, 최소한의 마이그레이션 SQL을 배포 체크리스트에 포함해야 한다&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/321</guid>
      <comments>https://winwin0219.tistory.com/321#entry321comment</comments>
      <pubDate>Mon, 11 May 2026 23:27:03 +0900</pubDate>
    </item>
    <item>
      <title>[JVM] Java는 실행될 때 메모리를 어떻게 나눠 쓸까?</title>
      <link>https://winwin0219.tistory.com/320</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; JVM Run-Time Data Areas &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM Run-Time Data Areas는 JVM이 Java 프로그램을 실행하는 동안 사용하는 메모리 영역들이며, 대표적으로 Method Area, Heap, JVM Stack 등이 있다&lt;/p&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;a href=&quot;https://winwin0219.tistory.com/312&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;JVM&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;에 대해 공부를 했었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 자바 버츄얼 머신의 약자로, Java 프로그램을 실행해주는 가상 실행기계.&lt;/p&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 코드를 쓴다고 바로 CPU가 알아듣는 게 아니라 .java파일을 javac가 .class파일로 바꾸고 JVM이 이 .class파일을 읽고 실행한다고 했었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이때 이 .class파일 안에 들어 있는 것이 바로 bytecode라고 한다&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;어떤 메서드가 있는지&lt;/li&gt;
&lt;li&gt;new로 만든 객체가 뭔지&lt;/li&gt;
&lt;li&gt;지금 어떤 메서드를 실행 중인지&lt;/li&gt;
&lt;li&gt;지역변수&amp;nbsp;값이&amp;nbsp;뭔지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 JVM은 실행 중 메모리를 여러 보관함으로 나눠쓴다 공식문서는 이걸 &lt;b&gt;Run-Time Data Areas&lt;/b&gt;라고 부른다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM 런타임 데이터 영역, 쉽게 말하면 JVM이 프로그램 실행 중 사용하는 메모리 영역들이다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 오늘 볼 핵심 보관함은 3개만 다뤄보겠다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Method Area&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Method Area는 JVM의 런타임 데이터 영역 중 하나로, 클래스별 구조를 저장하는 영역이며 클래스 정보, 필드/메서드 데이터, 런타임 상수 풀, 메서드/생성자 코드, static 변수와 관련 있다&lt;/p&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;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt; 공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt; 핵심 문장으론 아래와 같다&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Method Area는 클래스별 구조를 저장하고, 그 안에는 런타임 상수 풀, 필드/메서드 데이터, 메서드와 생성자 코드가 포함된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 정보 보관함, 공식문서에서는 Method Area가 클래스별 구조를 저장한다고 설명한다 예를 들면 런타임 상수 풀, 필드/메서드 데이터, 메서드와 생성자 코드 같은 것들을 말한다&lt;/p&gt;
&lt;pre id=&quot;code_1778487833793&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Student {
    String name;
    static String schoolName = &quot;소초초등학교&quot;;

    void sayHello() {
        System.out.println(&quot;안녕&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM은 Student클래스를 읽으면 이런 정보를 알아야 한다&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;이 클래스 이름은 Student구나&lt;/li&gt;
&lt;li&gt;필드는&amp;nbsp;name,&amp;nbsp;schoolName이&amp;nbsp;있구나&lt;/li&gt;
&lt;li&gt;schoolName은 static이구나&lt;/li&gt;
&lt;li&gt;sayHello()라는&amp;nbsp;메서드가&amp;nbsp;있구나&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 클래스 단위 정보가 Method Area쪽과 관련이 있다 이때 명확히 구분해서 기억해야 한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Method Area는 객체 보관함이 아니라 클래스 설명서 보관함이다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Heap&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap은 JVM의 런타임 데이터 영역 중 하나로, 모든 클래스 인스턴스와 배열이 할당되는 영역이며, new로 생성한 객체와 인스턴스 변수가 저장되고&lt;b&gt; GC의 관리 대상&lt;/b&gt;이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt; 공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt; 핵심 문장은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The heap is the run-time data area from which memory for all class instances and arrays is allocated.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap은 모든 클래스 인스턴스와 배열의 메모리가 할당되는 런타임 데이터 영역이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap은 new로 만든 객체가 들어가는 곳이다 공식문서도 Heap을 모든 클래스 인스턴스와 배열의 메모리가 할당되는 런타임 데이터영역이라고 설명한다 쉽게 말해 객체와 배열 저장소다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778487940754&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Student a = new Student();
Student b = new Student();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 실행하면 new Student() 때문에 객체가 2개 생긴다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778487961387&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Heap

Student 객체 a
- name = null

Student 객체 b
- name = null&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음엔&lt;/p&gt;
&lt;pre id=&quot;code_1778487970108&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;a.name = &quot;철수&quot;;
b.name = &quot;영희&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게하면 Heap 안에 각 객체 상태가 달라진다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778487984833&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Heap

Student 객체 a
- name = &quot;철수&quot;

Student 객체 b
- name = &quot;영희&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 포인트는 인스턴스 변수 name은 객체마다 따로 있으며 a.name과 b.name은 다를 수 있다는 점이다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; JVM Stack&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM Stack은 각 스레드마다 따로 생성되는 메서드 실행 공간이며, 메서드가 호출될 때마다 Stack Frame이 생성되고 그 안에 지역변수, 매개변수, operand stack, 중간 계산 결과가 저장된다&lt;/p&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;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt; 공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt; 핵심은&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A new frame is created each time a method is invoked...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드가 호출될 때마다 새로운 frame이 생성된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static은 지금 실행 중인 메서드들의 작업 공간이다 공식문서에서는 각 JVM 스레드가 private JVM stack을 가지고 메서드 호출마다 frame이 만들어진다고 설명한다 그리고 frame은 지역변수와 부분 결과 등을 저장한다고 되어있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해 JVM stack = 메서드 실행 기록 보관함이다&lt;/p&gt;
&lt;pre id=&quot;code_1778488081941&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    int result = add(1, 2);
}

static int add(int a, int b) {
    int sum = a + b;
    return sum;
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;실행 흐름&lt;/b&gt;&lt;br /&gt;main() 실행 시작 &amp;rarr; main()용 작업 공간이 Stack에 생김 add(1, 2) 호출 &amp;rarr; add()용 작업 공간이 Stack 위에 생김 add() 끝남 &amp;rarr; add() 작업 공간 사라짐 main() 끝남 &amp;rarr; main() 작업 공간 사라짐&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;그래서 Stack에는 보통 이런 것들이 들어간다고 보면 된다&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;지역변수&lt;/li&gt;
&lt;li&gt;매개변수&lt;/li&gt;
&lt;li&gt;중간 계산 결과&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Backend/☕ Java</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/320</guid>
      <comments>https://winwin0219.tistory.com/320#entry320comment</comments>
      <pubDate>Mon, 11 May 2026 17:35:07 +0900</pubDate>
    </item>
    <item>
      <title>[Java] static은 도대체 어디에 저장되고 언제 사라질까?</title>
      <link>https://winwin0219.tistory.com/319</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; static은 객체 것이 아니라 클래스 것이다&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt; 공식문서&lt;/u&gt;&lt;/span&gt;&lt;/b&gt;&lt;/a&gt; 기준에서 나온 핵심은 이거다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;static keyword to create fields and methods that belong to the class, rather than to an instance of the class.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;static 키워드는 필드와 메서드를 &lt;b&gt;객체(instance)가 아니라 클래스에 속하게&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;class는 붕어빵틀이고 object는 그 틀로 찍어낸 붕어빵 하나다&lt;/p&gt;
&lt;pre id=&quot;code_1778485556083&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Student {
    String name;
    static String schoolName = &quot;소초초등학교&quot;;
}
// 학생마다 이름은 다르다
// 하지만 학교 이름은 모두 같다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778485580418&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Student a = new Student();
Student b = new Student();

a.name = &quot;철수&quot;;
b.name = &quot;영희&quot;;
// 철수의 이름
// 영희의 이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 schoolName은? 소초초등학교&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 학생 한 명의 것이 아니라 &lt;b&gt;Student라는 클래스 전체의 것&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; static이 아닌 변수 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;; 이건 붕어빵마다 따로 들어가는 팥이다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;철수 붕어빵 안의 팥&lt;/li&gt;
&lt;li&gt;영희 붕어빵 안의 팥&lt;/li&gt;
&lt;li&gt;민수&amp;nbsp;붕어빵&amp;nbsp;안의&amp;nbsp;팥&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;객체마다 따로 있다 공식문서도 이렇게 말한다&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;they each have their own distinct copies of instance variables.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 클래스로 객체를 여러 개 만들면, 각 객체는 인스턴스 변수를 &lt;b&gt;자기 것 따로&lt;/b&gt; 가진다&lt;/p&gt;
&lt;pre id=&quot;code_1778485667672&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static String schoolName;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 붕어빵마다 들어가는 팥이 아니라, &lt;b&gt;붕어빵 틀 옆에 붙어있는 공통 표지판&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Student 클래스 공통 표지판 : 소초초등학교&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 학생이 100명이든 1000명이든 schoolName은 하나만 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/nutsandbolts/variables.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;에서도 이 문장이 핵심이다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;there is exactly one copy of this variable in existence, regardless of how many times the class has been instantiated.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스가 몇 번 객체로 만들어지든, static 변수는 &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;&lt;span style=&quot;color: #ee2323;&quot;&gt; static은 &lt;b&gt;객체마다 따로 생기는 게 아니라 클래스에 하나만 붙는 것&lt;/b&gt;이다&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인스턴스 변수란?&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 객체마다 따로 가지는 변수 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;static이란?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 객체가 아니라 클래스에 붙는 것 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;static 변수란?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 클래스가 하나만 가지는 공통 변수 &lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; static 메서드는 왜 객체 없이 호출 가능할까? &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;static 메서드도 변수랑 똑같다 객체 것이 아니라 클래스 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se9/html/jls-8.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;공식문서&lt;/b&gt; &lt;/u&gt;&lt;/span&gt;&lt;/a&gt;기준으로는 이렇게 말한다&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A method that is declared static is called a class method.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;static으로 선언된 메서드는 &lt;b&gt;클래스 메서드&lt;/b&gt;라고 부른다&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;A class method is always invoked without reference to a particular object.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 메서드는 &lt;b&gt;특정 객체를 참조하지 않고 호출된다&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778485866716&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Calculator {
    static int add(int a, int b) {
        return a + b;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 메서드는 어떤 계산기 객체의 기능이 아니다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778485890390&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Calculator calculator = new Calculator();
calculator.add(1, 2);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 객체를 만들어서 부를 필요가 없다 그냥 클래스 이름으로 부르면 된다&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778485906484&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Calculator.add(1, 2);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;왜냐면 add()는 계산기 한 대의 상태를 쓰지 않는다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;철수 계산기의 add()&lt;/li&gt;
&lt;li&gt;영희 계산기의 add()&lt;/li&gt;
&lt;li&gt;민수&amp;nbsp;계산기의&amp;nbsp;add()&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 따로 있을 필요가 없다 그냥 공통 기능 하나면 된다&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778485953556&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Calculator 클래스의 add()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 static 메서드는 객체 없이 호출할 수 있다&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; static 메서드 안에서는 왜 인스턴스 변수를 바로 못 쓸까? &lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1778485981077&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Student {
    String name;
    static String schoolName = &quot;소초초등학교&quot;;

    static void printSchool() {
        System.out.println(schoolName);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 가능하다 왜냐하면 schoolName도 static이고 printSchool()도 static이기 때문이다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 둘 다 클래스 소속이다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이건 안 된다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778486044671&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Student {
    String name;

    static void printName() {
        System.out.println(name); // 오류
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;왜냐하면 name은 객체마다 다르기 때문이다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778486057486&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;철수.name = 철수
영희.name = 영희
민수.name = 민수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 printName()은 객체 없이 호출된다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778486074267&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Student.printName();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그러면 자바 입장에서는 누구 이름을 출력하라는거니? 철수? 영희? 민수? 아직 객체도 없는데?? 가 된다 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 static 메서드 안에서는 인스턴스 변수를 그냥 바로 쓸 수 없다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;공식문서에서도 static메서드에서는 현재 객체를 가리키는 this나 super를 사용할 수 없다고 말한다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;즉, static메서드는 지금 이 객체라는 개념을 기본으로 가지지 않는다&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; static은 JVM 메모리에서 어디와 관련 있을까? &lt;/b&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;static은 클래스 보관함 쪽에 붙는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;쉽게 말하면 이건데 조금 더 개발자스럽게 말해보면&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;new로 만든 객체&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr; Heap&lt;/li&gt;
&lt;li&gt;static 변수의 실제 값&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr; Java 8+에선 Heap&lt;/li&gt;
&lt;li&gt;클래스&amp;nbsp;메타데이터&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;Metaspace&amp;nbsp;(Method&amp;nbsp;Area)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778486249512&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Student a = new Student();
Student b = new Student();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게하면 a, b 객체는 각각 따로 만들어진다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Heap&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;Student 객체 a
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;name = 철수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Student 객체 b
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;name = 영희&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 static 변수는 객체마다 복사되지 않는다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778486326306&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static String schoolName = &quot;소초초등학교&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이건 클래스 쪽에 하나만 있다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;Class 정보 쪽&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Student 클래스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;static schoolName = 소초초등학교&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그래서 객체가 100개 생겨도 schoolName은 100개가 아니라 1개다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; 인스턴스 변수는 객체마다 Heap에 따로 생기고, static 변수는 클래스가 로딩될 때 클래스 단위로 하나만 관리된다&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Heap이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;new로 만든 객체가 주로 저장되는 공간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Method Area란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;JVM이 클래스 메타데이터를 관리하는 메모리 영역 &lt;/span&gt;&lt;span&gt;클래스 이름, 필드 구조, 메서드 구조 같은 클래스 단위 정보와 관련 있 &lt;/span&gt;&lt;span&gt;(Java 8+에서 static 변수의 실제 값은 Heap에 저장됨)&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 그럼 static 변수는 언제 준비될까? &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;static 변수는 객체를 만들 때마다 생기는 것이 아니라, 해당 클래스가 JVM에 로딩될 때 클래스 단위로 준비된다&lt;/span&gt;&lt;br /&gt;&lt;span&gt;예를 들어 &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778487155394&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Student a = new Student();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;를 실행하면 JVM은 Student 클래스를 먼저 읽고, 그 클래스 정보와 static 변수 schoolName을 클래스 단위로 준비한다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; 그 다음에 a 객체가 Heap에 만들어진다&lt;/span&gt;&lt;br /&gt;&lt;span&gt;그래서 static 변수는 객체보다 먼저 준비될 수 있다. 객체를 100개 만들어도 static 변수는 100개 생기는 것이 아니라, 클래스가 로딩될 때 준비된 하나를 계속 공유한다&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 그럼 static 변수는 언제 사라질까? &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;static 변수는 일반 객체처럼 특정 객체 하나가 사라진다고 같이 사라지지 않는다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; 객체 a가 사라져도 Student 클래스의 static 변수 schoolName은 그대로 남아 있다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span&gt;static 변수는 해당 클래스가 JVM에서 더 이상 사용되지 않아 언로드될 때 함께 정리될 수 있다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; 일반적인 Spring Boot 애플리케이션에서는 서버가 실행되는 동안 주요 클래스들이 계속 사용되기 때문에, static 값도 보통 애플리케이션이 종료될 때까지 남아 있다고 이해하면 된다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; Spring Boot 앱이 종료되면 JVM 프로세스가 끝나고, 그 JVM 메모리 안에 있던 static 값도 사라진다고 이해하면 됨 &lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;static을 남발하면 왜 테스트와 유지보수가 어려울까? &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;static은 편하다 객체 안 만들어도 바로 쓸 수 있으니까.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778486400674&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;JwtUtil.createToken(...)
PasswordUtil.encrypt(...)
DateUtil.now()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;근데 문제가 있다 너무 쉽게 아무 데서나 접근할 수 있다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778486416517&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DiscountPolicy {
    static int discountRate = 10;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1778486422494&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DiscountPolicy.discountRate = 50;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;10에서 50으로 바꾼다고 가정하면 전부 영향을 받는다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A 테스트는 10% 할인을 기대함&lt;/li&gt;
&lt;li&gt;B 테스트가 static 값을 50%로 바꿈&lt;/li&gt;
&lt;li&gt;A&amp;nbsp;테스트가&amp;nbsp;갑자기&amp;nbsp;실패함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이런 식으로 static값은 공유 상태가 되기 쉽다 공유 상태란 쉽게 말해 여러 사람이 같이 쓰는 화이트보드와 같다&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;누가 뭘 지웠는지 누가 뭘 바꿨는지 모르면 나중에 찾기 어렵다 그래서 static은 특히 이런 경우 조심해야 한다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값이 바뀌는 static 변수&lt;/li&gt;
&lt;li&gt;외부 API를 호출하는 static 메서드&lt;/li&gt;
&lt;li&gt;현재&amp;nbsp;시간,&amp;nbsp;랜덤값,&amp;nbsp;토큰&amp;nbsp;생성처럼&amp;nbsp;테스트마다&amp;nbsp;결과가&amp;nbsp;달라지는&amp;nbsp;static&amp;nbsp;메서드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이런 것들은 테스트에서 가짜 객체로 바꾸기 어렵다&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Spring에서 무조건 static으로 안 만드는 이유 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring에서는 객체를 직접 만들지 않고 Spring이 객체를 관리한다 이 객체를 &lt;a href=&quot;https://winwin0219.tistory.com/311&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;Spring Bean&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;이라고 부른다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778486533942&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class PostService {
    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 PostService는 static으로 만들지 않는다 PostService는 혼자 일하는 게 아니라 PostRepository가 필요하다 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, 의존성이 있다&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PostService는 PostRepository가 필요함&lt;/li&gt;
&lt;li&gt;PostRepository는&amp;nbsp;DB&amp;nbsp;접근을&amp;nbsp;담당함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring은 이런 필요한 객체들을 대신 연결해준다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;PostService 만들 때 &amp;rarr; PostRepository 넣어줌 &amp;rarr; 필요한 설정 넣어줌 &amp;rarr; 테스트할 때 가짜 Repository로 바꿀 수도 있음&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 static으로 만들면 Spring이 관리하는 흐름에서 벗어나기 쉽다 &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778486674271&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PostService {
    public static void createPost() {
        // repository를 어떻게 넣을 건데?
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;static메서드는 객체 없이 호출되기 때문에 생성자를 통해 의존성을 주입받는 구조와 잘 맞지 않는다 그래서 Spring에서는 이렇게 생각한다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공통 상수 &amp;rarr; static final 가능&lt;/li&gt;
&lt;li&gt;상태 없는 단순 유틸 &amp;rarr; static 가능&lt;/li&gt;
&lt;li&gt;비즈니스&amp;nbsp;로직,&amp;nbsp;DB&amp;nbsp;접근,&amp;nbsp;외부&amp;nbsp;연동&amp;nbsp;&amp;rarr;&amp;nbsp;Spring&amp;nbsp;Bean으로&amp;nbsp;관리&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&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;여러 코드가 같이 바라보고 바꿀 수 있는 값&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Bean이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring이 대신 만들고 관리하는 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존성 주입(DI)이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 객체가 필요한 다른 객체를 직접 만들지 않고 외부에서 받는 방식&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 static은 객체마다 따로 생기는 값이 아니라 클래스 단위로 하나만 관리되는 값 또는 기능이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 공통 상수나 상태 없는 유틸 메서드에는 사용할 수 있지만, 값이 바뀌는 공유 상태나 DB 접근, 비즈니스 로직에는 남발하면 테스트와 유지보수가 어려워질 수 있다&lt;/p&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에서는 이런 비즈니스 객체를 static으로 만들기보다 Bean으로 등록하고, 필요한 의존성을 생성자를 통해 주입받는 방식이 더 자연스럽다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/☕ Java</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/319</guid>
      <comments>https://winwin0219.tistory.com/319#entry319comment</comments>
      <pubDate>Mon, 11 May 2026 17:13:31 +0900</pubDate>
    </item>
    <item>
      <title>[코드트리 후기] 코딩테스트 준비, 갭체크부터 시작하기</title>
      <link>https://winwin0219.tistory.com/318</link>
      <description>&lt;p style=&quot;color: #3f3f3f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.codetree.ai/ko/trail-info&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;코드트리&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;에서 챌린지를 한다고 해서 참여를 해보려고 한다&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #3f3f3f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #3f3f3f; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래 질문을 길잡이 삼아, 떠오르는 생각들을 편하게 들려주세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #2c2c2c; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #2c2c2c;&quot;&gt;갭체크를 응시하면서 어떤 생각이 드셨나요? 문제 난이도는 어땠나요?&lt;/span&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;span style=&quot;color: #2c2c2c;&quot;&gt;사용자의 수준에 맞춰서 문제가 나오는 것 같았다&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #2c2c2c;&quot;&gt;갭체크가 알려준 내 강점과 약점은 어떤 부분이었나요?&lt;/span&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;span style=&quot;color: #2c2c2c;&quot;&gt;문제가 조금만 길어져도 금방 포기해버리는 습관이 가장 큰 약점인 것 같고&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #2c2c2c;&quot;&gt;아는 문제가 나오면 빠르게 풀어버리는 게 강점인 것 같다&amp;nbsp;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #2c2c2c;&quot;&gt;아마 꾸준한 반복훈련이 동반된다면, 누구보다 빠르게 풀 가능성이 보였다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #2c2c2c;&quot;&gt;추천 학습 경로를 따라가 보니 어떤 점이 도움이 됐나요?&lt;/span&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;span style=&quot;color: #2c2c2c;&quot;&gt;아마 처음부터 어려운 문제나, 응용문제랍시고 새로운 문법을 요구한다면 포기했을 것 같은데, &lt;br /&gt;문법부터 차근차근 쌓아올라가는 게 큰 도움이 될 것 같다 기대가된다&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #2c2c2c;&quot;&gt;다른 학습 서비스와 비교해서 어떤 점이 좋았거나 아쉬웠나요?&lt;/span&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;span style=&quot;color: #2c2c2c;&quot;&gt;다른 학습 서비스는 문법을 익히는 시간은 주지 않는다 스스로 학습된 사용자만이 문제를 풀 수 있다는 점이 아쉬웠으며&amp;nbsp;&lt;br /&gt;코드트리의 가장 마음에 드는 점은, 커리큘럼과 진척도가 한 눈에 보인다는 점이다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #2c2c2c;&quot;&gt;아쉬운 점은 내 의지력인 것 같다 문장이 길어지면 의도를 파악하기가 매우 힘들고 이때 스스로가 문해력이 많이 딸리나?싶다 문제에 대한 해설풀이(문제의도?)가 있다면 좋을 것 같기도 하다 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #2c2c2c;&quot;&gt;또한 두번째 아쉬운 점은, 잔디심기다 연달아서 문제를 풀다보면, 다음 날 문제를 풀지 않았더라도 잔디가 심어진다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uIU6v/dJMcajhQfDR/oVERPpjWt0VWor6rrINuX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uIU6v/dJMcajhQfDR/oVERPpjWt0VWor6rrINuX0/img.png&quot; data-alt=&quot;위 결과물 또한, 1번부터 12초 소요시간이 걸렸다고 되어있는데 ㅋㅋㅋ 긴 문장일수록 빠른 포기를 해버린다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uIU6v/dJMcajhQfDR/oVERPpjWt0VWor6rrINuX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuIU6v%2FdJMcajhQfDR%2FoVERPpjWt0VWor6rrINuX0%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;941&quot; height=&quot;469&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위 결과물 또한, 1번부터 12초 소요시간이 걸렸다고 되어있는데 ㅋㅋㅋ 긴 문장일수록 빠른 포기를 해버린다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>갭체크</category>
      <category>코드트리 #코딩테스트 #코테공부 #코테준비 #알고리즘공부</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/318</guid>
      <comments>https://winwin0219.tistory.com/318#entry318comment</comments>
      <pubDate>Sun, 10 May 2026 14:44:02 +0900</pubDate>
    </item>
    <item>
      <title>[Java]String은 왜 불변인가 &amp;mdash; StringBuilder, StringBuffer와 차이</title>
      <link>https://winwin0219.tistory.com/317</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;String은 왜 불변인가&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/String.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; Java 공식문서&lt;/a&gt;에서 String을 이렇게 정의한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Strings are constant; their values cannot be changed after they are created.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면, String 객체는 &lt;b&gt;한번 만들어지면 내부 값을 절대 못 바꾼다&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778220383187&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String s = &quot;안녕&quot;;
s = &quot;안녕하세요&quot;; // 바꾼 거 아닌가?라는 의문이 들텐데&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바꾼 게 아니라 s라는 변수가 다른 객체를 가리키도록 바뀐 것이다 &quot;안녕&quot;이라는 객체 자체는 그대로 메모리에 살아있다&lt;/p&gt;
&lt;pre id=&quot;code_1778220413503&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;안녕&quot;      &amp;larr; 이 객체는 그대로 존재함
&quot;안녕하세요&quot; &amp;larr; 새로 만들어진 객체
s           &amp;larr; 이제 &quot;안녕하세요&quot;를 가리킴&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 불변으로 만들었냐&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. String Pool&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/lang/String.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 공식문서&lt;/a&gt;에 이런 말이 있다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The Java language provides special support for the string concatenation operator, and for conversion of other objects to strings. String objects are cached in a pool.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java는 String을 특별하게 취급해서, &lt;b&gt;같은 문자열이면 객체를 새로 만들지 않고 공유한다&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778220458676&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String a = &quot;민수&quot;;
String b = &quot;민수&quot;;

System.out.println(a == b); // true (같은 객체!)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;s랑 b가 같은 민수 객체를 공유하고 있다 근데 만약 String이 불변이 아니라면?&lt;/p&gt;
&lt;pre id=&quot;code_1778220518615&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;a.setValue(&quot;진우&quot;); // 만약 이게 된다면?
System.out.println(b); // &quot;진우&quot; 나와버림 &amp;larr; 대참사&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a를 바꿨는데 공유하고 있으니까 b도 바뀌어버리는 것.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 공유하려면 절대 못 바꾸게 해야 한다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 보안&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 연결할 때 자주 보는 코드가 있다&lt;/p&gt;
&lt;pre id=&quot;code_1778220615377&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String url = &quot;jdbc:mysql://localhost:3306/mydb&quot;;
connect(url);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connect() 메서드 안에 url을 몰래 바꿔버릴 수 있으면 큰일이라서 String이 불변이면 어디서도 못 바꾸니까 안전해진다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. hashCode 캐싱&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String은 HashMap의 key로 엄청 많이 쓰인다 hashCode()를 매번 계산하면 느리니까, 한번 계산한 hashCode를 내부에 저장해둔다 불변이라서 값이 안 바뀌고 그래서 캐싱이 가능한 것이다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;그럼 String 더하기는 어떻게 되냐&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1778220696210&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String s = &quot;안녕&quot;;
s = s + &quot; 민수&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;1. 안녕 객체 존재&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;3. 둘을 합친 안녕민수 객체를 새로 만듦&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. s가 새 객체를 가리킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 기존 안녕은 아무도 참조 안 하면 GC 대상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 더하기 한 번 할 때마다 새 객체가 만들어진다 그래서 아래 같은 코드는 위험하다&lt;/p&gt;
&lt;pre id=&quot;code_1778220761390&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String result = &quot;&quot;;
for (int i = 0; i &amp;lt; 10000; i++) {
    result += i; // 매 반복마다 새 객체 생성
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 번 반복하면 객체가 만 개 만들어지는 거라 메모리 낭비 + 느려진다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;그래서 나온 게 StringBuilder&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 공식문서&lt;/a&gt;에서 StringBuilder를 이렇게 설명한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A mutable sequence of characters. Provides an API compatible with StringBuffer, but with no guarantee of synchronization.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 가능한 문자 시퀀스. StringBuffer와 호환되는 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;핵심은, mutable(변경 가능)이다 append() 할 때마다 새로운 String 결과 객체를 계속 만들지 않고, 내부의 변경 가능한 버퍼에 문자를 누적한다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778220819103&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;StringBuilder sb = new StringBuilder();
for (int i = 0; i &amp;lt; 10000; i++) {
    sb.append(i); // 같은 객체에 이어붙임
}
String result = sb.toString();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 객체 하나로 끝나며 훨씬 빠르다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuffer.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에서 StringBuffer를 이렇게 설명한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A thread-safe, mutable sequence of characters.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 가능하지만 thread-safe다. 여러 스레드가 동시에 건드릴 수 있는 상황을 고려한 클래스다&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 68px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt; StringBuilder &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt; StringBuffer &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt; 동기화(thread-safe) &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;없음&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt; 속도 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;빠름&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt; 언제 씀 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&quot;&gt;단일 스레드&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px; text-align: center;&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;&lt;a href=&quot;https://docs.oracle.com/javase/9/docs/api/java/lang/StringBuilder.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 공식문서&lt;/a&gt;도 이렇게 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Where possible, it is recommended that this class be used in preference to StringBuffer as it will be faster under most implementations.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능한 경우, 대부분의 구현에서 더 빠르기 때문에 StringBuffer보다 StringBuilder를 쓰는 걸 권장한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 멀티 스레드 환경 아니면 StringBuilder를&amp;nbsp; 쓴다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문자열을 반복해서 이어붙이면 매번 새 String 객체가 만들어질 수 있어서 성능과 메모리 측면에서 비효율적이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 String은 불변이라 반복 연결 시 새 객체가 계속 생겨 비효율적이므로, 단일 스레드에서는 StringBuilder, 여러 스레드가 같은 버퍼를 공유하는 경우에는 동기화되는 StringBuffer를 고려한다&lt;/p&gt;</description>
      <category>Backend/☕ Java</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/317</guid>
      <comments>https://winwin0219.tistory.com/317#entry317comment</comments>
      <pubDate>Fri, 8 May 2026 15:26:21 +0900</pubDate>
    </item>
    <item>
      <title>[JPA]영속성 컨텍스트</title>
      <link>https://winwin0219.tistory.com/316</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;분량이 꽤 많아보이는데, 프로젝트에 접목시키며 이해하는 게 재밌어서 추가추가하다보니 목차만 열여덟개다 ^0^/&lt;/p&gt;
&lt;div class=&quot;toc-box&quot;&gt;
&lt;p class=&quot;toc-title&quot; 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;&lt;a href=&quot;#persistence-context&quot;&gt;영속성 컨텍스트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#entitymanager-relation&quot;&gt;EntityManager와 영속성 컨텍스트의 관계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#identity&quot;&gt;영속성 컨텍스트의 핵심 특징 1: 같은 id는 같은 객체다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#first-level-cache&quot;&gt;영속성 컨텍스트의 핵심 특징 2: 1차 캐시&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#entity-lifecycle&quot;&gt;영속성 컨텍스트의 핵심 특징 3: 엔티티 생명주기 관리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#dirty-checking&quot;&gt;Dirty Checking: 왜 save 없이 UPDATE가 나가나&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#flush&quot;&gt;Flush: DB에 실제 SQL이 나가는 시점&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#flush-timing&quot;&gt;Flush는 언제 일어나나&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#flush-commit&quot;&gt;Flush와 Commit은 다르다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#write-behind&quot;&gt;쓰기 지연: INSERT/UPDATE/DELETE를 바로 안 보내는 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#save-after-find&quot;&gt;findById 후 save를 또 부르면 왜 어색한가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#merge&quot;&gt;merge는 detached 객체를 다시 managed로 붙이는 것이 아니다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#transaction-scope&quot;&gt;영속성 컨텍스트는 트랜잭션 범위와 강하게 묶인다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#no-transactional&quot;&gt;@Transactional이 없으면 왜 수정이 안 되나&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#lazy-loading&quot;&gt;영속성 컨텍스트와 지연 로딩&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#large-context&quot;&gt;영속성 컨텍스트가 너무 커지면 문제다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#stale-data&quot;&gt;트랜잭션 안에서 조회한 값은 DB 최신값이 아닐 수도 있다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#isolation&quot;&gt;영속성 컨텍스트와 DB 트랜잭션 격리 수준은 다른 개념이다&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;공식문서&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;에서는 이렇게 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Every instance of EntityManager has an associated persistence context.&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;모든 EntityManager 인스턴스는 연결된 영속성 컨텍스트를 가진다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A persistence context is a set of entity instances in which for any given persistent entity identity there is a unique entity instance.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 엔티티 인스턴스들의 집합이며, 특정 영속 엔티티 식별자마다 유일한 엔티티 인스턴스가 존재한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Within the persistence context, the entity instances and their lifecycle are managed.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트 안에서 엔티티 인스턴스와 그 생명주기가 관리된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, EntityManager는 영속성 컨텍스트를 다루는 API이고, 영속성 컨텍스트는 실제로 엔티티 객체들을 붙잡고 관리하는 공간이다&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; EntityManager란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티를 저장, 조회, 삭제, 병합하는 JPA 핵심 API&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 직접 영속성 컨텍스트를 만지는 게 아니라, EntityManager라는 객체가 그 방의 문을 열고 닫고 안에 뭔가 넣고 꺼내는 역할을 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Persistence Context란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityManager가 관리하는 엔티티 보관/추적 공간&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;persistence-context&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;영속성 컨텍스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 @Transactional 안에서 JPA가 엔티티 객체를 추적 가능한 상태로 올려두는 메모리 공간이다&lt;/p&gt;
&lt;pre id=&quot;code_1778122928695&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void updatePost(Long postId, String title) {
    Post post = postRepository.findById(postId)
            .orElseThrow();

    post.changeTitle(title);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;save()를 안 불렀는데도 UPDATE가 나가는 이유는, post가 조회되는 순간 영속성 컨텍스트에 들어가서 managed상태가 되기 때문이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://docs.hibernate.org/orm/6.6/javadocs/org/hibernate/Session.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hibernate 공식 Javadoc&lt;/a&gt;&lt;/span&gt;&lt;/u&gt;&lt;/b&gt;도 이걸 정확히 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Persistent instances are held in a managed state by the persistence context.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속 인스턴스들은 영속성 컨텍스트에 의해 관리 상태로 유지된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Any change to the state of a persistent instance is automatically detected and eventually flushed to the database.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속 인스턴스 상태의 모든 변경은 자동으로 감지되고 결국 DB로 flush된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;This process of automatic change detection is called dirty checking&amp;hellip;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 자동 변경 감지 과정을 dirty checking이라고 부른다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Managed란?&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;b&gt;영속성 컨텍스트란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA가 엔티티 객체들을 담아두는 &lt;b&gt;임시 방, 이 방 안에 들어온 엔티티는 JPA가 감시한다. 값이 바뀌면 JPA가 알아채고, 나중에 DB에 자동으로 반영해줌 &lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;entitymanager-relation&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;EntityManager와 영속성 컨텍스트의 관계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityManager는 영속성 컨텍스트를 조작하는 손잡이다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/entitymanager&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Jakarta Persistence 공식 API 문서&lt;/a&gt;&lt;/span&gt;&lt;/u&gt;&lt;/b&gt;에서는 이렇게 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The EntityManager API is used to perform operations that affect the state of the persistence context, or that modify the lifecycle state of individual entity instances.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityManager API는 영속성 컨텍스트의 상태에 영향을 주거나 개별 엔티티 인스턴스의 생명주기 상태를 변경하는 작업에 사용된다&lt;/p&gt;
&lt;pre id=&quot;code_1778123141290&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;entityManager.persist(post);
entityManager.find(Post.class, 1L);
entityManager.remove(post);
entityManager.merge(post);
entityManager.flush();
entityManager.clear();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 메서드들은 전부 DB를 직접 만진다기보다, 우선 영속성 컨텍스트 안의 엔티티 상태를 바꾸는 명령으로 이해해야 한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드를 보자&lt;/p&gt;
&lt;pre id=&quot;code_1778123175287&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;entityManager.persist(post);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 호출했다고 항상 그 자리에서 즉시 INSERT SQL이 나가는 게 아니다&lt;/p&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;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://docs.hibernate.org/orm/6.6/introduction/html_single/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Hibernate 문서&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/u&gt;에 따르면 SQL은 보통 persist()나 remove() 같은 메서드와 동기적으로 바로 실행되지 않는다 flush 때 DB와 동기화된다&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;identity&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 영속성 컨텍스트의 핵심 특징 1: 같은 id는 같은 객체다 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서 제일 중요한 부분은 이거다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;for any given persistent entity identity there is a unique entity instance&lt;/blockquote&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;예를들어 DB에 posts.id = 1인 row가 있다고 하자&lt;/p&gt;
&lt;pre id=&quot;code_1778123345822&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post post1 = entityManager.find(Post.class, 1L);
Post post2 = entityManager.find(Post.class, 1L);

System.out.println(post1 == post2); // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 트랜잭션, 같은 영속성 컨텍스트 안에서는 post1과 post2가 같은 Java 객체다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 중요하냐면, 같은 DB row를 Java 객체 여러 개로 들고 있으면 데이터가 꼬인다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 이런 일이 생길 수 잇다&lt;/p&gt;
&lt;pre id=&quot;code_1778123392492&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post a = findPost(1L); // DB row #1을 표현하는 객체 A
Post b = findPost(1L); // DB row #1을 표현하는 객체 B (다른 객체)

a.changeTitle(&quot;제목 A&quot;);
b.changeTitle(&quot;제목 B&quot;);
// 어떤 값이 DB에 반영돼야 해??  &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, a와 b가 서로 다른 객체라면 마지막에 어떤 값이 DB에 반영되어야 하는지 헷갈린다 그래서 영속성 컨텍스트는 같은 id는 같은 객체 하나로 유지한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate공식 소개 문서도 이걸 1차 캐시 관점에서 설명한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A persistence context is a sort of cache; we sometimes call it the &quot;first-level cache&quot; ...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 일종의 캐시이며, 때때로 &amp;ldquo;first-level cache&amp;rdquo;라고 부른다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;the context holds a unique mapping from the identifier of the entity instance to the instance itself.&lt;/blockquote&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;span style=&quot;color: #ee2323;&quot;&gt;JPA는 이걸 방지하려고 &lt;b&gt;하나의 id &amp;rarr; 하나의 객체&lt;/b&gt; 보장을 함. 이게 &lt;b&gt;동일성 보장&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;first-level-cache&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 영속성 컨텍스트의 핵심 특징 2: 1차 캐시 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일성 보장이 작동하는 이유는 바로 1차 캐시때문이다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트 내부에는 이런 형태로 Map이 있다고 생각하면된다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778126538583&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Map&amp;lt;식별자, 엔티티 객체&amp;gt;
예: { 1L &amp;rarr; Post#1 객체, 2L &amp;rarr; Post#2 객체 }&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;pre id=&quot;code_1778126563945&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. 첫 번째 조회
Post post1 = entityManager.find(Post.class, 1L);
// &amp;rarr; Map에 {1L: post1}이 없음 &amp;rarr; DB에 SELECT 쿼리 실행 &amp;rarr; 결과를 Map에 저장

// 2. 두 번째 조회
Post post2 = entityManager.find(Post.class, 1L);
// &amp;rarr; Map에 {1L: post1}이 이미 있음 &amp;rarr; DB 안 가고 post1 반환&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;Jakarta API 문서도 find()에 대해 이렇게 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;If the entity instance is contained in the persistence context, it is returned from there.&lt;/blockquote&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;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;1차 캐시는 성능 최적화 용도도 있지만,&amp;nbsp;&lt;/span&gt;&lt;b&gt;본질은 같은 트랜잭션 안에서 엔티티 동일성을 보장하는 저장소&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;다 중복 조회 줄이는 건 덤으로 얻는 효과&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;entity-lifecycle&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 영속성 컨텍스트의 핵심 특징 3: 엔티티 생명주기 관리 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 공식문서는 엔티티 생명주기를 이렇게 나눈다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;An entity instance can be characterized as being new, managed, detached, or removed.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 인스턴스는 new, managed, detached, removed 상태로 구분될 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1778126671027&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 항상 네 가지 상태 중 하나
New(Transient) &amp;rarr; Managed &amp;rarr; Detached
                        ↘ Removed&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 1. New / Transient 상태 : JPA 모르는 순수 Java 객체 &lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A new entity instance has no persistent identity, and is not yet associated with a persistence context.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;new 엔티티 인스턴스는 영속 식별자가 없고, 아직 영속성 컨텍스트와 연결되어 있지 않다&lt;/p&gt;
&lt;pre id=&quot;code_1778123695402&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post post = new Post(&quot;title&quot;, &quot;content&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태는 그냥 Java 객체다 DB와도 관계없고 영속성 컨텍스트도 모른다 이떄 값을 바꿔도 JPA는 관심없다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778123726248&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;post.changeTitle(&quot;new title&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무 SQL도 안 나간다&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Persistent identity란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 엔티티를 식별하는 값. 보통 @Id&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 2. Managed 상태 : 영속성 컨텍스트가 감시 중인 상태 &lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A managed entity instance is an instance with a persistent identity that is currently associated with a persistence context.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;managed 엔티티 인스턴스는 영속 식별자를 가지고 현재 영속성 컨텍스트와 연결된 인스턴스다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해,&amp;nbsp;영속성 컨텍스트의 방에 들어온 상태다 값이 바뀌면 JPA가 알아채고 flush 시점에 자동으로 SQL 생성한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Managed가 되는 두 가지 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778126754636&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 방법 1. persist() &amp;mdash; 새 엔티티를 저장 예약
entityManager.persist(post);

// 방법 2. find() 또는 findById() &amp;mdash; 조회하면 자동으로 managed
Post post = entityManager.find(Post.class, 1L);
Post post = postRepository.findById(1L).orElseThrow(); // Spring Data JPA&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;조회된 엔티티는 &lt;b&gt;자동으로 managed&lt;/b&gt; 상태가 된다 이게 dirty checking 의 전제.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 3. Detached 상태 : JPA 관리가 끊긴 상태 &lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Detached entity instances continue to live outside of the persistence context in which they were persisted or retrieved.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;detached 엔티티 인스턴스는 자신이 저장되거나 조회되었던 영속성 컨텍스트 밖에서도 계속 존재한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Their state is no longer guaranteed to be synchronized with the database state.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 상태는 더 이상 DB 상태와 동기화된다고 보장되지 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 끝나면 영속성 컨텍스트도 닫힌다 그러면 그 안에 있던 엔티티들은 전부 datached 상태가 된다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778123845496&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public Post getPost(Long id) {
    return postRepository.findById(id).orElseThrow();
} // &amp;larr; 이 중괄호 넘어가는 순간 트랜잭션 종료, post는 detached가 됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밖에서 값을 바꿔도&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778123864107&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post post = postService.getPost(1L); // 이미 detached
post.changeTitle(&quot;수정&quot;);             // JPA가 추적 안 함 &amp;rarr; UPDATE 안 나감&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 4. Removed 상태 : 삭제 예약된 상태 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서의 remove() 설명은 이렇다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Mark a managed entity instance as removed, resulting in its deletion from the database when the persistence context is synchronized with the database.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;managed 엔티티 인스턴스를 removed로 표시하고, 영속성 컨텍스트가 DB와 동기화될 때 DB에서 삭제되게 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 포인트는 remove() 호출 즉시 DELETE SQL이 나간다는 뜻이 아니다 &lt;b&gt;삭제 예정 상태로 표시&lt;/b&gt;되고, flush 때 DELETE가 나간다&lt;/p&gt;
&lt;pre id=&quot;code_1778123907206&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post post = entityManager.find(Post.class, 1L);
entityManager.remove(post);
// &amp;rarr; 즉시 DELETE SQL이 나가는 게 아니라, &quot;삭제 예정&quot; 표시만 함
// &amp;rarr; flush 시점에 DELETE SQL 생성됨&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;dirty-checking&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Dirty Checking: 왜 save 없이 UPDATE가 나가나 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dirty Checking은 영속성 컨텍스트가 managed 엔티티의 변경을 감지하는 기능이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 공식 Javadoc에서&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Any change to the state of a persistent instance is automatically detected and eventually flushed to the database.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;persistent 인스턴스 상태의 변경은 자동으로 감지되고 결국 DB로 flush된다라고 말하고 있다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;This process of automatic change detection is called dirty checking&amp;hellip;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 자동 변경 감지 과정을 dirty checking이라고 한다&lt;/p&gt;
&lt;pre id=&quot;code_1778124064312&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void updateTitle(Long postId, String newTitle) {
    Post post = postRepository.findById(postId).orElseThrow();
    post.changeTitle(newTitle);
    // save() 어디에도 없음!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 UPDATE SQL이 왜 나갈까?&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;@Transactional&amp;nbsp;&amp;rarr;&amp;nbsp;트랜잭션&amp;nbsp;시작,&amp;nbsp;영속성&amp;nbsp;컨텍스트&amp;nbsp;열림 &lt;br /&gt;2.&amp;nbsp;findById()&amp;nbsp;&amp;rarr;&amp;nbsp;DB에서&amp;nbsp;Post&amp;nbsp;조회 &lt;br /&gt;3.&amp;nbsp;Post를&amp;nbsp;영속성&amp;nbsp;컨텍스트에&amp;nbsp;저장&amp;nbsp;(managed&amp;nbsp;상태) &lt;br /&gt;4.&amp;nbsp;이&amp;nbsp;시점의&amp;nbsp;Post&amp;nbsp;상태를&amp;nbsp;&quot;스냅샷&quot;으로&amp;nbsp;따로&amp;nbsp;기억해둠 &lt;br /&gt;5.&amp;nbsp;post.changeTitle(newTitle)&amp;nbsp;&amp;rarr;&amp;nbsp;Java&amp;nbsp;객체&amp;nbsp;값&amp;nbsp;변경&amp;nbsp;(아직&amp;nbsp;SQL&amp;nbsp;없음) &lt;br /&gt;6.&amp;nbsp;메서드&amp;nbsp;끝&amp;nbsp;&amp;rarr;&amp;nbsp;트랜잭션&amp;nbsp;commit&amp;nbsp;직전에&amp;nbsp;flush&amp;nbsp;실행 &lt;br /&gt;7.&amp;nbsp;flush:&amp;nbsp;현재&amp;nbsp;Post&amp;nbsp;상태&amp;nbsp;vs&amp;nbsp;스냅샷&amp;nbsp;비교&amp;nbsp;&amp;rarr;&amp;nbsp;달라졌으면&amp;nbsp;UPDATE&amp;nbsp;SQL&amp;nbsp;생성 &lt;br /&gt;8.&amp;nbsp;DB에&amp;nbsp;UPDATE&amp;nbsp;실행 &lt;br /&gt;9.&amp;nbsp;commit&amp;nbsp;&amp;rarr;&amp;nbsp;확정&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;스냅샷이 핵심&lt;/b&gt;이다. JPA는 엔티티를 영속성 컨텍스트에 넣을 때, &lt;b&gt;그 순간의 상태를 복사해서 따로 저장&lt;/b&gt;한다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;flush할 때 현재 상태랑 비교해서 뭐가 바뀌었는지 알아내는 것이다 이게 바로 &lt;b&gt;Dirty Checking&lt;/b&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate User Guide도 UPDATE 생성 시점을 이렇게 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The UPDATE statement is generated by EntityUpdateAction during flushing if the managed entity has been marked modified.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;managed 엔티티가 modified로 표시되면 flush 중에 EntityUpdateAction에 의해 UPDATE 문이 생성된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The dirty checking mechanism is responsible for determining if a managed entity has been modified since it was first loaded.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dirty checking 메커니즘은 managed 엔티티가 처음 로드된 이후 수정되었는지 판단하는 책임이 있다&lt;/p&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;color: #ee2323;&quot;&gt;&lt;b&gt; 단, managed 상태에서만 작동한다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778124116659&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post post = new Post();        // new &amp;rarr; dirty checking 없음
post.changeTitle(&quot;A&quot;);         // 추적 안 됨

entityManager.detach(post);   // detached &amp;rarr; dirty checking 없음
post.changeTitle(&quot;B&quot;);         // 추적 안 됨

Post post = entityManager.find(Post.class, 1L); // managed!
post.changeTitle(&quot;C&quot;);         // 이건 추적됨&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;flush&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Flush: DB에 실제 SQL이 나가는 시점 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flush는 &lt;b&gt;영속성 컨텍스트의 변경 사항을 DB SQL로 동기화하는 과정&lt;/b&gt;이다 Hibernate 공식문서에서는 아래처럼 말하고 있다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Flushing is the process of synchronizing the state of the persistence context with the underlying database.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flush는 영속성 컨텍스트의 상태를 underlying database와 동기화하는 과정이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 정말 중요한 문장으론,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The persistence context acts as a transactional write-behind cache, queuing any entity state change.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 transactional write-behind cache처럼 동작하며, 엔티티 상태 변경을 큐에 쌓는다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;changes are first applied in-memory and synchronized with the database during flush time.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경은 먼저 메모리에 적용되고, flush 시점에 DB와 동기화된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The flush operation takes every entity state change and translates it to an INSERT, UPDATE or DELETE statement.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flush 작업은 각 엔티티 상태 변경을 INSERT, UPDATE, DELETE 문으로 변환한다&lt;/p&gt;
&lt;pre id=&quot;code_1778124554582&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;postRepository.save(post);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1778124561288&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;post.changeTitle(&quot;수정&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;이 순간 DB가 바로 바뀐다고 생각하면 안 된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;[영속성 컨텍스트] &amp;rarr; flush &amp;rarr; [DB에 SQL 전송] &amp;rarr; commit &amp;rarr; [DB 확정]&lt;/blockquote&gt;
&lt;p id=&quot;flush-timing&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; flush는 SQL을 DB로 보내는 것, commit은 그 트랜잭션을 최종 확정하는 것&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Write-behind란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 SQL을 즉시 실행하지 않고 모아뒀다가 flush 때 실행하는 방식&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Flush는 언제 일어나는 세가지 시점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;트랜잭션&amp;nbsp;commit&amp;nbsp;직전&amp;nbsp;(기본) &lt;br /&gt;2.&amp;nbsp;JPQL/HQL&amp;nbsp;쿼리&amp;nbsp;실행&amp;nbsp;직전&amp;nbsp;(필요할&amp;nbsp;때) &lt;br /&gt;3.&amp;nbsp;entityManager.flush()&amp;nbsp;직접&amp;nbsp;호출&lt;/p&gt;
&lt;pre id=&quot;code_1778127228586&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post post = new Post(&quot;title&quot;, &quot;content&quot;);
entityManager.persist(post); // 저장 예약, 아직 DB에 없음

// 이 쿼리 결과에 방금 persist한 Post가 포함되어야 함
List&amp;lt;Post&amp;gt; posts = entityManager
    .createQuery(&quot;select p from Post p&quot;, Post.class)
    .getResultList();&lt;/code&gt;&lt;/pre&gt;
&lt;p id=&quot;flush-commit&quot; data-ke-size=&quot;size16&quot;&gt;만약, flush 없이 쿼리를 날리면 DB에는 아직 INSERT가 안 됐으니까 방금 만든 Post가&amp;nbsp; 결과에 안나온다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;그래서 Hibernate가 &lt;b&gt;쿼리 결과에 영향을 줄 수 있으면, 쿼리 전에 flush를 먼저 실행&lt;/b&gt;한다&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Flush와 Commit은 다르다 &lt;/b&gt;&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;flush&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;영속성 컨텍스트 변경을 SQL로 DB에 보냄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;commit&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;트랜잭션을 확정함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;rollback&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;트랜잭션 변경을 취소함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre id=&quot;code_1778124764867&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void test() {
    Post post = new Post(&quot;title&quot;, &quot;content&quot;);
    entityManager.persist(post);
    
    entityManager.flush(); // &amp;larr; SQL은 DB에 전송됨
    
    throw new RuntimeException(); // &amp;larr; 예외 발생
    // 메서드가 예외로 끝나면 @Transactional이 rollback 처리
}&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;flush() 때 INSERT SQL이 DB로 전송됨&lt;/li&gt;
&lt;li&gt;하지만 RuntimeException 때문에 @Transactional이 rollback 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최종적으로 DB에 저장 안 됨&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉, flush가 됐어도 commit 전에 rollback되면 원상보구다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;write-behind&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 쓰기 지연: INSERT/UPDATE/DELETE를 바로 안 보내는 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트 내부에는 &lt;b&gt;쓰기 지연 저장소(Action Queue)&lt;/b&gt; 가 있다&lt;/p&gt;
&lt;pre id=&quot;code_1778124805261&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void createMany() {
    entityManager.persist(new Post(&quot;A&quot;)); // INSERT 예약
    entityManager.persist(new Post(&quot;B&quot;)); // INSERT 예약
    entityManager.persist(new Post(&quot;C&quot;)); // INSERT 예약
    // 아직 SQL 안 나감
} // &amp;larr; 여기서 commit 직전에 flush &amp;rarr; INSERT A, B, C 한꺼번에 나감&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL을 모아서 한꺼번에 보낼 수 있어 (DB round-trip 줄임)&lt;/li&gt;
&lt;li&gt;Hibernate가 Batch Insert 최적화 적용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외, &lt;/b&gt;ID 생성 전략이 IDENTITY이면 (@GeneratedValue(strategy = IDENTITY))&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;persist() 직후에 즉시 INSERT가 나간다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면 DB가 INSERT를 실행해야 PK를 알 수 있어서, JPA가 1차 캐시에 저장하려면 id를 먼저 알아야 한&lt;/p&gt;
&lt;h2 id=&quot;save-after-find&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; findById 후 save를 또 부르면 왜 어색한가 &lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1778124879883&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 어색한 패턴
@Transactional
public void updatePost(Long id, String title) {
    Post post = postRepository.findById(id).orElseThrow();
    post.changeTitle(title);
    postRepository.save(post); // &amp;larr; 이게 왜 어색하냐
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;findById()로 조회한 post는 이미 managed 상태다 managed 엔티티는 dirty checking 대상이라, changeTitle()만 해도 flush 때 자동으로 UPDATE가 나간다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 save()가 불필요한 경우가 많다 Spring Data JPA의 save()는 내부에서 이미 managed면 merge를 시도하는데, merge는 아래에서 설명하지만 생각보다 위험하&lt;/p&gt;
&lt;pre id=&quot;code_1778124942725&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 자연스러운 패턴
@Transactional
public void updatePost(Long id, String title) {
    Post post = postRepository.findById(id).orElseThrow();
    post.changeTitle(title);
    // save() 없어도 flush 때 UPDATE 나감
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;b&gt;신규 저장&lt;/b&gt;에는 당연히 save() 써야한다&lt;/p&gt;
&lt;pre id=&quot;code_1778127451178&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post post = new Post(&quot;title&quot;, &quot;content&quot;); // new 상태
postRepository.save(post); // persist() 호출 &amp;rarr; managed 상태로 전환&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;merge&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; merge는 detached 객체를 다시 managed로 붙이는 것이 아니다 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 무슨 말이냐면, 공식문서부터 보자&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The merge operation allows for the propagation of state from detached entities onto persistent entities managed by the entity manager.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;merge 작업은 detached 엔티티의 상태를 EntityManager가 관리하는 persistent 엔티티로 전파할 수 있게 한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;If X is a detached entity, the state of X is copied onto a pre-existing managed entity instance X'&amp;hellip;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X가 detached 엔티티이면, X의 상태는 같은 식별자의 기존 managed 엔티티 X'에 복사된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 건 &lt;b&gt;detached 객체 자체가 managed로 바뀌는 게 아니라, 그 상태가 managed 객체로 복사된다&lt;/b&gt;는 점이다&lt;/p&gt;
&lt;pre id=&quot;code_1778125054165&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post detachedPost = new Post();
detachedPost.setId(1L);
detachedPost.changeTitle(&quot;수정된 제목&quot;);

Post managedPost = entityManager.merge(detachedPost);
// &amp;uarr; 반환값이 중요함!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;merge의 실제 동작&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;detachedPost의 id(1L)로 DB에서 또는 1차 캐시에서 Post#1을 조회&lt;/li&gt;
&lt;li&gt;조회된 Post#1 (managed) 에 detachedPost의 값을 복사함&lt;/li&gt;
&lt;li&gt;그 managed 엔티티를 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;detachedPost 자체가 managed로 바뀌는 게 아니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778125063926&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;detachedPost.changeTitle(&quot;A&quot;); // 추적 안 됨
managedPost.changeTitle(&quot;B&quot;);  // 추적됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;merge의 위험성&lt;/b&gt;: DTO처럼 일부 필드만 있는 객체를 merge하면&lt;/p&gt;
&lt;pre id=&quot;code_1778127507855&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post dto = new Post();
dto.setId(1L);
dto.setTitle(&quot;수정된 제목&quot;);
// content 필드는 null

entityManager.merge(dto);
// &amp;rarr; DB의 Post#1의 content가 null로 덮어써짐&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일반적으로 권장하는 패턴&lt;/p&gt;
&lt;pre id=&quot;code_1778127522756&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// DB에서 managed 엔티티 조회 후 필요한 필드만 직접 변경
Post post = postRepository.findById(id).orElseThrow();
post.changeTitle(&quot;수정된 제목&quot;);
// dirty checking으로 UPDATE&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;transaction-scope&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 영속성 컨텍스트는 트랜잭션 범위와 강하게 묶인다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 &lt;b&gt;트랜잭션이 시작될 때 열리고, 트랜잭션이 끝날 때 닫힌다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;@Transactional 시작 &amp;rarr; 영속성 컨텍스트 열림 &lt;br /&gt;@Transactional 종료 &amp;rarr; 영속성 컨텍스트 닫힘 &amp;rarr; 모든 엔티티 detached&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서도 이걸 그대로 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The persistence context ends when the associated JTA transaction commits or rolls back&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결된 트랜잭션이 commit 또는 rollback되면 영속성 컨텍스트가 끝난다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;all entities that were managed by the EntityManager become detached.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityManager가 관리하던 모든 엔티티는 detached가 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로 보면 이렇다&lt;/p&gt;
&lt;pre id=&quot;code_1778127940799&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void updatePost(Long id, String title) {
    // 영속성 컨텍스트 열려 있음
    Post post = postRepository.findById(id).orElseThrow(); // managed
    post.changeTitle(title); // 변경 감지됨
}
// &amp;larr; 여기서 트랜잭션 종료
//   &amp;rarr; flush &amp;rarr; UPDATE &amp;rarr; commit &amp;rarr; 영속성 컨텍스트 닫힘 &amp;rarr; post는 detached&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 @Transactional이 없으면, findById() 호출 순간에만 짧게 영속성 컨텍스트가 열렸다 바로 닫혀버린다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 반환된 post는 이미 detached라서 changeTitle()을 해도 JPA가 추적하지 못하고 UPDATE가 나가지 않는다&lt;/p&gt;
&lt;h2 id=&quot;no-transactional&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; @Transactional이 없으면 왜 수정이 안 되나 &lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1778125218175&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// @Transactional 없음
public void updatePost(Long id, String title) {
    Post post = postRepository.findById(id).orElseThrow();
    post.changeTitle(title);
    // UPDATE 안 나감
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;@Transactional이 없으면, findById() 호출할 때 짧게 살다 사라지는 영속성 컨텍스트가 열리고 닫혀&lt;/li&gt;
&lt;li&gt;메서드 밖에서 changeTitle()을 호출하는 시점엔 이미 영속성 컨텍스트가 닫혀 있어서 post는 detached&lt;/li&gt;
&lt;li&gt;dirty checking이 작동 안 함&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;pre id=&quot;code_1778125251332&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void updatePost(...) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 조회 전용이라면&lt;/p&gt;
&lt;pre id=&quot;code_1778125263678&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(readOnly = true)
public PostResponse getPost(...) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 고려한다 Hibernate 공식문서도 read-only mode에서는 dirty-check가 필요 없다고 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;It&amp;rsquo;s not necessary to dirty-check an entity instance in read-only mode.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;read-only mode의 엔티티 인스턴스는 dirty-check할 필요가 없다&lt;/p&gt;
&lt;h2 id=&quot;lazy-loading&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 영속성 컨텍스트와 지연 로딩 &lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hibernate Javadoc&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;When an associated entity has not yet been fetched from the database, references to the unfetched entity are represented by uninitialized proxies.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관 엔티티가 아직 DB에서 fetch되지 않았으면, 가져오지 않은 엔티티 참조는 초기화되지 않은 proxy로 표현된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The state of an unfetched entity is automatically fetched from the database when a method of its proxy is invoked, if and only if the proxy is associated with an open session.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가져오지 않은 엔티티의 상태는 proxy 메서드가 호출될 때 자동으로 DB에서 fetch되는데, 단 proxy가 열린 session과 연결되어 있을 때만 가능하다&lt;/p&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;color: #333333; text-align: start;&quot;&gt;@ManyToOne(fetch = LAZY) 같은 지연 로딩 관계는 처음에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;프록시 객체&lt;/b&gt;로 채워져 있어서 실제 데이터는 그 필드에 접근하는 순간 DB에서 가져온다&lt;/p&gt;
&lt;pre id=&quot;code_1778125333238&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void getComment(Long id) {
    Comment comment = commentRepository.findById(id).orElseThrow();
    // comment.user는 지금 proxy 객체 (실제 데이터 아직 없음)
    
    String nickname = comment.getUser().getNickname();
    // &amp;uarr; getNickname() 호출하는 순간 영속성 컨텍스트를 통해 DB SELECT 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 끝난 뒤에 지연 로딩 시도하면&lt;/p&gt;
&lt;pre id=&quot;code_1778125349062&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Comment comment = commentService.getComment(id); // 트랜잭션 끝남, detached

comment.getUser().getNickname(); //  LazyInitializationException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 proxy가 열려 있는 영속성 컨텍스트와 연결되어 있지 않으면 LazyInitializationException이 날 수 있다&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;LazyInitializationException: could not initialize proxy - no Session&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;프록시를 초기화할 수 없음 - Session(영속성 컨텍스트)이 없음&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 N+1 이슈로도 이어지는 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/314&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://winwin0219.tistory.com/314&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778125410420&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[JPA]N+1 이슈&quot; data-og-description=&quot;Fetching은 성능에 직접적인 영향을 준다https://docs.hibernate.org/orm/5.2/userguide/html_single/chapters/fetching/Fetching.html FetchingFetching, essentially, is the process of grabbing data from the database and making it available to the appl&quot; data-og-host=&quot;winwin0219.tistory.com&quot; data-og-source-url=&quot;https://winwin0219.tistory.com/314&quot; data-og-url=&quot;https://winwin0219.tistory.com/314&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dSiU2F/dJMb8Z3vpW0/WurlDRvHR3EWkJzatobeUk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bHbxlT/dJMb9g5faIb/K2bdRKF8OfxBPwXIq13xDk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/314&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://winwin0219.tistory.com/314&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dSiU2F/dJMb8Z3vpW0/WurlDRvHR3EWkJzatobeUk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bHbxlT/dJMb9g5faIb/K2bdRKF8OfxBPwXIq13xDk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[JPA]N+1 이슈&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Fetching은 성능에 직접적인 영향을 준다https://docs.hibernate.org/orm/5.2/userguide/html_single/chapters/fetching/Fetching.html FetchingFetching, essentially, is the process of grabbing data from the database and making it available to the appl&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;winwin0219.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 id=&quot;large-context&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 영속성 컨텍스트가 너무 커지면 문제다 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 관리하는 엔티티를 계속 참조한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Hibernate Javadoc&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A persistence context holds hard references to all its entities and prevents them from being garbage collected.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 모든 엔티티에 대한 hard reference를 가지고 있어 GC 대상이 되지 않게 한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Therefore, a Session is a short-lived object, and must be discarded as soon as a logical transaction ends.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Session은 짧게 살아야 하는 객체이며, 논리 트랜잭션이 끝나면 버려져야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영관점에서 중요하다 예를들어 10만 건을 한 트랜잭션에서 전부 조회/수정하면&lt;/p&gt;
&lt;pre id=&quot;code_1778125481658&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void updateAll() {
    List&amp;lt;Post&amp;gt; posts = postRepository.findAll(); // 10만 건 조회
    for (Post post : posts) {
        post.changeSomething();
    }
} // flush 시 10만 건 dirty check + UPDATE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트가 10만 개 엔티티를 붙잡는다&amp;nbsp;&lt;/p&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;&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;메모리에 10만 개 엔티티 올라옴 (OutOfMemory 위험)&lt;/li&gt;
&lt;li&gt;flush 때 10만 개 다 dirty check&lt;/li&gt;
&lt;li&gt;트랜잭션이 길어짐 &amp;rarr; DB lock 오래 잡음&lt;/li&gt;
&lt;li&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;Hibernate 문서도 flushing은 비용이 있을 수 있다고 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Since flushing is a somewhat expensive operation (the session must dirty-check every entity in the persistence context)&amp;hellip;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flush는 다소 비싼 작업인데, session이 영속성 컨텍스트의 모든 엔티티를 dirty-check해야 하기 때문이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 대량 처리에서는 보통 batch 단위로 끊고 아래처럼 고려한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 해결 &lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778125525550&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 배치 단위로 처리
for (batch in batches) {
    processBatch(batch);
    entityManager.flush();
    entityManager.clear(); // 영속성 컨텍스트 비워서 메모리 해방
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;stale-data&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 트랜잭션 안에서 조회한 값은 DB 최신값이 아닐 수도 있다 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 같은 id를 한 번 관리하기 시작하면, 다시 조회해도 그 객체를 반환한다&lt;/p&gt;
&lt;pre id=&quot;code_1778125549847&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Post post1 = entityManager.find(Post.class, 1L);
// &amp;rarr; DB에서 title = &quot;원래 제목&quot;을 가져옴 &amp;rarr; 1차 캐시에 저장

// [다른 트랜잭션에서 title을 &quot;바뀐 제목&quot;으로 UPDATE + commit]

Post post2 = entityManager.find(Post.class, 1L);
// &amp;rarr; 1차 캐시에 이미 Post#1 있음 &amp;rarr; DB 안 가고 post1 반환
// post2.getTitle() 여전히 &quot;원래 제목&quot; &amp;larr; 최신값 아님&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 DB 값을 다시 읽으려면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778125583436&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;entityManager.refresh(post1); // DB에서 강제 재조회&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;isolation&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 영속성 컨텍스트와 DB 트랜잭션 격리 수준은 다른 개념이다 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 격리수준은 다음에 다뤄질 내용이라서 짧게만 말해보겠다&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 개념 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 담당 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;영속성 컨텍스트&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Java 애플리케이션 메모리 안에서 엔티티 객체 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;DB 트랜잭션&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;DB 변경의 원자성, 격리성, commit/rollback 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;DB isolation level&lt;/td&gt;
&lt;td style=&quot;text-align: center;&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;영속성 컨텍스트는 애플리케이션 레벨의 객체 관리 공간이며 DB 트랜잭션은 DB 레벨의 작업 단위다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 둘은 같이 움직인다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;@Transactional 시작 &amp;rarr; EntityManager/영속성 컨텍스트 연결 &amp;rarr; 엔티티 조회/수정 &amp;rarr; flush &amp;rarr; DB transaction commit &amp;rarr; 영속성 컨텍스트 종료 &amp;rarr; 엔티티 detached&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;style&gt;
  .toc-box {
    margin: 24px 0 36px;
    padding: 20px 22px;
    border: 1px solid #e5e7eb;
    border-radius: 14px;
    background: #fafafa;
  }

  .toc-title {
    margin: 0 0 12px;
    font-size: 18px;
  }

  .toc-box ol {
    margin: 0;
    padding-left: 22px;
  }

  .toc-box li {
    margin: 8px 0;
    line-height: 1.6;
  }

  .toc-box a {
    color: inherit;
    text-decoration: none;
  }

  .toc-box a:hover {
    text-decoration: underline;
  }

  h2[id] {
    scroll-margin-top: 90px;
  }
&lt;/style&gt;
&lt;/div&gt;</description>
      <category>Backend/  JPA &amp;middot; DB</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/316</guid>
      <comments>https://winwin0219.tistory.com/316#entry316comment</comments>
      <pubDate>Thu, 7 May 2026 12:52:39 +0900</pubDate>
    </item>
    <item>
      <title>[Devlog] 댓글 목록 조회에서 숨은 추가 쿼리를 제거하기</title>
      <link>https://winwin0219.tistory.com/315</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 목록 조회를 구현하면서 처음에는 댓글과 작성자 nickname을 응답하면 된다고만 생각했다&lt;br /&gt;&lt;br /&gt;하지만 목록 조회 API에서는 단순히 값이 나오느냐보다, 요청 한 번에 DB 쿼리가 몇 번 실행되는지도 중요하다 댓글 수가 늘어날수록 작성자 조회 쿼리도 함께 늘어난다면, API 응답 시간과 DB 부하를 예측하기 어려워진다&lt;br /&gt;&lt;br /&gt;이번 글은 댓글 목록 조회에서 작성자 nickname을 가져오는 흐름을 점검하고, user를 fetch join하여 해당 조회 경로의 쿼리 수를 예측 가능하게 만든 기록이다&lt;/p&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;a href=&quot;https://winwin0219.tistory.com/314&quot;&gt;https://winwin0219.tistory.com/314&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778049376935&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[JPA]N+1 이슈&quot; data-og-description=&quot;Fetching은 성능에 직접적인 영향을 준다https://docs.hibernate.org/orm/5.2/userguide/html_single/chapters/fetching/Fetching.html FetchingFetching, essentially, is the process of grabbing data from the database and making it available to the appl&quot; data-og-host=&quot;winwin0219.tistory.com&quot; data-og-source-url=&quot;https://winwin0219.tistory.com/314&quot; data-og-url=&quot;https://winwin0219.tistory.com/314&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cn3ZXr/dJMb86O5Hzt/EPkAcoQ4xzbL9Eo46Qfd31/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dvtMai/dJMb9c9BPDE/F0dIJJqXGkQDlkoDYJRwnk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/314&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://winwin0219.tistory.com/314&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cn3ZXr/dJMb86O5Hzt/EPkAcoQ4xzbL9Eo46Qfd31/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dvtMai/dJMb9c9BPDE/F0dIJJqXGkQDlkoDYJRwnk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[JPA]N+1 이슈&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Fetching은 성능에 직접적인 영향을 준다https://docs.hibernate.org/orm/5.2/userguide/html_single/chapters/fetching/Fetching.html FetchingFetching, essentially, is the process of grabbing data from the database and making it available to the appl&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;winwin0219.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 JPA N+1 정리 글에서 확인한 것처럼, 목록 조회 후 DTO 변환 과정에서 LAZY 연관관계에 반복 접근하면 N+1 가능성이 생길 수 있다&lt;br /&gt;&lt;br /&gt;&lt;b&gt;CoreBoard 댓글 목록 응답에는 작성자 nickname이 항상 필요&lt;/b&gt;하다 기존 구조에서는 Comment 목록을 조회한 뒤 GetAllCommentResponse로 변환하는 과정에서 comment.getUser().getNickname()에 접근했다&lt;br /&gt;&lt;br /&gt;Comment.user는 LAZY 연관관계이기 때문에, user가 함께 로딩되지 않은 상태라면 &lt;b&gt;댓글 수만큼 작성자 조회가 추가로 발생&lt;/b&gt;할 수 있다 문제는 nickname이 필요한 것 자체가 아니라, 목록 조회에서 항상 필요한 작성자 정보를 조회 시점에 함께 가져오지 않고 &lt;b&gt;DTO 변환 시점까지 미뤄둔 구조&lt;/b&gt;였다&lt;br /&gt;&lt;br /&gt;그래서 댓글 목록 조회에서는 user를 fetch join하여 Comment와 작성자 정보를 함께 로딩하도록 변경했다&lt;/p&gt;
&lt;pre id=&quot;code_1778049559649&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;&quot;&quot;
    select c
    from Comment c
    join fetch c.user
    where c.post.id = :postId
    and c.status = :status
    &quot;&quot;&quot;)
Slice&amp;lt;Comment&amp;gt; findByPostIdAndStatusWithUser(
        @Param(&quot;postId&quot;) Long postId,
        @Param(&quot;status&quot;) CommentStatus status,
        Pageable pageable
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1214&quot; data-start=&quot;1052&quot; data-ke-size=&quot;size16&quot;&gt;이 변경으로 해당 조회 경로에서는 comment.getUser().getNickname() 접근이 추가 user 조회로 이어지지 않는다&lt;/p&gt;
&lt;p data-end=&quot;1214&quot; data-start=&quot;1052&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1214&quot; data-start=&quot;1052&quot; data-ke-size=&quot;size16&quot;&gt;댓글 목록 조회에서 작성자 nickname 접근으로 발생할 수 있는 N+1 가능성을 제거한 것이다&lt;/p&gt;
&lt;p data-end=&quot;1214&quot; data-start=&quot;1052&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1214&quot; data-start=&quot;1052&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DTO projection도 고려&lt;/b&gt;할 수 있었다 필요한 필드가 commentId, content, nickname, createdDate처럼 명확한 읽기 전용 조회이기 때문에 projection이 더 좁은 최적화가 될 수 있다&lt;/p&gt;
&lt;p data-end=&quot;1214&quot; data-start=&quot;1052&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1525&quot; data-start=&quot;1345&quot; data-ke-size=&quot;size16&quot;&gt;다만 JPQL constructor expression은 &lt;b&gt;DTO의 전체 패키지 경로가 쿼리에 들어가 가독성과 유지보수성이 떨어진다고 판단&lt;/b&gt;했다&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;1525&quot; data-start=&quot;1345&quot; data-ke-size=&quot;size16&quot;&gt;DTO&amp;nbsp;projection&amp;nbsp;=&amp;nbsp;필요한&amp;nbsp;필드만&amp;nbsp;DTO&amp;nbsp;형태로&amp;nbsp;조회하는&amp;nbsp;방식 &lt;br /&gt;&lt;br /&gt;그&amp;nbsp;구현&amp;nbsp;방법&amp;nbsp;중&amp;nbsp;하나&amp;nbsp;=&amp;nbsp;JPQL&amp;nbsp;constructor&amp;nbsp;expression&lt;/p&gt;
&lt;pre id=&quot;code_1778050046913&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DTO projection
├─ JPQL constructor expression
│  └─ select new com.example.CommentResponse(...)
├─ interface projection
│  └─ select c.id as commentId, u.nickname as nickname ...
└─ QueryDSL projection
   └─ Projections.constructor(...) / @QueryProjection 등&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1525&quot; data-start=&quot;1345&quot; data-ke-size=&quot;size16&quot;&gt;현재 단계에서는&lt;b&gt; 필요한 연관 엔티티가 Comment.user 하나&lt;/b&gt;이고 &lt;b&gt;ManyToOne 관계&lt;/b&gt;이므로, &lt;b&gt;fetch join으로 문제를 해결&lt;/b&gt;하는 것이 더 단순하고 설명 가능하다고 봤다&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6279%; text-align: center;&quot;&gt;&lt;b&gt; 후보 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.2326%; text-align: center;&quot;&gt;&lt;b&gt; 장점 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.0233%; text-align: center;&quot;&gt;&lt;b&gt; 단점 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10%; text-align: center;&quot;&gt;&lt;b&gt; 판단 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6279%; text-align: center;&quot;&gt;&lt;b&gt;기존 LAZY 유지&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.2326%; text-align: center;&quot;&gt;코드 변경이 거의 없음&lt;/td&gt;
&lt;td style=&quot;width: 38.0233%; text-align: center;&quot;&gt;DTO 변환 중 user.nickname 접근으로&lt;br /&gt;N+1 가능성 있음&lt;/td&gt;
&lt;td style=&quot;width: 10%; text-align: center;&quot;&gt;x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6279%; text-align: center;&quot;&gt;&lt;b&gt;fetch join&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.2326%; text-align: center;&quot;&gt;기존 DTO 변환 구조 유지 가능, &lt;br /&gt;해당 조회 경로의 추가 user 조회 방지&lt;/td&gt;
&lt;td style=&quot;width: 38.0233%; text-align: center;&quot;&gt;필요한 컬럼만 조회하는 방식은 아님&lt;/td&gt;
&lt;td style=&quot;width: 10%; text-align: center;&quot;&gt;&lt;b&gt;선택&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6279%; text-align: center;&quot;&gt;&lt;b&gt;DTO projection&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 35.2326%; text-align: center;&quot;&gt;필요한 필드만 조회 가능&lt;/td&gt;
&lt;td style=&quot;width: 38.0233%; text-align: center;&quot;&gt;조회 모델이 응답 DTO/쿼리에 &lt;br /&gt;강하게 묶일 수 있음&lt;br /&gt;(repository 쿼리가 dto의 생성자를 알고있음)&lt;/td&gt;
&lt;td style=&quot;width: 10%; text-align: center;&quot;&gt;x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO projection은 필요한 필드만 조회할 수 있어 더 좁은 최적화가 될 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 JPQL constructor expression 방식으로 구현하면 Repository 쿼리가 응답 DTO의 생성자 구조를 직접 알게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 DTO의 필드 추가나 타입 변경이 Repository 쿼리 변경으로 이어질 수 있어, 이번 단계에서는 기존 DTO 변환 구조를 유지하면서 N+1 가능성을 제거할 수 있는 fetch join을 선택했다&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-start=&quot;1527&quot; data-end=&quot;1546&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 현재 선택은 다음과 같다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;1548&quot; data-end=&quot;1638&quot;&gt;
&lt;li data-start=&quot;1548&quot; data-end=&quot;1576&quot;&gt;지금 댓글 목록 조회: fetch join&amp;nbsp;&lt;/li&gt;
&lt;li data-start=&quot;1577&quot; data-end=&quot;1638&quot;&gt;트래픽 증가 또는 조회 필드 최소화 필요: interface projection 또는 QueryDSL 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 변경의 핵심은 단순히 fetch join을 적용했다는 것이 아니다&lt;br /&gt;&lt;br /&gt;댓글 목록 응답에 필요한 데이터가 무엇인지 먼저 확인하고, 그 데이터가 DTO 변환 시점에 뒤늦게 조회되지 않도록 조회 경로를 명확히 만든 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 댓글 목록 조회에서 작성자 nickname 접근으로 발생할 수 있는 추가 쿼리를 제거하고, API 요청당 쿼리 수를 더 예측 가능하게 만들었다&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/315</guid>
      <comments>https://winwin0219.tistory.com/315#entry315comment</comments>
      <pubDate>Wed, 6 May 2026 15:52:07 +0900</pubDate>
    </item>
    <item>
      <title>[JPA]N+1 이슈</title>
      <link>https://winwin0219.tistory.com/314</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;b&gt;Fetching은 성능에 직접적인 영향을 준다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt; &lt;a style=&quot;background-color: #e6f5ff; color: #006dd7; text-align: start;&quot; href=&quot;https://docs.hibernate.org/orm/5.2/userguide/html_single/chapters/fetching/Fetching.html&quot;&gt;공식문서&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;/u&gt;는 이렇게 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Fetching, essentially, is the process of grabbing data from the database and making it available to the application.&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;fetching은 DB에서 데이터를 가져와 애플리케이션이 사용할 수 있게 만드는 과정이다 즉, JPA에서 객체를 다룬다고 해도 결국 내부에서는 어떤 데이터를 언제 DB에서 가져올지 결정해야 한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Tuning how an application does fetching is one of the biggest factors in determining how an application will perform.&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;애플리케이션이 데이터를 어떻게 가져오도록 설계하느냐가 성능을 결정하는 큰 요소 중 하나라는 뜻이다 N+1 이슈도 결국 필요한 데이터를 언제, 몇 번의 쿼리로 가져올 것인가의 문제다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;N+1이라는 이름은 어디서 왔는가 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1은 공식 스펙에서 정해진 에러 이름이라기 보단, ORM을 사용할 때 자주 발생하는 조회 패턴을 설명하는 말이다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;N+1 = 1번의 목록 조회 쿼리 + 목록 안의 N개의 데이터를 각각에 대해 추가&amp;nbsp; 조회하는 쿼리&amp;nbsp;&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;예를 들어, 댓글 10개를 조회했는데, 각 댓글의 작성자 정보를 가져오느라 user 조회가 10번 더 나가면 다음과 같은 구조가 된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;댓글 목록 조회 1번 + 댓글 작성자 조회 10번 = 총 11번 쿼리&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;그래서 이름이 N+1인 것이다. (여기서 N은 목록에서 조회된 데이터 개수)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;어떤 코드가 문제의 발단이 되는가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 목록 API를 만든다고 가정해보자, 댓글 목록 응답에는 댓글 내용뿐 아니라 작성자 nickname도 필요하다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 DTO는 이런 형태일 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1778046269416&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record GetAllCommentResponse(
        Long commentId,
        String content,
        String nickname,
        LocalDateTime createdDate
) {
    public static GetAllCommentResponse from(Comment comment) {
        return new GetAllCommentResponse(
                comment.getId(),
                comment.getContent(),
                comment.getUser().getNickname(),
                comment.getCreatedDate()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 보면 이 코드는 자연스럽다&lt;/p&gt;
&lt;pre id=&quot;code_1778046281156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;comment.getUser().getNickname()&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;하지만, 여기서 중요한 점이 있다 Comment 엔티티의 user가 보통 이런 식으로 매핑되어있다.&lt;/p&gt;
&lt;pre id=&quot;code_1778046306778&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.LAZY)
private Users user;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LAZY는 연관 객체를 처음부터 무조건 가져오지 않고 실제로 접근할 때마다 가져오겠다는 전략이다&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;we recommend that you use lazy fetching&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;대부분의 연관관계를 처음부터 무조건 가져오기보단, 기본적으로는 필요할 때 가져오는 lazy fetching을 쓰라는 뜻이다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 @ManyToOne(fetch = FetchType.LAZY)같은 방식을 예시로 들고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, LAZY 자체가 문제가 아니다 오히려 불필요한 조인을 줄이기 위한 정상적인 전략이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 목록 조회에서 nickname이 항상 필요한데도, user 조회를 DTO 변환 시점까지 미뤄둔 구조다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate의 기본 lazy fetching&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서 lazy association에 대해 이렇게 설명한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;By default, Hibernate uses lazy select fetching for collections and lazy proxy fetching for single-valued associations.&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;Hibernate는 기본적으로 컬렉션 연관관계에는 lazy fetching을, 단일 연관관계에는 lazy proxy fetching을 사용한다&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;그리고 바로 뒤에 batch fetching이라는 최적화도 언급하고 있다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;If you set hibernate.default_batch_fetch_size, Hibernate will use the batch fetch optimization for lazy fetching.&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;lazy fetching을 사용할 때도 default_batch_fetch_size를 설정하면 Hibernate가 여러 lazy 조회를 묶어서 가져오는 batch fetch 최적화를 사용할 수 있다는 의미다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, lazy자체가 나쁜 것은 아니지만, lazy로 인한 추가 조회를 어떻게 제어할지가 중요하다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 LAZY가 문제로 이어질 수 있는가&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LAZY는 나쁜 전략이 아니다 오히려 대부분의 상황에서 합리적이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 API가 항상 연관객체를 필요로 하는 것은 아니다 어떤 화면에서는 댓글 내용만 필요하고 작성자 정보는 필요없을 수 있다 이때 매번 작성자까지 가져오면 불필요한 조회가 된다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 목록 API처럼 연관 데이터가 항상 필요한 상황이다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;댓글 목록 응답에 nickname이 항상 필요하다.&lt;/li&gt;
&lt;li&gt;그런데 조회 쿼리는 Comment만 가져온다.&lt;/li&gt;
&lt;li&gt;이후 DTO 변환에서 User에 접근한다.&lt;/li&gt;
&lt;li&gt;그러면&amp;nbsp;User&amp;nbsp;조회가&amp;nbsp;댓글&amp;nbsp;개수만큼&amp;nbsp;추가될&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 전 흐름은 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 댓글 목록 조회&lt;/p&gt;
&lt;pre id=&quot;code_1778047549049&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from comments where post_id = ?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. DTO 변환 중 작성자 접근&lt;/p&gt;
&lt;pre id=&quot;code_1778047553153&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  comment.getUser().getNickname()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 작성자 추가 조회&lt;/p&gt;
&lt;pre id=&quot;code_1778047560130&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   select * from users where id = ?
   select * from users where id = ?
   select * from users where id = ?
   ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 user_id를 알고 있다는 것과 user.nickname()을 이미 가졎왔다는 것이 다르다는 점이다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;comments.user_id를 조회했다 &amp;ne; users.nickname을 조회했다&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 운영 관점에서 왜 문제가 되는가 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1이 무서운 이유는 쿼리가 조금 더 나간다가 아니다 API 요청 하나가 DB에 몇 번 접근하는지 예측 가능해야 하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 50개 &amp;rarr; 댓글 목록 조회 1번 + 작성자 조회 최대 50번 = 총 51번&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터 개수가 늘어날수록 DB쿼리 수가 같이 늘어난다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이문제는 다음으로 이어질 수 있다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 부하 증가&lt;/li&gt;
&lt;li&gt;API 응답 시간 증가&lt;/li&gt;
&lt;li&gt;커넥션 사용량 증가&lt;/li&gt;
&lt;li&gt;트래픽 증가 시 병목 발생&lt;/li&gt;
&lt;li&gt;장애 상황에서 원인 파악 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 목록 API는 자주 호출될 수 있다&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;그래서 N+1은 나중에 성능 좀 안 좋을 수 있음정도가 아니라 &lt;b&gt;API 요청당 DB 접근 횟수를 예측하기 어렵게 만드는 문제&lt;/b&gt;다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 해결 방향 1: fetch join &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 고려할 수 있는 해결책은 fetch join이다&lt;/p&gt;
&lt;pre id=&quot;code_1778047635895&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;&quot;&quot;
    select c
    from Comment c
    join fetch c.user
    where c.post.id = :postId
&quot;&quot;&quot;)
List&amp;lt;Comment&amp;gt; findByPostIdWithUser(Long postId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 comment를 조회하면서 User도 함께 가져오라고 명시한다&amp;nbsp;&lt;/p&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번당 작성자 조회1번이었다면 fetch join을 사용하여 댓글 + 작성자 join 조회 1번이 된다&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://vladmihalcea.com/n-plus-1-query-problem/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Vlad Mihalcea 글&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;에서도 엔티티 쿼리에서 N+1을 피하는 방법으로 JOIN FETCH를 언급한다&lt;/p&gt;
&lt;br /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 Vlad Mihalcea는 Hibernate/JPA 성능 쪽에서 유명한 분이다&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;But, if you want to use post association, then you can use JOIN FETCH to avoid the N+1 query problem&lt;/blockquote&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;연관 객체가 필요한 조회라면 JOIN FETCH를 사용하여 N+1 문제를 피할 수 있다는 의미&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다만, fetch join은 필요한 컬럼만 가져오는 방식은 아니다 엔티티와 연관 데이터를 함께 로딩하는 방식이다&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;fetch join &amp;rarr; Comment 엔티티 + User 엔티티를 함께 조회&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 쿼리 수를 줄이는 해결에는 적합하지만, 조회 컬럼을 최소화하는 해결책은 아니다&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방향 2: DTO Projection &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 API에서 필요한 필드가 명확하다면 DTO Projection도 많이 사용한다&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;commentId&lt;/li&gt;
&lt;li&gt;content&lt;/li&gt;
&lt;li&gt;nickname&lt;/li&gt;
&lt;li&gt;createdDate&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 전체를 가져오지 않고 필요한 값만 조회할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA 공식문서는 projection을 이렇게 설명한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Data allows modeling dedicated return types, to more selectively retrieve partial views of the managed aggregates.&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;Spring Data는 관리되는 엔티티 전체가 아니라 특정 속성만 선택적으로 조회하기 위한 반환 타입을 모델링할 수 있게 해준다는 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 목록 조회처럼 필요한 필드가 정해진 경우 projection을 사용할 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1778047977775&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;&quot;&quot;
    select new com.example.CommentResponse(
        c.id,
        c.content,
        u.nickname,
        c.createdDate
    )
    from Comment c
    join c.user u
    where c.post.id = :postId
&quot;&quot;&quot;)
List&amp;lt;CommentResponse&amp;gt; findCommentResponses(Long postId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점도 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPQL constructor expression을 쓰면 DTO의 전체 패키지 경로를 쿼리에 넣어야 해서 코드가 지저분할 수 있다 그래서 실무에서는 interface projection을 쓰거나 QueryDSL같은 도구를 쓰는 경우도 있다&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방향 3: EntityGraph &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring Data JPA에서는 @EntityGraph도 사용할 수 있다 &lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;&lt;a style=&quot;background-color: #ffffff;&quot; href=&quot;https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/EntityGraph.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;S&lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;pring Data JPA API문서는 @EntityGraph&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;를 이렇게 설명하고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1778048108775&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;EntityGraph (Spring Data JPA Parent 4.0.5 API)&quot; data-og-description=&quot;The name of the EntityGraph to use.&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/EntityGraph.html&quot; data-og-url=&quot;https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/EntityGraph.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/EntityGraph.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/EntityGraph.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;EntityGraph (Spring Data JPA Parent 4.0.5 API)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The name of the EntityGraph to use.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Annotation to configure the JPA 2.1 EntityGraphs that should be used on repository methods.&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;Repository 메서드에서 사용할 JPA EntityGraph를 설정하는 애노테이션이라는 뜻인데, 쿼리 메서드에 어떤 연관관계를 함께 가져올지 지정할 수 있다는 말이다&lt;/p&gt;
&lt;pre id=&quot;code_1778048152454&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EntityGraph(attributePaths = &quot;user&quot;)
List&amp;lt;Comment&amp;gt; findByPostId(Long postId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Data JPA 문서는 dynamic EntityGraph도 언급한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Since 1.9 we support the definition of dynamic EntityGraphs by allowing to customize the fetch-graph via attributePaths() ad-hoc fetch-graph configuration.&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;Spring Data JPA 1.9부터는 attributePaths()를 이용해서, 미리 이름 붙인 EntityGraph를 만들지 않아도 Repository 메서드에서 바로 가져올 연관 필드를 지정할 수 있다는 뜻&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityGraph는 fetch join보다 Repository 메서드 선언이 깔끔할 수 있다. 다만 복잡한 조건, 정렬, 페이징, 여러 연관관계가 얽히면 JPQL fetch join이나 projection이 더 명확할 때도 있다&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityGraph란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fetch join이랑 목적이 비슷하다, 이번 조회에서는 Comment만 가져오지 말고 Comment.user도 같이 가져와&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 fetch join은 아래처럼 쓰는데&lt;/p&gt;
&lt;pre id=&quot;code_1778048847424&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;&quot;&quot;
    select c
    from Comment c
    join fetch c.user
    where c.post.id = :postId
&quot;&quot;&quot;)
Slice&amp;lt;Comment&amp;gt; findByPostIdAndStatusWithUser(...);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;EntityGraph는 쿼리문에 join fetch를 직접 안 쓰고 메서드 위에 이렇게 붙임&lt;/p&gt;
&lt;pre id=&quot;code_1778048870998&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EntityGraph(attributePaths = &quot;user&quot;)
Slice&amp;lt;Comment&amp;gt; findByPostIdAndStatus(Long postId, CommentStatus status, Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;차이점&lt;br /&gt;fetch join &amp;rarr; JPQL 안에 join fetch를 직접 적음&lt;br /&gt;EntityGraph &amp;rarr; Repository 메서드 위에 @EntityGraph(attributePaths = &quot;...&quot;)로 적음&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;다만 복잡한 조건이나 정렬, 직접 작성한 JPQL과 함께 조회 의도를 명확히 드러내고 싶다면 fetch join을 사용하는 편이 더 직관적일 수 있다&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 방향 4: Batch Fetching &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch fetching은 N개의 추가조회를 1개씩 날리는 대신, 묶어서 가져오는 방식이다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;If you set hibernate.default_batch_fetch_size, Hibernate will use the batch fetch optimization for lazy fetching.&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;hibernate.default_batch_fetch_size를 설정하면 Hibernate가 lazy 로딩 대상들을 묶어서 조회하는 최적화를 사용할 수 있다는 뜻&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 댓글 100개의 작성자를 하나씩 조회하는 대신, 여러 id를 IN조건으로 묶어서 조회할 수 있다&lt;/p&gt;
&lt;pre id=&quot;code_1778048479951&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from users
where id in (?, ?, ?, ?, ...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Batch fetching은 전역으로 lazy 조회 비용을 완화할 수 있다는 장점이 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 특정 목록 API에서 반드시 필요한 연관 데이터를 명확히 가져오는 방식은 아니다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;fetch join &amp;rarr; 이 조회에서 필요한 연관 데이터를 처음부터 함께 가져온다&lt;br /&gt;batch fetching &amp;rarr; lazy 조회가 발생하더라도 여러 건을 묶어서 가져오도록 완화한다&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 보편적으로 어떻게 다루는가 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N+1 해결은 하나의 정답으로 고정되지 않는다 보통 다음과 같은 기준으로 선택한다&lt;/p&gt;
&lt;h4 data-end=&quot;9210&quot; data-start=&quot;9180&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 목록 조회에서 연관 데이터가 항상 필요하다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;9264&quot; data-start=&quot;9212&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 fetch join, EntityGraph, projection 중 하나를 고려한다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;예: 댓글 목록에 작성자 nickname이 항상 필요하다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;9348&quot; data-start=&quot;9312&quot; data-ke-size=&quot;size16&quot;&gt;이때는 조회 시점에 작성자 정보를 함께 가져오는 것이 자연스럽다&lt;/p&gt;
&lt;h4 data-end=&quot;9379&quot; data-start=&quot;9350&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 필요한 필드가 명확한 읽기 전용 API다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;9416&quot; data-start=&quot;9381&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 DTO projection이 좋은 선택이 될 수 있다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;예: 목록 응답에 id, title, nickname, createdDate만 필요하다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;9520&quot; data-start=&quot;9481&quot; data-ke-size=&quot;size16&quot;&gt;엔티티 전체를 가져오는 것보다 필요한 컬럼만 조회할 수 있기 때문이다.&lt;/p&gt;
&lt;h4 data-end=&quot;9544&quot; data-start=&quot;9522&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 도메인 로직을 수행해야 한다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;9565&quot; data-start=&quot;9546&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 엔티티 조회가 자연스럽다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;예: 댓글 수정, 삭제, 권한 검증, 상태 변경&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;9715&quot; data-start=&quot;9607&quot; data-ke-size=&quot;size16&quot;&gt;이런 작업은 응답용 필드만 필요한 것이 아니라 엔티티의 상태와 행위가 필요하다. 따라서 projection보다 엔티티를 조회하고 필요한 연관관계를 명시적으로 가져오는 방식이 더 적절할 수 있다.&lt;/p&gt;
&lt;h4 data-end=&quot;9750&quot; data-start=&quot;9717&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 여러 API에서 비슷한 lazy 조회가 반복된다&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;9782&quot; data-start=&quot;9752&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 batch fetching도 고려할 수 있다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;예: 여러 곳에서 User, Category 같은 단일 연관 객체를 자주 참조한다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 공식문서 기반으로 정리하면 다음과 같다 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 기본 연관관계는 불필요한 로딩을 피하기 위해 LAZY로 두는 것이 일반적이다&lt;br /&gt;2. 특정 조회에서 연관 데이터가 필요하다면 fetch join이나 EntityGraph로 가져올 대상을 명시한다&lt;br /&gt;3. 읽기 전용 목록 API에서는 projection으로 필요한 필드만 조회할 수 있다&lt;br /&gt;4. 반복적인 lazy 조회는 batch fetching으로 완화할 수 있다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 문제는 목록 조회에서 발생한다 &lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;목록을 조회한다&lt;/li&gt;
&lt;li&gt;각 항목을 DTO로 변환한다&lt;/li&gt;
&lt;li&gt;DTO 변환 중 LAZY 연관관계에 접근한다&lt;/li&gt;
&lt;li&gt;그&amp;nbsp;결과&amp;nbsp;항목&amp;nbsp;개수만큼&amp;nbsp;추가&amp;nbsp;조회가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; N+1의 본질은 이것 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 데이터가 무엇인지 이미 알고 있었는데, 조회 시점에 함께 가져오지 않고, 사용&amp;nbsp;시점에&amp;nbsp;하나씩&amp;nbsp;가져오게&amp;nbsp;둔&amp;nbsp;것&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 해결도 fetch join을 쓰자가 아니라, 조회 목적에 따라 달라진다 &lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연관 엔티티 자체가 필요하다 &lt;br /&gt;&amp;rarr; fetch join 또는 EntityGraph&lt;/li&gt;
&lt;li&gt;응답&amp;nbsp;필드만&amp;nbsp;필요하다 &lt;br /&gt;&amp;rarr; DTO/interface projection&lt;/li&gt;
&lt;li&gt;여러&amp;nbsp;lazy&amp;nbsp;조회를&amp;nbsp;전역적으로&amp;nbsp;완화하고&amp;nbsp;싶다 &lt;br /&gt;&amp;rarr; batch fetching&lt;/li&gt;
&lt;li&gt;도메인&amp;nbsp;상태&amp;nbsp;변경이&amp;nbsp;필요하다 &lt;br /&gt;&amp;rarr;&amp;nbsp;엔티티&amp;nbsp;조회&amp;nbsp;유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;보충 :&amp;nbsp; 컬렉션 연관관계(OneToMany, ManyToMany) fetch join&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 공식문서에는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;When limits or pagination are combined with a fetch join, Hibernate must retrieve all matching results from the database and apply the limit in memory!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;limit이나 pagination이 fetch join과 함께 사용되면, Hibernate가 DB에서 조건에 맞는 결과를 모두 가져온 뒤 메모리에서 limit을 적용해야 할 수 있다&lt;/b&gt;는 뜻이다 즉 DB에서 limit 10으로 딱 10개만 가져오는 게 아니라, 애플리케이션 메모리에서 잘라낼 수 있다&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;Care should be taken when fetch joining a collection-valued association&amp;hellip;&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;컬렉션 값을 가지는 연관관계, 예를 들면 @OneToMany, @ManyToMany를 fetch join 할 때 조심해야 한다는 뜻&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;This almost certainly isn&amp;rsquo;t the behavior you were hoping for, and in general will exhibit terrible performance characteristics.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 개발자가 기대한 동작이 아니고, 일반적으로 매우 나쁜 성능 특성을 보일 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분 때문에 fetch join + pagination 조심해라는 말이 나오는 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OneToMany + 페이징이 필요하다면? &amp;rarr; Batch Fetching 또는 쿼리 분리 방식&lt;/p&gt;</description>
      <category>Backend/  JPA &amp;middot; DB</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/314</guid>
      <comments>https://winwin0219.tistory.com/314#entry314comment</comments>
      <pubDate>Wed, 6 May 2026 15:30:48 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] WSL Integration 중단으로 테스트가 불안정했던 문제</title>
      <link>https://winwin0219.tistory.com/313</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Desktop에서 다음과 같은 메시지가 나타났다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷(2171).png&quot; data-origin-width=&quot;593&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjRGTh/dJMcaaFdsIv/WrFZlR3LGSsxYgAPFolgkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjRGTh/dJMcaaFdsIv/WrFZlR3LGSsxYgAPFolgkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjRGTh/dJMcaaFdsIv/WrFZlR3LGSsxYgAPFolgkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjRGTh%2FdJMcaaFdsIv%2FWrFZlR3LGSsxYgAPFolgkk%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;593&quot; height=&quot;491&quot; data-filename=&quot;스크린샷(2171).png&quot; data-origin-width=&quot;593&quot; data-origin-height=&quot;491&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&gt;이후 Docker가 켜져 있는 것처럼 보이는데도, Docker가 필요한 테스트가 실패하거나 컨테이너 실행/접속이 불안정해졌다. &lt;/span&gt;&lt;br /&gt;&lt;span&gt;이전에도 종종 같은 문제가 있었고, 매번 명령어를 다시 찾는 게 번거로워 트러블슈팅 기록으로 남긴다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 원인 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.docker.com/&quot;&gt;https://www.docker.com/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778037762773&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;Docker: Accelerated Container Application Development&quot; data-og-description=&quot;Docker is a platform designed to help developers build, share, and run container applications. We handle the tedious setup, so you can focus on the code.&quot; data-og-host=&quot;www.docker.com&quot; data-og-source-url=&quot;https://www.docker.com/&quot; data-og-url=&quot;https://www.docker.com/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/PTcHj/dJMb9c9BNLI/3KggDDqcLS0Q9Yw1ga7Sc1/img.png?width=1110&amp;amp;height=580&amp;amp;face=0_0_1110_580,https://scrap.kakaocdn.net/dn/bxnDJ9/dJMb8T93tvb/FFUYfIYx7ZxXvaPNhIy6S0/img.png?width=1110&amp;amp;height=580&amp;amp;face=0_0_1110_580,https://scrap.kakaocdn.net/dn/bl3XqH/dJMb9frJmhB/Ts4GNOr2LggCdb1Nc04wN1/img.png?width=2320&amp;amp;height=958&amp;amp;face=1598_268_1866_558&quot;&gt;&lt;a href=&quot;https://www.docker.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.docker.com/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/PTcHj/dJMb9c9BNLI/3KggDDqcLS0Q9Yw1ga7Sc1/img.png?width=1110&amp;amp;height=580&amp;amp;face=0_0_1110_580,https://scrap.kakaocdn.net/dn/bxnDJ9/dJMb8T93tvb/FFUYfIYx7ZxXvaPNhIy6S0/img.png?width=1110&amp;amp;height=580&amp;amp;face=0_0_1110_580,https://scrap.kakaocdn.net/dn/bl3XqH/dJMb9frJmhB/Ts4GNOr2LggCdb1Nc04wN1/img.png?width=2320&amp;amp;height=958&amp;amp;face=1598_268_1866_558');&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;Docker: Accelerated Container Application Development&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Docker is a platform designed to help developers build, share, and run container applications. We handle the tedious setup, so you can focus on the code.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.docker.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;With Docker Desktop running on WSL 2, users can leverage Linux workspaces...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉 Docker Desktop은 WSL 2 환경과 연결되어 동작할 수 있다. :contentReference[oaicite:0]{index=0}&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이번 문제는 Docker Desktop과 `Ubuntu-22.04` WSL 배포판 사이의 integration 프로세스가 비정상적으로 중단된 상황으로 보인다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;이 상태에서는 Docker Desktop UI는 떠 있어도 실제로는 다음 문제가 생길 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;- Docker 명령 실행 불안정&lt;/span&gt;&lt;br /&gt;&lt;span&gt;- 컨테이너 생성/접속 실패&lt;/span&gt;&lt;br /&gt;&lt;span&gt;- Testcontainers 기반 테스트 실패&lt;/span&gt;&lt;br /&gt;&lt;span&gt;- 전체 테스트가 환경 상태에 따라 성공/실패를 반복하는 것처럼 보임&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;해결 : Docker가 의존하는 WSL 실행 환경을 통째로 리셋 &lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1778037693645&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;wsl --shutdown&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/wsl/basic-commands&quot;&gt;https://learn.microsoft.com/en-us/windows/wsl/basic-commands&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778038105742&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;Basic commands for WSL&quot; data-og-description=&quot;Reference for the basic commands included with Windows Subsystem for Linux (WSL).&quot; data-og-host=&quot;learn.microsoft.com&quot; data-og-source-url=&quot;https://learn.microsoft.com/en-us/windows/wsl/basic-commands&quot; data-og-url=&quot;https://learn.microsoft.com/en-us/windows/wsl/basic-commands&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/rQFyr/dJMb88F8Szz/j34SLYjHAEdIfe0uV41UqK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/wsl/basic-commands&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.microsoft.com/en-us/windows/wsl/basic-commands&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/rQFyr/dJMb88F8Szz/j34SLYjHAEdIfe0uV41UqK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;Basic commands for WSL&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Reference for the basic commands included with Windows Subsystem for Linux (WSL).&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.microsoft.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;blockquote data-end=&quot;1407&quot; data-start=&quot;1300&quot; data-ke-style=&quot;style2&quot;&gt;Immediately terminates all running distributions and the WSL 2 lightweight utility virtual machine.&lt;/blockquote&gt;
&lt;p data-end=&quot;1003&quot; data-start=&quot;788&quot; data-ke-size=&quot;size16&quot;&gt;즉 실행 중인 WSL 환경을 통째로 종료하는 명령이다.&lt;/p&gt;
&lt;p data-end=&quot;1150&quot; data-start=&quot;1005&quot; data-ke-size=&quot;size16&quot;&gt;이번 문제에서는 wsl --shutdown으로 &lt;b&gt;Docker Desktop이 의존하는 WSL 실행 환경을 종료한 뒤 Docker Desktop을 다시 실행&lt;/b&gt;했다.&lt;br /&gt;그 결과 WSL/Docker 연결이 재초기화되어 Docker가 필요한 테스트도 정상 실행됐다.&lt;/p&gt;
&lt;p data-end=&quot;1596&quot; data-start=&quot;1516&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1596&quot; data-start=&quot;1516&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1199&quot; data-start=&quot;1159&quot; data-ke-size=&quot;size16&quot;&gt;이번 문제는 애플리케이션 코드 문제가 아니라 개발 환경 의존성 문제였다.&lt;/p&gt;
&lt;p data-end=&quot;1382&quot; data-start=&quot;1201&quot; data-ke-size=&quot;size16&quot;&gt;Docker Desktop의 WSL integration이 중단되면 Docker가 필요한 테스트가 랜덤하게 실패하는 것처럼 보일 수 있다.&lt;br /&gt;이 경우 wsl --shutdown으로 WSL 실행 환경을 종료한 뒤 Docker Desktop을 다시 실행하면, WSL/Docker 연결이 재초기화되어 문제가 해결될 수 있다.&lt;/p&gt;</description>
      <category>Backend/⚙️ Infra</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/313</guid>
      <comments>https://winwin0219.tistory.com/313#entry313comment</comments>
      <pubDate>Wed, 6 May 2026 12:33:28 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Java는 컴파일 언어일까, 인터프리터 언어일까?</title>
      <link>https://winwin0219.tistory.com/312</link>
      <description>&lt;p data-end=&quot;187&quot; data-start=&quot;151&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 자바는 컴파일언어고 자바스크립트는 인터프리터언어라고만 외웠다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p0q5Z/dJMcadhGjR7/IR25gxKHOkZc8k26rUdfq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p0q5Z/dJMcadhGjR7/IR25gxKHOkZc8k26rUdfq1/img.png&quot; data-alt=&quot;구글링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p0q5Z/dJMcadhGjR7/IR25gxKHOkZc8k26rUdfq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp0q5Z%2FdJMcadhGjR7%2FIR25gxKHOkZc8k26rUdfq1%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;596&quot; height=&quot;101&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;구글링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;187&quot; data-start=&quot;151&quot; data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, &lt;b&gt;Java는 보통 컴파일 언어로 분류된다 &lt;/b&gt;하지만 Java는 C처럼 소스코드를 바로 운영체제와 CPU가 실행할 수 있는 실행 파일로 만드는 방식은 아니다&lt;/p&gt;
&lt;p data-end=&quot;187&quot; data-start=&quot;151&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;327&quot; data-start=&quot;253&quot; data-ke-size=&quot;size16&quot;&gt;Java는 .java 소스코드를 javac가 .class 바이트코드로 컴파일하고, JVM이 그 바이트코드를 읽어 실행한다 그리고 JVM은 바이트코드를 단순히 한 줄씩 해석만 하는 것이 아니라, 처음에는 인터프리터로 실행하다가 자주 실행되는 코드는 JIT 컴파일러를 통해 기계어로 바꿔 더 빠르게 실행한다 즉 Java는 이렇게 정리할 수 있다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Java 소스코드 .java
  &amp;darr; javac
바이트코드 .class
  &amp;darr; JVM
인터프리터 실행 또는 JIT 컴파일
  &amp;darr;
기계어 실행&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;즉 자바는 소스코드를 바이트코드로 컴파일하고 JVM이 그 바이트코드를 인터프리터와 JIT 컴파일러를 사용해 실행하는 언어&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;컴파일이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;774&quot; data-start=&quot;719&quot; data-ke-size=&quot;size16&quot;&gt;컴파일은 사람이 작성한 소스코드를 다른 실행 환경이 이해할 수 있는 형태로 미리 변환하는 과정이다&amp;nbsp; C/C++에서는 보통 운영체제와 CPU가 직접 실행할 수 있는 실행 파일을 만든다&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;774&quot; data-start=&quot;719&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;774&quot; data-start=&quot;719&quot; data-ke-size=&quot;size16&quot;&gt;반면 Java에서는 JVM이 실행할 수 있는 .class 바이트코드 파일을 만든다.&lt;/p&gt;
&lt;p data-end=&quot;901&quot; data-start=&quot;874&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;u&gt;GCC 공식문서&lt;/u&gt;&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;는 컴파일 과정을 이렇게 설명하고 있다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Compilation can involve up to four stages: preprocessing, compilation proper, assembly and linking&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, 컴파일은 전처리, 좁은 의미의 컴파일, 어셈블리, 링킹 같은 여러 단계를 포함할 수 있다는 뜻이다&lt;/p&gt;
&lt;pre id=&quot;code_1777985242723&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;C 언어를 예로 들면 이런 흐름이다
소스코드
  &amp;darr;
전처리
  &amp;darr;
컴파일
  &amp;darr;
어셈블리
  &amp;darr;
오브젝트 파일
  &amp;darr;
링킹
  &amp;darr;
실행 파일&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 C에서는 이런 코드를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777985257230&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

int main() {
    printf(&quot;hello&quot;);
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 바로 실행되는 것이 아니라, 컴파일러가 먼저 처리해서 실행 파일을 만든다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777985268077&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;gcc main.c -o app
./app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 C/C++은 보통 최종적으로 운영체제와 CPU가 직접 실행할 수 있는 실행 파일을 만든다&lt;/p&gt;
&lt;p data-end=&quot;1452&quot; data-start=&quot;1438&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Java는 다르다&amp;nbsp;Java의 javac 컴파일은 최종 실행 파일을 만드는 과정이 아니라, JVM이 실행할 수 있는 .class 바이트코드 파일을 만드는 과정이다&lt;/p&gt;
&lt;h2 data-end=&quot;1556&quot; data-start=&quot;1545&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;컴파일 언어란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1609&quot; data-start=&quot;1558&quot; data-ke-size=&quot;size16&quot;&gt;컴파일 언어는 보통 실행 전에 소스코드를 다른 형태로 변환하는 과정이 중심인 언어를 말한다 컴파일 언어의 특징은 대략 이렇다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;실행 전에 번역 과정을 거친다.
컴파일 결과물이 만들어진다.
문법 오류나 타입 오류를 실행 전에 많이 잡을 수 있다.
실행 시점에는 이미 변환된 결과물을 사용한다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1752&quot; data-start=&quot;1737&quot; data-ke-size=&quot;size16&quot;&gt;다만 여기서 조심해야 한다&amp;nbsp;컴파일 결과물이 항상 운영체제와 CPU가 직접 실행하는 실행 파일인 것은 아니다&lt;/p&gt;
&lt;p data-end=&quot;1859&quot; data-start=&quot;1801&quot; data-ke-size=&quot;size16&quot;&gt;C/C++은 보통 네이티브 실행 파일을 만들지만, Java는 JVM이 실행하는 바이트코드 파일을 만든다&lt;/p&gt;
&lt;p data-end=&quot;1877&quot; data-start=&quot;1861&quot; data-ke-size=&quot;size16&quot;&gt;즉 Java도 컴파일을 한다&amp;nbsp;하지만 그 결과물이 C의 실행 파일과는 다르다&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;b&gt;인터프리터란? &lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1963&quot; data-start=&quot;1924&quot; data-ke-size=&quot;size16&quot;&gt;인터프리터는 실행 시점에 코드를 읽고 해석하면서 실행하는 프로그램이다&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2045&quot; data-start=&quot;1965&quot; data-ke-size=&quot;size16&quot;&gt;컴파일 언어가 실행 전에 변환 결과물을 만들어두는 방식에 가깝다면, 인터프리터 방식은 런타임이 프로그램을 읽고 해석하며 실행하는 방식에 가깝다&lt;/p&gt;
&lt;p data-end=&quot;2098&quot; data-start=&quot;2047&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 JavaScript는 보통 브라우저나 Node.js 같은 실행 환경에서 실행된다&lt;/p&gt;
&lt;pre id=&quot;code_1777986782040&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;app.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2184&quot; data-start=&quot;2144&quot; data-ke-size=&quot;size16&quot;&gt;여기서 app.js를 운영체제나 CPU가 직접 실행하는 것은 아니다&amp;nbsp;브라우저 안의 JavaScript 엔진이 JavaScript 코드를 읽고 실행한다&amp;nbsp;그래서 JavaScript는 전통적으로 인터프리터 언어로 설명되는 경우가 많다 다만 현대 JavaScript 엔진은 단순히 인터프리터만 사용하는 것이 아니라, 성능을 높이기 위해 JIT 컴파일러도 함께 사용한다&lt;/p&gt;
&lt;p data-end=&quot;2184&quot; data-start=&quot;2144&quot; data-ke-size=&quot;size16&quot;&gt;MDN 공식문서는 JavaScript를 이렇게 설명한다&lt;/p&gt;
&lt;blockquote data-end=&quot;2184&quot; data-start=&quot;2144&quot; data-ke-style=&quot;style2&quot;&gt;JavaScript (JS) is a lightweight interpreted (or just-in-time compiled) programming language&lt;/blockquote&gt;
&lt;p data-end=&quot;2385&quot; data-start=&quot;2342&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, JavaScript는 가벼운 인터프리터 또는 JIT 컴파일 언어라는 뜻이다&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2385&quot; data-start=&quot;2342&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2385&quot; data-start=&quot;2342&quot; data-ke-size=&quot;size16&quot;&gt;즉 요즘 언어와 런타임은 단순하게 컴파일만 한다, 인터프리터만 쓴다로 나뉘지 않는 경우가 많다&lt;/p&gt;
&lt;h2 data-end=&quot;2385&quot; data-start=&quot;2342&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;컴파일 언어와 인터프리터 언어의 가장 큰 차이 &lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-end=&quot;2385&quot; data-start=&quot;2342&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;컴파일 언어&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;2385&quot; data-start=&quot;2342&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 실행 전에 소스코드를 다른 형태로 변환한다&lt;br /&gt;&amp;rarr; 문법 오류나 타입 오류를 실행 전에 많이 잡을 수 있다&lt;br /&gt;&amp;rarr; 실행 시점에는 변환된 결과물을 사용한다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인터프리터 언어&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 런타임이 코드를 읽고 해석하며 실행하는 방식이 중심이다&lt;br /&gt;&amp;rarr; 별도의 실행 파일을 미리 만들지 않고 바로 실행하는 경우가 많다&lt;br /&gt;&amp;rarr; 특정 코드 경로의 문제는 실행 중에 드러나는 경우가 많다&lt;/p&gt;
&lt;p data-end=&quot;2912&quot; data-start=&quot;2886&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2912&quot; data-start=&quot;2886&quot; data-ke-size=&quot;size16&quot;&gt;여기서 에러가 언제 나느냐도 중요한 차이다&lt;/p&gt;
&lt;p data-end=&quot;2970&quot; data-start=&quot;2914&quot; data-ke-size=&quot;size16&quot;&gt;컴파일 언어에서는 문법이 틀리거나 타입이 맞지 않으면 실행 전에 컴파일 단계에서 막히는 경우가 많다&lt;/p&gt;
&lt;p data-end=&quot;3002&quot; data-start=&quot;2972&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 Java에서 이런 코드는 컴파일되지 않는다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;int age = &quot;스무살&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3069&quot; data-start=&quot;3034&quot; data-ke-size=&quot;size16&quot;&gt;int에는 숫자가 들어가야 하는데 문자열을 넣었기 때문이다&amp;nbsp;이런 경우 .class 파일이 제대로 만들어지지 않으므로 실행 자체가 되지 않는다&amp;nbsp;반면 실행 중에 드러나는 문제도 있다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int result = 10 / 0;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3194&quot; data-start=&quot;3178&quot; data-ke-size=&quot;size16&quot;&gt;이 코드는 문법 자체는 맞다&amp;nbsp;그래서 컴파일은 될 수 있다&amp;nbsp;하지만 실제 실행 중 이 코드에 도달하면 예외가 발생한다&lt;/p&gt;
&lt;p data-end=&quot;3279&quot; data-start=&quot;3248&quot; data-ke-size=&quot;size16&quot;&gt;즉 Java에서도 에러는 두 종류로 나눠 생각해야 한다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;컴파일 에러
&amp;rarr; 실행 전에 잡힘
&amp;rarr; 문법 오류, 타입 오류 등

런타임 예외
&amp;rarr; 실행 중에 발생
&amp;rarr; 0으로 나누기, null 접근, 배열 범위 초과 등&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3444&quot; data-start=&quot;3379&quot; data-ke-size=&quot;size16&quot;&gt;그래서 컴파일 언어는 실행 전에만 에러가 나고, 인터프리터 언어는 실행 중에만 에러가 난다라고 단정하면 안 된다&lt;/p&gt;
&lt;p data-end=&quot;3464&quot; data-start=&quot;3446&quot; data-ke-size=&quot;size16&quot;&gt;더 정확히는 이렇게 말해야 한다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;컴파일 언어는 실행 전에 많은 오류를 잡을 수 있다.
하지만 컴파일이 성공해도 실행 중 예외는 발생할 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;2385&quot; data-start=&quot;2342&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Java는 컴파일 언어냐 인터프리터 언어냐? &lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;3568&quot; data-start=&quot;3546&quot; data-ke-size=&quot;size16&quot;&gt;Java는 보통 컴파일 언어로 분류한다 &lt;a href=&quot;https://docs.oracle.com/en/java/javase/11/tools/javac.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;Oracle javac 공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;는 이렇게 설명한다&lt;/p&gt;
&lt;blockquote data-end=&quot;2385&quot; data-start=&quot;2342&quot; data-ke-style=&quot;style2&quot;&gt;The javac command reads class and interface definitions, written in the Java programming language, and compiles them into bytecode class files&lt;/blockquote&gt;
&lt;p data-end=&quot;3890&quot; data-start=&quot;3874&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, javac 명령은 Java 언어로 작성된 클래스와 인터페이스 정의를 읽고, 그것을 바이트코드 class 파일로 컴파일한다는 뜻이다&lt;/p&gt;
&lt;p data-end=&quot;3890&quot; data-start=&quot;3874&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3890&quot; data-start=&quot;3874&quot; data-ke-size=&quot;size16&quot;&gt;즉 Java는 컴파일을 한다&lt;/p&gt;
&lt;pre id=&quot;code_1777985740453&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hello.java
  &amp;darr; javac
Hello.class&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;3946&quot; data-start=&quot;3928&quot; data-ke-size=&quot;size16&quot;&gt;하지만 여기서 중요한 점이 있다&amp;nbsp;.class 파일은 CPU가 직접 실행하는 일반적인 실행 파일이 아니다 .class 파일은 JVM이 읽고 실행하는 바이트코드 파일이다&lt;/p&gt;
&lt;p data-end=&quot;3946&quot; data-start=&quot;3928&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4028&quot; data-start=&quot;3938&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4063&quot; data-start=&quot;4030&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se6/html/ClassFile.doc.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;JVM 공식 스펙&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;은 class 파일에 대해 이렇게 설명한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Each class file contains the definition of a single class or interface.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, 각 class 파일은 하나의 클래스나 인터페이스 정의를 담고 있다는 뜻이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Java의 실행 흐름은 이렇다&lt;/p&gt;
&lt;pre id=&quot;code_1777985780958&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Java 소스코드 .java
  &amp;darr; javac
바이트코드 .class
  &amp;darr; JVM
인터프리터 실행
  &amp;darr;
자주 실행되는 코드는 JIT 컴파일
  &amp;darr;
기계어 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4349&quot; data-start=&quot;4317&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Java는 소스코드를 바로 실행하는 언어가 아니다&amp;nbsp;실행 전에 .java 파일을 .class 파일로 컴파일한다&amp;nbsp;다만 Java의 컴파일 결과물은 CPU가 바로 실행하는 실행 파일이 아니라, JVM이 실행할 바이트코드다 그래서 Java는 컴파일 언어로 분류되지만, 실행 단계에서는 JVM의 인터프리터와 JIT 컴파일러가 함께 관여한다&lt;/p&gt;
&lt;p data-end=&quot;4349&quot; data-start=&quot;4317&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4526&quot; data-start=&quot;4517&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 이렇다&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-end=&quot;4577&quot; data-start=&quot;4565&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Java = 컴파일 언어로 분류&lt;/span&gt;&lt;br /&gt;&lt;span&gt;이유 = .java 파일을 javac가 .class 바이트코드로 컴파일하기 때문&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;하지만 실제 실행 = JVM이 .class 바이트코드를 읽고 실행&lt;/span&gt;&lt;br /&gt;&lt;span&gt;추가로 JVM은 인터프리터와 JIT 컴파일러를 함께 사용&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; JVM이 왜 필요한가? &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C 같은 언어는 보통 특정 운영체제와 CPU에 맞는 실행 파일을 만든다&amp;nbsp; 그런데 Java는 .class 바이트코드를 만들고, 각 환경에 설치된 JVM이 그 바이트코드를 실행한다&lt;/p&gt;
&lt;pre id=&quot;code_1777986953287&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;같은 Hello.class
  &amp;darr;
Windows JVM
Linux JVM
macOS JVM&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4909&quot; data-start=&quot;4870&quot; data-ke-size=&quot;size16&quot;&gt;운영체제마다 JVM은 다르지만, JVM이 같은 바이트코드를 실행해준다&amp;nbsp;그래서 Java는 특정 OS/CPU용 실행 파일을 직접 만드는 방식과 다르다&amp;nbsp;물론 실제 운영에서는 JVM 버전, 라이브러리 버전, OS 환경 차이 때문에 완전히 마법처럼 어디서나 똑같이 동작한다고 보면 안 된다&amp;nbsp;그래도 C/C++처럼 OS별 네이티브 실행 파일을 직접 따로 만드는 방식과는 다르다&lt;/p&gt;
&lt;h2 data-end=&quot;4909&quot; data-start=&quot;4870&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; JIT는 무엇인가? &lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;5139&quot; data-start=&quot;5102&quot; data-ke-size=&quot;size16&quot;&gt;JIT는 Just-In-Time Compilation의 약자다&amp;nbsp;뜻은 실행 중 필요한 시점에 컴파일한다는 의미다&amp;nbsp;Java의 .class 파일은 바이트코드다&amp;nbsp;바이트코드는 CPU가 직접 실행하는 기계어가 아니다&lt;/p&gt;
&lt;p data-end=&quot;5255&quot; data-start=&quot;5229&quot; data-ke-size=&quot;size16&quot;&gt;그래서 JVM은 이 바이트코드를 실행해야 한다 가장 단순한 방식은 인터프리터 방식이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;바이트코드 한 명령 읽기
  &amp;darr;
해석
  &amp;darr;
실행
  &amp;darr;
다음 바이트코드 읽기&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5352&quot; data-start=&quot;5338&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 시작하기 쉽다 하지만 계속 반복 실행되는 코드를 매번 해석하면 비효율적일 수 있다&lt;/p&gt;
&lt;p data-end=&quot;5420&quot; data-start=&quot;5394&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 서버에서 이런 메서드가 있다고 하자&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public int calculatePrice(int price, int quantity) {
    return price * quantity;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5578&quot; data-start=&quot;5519&quot; data-ke-size=&quot;size16&quot;&gt;이 메서드가 요청마다 수천 번, 수만 번 호출된다면 JVM이 매번 바이트코드를 해석하는 것은 비효율적이다&lt;/p&gt;
&lt;p data-end=&quot;5599&quot; data-start=&quot;5580&quot; data-ke-size=&quot;size16&quot;&gt;그래서 JIT 컴파일러가 필요하다&lt;/p&gt;
&lt;p data-end=&quot;5636&quot; data-start=&quot;5601&quot; data-ke-size=&quot;size16&quot;&gt;JIT 컴파일러는 모든 코드를 처음부터 기계어로 바꾸지 않는다&lt;/p&gt;
&lt;p data-end=&quot;5710&quot; data-start=&quot;5638&quot; data-ke-size=&quot;size16&quot;&gt;대신 JVM은 실행 중 자주 호출되는 코드, 즉 hot code를 찾고, 그 부분을 기계어로 컴파일해 이후 실행을 빠르게 만든다&lt;/p&gt;
&lt;p data-end=&quot;5734&quot; data-start=&quot;5712&quot; data-ke-size=&quot;size16&quot;&gt;Java 실행 흐름을 다시 보면 이렇다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;처음 실행
  &amp;darr;
JVM 인터프리터가 바이트코드를 해석하며 실행
  &amp;darr;
자주 실행되는 코드 감지
  &amp;darr;
JIT 컴파일러가 해당 코드를 기계어로 컴파일
  &amp;darr;
다음부터는 컴파일된 기계어를 더 빠르게 실행&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5874&quot; data-start=&quot;5862&quot; data-ke-size=&quot;size16&quot;&gt;중요한 점은 이것이다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5874&quot; data-start=&quot;5862&quot;&gt;JIT가 있다고 Java가 인터프리터 언어가 되는 것은 아니다&lt;/li&gt;
&lt;li data-end=&quot;5874&quot; data-start=&quot;5862&quot;&gt;JIT는 JVM이 바이트코드를 더 빠르게 실행하기 위한 최적화 방식이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;6043&quot; data-start=&quot;6028&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Java는 바이트코드로 컴파일된 뒤, JVM에서 인터프리터와 JIT 컴파일러를 함께 사용해 실행된다&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;6142&quot; data-start=&quot;6122&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;JavaScript와 비교하면?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;6184&quot; data-start=&quot;6144&quot; data-ke-size=&quot;size16&quot;&gt;JavaScript는 전통적으로 인터프리터 언어로 설명되는 경우가 많다&amp;nbsp;하지만 현대 JavaScript 엔진도 단순히 코드를 읽고 실행하는 것에서 끝나지 않는다&lt;/p&gt;
&lt;p data-end=&quot;6184&quot; data-start=&quot;6144&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6293&quot; data-start=&quot;6238&quot; data-ke-size=&quot;size16&quot;&gt;Chrome과 Node.js에서 사용하는 V8 엔진은 인터프리터와 최적화 컴파일러를 함께 사용한다&lt;/p&gt;
&lt;p data-end=&quot;6315&quot; data-start=&quot;6295&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6315&quot; data-start=&quot;6295&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://v8.dev/blog/launching-ignition-and-turbofan&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;V8 공식 블로그&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;의 Launching Ignition and TurboFan 글에서는 V8의 새로운 실행 파이프라인을 이렇게 설명한다 &amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;6403&quot; data-start=&quot;6317&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;6403&quot; data-start=&quot;6319&quot; data-ke-size=&quot;size16&quot;&gt;built upon Ignition, V8&amp;rsquo;s interpreter, and TurboFan, V8&amp;rsquo;s newest optimizing compiler&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;6472&quot; data-start=&quot;6405&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, V8은 Ignition이라는 인터프리터와 TurboFan이라는 최적화 컴파일러 위에 만들어졌다는 뜻이다&lt;/p&gt;
&lt;p data-end=&quot;6523&quot; data-start=&quot;6474&quot; data-ke-size=&quot;size16&quot;&gt;즉 JavaScript도 실제 실행 내부를 보면 인터프리터와 JIT 컴파일이 섞여 있다&lt;/p&gt;
&lt;p data-end=&quot;6536&quot; data-start=&quot;6525&quot; data-ke-size=&quot;size16&quot;&gt;대략 이런 흐름이다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;JavaScript 소스코드
  &amp;darr;
파싱
  &amp;darr;
바이트코드
  &amp;darr;
Ignition 인터프리터가 실행
  &amp;darr;
자주 실행되는 코드 감지
  &amp;darr;
TurboFan이 최적화 컴파일
  &amp;darr;
기계어로 빠르게 실행&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6672&quot; data-start=&quot;6663&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 이렇다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6672&quot; data-start=&quot;6663&quot;&gt;JavaScript = 전통적으로 인터프리터 언어로 설명됨
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6672&quot; data-start=&quot;6663&quot;&gt;이유 = 브라우저나 Node.js 같은 런타임이 코드를 읽고 실행하기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;6672&quot; data-start=&quot;6663&quot;&gt;하지만&amp;nbsp;실제&amp;nbsp;실행&amp;nbsp;=&amp;nbsp;현대&amp;nbsp;JavaScript&amp;nbsp;엔진은&amp;nbsp;인터프리터와&amp;nbsp;JIT&amp;nbsp;컴파일러를&amp;nbsp;함께&amp;nbsp;사용&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;그래서 Java와 JavaScript 모두 현대 런타임에서는 컴파일, 인터프리팅, JIT 최적화가 섞인다&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6891&quot; data-start=&quot;6877&quot; data-ke-size=&quot;size16&quot;&gt;다만 분류 기준이 다르다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Java
&amp;rarr; javac가 .java를 .class 바이트코드로 컴파일한다.
&amp;rarr; 그래서 보통 컴파일 언어로 분류한다.

JavaScript
&amp;rarr; 브라우저나 Node.js 런타임이 코드를 읽고 실행하는 방식이 중심이다.
&amp;rarr; 그래서 전통적으로 인터프리터 언어로 설명된다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;7078&quot; data-start=&quot;7059&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;백엔드 관점에서 왜 중요할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;7133&quot; data-start=&quot;7080&quot; data-ke-size=&quot;size16&quot;&gt;서버 개발자가 실행 방식을 알아야 하는 이유는 운영할 때 확인해야 할 지점이 달라지기 때문이다 Java 서버는 보통 jar를 배포하고 JVM으로 실행한다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;java -jar app.jar&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;7251&quot; data-start=&quot;7201&quot; data-ke-size=&quot;size16&quot;&gt;Node.js 서버는 JavaScript 소스코드와 Node.js 런타임을 함께 고려한다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;node app.js&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;7304&quot; data-start=&quot;7278&quot; data-ke-size=&quot;size16&quot;&gt;그래서 장애가 났을 때 확인하는 지점도 다르다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Java
- JVM 버전
- heap
- GC
- jar
- classpath
- JIT warm-up

Node.js
- Node 버전
- npm 패키지
- 이벤트 루프
- V8 메모리&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;7470&quot; data-start=&quot;7424&quot; data-ke-size=&quot;size16&quot;&gt;또한 JIT 기반 런타임은 실행 초반과 어느 정도 지난 뒤의 성능이 다를 수 있다&amp;nbsp;Java 서버도 처음 뜬 직후에는 클래스 로딩, JIT 최적화, 캐시 준비 등으로 초반 성능이 흔들릴 수 있다&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7470&quot; data-start=&quot;7424&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7557&quot; data-start=&quot;7536&quot; data-ke-size=&quot;size16&quot;&gt;그래서 운영에서는 이런 질문이 생긴다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&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;헬스체크는 단순 200 응답만 보면 되는가?&lt;/li&gt;
&lt;li&gt;초기 요청에서 응답 시간이 튀는가?&lt;/li&gt;
&lt;li&gt;JVM&amp;nbsp;warm-up이&amp;nbsp;필요한가?&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;또한 컴파일 단계에서 잡히는 오류와 실행 중 특정 코드 경로에서 드러나는 오류가 다르기 때문에 테스트 전략에도 영향을 준다&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;7778&quot; data-start=&quot;7733&quot; data-ke-size=&quot;size16&quot;&gt;Java에서는 타입이 맞지 않거나 문법이 틀리면 컴파일 단계에서 에러가 발생한다&lt;/p&gt;
&lt;p data-end=&quot;7859&quot; data-start=&quot;7780&quot; data-ke-size=&quot;size16&quot;&gt;반면 JavaScript에서는 문법 오류는 파싱 단계에서 잡히더라도, 타입이나 특정 코드 경로의 문제는 실제 실행 중에 드러나는 경우가 많다&lt;/p&gt;
&lt;p data-end=&quot;7859&quot; data-start=&quot;7780&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7876&quot; data-start=&quot;7861&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이렇게 볼 수 있다&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정적&amp;nbsp;타입&amp;nbsp;+&amp;nbsp;컴파일 &lt;br /&gt;&amp;rarr; 컴파일러가 1차 방어선 역할을 한다&lt;/li&gt;
&lt;li&gt;동적 타입 + 런타임 실행 중심 &lt;br /&gt;&amp;rarr; 테스트가 더 많은 실행 경로를 커버해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/☕ Java</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/312</guid>
      <comments>https://winwin0219.tistory.com/312#entry312comment</comments>
      <pubDate>Tue, 5 May 2026 22:32:54 +0900</pubDate>
    </item>
    <item>
      <title>Spring과 SpringBoot</title>
      <link>https://winwin0219.tistory.com/311</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;&lt;a href=&quot;https://spring.io/projects/spring-framework&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;Spring공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;는 이렇게 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;The Spring Framework provides a comprehensive programming and configuration model for modern Java-based enterprise applications&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework는 현대적인 Java 기반 엔터프라이즈 애플리케이션을 만들기 위한 &lt;b&gt;프로그래밍 모델과 설정 모델&lt;/b&gt;을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style10&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 27.7907%;&quot;&gt;&lt;b&gt; 영역 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 72.093%;&quot;&gt;&lt;b&gt; Spring이 해주는 일 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 27.7907%;&quot;&gt;객체 관리&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 72.093%;&quot;&gt;객체를 직접 new 하지 않고 Spring Container가 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 27.7907%;&quot;&gt;의존성 주입&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 72.093%;&quot;&gt;Service가 Repository를 직접 만들지 않고 주입받게 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 27.7907%;&quot;&gt;트랜잭션&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 72.093%;&quot;&gt;@Transactional로 DB 작업 단위를 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 27.7907%;&quot;&gt;웹 MVC&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 72.093%;&quot;&gt;Controller, DispatcherServlet, RequestMapping 등 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 27.7907%;&quot;&gt;데이터 접근&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 72.093%;&quot;&gt;JDBC, ORM, JPA 연동 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 27.7907%;&quot;&gt;테스트&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 72.093%;&quot;&gt;Spring Context 기반 통합 테스트 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 27.7907%;&quot;&gt;AOP&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 72.093%;&quot;&gt;공통 관심사, 예: 트랜잭션, 로깅, 보안 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서도 Spring Framework의 기능을 dependency injection, events, resources, i18n, validation, data binding, type conversion, SpEL, AOP, transactions, DAO support, JDBC, ORM, Spring MVC 등으로 나눠서 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바 객체들을 잘 조립하고, 웹/DB/트랜잭션 같은 서버 애플리케이션의 반복 인프라를 대신 관리해주는 기반 프레임워크&lt;/b&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;b&gt; Spring의 핵심은 IoC Container &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/basics.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext가 Spring IoC 컨테이너를 대표한다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; ApplicationContext란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring IoC Container의 대표 인터페이스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Bean이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Container가 생성하고 관리하는 객체&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1777964375995&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PostRepository postRepository = new PostRepository();
PostService postService = new PostService(postRepository);
PostController postController = new PostController(postService);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바는 위 코드처럼 한다 즉 개발자가 객체를 직접 만들고 연결한다 그러나 Spring에서는 보통 아래와 같이 한다&lt;/p&gt;
&lt;pre id=&quot;code_1777964407519&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class PostService {
    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로 보면 new PostRepository()가 안 보인다 그렇다면 누가 만들었을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Container가 만든다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에 따르면 ApplicationContext는 Bean을 instantiating, configuring, and assembling하는 책임을 가진다. 즉 객체를 생성하고, 설정하고, 서로 조립한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;눈으로 보는 숨은 구조&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1777964458433&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;내 코드
  ├─ Controller 클래스
  ├─ Service 클래스
  ├─ Repository 클래스
  └─ Config 클래스

Spring ApplicationContext
  ├─ 어떤 클래스를 Bean으로 만들지 읽음
  ├─ 필요한 의존성 관계를 파악함
  ├─ 객체를 생성함
  ├─ 생성자에 필요한 객체를 넣어줌
  └─ 완성된 애플리케이션 구조를 실행 가능하게 만듦&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Bean은 Spring이 관리하는 객체다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/introduction.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt; Spring IoC 컨테이너 &lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 애플리케이션의 뼈대를 이루고 Spring IoC 컨테이너가 관리하는 객체를 Bean이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style10&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 일반 객체 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; Bean &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;내가 new로 만듦&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Spring이 만듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;내가 생명주기 관리&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Spring이 생명주기 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;의존성 직접 연결&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Spring이 의존성 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;테스트에서 직접 조립 필요&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Spring Context에서 주입 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;프록시 적용 어려움&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;트랜잭션/AOP 프록시 적용 가능&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;pre id=&quot;code_1777964543598&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
public class PostController { }

@Service
public class PostService { }

@Repository
public interface PostRepository extends JpaRepository&amp;lt;Post, Long&amp;gt; { }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 것들은 Spring이 Bean으로 관리하는 대상이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Service를 붙이는 이유는 서비스니까 예쁘게 표시하려고가 아니라, &lt;b&gt;Spring Container가 이 클래스를 애플리케이션 구성 요소로 인식하고 관리하게 하기 위해서&lt;/b&gt;다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; DI는 Spring이 객체를 대신 연결해주는 방식이다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;Spring의존성관련&amp;nbsp; 문식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Dependency injection (DI) is a process whereby objects define their dependencies&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DI는 객체가 자신에게 필요한 의존성을 정의하고, 컨테이너가 그 의존성을 넣어주는 과정이다.&lt;/p&gt;
&lt;pre id=&quot;code_1777964605253&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CommentService {
    private final CommentRepository commentRepository;

    public CommentService(CommentRepository commentRepository) {
        this.commentRepository = commentRepository;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 CommentService는 &quot; 나는 CommentRepository가 필요해. 근데 내가 직접 만들지는 않을게. 누군가 넣어줘~&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;DI를 쓰면 객체가 자기 의존성을 직접 찾지 않고 의존성의 위치나 구체 클래스를 몰라도 되기 때문에 테스트하기 쉬워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 인터페이스나 추상 클래스에 의존하면 stub/mock 구현을 사용할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Mockito 테스트에서 겪는 구조와 연결된다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777964687004&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Mock
CommentRepository commentRepository;

@InjectMocks
CommentService commentService;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 Spring Container를 안 띄우는 대신, Mockito가 비슷하게 가짜 Repository를 Service에 넣어주는 거다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style10&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 상황 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 누가 의존성 주입함? &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;실제 Spring 실행&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;Spring Container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;단위 테스트 Mockito&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;Mockito&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;직접 Java 코드&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;개발자&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Spring MVC는 Spring 안에 있는 웹 프레임워크다&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/webmvc.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;SpringMVC공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Web MVC is the original web framework built on the Servlet API&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Web MVC는 Servlet API 위에 만들어진 Spring의 원래 웹 프레임워크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Spring MVC는 Spring Framework 전체 중 웹 요청 처리 담당 부분이다&lt;/p&gt;
&lt;pre id=&quot;code_1777964782701&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;브라우저 / Postman
   &amp;darr; HTTP 요청
DispatcherServlet
   &amp;darr;
Controller
   &amp;darr;
Service
   &amp;darr;
Repository
   &amp;darr;
DB&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 여기서 Spring MVC가 주로 해주는 일 &lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style10&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 기능 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 예시 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;URL 매핑&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@GetMapping(&quot;/posts&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;요청 파라미터 바인딩&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@RequestParam, @PathVariable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;JSON 요청 변환&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@RequestBody&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;JSON 응답 변환&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@ResponseBody, ResponseEntity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;예외 처리&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@ControllerAdvice, @ExceptionHandler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;인터셉터&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;인증/인가 전처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;메시지 컨버터&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Java 객체 &amp;harr; JSON 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 controller 테스트 시 MockMvc를 쓰는 건 단순히 메서드 테스트가 아니라 Spring MVC 요청 처리 흐름 일부를 흉내내는 테스트라고 보면된다&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 그럼 Spring Boot는 뭐냐&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;SpringBoot&amp;nbsp; 공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Boot helps you to create stand-alone, production-grade Spring-based applications&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 독립 실행 가능하고 운영 수준에 가까운 Spring 기반 애플리케이션을 만들도록 도와준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;핵심 3단계&lt;/b&gt;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 1. stand-alone : 혼자 실행 가능하다는 뜻 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전 Spring MVC 애플리케이션은 보통 WAR로 만들고, 외부 Tomcat에 배포했다.&lt;/p&gt;
&lt;pre id=&quot;code_1777964930997&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;외부 Tomcat 설치
  └─ WAR 배포
      └─ Spring MVC 애플리케이션 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는&lt;/p&gt;
&lt;pre id=&quot;code_1777964993604&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -jar app.jar
  └─ 내장 Tomcat 같이 뜸
      └─ Spring 애플리케이션 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서도 Spring Boot 애플리케이션은 java -jar로 시작할 수 있다고 설명한다. 그래서 EC2에서 app.jar 실행하고 systemd로 관리했던 구조다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. production-grade : 운영 수준이라는 뜻&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 공식문서의 목표에는 embedded servers, security, metrics, health checks, externalized configuration 같은 비기능 요구사항을 제공한다는 내용이 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Externalized Configuration란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml, 환경변수, command line args 등으로 설정을 코드 밖에서 관리하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Embedded Server란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 안에 포함된 Tomcat/Jetty/Undertow 같은 서버&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;이때, 비기능 요구사항이란&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style10&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기능&lt;span&gt; 요구사항 &lt;/span&gt;&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 비기능 요구사항 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;게시글 작성&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;서버가 죽었는지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;댓글 조회&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;응답 시간이 느린지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;로그인&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;설정을 dev/prod로 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;게시판 생성&lt;/td&gt;
&lt;td style=&quot;text-align: center;&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;즉, Spring Boot는 게시글 CRUD를 쉽게 만든다가 전부가 아니라 애플리케이션을 실행, 배포, 관찰하기 쉽게 만들어준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 바로 운영 관점에서의 Spring Boot의 가치&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.Spring-based : Spring 기반이라는 뜻 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 Spring 없이 독립적으로 존재하는 프레임워크가 아니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 공식문서도 Spring Boot를 Spring-based applications를 만들도록 돕는다고 표현한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;구조&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1777965159667&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Java
  &amp;darr;
Spring Framework
  ├─ IoC Container
  ├─ DI
  ├─ AOP
  ├─ Transaction
  ├─ Spring MVC
  └─ Data Access 지원
      &amp;darr;
Spring Boot
  ├─ Auto Configuration
  ├─ Starter Dependencies
  ├─ Embedded Tomcat
  ├─ Externalized Configuration
  ├─ Actuator
  └─ Executable Jar&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Auto Configuration란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;classpath, 설정값, Bean 존재 여부 등을 보고 Boot가 자동으로 설정 Bean을 등록하는 기능&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Boot가 해주는 핵심 1: 자동 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;SpringBoot 공식문서&lt;/b&gt;&lt;/a&gt;에서는 이렇게 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Auto-configuration is non-invasive.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto-configuration은 침투적이지 않다. 즉 네가 직접 설정하면 Boot 기본 설정은 물러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 프로젝트 상태를 보고 판단한다. 예를들어&lt;/p&gt;
&lt;pre id=&quot;code_1777965223581&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-web'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 넣으면 Boot는 대충 이런 판단을 한다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777965236424&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;어? spring-webmvc가 classpath에 있네?
어? Servlet 기반 웹 애플리케이션이겠네?
그럼 Tomcat 띄우고
DispatcherServlet 등록하고
Jackson 메시지 컨버터 넣고
기본 Error 처리도 넣자.&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Classpath란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 애플리케이션이 사용할 수 있는 라이브러리/클래스 경로&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 직접 Bean을 등록하면 boot는 아, 사용자가 직접 만들었네 그럼 내 기본값은 빠질게~라는 식으로 동작한다&lt;/p&gt;
&lt;pre id=&quot;code_1777965272912&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public ObjectMapper objectMapper() {
    return new ObjectMapper();
}&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;공식문서에서도 직접 DataSource Bean을 추가하면 기본 embedded database 지원이 물러난다고 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Boot를 잘 쓰려면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;8014&quot; data-start=&quot;7936&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;7954&quot; data-start=&quot;7936&quot;&gt;기본값으로 빠르게 시작한다.&lt;/li&gt;
&lt;li data-end=&quot;7987&quot; data-start=&quot;7955&quot;&gt;문제가 생기면 어떤 자동 설정이 적용됐는지 확인한다.&lt;/li&gt;
&lt;li data-end=&quot;8014&quot; data-start=&quot;7988&quot;&gt;필요한 부분만 직접 Bean으로 덮어쓴다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Spring Boot가 해주는 핵심 2: Starter &lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Starters are a set of convenient dependency descriptors&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Starter는 편리한 의존성 묶음이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Boot 없이 Spring MVC를 직접 구성한다고 생각해볼 때, 웹 서버 만들려면 대충 이런 것들을 알아서 골라야 한다&lt;/p&gt;
&lt;pre id=&quot;code_1777965350544&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring-web
spring-webmvc
jackson
validation
tomcat
logging
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 Spring Boot에서는 보통 이거 하나 넣는다&lt;/p&gt;
&lt;pre id=&quot;code_1777965361144&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-web'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 필요한 의존성들이 같이 들어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-boot-starter-web은 대충 이런 의도를 가진 묶음이다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;웹 MVC 애플리케이션 만들 거지?&lt;/li&gt;
&lt;li&gt;그러면&amp;nbsp;MVC,&amp;nbsp;JSON&amp;nbsp;변환,&amp;nbsp;내장&amp;nbsp;Tomcat,&amp;nbsp;검증,&amp;nbsp;로깅&amp;nbsp;같은&amp;nbsp;기본&amp;nbsp;묶음&amp;nbsp;넣어줄게.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-boot-starter-data-jpa는 이런 의도&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;JPA로 DB 접근할 거지?&lt;/li&gt;
&lt;li&gt;그러면 Spring Data JPA, Hibernate, JDBC 관련 기본 묶음 넣어줄게.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Starter는 라이브러리 하나라기보다 필요한 라이브러리 조합을 Spring Boot 팀이 추천 세트로 묶어둔 것에 가깝다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Spring Boot가 해주는 핵심 3: 의존성 버전 관리 &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에 따르면 Spring Boot는 각 릴리스마다 지원하는 의존성 목록을 제공하고, 실제로는 빌드 설정에서 이런 의존성들의 버전을 직접 적지 않아도 Boot가 관리한다. 또한 Spring Boot를 업그레이드하면 관련 의존성들도 일관된 방식으로 업그레이드된다.&lt;/p&gt;
&lt;pre id=&quot;code_1777965453194&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전을 안 적는다. 왜냐면 Spring Boot플러그인과 BOM이 이 Boot 버전에서는 이 라이브러리 버전 조합이 맞다를 관리해주기 때문이다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Spring Boot가 해주는 핵심 4: 내장 서버&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://spring.io/projects/spring-boot&quot;&gt;&lt;b&gt;SpringBoot 공식문서&lt;/b&gt;&lt;/a&gt;에서는 이렇게 말한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Embed Tomcat, Jetty or Undertow directly&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat, Jetty, Undertow를 애플리케이션 안에 직접 포함할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 Spring과 Spring Boot 차이 중 하나다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예전방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;서버에&amp;nbsp;Tomcat&amp;nbsp;설치 &lt;br /&gt;2.&amp;nbsp;WAR&amp;nbsp;파일&amp;nbsp;생성 &lt;br /&gt;3.&amp;nbsp;Tomcat&amp;nbsp;webapps에&amp;nbsp;배포 &lt;br /&gt;4.&amp;nbsp;Tomcat이&amp;nbsp;애플리케이션&amp;nbsp;실행&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Boot 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;JAR&amp;nbsp;파일&amp;nbsp;생성 &lt;br /&gt;2.&amp;nbsp;java&amp;nbsp;-jar&amp;nbsp;app.jar &lt;br /&gt;3.&amp;nbsp;JAR&amp;nbsp;안의&amp;nbsp;내장&amp;nbsp;Tomcat이&amp;nbsp;같이&amp;nbsp;실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 EC2에서 java -jar app.jar 혹은 systemd로 ExecStart=/usr/bin/java -jar /home/ubuntu/apps/coreboard/app.jar&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 운영할 수 있었던 것이다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; Spring Boot가 해주는 핵심 5: @SpringBootApplication&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle/&quot;&gt;&lt;b&gt;SpringBoot 공식문서&lt;/b&gt;&lt;/a&gt;에서는 이렇게 말한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 가장 많이 보는 게 아래 코드다&lt;/p&gt;
&lt;pre id=&quot;code_1777965591537&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
public class CoreBoardApplication {
    public static void main(String[] args) {
        SpringApplication.run(CoreBoardApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에 따르면 @SpringBootApplication 하나로 auto-configuration, component scan, extra configuration 정의를 활성화할 수 있다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Component Scan란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Component, @Service, @Controller 등을 찾아 Bean으로 등록하는 과정&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 이 애노테이션은 크게 세 가지 의미를 합친 것&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 88px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style10&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;b&gt; 구성 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;b&gt; 의미 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;@SpringBootConfiguration&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;이 클래스가 설정 클래스임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;@EnableAutoConfiguration&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;Boot 자동 설정 켬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;@ComponentScan&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;현재 패키지 기준으로 컴포넌트 스캔&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서도 @ComponentScan은 애플리케이션이 위치한 패키지에서 @Component scan을 활성화한다고 설명한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 패키지 위치가 중요하다.&lt;/p&gt;
&lt;pre id=&quot;code_1777965641837&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;com.example.coreboard
 ├─ CoreBoardApplication.java
 └─ domain
     ├─ post
     ├─ comment
     ├─ board
     └─ users&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;나쁜구조&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1777965656533&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;com.example
 ├─ post
 └─ app
     └─ CoreBoardApplication.java&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면 @SpringBootApplication이 붙은 클래스의 패키지를 기준으로 하위 패키지를 스캔하기 때문이다. Boot 공식문서도 main application class를 root package에 두는 것을 권장한다고 설명한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; SpringApplication.run()은 뭐 하는 애냐 &lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1777965684588&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SpringApplication.run(CoreBoardApplication.class, args);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한 줄은 단순히 main 메서드 실행이 아니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;애플리케이션&amp;nbsp;타입&amp;nbsp;판단&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;Servlet&amp;nbsp;웹인가?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;Reactive&amp;nbsp;웹인가?&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 일반 애플리케이션인가?&lt;br /&gt;2.&amp;nbsp;ApplicationContext&amp;nbsp;생성&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Spring IoC Container 준비&lt;br /&gt;3.&amp;nbsp;Environment&amp;nbsp;준비&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;application.yml&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;profile&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;command&amp;nbsp;line&amp;nbsp;args&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- OS 환경변수&lt;br /&gt;4.&amp;nbsp;BeanDefinition&amp;nbsp;로딩&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;@Component&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;@Service&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;@Repository&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;@Controller&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;@Configuration&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- @Bean&lt;br /&gt;5.&amp;nbsp;Auto&amp;nbsp;Configuration&amp;nbsp;적용&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- classpath와 조건 기반으로 자동 설정 적용&lt;br /&gt;6.&amp;nbsp;Bean&amp;nbsp;생성&amp;nbsp;및&amp;nbsp;의존성&amp;nbsp;주입&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Controller &amp;rarr; Service &amp;rarr; Repository&lt;br /&gt;7.&amp;nbsp;내장&amp;nbsp;웹&amp;nbsp;서버&amp;nbsp;실행&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- Tomcat 등&lt;br /&gt;8.&amp;nbsp;애플리케이션&amp;nbsp;준비&amp;nbsp;완료&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Boot 애플리케이션을 실행했을 때 로그에 이런 게 뜨는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1777965734044&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Started CoreBoardApplication in ... seconds&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 만약 8080 포트가 이미 사용 중이면 Boot는 실패 분석 메시지를 제공한다. 공식문서에서도 port 8080이 이미 사용 중일 때 APPLICATION FAILD TO START와 함께 원인과 조치를 보여주는 예시를 든다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring과 SpringBoot의 차이를 한 장으로 정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style10&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; Spring &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; Spring Boot &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;정체&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;기반 프레임워크&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Spring 기반 애플리케이션을 쉽게 만드는 도구층&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;핵심&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;IoC, DI, AOP, MVC, Transaction&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Auto Configuration, Starter, Embedded Server, Actuator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;설정 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;직접 설정이 많음&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;기본 설정 자동 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;실행 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;외부 WAS 배포가 흔했음&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;java -jar 단독 실행 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;의존성 관리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;개발자가 조합 관리&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Boot가 권장 버전 조합 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;운영 기능&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;직접 붙여야 함&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Actuator, metrics, health 등 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;대체 관계&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;기반&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Spring 위에서 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;ApplicationContext, @Service, @Transactional&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;@SpringBootApplication, starter, auto config&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;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Actuator란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;health check, metrics 등 운영용 기능을 제공하는 Spring Boot 모듈&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Boot를 쓰면 Spring을 몰라도 되냐? &lt;/b&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;No.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 알아야 한다고 본다 왜냐면 Boot가 해주는 자동 설정의 결과물이 결국 Spring Bean이기 때문이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어&lt;/p&gt;
&lt;pre id=&quot;code_1777965903783&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class PostService { }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 Spring 개념이다&lt;/p&gt;
&lt;pre id=&quot;code_1777965913524&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-data-jpa'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 Boot 개념이다&lt;/p&gt;
&lt;pre id=&quot;code_1777965925266&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void createPost(...) { }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 Spring transactional/AOP 개념이다&lt;/p&gt;
&lt;pre id=&quot;code_1777965941727&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 Boot external configuration 개념이다&lt;/p&gt;
&lt;pre id=&quot;code_1777965959591&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface PostRepository extends JpaRepository&amp;lt;Post, Long&amp;gt; { }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 Spring Data JPA + Boot 자동 설정이 같이 얽힌 구조다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring을 모르면&lt;/b&gt;&lt;br /&gt;- 어노테이션 붙였는데 왜 안 되지?&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Spring Boot만 모르면&lt;/b&gt;&lt;br /&gt;설정은 어디서 자동으로 된 거지?&lt;br /&gt;&lt;br /&gt;&lt;b&gt;둘 다 알면&lt;/b&gt;&lt;br /&gt;이 Bean은 component scan으로 등록됐고, &lt;br /&gt;DataSource는&amp;nbsp;Boot&amp;nbsp;auto-configuration으로&amp;nbsp;잡혔고, &lt;br /&gt;TransactionManager는&amp;nbsp;JPA&amp;nbsp;설정&amp;nbsp;기반으로&amp;nbsp;등록됐고, &lt;br /&gt;@Transactional은 프록시로 감싸져서 동작하겠네.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/  Spring</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/311</guid>
      <comments>https://winwin0219.tistory.com/311#entry311comment</comments>
      <pubDate>Tue, 5 May 2026 16:30:22 +0900</pubDate>
    </item>
    <item>
      <title>[Devlog]CoreBoard pagination 뜯어고쳐!</title>
      <link>https://winwin0219.tistory.com/310</link>
      <description>&lt;p data-end=&quot;228&quot; data-start=&quot;189&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;댓글 전체조회 기능을 구현하려고 하다가 페이지네이션에서 한 번 멈췄다.&amp;nbsp;처음에는 페이지네이션을 단순히 이렇게 이해하고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Offset&lt;/span&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Keyset&lt;/span&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Slice&lt;/span&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; letter-spacing: 0px;&quot;&gt;그런데 공부하다 보니 이 분류가 정확하지 않았다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;431&quot; data-start=&quot;324&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;431&quot; data-start=&quot;324&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Page와 Slice는 Spring Data가 조회 결과를 어떤 형태로 반환할지에 대한 모델이고, Offset과 Keyset은 DB에서 데이터를 어떤 방식으로 가져올지에 대한 조회 방식이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;449&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;449&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 내용은 따로 정리해두었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;485&quot; data-start=&quot;451&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/309&quot;&gt;https://winwin0219.tistory.com/309&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777962937007&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Page, Slice, Offset, Keyset&quot; data-og-description=&quot;댓글 전체조회 기능을 구현하려고 하다가 페이지네이션에서 한 번 멈췄다.처음에는 페이지네이션 종류를 이렇게 이해하고 있었다.OffsetKeysetSlice 근데 공부하다 보니 이 분류가 정확하지 않았다.&quot; data-og-host=&quot;winwin0219.tistory.com&quot; data-og-source-url=&quot;https://winwin0219.tistory.com/309&quot; data-og-url=&quot;https://winwin0219.tistory.com/309&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bTAZJT/dJMb8SXBDTp/UXOo5cQlXNcWbOldDAgvCK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/d2sk8B/dJMb8SXBDTq/SwAxq0UohAlQ03gKqcK9K0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/309&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://winwin0219.tistory.com/309&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bTAZJT/dJMb8SXBDTp/UXOo5cQlXNcWbOldDAgvCK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/d2sk8B/dJMb8SXBDTq/SwAxq0UohAlQ03gKqcK9K0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;Page, Slice, Offset, Keyset&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;댓글 전체조회 기능을 구현하려고 하다가 페이지네이션에서 한 번 멈췄다.처음에는 페이지네이션 종류를 이렇게 이해하고 있었다.OffsetKeysetSlice 근데 공부하다 보니 이 분류가 정확하지 않았다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;winwin0219.tistory.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-end=&quot;532&quot; data-start=&quot;487&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이걸 다시 공부하고 나니 CoreBoard의 페이지네이션 전략도 다시 봐야 했다.&amp;nbsp;처음에는 게시글 목록에 Keyset pagination을 적용했다.&amp;nbsp;이유는 단순했다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Offset보다 Keyset이 빠르다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;깊은 페이지로 갈수록 Offset 비용이 커질 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그러면&amp;nbsp;게시글&amp;nbsp;목록도&amp;nbsp;Keyset으로&amp;nbsp;가는&amp;nbsp;게&amp;nbsp;낫지&amp;nbsp;않을까?&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;704&quot; data-start=&quot;685&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;성능만 보면 틀린 판단은 아니었다.&amp;nbsp;하지만 문제는 게시글 목록의 사용 흐름이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;783&quot; data-start=&quot;733&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;783&quot; data-start=&quot;733&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;CoreBoard의 게시글 목록은 무한스크롤 피드라기보다는 일반적인 게시판 목록에 가깝다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;859&quot; data-start=&quot;785&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;사용자는 게시판 목록에서 1페이지, 2페이지처럼 특정 페이지로 이동할 수 있고, 전체 게시글 수나 전체 페이지 수가 필요할 수 있다. 예를 들면 이런 흐름이다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;1 2 3 4 5 다음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;942&quot; data-start=&quot;903&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이런 화면에서는 단순히 다음 데이터가 더 있는가?만으로는 부족하다.&amp;nbsp;전체 데이터가 몇 개인지, 전체 페이지가 몇 개인지 알아야 페이지 번호 기반 UI를 만들 수 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;942&quot; data-start=&quot;903&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1044&quot; data-start=&quot;1001&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그래서 게시글 목록은 Offset + Page가 더 자연스럽다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;게시글 목록
=&amp;gt; Offset + Page&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1132&quot; data-start=&quot;1083&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Offset은 page와 size를 기준으로 몇 개를 건너뛰고 몇 개를 가져올지 정한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM post
ORDER BY id DESC
LIMIT 10 OFFSET 20;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1242&quot; data-start=&quot;1202&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Page는 가져온 데이터와 함께 전체 개수, 전체 페이지 수를 응답한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;content&quot;: [
    {
      &quot;postId&quot;: 1,
      &quot;title&quot;: &quot;게시글 제목&quot;
    }
  ],
  &quot;pageInfo&quot;: {
    &quot;page&quot;: 2,
    &quot;size&quot;: 10,
    &quot;totalElements&quot;: 53,
    &quot;totalPages&quot;: 6
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1470&quot; data-start=&quot;1432&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;물론 Offset 방식은 깊은 페이지로 갈수록 비용이 커질 수 있다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;다만 게시판 목록에서는 페이지 번호 이동이라는 UX가 중요하므로, 무조건 Keyset을 적용하는 것보다 Offset을 사용하고 정렬 기준에 인덱스를 적용하는 쪽이 더 맞다고 판단했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1635&quot; data-start=&quot;1592&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;너무 깊은 페이지 접근은 검색이나 필터로 유도하는 것이 더 현실적이라고 봤다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;1668&quot; data-start=&quot;1642&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;댓글은 Page가 아니라 Slice가 맞다&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1700&quot; data-start=&quot;1670&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;반대로 댓글 목록은 게시글 목록과 사용 흐름이 다르다.&amp;nbsp;댓글에서 사용자가 정말 궁금한 것은 보통 이게 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1774&quot; data-start=&quot;1762&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;댓글이 총 몇 페이지인가?보다 댓글을 더 볼 수 있는가?에가깝다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1841&quot; data-start=&quot;1804&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;즉 댓글은 전체 페이지 수보다 다음 댓글 존재 여부가 더 중요하다. 그래서 댓글 전체조회는 Offset + Slice로 가는 것이 맞다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Slice&amp;lt;Comment&amp;gt; findByPostId(Long postId, Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1998&quot; data-start=&quot;1965&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Slice는 전체 개수와 전체 페이지 수를 알려주지 않는다.&amp;nbsp;대신 현재 목록과 다음 데이터 존재 여부를 알려준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2047&quot; data-start=&quot;2031&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;응답도 이런 형태면 충분하다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;content&quot;: [
    {
      &quot;commentId&quot;: 1,
      &quot;content&quot;: &quot;댓글 내용&quot;
    }
  ],
  &quot;pageInfo&quot;: {
    &quot;page&quot;: 0,
    &quot;size&quot;: 10,
    &quot;hasNext&quot;: true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2235&quot; data-start=&quot;2216&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;여기서 핵심은 hasNext다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;hasNext = true
=&amp;gt; 다음 댓글이 더 있음

hasNext = false
=&amp;gt; 더 이상 불러올 댓글이 없음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2364&quot; data-start=&quot;2316&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;댓글 화면에서 필요한 건 전체 댓글 페이지 수가 아니라 더보기 버튼을 보여줄지 말지다.&amp;nbsp;그래서 Page보다 Slice가 더 자연스럽다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;2428&quot; data-start=&quot;2399&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;PageInfo를 그대로 재사용하지 않기로 했다&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2467&quot; data-start=&quot;2430&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;기존에 Page 응답을 위해 PageInfo를 사용하고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class PageInfo {
    private final int page;
    private final int size;
    private final long totalElements;
    private final int totalPages;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2658&quot; data-start=&quot;2636&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 구조는 Page 응답에는 잘 맞는다.&amp;nbsp;totalElements와 totalPages가 있기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2729&quot; data-start=&quot;2701&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;하지만 Slice 응답에는 이 두 값이 필요 없다. Slice의 관심사는 전체 개수가 아니라 다음 데이터 존재 여부다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;PageInfo
=&amp;gt; page, size, totalElements, totalPages

SliceInfo
=&amp;gt; page, size, hasNext&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2920&quot; data-start=&quot;2867&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;처음에는 PageInfo를 상속해서 SliceInfo를 만들 수도 있지 않을까 생각했다.&amp;nbsp;하지만 그렇게 하면 Slice인데도 totalElements, totalPages를 억지로 들고 있어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Slice인데 totalElements가 있다?
Slice인데 totalPages가 있다?

그럼 이 값은 0이라는 뜻인가?
아니면 모른다는 뜻인가?&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3096&quot; data-start=&quot;3086&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;의미가 애매해진다.&amp;nbsp;그래서 이건 상속 문제가 아니라 설계 의미의 문제라고 봤다. SliceInfo는 PageInfo의 하위 개념이 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;3191&quot; data-start=&quot;3171&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;둘은 비슷해 보이지만 목적이 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;PageInfo
=&amp;gt; 전체 개수와 전체 페이지 수를 아는 응답 정보

SliceInfo
=&amp;gt; 다음 데이터 존재 여부만 아는 응답 정보&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3311&quot; data-start=&quot;3281&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그래서 Slice 응답용 정보는 따로 분리하기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class SliceInfo {
    private final int page;
    private final int size;
    private final boolean hasNext;

    public SliceInfo(int page, int size, boolean hasNext) {
        this.page = page;
        this.size = size;
        this.hasNext = hasNext;
    }

    public int getPage() {
        return page;
    }

    public int getSize() {
        return size;
    }

    public boolean isHasNext() {
        return hasNext;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3795&quot; data-start=&quot;3769&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 Slice 응답은 이런 식으로 구성한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;public class SliceResponse&amp;lt;T&amp;gt; {
    private final List&amp;lt;T&amp;gt; content;
    private final SliceInfo pageInfo;

    public SliceResponse(List&amp;lt;T&amp;gt; content, SliceInfo pageInfo) {
        this.content = content;
        this.pageInfo = pageInfo;
    }

    public List&amp;lt;T&amp;gt; getContent() {
        return content;
    }

    public SliceInfo getPageInfo() {
        return pageInfo;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4226&quot; data-start=&quot;4188&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이렇게 분리하면 Page 응답과 Slice 응답의 의미가 명확해진다.&amp;nbsp;Page 응답은 전체 개수와 전체 페이지 수를 포함한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4290&quot; data-start=&quot;4261&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Slice 응답은 다음 데이터 존재 여부만 포함한다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;4321&quot; data-start=&quot;4297&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Keyset은 버린 게 아니라 분리했다&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;4376&quot; data-start=&quot;4323&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번에 게시글 목록을 Offset + Page로 바꾼다고 해서 Keyset을 버린 것은 아니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4376&quot; data-start=&quot;4323&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4376&quot; data-start=&quot;4323&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Keyset은 여전히 장점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4455&quot; data-start=&quot;4399&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;특히 최신글 더보기나 무한스크롤처럼 계속 아래로 이어서 조회하는 화면에서는 Keyset이 잘 맞는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4510&quot; data-start=&quot;4457&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어 마지막으로 본 게시글 ID가 100이라면 다음 데이터는 이런 식으로 가져올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM post
WHERE id &amp;lt; 100
ORDER BY id DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4619&quot; data-start=&quot;4585&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 방식은 페이지 번호 이동에는 약하지만, 이어보기에는 좋다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4619&quot; data-start=&quot;4585&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4713&quot; data-start=&quot;4621&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;따라서 Keyset은 일반 게시판 목록에 억지로 적용하지 않고, 최신글 더보기나 무한스크롤 API가 필요해졌을 때 별도 API로 제공하는 방향이 더 맞다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;4728&quot; data-start=&quot;4720&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;최종 결정&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;4766&quot; data-start=&quot;4730&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;정리하면 CoreBoard의 페이지네이션 전략은 이렇게 가져간다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;4994&quot; data-start=&quot;4768&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;조회&amp;nbsp;&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt; 대상 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt; 전략이유 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;4862&quot; data-start=&quot;4802&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;4811&quot; data-start=&quot;4802&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;게시글 목록&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;4827&quot; data-start=&quot;4811&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Offset + Page&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;4862&quot; data-start=&quot;4827&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;페이지 번호 이동, 전체 개수, 전체 페이지 수가 필요함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;4919&quot; data-start=&quot;4863&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;4871&quot; data-start=&quot;4863&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;댓글 목록&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;4888&quot; data-start=&quot;4871&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Offset + Slice&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;4919&quot; data-start=&quot;4888&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;전체 페이지 수보다 다음 댓글 존재 여부가 중요함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;4994&quot; data-start=&quot;4920&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;4938&quot; data-start=&quot;4920&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;최신글 더보기 / 무한스크롤&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;4954&quot; data-start=&quot;4938&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Keyset 별도 API&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;4994&quot; data-start=&quot;4954&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;마지막으로 본 기준값 이후 데이터를 이어서 조회하는 흐름에 적합함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5026&quot; data-start=&quot;4996&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;처음에는 빠른 방식 하나를 고르면 된다고 생각했다.&amp;nbsp;하지만 다시 보니 페이지네이션은 성능만의 문제가 아니었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5143&quot; data-start=&quot;5062&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;사용자가 목록을 어떤 방식으로 탐색하는지, 응답에 어떤 정보가 필요한지, 그 정보를 만들기 위해 DB가 어떤 비용을 부담하는지를 같이 봐야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5143&quot; data-start=&quot;5062&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5186&quot; data-start=&quot;5145&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그래서 모든 목록 조회에 하나의 페이지네이션 전략을 적용하지 않기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5212&quot; data-start=&quot;5188&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;게시글 목록은 Offset + Page.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5238&quot; data-start=&quot;5214&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;댓글 목록은 Offset + Slice.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5278&quot; data-start=&quot;5240&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;최신글 더보기나 무한스크롤은 필요할 때 Keyset 별도 API.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5316&quot; data-start=&quot;5280&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5316&quot; data-start=&quot;5280&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이렇게 조회 목적에 따라 분리하는 쪽이 더 자연스럽다고 판단했다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;5328&quot; data-start=&quot;5323&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;5345&quot; data-start=&quot;5330&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번 결정의 핵심은 이거다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Offset이 느릴 수 있으니 무조건 Keyset을 쓰자가 아니다.
조회 목적에 따라 Offset + Page, Offset + Slice, Keyset을 나눠야 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5482&quot; data-start=&quot;5455&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;게시글 목록은 페이지 번호 기반 탐색이 중요하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5482&quot; data-start=&quot;5455&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5518&quot; data-start=&quot;5484&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;댓글은 전체 페이지 수보다 다음 데이터 존재 여부가 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5567&quot; data-start=&quot;5520&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;최신글 더보기는 마지막으로 본 기준값 이후 데이터를 이어서 가져오는 흐름이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;5629&quot; data-start=&quot;5569&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5629&quot; data-start=&quot;5569&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;따라서 페이지네이션 전략은 하나로 고정하는 것이 아니라, 조회 목적과 사용자 흐름에 맞게 나누는 것이 맞다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/310</guid>
      <comments>https://winwin0219.tistory.com/310#entry310comment</comments>
      <pubDate>Tue, 5 May 2026 15:40:05 +0900</pubDate>
    </item>
    <item>
      <title>[JPA]Page, Slice, Offset, Keyset</title>
      <link>https://winwin0219.tistory.com/309</link>
      <description>&lt;p data-end=&quot;168&quot; data-start=&quot;129&quot; data-ke-size=&quot;size16&quot;&gt;댓글 전체조회 기능을 구현하려고 하다가 페이지네이션에서 한 번 멈췄다.&lt;/p&gt;
&lt;p data-end=&quot;199&quot; data-start=&quot;170&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 페이지네이션 종류를 이렇게 이해하고 있었다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Offset
Keyset
Slice&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;260&quot; data-start=&quot;234&quot; data-ke-size=&quot;size16&quot;&gt;근데 공부하다 보니 이 분류가 정확하지 않았다.&lt;/p&gt;
&lt;p data-end=&quot;295&quot; data-start=&quot;262&quot; data-ke-size=&quot;size16&quot;&gt;정확히는 페이지네이션을 볼 때 두 가지를 나눠서 봐야 했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. DB에서 데이터를 어떻게 가져올 것인가?
   - Offset
   - Keyset

2. 가져온 결과를 어떤 형태로 응답할 것인가?
   - Page
   - Slice
   - List
   - Window&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;519&quot; data-start=&quot;431&quot; data-ke-size=&quot;size16&quot;&gt;즉, Offset, Keyset은 &lt;b&gt;조회 방식&lt;/b&gt;이고, Page, Slice는 Spring Data가 제공하는 &lt;b&gt;반환 타입&lt;/b&gt;에 가깝다.&amp;nbsp;처음에는 Page = Offset, Slice = Keyset 같은 느낌으로 이해했는데 아니었다.&lt;/p&gt;
&lt;p data-end=&quot;657&quot; data-start=&quot;581&quot; data-ke-size=&quot;size16&quot;&gt;Page는 전체 개수와 전체 페이지 수를 포함하는 반환 모델이고, Slice는 다음 데이터가 있는지만 알려주는 반환 모델이다.&lt;/p&gt;
&lt;p data-end=&quot;744&quot; data-start=&quot;659&quot; data-ke-size=&quot;size16&quot;&gt;반대로 Offset은 앞에서 몇 개를 건너뛰고 가져오는 방식이고, Keyset은 마지막으로 본 값을 기준으로 다음 데이터를 가져오는 방식이다.&lt;/p&gt;
&lt;pre id=&quot;code_1777973988872&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[클라이언트 요청]  &amp;rarr;  Pageable  &amp;rarr;  [DB 조회]  &amp;rarr;  Slice / Page  &amp;rarr;  [응답]
                     &amp;uarr; 입력                           &amp;uarr; 출력
                  &quot;몇 번째 페이지,                 &quot;결과 데이터 +
                   몇 개씩 줘&quot;                      hasNext/totalPage&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-end=&quot;767&quot; data-start=&quot;751&quot; data-ke-size=&quot;size26&quot;&gt;공식문서에서 확인한 내용&lt;/h2&gt;
&lt;p data-end=&quot;890&quot; data-start=&quot;769&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/commons/reference/repositories/query-methods-details.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;b&gt;Spring Data Commons 공식문서&lt;/b&gt;&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;는 Pageable, Sort, Limit 같은 타입을 인식해서 쿼리에 pagination, sorting, limiting을 동적으로 적용할 수 있다고 설명한다.&lt;/p&gt;
&lt;blockquote data-end=&quot;1056&quot; data-start=&quot;892&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;1056&quot; data-start=&quot;894&quot; data-ke-size=&quot;size16&quot;&gt;the infrastructure recognizes certain specific types like Pageable, Sort and Limit, to apply pagination, sorting and limiting to your queries dynamically.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;1198&quot; data-start=&quot;1058&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, Repository 메서드 파라미터에 Pageable 같은 타입을 넣으면 Spring Data가 그 정보를 보고 페이징, 정렬, 제한을 쿼리에 적용해준다는 뜻이다.&lt;/p&gt;
&lt;p data-end=&quot;1224&quot; data-start=&quot;1200&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 공식문서에도 이런 형태가 나온다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Page&amp;lt;User&amp;gt; findByLastname(String lastname, Pageable pageable);

Slice&amp;lt;User&amp;gt; findByLastname(String lastname, Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1466&quot; data-start=&quot;1367&quot; data-ke-size=&quot;size16&quot;&gt;같은 Pageable을 받더라도 반환 타입을 Page로 할 수도 있고, Slice로 할 수도 있다.&amp;nbsp;이 말은 곧 Pageable = Page 전용이 아니라는 뜻이다.&amp;nbsp;Pageable은 요청 정보다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;page
size
sort&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1568&quot; data-start=&quot;1557&quot; data-ke-size=&quot;size16&quot;&gt;이런 정보를 담는다.&lt;/p&gt;
&lt;p data-end=&quot;1598&quot; data-start=&quot;1570&quot; data-ke-size=&quot;size16&quot;&gt;반면 Page와 Slice는 응답 모델이다.&lt;/p&gt;
&lt;h2 data-end=&quot;1619&quot; data-start=&quot;1605&quot; data-ke-size=&quot;size26&quot;&gt;Page란 무엇인가?&lt;/h2&gt;
&lt;p data-end=&quot;1672&quot; data-start=&quot;1621&quot; data-ke-size=&quot;size16&quot;&gt;Page는 데이터 목록뿐 아니라 전체 개수와 전체 페이지 수까지 알려주는 반환 타입이다.&amp;nbsp;예를 들면 이런 응답이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;content&quot;: [
    {
      &quot;postId&quot;: 1,
      &quot;title&quot;: &quot;제목&quot;
    }
  ],
  &quot;pageInfo&quot;: {
    &quot;page&quot;: 0,
    &quot;size&quot;: 10,
    &quot;totalElements&quot;: 53,
    &quot;totalPages&quot;: 6
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1892&quot; data-start=&quot;1874&quot; data-ke-size=&quot;size16&quot;&gt;여기서 중요한 값은 이 두 개다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;totalElements
totalPages&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2021&quot; data-start=&quot;1932&quot; data-ke-size=&quot;size16&quot;&gt;totalElements는 조건에 맞는 전체 데이터 개수다.&amp;nbsp;&lt;br /&gt;totalPages는 전체 데이터를 size 기준으로 나눴을 때 몇 페이지가 나오는지다.&lt;/p&gt;
&lt;p data-end=&quot;2021&quot; data-start=&quot;1932&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2080&quot; data-start=&quot;2023&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 전체 게시글이 53개이고 한 페이지에 10개씩 보여준다면 전체 페이지 수는 6페이지가 된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;53 / 10 = 5.3
=&amp;gt; 올림해서 6페이지&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2155&quot; data-start=&quot;2122&quot; data-ke-size=&quot;size16&quot;&gt;문제는 DB가 이 값을 그냥 알고 있는 게 아니라는 점이다.&amp;nbsp;현재 페이지 데이터 10개를 가져오는 쿼리는 이런 식이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM post
ORDER BY id DESC
LIMIT 10 OFFSET 0;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2290&quot; data-start=&quot;2258&quot; data-ke-size=&quot;size16&quot;&gt;이 쿼리는 현재 페이지에 필요한 데이터 10개만 가져온다. 하지만 이 쿼리만으로는 전체 게시글이 53개인지, 530개인지, 5만 개인지 알 수 없다.&amp;nbsp;그래서 전체 개수를 알려면 DB에 따로 물어봐야 한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT COUNT(*)
FROM post;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2431&quot; data-start=&quot;2415&quot; data-ke-size=&quot;size16&quot;&gt;이게 count query다.&amp;nbsp;Spring Data 공식문서도 Page는 전체 element 수와 전체 page 수를 알고, 이를 계산하기 위해 infrastructure가 count query를 실행한다고 설명한다.&lt;/p&gt;
&lt;blockquote data-end=&quot;2706&quot; data-start=&quot;2541&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;2706&quot; data-start=&quot;2543&quot; data-ke-size=&quot;size16&quot;&gt;A Page knows about the total number of elements and pages available. It does so by the infrastructure triggering a count query to calculate the overall number.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;2842&quot; data-start=&quot;2708&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, Page는 전체 데이터 수와 전체 페이지 수를 알아야 하므로 Spring Data가 전체 개수를 계산하기 위한 count query를 실행한다는 뜻이다.&lt;/p&gt;
&lt;p data-end=&quot;2842&quot; data-start=&quot;2708&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2901&quot; data-start=&quot;2844&quot; data-ke-size=&quot;size16&quot;&gt;즉 Page는 단순히 데이터만 가져오는 게 아니라, 전체 개수를 계산하는 작업까지 포함할 수 있다. 공식문서도 이 count query가 비용이 클 수 있다고 말한다.&lt;/p&gt;
&lt;blockquote data-end=&quot;3037&quot; data-start=&quot;2942&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;3037&quot; data-start=&quot;2944&quot; data-ke-size=&quot;size16&quot;&gt;As this might be expensive (depending on the store used), you can instead return a Slice.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;3152&quot; data-start=&quot;3039&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, 저장소나 쿼리 상황에 따라 전체 개수를 세는 작업이 비쌀 수 있으므로, 대신 Slice를 반환할 수 있다는 뜻이다.&lt;/p&gt;
&lt;p data-end=&quot;3178&quot; data-start=&quot;3154&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Page는 이런 화면에 어울린다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;전체 게시글 수가 필요할 때
전체 페이지 수가 필요할 때
1, 2, 3, 4 같은 페이지 번호 UI가 필요할 때
관리자 화면처럼 정확한 총 개수가 중요할 때&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;3301&quot; data-start=&quot;3286&quot; data-ke-size=&quot;size26&quot;&gt;Slice란 무엇인가?&lt;/h2&gt;
&lt;p data-end=&quot;3338&quot; data-start=&quot;3303&quot; data-ke-size=&quot;size16&quot;&gt;Slice는 전체 개수와 전체 페이지 수를 알려주지 않는다. 대신 현재 데이터 목록과 다음 데이터가 있는지 여부만 알려준다.&lt;/p&gt;
&lt;p data-end=&quot;3391&quot; data-start=&quot;3377&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 응답이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;content&quot;: [
    {
      &quot;commentId&quot;: 1,
      &quot;content&quot;: &quot;댓글 내용&quot;
    }
  ],
  &quot;pageInfo&quot;: {
    &quot;page&quot;: 0,
    &quot;size&quot;: 10,
    &quot;hasNext&quot;: true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3579&quot; data-start=&quot;3560&quot; data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 hasNext다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;hasNext = 다음 데이터가 더 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3658&quot; data-start=&quot;3619&quot; data-ke-size=&quot;size16&quot;&gt;공식문서도 Slice는 다음 Slice가 있는지만 안다고 설명한다.&lt;/p&gt;
&lt;blockquote data-end=&quot;3726&quot; data-start=&quot;3660&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;3726&quot; data-start=&quot;3662&quot; data-ke-size=&quot;size16&quot;&gt;A Slice knows only about whether a next Slice is available&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;3809&quot; data-start=&quot;3728&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, Slice는 다음 조각이 존재하는지만 알고 있다는 뜻이다.&lt;/p&gt;
&lt;p data-end=&quot;3841&quot; data-start=&quot;3811&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3841&quot; data-start=&quot;3811&quot; data-ke-size=&quot;size16&quot;&gt;즉 Slice는 전체 데이터가 몇 개인지는 모른다. 대신 프론트가 더보기 버튼을 보여줄지 말지만 판단할 수 있게 해준다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;hasNext = true
=&amp;gt; 더보기 버튼 보여줌

hasNext = false
=&amp;gt; 더보기 버튼 숨김&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3980&quot; data-start=&quot;3955&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Slice는 이런 화면에 어울린다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;더보기
무한스크롤
다음 목록 불러오기
전체 개수보다 다음 데이터 존재 여부가 중요한 화면&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;4078&quot; data-start=&quot;4050&quot; data-ke-size=&quot;size26&quot;&gt;왜 Slice는 size + 1개를 조회할까?&lt;/h2&gt;
&lt;p data-end=&quot;4098&quot; data-start=&quot;4080&quot; data-ke-size=&quot;size16&quot;&gt;처음에 이 부분이 제일 헷갈렸다.&lt;/p&gt;
&lt;p data-end=&quot;4140&quot; data-start=&quot;4100&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4140&quot; data-start=&quot;4100&quot; data-ke-size=&quot;size16&quot;&gt;요청 size가 10이면 그냥 10개만 가져오면 되는 거 아닌가 싶었다.&lt;/p&gt;
&lt;p data-end=&quot;4174&quot; data-start=&quot;4142&quot; data-ke-size=&quot;size16&quot;&gt;근데 10개만 가져오면 다음 데이터가 있는지 알 수 없다.&lt;/p&gt;
&lt;p data-end=&quot;4202&quot; data-start=&quot;4176&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자가 10개를 요청했다고 해보자.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;요청 size = 10&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4249&quot; data-start=&quot;4230&quot; data-ke-size=&quot;size16&quot;&gt;DB에서 정확히 10개가 조회됐다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;조회 결과 = 10개&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4293&quot; data-start=&quot;4276&quot; data-ke-size=&quot;size16&quot;&gt;이때 서버 입장에서는 애매하다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;정말 전체 데이터가 딱 10개라서 끝인 건지
아니면 뒤에 데이터가 더 있는데 이번에 10개만 가져온 건지
알 수 없다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4414&quot; data-start=&quot;4375&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Slice는 보통 요청한 size보다 1개를 더 조회해본다.&amp;nbsp;Spring Data 공식문서의 표에도 Slice&amp;lt;T&amp;gt;는 Pageable.getPageSize() + 1만큼 가져온다고 되어 있다.&lt;/p&gt;
&lt;p data-end=&quot;4566&quot; data-start=&quot;4533&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 size가 10이면 실제로는 11개를 조회해본다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;요청 size = 10
실제 확인 = 11개&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4619&quot; data-start=&quot;4606&quot; data-ke-size=&quot;size16&quot;&gt;만약 11개가 조회되면?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;사용자에게 줄 10개 외에 뒤에 최소 1개가 더 있다는 뜻
=&amp;gt; hasNext = true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4714&quot; data-start=&quot;4685&quot; data-ke-size=&quot;size16&quot;&gt;응답으로는 11개를 다 주지 않고 10개만 내려준다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;content&quot;: [&quot;10개&quot;],
  &quot;hasNext&quot;: true
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4790&quot; data-start=&quot;4773&quot; data-ke-size=&quot;size16&quot;&gt;반대로 10개 이하만 조회되면?&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;뒤에 더 이상 데이터가 없다는 뜻
=&amp;gt; hasNext = false&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4852&quot; data-start=&quot;4843&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 이렇다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;Page는 전체 개수를 세어서 전체 페이지 수를 계산한다.
Slice는 전체 개수를 세지 않고, 1개를 더 조회해서 다음 데이터 존재 여부만 판단한다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4968&quot; data-start=&quot;4952&quot; data-ke-size=&quot;size16&quot;&gt;이건 N+1 문제와는 다르다.&lt;/p&gt;
&lt;p data-end=&quot;5010&quot; data-start=&quot;4970&quot; data-ke-size=&quot;size16&quot;&gt;N+1은 연관 엔티티를 가져오느라 쿼리가 여러 번 추가로 나가는 문제다.&lt;/p&gt;
&lt;p data-end=&quot;5103&quot; data-start=&quot;5012&quot; data-ke-size=&quot;size16&quot;&gt;반면 Slice의 size + 1은 쿼리를 여러 번 날리는 게 아니라, 한 번 조회할 때 row를 1개 더 가져와서 다음 데이터 존재 여부를 확인하는 방식이다.&lt;/p&gt;
&lt;h2 data-end=&quot;5127&quot; data-start=&quot;5110&quot; data-ke-size=&quot;size26&quot;&gt;Page와 Slice 비교&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;5357&quot; data-start=&quot;5129&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 구분 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;Page &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; Slice &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;5204&quot; data-start=&quot;5165&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;5170&quot; data-start=&quot;5165&quot;&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;5185&quot; data-start=&quot;5170&quot;&gt;전체 페이지 기반 조회&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5204&quot; data-start=&quot;5185&quot; data-col-size=&quot;sm&quot;&gt;더보기/다음 존재 여부 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;5224&quot; data-start=&quot;5205&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;5213&quot; data-start=&quot;5205&quot;&gt;&lt;b&gt;전체 개수&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5218&quot; data-start=&quot;5213&quot; data-col-size=&quot;sm&quot;&gt;있음&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5224&quot; data-start=&quot;5218&quot; data-col-size=&quot;sm&quot;&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;5247&quot; data-start=&quot;5225&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;5236&quot; data-start=&quot;5225&quot;&gt;&lt;b&gt;전체 페이지 수&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5241&quot; data-start=&quot;5236&quot; data-col-size=&quot;sm&quot;&gt;있음&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5247&quot; data-start=&quot;5241&quot; data-col-size=&quot;sm&quot;&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;5271&quot; data-start=&quot;5248&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;5260&quot; data-start=&quot;5248&quot;&gt;&lt;b&gt;다음 데이터 여부&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5265&quot; data-start=&quot;5260&quot; data-col-size=&quot;sm&quot;&gt;있음&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5271&quot; data-start=&quot;5265&quot; data-col-size=&quot;sm&quot;&gt;있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;5304&quot; data-start=&quot;5272&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;5286&quot; data-start=&quot;5272&quot;&gt;&lt;b&gt;count query&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5294&quot; data-start=&quot;5286&quot; data-col-size=&quot;sm&quot;&gt;보통 필요&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5304&quot; data-start=&quot;5294&quot; data-col-size=&quot;sm&quot;&gt;보통 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;5357&quot; data-start=&quot;5305&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;5314&quot; data-start=&quot;5305&quot;&gt;&lt;b&gt;적합한 화면&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;5338&quot; data-start=&quot;5314&quot;&gt;게시글 목록, 관리자 목록, 검색 결과&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;5357&quot; data-start=&quot;5338&quot; data-col-size=&quot;sm&quot;&gt;댓글, 알림, 피드, 더보기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;5401&quot; data-start=&quot;5364&quot; data-ke-size=&quot;size26&quot;&gt;Offset과 Keyset은 반환 타입이 아니라 조회 방식이다&lt;/h2&gt;
&lt;p data-end=&quot;5422&quot; data-start=&quot;5403&quot; data-ke-size=&quot;size16&quot;&gt;여기서 다시 한 번 정리해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;5422&quot; data-start=&quot;5403&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5449&quot; data-start=&quot;5424&quot; data-ke-size=&quot;size16&quot;&gt;Page, Slice는 반환 타입이다.&lt;/p&gt;
&lt;p data-end=&quot;5494&quot; data-start=&quot;5451&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5494&quot; data-start=&quot;5451&quot; data-ke-size=&quot;size16&quot;&gt;반면 Offset, Keyset은 DB에서 데이터를 가져오는 방식이다.&lt;/p&gt;
&lt;h3 data-end=&quot;5506&quot; data-start=&quot;5496&quot; data-ke-size=&quot;size23&quot;&gt;Offset&lt;/h3&gt;
&lt;p data-end=&quot;5540&quot; data-start=&quot;5508&quot; data-ke-size=&quot;size16&quot;&gt;Offset은 앞에서 몇 개를 건너뛰고 가져오는 방식이다.&lt;/p&gt;
&lt;p data-end=&quot;5598&quot; data-start=&quot;5542&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 page = 2, size = 10이면 앞에서 20개를 건너뛰고 10개를 가져온다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM post
ORDER BY id DESC
LIMIT 10 OFFSET 20;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5687&quot; data-start=&quot;5668&quot; data-ke-size=&quot;size16&quot;&gt;즉 Offset의 관심사는 이거다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;몇 개를 건너뛰고
몇 개를 가져올 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5781&quot; data-start=&quot;5726&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data에서 Pageable을 사용하면 보통 이 Offset 기반 조회와 연결된다.&lt;/p&gt;
&lt;p data-end=&quot;5904&quot; data-start=&quot;5783&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5904&quot; data-start=&quot;5783&quot; data-ke-size=&quot;size16&quot;&gt;공식문서의 표에서도 Slice&amp;lt;T&amp;gt;와 Page&amp;lt;T&amp;gt; 모두 Pageable.getOffset()에서 시작해 limit을 적용한다고 설명한다.&lt;/p&gt;
&lt;p data-end=&quot;5939&quot; data-start=&quot;5906&quot; data-ke-size=&quot;size16&quot;&gt;즉 아래 코드는 보통 Offset + Page에 가깝다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Page&amp;lt;Post&amp;gt; findByBoardId(Long boardId, Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6049&quot; data-start=&quot;6013&quot; data-ke-size=&quot;size16&quot;&gt;그리고 아래 코드는 보통 Offset + Slice에 가깝다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Slice&amp;lt;Comment&amp;gt; findByPostId(Long postId, Pageable pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6184&quot; data-start=&quot;6125&quot; data-ke-size=&quot;size16&quot;&gt;같은 Offset 기반 조회라도 반환 타입을 Page로 할 수도 있고, Slice로 할 수도 있다.&lt;/p&gt;
&lt;h3 data-end=&quot;6201&quot; data-start=&quot;6191&quot; data-ke-size=&quot;size23&quot;&gt;Keyset&lt;/h3&gt;
&lt;p data-end=&quot;6233&quot; data-start=&quot;6203&quot; data-ke-size=&quot;size16&quot;&gt;Keyset은 앞에서 몇 개를 건너뛰는 방식이 아니다. 마지막으로 본 데이터의 기준값을 이용해서 다음 데이터를 가져오는 방식이다.&lt;/p&gt;
&lt;p data-end=&quot;6323&quot; data-start=&quot;6278&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 마지막으로 본 게시글 ID가 100이라면 다음 요청은 이런 식이 된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM post
WHERE id &amp;lt; 100
ORDER BY id DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6415&quot; data-start=&quot;6398&quot; data-ke-size=&quot;size16&quot;&gt;Offset은 이렇게 생각한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;앞에서 100개 건너뛰고 10개 가져와.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6470&quot; data-start=&quot;6453&quot; data-ke-size=&quot;size16&quot;&gt;Keyset은 이렇게 생각한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;내가 마지막으로 본 id가 100이니까,
id 100보다 작은 데이터 중에서 10개 가져와.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6558&quot; data-start=&quot;6537&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Keyset의 관심사는 이거다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;마지막으로 본 기준값이 무엇인가?
그 기준값 이후/이전 데이터를 어떻게 가져올 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;6726&quot; data-start=&quot;6622&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data JPA 공식문서의 Scrolling 부분에서도 scrolling은 stable sort, scroll type, result limiting으로 구성된다고 설명한다.&lt;/p&gt;
&lt;blockquote data-end=&quot;6839&quot; data-start=&quot;6728&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;p data-end=&quot;6839&quot; data-start=&quot;6730&quot; data-ke-size=&quot;size16&quot;&gt;Scrolling consists of a stable sort, a scroll type (Offset- or Keyset-based scrolling) and result limiting.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;6979&quot; data-start=&quot;6841&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, 스크롤링 방식으로 큰 결과를 나눠서 볼 때는 안정적인 정렬 기준, Offset 또는 Keyset 같은 스크롤 방식, 한 번에 가져올 개수 제한이 필요하다는 뜻이다.&lt;/p&gt;
&lt;p data-end=&quot;6979&quot; data-start=&quot;6841&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7119&quot; data-start=&quot;6981&quot; data-ke-size=&quot;size16&quot;&gt;또한 공식문서는 Offset-based scrolling과 Keyset-based scrolling을 구분하고, Keyset 기반 조회는 적절한 인덱스 구조가 필요하다고 설명한다.&lt;/p&gt;
&lt;h2 data-end=&quot;7150&quot; data-start=&quot;7126&quot; data-ke-size=&quot;size26&quot;&gt;그래서 하나만 정하면 되는 게 아니었다&lt;/h2&gt;
&lt;p data-end=&quot;7194&quot; data-start=&quot;7152&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 Page를 쓸까? Slice를 쓸까?만 정하면 되는 줄 알았다.&amp;nbsp;근데 정확히는 두 가지를 같이 정해야 했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. 조회 방식
   - Offset
   - Keyset

2. 반환 타입
   - Page
   - Slice
   - CursorResponse
   - Window&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;7344&quot; data-start=&quot;7331&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 식이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Offset + Page
=&amp;gt; page/size로 건너뛰어 조회하고, 전체 개수와 전체 페이지 수까지 응답한다.

Offset + Slice
=&amp;gt; page/size로 건너뛰어 조회하되, 전체 개수는 세지 않고 다음 데이터 존재 여부만 응답한다.

Keyset + Slice 스타일 응답
=&amp;gt; 마지막으로 본 기준값 이후/이전 데이터를 조회하고, 다음 데이터 존재 여부와 다음 cursor를 응답한다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;7628&quot; data-start=&quot;7582&quot; data-ke-size=&quot;size16&quot;&gt;즉 Page = Offset이 아니고, Slice = Keyset도 아니다. Offset / Keyset은 데이터를 가져오는 방식이고, Page / Slice는 가져온 결과를 어떤 정보와 함께 내려줄지에 대한 응답 모델이다.&lt;/p&gt;
&lt;h2 data-end=&quot;7751&quot; data-start=&quot;7725&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;</description>
      <category>Backend/  JPA &amp;middot; DB</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/309</guid>
      <comments>https://winwin0219.tistory.com/309#entry309comment</comments>
      <pubDate>Tue, 5 May 2026 15:32:04 +0900</pubDate>
    </item>
    <item>
      <title>[Devlog] CoreBoard에서 기능보다 먼저 설계 기준을 세운 이유</title>
      <link>https://winwin0219.tistory.com/308</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/300&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://winwin0219.tistory.com/300&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1777877559565&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[회고] CoreBoard 프로젝트, 왜 만들었는가&quot; data-og-description=&quot;CoreBoard 기획했을 때로 거슬러 올라가자 때는 2025년 9월 말. Momentix에서 느꼈던 부담감이 있었다. 부트캠프의 문제점이자 나의 약점(비전공자)이었던 동작은 하지만 얕은 이해도였다. 요즘 현업에&quot; data-og-host=&quot;winwin0219.tistory.com&quot; data-og-source-url=&quot;https://winwin0219.tistory.com/300&quot; data-og-url=&quot;https://winwin0219.tistory.com/300&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6MaDF/dJMb8SXBxoD/f17AKSaCykyQKdjLNlrt6k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/d52vh9/dJMb8U8XkkB/yPNOGszcDXgqnCRt6KGtKK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/300&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://winwin0219.tistory.com/300&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6MaDF/dJMb8SXBxoD/f17AKSaCykyQKdjLNlrt6k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/d52vh9/dJMb8U8XkkB/yPNOGszcDXgqnCRt6KGtKK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[회고] CoreBoard 프로젝트, 왜 만들었는가&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;CoreBoard 기획했을 때로 거슬러 올라가자 때는 2025년 9월 말. Momentix에서 느꼈던 부담감이 있었다. 부트캠프의 문제점이자 나의 약점(비전공자)이었던 동작은 하지만 얕은 이해도였다. 요즘 현업에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;winwin0219.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#exception&quot;&gt;API 예외를 직접 정의한 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#restful&quot;&gt;RESTful하게 API를 설계하려고 한 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#jwt&quot;&gt;JWT를 선택한 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#interceptor&quot;&gt;토큰 검증을 Controller가 아니라 Interceptor에서 처리한 이유&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#testcontainers&quot;&gt;Testcontainers를 사용한 이유&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;exception&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[Devlog] API 예외를 직접 정의한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard를 만들면서 생각보다 빨리 마주친 문제가 있다. 바로 예외처리다. 처음에는 단순하게 생각했다.&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;잘못된 요청이면 400&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;권한 없으면 403&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;없으면 404&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;중복이면&amp;nbsp;409&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3881&quot; data-start=&quot;3862&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 정도만 정하면 되는 줄 알았다.&amp;nbsp;근데 실제로 코드를 짜다 보니 문제가 생겼다.&amp;nbsp;컨트롤러마다, 서비스마다 예외 메시지를 직접 쓰기 시작하면 금방 지저분해진다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777877730947&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;throw new RuntimeException(&quot;존재하지 않는 게시판입니다.&quot;);
throw new RuntimeException(&quot;권한이 없습니다.&quot;);
throw new RuntimeException(&quot;이미 존재하는 이름입니다.&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4145&quot; data-start=&quot;4113&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;근데 기능이 늘어나면 어느 순간부터 이게 관리가 안 된다.&amp;nbsp;메시지를 바꾸고 싶으면 서비스 코드를 뒤져야 하고, 같은 404인데 응답 모양이 다를 수도 있고, 프론트 입장에서는 어떤 에러인지 안정적으로 분기하기 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4280&quot; data-start=&quot;4237&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그래서 CoreBoard에서는 Custom Exception을 직접 정의했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4280&quot; data-start=&quot;4237&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;4280&quot; data-start=&quot;4237&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 공식문서에서 본 기준 &lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-exceptionhandler.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; Spring 공식문서&lt;/a&gt;에서는 @ControllerAdvice와 @ExceptionHandler를 이용해서 컨트롤러에서 발생한 예외를 처리할 수 있다고 설명한다.&lt;/span&gt;&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@Controller and @ControllerAdvice classes can have @ExceptionHandler methods to handle exceptions from controller methods라고 되어 있다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;한국어로 풀면, 컨트롤러마다 try-catch를 두는 게 아니라, 공통 예외 처리 클래스를 두고 특정 예외가 발생했을 때 한 곳에서 처리할 수 있다는 뜻이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CoreBoard의 예외 구조 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard에서는 예외를 그냥 문자열로 던지지 않았다.&amp;nbsp;대신 공통 부모 예외를 두고, 각 도메인별 예외가 그 구조를 따르게 했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777877804079&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ErrorException
 ├── AuthErrorException
 ├── BoardErrorException
 ├── PostErrorException
 └── ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 그리고 에러 정보는 enum으로 관리했다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777877812919&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AuthErrorCode.UNAUTHORIZED
BoardErrorCode.BOARD_NOT_FOUND
PostErrorCode.POST_NOT_FOUND&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 왜 RuntimeException 하나로 끝내지 않았는가? &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;5245&quot; data-start=&quot;5214&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RuntimeException 하나로도 기능은 돌아간다.&amp;nbsp;근데 CoreBoard의 목표는 돌아가는 코드가 아니라 왜 이렇게 나눴는지 설명 가능한 코드였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; RuntimeException만 쓰면 이런 문제가 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 어떤 도메인에서 발생한 예외인지 흐려진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. HTTP 상태코드와 메시지 관리가 흩어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. 프론트가 안정적으로 에러를 분기하기 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4.&amp;nbsp;테스트에서&amp;nbsp;어떤&amp;nbsp;예외가&amp;nbsp;터져야&amp;nbsp;하는지&amp;nbsp;명확히&amp;nbsp;검증하기&amp;nbsp;어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 반대로 Custom Exception을 두면 이런 장점이 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1.&amp;nbsp;예외의&amp;nbsp;의미가&amp;nbsp;코드에&amp;nbsp;드러난다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2.&amp;nbsp;도메인별&amp;nbsp;에러&amp;nbsp;정책을&amp;nbsp;분리할&amp;nbsp;수&amp;nbsp;있다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3.&amp;nbsp;GlobalExceptionHandler에서&amp;nbsp;공통&amp;nbsp;응답으로&amp;nbsp;변환하기&amp;nbsp;쉽다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4.&amp;nbsp;테스트에서&amp;nbsp;기대&amp;nbsp;예외를&amp;nbsp;명확히&amp;nbsp;검증할&amp;nbsp;수&amp;nbsp;있다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CoreBoard에서 중요하게 본 것 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 서비스 코드를 읽었을 때,&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777877882795&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;throw new BoardErrorException(BoardErrorCode.BOARD_NOT_FOUND);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;5845&quot; data-start=&quot;5796&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 한 줄만 보고도 게시판이 없어서 터진 예외구나를 바로 알 수 있게 만들고 싶었다. 그리고 응답은 항상 같은 형태로 내려가게 만들고 싶었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777877894645&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;success&quot;: false,
  &quot;message&quot;: &quot;존재하지 않는 게시판입니다.&quot;,
  &quot;data&quot;: {
    &quot;code&quot;: &quot;404&quot;,
    &quot;errors&quot;: [
      {
        &quot;field&quot;: &quot;BOARD_NOT_FOUND&quot;,
        &quot;reason&quot;: &quot;존재하지 않는 게시판입니다.&quot;
      }
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;restful&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[Devlog] RESTful하게 API를 설계하려고 한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 처음 API를 만들 때는 URL을 거의 감으로 지었다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777877985969&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;api/auth 이런식&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6382&quot; data-start=&quot;6361&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;근데 API가 늘어나면 문제가 생긴다. URL에 동사가 계속 붙고, 같은 자원을 다루는데도 규칙이 흔들린다. 그래서 CoreBoard에서는 최대한 RESTful하게 API를 설계하려고 했다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;6382&quot; data-start=&quot;6361&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; RESTful을 지키려 한 이유 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; RESTful하게 설계한다는 건 거창하게 말하면 어렵지만, 내가 이해한 기준은 이거였다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;URL은 자원을 표현하고, 행위는&amp;nbsp;HTTP&amp;nbsp;Method로&amp;nbsp;표현한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777878029254&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /boards          게시판 목록 조회
GET /boards/{id}     게시판 단건 조회
POST /admin/boards   게시판 생성
PATCH /admin/boards/{id} 게시판 수정
DELETE /admin/boards/{id} 게시판 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 공식문서에서 본 기준 &lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; HTTP 공식 명세인 RFC 9110&lt;/a&gt;에서는 멱등성에 대해 설명한다. &lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-style=&quot;style2&quot;&gt;A request method is considered &quot;idempotent&quot; if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request. &lt;br /&gt;Of the request methods defined by this specification, PUT, DELETE, and safe request methods are idempotent.&lt;/blockquote&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 한국어로 풀면, 같은 요청을 여러 번 보내도 서버에 남는 결과가 한 번 보낸 것과 같으면 멱등하다는 뜻이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7179&quot; data-start=&quot;7162&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이게 API 설계에서 중요했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;7179&quot; data-start=&quot;7162&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;예를 들어 DELETE /boards/{id}는 여러 번 호출되어도 그 게시판이 삭제된 상태라는 결과는 같다.&amp;nbsp;반대로 POST /boards는 호출할 때마다 새 게시판이 생길 수 있으니 멱등하다고 보기 어렵다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;7179&quot; data-start=&quot;7162&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;7364&quot; data-start=&quot;7307&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, HTTP Method는 단순한 장식이 아니라 클라이언트와 서버가 요청의 의미를 약속하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CoreBoard에서 RESTful을 지키며 고민한 부분 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;7438&quot; data-start=&quot;7402&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard에서는 관리자와 일반 사용자의 행위가 섞여 있다.&amp;nbsp;예를 들어 게시판 생성은 일반 사용자가 하는 일이 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;7553&quot; data-start=&quot;7474&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그래서 단순히 POST /boards로 둘 수도 있었지만, CoreBoard에서는 관리자 기능이라는 의미를 드러내기 위해 아래처럼 잡았다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777878101632&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /admin/boards
PATCH /admin/boards/{boardId}
DELETE /admin/boards/{boardId}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 반면 일반 사용자가 게시글을 조회하거나 작성하는 흐름은 게시판 자원과 게시글 자원의 관계를 드러내려고 했다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777878118569&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /boards/{boardId}/posts
POST /boards/{boardId}/posts
GET /posts/{postId}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4825&quot; data-start=&quot;4792&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 요청은 어떤 자원에 대한 요청인가?&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;4825&quot; data-start=&quot;4792&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 요청은 조회인가, 생성인가, 수정인가, 삭제인가?&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;4825&quot; data-start=&quot;4792&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이&amp;nbsp;요청은&amp;nbsp;관리자&amp;nbsp;권한이&amp;nbsp;필요한가?&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; RESTful이 항상 정답은 아니었다 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;8355&quot; data-start=&quot;8290&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;RESTful을 절대 규칙으로 보지는 않았다 물론 RESTful하게 만들겠다고 모든 API를 억지로 끼워 맞추면 오히려 이상해질 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특히 인증 API는 애매하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;회원가입은 보통 /signup, 로그인은 /login처럼 행위 중심으로 많이 표현한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;8355&quot; data-start=&quot;8290&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실제 서비스에서도 /auth/login 같은 URL은 흔하게 볼 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;8355&quot; data-start=&quot;8290&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;8355&quot; data-start=&quot;8290&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 CoreBoard에서는 이 부분도 가능하면 자원 중심으로 해석해보려고 했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;8355&quot; data-start=&quot;8290&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;회원가입은 가입한다는 행위보다 &lt;b&gt;사용자 자원을 생성한다는 관점&lt;/b&gt;으로 보고 &lt;b&gt;POST /users&lt;/b&gt;로 설계했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;로그인도 로그인한다는 행위보다 &lt;b&gt;인증 정보를 검증한 뒤 토큰 자원을 발급받는다는 관점&lt;/b&gt;으로 보고 POST /token으로 설계했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;8355&quot; data-start=&quot;8290&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, CoreBoard에서는 /signup,&amp;nbsp; /login처럼 동사를 URL에 직접 넣기보다, 서버에서 생성되거나 발급되는 자원을 기준으로 API를 표현하려고 했다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;인증 API처럼 행위성이 강한 요청은 언제든지 실무 관례와 팀 컨벤션에 따라 다르게 설계될 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;8355&quot; data-start=&quot;8290&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다만 CoreBoard는 RESTful 설계를 연습하는 프로젝트였기 때문에, &lt;b&gt;가능한 범위 안에서 URL은 자원, 행위는 HTTP Method라는 기준&lt;/b&gt;을 끝까지 적용해보려고 했다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API를 사용하는 사람 입장에서 덜 헷갈리게 만들고, HTTP Method의 의미와 URL의 역할을 코드와 문서에 맞추기 위해서였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;jwt&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[Devlog] JWT를 선택한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; JWT는 JSON Web Token의 약자다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://www.rfc-editor.org/info/rfc7519&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 공식 RFC 7519&lt;/a&gt;에서는 JWT를 &lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-style=&quot;style2&quot;&gt;JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties.&lt;/blockquote&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 한국어로 풀면, 두 주체 사이에서 사용자 정보 같은 claim을 전달하기 위한 작고 URL-safe한 토큰 형식이라는 뜻이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; CoreBoard에서는 JWT 안에 이런 정보를 담았다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777878468467&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;userId
username
role
type(access / refresh)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 즉, 서버는 요청이 들어올 때 토큰을 읽고, 이 사용자가 누구인지, 어떤 권한인지, 이 토큰이 access token인지 refresh token인지 판단할 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 왜 세션이 아니라 JWT였는가? &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;634&quot; data-start=&quot;610&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;세션 방식은 서버가 로그인 상태를 저장한다. 사용자가 로그인하면 서버는 세션 저장소에 로그인 정보를 저장하고, 클라이언트는 세션 ID를 쿠키로 들고 다닌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;634&quot; data-start=&quot;610&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;755&quot; data-start=&quot;700&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;반면 JWT 방식은 토큰 자체에 필요한 정보를 담고, 서버는 요청이 들어올 때마다 토큰을 검증한다. CoreBoard에서 JWT를 선택한 이유는 단순히 Spring Security를 사용하지 않았기 때문이 아니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;755&quot; data-start=&quot;700&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;873&quot; data-start=&quot;822&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Security를 사용하더라도 JWT 기반 인증 구조는 충분히 선택할 수 있다. 내가 JWT를 선택한 이유는 CoreBoard의 인증 방식을 &lt;b&gt;서버 세션 기반&lt;/b&gt;이 아니라 &lt;b&gt;토큰 기반&lt;/b&gt;으로 설계하고 싶었기 때문이다.&amp;nbsp;게시판 API는 로그인 이후 여러 요청에서 사용자를 식별하고 권한을 확인해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;873&quot; data-start=&quot;822&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1110&quot; data-start=&quot;1003&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이때 JWT를 사용하면 access token 안에 사용자 식별자와 권한 정보를 담고, 서버는 요청마다 토큰을 검증해서 누가 요청했는지,&amp;nbsp; 어떤 권한을 가진 사용자인지를 판단할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1110&quot; data-start=&quot;1003&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1110&quot; data-start=&quot;1003&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CoreBoard에서 JWT를 선택한 이유 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1177&quot; data-start=&quot;1145&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard에서 JWT를 선택한 이유는 세 가지였다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 서버가 세션 상태를 직접 들고 있지 않는 토큰 기반 인증 구조를 적용해보고 싶었다.
2. access token / refresh token 구조를 분리해서 인증 흐름을 설계하고 싶었다.
3. role claim을 통해 인증 이후 인가 흐름까지 연결하고 싶었다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1380&quot; data-start=&quot;1342&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;여기서 중요한 건 JWT 자체가 무조건 세션보다 좋다는 뜻이 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1380&quot; data-start=&quot;1342&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1448&quot; data-start=&quot;1382&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;세션은 서버에서 로그인 상태를 통제하기 쉽고, JWT는 요청마다 토큰을 검증하면서 서버의 세션 의존도를 줄일 수 있다. 대신 JWT는 한 번 발급된 토큰을 어떻게 만료시키고, 어디에 저장하고, 탈취 위험을 어떻게 줄일지 같이 고민해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1448&quot; data-start=&quot;1382&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1626&quot; data-start=&quot;1519&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, CoreBoard에서 JWT를 선택한 건 요즘 많이 쓰니까가 아니라, &lt;b&gt;인증 상태를 서버 세션에 저장하는 방식과 토큰으로 증명하는 방식의 차이를 직접 설계해보기 위한 선택&lt;/b&gt;이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1626&quot; data-start=&quot;1519&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1626&quot; data-start=&quot;1519&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 인증 방식 후보군 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;283&quot; data-start=&quot;263&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. Session 기반 인증&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;322&quot; data-start=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Session 기반 인증은 서버가 로그인 상태를 저장하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;322&quot; data-start=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;386&quot; data-start=&quot;324&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자가 로그인하면 서버는 세션 저장소에 사용자 정보를 저장하고, 클라이언트는 세션 ID를 쿠키로 들고 다닌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;432&quot; data-start=&quot;388&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;432&quot; data-start=&quot;388&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이후 요청이 들어오면 서버는 세션 ID를 기준으로 저장된 로그인 정보를 찾는다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client &amp;rarr; Session ID 전달
Server &amp;rarr; Session Store에서 사용자 정보 조회&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;556&quot; data-start=&quot;517&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 방식의 장점은 서버가 로그인 상태를 직접 관리할 수 있다는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;612&quot; data-start=&quot;558&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;예를 들어 특정 사용자를 강제로 로그아웃시키거나, 세션을 즉시 만료시키는 처리가 비교적 명확하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;686&quot; data-start=&quot;614&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다만 서버가 로그인 상태를 저장해야 하므로, 서버 인스턴스가 여러 대로 늘어날 경우 세션 저장소를 어떻게 공유할지 고민해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;686&quot; data-start=&quot;614&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;704&quot; data-start=&quot;688&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. JWT 기반 인증&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;771&quot; data-start=&quot;706&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;JWT 기반 인증은 서버가 로그인 상태를 세션 저장소에 저장하지 않고, 클라이언트가 토큰을 가지고 요청하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;771&quot; data-start=&quot;706&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;819&quot; data-start=&quot;773&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자가 로그인하면 서버는 사용자 식별자와 권한 같은 정보를 담아 토큰을 발급한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;851&quot; data-start=&quot;821&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;851&quot; data-start=&quot;821&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이후 클라이언트는 API 요청마다 토큰을 함께 보낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;913&quot; data-start=&quot;853&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버는 요청이 들어올 때마다 토큰의 서명과 만료 시간을 검증하고, 토큰 안의 정보를 읽어 사용자를 식별한다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client &amp;rarr; JWT 전달
Server &amp;rarr; JWT 검증 후 사용자 정보 확인&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1030&quot; data-start=&quot;984&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 방식의 장점은 서버가 매 요청마다 세션 저장소를 조회하지 않아도 된다는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1106&quot; data-start=&quot;1032&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또 access token 안에 userId, role 같은 정보를 담으면 요청마다 사용자를 식별하고 권한을 판단할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1186&quot; data-start=&quot;1108&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다만 토큰이 탈취되면 만료 전까지 악용될 수 있으므로, 만료 시간과 refresh token 저장 방식, 재발급 정책을 함께 설계해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 11.3953%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 비교 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.7208%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; &amp;nbsp;Session 기반 인증 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.7675%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; JWT 기반 인증 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 11.3953%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;개념&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.7208%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버가 로그인 상태를 저장하고, &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;클라이언트는 세션 ID를 쿠키로 전달하는 방식&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.7675%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버가 토큰을 발급하고, &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;클라이언트가 요청마다 JWT를 전달하는 방식&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 11.3953%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;상태 저장 &lt;br /&gt;위치&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.7208%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버가 세션 저장소에 로그인 상태를 저장한다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.7675%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;클라이언트가 토큰을 보관하고, &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버는 요청마다 토큰을 검증한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 11.3953%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;요청 처리&lt;br /&gt;방식&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.7208%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;요청이 들어오면 서버가 세션 ID로 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;세션 저장소에서 사용자 정보를 조회한다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.7675%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;요청이 들어오면 서버가 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;JWT의 서명, 만료 시간, claim을 검증한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 11.3953%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.7208%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버가 로그인 상태를 직접 관리할 수 있다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특정 사용자의 세션 만료나 강제 로그아웃 처리가 비교적 명확하다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.7675%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버가 매 요청마다 세션 저장소를 조회하지 않아도 된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 토큰 안에 userId, role을 담아 인증과 인가 흐름을 연결하기 쉽다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 11.3953%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.7208%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버가 세션 상태를 저장해야 한다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버가 여러 대로 늘어나면 세션 저장소 공유 문제가 생긴다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.7675%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;한 번 발급된 토큰은 만료 전까지 유효하다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;토큰 무효화가 세션보다 까다롭고, 토큰 크기가 커질 수 있다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 11.3953%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;주의점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.7208%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;세션 ID가 탈취되면 해당 세션으로 요청할 수 있다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;세션 저장소 장애가 인증 장애로 이어질 수 있다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.7675%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;토큰이 탈취되면 만료 전까지 악용될 수 있다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;민감한 정보를 토큰에 넣으면 노출 위험이 있다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 11.3953%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;해결방안&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.7208%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;쿠키에 httpOnly, secure, sameSite를 적용한다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버 확장 시 Redis 같은 외부 세션 저장소를 사용해 세션을 공유한다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.7675%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;access token은 짧게 가져가고 refresh token으로 재발급한다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;refresh token은 쿠키에 저장하면서 httpOnly, secure, sameSite를 고려한다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;필요하면 refresh token을 DB/Redis에 저장해 로그아웃과 재발급 제어를 보완한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 11.3953%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;CoreBoard 선택 관점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 38.7208%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버가 로그인 상태를 직접 들고 있는 구조라서, 이번 CoreBoard의 토큰 기반 인증 설계 방향과는 다소 거리가 있었다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 49.7675%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;요청마다 토큰을 검증해 사용자 식별과 권한 확인을 연결할 수 있고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; access token / refresh token 구조를 설계하기에 적합하다고 판단했다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;interceptor&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[Devlog] 왜 토큰 검증을 Controller가 아니라 Interceptor에서 처리했는가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1100&quot; data-start=&quot;1061&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard에서는 Spring Security를 사용하지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1100&quot; data-start=&quot;1061&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1100&quot; data-start=&quot;1061&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 Spring Security가 제공하는 Filter Chain 기반 인증/인가 흐름을 그대로 사용할 수 없었다. 하지만 JWT를 사용하는 이상, 요청마다 토큰을 검증하는 과정은 필요했다.&amp;nbsp;처음에는 Controller에서 Authorization 헤더를 읽고 토큰을 검증하는 방식도 생각할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(&quot;/posts&quot;)
public ResponseEntity&amp;lt;?&amp;gt; getPosts(@RequestHeader(&quot;Authorization&quot;) String token) {
    jwtUtil.validate(token);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1454&quot; data-start=&quot;1433&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 이 방식은 금방 문제가 생긴다.&amp;nbsp;인증이 필요한 API마다 아래 로직이 반복된다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Authorization 헤더 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Bearer 토큰 파싱&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;토큰 만료 여부 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;토큰 서명 검증&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자 식별 정보 추출&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;권한&amp;nbsp;확인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1638&quot; data-start=&quot;1570&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 로직이 Controller마다 들어가면 Controller는 더 이상 요청과 응답을 처리하는 역할에만 집중하지 못한다. 그래서 CoreBoard에서는 JWT 검증을 Controller 앞단으로 분리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1638&quot; data-start=&quot;1570&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1946&quot; data-start=&quot;1688&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring MVC에서 Interceptor는 Controller가 실행되기 전에 요청을 가로챌 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1946&quot; data-start=&quot;1688&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;1946&quot; data-start=&quot;1688&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; callback before the actual handler is run that returns a boolean. &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-end=&quot;1946&quot; data-start=&quot;1688&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1946&quot; data-start=&quot;1688&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/handlermapping-interceptor.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에서도 preHandle()은 실제 handler가 실행되기 전에 호출되고, false를 반환하면 이후 실행 체인을 중단해 handler가 호출되지 않는다고 설명한다. 쉽게 말하면, &lt;b&gt;Controller에 들어가기 전에 요청을 검사하고 막을 수 있는 위치&lt;/b&gt;라는 뜻이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1946&quot; data-start=&quot;1688&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1996&quot; data-start=&quot;1948&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard에서는 이 특성을 이용해 인증 검증을 Interceptor에 배치했다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client
 &amp;rarr; Interceptor에서 Authorization 헤더 확인
 &amp;rarr; JWT 검증
 &amp;rarr; 인증 실패 시 401/403 응답
 &amp;rarr; 인증 성공 시 Controller 진입
 &amp;rarr; Service 호출&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2164&quot; data-start=&quot;2126&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 구조로 두면 인증 실패 요청은 비즈니스 로직까지 들어가지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2262&quot; data-start=&quot;2166&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Controller는 요청 DTO를 받고 Service를 호출하는 역할에 집중할 수 있고, Service는 게시글 생성, 수정, 삭제 같은 비즈니스 규칙에 집중할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2332&quot; data-start=&quot;2264&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, Interceptor를 선택한 이유는,&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2432&quot; data-start=&quot;2334&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard에서는 인증 검증이 여러 API에 반복되는 공통 관심사였고, Controller 진입 전에 처리하는 것이 레이어 책임을 더 명확하게 나눈다고 판단했기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-end=&quot;2491&quot; data-start=&quot;2439&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Spring Security Filter와 CoreBoard Interceptor의 차이&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;&lt;br /&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2896&quot; data-start=&quot;2493&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;비교&amp;nbsp;&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Spring Security Filter&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ChainCoreBoard Interceptor&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2646&quot; data-start=&quot;2572&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2577&quot; data-start=&quot;2572&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;위치&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2597&quot; data-start=&quot;2577&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Servlet Filter 계층&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2646&quot; data-start=&quot;2597&quot; data-col-size=&quot;md&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring MVC HandlerMapping 이후, Controller 실행 전&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2732&quot; data-start=&quot;2647&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2652&quot; data-start=&quot;2647&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2693&quot; data-start=&quot;2652&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;인증/인가, 세션, CSRF, 보안 컨텍스트 등 전체 보안 흐름 처리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2732&quot; data-start=&quot;2693&quot; data-col-size=&quot;md&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard에서 JWT 헤더 확인과 토큰 검증을 직접 처리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2817&quot; data-start=&quot;2733&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2741&quot; data-start=&quot;2733&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;적용 &lt;br /&gt;이유&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2774&quot; data-start=&quot;2741&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Security가 제공하는 표준 보안 구조&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2817&quot; data-start=&quot;2774&quot; data-col-size=&quot;md&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Security 없이 인증 흐름을 직접 구현하기 위한 선택&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2896&quot; data-start=&quot;2818&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2824&quot; data-start=&quot;2818&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;주의점&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2851&quot; data-start=&quot;2824&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;설정과 개념이 복잡하지만 보안 기능이 풍부함&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2896&quot; data-start=&quot;2851&quot; data-col-size=&quot;md&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;직접 구현한 만큼 예외 처리, 우회 경로, 권한 체크를 스스로 관리해야 함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2979&quot; data-start=&quot;2910&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard에서 Interceptor를 사용한 이유는 &lt;b&gt;JWT를 Controller마다 검증하지 않기 위해서&lt;/b&gt;였다.&amp;nbsp;Spring Security가 있었다면 보통 Filter Chain 기반으로 인증 흐름을 구성했을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2979&quot; data-start=&quot;2910&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3158&quot; data-start=&quot;3042&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 CoreBoard는 Spring Security 없이 인증 구조를 직접 구현했기 때문에, Spring MVC에서 Controller 실행 전에 공통 처리를 할 수 있는 Interceptor를 선택했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;3310&quot; data-start=&quot;3160&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;결국 이 선택의 핵심은 Security가 없어서 어쩔 수 없이 Interceptor를 썼다가 아니라, &lt;b&gt;인증 검증이라는 공통 책임을 Controller 밖으로 분리하고, 비즈니스 로직 진입 전에 요청을 걸러내기 위해 Interceptor를 사용했다&lt;/b&gt;는 점이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;4825&quot; data-start=&quot;4792&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;testcontainers&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;[Devlog] Testcontainers를 사용한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;11998&quot; data-start=&quot;11970&quot; data-ke-size=&quot;size16&quot;&gt;테스트를 작성하면서 제일 찝찝했던 부분은 DB였다.&amp;nbsp;처음에는 H2를 쓰면 되는 줄 알았다.&amp;nbsp;빠르고, 가볍고, 설정도 쉽다.&lt;/p&gt;
&lt;p data-end=&quot;12081&quot; data-start=&quot;12042&quot; data-ke-size=&quot;size16&quot;&gt;근데 문제는 CoreBoard의 실제 DB가 MySQL이라는 점이었다.&amp;nbsp;테스트에서는 H2로 통과했는데 실제 MySQL에서는 다르게 동작할 수 있다.&lt;/p&gt;
&lt;h4 data-end=&quot;12164&quot; data-start=&quot;12142&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 H2만으로 부족하다고 느꼈는가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;12179&quot; data-start=&quot;12166&quot; data-ke-size=&quot;size16&quot;&gt;H2는 테스트하기 좋다. 하지만 &lt;b&gt;실제 운영 DB와 완전히 같지는 않다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;12179&quot; data-start=&quot;12166&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;12264&quot; data-start=&quot;12208&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 SQL 문법, 인덱스 동작, 제약조건, 트랜잭션 처리, 타입 처리에서 차이가 날 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;12310&quot; data-start=&quot;12266&quot; data-ke-size=&quot;size16&quot;&gt;CoreBoard는 메서드 하나가 호출되는지만 보는 프로젝트가 아니었다.&amp;nbsp;DB 제약조건, JPA 동작, 트랜잭션 흐름까지 실제와 가깝게 확인하고 싶었다.&lt;/p&gt;
&lt;p data-end=&quot;12310&quot; data-start=&quot;12266&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;12383&quot; data-start=&quot;12358&quot; data-ke-size=&quot;size16&quot;&gt;그래서 Testcontainers를 사용했다.&lt;/p&gt;
&lt;h4 data-end=&quot;12399&quot; data-start=&quot;12385&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;공식문서에서 본 기준&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Testcontainers for Java is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.&lt;/blockquote&gt;
&lt;p data-end=&quot;12669&quot; data-start=&quot;12401&quot; data-ke-size=&quot;size16&quot;&gt;한국어로 풀면, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;b&gt;H2는 서버 모드나 임베디드 모드로 사용할 수 있고, 디스크 기반 DB 또는 인메모리 DB로 사용할 수 있다&lt;/b&gt;는 뜻이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;12669&quot; data-start=&quot;12401&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;12669&quot; data-start=&quot;12401&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://java.testcontainers.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Testcontainers 공식문서&lt;/a&gt;는 Testcontainers를 JUnit tests를 지원하고, Docker container에서 실행 가능한 데이터베이스나 메시지 브로커 같은 의존성을 가볍고 일회용으로 제공하는 Java 라이브러리라고 설명한다.&lt;br /&gt;테스트할 때 진짜 &lt;b&gt;MySQL 같은 외부 의존성을 Docker 컨테이너로 띄웠다가 테스트가 끝나면 버릴 수 있게 해주는 도구&lt;/b&gt;라는 뜻이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;가짜 DB가 아니라 실제 MySQL에 가까운 환경에서 테스트하고 싶다.
하지만 매번 수동으로 DB를 만들고 지우고 싶지는 않다.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;12818&quot; data-start=&quot;12783&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;CoreBoard에서 Testcontainers를 쓴 이유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;12861&quot; data-start=&quot;12820&quot; data-ke-size=&quot;size16&quot;&gt;CoreBoard에서 Testcontainers를 쓴 이유는 세 가지였다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;1.&amp;nbsp;실제&amp;nbsp;MySQL&amp;nbsp;기준으로&amp;nbsp;JPA&amp;nbsp;동작을&amp;nbsp;검증하기&amp;nbsp;위해 &lt;br /&gt;2.&amp;nbsp;테스트마다&amp;nbsp;깨끗한&amp;nbsp;DB&amp;nbsp;상태를&amp;nbsp;만들기&amp;nbsp;위해 &lt;br /&gt;3.&amp;nbsp;로컬/CI&amp;nbsp;환경에서도&amp;nbsp;같은&amp;nbsp;방식으로&amp;nbsp;테스트하기&amp;nbsp;위해&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;13030&quot; data-start=&quot;12968&quot; data-ke-size=&quot;size16&quot;&gt;특히 Testcontainers를 쓰면 내 컴퓨터에서는 됐는데 CI에서는 안 됨 같은 문제를 줄일 수 있다.&amp;nbsp;테스트가 직접 MySQL 컨테이너를 띄우기 때문에, 테스트 환경이 코드에 가까워진다.&lt;/p&gt;
&lt;h4 data-end=&quot;13091&quot; data-start=&quot;13081&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;13125&quot; data-start=&quot;13093&quot; data-ke-size=&quot;size16&quot;&gt;물론 Testcontainers가 무조건 좋은 건 아니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;13125&quot; data-start=&quot;13093&quot;&gt;H2보다 느리다.&amp;nbsp;&lt;/li&gt;
&lt;li data-end=&quot;13136&quot; data-start=&quot;13127&quot;&gt;Docker가 필요하다.&lt;/li&gt;
&lt;li data-end=&quot;13136&quot; data-start=&quot;13127&quot;&gt;CI 환경에서도 Docker 실행 조건을 신경 써야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;13230&quot; data-start=&quot;13187&quot; data-ke-size=&quot;size16&quot;&gt;그래서 모든 테스트를 Testcontainers로 돌리는 건 부담일 수 있다.&amp;nbsp;CoreBoard에서는 테스트 성격을 나눠서 생각했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;단순 계산/분기 &amp;rarr; 단위 테스트
Spring MVC 요청/응답 &amp;rarr; Controller 테스트
DB/JPA/트랜잭션 검증 &amp;rarr; Testcontainers 기반 통합 테스트&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;13441&quot; data-start=&quot;13371&quot; data-ke-size=&quot;size16&quot;&gt;즉 Testcontainers는 모든 테스트를 대체하는 도구가 아니라, 실제 DB 검증이 필요한 곳에 사용하는 도구라고 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;기준&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; 비교 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; H2 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; Testcontainers &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;간략 개념&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;Java 기반의 가벼운 관계형 데이터베이스다. &lt;br /&gt;테스트에서는 주로 인메모리 DB로 사용한다.&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;테스트 실행 시 Docker 컨테이너로 &lt;br /&gt;실제 DB를 띄워 사용하는 방식이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;실행 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;애플리케이션 안에서 빠르게 실행할 수 있다. &lt;br /&gt;별도 DB 서버를 띄우지 않아도 된다.&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;테스트가 시작될 때 MySQL 같은 실제 DB 컨테이너를 띄우고, &lt;br /&gt;테스트가 끝나면 제거한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;빠르고 설정이 간단하다. &lt;br /&gt;단위 테스트나 간단한 Repository 테스트에 부담이 적다.&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;실제 운영 DB와 같은 종류의 DB로 테스트할 수 있다. &lt;br /&gt;MySQL 문법, 제약조건, 트랜잭션 동작을 &lt;br /&gt;더 현실적으로 검증할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;실제 운영 DB가 MySQL이라면 &lt;br /&gt;H2와 문법이나 동작 차이가 생길 수 있다. &lt;br /&gt;테스트에서는 통과했는데 &lt;br /&gt;실제 MySQL에서는 실패할 수 있다.&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;H2보다 느리다. &lt;br /&gt;Docker가 필요하다. &lt;br /&gt;CI 환경에서도 Docker 실행 조건을 신경 써야 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;주의점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;H2 테스트 통과가 실제 MySQL 통과를 보장하지 않는다. 특히 SQL 방언, 타입, 제약조건, 인덱스 동작 &lt;br /&gt;차이를 조심해야 한다.&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;테스트 속도가 느려질 수 있으므로 &lt;br /&gt;모든 테스트에 무조건 적용하면 부담이 커진다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;해결방안&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;단순한 단위 테스트나 빠른 검증에는 &lt;br /&gt;H2를 사용할 수 있지만, &lt;br /&gt;DB 호환성이 중요한 테스트는 &lt;br /&gt;실제 DB 기반으로 보완한다.&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;DB/JPA/트랜잭션/제약조건 검증이 필요한 &lt;br /&gt;통합 테스트에 집중해서 사용한다. &lt;br /&gt;단순 로직 테스트는 Mockito 같은 단위 테스트로 분리한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;CoreBoard 선택 관점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;빠르다는 장점은 있지만, &lt;br /&gt;CoreBoard의 실제 DB가 MySQL이므로 &lt;br /&gt;DB 차이로 인한 테스트 신뢰도 문제가 &lt;br /&gt;남는다고 봤다.&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;실제 MySQL 컨테이너로 테스트할 수 있어 &lt;br /&gt;CoreBoard의 JPA, 제약조건, 트랜잭션 흐름을 더 현실적으로 검증할 수 있다고 판단했다.&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-end=&quot;744&quot; data-start=&quot;721&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/308</guid>
      <comments>https://winwin0219.tistory.com/308#entry308comment</comments>
      <pubDate>Mon, 4 May 2026 16:45:39 +0900</pubDate>
    </item>
    <item>
      <title>[스케줄링]Spring Scheduler로 임시 첨부파일 정리하기</title>
      <link>https://winwin0219.tistory.com/307</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; Spring 스케줄링&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/reference/integration/scheduling.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777882854668&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;Task Execution and Scheduling :: Spring Framework&quot; data-og-description=&quot;All Spring cron expressions have to conform to the same format, whether you are using them in @Scheduled annotations, task:scheduled-tasks elements, or someplace else. A well-formed cron expression, such as * * * * * *, consists of six space-separated time&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/reference/integration/scheduling.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Task Execution and Scheduling :: Spring Framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;All Spring cron expressions have to conform to the same format, whether you are using them in @Scheduled annotations, task:scheduled-tasks elements, or someplace else. A well-formed cron expression, such as * * * * * *, consists of six space-separated time&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&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;Spring 스케줄링은 쉽게 말해 &lt;b&gt;서버가 켜져있는 동안, 내가 정한 시간마다 특정 메서드를 자동 실행&lt;/b&gt;하게 만드는 기능이다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; Spring아, 이 프로젝트에서 @Scheduled붙은 메서드를 찾아서 자동 실행해줘 -&amp;gt; &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;@EnableScheduling&lt;/span&gt; &lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1777084387080&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableScheduling
public class SchedulingConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서는 &quot;To enable support for @Scheduled &amp;hellip; add @EnableScheduling to one of your @Configuration classes.&quot;라고 나와있다 즉 @Scheduled 기능을 사용하려면 설정 크랠스 중 하나에 @EnableScheduling을 추가하라는 말이다&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Configuration : Spring 설정 파일 역할을 하는 클래스라는 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EnableScheduling : Spring에게 스케줄링 기능 켜라라고 알려주는 어노테이션 - 스위치&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; 이 메서드를 매일 새벽 3시에 자동 실행해줘 -&amp;gt; @Scheduled &lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1777084527550&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class AttachmentCleanupScheduler {

    @Scheduled(cron = &quot;0 0 3 * * *&quot;)
    public void deleteOldTempAttachments() {
        // 오래된 임시 첨부파일 삭제
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서는 &quot;You can add the @Scheduled annotation to a method, along with trigger metadata.&quot;라고 나와있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 메서드에 @Scheduled를 붙이고 언제 실행할지에 대한 실행 조건도 같이 적을 수 있다는 말이다 여기서 중요한 건 컨트롤러처럼 사용자의 요청이 들어와야 실행되는 게 아니라는 것.&lt;/p&gt;
&lt;pre id=&quot;code_1777084600715&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;서버 켜짐
&amp;rarr; 정해진 시간이 됨
&amp;rarr; Spring이 알아서 메서드 실행&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;trigger metadata : 언제 실행할지 알려주는 초기값 (cron, fixedDelay, fixedRate)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;아래 메서드는 5초마다 실행해줘 -&amp;gt; fixedDelay &lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1777084699803&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(fixedDelay = 5000)
public void doSomething() {
    // something that should run periodically
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서는 이 메서드가 이렇게 동작한다고 설명하고 있다 &quot;the period is measured from the completion time of each preceding invocation.&quot; 다음 실행까지의 시간은 이전 실행이 끝난 시점부터 계산된다는 말이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들면&lt;/p&gt;
&lt;pre id=&quot;code_1777084750104&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;12:00:00 작업 시작
12:00:03 작업 끝남
fixedDelay = 5초
12:00:08 다음 작업 시작&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&amp;nbsp;Spring 스케줄링의 기본 선택지&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1777084912897&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. fixedDelay
2. fixedRate
3. cron
+ initialDelay&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. fixedDelay&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/4.2.7.RELEASE/spring-framework-reference/html/scheduling.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/docs/4.2.7.RELEASE/spring-framework-reference/html/scheduling.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777882880545&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;33.&amp;nbsp;Task Execution and Scheduling&quot; data-og-description=&quot;In addition to the TaskExecutor abstraction, Spring 3.0 introduces a TaskScheduler with a variety of methods for scheduling tasks to run at some point in the future. The simplest method is the one named 'schedule' that takes a Runnable and Date only. That &quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/docs/4.2.7.RELEASE/spring-framework-reference/html/scheduling.html&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/docs/4.2.7.RELEASE/spring-framework-reference/html/scheduling.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/4.2.7.RELEASE/spring-framework-reference/html/scheduling.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/docs/4.2.7.RELEASE/spring-framework-reference/html/scheduling.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;33.&amp;nbsp;Task Execution and Scheduling&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;In addition to the TaskExecutor abstraction, Spring 3.0 introduces a TaskScheduler with a variety of methods for scheduling tasks to run at some point in the future. The simplest method is the one named 'schedule' that takes a Runnable and Date only. That&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&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;fixedDelay는 이전 작업이 끝난 뒤 정해진 시간만큼 기다렸다가 다음 작업을 실행하는 방식이다 위에서 다룬 것처럼, 공식문서에서는 &quot; The fixedDelay attribute &amp;hellip; makes sure that there is a delay of five seconds between the finish time of an execution and the start time of the next execution.&quot;라고 설명하고 있다 fixedDelay는 한 번 실행이 끝난 시점과 다음 실행이 시작되는 시점 사이에 5초 간격으로 보장한다는 말이다&lt;/p&gt;
&lt;pre id=&quot;code_1777085047272&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(fixedDelay = 5000)
public void cleanup() {
    // 작업 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1777085060449&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;12:00:00 작업 시작
12:00:03 작업 끝
5초 기다림
12:00:08 다음 작업 시작&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 작업이 끝난 뒤 다음 작업이 시작되는 거라 안전하다 작업 시간이 길어져도 끝나고 나서 다시 시작이라는 흐름이 덜 꼬이기 때문이다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 매 5초 매 1분 같은 고정시각을 보장하지는 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 시간이 길어지면 전체 실행 간격도 밀린다&lt;/p&gt;
&lt;pre id=&quot;code_1777085132058&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;작업 3초 + 대기 5초 = 다음 실행은 8초 뒤
작업 10초 + 대기 5초 = 다음 실행은 15초 뒤&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;언제 사용하는 게 적절할까?&lt;/b&gt; 했을 땐 외부 API동기화, 파일 정리, DB정ㄹ, 메일 재시도, 로그 정리 등에 적절해 보인다&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;b&gt;2. fixedRate &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fixedRate는 작업 시작 시각을 기준으로 일정 간격마다 실행하려는 방식이다&lt;/p&gt;
&lt;pre id=&quot;code_1777085210177&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(fixedRate = 5000)
public void collectMetrics() {
    // 작업 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서에서 fixedRate도 주기 실행 속석으로 제시되고 있다 Spring @Scheduled javadoc은 반복 작업에서 cron, fixedDelay, fixedRate 중 하나를 지정한다고 설명한다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777085252576&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;12:00:00 작업 시작
12:00:05 다음 작업 시작 예정
12:00:10 다음 작업 시작 예정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 시작 기준으로 5초마다라고 생각하면 이해하기 쉽다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정한 주기로 계속 실행하려는 작업에 좋다 예를들어 10초마다 현재 서버 상태를 수집하기 같은 건 시작 간격이 중요하다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업이 오래 걸리면 밀리거나 겹침 문제가 생길 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 공식문서는 같은 메서드에 여러 스케줄 선언이 있으면 독립적으로 처리되고 서로 겹쳐 병렬 실행되거나 바로 이어 실행될 수 있으니 겹치지 않게 주의하라고 말하고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;such co-located schedules may overlap and execute multiple times in parallel or in immediate succession&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;b&gt; 언제 사용하는 게 적절할까?&lt;/b&gt; 했을 땐 10초마다 메트릭 수집, 1분마다 상태 체크, 일정주기로 캐시 갱신 시도 등에 적절해 보인다&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;b&gt;3. cron &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 초, 몇 분, 몇 시, 몇 일, 몇 월, 무슨 요일에 실행할지를 식으로 정리하는 방식이다&lt;/p&gt;
&lt;pre id=&quot;code_1777085471897&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(cron = &quot;0 0 3 * * *&quot;)
public void cleanupOldTempFiles() {
    // 매일 새벽 3시 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot; A well-formed cron expression, such as * * * * * *, consists of six space-separated time and date fields, each with its own range of valid values. &quot; Spring 공식문서는 cron 표현식이 정해진 형식을 따라야 한다고 설명하고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cron 표현식은 6개의 칸으로 표현된다는 말이다&lt;/p&gt;
&lt;pre id=&quot;code_1777085509285&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;초 분 시 일 월 요일
0  0  3  *  *  * -&amp;gt; 매일 03:00:00&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 이해하기 쉬운 운영 시간 기준으로 실행할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 매일 새벽3시, 매주 월요일 오전 9시, 매달 1일 0시, 평일 18시.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식의 달력 기준 작업에 좋다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현식이 익숙하지 않으면 헷갈린다&lt;/p&gt;
&lt;pre id=&quot;code_1777085574891&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;0 0 3 * * *&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;pre id=&quot;code_1777085609346&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;서버 1대 &amp;rarr; 새벽 3시에 1번 실행
서버 2대 &amp;rarr; 각 서버에서 새벽 3시에 실행될 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 한 대면 단순하지만, 나중에 서버가 여러 대가 되면 중복 실행 방지도 고민해야 한다&lt;/p&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;언제 사용하는 게 적절할까?&amp;nbsp;&lt;/b&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;b&gt;4. initialDelay &lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;initialDelay는 서버 켜진 뒤 바로 실행하지 말고 처음 실행만 조금 늦추는 옵션이다&lt;/p&gt;
&lt;pre id=&quot;code_1777085679611&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(initialDelay = 10000, fixedDelay = 60000)
public void cleanup() {
    // 서버 시작 10초 후 첫 실행
    // 이후 이전 작업 종료 후 60초마다 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서 javadoc은 initialDelay를 이렇게 설명하고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Number of units of time to delay before the first execution&quot; 첫번째 실행 전에 얼마나 기다릴지 정하는 값이라는 뜻이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-projects/spring-framework/blob/main/spring-&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777882811689&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;spring-framework/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java at main &amp;middot; spring-project&quot; data-og-description=&quot;Spring Framework. Contribute to spring-projects/spring-framework development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java&quot; data-og-url=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bzQ9HT/dJMb9b3VLVl/kr8sWmERtVRxn8n3a6eD7k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/ovQan/dJMb9jOqD5z/LlKh9FsU8IJXS9l78kKsJ0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bzQ9HT/dJMb9b3VLVl/kr8sWmERtVRxn8n3a6eD7k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/ovQan/dJMb9jOqD5z/LlKh9FsU8IJXS9l78kKsJ0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;spring-framework/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java at main &amp;middot; spring-project&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Framework. Contribute to spring-projects/spring-framework development by creating an account 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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 시작 직후 무거운 작업이 바로 도는 걸 막을 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 막 켜져있을 때는 애플리케이션 초기화, DB 연결, 캐시 준비 등으로 바쁠 수 있따 그때 스케줄러까지 바로 돌면 부담이 될 수 있다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단독으로 반복 주기를 정하는 핵심 방식은 아니다 대부분 fixedDelay나 fixedRate와 같이 쓴다&lt;/p&gt;
&lt;p data-ke-size=&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;border-collapse: collapse; width: 100%; height: 214px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center; width: 11.8605%;&quot;&gt;&lt;b&gt; 방식 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center; width: 12.4419%;&quot;&gt;&lt;b&gt; 기준 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center; width: 25.6977%;&quot;&gt;&lt;b&gt; 장점 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center; width: 28.7209%;&quot;&gt;&lt;b&gt; 단점 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center; width: 21.1628%;&quot;&gt;&lt;b&gt; 어울리는 작업 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 44px;&quot;&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 11.8605%;&quot;&gt;fixedDelay&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 12.4419%;&quot;&gt;이전 작업 &lt;br /&gt;종료 후&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 25.6977%;&quot;&gt;이전 작업 끝난 뒤 실행이라 &lt;br /&gt;안전함&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 28.7209%;&quot;&gt;정확한 시각 실행은 아님&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 21.1628%;&quot;&gt;파일 정리, 재시도, &lt;br /&gt;DB 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 44px;&quot;&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 11.8605%;&quot;&gt;fixedRate&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 12.4419%;&quot;&gt;이전 작업 &lt;br /&gt;시작 후&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 25.6977%;&quot;&gt;일정 주기로 실행하려 할 때 좋음&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 28.7209%;&quot;&gt;작업이 오래 걸리면 밀림/겹침 고려&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 21.1628%;&quot;&gt;메트릭 수집, 상태 체크&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 44px;&quot;&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 11.8605%;&quot;&gt;cron&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 12.4419%;&quot;&gt;달력/시각&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 25.6977%;&quot;&gt;매일 3시 같은 운영 시간 설정 &lt;br /&gt;가능&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 28.7209%;&quot;&gt;표현식이 헷갈리고 중복 실행 고려 &lt;br /&gt;필요&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 21.1628%;&quot;&gt;새벽 정리, 통계, 정산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 44px;&quot;&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 11.8605%;&quot;&gt;initialDelay&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 12.4419%;&quot;&gt;첫 실행 전&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 25.6977%;&quot;&gt;서버 시작 직후 부담 방지&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 28.7209%;&quot;&gt;반복 방식 자체는 아님&lt;/td&gt;
&lt;td style=&quot;height: 44px; text-align: center; width: 21.1628%;&quot;&gt;fixedDelay, &lt;br /&gt;fixedRate 보조&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/  Spring</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/307</guid>
      <comments>https://winwin0219.tistory.com/307#entry307comment</comments>
      <pubDate>Sat, 25 Apr 2026 12:04:26 +0900</pubDate>
    </item>
    <item>
      <title>[Devlog] Spring Profile, 환경마다 yml을 분리한 이유</title>
      <link>https://winwin0219.tistory.com/304</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/300&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://winwin0219.tistory.com/300&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774228446565&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[회고] CoreBoard 프로젝트, 왜 만들었는가&quot; data-og-description=&quot;CoreBoard 기획했을 때로 거슬러 올라가자 때는 2025년 9월 말. Momentix에서 느꼈던 부담감이 있었다. 부트캠프의 문제점이자 나의 약점(비전공자)이었던 동작은 하지만 얕은 이해도였다. 요즘 현업에&quot; data-og-host=&quot;winwin0219.tistory.com&quot; data-og-source-url=&quot;https://winwin0219.tistory.com/300&quot; data-og-url=&quot;https://winwin0219.tistory.com/300&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pFaCu/dJMb8RRRfvg/wW3UYyNNfWrDEAXMulcMA0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/l3DG9/dJMb85vOjCP/b5yEWJ2V18iqM3ygUXug5k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/300&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://winwin0219.tistory.com/300&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pFaCu/dJMb8RRRfvg/wW3UYyNNfWrDEAXMulcMA0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/l3DG9/dJMb85vOjCP/b5yEWJ2V18iqM3ygUXug5k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[회고] CoreBoard 프로젝트, 왜 만들었는가&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;CoreBoard 기획했을 때로 거슬러 올라가자 때는 2025년 9월 말. Momentix에서 느꼈던 부담감이 있었다. 부트캠프의 문제점이자 나의 약점(비전공자)이었던 동작은 하지만 얕은 이해도였다. 요즘 현업에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;winwin0219.tistory.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;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;처음에 application.yml 하나만 사용했었다. 근데 로컬 개발할 때랑 EC2 올릴 때 DB 주소가 다르고 테스트 돌릴 때 실제 DB 건드리면 안되고 신경을 써야 한다는 것을 알았다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt; 로컬에서는&lt;/b&gt; DB가 내 컴퓨터 안에 있으니까 &lt;b&gt;localhost로 접속&lt;/b&gt;하지만, &lt;b&gt;EC2에 올리면&lt;/b&gt; 앱이 서버 위에서 돌아가기 때문에 localhost는 &lt;b&gt;EC2 자기 자신&lt;/b&gt;을 가리켜서 DB를 못 찾는다. 즉, 실행되는 환경이 달라지면 DB 주소도 달라질 수밖에 없다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;u&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/features/profiles.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;스프링 프로필이란?&lt;/b&gt;&lt;/span&gt;&lt;/a&gt;&lt;/u&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; 스프링 프로필은 환경별로 세팅해놓은 yml 또는 properties를 일컫는다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;CoreBoard 왜 이렇게 나누었는가?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1774240325223&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;application.yml          &amp;larr; 공통 (앱 이름만)
application-local.yml    &amp;larr; 로컬 개발
application-dev.yml      &amp;larr; 개발 서버
application-prod.yml     &amp;larr; 운영 (EC2)
application-test.yml     &amp;larr; 테스트&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;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;바로 본론부터 말해보자면, 시크릿 키가 유출되면 대형사고가 발생할 수 있기 때문에 환경변수를 .env에 담아 깃이그노어에 설정해놓는 게 국룰이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러나 스프링 프로필에 대해 알게되면 단순하게만 생각하던 것들이 정말 얕은 지식이었구나를 느낀다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;1. application.yml&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;공통으로 들어가는 것들 모음이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2. application-local.yml&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로컬 즉, 개발자가 개발하는 환경을 의미하는데 이곳에서는 협업을 진행해야 해서 시크릿키를 공유해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;CoreBoard는 이런 상황을 가정하여 프로필을 설정했다. &lt;/span&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;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;회사망에서만 개발 가능&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;깃허브 레포는 private&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 해당 로컬 야물 파일에는 jwt와 aes등 키가 들어있지만 &lt;b&gt;임시키&lt;/b&gt;고, &lt;b&gt;prod에서 실제 쓰이는 키와 다르다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;3. application-dev.yml&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; dev는 CI/CD 파이프라인에서 자동 배포 후 동작 확인용 환경이다. 운영(prod)처럼 민감한 환경변수 처리가 필요하진 않지만, 운영에 올리기 전 검증하는 역할을 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;show_sql은 끄고, ddl-auto는 validate로 설정했다 &amp;mdash; 스키마를 자동으로 건드리지 않겠다는 뜻. &lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;4. application-prod.yml&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;핵심은 prod 야물이다. 절대 유출되어선 안 되기 때문에&lt;b&gt; 환경변수&lt;/b&gt;로 사용하고 있고 &lt;b&gt;ec2환경에서 따로 관리&lt;/b&gt;되고 있다. 그래서 CoreBoard는 클론을 받더라도 .env를 할 필요가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5. application-test.yml &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&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;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;ddl-auto : create-drop&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;테스트가 실제 실행될 때 스키마를 새로 만들고, 테스트가 끝나면 전부 날린다. 실제 DB를 건드리지 않고 매 테스트마다 깨끗한 상태로 시작할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;bcrypt strength : 4&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기본값은 10인데 숫자가 클수록 해싱에 걸리는 시간이 길어진다. 테스트에서 보안보다 속도가 우선이기 때문에 최솟값인 4로 설정했다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/304</guid>
      <comments>https://winwin0219.tistory.com/304#entry304comment</comments>
      <pubDate>Mon, 23 Mar 2026 13:45:20 +0900</pubDate>
    </item>
    <item>
      <title>[Devlog] 코드 짜기 전에 기반부터 &amp;mdash; 공통 응답 포맷 설계</title>
      <link>https://winwin0219.tistory.com/302</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/300&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://winwin0219.tistory.com/300&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;[회고] CoreBoard 프로젝트, 왜 만들었는가&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;CoreBoard 기획했을 때로 거슬러 올라가자 때는 2025년 9월 말. Momentix에서 느꼈던 부담감이 있었다. 부트캠프의 문제점이자 나의 약점(비전공자)이었던 동작은 하지만 얕은 이해도였다. 요즘 현업에&quot; data-og-host=&quot;winwin0219.tistory.com&quot; data-og-source-url=&quot;https://winwin0219.tistory.com/300&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/bazU0Z/dJMb83ShKJt/AAAAAAAAAAAAAAAAAAAAALi38rvDpKU6MAOEzKnDv8d3hjbMpW-AynSYXICMInVB/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=LoYpwlvh%2FI%2FawKcozNdo9sP9nMY%3D&quot; data-og-url=&quot;https://winwin0219.tistory.com/300&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/300&quot; target=&quot;_blank&quot; data-source-url=&quot;https://winwin0219.tistory.com/300&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/bazU0Z/dJMb83ShKJt/AAAAAAAAAAAAAAAAAAAAALi38rvDpKU6MAOEzKnDv8d3hjbMpW-AynSYXICMInVB/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=LoYpwlvh%2FI%2FawKcozNdo9sP9nMY%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;[회고] CoreBoard 프로젝트, 왜 만들었는가&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;CoreBoard 기획했을 때로 거슬러 올라가자 때는 2025년 9월 말. Momentix에서 느꼈던 부담감이 있었다. 부트캠프의 문제점이자 나의 약점(비전공자)이었던 동작은 하지만 얕은 이해도였다. 요즘 현업에&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;winwin0219.tistory.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;br&gt;&amp;nbsp;&lt;br&gt;GlobalExceptionHandler 와 ApiResponse(공통 응답 포맷)에 대해 말해보려고 한다.&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;이 글에서 다루는 것&lt;/b&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;ApiRespones 설계 - success, message, data (제네릭)&lt;/li&gt; 
 &lt;li&gt;왜 GlobalExceptionHandler가 필요한가? 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;@RestControllerAdvice + @ExceptionHandler 동작원리&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
 &lt;li&gt;에러가 터졌을 때 ApiResponse로 어떻게 감싸서 내려보내는가?&lt;/li&gt; 
&lt;/ul&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1) &lt;/b&gt;&lt;b&gt;왜 공통 응답 포맷이 필요한가?&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Momentix에서 ApiResponse는 전혀 생각하지도 못했다. 백엔드 개발자는 서버와 데이터베이스 그리고 프론트엔드와 소통해야 한다. 근데 실제로 어떻게 소통해야 하는지, 프론트엔드에게 예의를 갖추려면 어떤 방식으로 해야 하는지 등에 대해서 부캠에서 알려주진 않는다.&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;사용자 화면에 어떤 아이템을 렌더링할 것인지 직접적으로 관여하는 사람은 백엔드가 아니라 프론트엔드 개발자다. 그래서 우리는&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프론트엔드 개발자가 이해하기 좋게 그리고 렌더링하기 편하게 공통응답으로 보내줘야 함&lt;/b&gt;을 깨달았다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2) &lt;/b&gt;&lt;b&gt;ApiRespones 설계 - success, message,&lt;/b&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;b&gt;data (제네릭)&lt;/b&gt;&lt;/h4&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{
&amp;nbsp;&amp;nbsp;&quot;success&quot;: false,
&amp;nbsp;&amp;nbsp;&quot;message&quot;: &quot;제목은 필수입니다.&quot;,
&amp;nbsp;&amp;nbsp;&quot;data&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;code&quot;: &quot;400&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;errors&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;field&quot;: &quot;title&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;reason&quot;: &quot;제목은 필수입니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;위 response는 실제 &lt;b&gt;CoreBoard 깃허브 레포에도 나와있는 응답 포맷&lt;/b&gt;이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;success&lt;/b&gt;는 boolean으로 프론트가&lt;b&gt; 분기처리하기 편하게&lt;/b&gt;, &lt;b&gt;data&lt;/b&gt;는 제네릭으로 &lt;b&gt;어떤 타입이든 담을 수 있게&lt;/b&gt; 설계했다.&amp;nbsp;&lt;br&gt;이게 무슨 말이냐면, 예를들어 auth와 board 응답을 보면 아래와 같다&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 게시글 조회 응답
public class ApiResponse {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private boolean success;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String message;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private Board data;&amp;nbsp;&amp;nbsp;// Board 타입 고정
}

// 유저 조회 응답
public class ApiResponse {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private boolean success;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private String message;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private User data;&amp;nbsp;&amp;nbsp;// User 타입 고정
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;즉, 기능마다 ApiResponse 클래스를 새로 만들어야 하는데 이러면 완전 비효율적이다 그래서 제네릭을 사용하게되면 클래스 하나로 어떤 타입이든 다 담을 수 있게 된다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ApiResponse&amp;lt;Board&amp;gt; boardResponse = new ApiResponse&amp;lt;&amp;gt;(true, &quot;조회 성공&quot;, board);
ApiResponse&amp;lt;User&amp;gt; userResponse = new ApiResponse&amp;lt;&amp;gt;(true, &quot;조회 성공&quot;, user);
ApiResponse&amp;lt;List&amp;lt;Board&amp;gt;&amp;gt; listResponse = new ApiResponse&amp;lt;&amp;gt;(true, &quot;조회 성공&quot;, boards);&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3) &lt;/b&gt;&lt;b&gt;왜 GlobalExceptionHandler가 필요한가?&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;간단하다. CoreBoard만 해도 예외처리된 게 엄청 많은데 예외핸들러가 없게되면 try-catch문이 수백개가 될 수도 있다. 이 경우 가독성 뿐만 아니라 관리도 쉽지 않게 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;다시 정리해보면,&amp;nbsp;&lt;br&gt;1. 중복 코드 엄청 생김&lt;br&gt;2. 컨트롤러가 예외 처리 책임까지 떠안 게 되어 SRP 위반임&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-1) @RestControllerAdvice + @ExceptionHandler 동작원리&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;b&gt;내가 잘못 이해했던 부분&lt;/b&gt;이 Filter나 Interceptor처럼 앞단에서 가로채는 건 줄 알았는데 예외가 controller 밖으로 탈출한 후에 잡는 것이었다. 즉, 예외 발생 위치는 어디든 상관이 없다.&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Controller 안에서 throw&amp;nbsp;&amp;nbsp;─┐
Service 안에서 throw&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ─┤→ 아무도 안 잡으면 → Controller 밖 탈출
Repository 안에서 throw&amp;nbsp;&amp;nbsp;─┘&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;↓
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@RestControllerAdvice가 여기서 낚아챔&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;DispatcherServlet이 예외를 받으면 직접 처리하지 않고 &lt;b&gt;ExceptionHandlerExceptionResolver한테 위임&lt;/b&gt;한다.&lt;br&gt;@RestControllerAdvice붙은 Bean을 뒤져서 터진 예외 타입과 일치하는 @ExceptionHandler 메서드를 찾아 실행한다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4) &lt;/b&gt;&lt;b&gt;에러가 터졌을 때 ApiResponse로 어떻게 감싸서 내려보내는가?&lt;/b&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 CoreBoard의 예외 구조를 이해해야 한다.&amp;nbsp; ErrorException이라는 추상 클래스가 있다. &lt;br&gt;status(HTTP 상태코드), code(숫자), message(메시지), errors(FieldError 목록) &lt;br&gt;이 네 가지를 들고 있는 공통 부모 클래스다.&lt;br&gt;그리고 도메인별로 이 클래스를 상속한 예외 클래스가 있다. auth쪽은 AuthErrorException, board쪽은 BoardErrorException이다.&lt;br&gt; 직접적으로 throw하지 않고 enum을 하나 더 둔다. AuthErrorCode라는 enum인데 여기에 발생할 수 있는 에러 케이스를 전부 정의해놓는다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 401, &quot;다시 로그인해 주세요.&quot;, ...)
NOT_FOUND(HttpStatus.NOT_FOUND, 404, &quot;존재하지 않는 사용자입니다.&quot;, ...)&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하는 이유는 에러 메시지나 코드를 한 곳에서 관리하기 위해서다. &lt;br&gt;나중에 메시지 수정이 필요할 때 각 서비스 파일을 다 뒤질 필요 없이 AuthErrorCode만 수정하면 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt; 실제 throw하는 방식은 이렇다 &lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;throw new AuthErrorException(AuthErrorCode.NOT_FOUND);&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 GlobalExceptionHandler가 이 예외를 받아서 아래처럼 처리한다&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ExceptionHandler(ErrorException.class)
public ResponseEntity&amp;lt;ApiResponse&amp;lt;ErrorResponse&amp;gt;&amp;gt; handleFailException(ErrorException e) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ErrorResponse detail = new ErrorResponse(String.valueOf(e.getCode()), e.getErrors());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return ResponseEntity
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.status(e.getStatus())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.body(ApiResponse.fail(e.getMessage(), detail));
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt; AuthErrorException은 ErrorException을 상속했기 때문에 @ExceptionHandler(ErrorException.class) 하나로 auth와 board 예외를 전부 잡을 수 있다.&amp;nbsp;&amp;nbsp;최종적으로 프론트가 받는 응답은 이렇다&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;{
&amp;nbsp;&amp;nbsp;&quot;success&quot;: false,
&amp;nbsp;&amp;nbsp;&quot;message&quot;: &quot;존재하지 않는 사용자입니다.&quot;,
&amp;nbsp;&amp;nbsp;&quot;data&quot;: {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;code&quot;: &quot;404&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;errors&quot;: [
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;field&quot;: &quot;USER NOT FOUND&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;reason&quot;: &quot;존재하지 않는 사용자입니다.&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/302</guid>
      <comments>https://winwin0219.tistory.com/302#entry302comment</comments>
      <pubDate>Wed, 18 Mar 2026 14:32:57 +0900</pubDate>
    </item>
    <item>
      <title>[Devlog] SequenceDiagram</title>
      <link>https://winwin0219.tistory.com/301</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/300&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://winwin0219.tistory.com/300&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1773793927333&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[회고] CoreBoard 프로젝트, 왜 만들었는가&quot; data-og-description=&quot;CoreBoard 기획했을 때로 거슬러 올라가자 때는 2025년 9월 말. Momentix에서 느꼈던 부담감이 있었다. 부트캠프의 문제점이자 나의 약점(비전공자)이었던 동작은 하지만 얕은 이해도였다. 요즘 현업에&quot; data-og-host=&quot;winwin0219.tistory.com&quot; data-og-source-url=&quot;https://winwin0219.tistory.com/300&quot; data-og-url=&quot;https://winwin0219.tistory.com/300&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bazU0Z/dJMb83ShKJt/9VNuWzyEhAYkcnThOWXqw1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/disetp/dJMb9eTOCeB/GuBhUX7Kb1Yf9zl3MdHgH0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://winwin0219.tistory.com/300&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://winwin0219.tistory.com/300&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bazU0Z/dJMb83ShKJt/9VNuWzyEhAYkcnThOWXqw1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/disetp/dJMb9eTOCeB/GuBhUX7Kb1Yf9zl3MdHgH0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[회고] CoreBoard 프로젝트, 왜 만들었는가&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;CoreBoard 기획했을 때로 거슬러 올라가자 때는 2025년 9월 말. Momentix에서 느꼈던 부담감이 있었다. 부트캠프의 문제점이자 나의 약점(비전공자)이었던 동작은 하지만 얕은 이해도였다. 요즘 현업에&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;winwin0219.tistory.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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이전 글에서 프로젝트의 등장배경에 대해 설명했으니 &lt;b&gt;CoreBoard의 0단계&lt;/b&gt;였던 &lt;b&gt;시퀀스다이어그램&lt;/b&gt;에 대해 쓰려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;시퀀스다이어그램은 부트캠프에서 알려주진 않는다. 그저 ERD와 API명세서 정도만 알려주는데 실제로 스웨거API가 다 해주기 때문에 시간을 많이 잡아먹는 API명세서는 삽질처럼 보인다 ^^&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOnMiz/dJMcadVyDqr/XmWybUK2mG9ak09b8q7lk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOnMiz/dJMcadVyDqr/XmWybUK2mG9ak09b8q7lk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOnMiz/dJMcadVyDqr/XmWybUK2mG9ak09b8q7lk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOnMiz%2FdJMcadVyDqr%2FXmWybUK2mG9ak09b8q7lk0%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;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&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;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;먼저, 구글링해보면 어려운 그림들만 등장하는데 사실 그렇게 어려운 것도 아닌데 이때는 좀 어렵게 느껴졌었다.&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;이때 내가 한 삽질&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. &lt;b&gt;시퀀스다이어그램이 왜 필요하지?&lt;/b&gt;라는 생각에 구현부터 하려고 했음&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 근데 &lt;b&gt;TDD에서 막힘&lt;/b&gt;. 말 그대로 Test Driven Development 즉, 테스트 먼저 한 다음에 green으로 맞추기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. 그러면 TDD부터 해야겠네? 싶은데 이땐 &lt;b&gt;테스트코드가 왜 중요한 지 몰라&lt;/b&gt;서 인증 인가부터 구현하려고 함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4. 근데 &lt;b&gt;시큐리티 없이 하려니 아주 죽을 맛&lt;/b&gt;임&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, 또 Momentix 때로 뇌가 움직이기 시작했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 며칠이 지나도록 기능구현 하나 못하자 더는 안 되겠다싶어서 제일&lt;b&gt; 먼저 했던 게 시퀀스다이어그램의 개념과 필요성&lt;/b&gt;에 대해 알아보자였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;먼저, 그림부터 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram (6).jpg&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbmUbL/dJMcajuIQXW/kzKSlIl3if55jCZoraJKSk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbmUbL/dJMcajuIQXW/kzKSlIl3if55jCZoraJKSk/img.jpg&quot; data-alt=&quot;내가 직접 그림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbmUbL/dJMcajuIQXW/kzKSlIl3if55jCZoraJKSk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbmUbL%2FdJMcajuIQXW%2FkzKSlIl3if55jCZoraJKSk%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;1255&quot; height=&quot;729&quot; data-filename=&quot;diagram (6).jpg&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;729&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이해를 돕고자 아주 간략하게만 그려보면 이러한데, 사용자의 어떠한 행위로 인해 HTTP 요청이 발생되고 이를 Application 안에서 어떻게 동작하는지 시각적으로 그려놓은 docs다.&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;SequenceDiagram의 개념과 중요한 이유&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. 프론트엔드와 개발자의 소통&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이게 무슨 말이냐면, 어떤 요청을 보내야 하고 어떤 응답이 오는지 한 눈에 보이고 API명세서 보다 흐름이 잘 보여서 협업 시 오해가 준다는 의미다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. 테스트 코드 작성에 유리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;흐름이 그러져있으면 여기서 이 값이 들어왔을 때, 어떤 결과 혹은 에러가 나와야 하는가 등, 촘촘하게 설계할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, 테스트 케이스 도출이 쉬워진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 레이어 역할 분리&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 로직이 controller 책임인지 service 책임인지를 코드 짜기 전에 고민할 수 있어서 좋다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, SRP 위반 최소화할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4. 복잡한 흐름의 사전 검증&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특히 인증 / 인가처럼 레이어를 여러 개 거치는 흐름에서 머릿속으로만 생각해서 놓쳤던 케이스를 다이어그램 그리면서 발견하게 되어 좋다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;개인적으로 특히 3번이 핵심인 것 같다. 왜냐면 그 당시 3번을 놓쳐서 레이어 위반을 엄청나게 했기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(예를들어, DB 조회가 필요없는 로직이 service에 있다거나)&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;시퀀스다이어그램 별 거 없다. 이 다이어그램 덕분에&lt;b&gt; 어디부터 짜야하는지&lt;/b&gt;, &lt;b&gt;어떤 DTO가 있어야 하는지&lt;/b&gt;, &lt;b&gt;TDD로 무엇을 검증해야 하는지&lt;/b&gt; 전부 알 수 있게 해준 반드시 필요한 docs다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Momentix 때 하던 설계/기획은 미완성이라는 것을 더더욱 체감하게 되는 순간이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/301</guid>
      <comments>https://winwin0219.tistory.com/301#entry301comment</comments>
      <pubDate>Wed, 18 Mar 2026 10:04:02 +0900</pubDate>
    </item>
    <item>
      <title>[회고] CoreBoard 프로젝트, 왜 만들었는가</title>
      <link>https://winwin0219.tistory.com/300</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CoreBoard 기획했을 때로 거슬러 올라가자&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;때는 2025년 9월 말. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Momentix에서 느꼈던 부담감이 있었다. &lt;b&gt;부트캠프의 문제점&lt;/b&gt;이자 &lt;b&gt;나의 약점&lt;/b&gt;(비전공자)이었던 &lt;b&gt;동작은 하지만 얕은 이해도&lt;/b&gt;였다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 요즘 현업에서는 AI로 코드 짜는 게 당연해졌다지만, 배우는 입장에서 나는 그렇게 하고 싶지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;비전공자가 개발할 때 AI에 의지하여 복붙만 해서 쓴다&lt;/b&gt;라는 건 &lt;b&gt;성장할 의지가 없다&lt;/b&gt;와 같은 말처럼 들렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;부트캠프에서 내가 기대한 바는 &lt;b&gt;언어와 친해지기, 팀프로젝트, 개발자 인사이트&lt;/b&gt;를 얻는 것 이렇게 3개였는데 실제로는 데드라인에 맞춰 말도 안 되는 깊이를 모두 때려넣었어야 했다. 회사에 들어가게되면 일상이겠지만, 배우는 단계에서는 정말 최악이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;부트캠프에서 하나만 파자라는 생각으로 임하긴 했는데 왜인지는 모르겠지만 인증 / 인가만 담당해왔다. 그러나 Spring Security에 대해 이해하지 못한 채 수료해버렸다. 근데 생각해보면, 신입에게 인증 / 인가 업무를 주진 않을텐데 ㅋㅋㅋㅋ 아마 그때, 내가 생각하기에 중요해 보이는 것 보다는 가장 어려워 보이는 걸 팠던 것 같다.&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그래서 등장한 게 CoreBoard.&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;CoreBoard의 핵심 목표&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. Lombok이 숨겨주는 코드를 직접 작성하면서, 의존성 주입 구조와 결합도를 눈으로 확인하기 위해 &lt;b&gt;Lombok 사용을 금할 것&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. &lt;b&gt;SequenceDiagram&lt;/b&gt;이&amp;nbsp;왜 중요한지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. &lt;b&gt;Spring MVC&lt;/b&gt; 요청 처리 흐름( Spring MVC Request Lifecycle)에 대해 이해할 것&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4. &lt;b&gt;레이어 계층 간 역할&lt;/b&gt; 분리 (SRP)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;5. &lt;b&gt;TDD&lt;/b&gt;의 역할&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;6. &lt;b&gt;Spring Security&lt;/b&gt;가 해준 기능 이해하고 &lt;b&gt;직접 구현(Security금지)&lt;/b&gt;해볼 것&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;7. 전체 조회(FindAll) 설계 시 페이지네이션 필요성과 성능 문제 직접 체감하기 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;8. CRUD 구현 시&amp;nbsp;&lt;b&gt;AI 안 쓰고&lt;/b&gt; 구글링 &amp;amp; 공식문서 보고 할 것 (공부할 때만 AI 씀)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이걸 직접 구현해 보면서 느낀 점이 &lt;b&gt;AI없이는 진짜 아무것도 못하는구나&lt;/b&gt; 싶었다. 진행하는 동안에 굉장히 쓴 피맛이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;너무너무 무지했고 광범위했고 .. 그래도 &lt;b&gt;일을 벌렸으니 해야지 어떡해&lt;/b&gt;라는 마인드로 진지하게 임했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;참고로 CoreBoard는 애정이 많은 프로젝트다&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 프로젝트를 만드는 동안에 일단 살고봐야 해서 블로그에 기록하는 시간을 제한했었는데, 기억을 되짚어보며 회고록을 끄적이기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;회고록에는 &lt;b&gt;어떠한 문제&lt;/b&gt;를 마주했고 &lt;b&gt;왜 이렇게 해결&lt;/b&gt;했는지 &lt;b&gt;왜 고민&lt;/b&gt;했는지 그리고 약간의 한탄까지 ㅎㅎ 덧붙여서 할 예정이다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;나중에 내가 이 글을 다시 보게될 경우까지 고려해서 작성 할 예정이다&lt;/span&gt;&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/300</guid>
      <comments>https://winwin0219.tistory.com/300#entry300comment</comments>
      <pubDate>Wed, 18 Mar 2026 09:27:59 +0900</pubDate>
    </item>
    <item>
      <title>[Momentix] Redis TTL 기반 이메일 인증 상태 관리 설계</title>
      <link>https://winwin0219.tistory.com/295</link>
      <description>&lt;h1 style=&quot;text-align: right;&quot;&gt;&lt;b&gt;왜 Redis를 썼는가?&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Momentix에서 이메일 인증은 잠깐 유지되었다가 사라져야 하는 상태다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증코드&lt;/li&gt;
&lt;li&gt;인증 완료 토큰&lt;/li&gt;
&lt;li&gt;재전송 쿨다운 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정보들은 &lt;b&gt;영구 저장 대상이 아니다&lt;/b&gt; 일정 시간이 지나면 자동으로 만료되어야 한다 그래서 DB 대신 &lt;b&gt;TTL이 있는 Redis&lt;/b&gt;를 사용했다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;DB 대신 Redis를 선택한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 자동 만료가 필요했기 때문&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 저장한다면?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만료된 데이터 삭제 로직을 따로 구현해야 한다&lt;/li&gt;
&lt;li&gt;스케줄러나 배치 작업이 필요하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 Key에 TTL을 걸면 자동 삭제된다 &amp;rarr; 별도 정리 로직이 필요 없다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 인증은 임시 상태이기 때문&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 인증 정보는 짧게 존재해야 하며 일정 시간 후 의미 없어지고 재사용을 방지 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 성격은 영구 저장소보다 &lt;b&gt;임시 저장소&lt;/b&gt;에 가깝다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 조회 속도가 중요하기 때문&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증은 로그인/회원가입과 같이 자주 발생한다 Redis는 메모리 기반이라 조회가 빠르다 DB 접근 없이 상태 확인이 가능하다&lt;/p&gt;
&lt;p data-ke-size=&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;b&gt;Momentix의 실제 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Momentix에서는 인증 상태를 3개로 나눴다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1) 인증 코드 (5분 유지)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key: momentix:email:code:{email}&lt;/li&gt;
&lt;li&gt;Value: 6자리 코드&lt;/li&gt;
&lt;li&gt;TTL: 300초&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력해야 하는 코드다 5분 후 자동 삭제된다 코드가 맞으면 즉시 삭제해서 재사용을 막는다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2) 검증 토큰 (15분 유지)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key: momentix:email:verified:{token}&lt;/li&gt;
&lt;li&gt;Value: email&lt;/li&gt;
&lt;li&gt;TTL: 900초&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 통과하면 UUID 기반 토큰을 발급한다 이 토큰은 이 이메일은 방금 인증을 완료했다는 증명 역할이다 회원가입 API는 이 토큰을 Redis에서 확인한다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3) 재전송 쿨다운 (45초)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Key: momentix:email:cooldown:{email}&lt;/li&gt;
&lt;li&gt;TTL: 45초&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 키가 존재하면 인증 코드 재요청을 막는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 메일 폭탄 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 남발 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[깨알정리] 5분/15분/45초 시간 설정 기준 정리&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style9&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;항목&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;TTL&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;기준&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;인증 코드&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;5분&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;사용자 입력 시간 + 보안 고려&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;검증 토큰&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;15분&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;가입 폼 작성 시간 고려&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;쿨다운&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;45초&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;남발 방지 + 사용자 불편 최소화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[깨알정리]&amp;nbsp;&lt;/b&gt;&lt;b&gt;왜 인증상태를 3개로 나누었는가?&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style9&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;구분&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;목적&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;분리한 이유&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;인증 코드&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;사용자가 직접 입력해야 하는 비밀값&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;보안 민감도가 높고 1회성 처리 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;검증 토큰&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;코드 통과 이후 상태 유지&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;가입 폼 작성 등 후속 단계 연결 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;쿨다운&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;재요청 빈도 제한&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;인증 로직과 목적이 완전히 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;이 설계의 핵심&lt;/b&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;인증 단계가 명확히 분리된다&lt;/li&gt;
&lt;li&gt;남발 방지가 간단해진다&lt;/li&gt;
&lt;li&gt;서버가 여러 대여도 상태 공유가 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Momentix에서는 이메일 인증을 영구 데이터가 아니라 시간이 지나면 사라져야 하는 상태로 정의했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Redis + TTL을 사용했다&lt;/p&gt;</description>
      <category>Backend</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/295</guid>
      <comments>https://winwin0219.tistory.com/295#entry295comment</comments>
      <pubDate>Wed, 11 Feb 2026 10:49:32 +0900</pubDate>
    </item>
    <item>
      <title>[부하테스트] GET /board p95 1.66s, 이게 정상일까? (&amp;rarr; 369ms)</title>
      <link>https://winwin0219.tistory.com/294</link>
      <description>&lt;h1&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;RPS.png&quot; data-origin-width=&quot;1623&quot; data-origin-height=&quot;325&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIaTFp/dJMcafZNvLb/xKZ8HduHfb8zbHq19TslLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIaTFp/dJMcafZNvLb/xKZ8HduHfb8zbHq19TslLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIaTFp/dJMcafZNvLb/xKZ8HduHfb8zbHq19TslLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIaTFp%2FdJMcafZNvLb%2FxKZ8HduHfb8zbHq19TslLk%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;1623&quot; height=&quot;325&quot; data-filename=&quot;RPS.png&quot; data-origin-width=&quot;1623&quot; data-origin-height=&quot;325&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreBoard에 게시글 더미 데이터 10만 건을 생성한 뒤 GET /board(목록 조회) API에 k6로 부하테스트를 수행했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 p95 응답 시간이 약 1.66s까지 상승했고 내가 설정한 기준인 p95&amp;lt;500ms Threshold를 통과하지 못했다&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;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;부하테스트실패.png&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVI1zO/dJMcaibbvrO/ztNKdWzFptFjdOPNzD7h9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVI1zO/dJMcaibbvrO/ztNKdWzFptFjdOPNzD7h9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVI1zO/dJMcaibbvrO/ztNKdWzFptFjdOPNzD7h9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVI1zO%2FdJMcaibbvrO%2FztNKdWzFptFjdOPNzD7h9k%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;368&quot; height=&quot;239&quot; data-filename=&quot;부하테스트실패.png&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 평균(ms)을 안 보고 p95를 보는가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균은 몇 개의 엄청 느린 요청(꼬리 지연) 때문에 체감이랑 안 맞을 때가 많다 p95는 대부분 유저가 겪는 체감 + 느린 꼬리를 같이 보기에 좋음&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h1&gt;&lt;b&gt;원인 분석 &amp;mdash; DB EXPLAIN 확인하기&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, GET /board 에서 실행되는 SQL이 DB에서 어떤 방식으로 처리되고 있는지 확인한다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1) 인덱스 현황 확인&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SHOW INDEX FROM board;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 결과, board 테이블에는 PRIMARY KEY(id)만 존재했고 ORDER BY title에 사용되는 title 컬럼에는 인덱스가 없었다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;showIndex.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dh7qkH/dJMcac9N1dD/n3RkkzyXFbPFtCK5vPqEok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dh7qkH/dJMcac9N1dD/n3RkkzyXFbPFtCK5vPqEok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dh7qkH/dJMcac9N1dD/n3RkkzyXFbPFtCK5vPqEok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh7qkH%2FdJMcac9N1dD%2Fn3RkkzyXFbPFtCK5vPqEok%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;1280&quot; height=&quot;122&quot; data-filename=&quot;showIndex.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2) 목록 조회 쿼리 EXPLAIN&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;EXPLAIN
select ...
from board
order by title
limit 0, 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EXPALIN 결과는 다음과 같았다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;type = ALL &amp;rarr; 테이블 전체 스캔&lt;/li&gt;
&lt;li&gt;key = NULL &amp;rarr; 인덱스 사용 못 함&lt;/li&gt;
&lt;li&gt;rows = 100,000 &amp;rarr; 매 요청마다 10만 건 조회&lt;/li&gt;
&lt;li&gt;Extra = Using filesort &amp;rarr; 인덱스로 정렬하지 못해 filesort 발생&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 10개만 가져오는 요청인데도 DB가 매번 10만 건을 읽고 정렬하고 있었고 이 구조가 부하 상황에서 p95를 크게 끌어올리고 있었다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;개선전 EXPLAIN.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qOgg4/dJMcaflc6ki/CAYaNJ2ec3g2nDDiEeRxkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qOgg4/dJMcaflc6ki/CAYaNJ2ec3g2nDDiEeRxkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qOgg4/dJMcaflc6ki/CAYaNJ2ec3g2nDDiEeRxkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqOgg4%2FdJMcaflc6ki%2FCAYaNJ2ec3g2nDDiEeRxkk%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;1280&quot; height=&quot;124&quot; data-filename=&quot;개선전 EXPLAIN.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;124&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;h1&gt;&lt;b&gt;개선 방법 &amp;mdash; 정렬 인덱스 추가&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 핵심은 ORDER BY title 정렬이 인덱스를 전혀 활용하지 못하고 있다는 점이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 title 기준 정렬을 인덱스로 처리할 수 있도록 다음과 같이 복합 인덱스를 추가했다&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE INDEX idx_board_title_id ON board (title, id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;title &amp;mdash; 정렬 대상 컬럼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id &amp;mdash; 동일 title 값이 많을 때 정렬 안정성 확보용&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱스 적용 후 EXPLAIN&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 추가 후 동일한 쿼리를 다시 확인했다&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;EXPLAIN
select ...
from board
order by title
limit 0, 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 명확하게 달랐다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;key = idx_board_title_id &amp;rarr; 인덱스 사용&lt;/li&gt;
&lt;li&gt;rows = 10 &amp;rarr; 필요한 만큼만 조회&lt;/li&gt;
&lt;li&gt;Using filesort 사라짐&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;개선후EXPAIN.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNJ5FN/dJMcabC7OtG/CN4mZfQMVioRGCTSBpfWMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNJ5FN/dJMcabC7OtG/CN4mZfQMVioRGCTSBpfWMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNJ5FN/dJMcabC7OtG/CN4mZfQMVioRGCTSBpfWMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNJ5FN%2FdJMcabC7OtG%2FCN4mZfQMVioRGCTSBpfWMk%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;1280&quot; height=&quot;223&quot; data-filename=&quot;개선후EXPAIN.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;개선 결과 &amp;mdash; 부하테스트 재실행&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 조건으로 k6 부하테스트를 다시 실행했다&lt;/p&gt;
&lt;p 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-filename=&quot;개선결과.png&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3Y7pf/dJMcahDjlON/WDCL5UKaYCkUd7ihjqbrqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3Y7pf/dJMcahDjlON/WDCL5UKaYCkUd7ihjqbrqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3Y7pf/dJMcahDjlON/WDCL5UKaYCkUd7ihjqbrqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3Y7pf%2FdJMcahDjlON%2FWDCL5UKaYCkUd7ihjqbrqk%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;557&quot; height=&quot;361&quot; data-filename=&quot;개선결과.png&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;p95: 1.66s &amp;rarr; 369ms&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Threshold(p95 &amp;lt; 500ms) 통과&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 에러 0%&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/294</guid>
      <comments>https://winwin0219.tistory.com/294#entry294comment</comments>
      <pubDate>Tue, 10 Feb 2026 20:54:21 +0900</pubDate>
    </item>
    <item>
      <title>[부하테스트-1] k6 스모크 테스트: /board 200 확인 + Grafana로 요청 유입 검증</title>
      <link>https://winwin0219.tistory.com/292</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 스모크 테스트를 먼저 했는가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부하테스트를 바로 돌리기 전에 요청이 서버에 정상 유입되고 응답이 200으로 떨어지는지 먼저 확인했다 이 단계가 없으면 부하테스트 결과가 이상해도 &lt;b&gt;서버 문제인지 / 테스트 스크립트 문제인지 / 모니터링 연결 문제인지 구분이 안 되기 때문&lt;/b&gt;이다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;k6 스모크 테스트 실행&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/board API가 200으로 정상 응답하는지 확인&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;import http from &quot;k6/http&quot;;
import { check, sleep } from &quot;k6&quot;;

export const options = {
  stages: [
    { duration: &quot;10s&quot;, target: 10 },
    { duration: &quot;40s&quot;, target: 10 },
    { duration: &quot;10s&quot;, target: 0 },
  ],
  thresholds: {
    http_req_failed: [&quot;rate&amp;lt;0.01&quot;],
  },
};

const BASE_URL = &quot;http://3.38.144.47:8080&quot;;

export default function () {
  const res = http.get(`${BASE_URL}/board?page=0&amp;amp;size=10`);
  check(res, { &quot;status is 200&quot;: (r) =&amp;gt; r.status === 200 });
  if (res.status !== 200) console.log(`status=${res.status}, url=${res.url}, body=${res.body?.slice(0, 120)}`);

  sleep(1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기대결과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http_req_faild가 0%에 가까움&lt;/li&gt;
&lt;li&gt;상태코드가 200으로만 떨어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/busDh5/dJMcafMiL8B/4O9IYEYBmdvkGFInnVvyk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/busDh5/dJMcafMiL8B/4O9IYEYBmdvkGFInnVvyk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/busDh5/dJMcafMiL8B/4O9IYEYBmdvkGFInnVvyk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbusDh5%2FdJMcafMiL8B%2F4O9IYEYBmdvkGFInnVvyk1%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;1920&quot; height=&quot;1080&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Grafana&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k6를 실행한 시간대에 맞춰 Grafana Drilldown에서 확인했다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 요청이 실제로 유입됐나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지표 : http_server_request_seconds_count&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미 : 서버가 받은 HTTP 요청 수 (카운트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; k6 실행 구간에 그래프가 튀면 요청 유입이 확인된다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;325&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YFTuP/dJMcahJ7dqn/rwZYt3vjmYNlWg5G4tsjgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YFTuP/dJMcahJ7dqn/rwZYt3vjmYNlWg5G4tsjgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YFTuP/dJMcahJ7dqn/rwZYt3vjmYNlWg5G4tsjgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYFTuP%2FdJMcahJ7dqn%2FrwZYt3vjmYNlWg5G4tsjgk%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;1280&quot; height=&quot;325&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;325&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;h3 data-ke-size=&quot;size23&quot;&gt;2) 내가 떄린 엔드포인트가 맞나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Breakdown &amp;rarr; uri&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미 : 어떤 URL로 요청이 들어왔는지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 : /board가 잡히면 OK&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 : /actuator/prometer 가 같이 보이는데 이건 Prometheus가 메트릭 수집하느라 호출하는 관측용 트래픽이다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OYXOd/dJMcahJ7dmJ/Ts2Ym3VCYJIBRyNkLlltck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OYXOd/dJMcahJ7dmJ/Ts2Ym3VCYJIBRyNkLlltck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OYXOd/dJMcahJ7dmJ/Ts2Ym3VCYJIBRyNkLlltck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOYXOd%2FdJMcahJ7dmJ%2FTs2Ym3VCYJIBRyNkLlltck%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;1280&quot; height=&quot;552&quot; data-filename=&quot;img1.daumcdn.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 결과가 정상인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Breakdown &amp;rarr; status&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;확인 : 200만 있으면 정상&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;519&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HPtJi/dJMcacINRJF/TK4FSvHkuv6cWHXXfrPzik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HPtJi/dJMcacINRJF/TK4FSvHkuv6cWHXXfrPzik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HPtJi/dJMcacINRJF/TK4FSvHkuv6cWHXXfrPzik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHPtJi%2FdJMcacINRJF%2FTK4FSvHkuv6cWHXXfrPzik%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;1280&quot; height=&quot;519&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;519&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;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Breakdown &amp;rarr; outcome&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;의미 : 결과 분류(SUCCESS/CLIENT_ERROR/SERVER_ERROR)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;확인 : SUCCESS만 있으면 정상&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J1NQ5/dJMcacINRJJ/um9dFKhfBzkMZW94S6upW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J1NQ5/dJMcacINRJJ/um9dFKhfBzkMZW94S6upW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J1NQ5/dJMcacINRJJ/um9dFKhfBzkMZW94S6upW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ1NQ5%2FdJMcacINRJJ%2Fum9dFKhfBzkMZW94S6upW1%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;1280&quot; height=&quot;632&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;632&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k6 스모크 태스트로 /board가 200으로 정상 응답했고 Grafana에서 요청 유입(요청량), URI(/board), 결과(200/SUCCESS)까지 확인했다 이제 다음 단계를 진행해도 된다&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/292</guid>
      <comments>https://winwin0219.tistory.com/292#entry292comment</comments>
      <pubDate>Tue, 10 Feb 2026 12:18:38 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble Shooting] Prometheus + Grafana (EC2) 2</title>
      <link>https://winwin0219.tistory.com/290</link>
      <description>&lt;h2 style=&quot;text-align: right;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 막혔던 포인트 &amp;mdash;&lt;br /&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Target이 unknown이었던 이유 &lt;/span&gt;&lt;/b&gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 상황&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus 컨테이너는 정상적으로 실행 중이었다&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;$ docker ps
CONTAINER ID   IMAGE              STATUS
af86f99da74a   prom/prometheus    Up
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus UI도 정상 접근 가능했다&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://&amp;lt;EC2_PUBLIC_IP&amp;gt;:9090
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 애플리케이션 역시 실행 중이었고, Actuator endpoint도 직접 호출하면 정상 응답을 반환했다&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;$ curl &amp;lt;http://127.0.0.1:8080/actuator/prometheus&amp;gt;
# HELP application_ready_time_seconds ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 /targets 화면에서는 상태가 달랐다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;State: UNKNOWN
Last scrape: 0001-01-01T00:00:00Z
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prometheus 서버는 살아 있음&lt;/li&gt;
&lt;li&gt;Spring Boot도 살아 있음&lt;/li&gt;
&lt;li&gt;/actuator/prometheus도 열림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그런데 Prometheus는 긁지 못하고 있음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;가설 4가지&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;가설 1. Spring Boot가 실제로 안 떠 있는 것 아닐까?&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브라우저 / curl 모두 정상 응답 &amp;rarr; 탈락&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus의 scrape 대상은 단순히 프로세스가 떠 있는지가 아니라 지정한 endpoint가 실제로 응답을 반환하는지 여부다 이미 /actuator/prometheus가 curl 기준으로 정상적인 메트릭 텍스트를 반환하고 있었기 때문에 SpringBoot 자체가 내려가 있었을 가능성은 배제하였다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;가설 2. Actuator / Micrometer 설정 문제?&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;/actuator/prometheus가 이미 메트릭을 출력 중 &amp;rarr; 탈락&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actuator나 Micrometer 설정 문제였다면, Prometheus가 scrape를 시도했을 때 HTTP 404, 403 혹은 매트릭 포맷 오류가 발생했을 것이다 히지만 /actuator/prometheus endpoint는&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;Micrometer 지표들도 함께 노출되고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 지표 생성 단계의 문제라기 보단 Prometheus가 해당 endpoint에 도달하지 못하고 있을 가능성이 더 높다고 판단했다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;가설 3. 보안 그룹 문제?&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Prometheus는 같은 EC2 내부에서 접근 (외부 노출 포트와 무관) &amp;rarr; 탈락&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 외부에서 StringBoot로 접근하는 구조가 아니라 같은 EC2 내부에서 애플리케이션을 scrape 하는 구조다 즉 이 시점의 문제는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 &amp;rarr; 내부 통신 문제가 아님&lt;/li&gt;
&lt;li&gt;내부 &amp;rarr; 내부 통신 문제였음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;가설 4. Prometheus가 실제로 scrape를 시도했는지 확인이 필요하다&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI의 unknown 상태만으로는 왜 실패했는지 알 수 없다 그래서 &lt;b&gt;Prometheus의 내부 상태 API를 직접 확인&lt;/b&gt;했다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;/api/v1/targets 를 본 이유&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 scrape 결과를 내부 API로 노출한다&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ curl &amp;lt;http://localhost:9090/api/v1/targets&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 내가 집중해서 본 필드는 아래 네 가지다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&quot;lastError&quot; -&amp;gt; Prometheus 내부 기준으로 이 target을 정상(up)으로 판단하는지 여부
&quot;lastScrape&quot; -&amp;gt; 실제 scrape가 수행된 적 있는지, 있었다면 언제였는지 확인하기 위함
&quot;lastScrapeDuration&quot; -&amp;gt; scrape 시도가 실제로 실행되었는지, 혹은 네트워크 단계에서 막혔는지 판단하기 위함
&quot;health&quot;-&amp;gt; scrape 실패의 직접적인 원인 메시지를 확인하기 위함&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이 네 가지를 통해 Prometheus가 시도조차 하지 않앗는지 시도는 했지만 어디에서 실패했는지를 구분하려고 했다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;결과 &amp;rarr; 문제 시점&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;lastError&quot;: &quot;dial tcp 127.0.0.1:8080: connect: connection refused&quot;
&quot;lastScrape&quot;: &quot;0001-01-01T00:00:00Z&quot;
&quot;health&quot;: &quot;down&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;b&gt;핵심 단서 &amp;rarr; lastError 값을 보고&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;HTTP 요청 단계까지 가지도 못했고 TCP 연결 자체가 성립되지 않았다는 뜻이다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prometheus 설정 파일이 잘못 로드되었거나 endpoint 경로가 틀린 문제가 아님&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;네트워크 레벨에서 해당 주소에 접근할 수 없다는 의미 ㅇㅇ&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일이 안 먹었다가 아니라 네트워크 관점에서 접근 자체가 실패 하고 있었다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;컨테이너 안에서의 127.0.0.1은 누구인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus 컨테이너에서 보는 127.0.0.1은 EC2의 127.0.0.1과 같은가???&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 답은 NO&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot &amp;rarr; EC2 호스트에서 8080 포트로 실행 중&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus &amp;rarr; Docker 컨테이너 내부에서 실행 중&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Prometheus 컨테이너 기준 127.0.0.1 = Prometheus 컨테이너 자신&lt;br /&gt;그래서 Prometheus 입장에서는&lt;br /&gt;127.0.0.1:8080 &amp;rarr; 컨테이너 내부에 8080 서버가 없음 &amp;rarr; connection refused&lt;/blockquote&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[깨알공유] 컨테이너 안에서의 127.0.0.1&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus target이 unknown일 때 설정 파일부터 의심하기 보다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lastError가 네트워크 에러인지부터 확인하는게 훨씬 빠르다&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;해결 2가지&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;b&gt;방법 1. Docker 네트워크 브리지 + 호스트 IP 사용&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2 사설 IP 지정&lt;/li&gt;
&lt;li&gt;Docker 네트워크 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;방법 2. Prometheus를 host 네트워크로 실행 V&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너가 &lt;b&gt;호스트 네트워크를 그대로 사용&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; [&lt;/b&gt;&lt;b&gt;방법 2 채택 이유&lt;/b&gt;&lt;b&gt;]&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재목표는 네트워크 개념을 단순화한 상태에서 Prometheus와 SpringBoot의 연결 관계를 명확히 이해하는 것이었기 때문이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브리지 네트워크 + 사설 IP 방식은 더 유연하고 실무에서는 더 자주 사용되지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 모니터닝 구조를 이해하는 단계에서는 컨테이너가 곧 호스트처럼 동작한다는 가정이 문제 원인을 추적하는 데 훨씬 명확하다고 판단하였다&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;적용한 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus 실행 방식 변경&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;docker run -d \\
  --name prometheus \\
  --net=host \\
  -v ~/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml \\
  prom/prometheus
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 doker-compose 기준&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;services:
  prometheus:
    image: prom/prometheus
    network_mode: host
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;상태 변화 확인&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재기동 후 다시 확인했다&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ curl &amp;lt;http://localhost:9090/api/v1/targets&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;lastError&quot;: &quot;&quot;
&quot;lastScrape&quot;: &quot;2026-02-09T03:12:12Z&quot;
&quot;health&quot;: &quot;up&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/290</guid>
      <comments>https://winwin0219.tistory.com/290#entry290comment</comments>
      <pubDate>Mon, 9 Feb 2026 13:06:09 +0900</pubDate>
    </item>
    <item>
      <title>Prometheus + Grafana (EC2) 1</title>
      <link>https://winwin0219.tistory.com/289</link>
      <description>&lt;h2 style=&quot;text-align: right;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 전체 구조와 연결 흐름&lt;/b&gt;&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;CI/CD 배포까지 끝났지만, 서버가 실제로 어떻게 동작하는지 알 수 없었다 CPU때문인지, 메모리때문인지, API 때문인지 감이 아니라 지표를 보고 싶었다&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Spring Boot (Actuator)
 &amp;rarr; /actuator/prometheus
 &amp;rarr; Prometheus (scrape)
 &amp;rarr; Grafana (visualize)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 보는 도구가 아니라 긁어오는 서버다 Grafana는 Prometheus를 대신 조회하는 화면이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SpringBoot 설정&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Actuator 의존성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus'
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/actuator/prometheus 노출&lt;/li&gt;
&lt;li&gt;Micrometer가 중간에서 지표를 변환해줌&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;설정에서 막혔던 포인트&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;docker-compose.monitoring.yml 위치 문제&lt;/li&gt;
&lt;li&gt;컨테이너 안에서 127.0.0.1의 의미&lt;/li&gt;
&lt;li&gt;target가 unknown &amp;rarr; up으로 바뀌는 과정&lt;/li&gt;
&lt;li&gt;/api/v1/targets로 상태 확인한 이유&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;보안그룹&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;- 9090: Prometheus
- 3000: Grafana

Prometheus는 서버 내부 수집용,
Grafana만 외부 접근이 필요했다.
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 애플리케이션과 같은 서버 내부에서만 통신하므로 외부에 노출할 필요가 없었다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트를 열어서 접근하면 아래 사진처럼 뜬다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;971&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blhYYY/dJMcaiIXXxm/NPwLaSh1KjzBigJbgygUq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blhYYY/dJMcaiIXXxm/NPwLaSh1KjzBigJbgygUq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blhYYY/dJMcaiIXXxm/NPwLaSh1KjzBigJbgygUq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblhYYY%2FdJMcaiIXXxm%2FNPwLaSh1KjzBigJbgygUq1%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;1920&quot; height=&quot;971&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;971&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;205&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1q5ZR/dJMcachI0Mv/F6OxoD6HVKCamvA1ld3h01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1q5ZR/dJMcachI0Mv/F6OxoD6HVKCamvA1ld3h01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1q5ZR/dJMcachI0Mv/F6OxoD6HVKCamvA1ld3h01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1q5ZR%2FdJMcachI0Mv%2FF6OxoD6HVKCamvA1ld3h01%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;1920&quot; height=&quot;205&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;205&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;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/289</guid>
      <comments>https://winwin0219.tistory.com/289#entry289comment</comments>
      <pubDate>Mon, 9 Feb 2026 12:29:08 +0900</pubDate>
    </item>
    <item>
      <title>[IDE] Coverage Gutters - VSCode에서 Spring Boot(JaCoCo) 커버리지 초록/빨강으로 표시하는 법</title>
      <link>https://winwin0219.tistory.com/288</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot + Gradle 프로젝트에서 텍스트 커버리지를 확인하려고 JaCoCo HTML 리포트를 열어보면 브라우저로 들어가야 하고 파일 이동도 불편해서 개발 중에 꽤 스트레스였다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 VSCode 안에서 바로 커버리지를 초록/빨강으로 표시하는 방법을 정리했다&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;VSCode 코드 왼쪽 줄 번호 옆(gutter)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;  초록: 커버된 라인&lt;/li&gt;
&lt;li&gt;  빨강: 커버 안 된 라인&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;준비물&lt;/b&gt;&lt;br /&gt;Gradle 기반 Spring Boot 프로젝트&lt;br /&gt;JaCoCo 사용 가능 상태(대부분 기본 세팅으로 가능)&lt;br /&gt;VSCode 확장:&amp;nbsp;Coverage Gutters&amp;nbsp;&lt;/blockquote&gt;
&lt;h1&gt;&lt;b&gt;1. JaCoco 커버리지 리포트(XML) 생성하기&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트에서 실행&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;./gradlew test jacocoTestReport
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성되면 아래 루트대로 파일이 있어야 한다&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;build/reports/jacoco/test/jacocoTestReport.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML이 아니라 XML 파일을 Coverage Gutters가 읽는다&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;2. VSCode 확장 설치&lt;/b&gt;&lt;/h1&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;VSCode &amp;rarr; Extensions (Ctrl + Shift + X)&lt;/li&gt;
&lt;li&gt;Coverage Gutters 검색&lt;/li&gt;
&lt;li&gt;설치 후 Reload가 뜨면 Reload&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;&lt;b&gt;3. settings.json에 커버리지 파일 경로 등록&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VSCode 설정은 탐색기에서 파일을 찾는 게 아니라, 명령으로 열어야 빠르다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;settings.json 여는 법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Ctrl + Shift + P&lt;/li&gt;
&lt;li&gt;Open Settings (JSON) 입력&lt;/li&gt;
&lt;li&gt;Enter&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열린 JSON에 아래 설정을 추가한다:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;coverage-gutters.coverageFileNames&quot;: [
    &quot;build/reports/jacoco/test/jacocoTestReport.xml&quot;
  ],
  &quot;coverage-gutters.showLineCoverage&quot;: true
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 저장:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Ctrl + S&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 적용을 위해 VSCode 리로드:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Ctrl + Shift + P &amp;rarr; Reload Window&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;4. 커버리지 표시 실행&lt;/b&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Ctrl + Shift + P&lt;/li&gt;
&lt;li&gt;coverage 입력&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Coverage Gutters: Display Coverage&lt;/b&gt; 클릭&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 코드 왼쪽 여백에 초록/빨강이 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAEbIw/dJMcahJ6NH4/uk7qRfk3bnnsbxEHR05q41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAEbIw/dJMcahJ6NH4/uk7qRfk3bnnsbxEHR05q41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAEbIw/dJMcahJ6NH4/uk7qRfk3bnnsbxEHR05q41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAEbIw%2FdJMcahJ6NH4%2Fuk7qRfk3bnnsbxEHR05q41%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;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Backend/  Test</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/288</guid>
      <comments>https://winwin0219.tistory.com/288#entry288comment</comments>
      <pubDate>Mon, 9 Feb 2026 11:40:41 +0900</pubDate>
    </item>
    <item>
      <title>[Trouble Shooting] CI/CD 구축 중 JAR 배포 반복 실패</title>
      <link>https://winwin0219.tistory.com/287</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 시도한 목표와 초기 상태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트를 GitHub Actions로 빌드한 뒤 EC2 서버에 JAR 파일을 배포하고 systemd로 실행하는 CI/CD를 구성하려고 했다 의도한 전체 흐름은 단순했다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GitHub push&lt;/li&gt;
&lt;li&gt;Actions에서 Gradle build&lt;/li&gt;
&lt;li&gt;생성된 JAR를 EC2로 복사&lt;/li&gt;
&lt;li&gt;서버에서 app.jar로 교체&lt;/li&gt;
&lt;li&gt;서비스 재시작&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 &lt;b&gt;빌드 이후 배포 단계에서 계속 실패&lt;/b&gt;했다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 첫 번째 벽 &amp;mdash; 서버에 JAR가 없다는 에러&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 워크플로우를 작성했을 때, 배포 단계에서 다음 에러가 발생했다&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;ls: cannot access '*-SNAPSHOT.jar': No such file or directory
mv: cannot stat '': No such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이 에러가 의미하는 것&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드는 끝났는데 서버에 접속한 뒤 -SNAPSHOT.jar를 찾지 못했다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, JAR가 아예 없거나 / 있긴 한데 내가 보고 있는 위치에 없다는 뜻이었다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 1차 추론 &amp;mdash; 문제는 빌드가 아니라 전송 이후&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 전제 하나를 확인했다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub Actions 로그에서 build/libs/*.jar 생성은 정상&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;빌드는 성공&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 문제 후보를 두 가지로 좁혔다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;후보군 A&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JAR 파일이 서버로 전송되지 않았다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;후보군 B&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JAR 파일은 전송됐지만, 내가 생각한 위치와 다르다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드가 성공했다는 점 때문에 &lt;b&gt;A보다는 B가 더 가능성이 높아 보였다&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 서버 직접 확인 &amp;mdash; 결정적인 단서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD 실패 직후, EC2 서버에 직접 접속해서 파일을 탐색했다&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;find /home/ubuntu/apps/coreboard -type f -name&quot;*.jar&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같았다&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;/home/ubuntu/apps/coreboard/app.jar
/home/ubuntu/apps/coreboard/build/libs/CoreBoard-0.0.1-SNAPSHOT.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 모든 게 설명됐다&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JAR 파일은 분명 서버에 있음&lt;/li&gt;
&lt;li&gt;하지만 &lt;b&gt;/home/ubuntu/apps/coreboard/ 바로 아래가 아니라 build/libs/ 경로를 그대로 포함한 채 업로드&lt;/b&gt;되어 있었다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버에서 실행한&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ls *-SNAPSHOT.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 실패하는 건 &lt;b&gt;당연한 상황&lt;/b&gt;이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;후보군 B가 확정&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 왜 이런 일이 발생했는가 &amp;mdash; scp 전송 방식의 오해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI에서 사용한 설정은 다음과 같은 형태였다&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;source:build/libs/*-SNAPSHOT.jartarget:/home/ubuntu/apps/coreboard
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이 설정을 이렇게 이해하고 있었다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JAR 파일 하나만 target 디렉토리에 떨어질 것이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 동작은 달랐다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 동작&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;build/libs/... &lt;b&gt;경로 구조 자체를 유지한 채&lt;/b&gt; 서버로 복사됨&lt;/li&gt;
&lt;li&gt;결과적으로 서버에는 이런 구조가 생김&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;/home/ubuntu/apps/coreboard/build/libs/CoreBoard-0.0.1-SNAPSHOT.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;CI에서 생각한 경로와 서버에서 가정한 경로가 어긋나 있었다&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 첫 번째 해결 시도 &amp;mdash; 서버 쪽에서 찾게 만들기 (실패)&lt;/h2&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;그럼 서버에서 build/libs까지 내려가서 찾으면 되지 않을까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 SSH 스크립트에서 ls, find 등을 써서 JAR 파일을 동적으로 찾는 방식을 고려했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 곧 문제가 보였다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배포 스크립트가 점점 복잡해짐&lt;/li&gt;
&lt;li&gt;파일 구조가 바뀌면 또 깨질 가능성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI/CD가 추론하는 시스템이 되어버림&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 근본 해결이 아니라고 판단했다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 방향 전환 &amp;mdash; 서버가 아니라 CI에서 고쳐야 한다&lt;/h2&gt;
&lt;p 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;❌ 서버에서 JAR 위치를 추론하게 만들지 말자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;CI 단계에서 결과물의 위치를 고정하자&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 서버는 항상 여기, 이 이름의 파일이 온다고 가정 불확실성은 CI 쪽에서 제거&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 두 번째 해결 &amp;mdash; 경로를 제거하고 파일만 배포&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI 설정에서 &lt;b&gt;경로를 제거하는 옵션&lt;/b&gt;을 적용했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 build/libs/ 경로는 제거되고 JAR 파일만 target 디렉토리에 바로 복사되었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 서버에는 항상 다음 상태가 보장되었다&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/home/ubuntu/apps/coreboard/CoreBoard-0.0.1-SNAPSHOT.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 그런데 또 다른 에러 &amp;mdash; 이번엔 mv가 실패한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 서버에 JAR는 있었다 그런데 이번엔 이런 에러가 발생했다&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;mv: cannot stat 'CoreBoard-0.0.1-SNAPSHOT.jar': No such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다시 추론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서 가능한 후보는 두 가지였다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A. JAR 이름이 내가 생각한 것과 다르다&lt;/li&gt;
&lt;li&gt;B. 현재 작업 디렉토리가 내가 생각한 곳이 아니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 다시 확인해보니 JAR는 build/libs 아래에 존재 SSH 스크립트는 상위 디렉토리에서 실행 중&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번에도 원인은 경로 불일치&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 최종 해결 &amp;mdash; 경로와 이름을 완전히 고정&lt;/h2&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;서버 스크립트에서는 절대 추론하지 않는다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최종적으로 다음과 같이 정리했다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI 단계에서 JAR를 &lt;b&gt;정해진 위치&lt;/b&gt;로 보냄&lt;/li&gt;
&lt;li&gt;서버에서는 &lt;b&gt;정해진 경로의 파일을 그대로 app.jar로 교체&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;mv -f build/libs/CoreBoard-0.0.1-SNAPSHOT.jar app.jarsudo systemctl restart coreboard
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 CI/CD 성공, 서비스 정상 실행, 재시도 없이 안정적으로 배포 가능&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 이 트러블슈팅에서 얻은 핵심 교훈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 본질은 도구 문제가 아니었다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 원인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일 흐름을 추론에 맡긴 구조&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD에서 가장 중요한 건 &lt;b&gt;결과물의 위치와 이름을 고정하는 것&amp;nbsp;&lt;/b&gt;ls, find가 많아질수록 파이프라인은 불안정해진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 대부분 &lt;b&gt;빌드가 아니라, 전송 이후 상태&lt;/b&gt;에서 발생한다&lt;/p&gt;</description>
      <category>CoreBoard</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/287</guid>
      <comments>https://winwin0219.tistory.com/287#entry287comment</comments>
      <pubDate>Sun, 8 Feb 2026 11:58:37 +0900</pubDate>
    </item>
    <item>
      <title>[네트워크] 회선 교환 방식과 패킷 교환 방식</title>
      <link>https://winwin0219.tistory.com/286</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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;회선 교환&lt;/b&gt;은 전화 통화에 최적화된 방식, &lt;br /&gt;&lt;b&gt;패킷 교환&lt;/b&gt;은 인터넷과 서버 통신을 위해 만들어진 방식&lt;/span&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;백엔드 개발자가 다루는 HTTP, TCP/IP, REST, API, DB 통신은 전부 패킷 교환 방식 위에서 동작한다 그래서 이 차이를 이해하지 못하면 지연, 타임아웃, 재시도, 부하, 장애를 감으로만 다루게 된다&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;회선교환 방식(Circuit Switching이란?&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신을 시작하기 전에, 송신자와 수신자 사이의 물리적 경로를 미리 확보하고 통신이 끝날 때까지 그 경로를 독점적으로 사용하는 방식이다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;동작 흐름&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;통신 요청&lt;/li&gt;
&lt;li&gt;중간 교환기들이 하나의 고정 경로를 설정&lt;/li&gt;
&lt;li&gt;통신 시작&lt;/li&gt;
&lt;li&gt;통신 종료 후 회선 해제&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 안정적일까?&lt;/b&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;/li&gt;
&lt;li&gt;대역폭 보장됨&lt;/li&gt;
&lt;li&gt;중간에 다른 트래픽이 끼어들 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 지연(Iatency)과 품질(QoS)이 예측 가능하다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;단점이 치명적인 이유&lt;/b&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;/li&gt;
&lt;li&gt;사용하지 않는 시간도 자원 낭비됨&lt;/li&gt;
&lt;li&gt;동시에 많은 사용자를 수용하기 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;대표 사례&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유선 전화망 (PSTN)&lt;/li&gt;
&lt;li&gt;초기 이동 통신 음성 통화&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;패킷 교환 방식(Packet Switching)이란?&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷 교환 방식은 데이터를 작은 단위(패킷)로 나누어 보내고 각 패킷이 독립적으로 최적의 경로를 찾아 이동하는 방식이다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;동작 흐름&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;메시지를 여러 패킷으로 분할&lt;/li&gt;
&lt;li&gt;각 패킷에 목적지 정보(헤더) 추가&lt;/li&gt;
&lt;li&gt;라우터가 패킷마다 최적 경로 선택&lt;/li&gt;
&lt;li&gt;목적지에서 패킷 재조립&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 효율적인가?&lt;/b&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;/li&gt;
&lt;li&gt;여러 사용자가 하나의 네트워크를 공유 가능&lt;/li&gt;
&lt;li&gt;특정 경로가 막혀도 우회 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 인터넷이 대규모로 확장될 수 있다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;단점&lt;/b&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;/li&gt;
&lt;li&gt;패킷 순서 뒤바뀜 가능&lt;/li&gt;
&lt;li&gt;경로 탐색으로 인한 지연 발생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;회선 교환 VS 패킷 교확 비교&lt;/b&gt;&lt;/h1&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 132px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;b&gt; 구분&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;b&gt; 회선 교환 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;패킷 교환&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;경로&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;통신 전 고정&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;패킷마다 유동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;자원 사용&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;항상 점유&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;전송 시만 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;안정성&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;매우 높음&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;상대적으로 낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;확장성&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;매우 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;대표 예&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;전화망&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&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;</description>
      <category>Backend/  Network</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/286</guid>
      <comments>https://winwin0219.tistory.com/286#entry286comment</comments>
      <pubDate>Fri, 6 Feb 2026 22:49:58 +0900</pubDate>
    </item>
    <item>
      <title>[React]리액트에서 성능 최적화를 위해 적용할 수 있는 방법들</title>
      <link>https://winwin0219.tistory.com/285</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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;useMemo, useCallback, Code Splitting &lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 애플리케이션이 커질수록 화면이 느려지거나, 불필요하게 다시 렌더링되는 문제가 발생한다 성능 최적화는 이런 문제를 느낌이 아니라 &lt;b&gt;구조적으로 줄이기 위한 작업&lt;/b&gt;이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 성능 이슈는 보통 다음 세 가지 상황에서 발생한다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;필요 없는 &lt;b&gt;리렌더링이 반복되는 경우&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;한 번 렌더링할 때 &lt;b&gt;계산이나 DOM 작업이 너무 무거운 경우&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;초기 로딩 시 &lt;b&gt;번들 크기가 커서 화면이 늦게 뜨는 경우&lt;/b&gt;&lt;/li&gt;
&lt;/ol&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 불필요한 리렌더링 줄이기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React.memo를 활용한 컴포넌트 메모이제이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서 컴포넌트는 부모가 리렌더링되면 함께 다시 렌더링된다 하지만 &lt;b&gt;props가 실제로 변경되지 않았는데도&lt;/b&gt; 자식 컴포넌트가 계속 렌더링되는 경우가 많다 이때 사용할 수 있는 것이 React.memo이다 React.memo는 컴포넌트를 감싸서 &lt;b&gt;이전 props와 현재 props를 비교한 뒤&lt;/b&gt;, 변경되지 않았다면 &lt;b&gt;이전 렌더 결과를 그대로 재사용&lt;/b&gt;한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법은 특히 다음과 같은 경우에 효과적이다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모 컴포넌트는 자주 렌더링되지만 자식 컴포넌트의 props는 거의 바뀌지 않고 자식 컴포넌트의 렌더링 비용이 큰 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 React.memo는 &lt;b&gt;props의 값이 같아 보이는지&lt;/b&gt;가 아니라 &lt;b&gt;참조(reference)가 같은지&lt;/b&gt;를 기준으로 판단한다 그래서 매번 새 객체나 새 함수를 props로 전달하면 메모이제이션 효과가 사라진다&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;useCallback으로 함수 재생성 방지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트 컴포넌트는 렌더링될 때마다 내부에 선언된 함수도 새로 만들어진다 이 함수가 자식 컴포넌트의 props로 전달되면, 자식 입장에서는 props가 변경된 것으로 인식된다 useCallback은 이 문제를 해결하기 위한 훅이다 useCallback을 사용하면 함수 자체를 메모이제이션하여 &lt;b&gt;의존성이 변경되지 않는 한 같은 함수 참조를 유지&lt;/b&gt;할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 React.memo로 감싼 자식 컴포넌트가 불필요하게 리렌더링되는 것을 막을 수 있다 중요한 점은 useCallback의 목적이 함수를 빠르게 만드는 것이 아니라 &lt;b&gt;리렌더링의 원인이 되는 참조 변화를 막는 것&lt;/b&gt;이라는 점이다&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;상태를 어디에 두느냐를 조정하는 것&lt;/b&gt;이다 상태가 상위 컴포넌트에 있으면 그 상태가 변경될 때마다 하위 컴포넌트들이 전부 리렌더링된다 해결 방법은 간단하다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 상태가 필요한 컴포넌트 가까이에 상태를 두기&lt;/li&gt;
&lt;li&gt;공통 상태라면, 실제로 필요한 컴포넌트만 영향을 받도록 분리하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 추가적인 최적화 도구 없이도 리렌더링 범위를 크게 줄일 수 있다&lt;/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. 렌더링 비용 자체를 줄이기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;useMemo로 값의 재계산 방지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 내부에서 정렬, 필터링, 변환처럼 계산 비용이 큰 작업을 수행하는 경우가 있다 이런 계산은 렌더링이 발생할 때마다 반복되는데, 입력 값이 바뀌지 않았다면 같은 결과를 다시 계산할 필요는 없다 useMemo는 이러한 &lt;b&gt;계산 결과를 메모이제이션&lt;/b&gt;하여 의존성이 변경되지 않는 한 이전 결과를 재사용한다 다만 모든 계산에 useMemo를 사용하는 것은 바람직하지 않다 계산 비용이 크지 않다면, 메모이제이션 자체가 오히려 부담이 될 수 있다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key 값을 안정적으로 부여하지 않으면 리액트가 기존 DOM을 재사용하지 못하고 불필요한 렌더링이 발생한다&lt;/li&gt;
&lt;li&gt;긴 리스트를 한 번에 렌더링하면 DOM 노드 수가 급격히 증가한다&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;3. 초기 로딩 속도 개선하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 스플리팅(Code Splitting)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 커질수록 초기 로딩 시 다운로드해야 할 자바스크립트 파일도 커진다 코드 스플리팅은 애플리케이션을 여러 개의 작은 청크로 나누어 &lt;b&gt;실제로 필요한 코드만 그 시점에 로드&lt;/b&gt;하는 방식이다 리액트에서는 React.lazy와 Suspense를 사용해 컴포넌트를 동적으로 로드할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법을 적용하면 초기 로딩 속도를 크게 개선할 수 있다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 성능 최적화의 전제는 측정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 최적화는 추측으로 하면 실패한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 컴포넌트가 자주 렌더링되는지&lt;/li&gt;
&lt;li&gt;왜 렌더링이 발생하는지&lt;/li&gt;
&lt;li&gt;실제로 시간이 오래 걸리는 지점이 어디인지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 정보는 &lt;b&gt;측정 도구를 통해 확인한 뒤&lt;/b&gt; 판단해야 한다 불필요한 최적화는 코드 복잡도만 높이고 실제 성능 개선 효과는 없을 수 있다&lt;/p&gt;</description>
      <category>Frontend</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/285</guid>
      <comments>https://winwin0219.tistory.com/285#entry285comment</comments>
      <pubDate>Fri, 6 Feb 2026 22:39:33 +0900</pubDate>
    </item>
    <item>
      <title>[React] 리액트의 props와 state</title>
      <link>https://winwin0219.tistory.com/284</link>
      <description>&lt;h1&gt;props&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;props는 부모 컴포넌트가 자식 컴포넌트에 전달하는 데이터다 읽기 전용으로, 자식 컴포넌트는 props를 수정할 수 없다&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function ChildComponent(props){
	props.name = &quot;new name&quot;;
	return &amp;lt;div&amp;gt;{props.name}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 컴포넌트 간의 데이터 흐름을 예측 가능하게 만들고 컴포넌트의 재사용성을 높인다&lt;/p&gt;
&lt;h1&gt;state&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;state는 컴포넌트 내부에서 관리되는 데이터다 동적으로 변경될 수 있으며 컴포넌트의 렌더링에 영향을 미친다 state를 변경하면 컴포넌트는 다시 렌더링되며 UI가 업데이트된다 state는 주로 사용자 입력이나 네트워크 요청의 응답에 따라 변하는 데이터를 관리할 때 사용된다&lt;/p&gt;
&lt;h1&gt;props가 자식 컴포넌트에서 변하지 않는 이유는?&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;props가 자식 컴포넌트에서 변하지 않는 이유는 리액트의 단방향 데이터 흐름 원칙 때문이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트는 부모 컴포넌트가 자식 컴포넌트에 데이터를 전달할 때 단방향으로 전달하도록 설계되었다 이렇게하면 컴포넌트 간의 데이터 흐름을 예측 가능하고 일관성있게 만들 수 있어 애플리케이션 상태 관리가 간단해진다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;props는 읽기 전용이기 때문에 부모 컴포넌트에서 전달된 값이 자식 컴포넌트 내에서 임의로 변경되지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 특정 상태가 어디서 어떻게 변했는지를 예측할 수 있어 버그 발생 가능성을 줄이고 디버깅을 쉽게 한다&lt;/p&gt;
&lt;h1&gt;만약 props가 변경될 수 있다면&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 컴포넌트는 독립적으로 동작하지 않게 되고 재사용이 어려워질 수 있다 props가 불변으로 유지됨으로써 컴포넌트는 외부 입력에 의존할 뿐 내부적으로 변경하지 않아 재사용성이 높아지고 코드의 캡슐화가 강화된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 자식 컴포넌트가 부모로부터 받은 props를 변경해야 한다면, 부모 컴포넌트에서 상태로 해당 데이터를 관리하고 상태 변경 함수를 자식 컴포넌트로 전달하는 방식으로 구현해야 한다 이렇게 하면 데이터는 여전히 단방향으로 흐르고 상태는 부모 컴포넌트가 관리하므로 일관성을 유지할 수 있다 이러한 기법을 &lt;b&gt;상태 끌어올리기&lt;/b&gt;라고 한다&lt;/p&gt;</description>
      <category>Frontend</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/284</guid>
      <comments>https://winwin0219.tistory.com/284#entry284comment</comments>
      <pubDate>Fri, 6 Feb 2026 22:22:15 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 이벤트 루프</title>
      <link>https://winwin0219.tistory.com/283</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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;자바스크립트가 싱글 스레드 기반 언어임에도 불구하고 &lt;br /&gt;비동기 작업을 처리할 수 있게 해주는 중요한 메커니즘이다&lt;/span&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;이벤트 루프의 동작을 설명하기 위해 간단한 예로&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;setTimeout(callback, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 들어보겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setTimeout을 호출하면 이 콜백 함수는 바로 실행되는 것이 아니라 웹 API에 의해 타이머가 설정되고 그 타이머가 0밀리초 후에 만료되면 콜백 함수가 태스크 큐에 추가된다 그 후 콜 스택이 비어 있는 시점에 이벤트 루프가 태스크 큐에서 대기 중인 callback을 꺼내서 실행한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 setTimeout을 호출해도 현재 실행 중인 모든 동기 작업들이 완료된 후에야 그 콜백이 실행된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때문에 setTimeout을 사용하면 코드의 실행을 다음 이벤트 루프 사이클로 미뤄진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같이 이벤트 루프는 자바스크립트의 비동기 작업을 처리하는데 있어서 매우 중요한 역할을 한다 이벤트 루프 덕분에 자바 스크립트는 UI 업데이트나 사용자 입력 처리를 수행하면서도 비동기 작업을 블로킹 없이 효율적으로 처리할 수 있다&lt;/p&gt;</description>
      <category>Frontend</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/283</guid>
      <comments>https://winwin0219.tistory.com/283#entry283comment</comments>
      <pubDate>Fri, 6 Feb 2026 22:21:42 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 클로저</title>
      <link>https://winwin0219.tistory.com/282</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;클로저는 함수가 선언될 때의 스코프를 기억하며 함수가 생성된 이후에도 그 스코프에 접근할 수 있는 기능이다&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유하자면, 함수가 자신이 생성된 환경을 기억하는 것이라고 할 수 있다 클로저는 자바스크립트의 함수가 일급 객체라는 특성과 렉시컬 스코프의 조합으로 만들어진다&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;클로저 예시 코드&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function outerFunction(outerVariable) {
  return function innerFunction(innerVariable) {
    console.log('Outer Variable: ' + outerVariable);
    console.log('Inner Variable: ' + innerVariable);
  };
}

const newFunction = outerFunction('outside');
newFunction('inside');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서&amp;nbsp;innerFunction은&amp;nbsp;outerFunction의 내부에 정의되어 있다&amp;nbsp;innerFunction은 자신이 생성된 스코프, 즉&amp;nbsp;outerFunction의 스코프를 기억하고,&amp;nbsp;outerFunction의 호출이 완료된 이후에도 그 스코프에 접근할 수 있다 그리고 이에 따라&amp;nbsp;innerFunction은&amp;nbsp;outerVariable에도 접근할 수 있다 이것이 클로저가 동작하는 방식이다&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;클로저는 언제 활용하나요?&lt;/b&gt;&lt;/h1&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;&lt;b&gt;둘째, 비동기 작업에 활용된다&lt;/b&gt; 클로저는 비동기 작업에서 이전의 실행 컨텍스트를 유지해야 할 때 유용하다 콜백 함수가 비동기적으로 실행될 때 클로저를 사용하면 함수 실행 시점의 변수를 참조할 수 있다&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function createLogger(name) {
  return function() {
    console.log(`Logger: ${name}`);
  };
}

const logger = createLogger('MyApp');
setTimeout(logger, 1000); // 1초 후에 'Logger: MyApp' 출력
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시에서 클로저가 name 변수('MyApp')를 저장하여 1초 후에도 해당 값이 유지되어 출력된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;셋째, 모듈 패턴을 구현하는 데 활용된다&lt;/b&gt; 모듈 패턴은 특정 기능을 캡슐화하고, 외부에 공개하고자 하는 부분만 선택적으로 노출하여 코드의 응집력을 높이고, 유지보수성을 향상시키는 패턴이다 클로저를 활용하면 필요한 함수와 데이터만 외부로 노출함으로써 모듈 패턴을 쉽게 구현할 수 있다&lt;/p&gt;</description>
      <category>Frontend</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/282</guid>
      <comments>https://winwin0219.tistory.com/282#entry282comment</comments>
      <pubDate>Fri, 6 Feb 2026 22:20:52 +0900</pubDate>
    </item>
    <item>
      <title>[모니터링&amp;middot;관측성] Micrometer</title>
      <link>https://winwin0219.tistory.com/281</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;어떤&amp;nbsp;일이&amp;nbsp;일어나는지&amp;nbsp;수치로&amp;nbsp;측정/모니터링(라이브러리)&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770386365039&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Spring Boot]
   └ Actuator   &amp;larr; 서버 내부 상태를 숫자로 공개
       └ Micrometer &amp;larr; 숫자 포맷 표준화
            &amp;darr;
      Prometheus &amp;larr; 주기적으로 긁어감 (저장)
            &amp;darr;
        Grafana &amp;larr; 그래프로 보여줌&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;Micrometer란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Micrometer는 벤더 중립적인 메트릭 계측 라이브러리로, &lt;b&gt;애플리케이션에서 무슨 일이 실제로 벌어지고 있는지를 숫자로 측정&lt;/b&gt;한다 단순히 서버가 느린 것 같다, 요청이 많아진 것 같다라는 감이 아니라, 요청이 많아졌을 때 &lt;b&gt;CPU 때문인지, DB 때문인지&lt;/b&gt; 응답이 느릴 때 &lt;b&gt;어느 API가 병목인지&lt;/b&gt;를 &lt;b&gt;지표로 확인하기 위해 사용된다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreBoard에서는 배포 후 서버가 느려지거나 문제가 생겼을 때, 그 원인을 코드가 아니라 수치로 설명할 수 있어야 한다 는 목적 때문에 Micrometer를 도입했다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Micrometer는 CPU 사용량, 메모리 소비, HTTP 요청 처리 시간, 커스텀 이벤트 등의 지표를 지속적으로 수집한다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Spring Boot Actuator와 Micrometer의 관계&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot Actuator는 애플리케이션의 상태, 헬스 체크, 로그, 환경 정보 등 &lt;b&gt;운영 중인 서버를 관찰하기 위한 관리 엔드포인트&lt;/b&gt;를 제공한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreBoard처럼 배포 이후 실제로 서버를 띄워두고 부하 테스트나 운영 시나리오를 실험하려는 경우 &lt;b&gt;애플리케이션 내부 상태를 외부에서 확인할 수 있는 창구&lt;/b&gt;가 필요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 Actuator는 &lt;b&gt;Micrometer를 사용해&lt;/b&gt; JVM, HTTP 요청, DB 커넥션 풀 등의 메트릭을 수집한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 역할은 명확히 나뉜다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Micrometer&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&amp;rarr; 실제 수치를 측정하는 &lt;b&gt;계측 엔진&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Actuator&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&amp;rarr; 그 측정 결과를 /actuator/** 형태로 &lt;b&gt;외부에 노출하는 인터페이스&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Micrometer가 재고를 쌓아두는 역할이라면, Actuator는 그 재고를 밖에서 볼 수 있게 진열하는 역할이다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Micrometer로 무엇을 측정하는가?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Micrometer는 지표의 성격에 따라 다음과 같이 측정한다&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;Counter&lt;/td&gt;
&lt;td&gt;누적 증가 값 (예: 총 HTTP 요청 수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;처리 시간 측정 (예: API 응답 시간, 퍼센타일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;현재 상태 값 (예: 활성 세션 수, 메모리 사용량)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지표들은 단순한 숫자가 아니라, 서버에서 어떤 일이 벌어지고 있는지 설명하는 근거가 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 CoreBoard 기준으로 보면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Counter&lt;/li&gt;
&lt;li&gt;&amp;rarr; 특정 API가 예상보다 많이 호출되고 있는지&lt;/li&gt;
&lt;li&gt;Timer&lt;/li&gt;
&lt;li&gt;&amp;rarr; 게시글 조회/생성 중 어떤 API가 느린지&lt;/li&gt;
&lt;li&gt;Gauge&lt;/li&gt;
&lt;li&gt;&amp;rarr; DB 커넥션 풀이 고갈되고 있는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 &lt;b&gt;눈으로 확인할 수 있다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 수집된 지표는 Actuator를 통해 노출되고, Prometheus 같은 모니터링 시스템이 주기적으로 수집(scrape)한다&lt;/p&gt;</description>
      <category>Backend/⚙️ Infra</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/281</guid>
      <comments>https://winwin0219.tistory.com/281#entry281comment</comments>
      <pubDate>Fri, 6 Feb 2026 19:52:00 +0900</pubDate>
    </item>
    <item>
      <title>[IDE] IntelliJ에서는 되는데 VS Code에서는 안 되던 Spring Profile 미적용 문제</title>
      <link>https://winwin0219.tistory.com/280</link>
      <description>&lt;h4 data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a class=&quot;neon-violetblue&quot; style=&quot;text-decoration: none;&quot; href=&quot;#conclusion&quot;&gt; [IntelliJ에서는 되고 VS Code에서는 안 되는 이유 바로가잣!] &lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;VSCode에서 Spring Boot 실행 시 애플리케이션 부팅 중단되었다&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그에서 보인 문구&lt;/p&gt;
&lt;pre id=&quot;code_1770372551383&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;No active profile set, falling back to 1 default profile: &quot;default&quot;
Failed to configure a DataSource: 'url' attribute is not specified
Failed to determine a suitable driver class&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;원인&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트는 application.yml과 application-local.yml로 분리되어 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 VS Code에서 Run Java/Debug Java 방식으로 실행하면 spring.profile.active=local이 전달되지 않아 default 프로필로 부팅된다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 application-local.yml이 로딩되지 않고 datasource 설정(url, driver-class-name 등)이 없어 DataSource 생성 실패한다&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;&lt;b&gt;해결&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷(1227).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yMZfv/dJMcaaqEhRS/PbMDJPoVwLKlM6vkoyKO6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yMZfv/dJMcaaqEhRS/PbMDJPoVwLKlM6vkoyKO6k/img.png&quot; data-alt=&quot;컨트롤 + 시프트 + D&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yMZfv/dJMcaaqEhRS/PbMDJPoVwLKlM6vkoyKO6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyMZfv%2FdJMcaaqEhRS%2FPbMDJPoVwLKlM6vkoyKO6k%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;1920&quot; height=&quot;1080&quot; data-filename=&quot;스크린샷(1227).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컨트롤 + 시프트 + D&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷(1228).png&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;519&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1Dhtk/dJMb99SNIj8/8hXbCWuVeCvlEnTfN2WM21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1Dhtk/dJMb99SNIj8/8hXbCWuVeCvlEnTfN2WM21/img.png&quot; data-alt=&quot;launch.json 파일 만들기 클릭&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1Dhtk/dJMb99SNIj8/8hXbCWuVeCvlEnTfN2WM21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1Dhtk%2FdJMb99SNIj8%2F8hXbCWuVeCvlEnTfN2WM21%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;655&quot; height=&quot;519&quot; data-filename=&quot;스크린샷(1228).png&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;519&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;launch.json 파일 만들기 클릭&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code에서 실행 시 JVM 옵션으로 프로필을 명시하도록 launch.json 실행 구성을 추가하면 된다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770371999907&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;version&quot;: &quot;0.2.0&quot;,
  &quot;configurations&quot;: [
    {
      &quot;type&quot;: &quot;java&quot;,
      &quot;name&quot;: &quot;CoreBoard (local)&quot;,
      &quot;request&quot;: &quot;launch&quot;,
      &quot;mainClass&quot;: &quot;com.example.coreboard.CoreBoardApplication&quot;,
      &quot;projectName&quot;: &quot;CoreBoard&quot;,
      &quot;vmArgs&quot;: &quot;-Dspring.profiles.active=local&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;이후 실행 및 디버그에서 CoreBoard(local) 구성으로 실행한다&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;&lt;b&gt;결과(검증)&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그에서 No active profile set... 문구가 사라지고 Started CoreBoardApplication까지 정상 가동되었다&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;&lt;b&gt;회고 / 재발 방지&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE마다 실행 구성(프로필/VM 옵션)을 전달하는 방식이 다르며 IntelliJ는 Run Configuration UI에 숨겨져 있으며&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code는 launch.json으로 명시해야 한다&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;왜 IntelliJ에서는 되는가?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면, Spring Profile은 IDE가 아니라 JVM 옵션으로 결정된다 IntelliJ는 JVM을 띄울 때 옵션을 자동으로 넣어주고 VS Code는 명시하지 않으면 아무 옵션도 넣지 않는다&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Spring Boot는 언제 프로필을 결정할까?&lt;/b&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;JVM이 시작된 직후, 정확히는 SpringApplication이 초기화되기 전에 JVM 시스템 프로퍼티에서 아래 값을 읽는다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770373248761&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.profiles.active&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값의 출처는 총 3가지이며 아래 순서대로 조회된다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. JVM 시스템 프로퍼티&amp;nbsp;&lt;/h4&gt;
&lt;pre id=&quot;code_1770373017373&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-Dspring.profiles.active=local&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[깨알공유] JVM &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;시스템&lt;/span&gt; 프로퍼티와 application.properties는 다름!&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;775&quot; data-start=&quot;664&quot; data-ke-size=&quot;size16&quot;&gt;JVM 시스템 프로퍼티는 application.properties / application.yml &lt;b&gt;보다 상위에서 결정되는 값&lt;/b&gt;이다&lt;br /&gt;그래서 &lt;b&gt;역할도 다르고, 적용 시점도 다르다&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;900&quot; data-start=&quot;777&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;840&quot; data-start=&quot;777&quot;&gt;-Dspring.profiles.active=local&lt;br /&gt;&amp;rarr; JVM 자체에 주입되는 &lt;b&gt;전역 설정&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;873&quot; data-start=&quot;841&quot;&gt;Java 프로그램이 시작되자마자 &lt;b&gt;즉시 사용 가능&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;900&quot; data-start=&quot;874&quot;&gt;Spring보다 &lt;b&gt;훨씬 앞단&lt;/b&gt;에 존재한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;916&quot; data-start=&quot;902&quot; data-ke-size=&quot;size16&quot;&gt;실제 흐름은 다음과 같다&lt;/p&gt;
&lt;pre id=&quot;code_1770373141023&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[JVM 시작]  &amp;larr; 여기서 이미 존재
   &amp;darr;
SpringApplication 생성
   &amp;darr;
Spring이 프로퍼티 조회&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[반면, application.properties(application.yml)는?]&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1105&quot; data-start=&quot;1065&quot;&gt;Spring Boot가 &lt;b&gt;자기 자신을 로딩하면서&lt;/b&gt; 읽는 설정 파일&lt;/li&gt;
&lt;li data-end=&quot;1136&quot; data-start=&quot;1106&quot;&gt;&lt;b&gt;Spring 컨텍스트 내부에서만 의미&lt;/b&gt;가 있다&lt;/li&gt;
&lt;li data-end=&quot;1177&quot; data-start=&quot;1137&quot;&gt;JVM 입장에서는 &lt;b&gt;그냥 클래스패스에 있는 리소스 파일&lt;/b&gt;일 뿐이다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1770373188692&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[JVM 시작]
   &amp;darr;
SpringApplication 시작
   &amp;darr;
설정 파일 로딩 (application.yml)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;프로필을 결정하는 단계에는 아직 설정 파일이 읽히지 않았다&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. OS 환경 변수&lt;/h4&gt;
&lt;pre id=&quot;code_1770373221937&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SPRING_PROFILES_ACTIVE=local&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[깨알공유] OS 환경변수란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제(OS) 프로그램 실행할 때 같이 넘겨주는 이름=값형태의 설정값이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 실행할 때 이 값을 읽어서 프로필을 결정할 수 있다 launch.json 의 -Dspring.profiles.active=local과 같은 목적이고 코드 밖에서 설정을 바꾸는 통로다&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 설정 파일 내부&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(예: spring.profiles.default, 또는 드물게 spring.profiles.active)&lt;/p&gt;
&lt;p data-end=&quot;1752&quot; data-start=&quot;1676&quot; data-ke-size=&quot;size16&quot;&gt;기술적으로는 가능하지만, &lt;b&gt;프로필을 결정하기 위해 다시 설정 파일을 읽어야 하는 구조&lt;/b&gt;이므로 실무에서는 거의 사용하지 않는다&lt;/p&gt;
&lt;p data-end=&quot;1752&quot; data-start=&quot;1676&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;IntelliJ애서는 왜 됐는가?&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ에서 Spring Boot를 실행하면, 실제로는 내부적으로 다음과 같은 JVM 커맨드를 구성한다&lt;/p&gt;
&lt;pre id=&quot;code_1770373329422&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -Dspring.profiles.active=local \
     -classpath ... \
     com.example.coreboard.CoreBoardApplication&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Run Configuration에 설정된 VM Options, Enviroment Variavles가 자동으로 JVM 실행 명령에 포함되기 때문이다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아무것도 설정 안 했는데? 라고 느껴지지만 실제로는 JVM에는 옵션이 이미 전달된 상태였던 것.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[여기서 중요한 점]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;2248&quot; data-start=&quot;2150&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JVM 입장에서는 VS Code든 IntelliJ든 아무 차이가 없다는 것&lt;/b&gt;이다&lt;br /&gt;차이는 &lt;b&gt;누가 JVM 옵션을 만들어 주느냐&lt;/b&gt;에 있다&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;왜 launch.json이 해결책이었나?&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1770373435098&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;vmArgs&quot;: &quot;-Dspring.profiles.active=local&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code에게 이렇게 지시한 것과 같다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM을 띄울 때 이 옵션을 반드시 포함시켜라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 VS Code는 JVM을 이렇게 띄운다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770373465339&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -Dspring.profiles.active=local \
     -agentlib:jdwp=... \
     -classpath ... \
     com.example.coreboard.CoreBoardApplication&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JVM이 시작되자마자 spring.profiles.active=local 값을 인식하고 Spring Boot는 application-local.yml을 로딩하여 &lt;b&gt;DataSource 생성에 성공&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;</description>
      <category>Backend/ Trouble Tang Tang</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/280</guid>
      <comments>https://winwin0219.tistory.com/280#entry280comment</comments>
      <pubDate>Fri, 6 Feb 2026 19:07:50 +0900</pubDate>
    </item>
    <item>
      <title>[ubuntu]수동배포 - CI/CD 대응되는 포인트</title>
      <link>https://winwin0219.tistory.com/279</link>
      <description>&lt;h4 data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a class=&quot;neon-violetblue&quot; style=&quot;text-decoration: none;&quot; href=&quot;#conclusion&quot;&gt; [CI/CD 대응되는 포인트 모아보기 바로가잣!] &lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;git clone 받고 배포해보자&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;나중에 어떻게 했더라~ 다시 보려고 정리함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;왜 수동 배포를 먼저 했는가&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;처음부터 CI/CD를 붙이면 편하다는 건 알고 있었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 &lt;b&gt;편리함이 무엇을 대체해 주는지 이해하지 못한 상태에서 자동화를 쓰는 건 공허하다고 느꼈다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;접속을 원한다면 8080 포트 열면 됨&lt;/span&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t6JzE/dJMcag5tdOg/ZY6LYynHjELhM2VSqnXzk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t6JzE/dJMcag5tdOg/ZY6LYynHjELhM2VSqnXzk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t6JzE/dJMcag5tdOg/ZY6LYynHjELhM2VSqnXzk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft6JzE%2FdJMcag5tdOg%2FZY6LYynHjELhM2VSqnXzk1%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;1920&quot; height=&quot;1080&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. 페어키 미리 다운로드 받음 &lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 1. 서버 열기 &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;내 선택: .pem + ED25519&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260204184355.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2M7nr/dJMcaaxpJVL/wU8UiTBbidPByu2kfSCyy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2M7nr/dJMcaaxpJVL/wU8UiTBbidPByu2kfSCyy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2M7nr/dJMcaaxpJVL/wU8UiTBbidPByu2kfSCyy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2M7nr%2FdJMcaaxpJVL%2FwU8UiTBbidPByu2kfSCyy1%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;1920&quot; height=&quot;1080&quot; data-filename=&quot;Pasted image 20260204184355.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위 사진에서 .pem or .ppk 골라야 하는데 난 .pem 선택했다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(참고로 사진에는 RSA 체크되어있는데 ED25519 체크했으니 당황 놉!!)&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;.pem + ED25519 선택 이유&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; &amp;mdash; 로컬은 Windows지만, 실제 접속/배포/CI 환경은 OpenSSH 기반이므로 .pem + ED25519를 선택했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;OpenSSH 기준으로 바로 쓸 수 있는 키 포맷이냐?&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;powerShell/Window Terminal의 ssh&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Linux 서버의 sshd&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;GitHubActions/CI 러너&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; 전부 OpenSSH.pem&amp;rarr; 변환 불필요&amp;rarr; PuTTY 전용 포맷&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; CI/Linux 서버에서는 직접 쓸 수 없음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;.ppk&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; OpenSSH 가 그대로 읽는다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;핵심 판단 기준&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 1. 수동 배포(윈도우)에서 PuTTY를 쓸 거면 .ppk가 편해서 PuTTYgen 씀 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 윈도우 powerShell의 ssh로 접속할 거면 .pem 이면 끝난다 &lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[접속 방식 A]&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;PowerShell/Window Terminal로 ssh 키 : .pem &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점 : PuTTY/PuTTYgen 필요 없음, 가장 표준 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점 : 처음엔 권한/경로 때문에 에러를 한 번쯤 봄&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[접속 방식 B]&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;PuTTY로 접속 키 : .ppk &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점 : GUI 라 직관적 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점 : PuTTYgen / 변환이 귀찮음&lt;/span&gt;&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CICD 하더라도 보통 .pem 택해야 함&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;러너가 리눅스에서 돌아가고 SSH는 OpenSSH를 쓴다 그래서 개인키는 pem파일 내용 또는 -----BEGIN OPENSSH PRIVATE KEY----- 형태의 OpenSSH 개인 키 텍스트를 Secret에 넣는 게 일반적이다&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;.ppk 가 필요한 경우는? &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD 파이프라인 안에서 PuTTY/pscp/plink 같은 PuTTY 도구를 일부러 쓰는 경우 &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[깨알공유] OpenSSH란?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;SecureShell은 인터넷/네트워크로 다른 컴퓨터 서버에 안전하게 접속하는 프로토콜인데 그 SSH를 진짜로 실행해주는 오픈 소스다&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;구성&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;클라이언트: ssh, scp, sftp&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버(ec2): sshd (SSH데몬=서버에서 접속을 받아주는 프로그램)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;왜 OpenSSH랑 .pem이랑 엮을까?&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;파일 확장자가 핵심이 아니라 키 포맷이 핵심이다&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;OpenSSH는 개인키를 OpenSSH형식으로 읽는 게 일반적이다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;.pem 을 주면 보통 바로 쓴다&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;반면 .ppk는 PuTTY 전용 포맷이라 OpenSSH가 그대로 못 쓰는 경우가 많아서 변환이 필요함 그래서 이전에 이거 모르고 .ppk로 했다가 설치한 흔적이 있다 ㅠ&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;55&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RE6fI/dJMcabC5z8L/CqAhzXJQ6rdjEnYZ5D7kl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RE6fI/dJMcabC5z8L/CqAhzXJQ6rdjEnYZ5D7kl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RE6fI/dJMcabC5z8L/CqAhzXJQ6rdjEnYZ5D7kl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRE6fI%2FdJMcabC5z8L%2FCqAhzXJQ6rdjEnYZ5D7kl1%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;680&quot; height=&quot;55&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;55&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 2. 서버와 연결 (배포) &lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1770259413092&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;나: &amp;ldquo;pem넣어서 접속할게&amp;rdquo;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;SSH : &amp;ldquo;OK, 키 보자&amp;rdquo;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;SSH : &amp;ldquo;엥? 이 키 파일을 너 말고도 다른 사용자/그룹이 읽을 수 있네???&amp;rdquo; &amp;rarr; 위험하다!&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; 키 무시 &amp;rarr; 인증 실패 &lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그래서&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt; ssh -i &quot;$env:USERPROFILE\\\\.ssh\\\\study-ec2-ssh.pem&quot; ubuntu@15.165.161.222&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 env:USERPROFILE 해줘야 함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;참고로 해당 루트에 .ssh가 있어야 한다 Users/user&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[CI/CD 대응되는 포인트]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;수동 배포&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;CI/CD&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;개발자가 SSH로 접속&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD Runner가 서버에 접근&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사람이 직접 명령 실행&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자동화된 Job 실행&lt;/span&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2-1) 우분투에서 프로그램 설치 목록 최신으로 갱신하는 명령어&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259452099&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 2-2) java 버전도 확인해줘야 함 &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259461920&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java --version&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 없다면 설치해줘야 한다 &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259473968&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt install -y openjdk-17-jdk&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;apt&lt;/b&gt;: 우분투 패키지 관리자&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;install&lt;/b&gt;: 설치하겠다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;y&lt;/b&gt;: &amp;ldquo;설치 중에 나오는 질문 전부 yes로 자동 응답&amp;rdquo;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;openjdk-17-jdk&lt;/b&gt;: 설치할 대상&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아래처럼 버전이 나온 걸 확인했다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259483466&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;openjdk version &quot;17.0.18&quot; 2026-01-20
OpenJDK Runtime Environment (build 17.0.18+8-Ubuntu-124.04.1)
OpenJDK 64-Bit Server VM (build 17.0.18+8-Ubuntu-124.04.1, mixed mode, sharing)&lt;/code&gt;&lt;/pre&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[CI/CD 대응되는 포인트]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버 상태 직접 관리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Docker / Runner 환경 고정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;매번 수동 설치&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;미리 정의된 실행 환경&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2-3) git 버전도 확인해줘야 한다 &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259494920&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git --version&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[깨알공유] 만일, git -version 으로 했다면 아래처럼 뜰 것이다&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;-version는 옵션까지 모른다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 뜬다면 git 설치는 되어있는 상태인 거다 그래도 다시 한 번 정확하게 버전을 확인해보자&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259552394&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;unknown option: -version
usage: git [-v | --version] [-h | --help] [-C &amp;lt;path&amp;gt;] [-c &amp;lt;name&amp;gt;=&amp;lt;value&amp;gt;]
           [--exec-path[=&amp;lt;path&amp;gt;]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=&amp;lt;path&amp;gt;] [--work-tree=&amp;lt;path&amp;gt;] [--namespace=&amp;lt;name&amp;gt;]
           [--config-env=&amp;lt;name&amp;gt;=&amp;lt;envvar&amp;gt;] &amp;lt;command&amp;gt; [&amp;lt;args&amp;gt;]&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1770259569460&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git --version&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2-4) git clone 받기 &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259587238&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mkdir -p ~/apps
cd ~/apps
git clone &amp;lt;YOUR_PRIVATE_REPO_URL&amp;gt; no-cicd-deploy
cd no-cicd-deploy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;여기서 생기는 문제점이 있다&lt;/span&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 data-end=&quot;1302&quot; data-start=&quot;1288&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;브랜치를 잘못 받으면?&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1317&quot; data-start=&quot;1303&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최신 코드인지 헷갈린다&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1331&quot; data-start=&quot;1318&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;커밋 기준이 애매하다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[CI/CD 대응되는 포인트]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;git clone 직접 실행&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;checkout step&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;어떤 커밋인지 직접 확인&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특정 commit SHA 기준 실행&lt;/span&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;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[깨알공유] 이때 apps으로 이동하는 이유&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;~/apps = /home/ubuntu/apps 폴더&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;apps에 두면&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;재배포가 쉽다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;파일/폴더 안 흩어진다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;권한/보안이 단순해진다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버에 여러 프로젝트 올릴 때 확장된다&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;mkdir -p는 왜 붙일까? 폴더 없으면 만들고 이미 있으면 에러 없이 넘어가게 하려고 -p = 있으면 그냥 통과&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, ubuntu 계정이 관리하는 앱/프로젝트 폴더라고 보면 된다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;~ = 지금 로그인한 사용자의 홈 디렉토리. 보통 /home/ubuntu&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2-5) 빌드(Gradle) &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259623394&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;chmod +x ./gradlew
./gradlew clean build -x test
ls -al build/libs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1581&quot; data-start=&quot;1564&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;여기서 가장 큰 피로를 느꼈다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1645&quot; data-start=&quot;1583&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1598&quot; data-start=&quot;1583&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;빌드 실패 &amp;rarr; 원인 추적&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1623&quot; data-start=&quot;1599&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;성공해도 이게 맞는 결과물인가? 불안&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1645&quot; data-start=&quot;1624&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;로컬과 서버 결과가 다를 수도 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[CI/CD 대응되는 포인트]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버에서 직접 빌드&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;build job&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;성공/실패 수동 확인&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;pipeline status&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2-6) dev 프로필로 실행 (백그라운드) &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259636745&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nohup java -jar build/libs/*.jar --spring.profiles.active=dev &amp;gt; app.log 2&amp;gt;&amp;amp;1 &amp;amp;
tail -f app.log&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2-7) 로그 확인 &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259650517&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;tail -f app.log&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 만일 아래처럼 뜬다면 성공&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259658249&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; :: Spring Boot ::                (v4.0.2)

2026-02-04T11:42:12.686Z  INFO 3814 --- [           main] c.e.n.NoCicdDeployApplication            : Starting NoCicdDeployApplication v0.0.1-SNAPSHOT using Java 17.0.18 with PID 3814 (/home/ubuntu/apps/no-cicd-deploy/build/libs/no-cicd-deploy-0.0.1-SNAPSHOT.jar started by ubuntu in /home/ubuntu/apps/no-cicd-deploy)
2026-02-04T11:42:12.699Z  INFO 3814 --- [           main] c.e.n.NoCicdDeployApplication            : The following 1 profile is active: &quot;dev&quot;
2026-02-04T11:42:14.119Z  INFO 3814 --- [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat initialized with port 8080 (http)
2026-02-04T11:42:14.145Z  INFO 3814 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2026-02-04T11:42:14.147Z  INFO 3814 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/11.0.15]
2026-02-04T11:42:14.189Z  INFO 3814 --- [           main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 1365 ms
2026-02-04T11:42:14.749Z  INFO 3814 --- [           main] o.s.boot.tomcat.TomcatWebServer          : Tomcat started on port 8080 (http) with context path '/'
2026-02-04T11:42:14.783Z  INFO 3814 --- [           main] c.e.n.NoCicdDeployApplication            : Started NoCicdDeployApplication in 2.919 seconds (process running for 3.596)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위 메시지 중&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;The following 1 profile is active: &quot;dev&quot;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Tomcat started on port 8080 (http) with context path '/'&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Started NoCicdDeployApplication&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;직역하면&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;dev 프로필 적용됨&lt;/b&gt; (application-dev.yml이 로드됨)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;서버가 8080 포트에서 떠 있음&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;앱이 죽지 않고 &lt;b&gt;계속 실행 중&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;여기서 만약, 에러가 난다면? 이것도 치명적인 문제로 느꼈다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;에러가 코드 문제인지 배포 문제인지 구분이 안된다는 점이다&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[CI/CD 대응되는 포인트]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버에 접속해서 로그 확인&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Job log&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실패 원인 추적 어려움&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단계별 실패 지점 명확&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre id=&quot;code_1770259680805&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; curl -i &amp;lt;http://localhost:8080&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 마지막으로 파워쉘에 위 명령어를 입력하면&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259688945&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 404
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 04 Feb 2026 11:43:47 GMT

{&quot;timestamp&quot;:&quot;2026-02-04T11:43:47.456Z&quot;,&quot;status&quot;:404,&quot;error&quot;:&quot;Not Found&quot;,&quot;path&quot;:&quot;/&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 뜰텐데 서버는 살아잇는데 / 경로를 처리하는 컨트롤러가 없어서 404 뜬 것이다&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;HTTP/1.1 404&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;{&quot;status&quot;:404,&quot;error&quot;:&quot;Not Found&quot;,&quot;path&quot;:&quot;/&quot;} 직역하면&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;ldquo;서버는 응답했는데, /에 해당하는 페이지(엔드포인트)가 없다&amp;rdquo;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉 &lt;b&gt;네트워크/포트/톰캣/앱 기동은 전부 성공&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 3. 확인 &lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 확인용 api 하나 만들었다&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259716312&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/ok&quot;)
public ResponseEntity&amp;lt;String&amp;gt; ok(){
        return ResponseEntity.ok(&quot;ok&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 그리고 수동 배포니까 코드 업데이트를 해야 한다 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;코드 업데이트&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1) pull&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259735347&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd ~/apps/no-cicd-deploy
git pull&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2) build&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;여기까지 오면 대충 눈치 챘겠지만, pull 땡기고 build하고 실행하는 중인 것!&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259744667&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew clean build -x test&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 프로젝트를 깨끗하게 다시 빌드하되 테스트는 건너뛸 거다라는 의미&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3) PID 찾기 (프로세스 ID)&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259766621&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ps -ef | grep no-cicd-deploy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;PID(Process ID) : 실행 중인 프로그램(프로세스)을 식별하는 고유 번호 이전에&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259775657&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[1] 3814&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 생긴 걸 봤을텐데 이것이 PID .&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259784160&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ubuntu      3814    1263  1 11:42 pts/0    00:00:08 java -jar build/libs/no-cicd-deploy-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
ubuntu      3938    1263  0 11:52 pts/0    00:00:00 grep --color=auto no-cicd-deploy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 뜰텐데 직역하면&lt;/span&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ubuntu 3814 ... java -jar ... &amp;rarr; 이게 &lt;b&gt;진짜 서버(스프링 부트 앱) 프로세스&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ubuntu 3938 ... grep --color=auto no-cicd-deploy &amp;rarr; 이건 &lt;b&gt;너가 입력한 검색 명령(grep)&lt;/b&gt; 이 잠깐 실행된 것&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3814 는 지금 실행중인 앱이라서 kill 해주고 다시 빌드해야 한다&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;빌드까지했다면 실행해주면 된다&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt; nohup java -jar build/libs/no-cicd-deploy-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev &amp;gt; app.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그럼 사용중인 PID가 뜰 것이고 GET 요청을 해보면 된다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이게 진짜 불편했다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1902&quot; data-start=&quot;1884&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이전 프로세스가 남아 있으면?&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1914&quot; data-start=&quot;1903&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;포트 충돌 나면?&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1934&quot; data-start=&quot;1915&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;백그라운드 실행 관리가 번거롭다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[CI/CD 대응되는 포인트]&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;수동 배포&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;CI/CD&lt;/span&gt; &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;java -jar 직접 실행&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;deploy step&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;프로세스 직접 관리&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;스크립트/컨테이너 기반 실행&lt;/span&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;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3-1) GET 요청 &lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1770259825229&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -i &amp;lt;http://localhost:8080/api/ok&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 그리고 아래처럼 뜬다면 성공이다 &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770259834552&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 2
Date: Wed, 04 Feb 2026 11:56:30 GMT&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; [결론]CI/CD 부재를 체감하기 위한 수동 배포 실험 회고&lt;/b&gt; &lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[실험 환경]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Boot 단일 애플리케이션&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;EC2 + 수동 배포&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Git push 이후 자동화 없음&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;빌드 / 실행 / 검증 전 과정 수동&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제의 본질은 &amp;ldquo;단계가 많다&amp;rdquo;가 아니었다 &lt;b&gt;개발 중 사고 흐름이 배포 절차에 의해 계속 끊긴다는 점&lt;/b&gt;이었다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;코드 변경 &amp;rarr; 결과 확인 사이에 불확실한 구간이 생긴다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD 가 없는 상태에서는 코드를 수정한 뒤 결과를 보기까지 다음을 사람이 직접 보장해야 한다&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;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;jar가 정말 방금 수정한 코드인지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 프로세스가 완전히 내려갔는지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;새 프로세스가 정상 기동했는지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;요청이 새 코드로 처리되고 있는지&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, 결과를 해석하기 전에 이 결과가 신뢰가능한가?를 먼저 판단해야 하는 상태가 되었다&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 결과가 틀렸을 때 원인이 코드인지 배포인지 즉시 알 수 없다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API 응답이 기대와 다를 때 요청이 아예 안 들어올 때 이게 코드 로직 문제인지 jar 교체 실패인지(근데 이건 아닌듯) 포트/프로세스 문제인지 이전 버전이 아직 떠있는 건지 전부 추론해야 했다&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 배포가 수단이 아니라 주의 대상이된다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;가장 인상 깊었던 점은 어느 순간부터 이 코드가 맞나?보다 지금 서버 상태가 맞나?를 더 자주 확인하고 있었다는 점이다&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD가 없으면 개발자는 코드를 디버깅하기 전에 먼저 배포가 제대로 됐는지를 디버깅하게 된다&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&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;style6&quot; /&gt;
&lt;h2 id=&quot;conclusion&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[CI/CD 대응되는 포인트 모아보기]&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-end=&quot;514&quot; data-start=&quot;493&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. EC2 서버 접속 (SSH)&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1770261156505&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -i my-key.pem ubuntu@xx.xx.xx.xx&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;642&quot; data-start=&quot;586&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;602&quot; data-start=&quot;586&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버에 직접 접속해야 한다&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;624&quot; data-start=&quot;603&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;배포할 때마다 같은 작업을 반복한다&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;642&quot; data-start=&quot;625&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실수하면 서버 상태가 꼬인다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;774&quot; data-start=&quot;667&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;743&quot; data-start=&quot;704&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;719&quot; data-start=&quot;704&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;개발자가 SSH로 접속&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;743&quot; data-start=&quot;719&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD Runner가 서버에 접근&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;774&quot; data-start=&quot;744&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;759&quot; data-start=&quot;744&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사람이 직접 명령 실행&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;774&quot; data-start=&quot;759&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;자동화된 Job 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;blockquote data-end=&quot;821&quot; data-start=&quot;776&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD는 사람 대신 서버가 배포 명령을 실행하게 만드는 구조다&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-end=&quot;857&quot; data-start=&quot;828&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-end=&quot;857&quot; data-start=&quot;828&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 서버 환경 세팅 (Java, Git 설치)&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1770261175510&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update sudo apt install openjdk-17-jdk git&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;937&quot; data-start=&quot;924&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버가 바뀌면 다시 설치해야 한다&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1006&quot; data-start=&quot;939&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;978&quot; data-start=&quot;960&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;버전이 달라지면 문제가 생긴다&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1006&quot; data-start=&quot;979&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 서버는 어떤 상태인가?를 기억해야 한다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1135&quot; data-start=&quot;1031&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1107&quot; data-start=&quot;1068&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1082&quot; data-start=&quot;1068&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버 상태 직접 관리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;1107&quot; data-start=&quot;1082&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Docker / Runner 환경 고정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1135&quot; data-start=&quot;1108&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1119&quot; data-start=&quot;1108&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;매번 수동 설치&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;1135&quot; data-start=&quot;1119&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;미리 정의된 실행 환경&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;blockquote data-end=&quot;1179&quot; data-start=&quot;1137&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD는 서버 상태를 기억하지 않아도 되게 만드는 장치다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1211&quot; data-start=&quot;1186&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. Git Clone &amp;amp; 코드 가져오기&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1770261222459&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git clone https://github.com/xxx/project.git cd project&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1331&quot; data-start=&quot;1288&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1302&quot; data-start=&quot;1288&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;브랜치를 잘못 받으면?&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1317&quot; data-start=&quot;1303&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최신 코드인지 헷갈린다&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1331&quot; data-start=&quot;1318&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;커밋 기준이 애매하다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 66px;&quot; border=&quot;1&quot; data-end=&quot;1468&quot; data-start=&quot;1356&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-end=&quot;1428&quot; data-start=&quot;1393&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1411&quot; data-start=&quot;1393&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;git clone 직접 실행&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot; data-end=&quot;1428&quot; data-start=&quot;1411&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;checkout step&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-end=&quot;1468&quot; data-start=&quot;1429&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1445&quot; data-start=&quot;1429&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;어떤 커밋인지 직접 확인&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 22px;&quot; data-end=&quot;1468&quot; data-start=&quot;1445&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특정 commit SHA 기준 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;blockquote data-end=&quot;1509&quot; data-start=&quot;1470&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD는 항상 동일한 코드 상태로 배포되게 보장한다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1533&quot; data-start=&quot;1516&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4. 빌드 (Gradle)&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1770261256622&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew build&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1645&quot; data-start=&quot;1583&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1598&quot; data-start=&quot;1583&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;빌드 실패 &amp;rarr; 원인 추적&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1623&quot; data-start=&quot;1599&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;성공해도 &amp;ldquo;이게 맞는 결과물인가?&amp;rdquo; 불안&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1645&quot; data-start=&quot;1624&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;로컬과 서버 결과가 다를 수도 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 66px;&quot; border=&quot;1&quot; data-end=&quot;1767&quot; data-start=&quot;1670&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-end=&quot;1733&quot; data-start=&quot;1707&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1720&quot; data-start=&quot;1707&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버에서 직접 빌드&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot; data-end=&quot;1733&quot; data-start=&quot;1720&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;build job&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot; data-end=&quot;1767&quot; data-start=&quot;1734&quot;&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;1748&quot; data-start=&quot;1734&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;성공/실패 수동 확인&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 22px; text-align: center;&quot; data-end=&quot;1767&quot; data-start=&quot;1748&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;pipeline status&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;blockquote data-end=&quot;1808&quot; data-start=&quot;1769&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD는 빌드 결과를 신뢰할 수 있게 만드는 장치다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1830&quot; data-start=&quot;1815&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;5. 애플리케이션 실행&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1770261295575&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java -jar build/libs/app.jar&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1934&quot; data-start=&quot;1884&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1902&quot; data-start=&quot;1884&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이전 프로세스가 남아 있으면?&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1914&quot; data-start=&quot;1903&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;포트 충돌 나면?&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1934&quot; data-start=&quot;1915&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;백그라운드 실행 관리가 번거롭다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2062&quot; data-start=&quot;1959&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2029&quot; data-start=&quot;1996&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2014&quot; data-start=&quot;1996&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;java -jar 직접 실행&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2029&quot; data-start=&quot;2014&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;deploy step&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2062&quot; data-start=&quot;2030&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2043&quot; data-start=&quot;2030&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;프로세스 직접 관리&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2062&quot; data-start=&quot;2043&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;스크립트/컨테이너 기반 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;blockquote data-end=&quot;2100&quot; data-start=&quot;2064&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD는 실행까지 포함한 배포 흐름을 책임진다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;2123&quot; data-start=&quot;2107&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;6. 로그 확인 &amp;amp; 검증&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1770261330723&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl http://server/api/health tail -f log.txt&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2204&quot; data-start=&quot;2184&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;여기서 가장 치명적인 문제를 느꼈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;2204&quot; data-start=&quot;2184&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;에러가 코드 문제인지, 배포 문제인지 구분이 안 된다&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2361&quot; data-start=&quot;2264&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style9&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 수동 배포 &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; CI/CD &lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2329&quot; data-start=&quot;2301&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2318&quot; data-start=&quot;2301&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;서버에 접속해서 로그 확인&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2329&quot; data-start=&quot;2318&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Job log&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2361&quot; data-start=&quot;2330&quot;&gt;
&lt;td style=&quot;text-align: center;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2345&quot; data-start=&quot;2330&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실패 원인 추적 어려움&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot; data-end=&quot;2361&quot; data-start=&quot;2345&quot; data-col-size=&quot;sm&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단계별 실패 지점 명확&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;blockquote data-end=&quot;2400&quot; data-start=&quot;2363&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CI/CD는 문제의 위치를 바로 알 수 있게 만든다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend/⚙️ Infra</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/279</guid>
      <comments>https://winwin0219.tistory.com/279#entry279comment</comments>
      <pubDate>Thu, 5 Feb 2026 11:52:41 +0900</pubDate>
    </item>
    <item>
      <title>GET</title>
      <link>https://winwin0219.tistory.com/278</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1768812139007&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
&quot;success&quot;: true,
&quot;message&quot;: &quot;목록 조회 성공&quot;,
&quot;data&quot;: {
    &quot;items&quot;: [
        { &quot;postId&quot;: 1, &quot;title&quot;: &quot;a&quot;, &quot;content&quot;: &quot;...&quot; },
        { &quot;postId&quot;: 2, &quot;title&quot;: &quot;b&quot;, &quot;content&quot;: &quot;...&quot; }
        ],
            &quot;page&quot;: 0,
            &quot;size&quot;: 20,
            &quot;totalElements&quot;: 132,
            &quot;totalPages&quot;: 7,
            &quot;hasNext&quot;: true
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 : GET&lt;/li&gt;
&lt;li&gt;URL 전체 목록 : GET /posts&lt;/li&gt;
&lt;li&gt;특정 조건 목록(검색/필터) : GET /posts?keyword=&amp;hellip;&amp;amp;status=&amp;hellip;&lt;/li&gt;
&lt;li&gt;내 글만 : GET /users/{userId}/posts 또는 GET /posts?authorId=&amp;hellip;&lt;/li&gt;
&lt;li&gt;GET은 RequestBody 거의 안 쓴다 대부분 Query Parameter로 조건을 전달한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;전체 조회에서 거의 무조건 필요한 3종 세트!&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1) Pagination&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체를 진짜 다 주면 DB / 네트워크 터질 수 있어서 보통은 페이지 단위로 잘라서 준다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 GET /posts?page=0&amp;amp;size=20&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;page : 몇 번째 페이지(보통 0)&lt;/li&gt;
&lt;li&gt;size : 한 페이지에 몇 개&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2) Sort&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 GET /posts?sort=createdAt, desc&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;createdAt 기준 내림차순&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3) Filter/Search&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 GET /posts?keyword=spring&amp;amp;authorId=3&lt;/p&gt;
&lt;h1&gt;응답 형태&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 주로 만드는 공통 응답 포맷에 맞춰서 한다면,&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;success&quot;: true,
  &quot;message&quot;: &quot;목록 조회 성공&quot;,
  &quot;data&quot;: {
    &quot;items&quot;: [ // items: 실제 목록 데이터 배열
      { &quot;postId&quot;: 1, &quot;title&quot;: &quot;a&quot;, &quot;content&quot;: &quot;...&quot; },
      { &quot;postId&quot;: 2, &quot;title&quot;: &quot;b&quot;, &quot;content&quot;: &quot;...&quot; }
    ],
    &quot;page&quot;: 0,
    &quot;size&quot;: 20,
    &quot;totalElements&quot;: 132, // totalElements: 전체 데이터 개수
    &quot;totalPages&quot;: 7, // totalPages: 전체 페이지 수
    &quot;hasNext&quot;: true // hasNext: 다음 페이지 존재 여부
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;spring MVC Controller 예시&lt;/h1&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping
public ResponseEntity&amp;lt;ApiResponse&amp;lt;PageResponse&amp;lt;PostListItem&amp;gt;&amp;gt;&amp;gt; findALl(
	@RequestParam(defaultValue = &quot;0&quot;) int page,
	@RequestParam(defaultValue = &quot;20&quot;) int size,
	@RequestParam(required = false) String keywrod
){
	return ResponesEntity.ok(...);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RequestParam은 URL의 ?page=00&amp;amp;size=20같은 값을 받는 방법&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;defaultValue는 값 안 주면 기본 값&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;requried = false 는 없어도 되는 옵션 파라미터 (검색어 등)&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 필요 없고 내가 왜 어려워 했냐면, 응답 포맷이 안그려져서다&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;HTTP/1.1 200 OK
Content-Type: application/json
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 공통 응답 포맷 없이 그냥 반환 할 경우&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;{
  &quot;items&quot;: [
    { &quot;postId&quot;: 1, &quot;title&quot;: &quot;a&quot; },
    { &quot;postId&quot;: 2, &quot;title&quot;: &quot;b&quot; }
  ],
		&quot;page&quot;: 0,
		&quot;size&quot;: 20,
		&quot;totalElements&quot;: 132,
		&quot;totalPages&quot;: 7
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해, 132개의 게시글(totalElements)을 size 20개 단위로 한다면 7페이지(totalPages)다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;totalPages = ceil(132 / 20) = ceil(6.6) = 7&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억하자&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;items&quot;: [...],
  &quot;page&quot;: 0,
  &quot;size&quot;: 20,
  &quot;totalElements&quot;: 132,
  &quot;totalPages&quot;: 7
}

&lt;/code&gt;&lt;/pre&gt;</description>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/278</guid>
      <comments>https://winwin0219.tistory.com/278#entry278comment</comments>
      <pubDate>Mon, 19 Jan 2026 17:43:33 +0900</pubDate>
    </item>
    <item>
      <title>[아키텍처 패턴] CQRS</title>
      <link>https://winwin0219.tistory.com/277</link>
      <description>&lt;h1&gt;명령쿼리 책임 분리 패턴&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태를 변경하기 위한 명령을 위한 모델과 상태를 제공하는 조회(Query)를 위한 모델을 분리하는 패턴을 의미한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 Order라는 리소스를 Order(명령용), OrderData(조회용) 2개의 모델로 나누어서 관리할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order &amp;mdash; 명령용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OrderData &amp;mdash; 조회용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 OrderData를 이용해서 표현계층에 데이터를 출력하는 데 사용하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서는 Order를 활용해 변경을 수행할 수 있다&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CQRS 한 줄 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 (command)와 읽기 (Query)를 같은 모델/같은 설계로 억지로 처리하지 말고 아예 분리해서 각각 최적화하자는 패턴&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Command : 주문 취소, 결게, 회원가입처럼 상태가 바뀜&lt;/li&gt;
&lt;li&gt;Query : 주문서 조회, 사용자 목록처럼 상태를 읽기만 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 책임 분리 (모델/코드/DB까지도 분리될 수 있음)&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;CQRS 패턴을 따르면, 소프트웨어의 유지보수성을 높일 수 있고 모델 별로 성능이나 요구사항에 맞는 데이터베이스나 데이터 접근 기술을 사용할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 명령 모델은 트랜잭션이 지원되는 RDB를 사용하고 조회 모델은 조회 성능이 높은 NoSQL을 사용할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 NoSQL은 조회를 잘 할까?&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;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;수평 확장 &amp;mdash; 샤딩&lt;/li&gt;
&lt;li&gt;특정 목적 최소화
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Redis &amp;mdash; 메모리 기반 키-값 &amp;rarr; 초고속 조회 / 캐시&lt;/li&gt;
&lt;li&gt;Elasticsearch &amp;mdash; 검색 / 집계에 최적화된 인덱스 구조&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;단, 해당 방식은 명령 모델의 변경을 조회 모델로 전파하여 동기화시켜야 할 필요가 있다 또 다른 예시로, 단일 데이터베이스의 테이블에 대해 CQRS 패턴을 사용한다고 가정할 때는 명령 모델은 도메인 모델을 구현하는 데 유리한 JPA를 사용하고 조회 모델에서는 SQL 데이터 조회에 유리한 MyBatis를 사용할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 CQRS 패턴은 구현 코드가 많고 더 많은 구현 기술이 필요하다는 점이 단점이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 모델을 사용할 때 발생하는 복잡함 때문에 발생하는 구현 비용과 조회 전용 모델을 만들 때 발생하는 복잡함 때문에 발생하는 구현 비용을 비교해서 신중하게 도입을 결정해야 한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 책임 분리 &amp;rarr; 변경이 덜 번짐 &amp;rarr; 유지보수/확장/성능 튜닝에 유리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: 설계/코드/운영이 늘어남 &amp;rarr; 팀/서비스 규모에 따라 오버엔지니어링 될 수 있음&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Backend/  Architecture</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/277</guid>
      <comments>https://winwin0219.tistory.com/277#entry277comment</comments>
      <pubDate>Mon, 19 Jan 2026 17:23:07 +0900</pubDate>
    </item>
    <item>
      <title>[서버 라이프사이클] Graceful Shutdown</title>
      <link>https://winwin0219.tistory.com/276</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;span data-token-index=&quot;0&quot;&gt;서버를 끌 때, 갑자기 꺼버리지 말고 안전하게 정리하고 꺼라&lt;/span&gt; &lt;/span&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;h1&gt;&lt;b&gt;우아한 종료란&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 종료될 때 바로 종료하는 것이 아니라 현재 처리하고 있는 작업을 마무리하고 리소스를 정리한 이후 종료하는 방식을 의미한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 애플리케이션에서 일반적인 Graceful Shutdown는 SIGTERM 신호를 받았을 때 새로운 요청은 차단하고 기존 처리 중인 요청을 모두 완료한 뒤에 프로세스를 종료한다&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;h1&gt;SIGTERM VS SIGKILL&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유닉스 및 리눅스 운영체제에서 사용되는 프로세스 종료 시그널이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SIGKILL은 프로세스를 강제 종료하는 신호다 프로세스가 종료하기 전에 수행되어야 하는 절차들을 실행하지 않고 즉시 종료한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SIGTERM은 프로세스가 해당 시그널을 핸들링할 수 있다&lt;/p&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;OS(리눅스)가 프로세스(자바 앱)에게 끝내라~라고 말하는 방식이 시그널이고 그 종류가 SIGTERM / SIGKILL이다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SIGTERM &amp;mdash; 15번&lt;/li&gt;
&lt;/ol&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SIGKILL &amp;mdash; 9번&lt;/li&gt;
&lt;/ol&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Graceful Shotdown은 보통 SIGTERM을 받았을 때 동작하도록 설계한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;Graceful Shutdown이 실제로 서버에서 하는 일&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SIGTERM이 들어오면 즉시 종료 대신 보통 이런 순서로 한다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;새로운 요청을 더 이상 받지 않는다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;로드밸런서/프록시가 빼주거나, 앱 자체가 커넥션/요청을 거절하도록 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이미 처리 중인 요청은 끝까지 처리한다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;예를들어 DB 트랜잭션 commit/rollbace 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;리소스 정리한다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DB 커넥션 풀, 스레드 풀, 메시지 소비자, 파일 핸들 등 close&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;프로세스 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;Spring에서 Graceful Shutdown을 하는 방법은?&lt;/h1&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s // 타임 아웃
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 Graceful Shutdow 설정을 지원해준다&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;h3 data-ke-size=&quot;size23&quot;&gt;server.shutdown=graceful&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트(내장톰캣/제티/언더토우)가 종료 모드를 바꾼다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;immediate &amp;mdash; 기본이었던 시절&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종료 신호 오면 바로 닫아버림 (진행 중 요청도 끊길 수 있다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;graceful &amp;mdash; 종료 신호 오면 새 요청은 막고 진행 중 요청을 기다렸다가 종료&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 톰캣이 종료할 때 사용자 요청을 배려하는 모드로 바꿔라임&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;spring.lifecycle.timeout-per-shutdown-phase=20s&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 요청이 &lt;b&gt;무한루프&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;어떤 락이 풀리지 않아 &lt;b&gt;데드락&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;외부 API가 안 돌아와서 &lt;b&gt;계속 대기&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;스프링 부트 기본 종료 동작이랑 IDE/도커/쿠버네티스가 보내는 시그널 차이를 웹에서 확인해서 정리해보도록 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SpringBoot 4.0.1에서는 Graceful ShutDown이 기본 활성화다&lt;/p&gt;</description>
      <category>Backend</category>
      <author>HS0601</author>
      <guid isPermaLink="true">https://winwin0219.tistory.com/276</guid>
      <comments>https://winwin0219.tistory.com/276#entry276comment</comments>
      <pubDate>Mon, 19 Jan 2026 17:22:15 +0900</pubDate>
    </item>
  </channel>
</rss>