<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>최코딩의 개발</title>
    <link>https://balhae.tistory.com/</link>
    <description>코딩기록을 남기자</description>
    <language>ko</language>
    <pubDate>Thu, 2 Jul 2026 12:50:51 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>seung_ho_choi.s</managingEditor>
    <image>
      <title>최코딩의 개발</title>
      <url>https://tistory1.daumcdn.net/tistory/5477533/attach/456c3e365dc348b499a6e32772547984</url>
      <link>https://balhae.tistory.com</link>
    </image>
    <item>
      <title>[3부] CS 과제 공부하기</title>
      <link>https://balhae.tistory.com/344</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; &lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;1. JDBC 대량 데이터 처리&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;1-1. 개요&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/118&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/118&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769062052306&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;섹션1  JDBC 이해&quot; data-og-description=&quot;최코딩의 개발 섹션1 JDBC 이해 본문 스프링/스프링 DB 섹션1 JDBC 이해 seung_ho_choi.s 2023. 10. 22. 00:26&quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/118&quot; data-og-url=&quot;https://balhae.tistory.com/118&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/53gxS/dJMb9cBCdL2/dif75FpKCtcKfdSWma6eMk/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/118&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/118&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/53gxS/dJMb9cBCdL2/dif75FpKCtcKfdSWma6eMk/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;섹션1 JDBC 이해&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최코딩의 개발 섹션1 JDBC 이해 본문 스프링/스프링 DB 섹션1 JDBC 이해 seung_ho_choi.s 2023. 10. 22. 00:26&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2년 5개월전에 JDBC 생태계를 작성해봤다. 해당 블로그에 레거시한 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;자바에서 데이터베이스에 대량의 데이터를 삽입하거나 수정할 때, &lt;b&gt;executeUpdate()&lt;/b&gt;를 반복해서 호출하는 것은 &lt;b&gt;성능에 치명적&lt;/b&gt;이다. 이를 해결하기 위해 JDBC는 여러 SQL을 묶어서 한 번에 보내는 &lt;b data-index-in-node=&quot;114&quot; data-path-to-node=&quot;4&quot;&gt;Batch(배치)&lt;/b&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;1-2. 표준 Batch 처리 (addBatch, executeBatch)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1769062589265&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pstmt = conn.prepareStatement(sql);
for (int i = 1; i &amp;lt;= 5000; i++) {
    pstmt.setString(1, &quot;Data_&quot; + i);
    pstmt.addBatch(); // 배치 실행 요청을 메모리에 누적

    // 1000건 단위로 나누어 전송 (메모리 및 네트워크 효율 고려)
    if (i % 1000 == 0) {
        pstmt.executeBatch(); // 누적된 배치를 한 번에 DB로 전송
        pstmt.clearBatch();   // 배치 버퍼 초기화
    }
}
pstmt.executeBatch(); // 잔여 배치 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;표준 JDBC 인터페이스에서 제공하는 방식으로, 대부분의 DBMS(MySQL, Oracle, PostgreSQL 등)에서 공통적으로 사용된다. 개발자가 명시적으로 전송 시점을 결정하는 점이 특징이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;작동 원리:&lt;/b&gt; addBatch()를 통해 SQL을 메모리 큐에 쌓고, executeBatch()를 호출하는 순간 네트워크를 통해 DB로 전송한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;장점:&lt;/b&gt; DB에 의존적이지 않아 코드 이식성이 높고, 전송 타이밍을 개발자가 정밀하게 제어할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, 다수의 SQL 실행 요청을 클라이언트 메모리에 누적하고, executeBatch() 호출 시 이를 하나의 요청으로 DB에 전달함으로써, 1,000건의 개별 SQL 실행으로 발생하던 네트워크 왕복 비용을 단 한 번으로 줄일 수 있다.&lt;/b&gt; &lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1-3. 오라클 전용 Batch 처리 (setExecuteBatch, sendBatch)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/21485461/running-batch-statements-in-oracle-sendbatch-vs-executebatch&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stackoverflow.com/questions/21485461/running-batch-statements-in-oracle-sendbatch-vs-executebatch&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769063098300&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;running batch statements in Oracle: sendBatch() vs executeBatch()&quot; data-og-description=&quot;I'm writing Java code to execute a batch of insert statements into an Oracle database. I've seen in some of the documentation (http://docs.oracle.com/cd/B28359_01/java.111/b31224/oraperf.htm) that ...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/21485461/running-batch-statements-in-oracle-sendbatch-vs-executebatch&quot; data-og-url=&quot;https://stackoverflow.com/questions/21485461/running-batch-statements-in-oracle-sendbatch-vs-executebatch&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3nZQx/dJMb85WNlhl/H2EFKaN4bxrnQtmewgklf0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/21485461/running-batch-statements-in-oracle-sendbatch-vs-executebatch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/21485461/running-batch-statements-in-oracle-sendbatch-vs-executebatch&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3nZQx/dJMb85WNlhl/H2EFKaN4bxrnQtmewgklf0/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;running batch statements in Oracle: sendBatch() vs executeBatch()&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I'm writing Java code to execute a batch of insert statements into an Oracle database. I've seen in some of the documentation (http://docs.oracle.com/cd/B28359_01/java.111/b31224/oraperf.htm) that ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정보가 없어서 위 링크를 참고했다. (AI는 100% 신뢰 못하므로..!)&lt;/p&gt;
&lt;pre id=&quot;code_1769063175296&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 오라클 전용 방식 (캐스팅 필요)
OraclePreparedStatement opstmt = (OraclePreparedStatement) conn.prepareStatement(sql);

// 100건이 쌓이면 자동으로 전송하도록 설정
opstmt.setExecuteBatch(100); 

for (int i = 1; i &amp;lt;= 250; i++) {
    opstmt.setString(1, &quot;Oracle_&quot; + i);
    // 100, 200번째에 자동으로 네트워크 전송 발생
    opstmt.executeUpdate(); 
}

// 나머지 50건은 설정값에 도달하지 못했으므로 수동 전송
opstmt.sendBatch();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;오라클 JDBC 드라이버(OraclePreparedStatement)에서만 제공하는 독자적인 기능이다. 드라이버가 내부적으로 설정된 개수만큼 데이터를 관리하다가 자동으로 전송하는 &lt;b data-index-in-node=&quot;100&quot; data-path-to-node=&quot;13&quot;&gt;묵시적 방식&lt;/b&gt;으로 동작한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,0,0&quot;&gt;setExecuteBatch(int size):&lt;/b&gt; 배치 크기를 미리 설정한다. 이 설정값만큼 executeUpdate()가 호출되면 드라이버가 알아서 DB로 데이터를 보낸다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;sendBatch():&lt;/b&gt; 설정된 크기(size)에 아직 도달하지 않았더라도, 현재 쌓여있는 데이터를 즉시 강제로 전송할 때 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;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;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;2. Oracle DB 동기화 및 이중화(RAC/HA)&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;DB를 운영할 때 가장 중요한 것은 &lt;b&gt;&quot;서비스가 중단되지 않는 것&quot;&lt;/b&gt;과 &lt;b&gt;&quot;데이터를 잃지 않는 것&quot;&lt;/b&gt;이다. 이 두 마리 토끼를 잡기 위한 핵심 기술들을 소개하겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;2-1. HA (High Availability: 고가용성) &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;개념:&lt;/b&gt; 운영 DB 인스턴스가 죽으면, 대기하고 있던 서버에서 인스턴스를 새로 올려 서비스를 이어받는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;DB 매커니즘:&lt;/b&gt; OS 레벨의 클러스터 소프트웨어가 '인스턴스 A'를 감시합니다. 장애 시 A를 강제 종료하고, 공유 저장소의 소유권을 서버 B로 넘긴 뒤 '인스턴스 B'를 새로 기동(Cold Failover)한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;비유:&lt;/b&gt; 요리사가 요리하다 쓰러지면, 집에서 쉬던 요리사가 연락받고 달려와 주방을 이어받는다. (출근 시간 동안 주방 정지)&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;장점:&lt;/b&gt; 구성이 단순하고 라이선스 비용이 RAC보다 저렴하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,2,0&quot;&gt;단점:&lt;/b&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;&lt;b&gt;도식화:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769068248312&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# HA (High Availability) - 전체 구조

[ 유저 ]
   │
   ├──▶ [WAS 서버 A]  ──┐
   │                    │
   └──▶ [WAS 서버 B]  ──┤
                        │
                        ├──▶ [DB 서버 A]              [DB 서버 B]
                        │      └─ 인스턴스 A             └─ 인스턴스 B
                        │         (Active)                  (Standby)
                        │            │                          │
                        └────────────┴──▶ [공유 디스크] ◀───────┘
                                            (DB 스토리지)
                                            
WAS는 1대여도 상관X&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2. OPS (Oracle Parallel Server): RAC의 조상&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10&quot;&gt;개념:&lt;/b&gt; 여러 개의 인스턴스가 하나의 데이터베이스(공유 저장소)를 공유하는 최초의 모델이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;DB 매커니즘:&lt;/b&gt; 여러 인스턴스가 동시에 가동되지만, 인스턴스 간 데이터 교환 기능이 없다. A 인스턴스가 수정 중인 데이터를 B가 읽으려면 반드시 &lt;b data-index-in-node=&quot;85&quot; data-path-to-node=&quot;11,0,0&quot;&gt;디스크(Storage)에 쓰고 읽는 과정&lt;/b&gt;이 필요했습니다. 이를 &lt;b data-index-in-node=&quot;120&quot; data-path-to-node=&quot;11,0,0&quot;&gt;Disk Ping&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0&quot;&gt;비유:&lt;/b&gt; 두 요리사가 한 주방에서 일하는데, 서로 대화를 안 하고 메모지를 벽(디스크)에 붙여서 소통한다. (답답하고 느림)&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,1,0&quot;&gt;장점:&lt;/b&gt; 여러 서버를 동시에 사용(Active-Active)하는 개념을 처음 도입했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,2,0&quot;&gt;단점:&lt;/b&gt; 디스크 I/O가 빈번하게 발생하여 노드(서버)를 늘려도 성능이 오히려 떨어지는 병목 현상이 심했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,3,0&quot;&gt;도식화&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769068804763&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# OPS (Oracle Parallel Server) - 전체 구조 (정리 버전)

[ User ]
   |
   v
[ WAS (1대든 여러대든 OK) ]
   |
   v
+------------------- Oracle OPS Cluster -------------------+
|                                                          |
|   [DB Server A]                          [DB Server B]   |
|   (Instance A)                           (Instance B)    |
|        |                                      |          |
|        |     (인스턴스 간 전용 통신망 없음)        |          |
|        |     (메모리&amp;harr;메모리 교환 없음)             |          |
|        |                                      |          |
|        +---------------&amp;gt; [ Shared Disk ] &amp;lt;--------------+ |
|                        (DB files on Storage)             |
|                                                          |
|      Disk Ping = 디스크를 중계로 데이터 동기화/교환        |
|                (Instance A &amp;rarr; Disk &amp;rarr; Instance B)          |
|                                                          |
+----------------------------------------------------------+&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-3. RAC (Real Application Clusters): 현대 가용성의 표준&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15&quot;&gt;개념:&lt;/b&gt; OPS의 성능 문제를 &lt;b data-index-in-node=&quot;16&quot; data-path-to-node=&quot;15&quot;&gt;Cache Fusion&lt;/b&gt; 기술로 해결한 진보된 공유 구조이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,0,0&quot;&gt;DB 매커니즘:&lt;/b&gt; 인스턴스 간 고속 전용망(Interconnect)을 구축합니다. A 인스턴스 메모리(SGA)에 있는 데이터를 디스크를 거치지 않고 &lt;b data-index-in-node=&quot;82&quot; data-path-to-node=&quot;16,0,0&quot;&gt;직접 B 인스턴스 메모리로 쏴줍니다.&lt;/b&gt; 이를 통해 &lt;b&gt;'디스크 핑'&lt;/b&gt;을 제거했다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,1,0&quot;&gt;비유:&lt;/b&gt; 요리사들이 무전기를 차고 실시간으로 대화하며 재료를 주고받음. 한 명의 요리사가 쓰러져도 옆 사람이 즉시 요리를 낚아챈다. (중단 없음, 고성능)&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt; 서비스 중단 없는 고가용성(High Availability)과 서버를 추가하면 성능이 올라가는 확장성(Scalability)을 동시에 만족한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,2,0&quot;&gt;단점:&lt;/b&gt; 라이선스 비용이 매우 비싸고, 인스턴스 간 통신을 관리하는 클러스터 웨어 설정이 복잡하다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,3,0&quot;&gt;도식화&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769068853777&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# RAC (Real Application Clusters) - 전체 구조 (정리 버전)

[ User ]
   |
   v
[ WAS (1대든 여러대든 OK) ]
   |
   v
+------------------- Oracle RAC Cluster -------------------+
|                                                          |
|   [DB Server A]                          [DB Server B]   |
|   (Instance A)                           (Instance B)    |
|        |                                      |          |
|        |&amp;lt;========= Interconnect (Private) ========&amp;gt;|     |
|        |        (Cache Fusion: 메모리 블록 전송)       |     |
|        |                                      |          |
|        +---------------&amp;gt; [ Shared Disk ] &amp;lt;--------------+ |
|                        (DB files on Storage)             |
|                                                          |
+----------------------------------------------------------+&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-4. 데이터 동기화&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2-4-1. 개요&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 동기화는 두 개 이상의 데이터베이스 간에 데이터를 일치시키는 과정이다. 서로 다른 위치나 시스템에 있는 데이터베이스들이 동일한 정보를 유지하도록 하는 것이 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2-4-2. 주요 동기화 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단방향 동기화&lt;/b&gt;는 하나의 마스터 데이터베이스에서 하나 이상의 슬레이브 데이터베이스로 데이터가 흐른다. 주로 읽기 성능을 향상시키기 위한 복제(Replication)에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;양방향 동기화&lt;/b&gt;는 여러 데이터베이스가 모두 쓰기와 읽기가 가능하며, 변경사항이 서로 동기화된다. 충돌 해결 메커니즘이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2-4-2. 동기화 시점&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;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;3. DataBase CDC (Change Data Capture ) 개념&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-1.&amp;nbsp; 개념&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;Change Data Capture (CDC)는 데이터베이스의 변화를 실시간으로 감지하고 캡처하는 기술이다. 이를 조금더 직관적으로 해석 한다면, 데이터베이스에서의 삽입(insert), 수정(update), 삭제(delete)와 같은 이벤트가 발생할 때마다 순서대로 기록한다.&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-2. 동작&lt;/b&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;249&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b86WuH/dJMcabQvW8R/VGhQMbhjY06qG1E1g7tZJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b86WuH/dJMcabQvW8R/VGhQMbhjY06qG1E1g7tZJK/img.png&quot; data-alt=&quot;https://monday9pm.com/what-is-the-cdc-and-what-can-it-do-2cd4a002b061&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b86WuH/dJMcabQvW8R/VGhQMbhjY06qG1E1g7tZJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb86WuH%2FdJMcabQvW8R%2FVGhQMbhjY06qG1E1g7tZJK%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;931&quot; height=&quot;249&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;249&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://monday9pm.com/what-is-the-cdc-and-what-can-it-do-2cd4a002b061&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;위 그림은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;DMS( Database Migration Service)&lt;/b&gt; 를 통해 Kafka 가 MySQL 의 &lt;b&gt;CDC 이벤트&lt;/b&gt;를 받은 메시지의 예시이&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;다. 데이터 변경의 타입, 데이터, 테이블, 시간, 트랜젝션ID 등의 정보가 담겨있다. &amp;ldquo;x 테이블의 x 데이터가 언제 어떤 순서로 변경 되었다&amp;rdquo; 판단하는데 도움을 준다.&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;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;insert, update, delete 를 흔히 데이터 조작어 (DML: Data Manipulation Language)라고 부른다. CDC 는 DML 뿐 아니라 데이터 정의어 (DDL: Data Definition Language) 도 기록된다. DDL 이벤트를 활용하면 완전히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;같은 스키마를 유지하는 데이터를 복제하여 유지&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;CDC가 구현되기 위해서는 DBMS (DataBase Management System) 의 도움이 필요한다. DBMS 는 관계형 데이터베이스인 PostgreSQL, MySQL 또는 No SQL 인 MongoDB, Cassandra, DynamoDB 등이 있다. 이 중에서 PostgreSQL 은 pg_wal 폴더 안에 WAL 파일에 CDC 이벤트를 기록한다.&amp;nbsp;&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;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;정리하자면&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;45ba&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;b&gt;MySQL&lt;/b&gt;: binlog (Binarly Log)&lt;/li&gt;
&lt;li id=&quot;3f55&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;b&gt;PostgreSQL&lt;/b&gt;: WAL(Write Ahead Log)&lt;/li&gt;
&lt;li id=&quot;71fd&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;b&gt;MongoDB&lt;/b&gt;: Oplog(Operations Log)&lt;/li&gt;
&lt;li id=&quot;a78f&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;b&gt;Apache Cassandra&lt;/b&gt;: commitlog&lt;/li&gt;
&lt;li id=&quot;f625&quot; style=&quot;list-style-type: disc; color: #242424;&quot; data-selectable-paragraph=&quot;&quot;&gt;&lt;b&gt;DynamoDB&lt;/b&gt;: DynanaDB Streams / ON&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;이와 같다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot;&gt;3-3. CDC로 할 수 있는거&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p id=&quot;6708&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-3-1. 특정시점으로 복구&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터베이스에 문제가 생겼을 때, 단순히 '어제의 백업본'으로 돌아가는 것이 아니라 &quot;오늘 오후 2시 10분 5초&quot;와 같이 아주 정밀한 시점으로 복구하는 기술이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;eb34&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-3-2. 실시간 데이터 복제(위쪽에 동기화)&lt;/b&gt;&lt;/p&gt;
&lt;p id=&quot;7d98&quot; style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;운영 중인 메인 DB의 데이터를 타겟 시스템(분석용 DB, 검색 엔진, 캐시 서버 등)에 실시간으로 똑같이 맞추는 작업이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-3-3. 무중단 데이터 이관&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스를 멈추지 않고 운영 중인 DB를 새로운 서버나 클라우드로 옮기는 고난도 작업이다.&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-4. 참고&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #242424; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://monday9pm.com/what-is-the-cdc-and-what-can-it-do-2cd4a002b061&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://monday9pm.com/what-is-the-cdc-and-what-can-it-do-2cd4a002b061&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;b&gt;4. Oracle 주요 객체와 트랜잭션 메커니즘 정리&lt;/b&gt;&lt;br /&gt;(Function / Procedure / Package / Trigger / Redo&amp;middot;Undo / Commit&amp;middot;Rollback) &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&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;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;4-1. PL/SQL 객체 (Function, Procedure, Package, Trigger) &lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-1-1. Function(함수)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;특정 연산을 수행하고 결과를 &lt;b data-index-in-node=&quot;16&quot; data-path-to-node=&quot;3&quot;&gt;반드시 하나의 값&lt;/b&gt;으로 돌려주는 객체다&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;4&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.4651%;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.6976%;&quot;&gt;&lt;b&gt;장점&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.6047%;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.4651%;&quot;&gt;&lt;span data-path-to-node=&quot;4,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,0,0&quot;&gt;Function&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 45.6976%;&quot;&gt;&lt;span data-path-to-node=&quot;4,1,1,0&quot;&gt;SQL 문장(SELECT, WHERE 등) 안에서 직접 호출할 수 있어 계산 로직의 재사용성이 매우 높다&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.6047%;&quot;&gt;&lt;span data-path-to-node=&quot;4,1,2,0&quot;&gt;대량의 데이터를 조회할 때 각 행마다 함수가 실행되면 성능이 급격히 떨어질 수 있다&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1769143955363&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE OR REPLACE FUNCTION fn_get_tax_price(p_price NUMBER) 
RETURN NUMBER IS
BEGIN
    -- 가격에 1.1을 곱해 부가세 포함 금액 반환
    RETURN (p_price * 1.1); 
END;

-- 사용 예시: SELECT name, fn_get_tax_price(price) FROM products;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;실제 예시:&lt;/b&gt; 상품 가격을 입력하면 부가세(10%)가 포함된 금액을 계산해 주는 함수&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-1-2. Procedure (프로시저)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;특정 비즈니스 로직을 일괄 처리하기 위한 &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;9&quot;&gt;실행 단위&lt;/b&gt;다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 60px;&quot; border=&quot;1&quot; data-path-to-node=&quot;10&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; width: 10.6977%;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 46.9768%;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 42.2093%;&quot;&gt;&lt;b&gt;단점&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 10.6977%;&quot;&gt;&lt;span data-path-to-node=&quot;10,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0,0&quot;&gt;Procedure&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 46.9768%;&quot;&gt;&lt;span data-path-to-node=&quot;10,1,1,0&quot;&gt;여러 개의 쿼리를 하나의 묶음으로 처리하여 네트워크 부하를 줄이고 로직을 캡슐화할 수 있다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 42.2093%;&quot;&gt;&lt;span data-path-to-node=&quot;10,1,2,0&quot;&gt;일반적인 SQL문(SELECT) 내부에서 함수처럼 호출하여 값을 가져올 수는 없다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;11,0,0&quot;&gt;실제 예시:&lt;/b&gt; 특정 부서 사원들의 급여를 일괄적으로 10% 인상하는 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1769144108691&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE OR REPLACE PROCEDURE sp_raise_salary(p_dept_id NUMBER) IS
BEGIN
    UPDATE employees 
    SET salary = salary * 1.1 
    WHERE department_id = p_dept_id;
    
    COMMIT; -- 절차 완료 후 확정
END;

-- 사용 예시: EXEC sp_raise_salary(10);&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;==&amp;gt; 즉&amp;nbsp; 함수는 &amp;ldquo;값을 계산해 돌려주는 것&amp;rdquo;이고, 프로시저는 &amp;ldquo;작업을 수행하는 것&amp;rdquo;이다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-1-3. Package (패키지)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;관련 있는 함수와 프로시저를 &lt;b data-index-in-node=&quot;16&quot; data-path-to-node=&quot;15&quot;&gt;하나의 그룹&lt;/b&gt;으로 묶어 관리하는 저장소다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 60px;&quot; border=&quot;1&quot; data-path-to-node=&quot;16&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px; width: 11.8605%;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 43.3721%;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px; width: 44.6512%;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px; width: 11.8605%;&quot;&gt;&lt;span data-path-to-node=&quot;16,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,1,0,0&quot;&gt;Package&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 43.3721%;&quot;&gt;&lt;span data-path-to-node=&quot;16,1,1,0&quot;&gt;관련 로직을 모듈화하여 관리가 쉽고, 패키지 내 변수를 공유할 수 있으며 메모리 효율이 좋다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px; width: 44.6512%;&quot;&gt;&lt;span data-path-to-node=&quot;16,1,2,0&quot;&gt;선언부(Spec)와 본체(Body)를 따로 작성해야 해서 초기 설계와 구현에 공수가 더 든다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre id=&quot;code_1769145840659&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 1. 선언부 (명세서)
CREATE OR REPLACE PACKAGE emp_admin_pkg AS
    PROCEDURE add_emp(p_name VARCHAR2);
    FUNCTION get_emp_count RETURN NUMBER;
END emp_admin_pkg;

-- 2. 본체 (실제 구현)
CREATE OR REPLACE PACKAGE BODY emp_admin_pkg AS
    PROCEDURE add_emp(p_name VARCHAR2) IS BEGIN ... END;
    FUNCTION get_emp_count RETURN NUMBER IS BEGIN ... END;
END emp_admin_pkg;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4-1-4. Trigger (트리거)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;특정 사건(Insert, Update, Delete)이 일어날 때 &lt;b data-index-in-node=&quot;37&quot; data-path-to-node=&quot;21&quot;&gt;자동으로 실행&lt;/b&gt;되는 감시자다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;22&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 8.13953%;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.0698%;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 8.13953%;&quot;&gt;&lt;span data-path-to-node=&quot;22,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,1,0,0&quot;&gt;Trigger&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.0698%;&quot;&gt;&lt;span data-path-to-node=&quot;22,1,1,0&quot;&gt;데이터 무결성을 강제하거나, 로그 기록(Auditing) 등 수동으로 누락하기 쉬운 작업을 자동화한다.&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot;&gt;&lt;span data-path-to-node=&quot;22,1,2,0&quot;&gt;너무 많아지면 &quot;왜 데이터가 바뀌었지?&quot;를 추적하기 어렵고 전체적인 DB 성능을 예측하기 힘들다.&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;23,0,0&quot;&gt;실제 예시:&lt;/b&gt; 사원 테이블의 정보가 삭제될 때, 누가 삭제했는지 이력 테이블에 자동으로 저장함&lt;/p&gt;
&lt;pre id=&quot;code_1769146459365&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE OR REPLACE TRIGGER trg_emp_delete_log
AFTER DELETE ON employees FOR EACH ROW
BEGIN
    -- 삭제된 사원의 이름과 현재 시간을 로그 테이블에 기록
    INSERT INTO emp_history(emp_name, del_date, user_id)
    VALUES (:OLD.first_name, SYSDATE, USER);
END;

-- 사용자가 DELETE 문만 실행해도 위 로직이 자동 실행됨~~꼸&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4-2. Redo Log vs Undo Log&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;이 두 로그는 데이터의 안전과 일관성을 책임지는 &quot;기록장&quot;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;Redo Log (다시 하기):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,1,0,0&quot;&gt;역할:&lt;/b&gt; 변경된 모든 내용을 기록합니다. DB가 갑자기 꺼졌을 때, 로그를 보고 마지막 상태로 &lt;b data-index-in-node=&quot;52&quot; data-path-to-node=&quot;8,0,1,0,0&quot;&gt;재현한&lt;/b&gt;다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,1,1,0&quot;&gt;장점:&lt;/b&gt; 장애 시 데이터 유실 방지 (복구용)&lt;/li&gt;
&lt;li&gt;&lt;b&gt; 내 작업이 절대로 사라지지 않게 기록하는 것이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;Undo Log (되돌리기):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,1,0,0&quot;&gt;역할:&lt;/b&gt; 변경 전의 데이터를 보관합니다. 사용자가 작업을 취소하거나, 다른 사용자가 수정 중인 데이터를 조회할 때 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,1,1,0&quot;&gt;장점:&lt;/b&gt; 작업 취소 가능, 작업 중에도 다른 사람에게 이전 데이터 보여준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;즉, 내가 한 작업을 없던 일로 하거나, 남들에게 예전 데이터를 보여주기 위해 원본을 기록 &lt;/b&gt;하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;br /&gt;4-3. Commit vs Rollback&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭션을 끝내는 두 가지 방법이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;Commit:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,1,0,0&quot;&gt;의미:&lt;/b&gt; &quot;지금까지 한 작업을 DB에 진짜로 반영해!&quot;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,1,1,0&quot;&gt;결과:&lt;/b&gt; Undo 세그먼트의 정보가 불필요해지고, Redo 로그에 기록된 내용이 영구 저장된다. 다른 사용자도 내가 수정한 데이터를 볼 수 있게 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점:&lt;/b&gt; 데이터 변경을 확정 짓고 다른 사용자에게 공유한다. 작업 결과가 영구적으로 보존된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점:&lt;/b&gt; 한 번 커밋하면 이전 상태로 되돌리기가 매우 까다롭다(복구 작업 필요)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;Rollback:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,1,0,0&quot;&gt;의미:&lt;/b&gt; &quot;아차, 실수했다! 작업하기 전으로 다 돌려놔!&quot;&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,1,1,0&quot;&gt;결과:&lt;/b&gt; &lt;b data-index-in-node=&quot;4&quot; data-path-to-node=&quot;12,1,1,1,0&quot;&gt;Undo Log&lt;/b&gt;에 저장해뒀던 '변경 전 데이터'를 다시 덮어써서 원래 상태로 복구한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장점:&lt;/b&gt; 실수하거나 오류가 난 작업을 즉시 취소하여 데이터 오염을 막을 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점:&lt;/b&gt; 이미 커밋된 데이터는 롤백할 수 없다. 즉 커밋 완료 시점 전으로 갈 수가 없다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;==&amp;gt; 즉 &lt;b&gt;Commit&lt;/b&gt;과 &lt;b&gt;Rollback&lt;/b&gt;은 사용자가 내리는 &lt;b&gt;'명령'&lt;/b&gt;이고, &lt;b&gt;Redo&lt;/b&gt;와 &lt;b&gt;Undo&lt;/b&gt;는 그 명령을 수행하기 위해 DB가 뒤에서 움직이는 &lt;b&gt;'수단'&lt;/b&gt;이라고 보면 된다. 더군다나 Redo는 &lt;b&gt;'결과'&lt;/b&gt;만 기록하고 Undo는 &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-path-to-node=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;300원에서 100원으로 수정하고 &lt;b&gt;커밋(Commit)&lt;/b&gt;을 완료한 시점의 최종 정리다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;2&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&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;td&gt;&lt;b&gt;핵심 역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;2,1,0,0&quot;&gt;현재 잔고&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,1,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;2,1,1,0&quot;&gt;100원&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,1,2,0&quot;&gt;커밋을 했으므로 현재 공식 데이터가 됨&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;2,2,0,0&quot;&gt;Redo&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,2,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;2,2,1,0&quot;&gt;100원&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,2,2,0&quot;&gt;&quot;100원으로 바꿨음&quot;을 기록 (DB가 깨지면 &lt;b data-index-in-node=&quot;26&quot; data-path-to-node=&quot;2,2,2,0&quot;&gt;100원&lt;/b&gt;으로 복구하기 위함)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;2,3,0,0&quot;&gt;Undo&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,3,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;2,3,1,0&quot;&gt;300원&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;2,3,2,0&quot;&gt;&quot;원래 300원이었음&quot;을 기록 (하지만 커밋 후엔 &lt;b data-index-in-node=&quot;28&quot; data-path-to-node=&quot;2,3,2,0&quot;&gt;버려지는 데이터&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회사/갤럭시아머니트리</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/344</guid>
      <comments>https://balhae.tistory.com/344#entry344comment</comments>
      <pubDate>Fri, 23 Jan 2026 15:39:22 +0900</pubDate>
    </item>
    <item>
      <title>[2부] CS 과제 공부하기</title>
      <link>https://balhae.tistory.com/343</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Byte Stream vs Character Stream &lt;/b&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;115&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v2QXo/dJMcafyB3ir/LTvQqT6BHG3sT2FsfeWpA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v2QXo/dJMcafyB3ir/LTvQqT6BHG3sT2FsfeWpA1/img.png&quot; data-alt=&quot;https://xxeol.tistory.com/45&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v2QXo/dJMcafyB3ir/LTvQqT6BHG3sT2FsfeWpA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv2QXo%2FdJMcafyB3ir%2FLTvQqT6BHG3sT2FsfeWpA1%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;704&quot; height=&quot;115&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;115&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://xxeol.tistory.com/45&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;
&lt;h4 data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1-1. 이론&lt;/b&gt;&lt;/h4&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;Stream은 데이터가 이동하는 통로이다. 데이터는 &lt;b&gt;FIFO형태&lt;/b&gt;로 전송된다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;Stream의 가장 큰 특징은 단방향으로만 흘러간다는 점인데, &lt;b&gt;하나의 스트림으로 입출력을 같이 처리할 수 없어 입출력을 위해서는 두 개의 스트림(InputStream/OutputStream)이 필요&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InputStream은 외부에서 데이터를 읽는(입력) 역할을 수행하며, OuputStream은 외부로 데이터를 출력하는 역할을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;Stream은 처리하는 데이터의 유형에 따라 크게 두 가지 유형으로 나뉜다.&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;1-2.&amp;nbsp; Byte Stream&lt;/b&gt;&lt;/h4&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 &lt;b data-index-in-node=&quot;5&quot; data-path-to-node=&quot;6&quot;&gt;8비트(1 byte)&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;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;InputStream, OutputStream&lt;/b&gt;를 상속하여 구현한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EDRzJ/dJMcaiIPGu5/CVuTMeOqbdwgX5bTtptnf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EDRzJ/dJMcaiIPGu5/CVuTMeOqbdwgX5bTtptnf0/img.png&quot; data-alt=&quot;https://xxeol.tistory.com/45&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EDRzJ/dJMcaiIPGu5/CVuTMeOqbdwgX5bTtptnf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEDRzJ%2FdJMcaiIPGu5%2FCVuTMeOqbdwgX5bTtptnf0%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;722&quot; height=&quot;659&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://xxeol.tistory.com/45&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;1byte는 범위가 0~255로 알파벳의 대/소문자 아스키 코드 값은 모두 해당 범위에 속한다. 하지만 한글의 아스키 코드 값을 나타내려면 2byte가 필요하고, 따라서 ByteStream에서는 한글이 깨진다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그래서&amp;nbsp;한글 File을 읽고 쓸 때는 Character Streams(Reader/Writer)를 활용해야한다.&lt;/b&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;1-3. Character Stream&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 &lt;b data-index-in-node=&quot;5&quot; data-path-to-node=&quot;11&quot;&gt;16비트(2 byte)&lt;/b&gt; 단위로 처리하며, 문자 인코딩을 자동으로 처리해 준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;텍스트 데이터(문자)&lt;/b&gt;를 처리하기 위해 설계되었습니다. &lt;b&gt;세계의 모든 언어(유니코드)&lt;/b&gt;를 안전하게 입출력할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;0 0 []&quot; data-ke-size=&quot;size16&quot;&gt;추상 클래스 &lt;b&gt;Reader, Writer&lt;/b&gt;를 상속하여 구현한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blPwdw/dJMcagRPvVY/JlYk7owW8YjHB6Eijc4GmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blPwdw/dJMcagRPvVY/JlYk7owW8YjHB6Eijc4GmK/img.png&quot; data-alt=&quot;https://xxeol.tistory.com/45&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blPwdw/dJMcagRPvVY/JlYk7owW8YjHB6Eijc4GmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblPwdw%2FdJMcagRPvVY%2FJlYk7owW8YjHB6Eijc4GmK%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;734&quot; height=&quot;642&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://xxeol.tistory.com/45&lt;/figcaption&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-4. 결론&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;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,0,0&quot;&gt;텍스트 파일을 읽거나 쓸 때:&lt;/b&gt; 무조건 &lt;b data-index-in-node=&quot;21&quot; data-path-to-node=&quot;18,0,0&quot;&gt;Character Stream&lt;/b&gt;을 권장한다. (특히 BufferedReader를 쓰면 성능이 훨씬 좋아진다.)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,1,0&quot;&gt;그 외 모든 파일(이미지, PDF 등):&lt;/b&gt; &lt;b data-index-in-node=&quot;23&quot; data-path-to-node=&quot;18,1,0&quot;&gt;Byte Stream&lt;/b&gt;을 사용해야 데이터 손실이 없다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,2,0&quot;&gt;바이트를 문자로 변환하고 싶을 때:&lt;/b&gt; InputStreamReader나 OutputStreamWriter라는 &quot;브릿지(Bridge) 스트림&quot;을 사용하면 바이트 스트림을 문자 스트림처럼 다룰 수 있다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;이때 브릿지 스트림은 브릿지 스트림(InputStreamReader, OutputStreamWriter)은 &lt;b&gt;캐릭터 스트림 계열에 속하지만&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;바이트 스트림과 캐릭터 스트림을 연결하는 변환 전용 스트림이다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 이 코드가 대표케이스 코딩테스트 공부할때 그냥 썼는데 정확히 공부하니깐 의문점들이 풀렸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;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;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;2. String, StringBuffer, StringBuilder 차이 및 장단점&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1. 개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;자바에서는 대표적으로 문자열을 다루는 자료형 클래스로 &lt;b&gt;String, StringBuffer, StringBuilder&lt;/b&gt; 라는 &lt;b&gt;3가지 자료형&lt;/b&gt;을 지원한다. &lt;/span&gt;&lt;span&gt;위 3가지 클래스 자료형은 모두 문자열을 다루는데 있어 공통적으로 사용되지만, 사용 목적에 따라 쓰임새가 많이 달라지게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 목차에는 &lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;String&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;StringBuffer&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;StringBuilder&lt;span&gt;&lt;span&gt; 클래스 차이점을 알아보고, 이 3가지 중 어느 상황에서 어느 자료형을 사용하는 것이 이상적이고 성능적으로는 어느것이 더 좋은지 총정리 해보는 시간을 가져보겠다.&lt;/span&gt;&lt;/span&gt; &lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;2-2. String&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적으로 자바에서는&amp;nbsp;String 객체의 값은 변경할 수 없다. 이는 한번 할당된 공간이 변하지 않는다고 해서 &lt;b&gt;'불변(immutable)' 자료형&lt;/b&gt; 이라고 불리운다. 그래서 초기공간과 다른 값에 대한 연산에서 많은 시간과 자원을 사용하게 된다는 특징이 있다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769044183708&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;;
result += &quot;hello&quot;;
result += &quot; &quot;;
result += &quot;jump to java&quot;;
System.out.println(result); // hello jump to java
// &amp;rarr; 심플하지만 연산 속도가 느리다는 단점이 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;263&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;263&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;String 자료형만으로도 + 연산자나 concat() 메소드를 사용해 문자열을 이어 붙일 수 있다. 그러나 + 연산자를 이용해 String 인스턴스의 문자열을 결합할 경우, &lt;b&gt;기존 문자열이 수정되는 것이 아니라 내용이 합쳐진 새로운 String 인스턴스가 매번 생성된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;384&quot; data-start=&quot;265&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;384&quot; data-start=&quot;265&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;386&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;538&quot; data-start=&quot;386&quot; data-ke-size=&quot;size16&quot;&gt;이는 마치 글을 한 줄씩 추가할 때마다 기존 종이에 이어서 쓰는 것이 아니라,&lt;br /&gt;&lt;b&gt;새 종이를 꺼내 기존 내용을 모두 다시 옮겨 적은 뒤 새로운 내용을 덧붙이는 것과 같다.&lt;/b&gt;&lt;br /&gt;문장이 길어질수록 종이를 더 많이 쓰게 되고, 다시 옮겨 적는 시간도 점점 늘어나게 된다.&lt;/p&gt;
&lt;h4 data-end=&quot;538&quot; data-start=&quot;386&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;2-3. StringBuffer&amp;nbsp;/&amp;nbsp;StringBuilder&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;2-3-1. 그들의 이론&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;287&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;이러한 비효율성을 해결하기 위해 자바에서는 문자열 연산을 전용으로 처리할 수 있는 StringBuffer(또는 StringBuilder) 클래스를 제공한다. 이들 클래스는 내부에 &lt;b&gt;버퍼(buffer)라는 독립적인 공간&lt;/b&gt;을 두어 기존 문자열을 복사하지 않고 &lt;b&gt;같은 공간에 문자열을 직접 추가&lt;/b&gt;하는 방식으로 동작한다.&lt;/p&gt;
&lt;pre id=&quot;code_1769044609566&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;StringBuffer sb = new StringBuffer();  // StringBuffer 객체 sb 생성
sb.append(&quot;hello&quot;);
sb.append(&quot; &quot;);
sb.append(&quot;jump to java&quot;);
String result = sb.toString();
System.out.println(result); // hello jump to java
// &amp;rarr; + 연산보다는 복잡해 보이지만 연산 속도가 빠르다는 장점이 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;287&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;287&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;이는 문자열을 이어 붙일 때마다 새 종이를 꺼내 다시 쓰는 대신, &lt;b&gt;하나의 화이트보드에 내용을 지우지 않고 계속 이어서 작성하는 것과 같다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;287&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;287&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;이미 적힌 내용을 다시 옮겨 적을 필요가 없기 때문에 불필요한 공간 낭비가 발생하지 않으며, 작업 속도 또한 훨씬 빠르다.&lt;/p&gt;
&lt;p data-end=&quot;561&quot; data-start=&quot;447&quot; data-ke-size=&quot;size16&quot;&gt;이와 같은 방식 덕분에 StringBuffer와 StringBuilder는 문자열을 반복적으로 결합해야 하는 상황에서&lt;br /&gt;&lt;b&gt;메모리 사용을 줄이고 문자열 연산 성능을 크게 향상시킬 수 있다.&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769044639322&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;StringBuffer sb = new StringBuffer(); // 기본 16 버퍼 크기로 생성

// sb.capacity() - StringBuffer 변수의 배열 용량의 크기 반환
System.out.println(sb.capacity()); // 16 
 
sb.append(&quot;1111111111111111111111111111111111111111&quot;); // 40길이의 문자열을 append
System.out.println(sb.capacity()); // 40 (추가된 문자열 길이만큼 늘어남)&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;StringBuffer의 버퍼(데이터 공간) 크기의 기본값은 16개의 문자&lt;/b&gt;를 저장할 수 있는 크기이며, 생성자를 통해 그 크기를 별도로 설정할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 문자열 연산중에 할당된 버퍼의 크기를 넘게 되면 &lt;b&gt;자동으로 버퍼를 증강&lt;/b&gt; 시키니 걱정 안해도 된다. &lt;b&gt;다만, 효율이 떨어질 수 있으므로 버퍼의 크기는 넉넉하게 잡는 것이 좋다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2-3-2. StringBuffer와 StringBuilder의 차이&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;270&quot; data-start=&quot;67&quot; data-ke-size=&quot;size16&quot;&gt;StringBuffer와 StringBuilder 클래스는 둘 다 크기가 유연하게 변하는 &lt;b&gt;가변적인 특성&lt;/b&gt;을 가지고 있으며, 제공하는 메서드와 사용 방법도 동일하다.&lt;/p&gt;
&lt;p data-end=&quot;270&quot; data-start=&quot;67&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그렇다면 이 둘의 차이점은 무엇일까? 어렵게 생각할 필요 없이, 사실 차이는 딱 한 가지이다.&lt;br /&gt;바로 &lt;b&gt;멀티 스레드(Thread) 환경에서 안전(thread-safe)하냐 아니냐의 차이&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;450&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;450&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;StringBuffer는 모든 메서드에 동기화(synchronized)가 적용되어 멀티 스레드 환경에서도 안전하지만,&lt;br /&gt;이로 인해 &lt;b&gt;불필요한 락(lock) 비용이 발생하여 성능이 상대적으로 떨어질 수 있다.&lt;/b&gt;&lt;br /&gt;반면 StringBuilder는 동기화를 지원하지 않는 대신 이러한 비용이 없어 더 빠르게 동작한다.&lt;/p&gt;
&lt;p data-end=&quot;450&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;3. Call by Value / Call by Reference / Reflection&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-1. 개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;335&quot; data-start=&quot;209&quot; data-ke-size=&quot;size16&quot;&gt;메서드를 호출할 때 전달되는 값이 &lt;b&gt;어떤 방식으로 전달되는지&lt;/b&gt;에 따라 프로그램의 동작 결과가 달라질 수 있다.&lt;br /&gt;이를 설명하는 개념이 바로 &lt;b&gt;Call by Value&lt;/b&gt;와 &lt;b&gt;Call by Reference&lt;/b&gt;이다. 특히 자바에서는 객체를 전달할 때 동작 방식이 직관적이지 않아 Call by Reference로 오해하는 경우가 많다.&lt;/p&gt;
&lt;p data-end=&quot;335&quot; data-start=&quot;209&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;556&quot; data-start=&quot;337&quot; data-ke-size=&quot;size16&quot;&gt;이번 목차에서는 Call by Value와 Call by Reference의 개념을 정리하고,&lt;br /&gt;자바에서는 어떤 호출 방식을 사용하는지 예제를 통해 살펴본다.&lt;br /&gt;또한 런타임 시점에 클래스 정보를 동적으로 다루는 &lt;b&gt;Reflection&lt;/b&gt; 개념까지 함께 정리한다.&lt;/p&gt;
&lt;h4 data-end=&quot;556&quot; data-start=&quot;337&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-2. Call by Value (값에 의한 호출)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;708&quot; data-start=&quot;598&quot; data-ke-size=&quot;size16&quot;&gt;Call by Value는 메서드 호출 시 &lt;b&gt;값(value)을 복사하여 전달하는 방식&lt;/b&gt;이다. 메서드 내부에서 전달받은 값을 변경하더라도&lt;br /&gt;&lt;b&gt;원본 변수에는 아무런 영향을 미치지 않는다. &lt;/b&gt;자바에서 기본 자료형(primitive type)은 모두 &lt;b&gt;Call by Value 방식&lt;/b&gt;으로 전달된다.&lt;/p&gt;
&lt;pre id=&quot;code_1769046488230&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Test {
    static void change(int x) {
        x = 10;
    }

    public static void main(String[] args) {
        int a = 5;
        change(a);
        System.out.println(a); // 5
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;708&quot; data-start=&quot;598&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;708&quot; data-start=&quot;598&quot; data-ke-size=&quot;size16&quot;&gt;위 코드에서 &lt;b&gt;change()&lt;/b&gt; 메서드는 &lt;b&gt;a&lt;/b&gt;의 값을 복사한 &lt;b&gt;x&lt;/b&gt;를 전달받는다.&lt;br /&gt;따라서 메서드 내부에서 &lt;b&gt;x&lt;/b&gt;의 값을 변경하더라도&lt;br /&gt;원본 변수 &lt;b&gt;a&lt;/b&gt;의 값은 변경되지 않는다.&lt;/p&gt;
&lt;h4 data-end=&quot;708&quot; data-start=&quot;598&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 3-3. Call by Reference (참조에 의한 호출)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1230&quot; data-start=&quot;1132&quot; data-ke-size=&quot;size16&quot;&gt;Call by Reference는 메서드 호출 시 &lt;b&gt;변수의 주소(참조)를 전달하는 방식&lt;/b&gt;이다.&lt;br /&gt;메서드 내부에서 값을 변경하면 &lt;b&gt;원본 변수의 값도 함께 변경된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1256&quot; data-start=&quot;1232&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은&lt;b&gt; C++과 같은 언어&lt;/b&gt;에서 지원된다. (Java XXX)&lt;/p&gt;
&lt;pre id=&quot;code_1769046655724&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;

void change(int&amp;amp; x) {
    x = 10;
}

int main() {
    int a = 5;
    change(a);
    cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; endl; // 10
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;708&quot; data-start=&quot;598&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;708&quot; data-start=&quot;598&quot; data-ke-size=&quot;size16&quot;&gt;위 코드에서 &lt;b&gt;int&amp;amp; x&lt;/b&gt;는 &lt;b&gt;a&lt;/b&gt;의 &lt;b&gt;메모리 주소를 직접 참조&lt;/b&gt;한다.&lt;br /&gt;따라서 함수 내부에서 값을 변경하면&lt;br /&gt;&lt;b&gt;main 함수&lt;/b&gt;의 &lt;b&gt;a 값&lt;/b&gt;도 함께 변경된다. &lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 3-4. Java는 Call by Value일까? Call by Reference일까? &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면 &lt;b&gt;자바는 100% Call by Value만 지원한다.&lt;/b&gt;&lt;br /&gt;다만 객체를 전달할 때 &lt;b&gt;참조값(reference value)을 값으로 전달&lt;/b&gt;하기 때문에&lt;br /&gt;Call by Reference처럼 보이는 경우가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1769046877881&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Box {
    int value;
}

public class Test {
    static void change(Box box) {
        box.value = 10;
    }

    public static void main(String[] args) {
        Box b = new Box();
        b.value = 5;
        change(b);
        System.out.println(b.value); // 10
    }
}&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;2077&quot; data-start=&quot;2012&quot; data-ke-size=&quot;size16&quot;&gt;위 예제에서는 객체의 참조값이 복사되어 전달되므로&lt;br /&gt;&lt;b&gt;같은 객체를 가리키게 되고, 그 결과 객체 내부 값이 변경된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2107&quot; data-start=&quot;2079&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 참조 자체를 변경하면 원본에는 영향이 없다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769047537188&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static void change(Box box) {
    box = new Box();
    box.value = 10;
}&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;이 경우 box는 새로운 객체를 가리키게 되며&lt;br /&gt;&lt;b&gt;원본 객체 b는 변경되지 않는다.&lt;/b&gt;&lt;br /&gt;이를 통해 자바가 Call by Value 방식임을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 자바의 리플렉션&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4-1. 개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://balhae.tistory.com/309&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/309&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769151229536&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;[리메이크] 스프링 컨테이너와 빈에 관해서&quot; data-og-description=&quot;  스프링 컨테이너란?스프링 컨테이너는 자바 객체(빈, Bean)를 생성하고 관리하며, 의존관계를 자동으로 연결해주는 역할을 한다. 단순한 객체 생성만 담당하는 것이 아니라, 생명주기 관리, &quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/309&quot; data-og-url=&quot;https://balhae.tistory.com/309&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bEvyWT/dJMb89x7JIj/Atz2IkVazA1OKiZQzvZMa0/img.jpg?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/TRGQH/dJMb85vI268/xTDcc7ULaCOLmL5VutxNWK/img.jpg?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/3LS0S/dJMb81fMONG/kLEWrIecOcB1Eax4PKFct1/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/309&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/309&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bEvyWT/dJMb89x7JIj/Atz2IkVazA1OKiZQzvZMa0/img.jpg?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/TRGQH/dJMb85vI268/xTDcc7ULaCOLmL5VutxNWK/img.jpg?width=768&amp;amp;height=500&amp;amp;face=0_0_768_500,https://scrap.kakaocdn.net/dn/3LS0S/dJMb81fMONG/kLEWrIecOcB1Eax4PKFct1/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;[리메이크] 스프링 컨테이너와 빈에 관해서&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  스프링 컨테이너란?스프링 컨테이너는 자바 객체(빈, Bean)를 생성하고 관리하며, 의존관계를 자동으로 연결해주는 역할을 한다. 단순한 객체 생성만 담당하는 것이 아니라, 생명주기 관리,&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.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;b&gt;리플렉션은&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4-2. 동작 과정&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;Class 객체 획득 - 클래스 정보를 담고 있는 메타데이터 객체를 가져온다&lt;/li&gt;
&lt;li&gt;필요한 정보 조회 - 필드, 메서드, 생성자 등의 정보를 Field, Method, Constructor 객체로 얻는다&lt;/li&gt;
&lt;li&gt;접근 권한 설정 - setAccessible(true)로 private 멤버 접근을 허용한다&lt;/li&gt;
&lt;li&gt;실행/조작 - 메서드 호출, 필드 값 변경, 객체 생성 등을 수행한다&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4-3. 주요 메서드&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-3-1.&amp;nbsp;Class 객체 얻기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Class&amp;lt;?&amp;gt; clazz1 = Class.forName(&quot;com.example.User&quot;);
Class&amp;lt;?&amp;gt; clazz2 = User.class;
Class&amp;lt;?&amp;gt; clazz3 = user.getClass();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-3-2. 필드 관련&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;Field[] fields = clazz.getDeclaredFields();  // 모든 필드
Field field = clazz.getDeclaredField(&quot;name&quot;); // 특정 필드
field.setAccessible(true);                    // 접근 허용
Object value = field.get(instance);           // 값 읽기
field.set(instance, newValue);                // 값 쓰기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-3-3. 메서드 관련&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;Method[] methods = clazz.getDeclaredMethods();           // 모든 메서드
Method method = clazz.getMethod(&quot;getName&quot;, String.class); // 특정 메서드
method.setAccessible(true);                              // 접근 허용
Object result = method.invoke(instance, args);           // 메서드 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-3-4. 생성자 관련&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;Constructor&amp;lt;?&amp;gt; constructor = clazz.getConstructor(String.class, int.class);
Object instance = constructor.newInstance(&quot;홍길동&quot;, 25);
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-3-5. 어노테이션 관련&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;boolean hasAnnotation = method.isAnnotationPresent(Override.class);
Annotation annotation = method.getAnnotation(ApiEndpoint.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4-4. 실제 활용 사례&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 프레임워크 개발&lt;/b&gt; - Spring의 의존성 주입(@Autowired), Hibernate의 ORM 매핑이 리플렉션으로 동작한다. 런타임에 어노테이션을 읽어 빈을 자동 주입하거나 테이블과 객체를 매핑한다.&lt;/p&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; - Jackson, Gson 같은 JSON 라이브러리가 객체의 필드를 분석해 JSON으로 변환하거나 그 반대 작업을 수행한다.&lt;/p&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; - JUnit이 @Test 어노테이션이 붙은 메서드를 찾아 실행한다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 동적 프록시&lt;/b&gt; - AOP, 로깅, 트랜잭션 관리 등을 위해 런타임에 프록시 객체를 생성한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4-5. 누가 리플렉션을 사용하는가?&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-5-1. 프레임워크 내부 동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// 일반 개발자가 작성하는 코드 (리플렉션 없음!)
@Component
public class UserService {
    @Autowired
    private UserRepository userRepository;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Spring 내부에서 자동으로 실행되는 코드 (프레임워크 개발자가 작성)
Class&amp;lt;?&amp;gt; clazz = UserService.class;
Object instance = clazz.getDeclaredConstructor().newInstance();

for (Field field : clazz.getDeclaredFields()) {
    if (field.isAnnotationPresent(Autowired.class)) {
        field.setAccessible(true);
        Object dependency = getBeanFromContainer(field.getType());
        field.set(instance, dependency); // 의존성 자동 주입!
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 개발자는 @Autowired만 붙이면 되고, Spring이 런타임에 리플렉션으로 자동으로 객체를 주입해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-5-2. JSON 라이브러리 동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 일반 개발자 코드
User user = new User(&quot;김철수&quot;, 25);
String json = objectMapper.writeValueAsString(user);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;// Jackson 내부 코드 (라이브러리 개발자가 작성)
Class&amp;lt;?&amp;gt; clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();

for (Field field : fields) {
    field.setAccessible(true);
    Object value = field.get(obj);
    jsonBuilder.append(field.getName(), value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;5. Java Compile option&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;5-1. 개요&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/276&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/276&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769048757461&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;자바의 실행 환경&quot; data-og-description=&quot;1. JVM이란?JVM(Java Virtual Machine)은 이름 그대로 '자바를 실행하기 위한 가상 기계'입니다. Java의 가장 큰 특징 중 하나는 OS에 종속적이지 않다는 점인데, 이것이 가능한 이유가 바로 이 JVM 때문입니&quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/276&quot; data-og-url=&quot;https://balhae.tistory.com/276&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b17FqG/dJMb8TB3Bdm/yeN7wdJNLxnFkg2vTzQShK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/uiu5m/dJMb8SpBZQS/CwR6ri1sFBkRWjKPdNViCK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/cBj5Hw/dJMb8VNpomC/0k1KvAxjXE5kdKxfPpPVWK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/276&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/276&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b17FqG/dJMb8TB3Bdm/yeN7wdJNLxnFkg2vTzQShK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/uiu5m/dJMb8SpBZQS/CwR6ri1sFBkRWjKPdNViCK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/cBj5Hw/dJMb8VNpomC/0k1KvAxjXE5kdKxfPpPVWK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;자바의 실행 환경&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. JVM이란?JVM(Java Virtual Machine)은 이름 그대로 '자바를 실행하기 위한 가상 기계'입니다. Java의 가장 큰 특징 중 하나는 OS에 종속적이지 않다는 점인데, 이것이 가능한 이유가 바로 이 JVM 때문입니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;자바 컴파일러(javac)는 우리가 작성한 소스 코드(.java)를 JVM이 이해할 수 있는 중간 단계 언어인 &lt;b&gt;바이트코드(Bytecode, .class)&lt;/b&gt;로 변환한다. 이 과정은 단순히 파일을 바꾸는 것이 아니라, 컴파일러에게 &quot;어떤 재료를 참고할지&quot;, &quot;어느 버전의 JVM에 맞출지&quot; 등을 명령하는 과정이다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5-2. 핵심 옵션: -classpath (-cp)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;자바 컴파일의 성패는 필요한 재료(라이브러리)를 얼마나 잘 찾느냐에 달려 있다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-2-1. Classpath란 무엇인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;우리가 요리(컴파일)를 하려고 레시피(소스 코드)를 펼쳤는데, 재료 리스트에 &lt;b&gt;&quot;마트에서 파는 특제 소스(외부 라이브러리)&quot;&lt;/b&gt;가 적혀 있다고 가정하자. 요리사(컴파일러)가 주방을 아무리 뒤져봐도 이 소스가 없다면 요리는 중단된다. 이때 요리사에게 &lt;b&gt;&quot;그 소스는 옆 동네 창고(특정 경로)에 있어!&quot;&lt;/b&gt;라고 위치를 알려주는 행위가 바로 &lt;b data-index-in-node=&quot;190&quot; data-path-to-node=&quot;8&quot;&gt;Classpath 설정&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-2-2. 왜 에러(package does not exist)가 발생하는가?&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769051438840&quot; class=&quot;subunit&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;error: package org.springframework... does not exist
error: package ???.????.?????? does not exist&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;스프링 프로젝트처럼 외부 라이브러리에 의존하는 코드를 리눅스에서 직접 컴파일할 때 이 옵션을 빼먹으면, 컴파일러는 라이브러리 위치를 몰라 아래와 같은 에러를 내뱉는다. 재료가 어디 있는지 알려주지 않은 셈이다.&lt;/p&gt;
&lt;pre id=&quot;code_1769051431341&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# &quot;lib 폴더 안의 모든(*) 재료를 참고해서 요리해라&quot;
javac -classpath &quot;프로젝트경로/WEB-INF/lib/*&quot; SomeFile.java&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;이때 위와 같이 -classpath 옵션을 사용하여 라이브러리가 모여 있는 lib 폴더의 경로를 알려주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1769051454315&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 리눅스는 콜론(:), 윈도우는 세미콜론(;)으로 경로 연결
javac -classpath &quot;프로젝트경로/lib/*:톰캣경로/lib/*&quot; SomeFile.java&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;만약 프로젝트 내부 재료뿐만 아니라 톰캣(Tomcat) 엔진의 재료까지 필요하다면 구분자(:)를 사용해 이어준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5-3. javac 컴파일 옵션 상세 정리&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mLKaq/dJMcai27QR6/7r7hLWh0Af0OrwDPeR3SKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mLKaq/dJMcai27QR6/7r7hLWh0Af0OrwDPeR3SKk/img.png&quot; data-alt=&quot;https://internet-craft.tistory.com/24&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mLKaq/dJMcai27QR6/7r7hLWh0Af0OrwDPeR3SKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmLKaq%2FdJMcai27QR6%2F7r7hLWh0Af0OrwDPeR3SKk%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;1072&quot; height=&quot;186&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://internet-craft.tistory.com/24&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;22&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&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;td&gt;&lt;b&gt;실생활 비유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,1,0,0&quot;&gt;-bootclasspath&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,1,1,0&quot;&gt;자바 기본 런타임 클래스 위치 지정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,1,2,0&quot;&gt;요리의 기본인 '물과 불'을 어디서 끌어올지 정함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,2,0,0&quot;&gt;-classpath, -cp&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,2,1,0&quot;&gt;필요한 외부 클래스/라이브러리 위치 지정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,2,2,0&quot;&gt;특제 소스가 있는 옆 동네 창고 위치 알려주기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,3,0,0&quot;&gt;-d&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,3,1,0&quot;&gt;생성된 클래스 파일 저장 위치 지정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,3,2,0&quot;&gt;요리 완성본을 담을 접시(폴더) 정하기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,4,0,0&quot;&gt;-deprecation&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,4,1,0&quot;&gt;사용 권장되지 않는 API 사용 시 경고&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,4,2,0&quot;&gt;&quot;그 재료는 유통기한 임박이니 주의해!&quot;라고 경고함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,5,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,5,0,0&quot;&gt;-encoding&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,5,1,0&quot;&gt;소스 코드 문자의 인코딩 방식 지정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,5,2,0&quot;&gt;레시피가 한국어인지 영어인지 명시하기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,6,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,6,0,0&quot;&gt;-endorseddirs&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,6,1,0&quot;&gt;승인된 표준 경로 위치 재정의&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,6,2,0&quot;&gt;정부 인증을 받은 정품 재료 경로 지정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,7,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,7,0,0&quot;&gt;-extdirs&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,7,1,0&quot;&gt;확장 기능(Extensions) 위치 재정의&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,7,2,0&quot;&gt;주방에 추가로 설치한 특수 도구함 위치&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,8,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,8,0,0&quot;&gt;-g&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,8,1,0&quot;&gt;모든 디버깅 정보 생성&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,8,2,0&quot;&gt;요리 과정을 처음부터 끝까지 영상으로 촬영하기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,9,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,9,0,0&quot;&gt;-g:none&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,9,1,0&quot;&gt;디버깅 정보를 생성하지 않음&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,9,2,0&quot;&gt;기록 없이 요리만 빠르게 끝내기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,10,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,10,0,0&quot;&gt;-help&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,10,1,0&quot;&gt;표준 옵션 개요 출력&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,10,2,0&quot;&gt;요리 도구 사용 설명서 읽기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,11,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,11,0,0&quot;&gt;-J&amp;lt;option&amp;gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,11,1,0&quot;&gt;JVM에 직접 옵션 전달&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,11,2,0&quot;&gt;가스레인지 화력(메모리)을 직접 조절하기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,12,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,12,0,0&quot;&gt;-nowarn&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,12,1,0&quot;&gt;경고 메시지 출력 안 함&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,12,2,0&quot;&gt;요리사가 궁시렁대는 소리(경고) 무시하기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,13,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,13,0,0&quot;&gt;-source&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,13,1,0&quot;&gt;소스 코드의 자바 버전 지정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,13,2,0&quot;&gt;구버전 레시피인지 신버전 레시피인지 명시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,14,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,14,0,0&quot;&gt;-sourcepath&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,14,1,0&quot;&gt;원본 소스 파일 위치 지정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,14,2,0&quot;&gt;재료 손질 전 원물(소스)이 쌓여있는 창고 지정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,15,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,15,0,0&quot;&gt;-target&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,15,1,0&quot;&gt;결과물이 실행될 JVM 버전 지정&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,15,2,0&quot;&gt;이 요리가 뷔페용인지(고버전), 도시락용인지(저버전) 정함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,16,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,16,0,0&quot;&gt;-verbose&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,16,1,0&quot;&gt;컴파일 수행 상세 메시지 출력&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,16,2,0&quot;&gt;요리사가 지금 뭐 하는지 하나하나 말하면서 요리함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,17,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,17,0,0&quot;&gt;-version&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,17,1,0&quot;&gt;컴파일러 버전 정보 출력&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,17,2,0&quot;&gt;요리사의 자격증 등급 확인하기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,18,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,18,0,0&quot;&gt;-X&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,18,1,0&quot;&gt;비표준 옵션 개요 출력&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;22,18,2,0&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;6. Java Memory - Garbage Collection&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;6-1. 개요&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/276&quot;&gt;https://balhae.tistory.com/276&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769056281732&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;자바의 실행 환경&quot; data-og-description=&quot;1. JVM이란?JVM(Java Virtual Machine)은 이름 그대로 '자바를 실행하기 위한 가상 기계'입니다. Java의 가장 큰 특징 중 하나는 OS에 종속적이지 않다는 점인데, 이것이 가능한 이유가 바로 이 JVM 때문입니&quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/276&quot; data-og-url=&quot;https://balhae.tistory.com/276&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b17FqG/dJMb8TB3Bdm/yeN7wdJNLxnFkg2vTzQShK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/uiu5m/dJMb8SpBZQS/CwR6ri1sFBkRWjKPdNViCK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/cBj5Hw/dJMb8VNpomC/0k1KvAxjXE5kdKxfPpPVWK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/276&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/276&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b17FqG/dJMb8TB3Bdm/yeN7wdJNLxnFkg2vTzQShK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/uiu5m/dJMb8SpBZQS/CwR6ri1sFBkRWjKPdNViCK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/cBj5Hw/dJMb8VNpomC/0k1KvAxjXE5kdKxfPpPVWK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;자바의 실행 환경&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. JVM이란?JVM(Java Virtual Machine)은 이름 그대로 '자바를 실행하기 위한 가상 기계'입니다. Java의 가장 큰 특징 중 하나는 OS에 종속적이지 않다는 점인데, 이것이 가능한 이유가 바로 이 JVM 때문입니&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;javac와 동일하게 가비지 컬렉션도 위 링크를 보면 자세히 알 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;7. Java Static &lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;7-1. 개요&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 static 키워드는 &lt;b&gt;&quot;메모리에 한 번만 올라가고, 모든 객체가 공유한다&quot;&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;background-color: #ffffff; color: #222222; text-align: start;&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/34&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1769058291776&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;열 세번째, static에 관해&quot; data-og-description=&quot;개요: 필자가 하도 헷갈린 드디어 그녀석에 대한 풀이를 해보겠다. https://vaert.tistory.com/101 static - 정적(static)은 고정된 의미를 가지고 있다. - static 키워드를 사용하면 static 변수와 static 메소드를 &quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/34&quot; data-og-url=&quot;https://balhae.tistory.com/34&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/qQIGm/dJMb88622bq/KmVeKKgIzn8VJveS3wiX6K/img.png?width=800&amp;amp;height=401&amp;amp;face=0_0_800_401,https://scrap.kakaocdn.net/dn/bkf0RP/dJMb9iaLff5/epBmksYLXaRVkpKk6bMpU1/img.png?width=800&amp;amp;height=401&amp;amp;face=0_0_800_401,https://scrap.kakaocdn.net/dn/cgfTup/dJMb895XG5k/D8YuGOsMBD5zOkFglKHICK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/34&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/34&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/qQIGm/dJMb88622bq/KmVeKKgIzn8VJveS3wiX6K/img.png?width=800&amp;amp;height=401&amp;amp;face=0_0_800_401,https://scrap.kakaocdn.net/dn/bkf0RP/dJMb9iaLff5/epBmksYLXaRVkpKk6bMpU1/img.png?width=800&amp;amp;height=401&amp;amp;face=0_0_800_401,https://scrap.kakaocdn.net/dn/cgfTup/dJMb895XG5k/D8YuGOsMBD5zOkFglKHICK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;열 세번째, static에 관해&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요: 필자가 하도 헷갈린 드디어 그녀석에 대한 풀이를 해보겠다. https://vaert.tistory.com/101 static - 정적(static)은 고정된 의미를 가지고 있다. - static 키워드를 사용하면 static 변수와 static 메소드를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무려 3년전에 작성한 게시글..... 지금 보니깐 AI 도움없이 작성했는데 나름 선방하게 작성한거 같다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7-2. static 변수(정적 변수)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;클래스 수준에서 정의된 변수로, 해당 클래스로 생성된 모든 객체가 &lt;b data-index-in-node=&quot;37&quot; data-path-to-node=&quot;5&quot;&gt;하나의 메모리 공간을 공유&lt;/b&gt;한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;실생활 비유:&lt;/b&gt; 아파트 단지의 '공용 놀이터'와 같다. 집(객체)은 여러 채지만, 놀이터는 하나뿐이며 모든 입주민이 함께 사용한다. 누군가 놀이터에 낙서를 하면 모든 입주민에게 똑같이 보인다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,1,0&quot;&gt;특징:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인스턴스 생성 전, 클래스가 로딩될 때 메모리(Method Area)에 할당된다.&lt;/li&gt;
&lt;li&gt;프로그램이 종료될 때까지 메모리에 유지된다.&lt;/li&gt;
&lt;li&gt;공통으로 사용되는 상수나 카운팅 변수에 자주 쓰인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7-3. static 메서드(정적 메서드)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;객체를 생성하지 않고도 클래스명.메서드명()으로 호출할 수 있는 메서드이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;용도:&lt;/b&gt; &lt;b&gt;유틸리티 성격의 함수&lt;/b&gt;를 만들 때 주로 사용한다. (예: Math.abs(), String.valueOf())&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;주의사항:&lt;/b&gt; static 메서드 안에서는 &lt;b&gt;인스턴스 변수(static이 안 붙은 변수)&lt;/b&gt;를 사용할 수 없다. 인스턴스 변수는 객체가 생성되어야만 존재하는데, &lt;b&gt;static은 객체 생성 전부터 메모리에 올라가 있기 때문&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7-4. static의 메모리적 관점 (바이트코드와 실행)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;우리가 앞서 배운 &lt;b data-index-in-node=&quot;10&quot; data-path-to-node=&quot;12&quot;&gt;컴파일 과정&lt;/b&gt;과 연결해서 생각하면 static의 원리가 더 명확해진다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,0,0&quot;&gt;컴파일:&lt;/b&gt; javac가 소스를 읽어 바이트코드(.class)를 만든다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,1,0&quot;&gt;클래스 로딩:&lt;/b&gt; JVM이 실행될 때 클래스 로더가 이 바이트코드를 읽는다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;13,2,0&quot;&gt;static 할당:&lt;/b&gt; 이때 클래스 파일 안에 static으로 선언된 녀석들을 발견하면, 즉시 &lt;b&gt;메서드 영역(Method Area)&lt;/b&gt;에 자리를 잡고 값을 초기화한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;이 과정이 객체 생성(new)보다 먼저 일어나기 때문에, 우리가 new를 하지 않고도 System.out.println()처럼 바로 꺼내 쓸 수 있는 것이다.&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7-5. static 사용 시 주의할 점 &lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;static은 편하지만 남용하면 독이 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;16&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,0,0&quot;&gt;가비지 컬렉션(GC)의 관리 대상이 아니다:&lt;/b&gt; 일반 객체는 안 쓰면 GC가 치워버리지만, static은 프로그램이 꺼질 때까지 메모리에 박혀 있다. 너무 많이 만들면 메모리 부족 현상이 생길 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,1,0&quot;&gt;객체지향(OOP)과 멀어진다:&lt;/b&gt; 데이터와 로직을 묶는 것이 객체지향인데, static은 데이터와 로직을 분리해버리는 경향이 있다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;16,2,0&quot;&gt;멀티쓰레드 환경의 위험성:&lt;/b&gt; 모든 객체가 하나의 변수를 공유하므로, 여러 명이 동시에 값을 바꾸려 하면 데이터가 꼬일 수 있다. &lt;b&gt;(동기화 이슈)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회사/갤럭시아머니트리</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/343</guid>
      <comments>https://balhae.tistory.com/343#entry343comment</comments>
      <pubDate>Thu, 22 Jan 2026 14:15:56 +0900</pubDate>
    </item>
    <item>
      <title>[1부] CS 과제 공부하기</title>
      <link>https://balhae.tistory.com/342</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 회사에서 CS 학습 과제로 &lt;b&gt;총 9가지 주제&lt;/b&gt;를 정리해보려고 한다.&lt;br /&gt;이 중 일부는 이전에 작성한 글이 있어, 해당 내용은 &lt;b&gt;기존 글 링크로 대체&lt;/b&gt;한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. WebServer vs WAS&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/312&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/312&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768953902551&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;Web Server와 WAS의 차이와 웹 서비스 구조&quot; data-og-description=&quot;최코딩의 개발 Web Server와 WAS의 차이와 웹 서비스 구조 본문 CS Web Server와 WAS의 차이와 웹 서비스 구조 seung_ho_choi.s 2025. 6. 24. 21:00&quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/312&quot; data-og-url=&quot;https://balhae.tistory.com/312&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oBxmz/dJMb9bvWgSa/lVoHn3uNpAdtXHZn7kfqfk/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/312&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/312&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oBxmz/dJMb9bvWgSa/lVoHn3uNpAdtXHZn7kfqfk/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;Web Server와 WAS의 차이와 웹 서비스 구조&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최코딩의 개발 Web Server와 WAS의 차이와 웹 서비스 구조 본문 CS Web Server와 WAS의 차이와 웹 서비스 구조 seung_ho_choi.s 2025. 6. 24. 21:00&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Get과 Post의 차이점&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-1. 기본 개념 차이&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GET 메서드&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;요청 파라미터가 URL에 노출됨&lt;/li&gt;
&lt;li&gt;예: &lt;a href=&quot;https://example.com/search?keyword=보안&amp;amp;page=1&quot;&gt;https://example.com/search?keyword=보안&amp;amp;page=1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;POST 메서드&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;요청 파라미터가 HTTP Body에 포함됨&lt;/li&gt;
&lt;li&gt;URL에는 파라미터가 노출되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-2. 주요 차이점 비교표&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;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;데이터 위치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;URL 쿼리스트링&lt;/td&gt;
&lt;td&gt;HTTP Request Body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;URL 예시&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;/user?id=123&amp;amp;name=홍길동&lt;/td&gt;
&lt;td&gt;/user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;데이터 길이&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;제한 있음 (보통 2KB)&lt;/td&gt;
&lt;td&gt;제한 없음 (서버 설정에 따라)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;캐싱&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;가능 (브라우저 캐시)&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;북마크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;브라우저 히스토리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;저장됨&lt;/td&gt;
&lt;td&gt;저장 안 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;뒤로가기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;안전함&lt;/td&gt;
&lt;td&gt;재전송 확인 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;보안성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;상대적으로 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;멱등성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;있음 (여러 번 호출해도 결과 동일)&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;용도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;조회, 검색&lt;/td&gt;
&lt;td&gt;생성, 수정, 삭제, 로그인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2-3. 보안적 관점에서의 차이&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2-3.1 GET의 보안 취약점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;① URL 노출로 인한 정보 유출&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# 로그인을 GET으로 처리하는 잘못된 예
GET /login?username=admin&amp;amp;password=1234

문제점:
✗ 브라우저 히스토리에 저장
✗ 서버 로그에 기록
✗ 프록시 서버 로그에 남음
✗ Referer 헤더를 통해 다른 사이트로 전송될 수 있음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;② 서버 로그 파일 노출&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;# Apache 액세스 로그 예시
192.168.1.100 - - [21/Jan/2026:10:30:45] &quot;GET /login?id=admin&amp;amp;pw=secret123 HTTP/1.1&quot; 200
192.168.1.101 - - [21/Jan/2026:10:31:20] &quot;GET /user?ssn=901234-1234567 HTTP/1.1&quot; 200

&amp;rarr; 로그 파일이 유출되면 민감한 정보가 그대로 노출됨
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;③ Referer 헤더를 통한 정보 유출&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;시나리오:
사용자가 https://bank.com/transfer?account=123&amp;amp;amount=1000000 페이지에서
외부 링크를 클릭하면:

다음 사이트로 전송되는 HTTP 헤더:
Referer: https://bank.com/transfer?account=123&amp;amp;amount=1000000

&amp;rarr; 외부 사이트가 계좌번호와 금액 정보를 알 수 있음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;④ 브라우저 캐싱 문제&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET 요청은 브라우저에 캐시될 수 있어서:
✗ 공용 PC에서 다음 사용자가 뒤로가기로 정보 조회 가능
✗ 캐시 파일 분석으로 민감 정보 복원 가능
✗ 중간자 공격 시 캐시된 데이터 탈취 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2-3.2 POST가 보안적으로 우수한 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;① HTTP Body에 데이터 은닉&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 29

username=admin&amp;amp;password=1234
&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✓ URL에 노출되지 않아 서버 로그에 민감 정보 미기록&lt;/li&gt;
&lt;li&gt;✓ 브라우저 히스토리에 파라미터 저장 안 됨&lt;/li&gt;
&lt;li&gt;✓ Referer 헤더로 전송되지 않음&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;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;GET:  2KB 제한 &amp;rarr; 복잡한 암호화 데이터 전송 어려움
POST: 무제한   &amp;rarr; 긴 암호화 토큰, 대용량 데이터 전송 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;③ CSRF 토큰 활용 가능&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;form method=&quot;POST&quot; action=&quot;/transfer&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;csrf_token&quot; value=&quot;random_token_xyz123&quot;&amp;gt;
    &amp;lt;input name=&quot;amount&quot; value=&quot;10000&quot;&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;송금&amp;lt;/button&amp;gt;
&amp;lt;/form&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;POST는 CSRF 보호 메커니즘을 적용하기 용이함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(✔&lt;b&gt;CSRF:&lt;/b&gt; CSRF는 &lt;b&gt;사용자가 로그인된 상태를 악용해&lt;/b&gt;, 사용자의 의도와 상관없이 &lt;b&gt;공격자가 요청을 대신 보내게 만드는 공격&lt;/b&gt;이다. 그래서 서버는 &lt;b&gt;CSRF 토큰&lt;/b&gt; 같은 걸로 &amp;ldquo;이 요청이 진짜 사용자 요청인지&amp;rdquo;를 확인한다. )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;④ 브라우저 재전송 방지&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;POST 요청 후 뒤로가기 시:
&quot;양식을 다시 제출하시겠습니까?&quot; 경고 표시
&amp;rarr; 중복 거래 방지
&amp;rarr; 실수로 인한 중복 처리 방지
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2-4. 활용&amp;nbsp;가이드라인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GET을 사용해야 하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;✓ 검색 기능: /search?q=keyword
✓ 필터링: /products?category=electronics&amp;amp;price_min=10000
✓ 페이징: /articles?page=2
✓ 공유 가능한 URL: /post?id=123
✓ 캐싱이 필요한 조회: /api/users/123
&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;POST를 반드시 사용해야 하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;✓ 로그인: 사용자명, 비밀번호
✓ 회원가입: 개인정보
✓ 결제: 카드 정보, 계좌 정보
✓ 데이터 생성/수정/삭제
✓ 파일 업로드
✓ 민감한 검색 (의료 정보, 금융 정보 등)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. L3와 L4 대해&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-1. OSI에 대해&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-1-1. OSI 7계층이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거에 대해 알아보기전에 먼저 OSI(Open Systems Interconnection) 7계층은 네트워크 통신이 일어나는 과정을 7단계로 나눈 국제 표준 모델입니다. 각 계층은 독립적인 역할을 수행하며, 상위 계층은 하위 계층의 서비스를 이용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-1-2. OSI 7계층 구조&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7계층 - Application Layer (응용 계층)&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;프로토콜: HTTP, HTTPS, FTP, SMTP, DNS&lt;/li&gt;
&lt;li&gt;실생활 비유: 편지를 쓰는 사람 (내용을 작성)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6계층 - Presentation Layer (표현 계층)&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;프로토콜: SSL, TLS, JPEG, MPEG&lt;/li&gt;
&lt;li&gt;실생활 비유: 번역가 (언어를 상대방이 이해할 수 있게 변환)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5계층 - Session Layer (세션 계층)&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;프로토콜: NetBIOS, RPC&lt;/li&gt;
&lt;li&gt;실생활 비유: 통화 연결 관리자 (통화 시작과 종료를 관리)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4계층 - Transport Layer (전송 계층) ⭐ L4&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;프로토콜: TCP, UDP&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;3계층 - Network Layer (네트워크 계층) ⭐ L3&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;프로토콜: IP, ICMP, ARP&lt;/li&gt;
&lt;li&gt;IP 주소를 사용하여 목적지 식별&lt;/li&gt;
&lt;li&gt;실생활 비유: 우체국 (주소를 보고 어느 지역으로 보낼지 결정)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2계층 - Data Link Layer (데이터 링크 계층)&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;프로토콜: Ethernet, Wi-Fi, MAC&lt;/li&gt;
&lt;li&gt;MAC 주소를 사용&lt;/li&gt;
&lt;li&gt;실생활 비유: 동네 배달원 (같은 동네 내에서 집 찾아가기)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1계층 - Physical Layer (물리 계층)&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;li&gt;실생활 비유: 도로와 차량 (실제로 물건을 운반하는 물리적 수단)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-2. L3 (Network Layer) 깊이 파헤치기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-2-1. L3의 핵심 역할&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L3은 &lt;b&gt;서로 다른 네트워크를 연결&lt;/b&gt;하는 계층입니다. 집 주소처럼 고유한 IP 주소를 사용해 전 세계 어디든 데이터를 전달할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-2-2. 실생활 비유로 이해하기&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IP 주소&lt;/b&gt;: 친구 집의 전체 주소 (부산광역시 해운대구 ○○동 123-45)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라우터&lt;/b&gt;: 각 지역의 우체국과 물류센터&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라우팅&lt;/b&gt;: 서울 &amp;rarr; 경기 물류센터 &amp;rarr; 대전 물류센터 &amp;rarr; 부산 물류센터 &amp;rarr; 해운대구 우체국으로 가는 최적 경로를 찾는 과정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우체국 직원이 주소를 보고 &quot;이건 부산이니까 남쪽으로 보내야겠다&quot;라고 판단하는 것처럼, 라우터는 IP 주소를 보고 어느 방향으로 패킷을 전달할지 결정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-2-3. L3 주요 기능&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;IP 주소 지정&lt;/b&gt;: 각 기기에 고유한 주소 부여&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라우팅&lt;/b&gt;: 최적의 경로를 찾아 데이터 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패킷 분할 및 재조립&lt;/b&gt;: 큰 데이터를 작은 패킷으로 나누고 다시 합침&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 간 통신&lt;/b&gt;: 서로 다른 네트워크를 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-2-4. L3 장비&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;라우터&lt;/b&gt;: 네트워크 간 경로를 설정하고 패킷을 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;L3 스위치&lt;/b&gt;: 스위칭과 라우팅 기능을 모두 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-3. L4 (Transport Layer) 깊이 파헤치기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-3-1. L4의 핵심 역할&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;L4는 &lt;b&gt;실제 데이터가 정확하고 안전하게 전달되도록 보장&lt;/b&gt;하는 계층입니다. 포트 번호를 사용해 같은 컴퓨터 안에서 어떤 응&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용 프로그램으로 데이터를 보낼지 구분합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-3-2. 실생활 비유로 이해하기&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IP 주소&lt;/b&gt;: 아파트 단지 주소 (○○아파트 123동)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;포트 번호&lt;/b&gt;: 호수 (104동 402호)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP&lt;/b&gt;: 등기 우편 (받는 사람의 서명을 받고, 분실되면 다시 보내줌)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UDP&lt;/b&gt;: 일반 우편 (그냥 우편함에 넣고 감, 빠르지만 분실 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아파트 경비실(라우터)에서 택배를 받으면, 104동까지는 L3이 전달하고, 402호 문 앞까지는 L4가 정확하게 배달하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-3-3. L4 주요 기능&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;포트 번호 지정&lt;/b&gt;: 응용 프로그램 구분 (HTTP:80, HTTPS:443, SSH:22)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오류 제어&lt;/b&gt;: 데이터 손상 검사 및 재전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;흐름 제어&lt;/b&gt;: 수신자의 처리 속도에 맞춰 전송 속도 조절&lt;/li&gt;
&lt;li&gt;&lt;b&gt;혼잡 제어&lt;/b&gt;: 네트워크 혼잡 방지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3-3-4. L4 장비&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;L4 스위치&lt;/b&gt;: 포트 번호 기반으로 트래픽 분산 (로드 밸런싱)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방화벽&lt;/b&gt;: 포트와 프로토콜 기반 접근 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-4. L3와 L4의 차이점 정리&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 140px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;L3 (Network Layer)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;L4 (Transport Layer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;주소 체계&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;IP 주소&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;포트 번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;역할&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;네트워크 간 경로 설정&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;종단 간 데이터 전송 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;프로토콜&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;IP, ICMP, ARP&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;TCP, UDP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;관심사&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&quot;어디로&quot; 보낼까?&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&quot;어떻게&quot; 보낼까?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;장비&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;라우터, L3 스위치&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;L4 스위치, 방화벽&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;비유&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;우체국 (지역 간 배송)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;택배 기사 (정확한 배달)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3-5. 실무에서의 활용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;L3 활용 사례&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;라우팅&lt;/b&gt;: 서로 다른 네트워크(사무실 네트워크, 데이터센터)를 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VPN&lt;/b&gt;: IP 기반으로 원격지 네트워크 연결&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;b&gt;L4 활용 사례&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로드 밸런싱&lt;/b&gt;: 웹 서버 여러 대에 트래픽 분산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방화벽 정책&lt;/b&gt;: 특정 포트만 허용/차단&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 구분&lt;/b&gt;: 같은 서버에서 웹(80), 메일(25), SSH(22) 동시 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 쿠키와 세션의 차이&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/274&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/274&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768953916628&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;쿠키 VS 세션&quot; data-og-description=&quot;오늘은 기술면접 단골 질문인 쿠키vs 세션에 관해서 정확한 포스팅을 하겠습니다.https://balhae.tistory.com/65 HTTP(CH7)최코딩의 개발 HTTP(CH7) 본문 스프링/HTTP HTTP(CH7) seung_ho_choi.s 2023. 5. 28. 19:39balhae.tistor&quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/274&quot; data-og-url=&quot;https://balhae.tistory.com/274&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/chfqfG/dJMb9iIA6zh/xHb0bzBILwHkLUzvdnuKKk/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/dsYdmo/dJMb9b3L3JO/PpoSYa4NveFlXqhrNbwGj0/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/cj9rmf/dJMb9lL5zd3/TS8g9IVJrFGu735sRrsPck/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/274&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/274&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/chfqfG/dJMb9iIA6zh/xHb0bzBILwHkLUzvdnuKKk/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/dsYdmo/dJMb9b3L3JO/PpoSYa4NveFlXqhrNbwGj0/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/cj9rmf/dJMb9lL5zd3/TS8g9IVJrFGu735sRrsPck/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;쿠키 VS 세션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 기술면접 단골 질문인 쿠키vs 세션에 관해서 정확한 포스팅을 하겠습니다.https://balhae.tistory.com/65 HTTP(CH7)최코딩의 개발 HTTP(CH7) 본문 스프링/HTTP HTTP(CH7) seung_ho_choi.s 2023. 5. 28. 19:39balhae.tistor&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/285&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/285&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768953930664&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;JWT vs 세션&quot; data-og-description=&quot;https://balhae.tistory.com/274 쿠키 VS 세션오늘은 기술면접 단골 질문인 쿠키vs 세션에 관해서 정확한 포스팅을 하겠습니다.https://balhae.tistory.com/65 HTTP(CH7)최코딩의 개발 HTTP(CH7) 본문 스프링/HTTP HTTP(CH7) &quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/285&quot; data-og-url=&quot;https://balhae.tistory.com/285&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b04CmO/dJMb8UHI76P/FuWN763OTQk50khaYddgGk/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/b808FP/dJMb8Xj9nER/WToAoPimXDUFUOhq6V7T1k/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/icJgi/dJMb9fZphj0/DubTiktRdfg4FyKah7OtiK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/285&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/285&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b04CmO/dJMb8UHI76P/FuWN763OTQk50khaYddgGk/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/b808FP/dJMb8Xj9nER/WToAoPimXDUFUOhq6V7T1k/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/icJgi/dJMb9fZphj0/DubTiktRdfg4FyKah7OtiK/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;JWT vs 세션&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://balhae.tistory.com/274 쿠키 VS 세션오늘은 기술면접 단골 질문인 쿠키vs 세션에 관해서 정확한 포스팅을 하겠습니다.https://balhae.tistory.com/65 HTTP(CH7)최코딩의 개발 HTTP(CH7) 본문 스프링/HTTP HTTP(CH7)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/266&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/266&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1768953943341&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;AccessToken과 RefreshToken 인증 방식&quot; data-og-description=&quot;최코딩의 개발 AccessToken과 RefreshToken 인증 방식 본문 CS AccessToken과 RefreshToken 인증 방식 seung_ho_choi.s 2025. 4. 5. 23:35&quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/266&quot; data-og-url=&quot;https://balhae.tistory.com/266&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/zbedH/dJMb9kTWWua/3gpySM5eQKzV2316kKPmGk/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/266&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/266&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/zbedH/dJMb9kTWWua/3gpySM5eQKzV2316kKPmGk/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;AccessToken과 RefreshToken 인증 방식&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;최코딩의 개발 AccessToken과 RefreshToken 인증 방식 본문 CS AccessToken과 RefreshToken 인증 방식 seung_ho_choi.s 2025. 4. 5. 23:35&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 세션 클러스터링&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5-1. 세션 클러스터링(Session Clustering)이란?&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;&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;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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5-2. 실생활 비유&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;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;: 강남 지점에서 통장을 만들었는데, 다음 날 홍대 지점에 가니 &quot;고객님 정보가 없네요?&quot;라고 함&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;사용자는 서버 A, B, C 중 어디에 연결되든 상관없이 자신의 로그인 상태와 데이터를 유지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5-3. 왜 세션 클러스터링이 필요한가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-3-1. 단일 서버의 문제점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;&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;사용자 1만 명이 동시 접속하면 서버 1대가 감당 못함&lt;/li&gt;
&lt;li&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버가 다운되면 모든 서비스 중단&lt;/li&gt;
&lt;li&gt;세션 정보 모두 손실&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제 3: 확장성 부족&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;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-3-2. 해결책: 서버 여러 대 운영 (클러스터링)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;b&gt;바로 세션 불일치 문제!&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;gcode&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;사용자 요청 #1 &amp;rarr; 로드 밸런서 &amp;rarr; 서버A (로그인, 세션 생성)
사용자 요청 #2 &amp;rarr; 로드 밸런서 &amp;rarr; 서버B (세션 없음! 로그인 안 된 것으로 인식)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-3-3. 실생활 비유&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;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;: 강남 지점에서 통장을 만들었는데, 다음 날 홍대 지점에 가니 &quot;고객님 정보가 없네요?&quot;&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;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-4. 세션 클러스터링 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-4-1. Sticky Session (Session Affinity)&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;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;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;
&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;b&gt;  실생활 비유&lt;/b&gt;&lt;br /&gt;단골 미용실처럼 항상 같은 디자이너에게만 머리를 맡기는 것. 그 디자이너가 퇴사하면 내 헤어 스타일 기록이 사라짐.&lt;b&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;b&gt;동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;로드 밸런서가 쿠키나 IP 주소를 보고 항상 같은 서버로 연결
사용자A &amp;rarr; 항상 서버1
사용자B &amp;rarr; 항상 서버2
사용자C &amp;rarr; 항상 서버1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-4-2. Session Replication (세션 복제)&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;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;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;
&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;
&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;서버A: 세션1, 세션2, 세션3
서버B: 세션1, 세션2, 세션3 (복제본)
서버C: 세션1, 세션2, 세션3 (복제본)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;Tomcat Session Clustering&lt;/li&gt;
&lt;li&gt;WildFly/JBoss Clustering&lt;/li&gt;
&lt;li&gt;Multicast 기반 복제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5-4-3. Session Clustering (중앙 세션 저장소) ⭐ 가장 많이 사용&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;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;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;
&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;
&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;div&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;서버A ─┐
서버B ─┼─&amp;rarr; Redis/Memcached (중앙 저장소)
서버C ─┘

모든 서버가 중앙 저장소에서 세션을 읽고 씀&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;
&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;Redis (가장 인기)&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;In-Memory 데이터베이스&lt;/li&gt;
&lt;li&gt;빠른 읽기/쓰기 속도&lt;/li&gt;
&lt;li&gt;TTL(Time To Live) 자동 만료 지원&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;Memcached&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;Redis보다 단순하고 가벼움&lt;/li&gt;
&lt;li&gt;데이터 영속성 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터베이스 (MySQL, PostgreSQL)&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;li&gt;대용량 트래픽에 부적합&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6.&amp;nbsp;Character Set &amp;amp; Encoding이란?&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-path-to-node=&quot;4&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6-1. 이론&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;컴퓨터는 0과 1밖에 모르는 '바보'입니다. 우리가 사용하는 &quot;A&quot;, &quot;가&quot;, &quot; &quot; 같은 문자를 컴퓨터가 이해하게 하려면 두 가지 단계가 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;Character Set (문자 집합):&lt;/b&gt; 문자에 고유한 숫자 번호를 매기는 '약속' (예: &quot;A&quot;는 65번이야) 즉 표현할 수 있는 문자들의 목록&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;Encoding (인코딩):&lt;/b&gt; 그 숫자를 컴퓨터 메모리에 저장하기 위해 0과 1로 변환하는 '방식' (예: 65를 어떻게 2진수로 바꿀까?)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;실생활 비유: 음식 주문 시스템&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;7&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,0,0&quot;&gt;Character Set (메뉴판):&lt;/b&gt; 메뉴판에 '김치찌개는 1번', '된장찌개는 2번'이라고 번호를 매겨놓은 상태&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;7,1,0&quot;&gt;Encoding (주문 방식):&lt;/b&gt; 그 번호를 주방에 전달할 때 '손가락으로 표시'할지, '종이에 적어서' 줄지, '무전기로' 말할지 정하는 규칙&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6-2. 왜 문자 집합과 인코딩이 필요한가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6-2-1. 단일 언어 시대의 한계 (ASCII)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;과거에는 영어와 기본 기호만 처리하면 됐기 때문에 7비트(128개)면 충분했습니다. 하지만 한글, 한자, 아랍어 등이 등장하며 문제가 생겼습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;문제 1:&lt;/b&gt; 각 국가가 자기들만의 인코딩 방식(EUC-KR, Shift-JIS 등)을 만듦&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;문제 2:&lt;/b&gt; 서로 다른 인코딩을 사용하는 시스템끼리 데이터를 주고받으면 글자가 깨짐 (외계어 발생)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,2,0&quot;&gt;해결책:&lt;/b&gt; 전 세계 모든 문자를 담을 수 있는 거대한 그릇인 &lt;b&gt;유니코드(Unicode)&lt;/b&gt;가 탄생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6-2-2. 주요 Character Set (문자 집합) 방식&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 80px;&quot; border=&quot;1&quot; data-path-to-node=&quot;15&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;종류&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;비유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;15,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,0,0&quot;&gt;ASCII&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;15,1,1,0&quot;&gt;영어, 숫자, 특수문자만 포함 (128개)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;15,1,2,0&quot;&gt;아주 작은 동네 수첩&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;15,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,2,0,0&quot;&gt;ANSI&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;15,2,1,0&quot;&gt;ASCII에 각 나라 언어를 확장한 것 (EUC-KR 등)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;15,2,2,0&quot;&gt;동네마다 양식이 다른 장부&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;15,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,3,0,0&quot;&gt;Unicode&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;15,3,1,0&quot;&gt;전 세계 모든 문자에 고유 번호 부여&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;15,3,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,3,2,0&quot;&gt;전 세계 공통 백과사전&lt;/b&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;h4 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6-3. 인코딩 방식 (Encoding) ⭐ 가장 중요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;유니코드라는 '번호표'가 있어도, 이걸 실제로 어떻게 저장하느냐에 따라 효율이 달라집니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6-3-1. UTF-8 (가변 길이 인코딩) - 가장 많이 사용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;20&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,0,0&quot;&gt;개념:&lt;/b&gt; 문자에 따라 1바이트에서 4바이트까지 길이를 다르게 사용&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,1,0&quot;&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바이트, 한글은 3바이트로 처리&lt;/li&gt;
&lt;li&gt;ASCII와 완벽하게 호환됨&lt;/li&gt;
&lt;li&gt;웹 표준이며 가장 효율적임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;20,2,0&quot;&gt;비유:&lt;/b&gt; 글자 크기에 맞춰서 박스 크기를 조절하는 '스마트 포장법'&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6-3-2. UTF-16 (고정/가변 혼합)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;22&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,0,0&quot;&gt;개념:&lt;/b&gt; 기본적으로 2바이트 또는 4바이트 사용&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,1,0&quot;&gt;특징&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한글을 2바이트로 저장할 수 있어 한글 위주 데이터에선 UTF-8보다 용량이 적을 수 있음&lt;/li&gt;
&lt;li&gt;하지만 ASCII 호환성이 떨어지고 관리가 복잡함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;22,2,0&quot;&gt;비유:&lt;/b&gt; 모든 물건을 중형 또는 대형 박스에만 담는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6-3-4. EUC-KR / CP949 (한글 전용)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;24&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,0,0&quot;&gt;개념:&lt;/b&gt; 한글만을 위해 만들어진 과거의 방식&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,1,0&quot;&gt;특징:&lt;/b&gt;&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한글 2바이트 처리&lt;/li&gt;
&lt;li&gt;유니코드와 호환되지 않아 현대 웹 환경에선 권장되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;24,2,0&quot;&gt;비유:&lt;/b&gt; 우리나라에서만 통용되는 지역 화폐&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;26&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6-4. 왜 글자가 깨질까요? (Decoding Error)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;우리가 흔히 보는 ``나 뷁 같은 현상은 &lt;b&gt;인코딩(저장 규칙)&lt;/b&gt;과 &lt;b&gt;디코딩(해석 규칙)&lt;/b&gt;이 일치하지 않을 때 발생합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;28&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,0,0&quot;&gt;상황:&lt;/b&gt; &quot;안녕&quot;을 &lt;b&gt;UTF-8(규칙 A)&lt;/b&gt;로 저장함&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,1,0&quot;&gt;오류:&lt;/b&gt; 읽을 때 &lt;b&gt;EUC-KR(규칙 B)&lt;/b&gt;로 해석하려고 함&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;28,2,0&quot;&gt;결과:&lt;/b&gt; 컴퓨터는 엉뚱한 번호를 찾아내어 이상한 글자를 출력함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 암호화 단반향 양방햐 공개키 비공개키 암호화 종류&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-path-to-node=&quot;0&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7-1. 단방향 vs 양방향 암호화 비교&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&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;단방향 (One-Way)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;양방향 (Two-Way)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;1,1,0,0&quot;&gt;복호화 (다시 풀기)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,1,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;1,1,1,0&quot;&gt;불가능&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,1,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;1,1,2,0&quot;&gt;가능&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;1,2,0,0&quot;&gt;핵심 목적&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,2,1,0&quot;&gt;데이터의 무결성 확인 (진위 여부), 비밀번호 저장&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,2,2,0&quot;&gt;데이터의 안전한 전송 및 보관&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;1,3,0,0&quot;&gt;주요 용도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,3,1,0&quot;&gt;비밀번호 저장, 파일 변조 확인&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,3,2,0&quot;&gt;개인정보 암호화, 이메일 보안&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;1,4,0,0&quot;&gt;특징&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,4,1,0&quot;&gt;같은 입력값은 항상 같은 결과값이 나옴&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,4,2,0&quot;&gt;키(Key)가 있어야 원래 내용을 볼 수 있음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;1,4,0,0&quot;&gt;주요 알고리즘&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;1,4,1,0&quot;&gt; MD5, SHA-1, SHA-256, SHA-512, bcrypt, scrypt &lt;/span&gt;&lt;/td&gt;
&lt;td&gt;7-2에 설명&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-path-to-node=&quot;3&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7-2. 대칭키 vs 비대칭키(공개키/개인키) 비교&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3999&quot; data-origin-height=&quot;1748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WMhyJ/dJMcaajLmkj/hSwp1cAfDKKdNkNTanKDMK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WMhyJ/dJMcaajLmkj/hSwp1cAfDKKdNkNTanKDMK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WMhyJ/dJMcaajLmkj/hSwp1cAfDKKdNkNTanKDMK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWMhyJ%2FdJMcaajLmkj%2FhSwp1cAfDKKdNkNTanKDMK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;560&quot; height=&quot;245&quot; data-origin-width=&quot;3999&quot; data-origin-height=&quot;1748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 117px;&quot; border=&quot;1&quot; data-path-to-node=&quot;4&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;대칭키 (Symmetric)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;비대칭키 (Asymmetric / Public Key)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,0,0&quot;&gt;사용 키 개수&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,1,1,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,1,0&quot;&gt;1개&lt;/b&gt; (암호화 키 = 복호화 키)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,1,2,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,1,2,0&quot;&gt;2개&lt;/b&gt; (공개키로 암호화, 개인키로 복호화)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,2,0,0&quot;&gt;키 전달 문제&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,2,1,0&quot;&gt;키를 전달하다 탈취당할 위험이 있음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,2,2,0&quot;&gt;공개키는 공개되어도 안전함 (개인키만 보관)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,3,0,0&quot;&gt;암호화 속도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,3,1,0&quot;&gt;매우 빠름&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,3,2,0&quot;&gt;느림 (연산 과정이 복잡함)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span data-path-to-node=&quot;4,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,3,0,0&quot;&gt;주요 알고리즘&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span data-path-to-node=&quot;4,3,1,0&quot;&gt; AES, DES, 3DES, ARIA, SEED, ChaCha20 &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span data-path-to-node=&quot;4,3,2,0&quot;&gt; RSA, ECC, DSA, ElGamal, Diffie-Hellman &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 암호화 키 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;4,3,1,0&quot;&gt;동일한 키&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;4,3,2,0&quot;&gt;공개키&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;복호화 키&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;4,3,1,0&quot;&gt;동일한 키&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;4,3,2,0&quot;&gt;비밀키&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;4,4,0,0&quot;&gt;실생활 비유&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,4,1,0&quot;&gt;금고 비밀번호 (번호만 알면 누구나 염)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;4,4,2,0&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 유닉스 &amp;amp; 리눅스 명령어: Crontab (스케줄링 작업)&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;8-1. Crontab이란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Crontab(크론탭)&lt;/b&gt;은 리눅스나 유닉스 계열 시스템에서 특정 시간에 특정 작업을 자동으로 실행하게 해주는 &lt;b data-index-in-node=&quot;64&quot; data-path-to-node=&quot;4&quot;&gt;'예약 실행(스케줄링)'&lt;/b&gt; 도구입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;Cron:&lt;/b&gt; 백그라운드에서 주기적으로 정해진 작업을 실행하는 프로세스 (데몬)&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;Crontab:&lt;/b&gt; 그 작업을 언제, 어떻게 실행할지 적어놓은 '시간표(Table)'&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-path-to-node=&quot;6&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;실생활 비유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 아침 7시에 울리는 스마트폰 알람과 같습니다. 한 번 설정해두면 내가 신경 쓰지 않아도 정해진 시간에 맞춰 시스템이 일을 수행합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;7&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;8-2. Crontab 설정 기본 문법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;크론탭은 5개의 별(*)과 실행할 명령어로 구성됩니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 120px;&quot; border=&quot;1&quot; data-path-to-node=&quot;10&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;필드 (순서)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;범위&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,1,0,0&quot;&gt;1번째 (*)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,1,1,0&quot;&gt;분 (Minute)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,1,2,0&quot;&gt;0 ~ 59&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,2,0,0&quot;&gt;2번째 (*)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,2,1,0&quot;&gt;시 (Hour)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,2,2,0&quot;&gt;0 ~ 23&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,3,0,0&quot;&gt;3번째 (*)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,3,1,0&quot;&gt;일 (Day of Month)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,3,2,0&quot;&gt;1 ~ 31&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,4,0,0&quot;&gt;4번째 (*)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,4,1,0&quot;&gt;월 (Month)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,4,2,0&quot;&gt;1 ~ 12&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,5,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;10,5,0,0&quot;&gt;5번째 (*)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,5,1,0&quot;&gt;요일 (Day of Week)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span data-path-to-node=&quot;10,5,2,0&quot;&gt;0 ~ 6 (0:일요일, 6:토요일)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-path-to-node=&quot;11&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-path-to-node=&quot;12&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;8-3. 주요 명령어&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;13&quot; 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-path-to-node=&quot;14&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&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;td&gt;&lt;b&gt;비유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,1,0,0&quot;&gt;crontab -e&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,1,1,0&quot;&gt;크론탭 설정 편집 (Edit)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,1,2,0&quot;&gt;시간표 작성/수정하기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,2,0,0&quot;&gt;crontab -l&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,2,1,0&quot;&gt;설정된 내용 확인 (List)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,2,2,0&quot;&gt;내 시간표 구경하기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;14,3,0,0&quot;&gt;crontab -r&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,3,1,0&quot;&gt;모든 크론탭 삭제 (Remove)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;14,3,2,0&quot;&gt;시간표 초기화하기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-path-to-node=&quot;15&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;8-4. 실전 활용 예시&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;17&quot; 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-path-to-node=&quot;18&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&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;Crontab 설정값&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,1,0,0&quot;&gt;매분마다&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,1,1,0&quot;&gt;* * * * * /path/script.sh&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,1,2,0&quot;&gt;매분 0초에 스크립트 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,2,0,0&quot;&gt;매일 새벽 3시&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,2,1,0&quot;&gt;0 3 * * * /path/script.sh&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,2,2,0&quot;&gt;서버가 한가한 새벽에 데이터 백업&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,3,0,0&quot;&gt;매주 월요일 오전 9시&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,3,1,0&quot;&gt;0 9 * * 1 /path/script.sh&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,3,2,0&quot;&gt;업무 시작 시간 맞춰 주간 보고서 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,4,0,0&quot;&gt;10분마다&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,4,1,0&quot;&gt;*/10 * * * * /path/script.sh&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;18,4,2,0&quot;&gt;특정 간격으로 반복 작업 수행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-path-to-node=&quot;19&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-path-to-node=&quot;20&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;8-5. 왜 Crontab이 필요한가? (사용 이유)&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;21&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21,0,0&quot;&gt;반복 작업 자동화:&lt;/b&gt; 매일 수동으로 하던 로그 정리, DB 백업 등을 자동화하여 실수를 방지합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21,1,0&quot;&gt;리소스 효율:&lt;/b&gt; 트래픽이 적은 새벽 시간에 무거운 배치(Batch) 작업을 돌려 서버 부담을 분산합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;21,2,0&quot;&gt;시스템 모니터링:&lt;/b&gt; 주기적으로 시스템 상태를 체크하고 관리자에게 알림을 보내는 용도로 활용됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;2&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;9. 보안의 핵심: SSL(Secure Sockets Layer) &amp;amp; TLS(Transport Layer Security)&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;9-1. SSL/TLS란?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;웹사이트와 브라우저 사이의 통신을 &lt;b data-index-in-node=&quot;19&quot; data-path-to-node=&quot;4&quot;&gt;암호화&lt;/b&gt;하여 해커가 중간에서 정보를 훔쳐보지 못하게 만드는 보안 프로토콜입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;5&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,0,0&quot;&gt;SSL:&lt;/b&gt; 1990년대 초반에 넷스케이프에서 처음 만든 보안 규격입니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;5,1,0&quot;&gt;TLS:&lt;/b&gt; SSL의 결함을 보완하고 업그레이드한 &lt;b data-index-in-node=&quot;26&quot; data-path-to-node=&quot;5,1,0&quot;&gt;SSL의 최신 버전&lt;/b&gt;입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-path-to-node=&quot;6&quot; 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;사실상 지금 우리가 사용하는 기술은 대부분 TLS이지만, 사람들에게 익숙한 용어인 SSL로 혼용해서 부르는 경우가 많습니다. (마치 모든 스테이플러를 '호치케스'라고 부르는 것과 비슷합니다.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;9-2. 실생활 비유: 봉인된 특급 우편&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;HTTP (일반 우편):&lt;/b&gt; 누구나 편지 봉투를 뜯어서 내용을 볼 수 있는 일반 엽서 상태&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,1,0&quot;&gt;HTTPS (SSL/TLS 적용):&lt;/b&gt; 특수 제작된 금고 상자에 편지를 넣고, 받는 사람만 가진 열쇠로 열 수 있는 특급 보안 우편&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;9-3. SSL/TLS의 주요 역할&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;12&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&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;&lt;span data-path-to-node=&quot;12,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0,0&quot;&gt;암호화 (Encryption)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,1,1,0&quot;&gt;제3자가 데이터를 가로채도 내용을 읽을 수 없게 만듦&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,2,0,0&quot;&gt;인증 (Authentication)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,2,1,0&quot;&gt;내가 접속한 사이트가 진짜 그 사이트가 맞는지 확인 (피싱 방지)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,3,0,0&quot;&gt;데이터 무결성 (Integrity)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;12,3,1,0&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;h4 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;9-4. 동작 방식: SSL 핸드셰이크(Handshake)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 주고받기 전, 클라이언트(브라우저)와 서버가 서로 인사를 나누며 암호화 방식을 정하는 과정입니다. 위에서 설명한 &lt;b&gt;공개키(비대칭키)&lt;/b&gt;와 &lt;b data-index-in-node=&quot;82&quot; data-path-to-node=&quot;15&quot;&gt;대칭키&lt;/b&gt; 방식이 여기서 모두 쓰입니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;9-4-1. naver.com 접속 시 발생하는 SSL/TLS 핸드셰이크 과정&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계: Client Hello (브라우저 &amp;rarr; 네이버 서버)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 주소창에 엔터를 치면 브라우저가 네이버 서버에 첫 인사를 건넵니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;6&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;6,0,0&quot;&gt;보내는 정보:&lt;/b&gt; 브라우저가 지원하는 &lt;b data-index-in-node=&quot;19&quot; data-path-to-node=&quot;6,0,0&quot;&gt;TLS 버전&lt;/b&gt;, 지원하는 &lt;b data-index-in-node=&quot;32&quot; data-path-to-node=&quot;6,0,0&quot;&gt;암호화 방식(Cipher Suite)&lt;/b&gt;, 그리고 임의의 난수(Client Random)를 전달합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계: Server Hello &amp;amp; Certificate (네이버 서버 &amp;rarr; 브라우저)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;네이버 서버가 응답합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;9,0,0&quot;&gt;보내는 정보:&lt;/b&gt; 브라우저가 제안한 방식 중 하나를 선택하고, 자신의 &lt;b&gt;신분증인 'SSL/TLS 인증서'&lt;/b&gt;를 보냅니다. 이 인증서 안에는 네이버의 &lt;b data-index-in-node=&quot;82&quot; data-path-to-node=&quot;9,0,0&quot;&gt;공개키&lt;/b&gt;가 들어있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3단계: 인증서 검증 (브라우저의 확인 작업) ⭐ 중요&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;브라우저는 받은 인증서가 가짜인지 확인합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;방법:&lt;/b&gt; 브라우저에 내장된 '신뢰할 수 있는 기관(CA)' 목록과 대조합니다. 만약 인증서의 디지털 서명이 유효하다면 &quot;이 서버는 진짜 네이버가 맞구나!&quot;라고 신뢰하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4단계: Pre-Master Secret 생성 및 전달 (브라우저 &amp;rarr; 네이버 서버)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;진짜인 게 확인됐으니, 이제 데이터를 암호화할 '비밀번호'를 만들어야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,0,0&quot;&gt;동작:&lt;/b&gt; 브라우저는 임의의 비밀번호(Pre-Master Secret)를 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,0&quot;&gt;암호화:&lt;/b&gt; 이걸 그냥 보내면 해커가 가로채겠죠? 그래서 아까 받은 &lt;b data-index-in-node=&quot;36&quot; data-path-to-node=&quot;15,1,0&quot;&gt;네이버의 공개키&lt;/b&gt;로 이 비밀번호를 꽁꽁 얼려버립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5단계: 비밀번호 복호화 (네이버 서버)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;서버는 암호화된 비밀번호를 받습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;18&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,0,0&quot;&gt;동작:&lt;/b&gt; 서버는 자신만 가지고 있는 &lt;b&gt;개인키(Private Key)&lt;/b&gt;를 꺼내서 브라우저가 보낸 비밀번호를 풀어냅니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;18,1,0&quot;&gt;결과:&lt;/b&gt; 이제 브라우저와 네이버 서버만 아는 &lt;b&gt;공통의 대칭키(세션키)&lt;/b&gt;가 생겼습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6단계: 통신 준비 완료 (Change Cipher Spec)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;양측은 &quot;이제부터 우리가 공유한 이 대칭키로 모든 데이터를 암호화해서 주고받자!&quot;라고 최종 확인 사인을 보냅니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7단계: 데이터 전송 (HTTPS 시작)&lt;/b&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;이제 우리가 보는 뉴스, 메일 등의 데이터가 &lt;b&gt;대칭키(AES 등)&lt;/b&gt;로 암호화되어 전송됩니다. 주소창에는 초록색 자물쇠 아이콘이 뜹니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;9-5. HTTP vs HTTPS (보안 적용 여부)&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-path-to-node=&quot;19&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&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;HTTP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;HTTPS (HTTP over SSL/TLS)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,1,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,1,0,0&quot;&gt;포트 번호&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,1,1,0&quot;&gt;80번&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,1,2,0&quot;&gt;443번&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,2,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,2,0,0&quot;&gt;보안성&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,2,1,0&quot;&gt;암호화 없음 (위험)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,2,2,0&quot;&gt;강력한 암호화 (안전)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,3,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,3,0,0&quot;&gt;검색 노출&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,3,1,0&quot;&gt;검색 순위에서 밀릴 수 있음&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,3,2,0&quot;&gt;구글 등 검색 엔진 최적화(SEO) 우대&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,4,0,0&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,4,0,0&quot;&gt;신뢰도&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,4,1,0&quot;&gt;브라우저에 '주의 요함' 표시&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span data-path-to-node=&quot;19,4,2,0&quot;&gt;주소창 옆에 자물쇠 아이콘 표시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-path-to-node=&quot;20&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회사/갤럭시아머니트리</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/342</guid>
      <comments>https://balhae.tistory.com/342#entry342comment</comments>
      <pubDate>Wed, 21 Jan 2026 11:55:11 +0900</pubDate>
    </item>
    <item>
      <title>서브쿼리와 조인 전략</title>
      <link>https://balhae.tistory.com/341</link>
      <description>&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 회사에서 서브쿼리와 JOIN에 대해 공부하라는 과제를 받아서, 실제 업무에서 마주친 문제를 중심으로 정리해봤다.&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;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가맹점 게시글 목록을 조회할 때 작성자 이름을 함께 보여줘야 한다. 그런데 게시글의 TARGET_TYPE 값에 따라 작성자가 서로 다른 테이블에 저장되어 있다:&lt;/p&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&gt;TARGET_TYPE = 'customerAdmin' &amp;rarr; &lt;b&gt;NTCFG_AGENTUSER&lt;/b&gt; 테이블 조회&lt;/li&gt;
&lt;li&gt;TARGET_TYPE = 'masterAdmin' &amp;rarr; &lt;b&gt;NTEMP_ADMINUSER&lt;/b&gt; 테이블 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 &quot;작성자 이름&quot;이지만 데이터 출처가 다른 상황이다. 이걸 어떻게 한 번의 쿼리로 가져올 수 있을까?&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-end=&quot;1816&quot; data-start=&quot;1799&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결 방법 1: 서브쿼리 방식으로 구현&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1816&quot; data-start=&quot;1799&quot; data-ke-size=&quot;size16&quot;&gt;- JOIN 없이 CASE + 상관 서브쿼리 사용&lt;/p&gt;
&lt;p data-end=&quot;1816&quot; data-start=&quot;1799&quot; data-ke-size=&quot;size16&quot;&gt;- 행마다 필요한 테이블만 조회&lt;/p&gt;
&lt;pre id=&quot;code_1768801729919&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM (
    SELECT
        ROWNUM AS RN,
        t.*
    FROM (
        SELECT
            get_code_name('CshCategory', B.CATEGORY) AS CATEGORY,
            B.TITLE,
            B.VIEW_COUNT,
            B.CREATED_AT,
            B.BOARD_ID,
            CASE
                WHEN B.TARGET_TYPE = 'customerAdmin' THEN
                    (
                        SELECT ca.AGENTUSER_NAME
                        FROM NTCFG_AGENTUSER ca
                        WHERE ca.AGENTUSER_ID = B.ADMINUSER_ID
                    )
                WHEN B.TARGET_TYPE = 'masterAdmin' THEN
                    (
                        SELECT ma.ADMINUSER_NAME
                        FROM NTEMP_ADMINUSER ma
                        WHERE ma.ADMINUSER_ID = B.ADMINUSER_ID
                    )
            END AS WRITER_NAME
        FROM TEST_CSH_BOARD B
        WHERE SUBSTR(B.CREATED_AT, 1, 8)
              BETWEEN '20260119' AND '20260119'
        ORDER BY B.BOARD_ID DESC
    ) t
    WHERE ROWNUM &amp;lt;= 15
)
WHERE RN &amp;gt;= 1;&lt;/code&gt;&lt;/pre&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; data-start=&quot;1799&quot; data-end=&quot;1816&quot;&gt;&lt;b&gt;해결 방법 2: &lt;/b&gt;&lt;b&gt;JOIN 방식으로 구현&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;567&quot; data-start=&quot;550&quot; data-ke-size=&quot;size16&quot;&gt;- 두 사용자 테이블을 모두 LEFT JOIN&lt;/p&gt;
&lt;p data-end=&quot;567&quot; data-start=&quot;550&quot; data-ke-size=&quot;size16&quot;&gt;- CASE WHEN으로 작성자 이름 분기&lt;/p&gt;
&lt;pre id=&quot;code_1768801715571&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
    RN,
    CATEGORY,
    TITLE,
    VIEW_COUNT,
    CREATED_AT,
    BOARD_ID,
    WRITER_NAME
FROM (
    SELECT
        ROWNUM AS RN,
        t.*
    FROM (
        SELECT
            get_code_name('CshCategory', B.CATEGORY) AS CATEGORY,
            B.TITLE,
            B.VIEW_COUNT,
            B.CREATED_AT,
            B.BOARD_ID,
            CASE
                WHEN B.TARGET_TYPE = 'customerAdmin' THEN CA.AGENTUSER_NAME
                WHEN B.TARGET_TYPE = 'masterAdmin' THEN MA.ADMINUSER_NAME
            END AS WRITER_NAME
        FROM TEST_CSH_BOARD B
        LEFT JOIN NTCFG_AGENTUSER CA
            ON B.TARGET_TYPE = 'customerAdmin'
           AND CA.AGENTUSER_ID = B.ADMINUSER_ID
        LEFT JOIN NTEMP_ADMINUSER MA
            ON B.TARGET_TYPE = 'masterAdmin'
           AND MA.ADMINUSER_ID = B.ADMINUSER_ID
        WHERE SUBSTR(B.CREATED_AT, 1, 8)
              BETWEEN '20260119' AND '20260119'
        ORDER BY B.BOARD_ID DESC
    ) t
    WHERE ROWNUM &amp;lt;= 15
)
WHERE RN &amp;gt;= 1;&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;이렇게 2가지 방식으로 데이터를 가져오는 방식이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;263&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;서브쿼리와 JOIN의 차이는 단순한 문법 선택의 문제가 아니라,&lt;/p&gt;
&lt;p data-end=&quot;263&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 &lt;b&gt;어떻게 다룰 것인가에 대한 관점의 차이&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;263&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;263&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;즉, 결과 집합에서 &lt;b&gt;행(row)을 줄이는 데 초점을 둘 것인지&lt;/b&gt;,&lt;/p&gt;
&lt;p data-end=&quot;263&quot; data-start=&quot;98&quot; data-ke-size=&quot;size16&quot;&gt;아니면 &lt;b&gt;열(column)을 확장해 데이터를 결합할 것인지&lt;/b&gt;의 차이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;265&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;265&quot; data-ke-size=&quot;size16&quot;&gt;JOIN은 여러 테이블의 데이터를 결합해 조회 결과를 구성하는 데 적합하다.&lt;br /&gt;필요한 컬럼들을 한 번에 가져와 화면이나 리포트에 보여줘야 하는 경우,&lt;br /&gt;JOIN은 결과 구조가 명확하고 활용도가 높다.&lt;/p&gt;
&lt;p data-end=&quot;561&quot; data-start=&quot;380&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;561&quot; data-start=&quot;380&quot; data-ke-size=&quot;size16&quot;&gt;반면 서브쿼리는 다른 테이블의 데이터를 직접 결합하기보다는,&lt;br /&gt;조건을 계산하거나 존재 여부를 판단해 &lt;b&gt;불필요한 행을 사전에 걸러내는 데&lt;/b&gt; 목적이 있다.&lt;br /&gt;특히 대량의 데이터 중 특정 조건을 만족하는 대상만 추려내는 상황에서는&lt;br /&gt;서브쿼리가 쿼리의 의도를 더 분명하게 드러내며, 실행 계획상으로도 효율적인 경우가 많다.&lt;/p&gt;
&lt;p data-end=&quot;666&quot; data-start=&quot;563&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;666&quot; data-start=&quot;563&quot; data-ke-size=&quot;size16&quot;&gt;따라서 조회 결과를 &lt;b&gt;어떤 형태로 만들고 싶은지&lt;/b&gt;,&lt;br /&gt;즉 데이터를 &amp;ldquo;붙일 것인지&amp;rdquo; 아니면 &amp;ldquo;줄일 것인지&amp;rdquo;를 기준으로&lt;br /&gt;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;&amp;nbsp;&lt;/p&gt;</description>
      <category>회사/갤럭시아머니트리</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/341</guid>
      <comments>https://balhae.tistory.com/341#entry341comment</comments>
      <pubDate>Mon, 19 Jan 2026 15:54:11 +0900</pubDate>
    </item>
    <item>
      <title> 2025년을 마무리 하며...</title>
      <link>https://balhae.tistory.com/340</link>
      <description>&lt;h2 data-end=&quot;150&quot; data-start=&quot;113&quot; data-ke-size=&quot;size26&quot;&gt; &amp;zwj;♂️ &lt;b&gt;무너지고, 다시 일어서며 &amp;mdash; 2025년 개발자 회고&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;269&quot; data-start=&quot;152&quot; data-ke-size=&quot;size16&quot;&gt;2025년은 개발자로서 많이 무너지고 좌절했지만, 그 끝에서 취업이라는 결과를 만들며 다시 회복할 수 있었던 한 해였다.&lt;br /&gt;무엇보다 이 과정을 지나오면서 주변 친구들의 소중함을 많이 깨닫게 된 해이기도 했다.&lt;/p&gt;
&lt;h3 data-end=&quot;287&quot; data-start=&quot;271&quot; 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-end=&quot;382&quot; data-start=&quot;289&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;311&quot; data-start=&quot;289&quot;&gt;&lt;b&gt;프로젝트&lt;/b&gt;: 4개 이상 완료&lt;/li&gt;
&lt;li data-end=&quot;338&quot; data-start=&quot;312&quot;&gt;&lt;b&gt;자격증&lt;/b&gt;: 토익 스피킹 IM3 취득&lt;/li&gt;
&lt;li data-end=&quot;369&quot; data-start=&quot;339&quot;&gt;&lt;b&gt;수상&lt;/b&gt;: 2회 (블레이버스, 정션 아시아)&lt;/li&gt;
&lt;li data-end=&quot;382&quot; data-start=&quot;370&quot;&gt;&lt;b&gt;취업&lt;/b&gt;: 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;387&quot; data-start=&quot;384&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;409&quot; data-start=&quot;389&quot; data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;1월 &amp;mdash; 불타오르던 시작&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;543&quot; data-start=&quot;411&quot; data-ke-size=&quot;size16&quot;&gt;1월은 정말 우여곡절이 많았던 달이었다.&lt;br /&gt;미뤄왔던 토익 스피킹을 드디어 취득했고, 당근 인턴 서류에도 합격했다. 동아리에서 강의를 하며 나름 의미 있는 활동들도 많이 했다.&lt;br /&gt;그리고&amp;hellip; 부모님께 &lt;b&gt;ㄷㅂ&lt;/b&gt;도 들켰다. 정말 끔찍했다.&lt;/p&gt;
&lt;p data-end=&quot;655&quot; data-start=&quot;545&quot; data-ke-size=&quot;size16&quot;&gt;이 시기에 처음으로 중소기업 면접도 봤다. 결과는 합격이었지만, 더 좋은 곳을 가고 싶다는 생각에 입사를 포기했다.&lt;br /&gt;이때까지만 해도 에너지가 넘쳤고, 뭐든 할 수 있을 것 같은 자신감이 있었다.&lt;/p&gt;
&lt;hr data-end=&quot;660&quot; data-start=&quot;657&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;687&quot; data-start=&quot;662&quot; data-ke-size=&quot;size23&quot;&gt; &lt;b&gt; 2월 &amp;mdash; 좌절과 운이 공존했던 달&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;824&quot; data-start=&quot;689&quot; data-ke-size=&quot;size16&quot;&gt;당근 면접을 위해 약 10일 동안 준비했다. 면접 당일에는 1시간 30분 가까이 면접을 봤는데, 정말 힘들었다.&lt;br /&gt;중간중간 개발자로서 아직 부족하다는 걸 뼈저리게 느꼈고, 그래도 아주 망친 것 같지는 않아서 결과를 기대했지만 결국 탈락했다.&lt;/p&gt;
&lt;p data-end=&quot;952&quot; data-start=&quot;826&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;952&quot; data-start=&quot;826&quot; data-ke-size=&quot;size16&quot;&gt;이후 바로 블레이버스 해커톤에 참가했다.&lt;br /&gt;모든 기능을 완벽히 구현하지는 못했지만, 기획자의 발표와 팀의 적극적인 시연 덕분에 수상까지 이어졌다.&lt;br /&gt;이 경험을 통해 결과만큼이나 &lt;b&gt;자신감과 태도도 중요하다&lt;/b&gt;는 걸 느꼈다.&lt;/p&gt;
&lt;p data-end=&quot;983&quot; data-start=&quot;954&quot; data-ke-size=&quot;size16&quot;&gt;이때부터 졸업과 함께 본격적인 취업 준비를 시작했다.&lt;/p&gt;
&lt;hr data-end=&quot;988&quot; data-start=&quot;985&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1010&quot; data-start=&quot;990&quot; data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;3월 &amp;mdash; 지원, 또 지원&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1123&quot; data-start=&quot;1012&quot; data-ke-size=&quot;size16&quot;&gt;대기업, 중견, 중소 가리지 않고 정말 많은 회사에 지원했다.&lt;br /&gt;면접 준비도 많이 했고, 운 좋게 또 한 번 중소기업 최종 합격을 했다.&lt;br /&gt;하지만 연봉과 기술 스택이 마음에 걸려 입사를 포기했다.&lt;/p&gt;
&lt;p data-end=&quot;1123&quot; data-start=&quot;1012&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1176&quot; data-start=&quot;1125&quot; data-ke-size=&quot;size16&quot;&gt;자소서 첨삭을 받으면서 스스로를 돌아보는 시간이 많았고, 내적으로도 많이 성장했다고 느꼈다.&lt;/p&gt;
&lt;hr data-end=&quot;1181&quot; data-start=&quot;1178&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1204&quot; data-start=&quot;1183&quot; data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;4월 &amp;mdash; 처음 느낀 큰 벽&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1279&quot; data-start=&quot;1206&quot; data-ke-size=&quot;size16&quot;&gt;CJ 올리브영 서류에 합격해 테스트를 봤는데, 생각보다 훨씬 어려웠다.&lt;br /&gt;처음으로 &amp;ldquo;아, 이게 진짜 벽이구나&amp;rdquo;라는 느낌을 받았다.&lt;/p&gt;
&lt;p data-end=&quot;1279&quot; data-start=&quot;1206&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1372&quot; data-start=&quot;1281&quot; data-ke-size=&quot;size16&quot;&gt;대전에서 열린 STDev 해커톤에도 참가했지만, 이 대회에서는 수상을 하지 못했다.&lt;br /&gt;대신 내 문제점을 돌아보고, 앞으로 고쳐야 할 것들을 분명히 인식하게 됐다.&lt;/p&gt;
&lt;hr data-end=&quot;1377&quot; data-start=&quot;1374&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1400&quot; data-start=&quot;1379&quot; data-ke-size=&quot;size23&quot;&gt; &amp;zwj; &lt;b&gt; 5월 &amp;mdash; 잠깐의 휴식&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1486&quot; data-start=&quot;1402&quot; data-ke-size=&quot;size16&quot;&gt;작년과 마찬가지로 5월은 잠시 숨을 고르는 달이었다.&lt;br /&gt;중간에 본 면접은 솔직히 많이 실망스러웠다.&lt;br /&gt;연봉 3000, 전자정부프레임워크 기반 환경.&lt;/p&gt;
&lt;p data-end=&quot;1550&quot; data-start=&quot;1488&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;내가 이런 대우를 받으면서 회사를 다녀야 하나?&amp;rdquo;라는 생각이 들었고, 그래서 오히려 더 열심히 공부하게 됐다.&lt;/p&gt;
&lt;hr data-end=&quot;1555&quot; data-start=&quot;1552&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1579&quot; data-start=&quot;1557&quot; data-ke-size=&quot;size23&quot;&gt; &lt;b&gt; 6월 &amp;mdash; 코딩 테스트의 좌절&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1656&quot; data-start=&quot;1581&quot; data-ke-size=&quot;size16&quot;&gt;삼성 계열사 중 한 곳의 코딩 테스트를 보게 됐다.&lt;br /&gt;문제는 두 개였는데, 올리브영보다도 훨씬 어려웠고 결국 한 문제도 풀지 못했다.&lt;/p&gt;
&lt;p data-end=&quot;1744&quot; data-start=&quot;1658&quot; data-ke-size=&quot;size16&quot;&gt;그동안 코딩 테스트 준비를 정말 열심히 했기에 충격이 컸다.&lt;br /&gt;당황했고, 스스로에게 실망도 많이 했다.&lt;br /&gt;그래도 포기하지 않고 계속 지원하고 준비했다.&lt;/p&gt;
&lt;hr data-end=&quot;1749&quot; data-start=&quot;1746&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1771&quot; data-start=&quot;1751&quot; data-ke-size=&quot;size23&quot;&gt; &lt;b&gt; 7월 &amp;mdash; 프로젝트와 현타&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;1843&quot; data-start=&quot;1773&quot; data-ke-size=&quot;size16&quot;&gt;스위프에 참여해 프로젝트를 진행했다.&lt;br /&gt;MSA와 RabbitMQ를 다뤄보긴 했지만, 아직 공부가 많이 필요하다는 걸 느꼈다.&lt;/p&gt;
&lt;p data-end=&quot;1843&quot; data-start=&quot;1773&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1843&quot; data-start=&quot;1773&quot; data-ke-size=&quot;size16&quot;&gt;이 시기에 동아리에서 강연도 진행했다.&lt;br /&gt;2번째였지만 누군가에게 설명하면서 내가 알고 있는 것과 모르는 것이 더 명확해졌고, 생각보다 많은 걸 배우는 경험이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1843&quot; data-start=&quot;1773&quot; data-ke-size=&quot;size16&quot;&gt;지금 깃허브 프로필이 이 강연 사진이다.&lt;/p&gt;
&lt;p data-end=&quot;1843&quot; data-start=&quot;1773&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1915&quot; data-start=&quot;1845&quot; data-ke-size=&quot;size16&quot;&gt;약학창업 해커톤에도 참가했는데, 프론트 팀원의 실력 차이로 중간중간 현타가 왔다.&lt;br /&gt;결국 이 대회에서도 수상은 하지 못했다.&lt;/p&gt;
&lt;hr data-end=&quot;1920&quot; data-start=&quot;1917&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1943&quot; data-start=&quot;1922&quot; data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;8월 &amp;mdash; 다시 찾아온 성과&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2008&quot; data-start=&quot;1945&quot; data-ke-size=&quot;size16&quot;&gt;스위프 프로젝트를 마무리하고 정션 아시아 해커톤에 참가했다.&lt;br /&gt;그리고 드디어 &lt;b&gt;6개월 만에 다시 수상&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-end=&quot;2094&quot; data-start=&quot;2010&quot; data-ke-size=&quot;size16&quot;&gt;과정도 배운 점도 많았고, 상금 200만 원에 기념품까지 정말 만족스러운 대회였다.&lt;br /&gt;&amp;ldquo;그래도 내가 헛되게 달려온 건 아니었구나&amp;rdquo;라는 생각이 들었다.&lt;/p&gt;
&lt;hr data-end=&quot;2099&quot; data-start=&quot;2096&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2123&quot; data-start=&quot;2101&quot; data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;9월 &amp;mdash; 조용히 지나간 시간&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2210&quot; data-start=&quot;2125&quot; data-ke-size=&quot;size16&quot;&gt;서비스 기업 서류 합격 후 준비를 했고, 하나금융그룹 면접도 봤다.&lt;br /&gt;스위프 최종 발표회에서 발표도 했지만, 그 외에는 크게 기억에 남는 일은 없었다.&lt;/p&gt;
&lt;hr data-end=&quot;2215&quot; data-start=&quot;2212&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2241&quot; data-start=&quot;2217&quot; data-ke-size=&quot;size23&quot;&gt;  &lt;b&gt;10~11월 &amp;mdash; 끝내 도착한 곳&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2293&quot; data-start=&quot;2243&quot; data-ke-size=&quot;size16&quot;&gt;정말 바쁘고 힘들었던 두 달이었다.&lt;br /&gt;특히 다날 기업 면접은 기억에 남을 정도로 빡셌다.&lt;/p&gt;
&lt;p data-end=&quot;2391&quot; data-start=&quot;2295&quot; data-ke-size=&quot;size16&quot;&gt;그럼에도 운 좋게 효성 계열사 &lt;b&gt;갤럭시아머니트리&lt;/b&gt;에 최종 합격하며 다시 PG사로 가게 됐다.&lt;br /&gt;입사까지 한 달 동안 글램핑도 가고, 쿠팡도 뛰어보고, 이것저것 해봤다.&lt;/p&gt;
&lt;p data-end=&quot;2435&quot; data-start=&quot;2393&quot; data-ke-size=&quot;size16&quot;&gt;3년 동안 쉬지 않고 달려왔던 탓인지, 처음으로 깃허브 잔디가 비어 있었다.&lt;/p&gt;
&lt;hr data-end=&quot;2440&quot; data-start=&quot;2437&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2461&quot; data-start=&quot;2442&quot; data-ke-size=&quot;size23&quot;&gt; &lt;b&gt; 12월 &amp;mdash; 새로운 시작&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;2522&quot; data-start=&quot;2463&quot; data-ke-size=&quot;size16&quot;&gt;본격적으로 회사에 출근하면서 2025년이 마무리됐다.&lt;br /&gt;정신없이 지나갔지만, 분명 의미 있는 한 해였다.&lt;/p&gt;
&lt;p data-end=&quot;2522&quot; data-start=&quot;2463&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2522&quot; data-start=&quot;2463&quot; data-ke-size=&quot;size16&quot;&gt;추가로 우아한테크코스 1차 합격했다..... 하 작년에 준비할걸... 너무 후회된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2522&quot; data-start=&quot;2463&quot; data-ke-size=&quot;size16&quot;&gt;지금으로서는 경력이 더 중요하다. 앞으로 신입 개발자는 더더욱 어려워 질테니...&lt;/p&gt;
&lt;hr data-end=&quot;2527&quot; data-start=&quot;2524&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2543&quot; data-start=&quot;2529&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  2026년 목표&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2577&quot; data-start=&quot;2545&quot; data-ke-size=&quot;size16&quot;&gt;2026년에도 2025년과 비슷한 흐름으로 성장하고 싶다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2808&quot; data-start=&quot;2579&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2641&quot; data-start=&quot;2579&quot;&gt;&lt;b&gt;~2월&lt;/b&gt;: 메시지 큐 심화 학습
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2641&quot; data-start=&quot;2606&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2641&quot; data-start=&quot;2606&quot;&gt;블로그가 아닌 &lt;b&gt;GitHub에 논문&amp;middot;보고서 형태로 정리&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2688&quot; data-start=&quot;2642&quot;&gt;&lt;b&gt;3~4월&lt;/b&gt;: 대규모 티켓팅 프로젝트
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2688&quot; data-start=&quot;2671&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2688&quot; data-start=&quot;2671&quot;&gt;MSA, 분산 트랜잭션 중심&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2720&quot; data-start=&quot;2689&quot;&gt;&lt;b&gt;5~8월&lt;/b&gt;: 수익화를 목표로 한 사이드 프로젝트&lt;/li&gt;
&lt;li data-end=&quot;2808&quot; data-start=&quot;2721&quot;&gt;이후에는 회사에서 배운 시스템을 바탕으로&lt;br /&gt;최신 기술 스택을 적용한 &lt;b&gt;나만의 시스템&lt;/b&gt;을 만들고,&lt;br /&gt;문제점을 하나씩 개선하며 성장하고 싶다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 중간에 계속 CS, 김영한 선생님 강의, 자소서 등 공부하면서 열심히 살것이다.&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동 및 자격증 후기</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/340</guid>
      <comments>https://balhae.tistory.com/340#entry340comment</comments>
      <pubDate>Wed, 31 Dec 2025 20:05:44 +0900</pubDate>
    </item>
    <item>
      <title>우아한테크코스 8기 프리코스 후기</title>
      <link>https://balhae.tistory.com/339</link>
      <description>&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;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cZQBLi/dJMcaiIscSY/KxYiZB79ThVoVUeK7kKS71/app-release.apk?attach=1&amp;amp;knm=tfile.apk&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;app-release.apk&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;5.81MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이번 자유 주제에서 개발한 역사 문화재 인식앱이다.&lt;/b&gt; 안드로이드 폰만 작동이 된다. 시연 영상도 추후 다시 찍겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;처음 우테코에 지원하기까지&lt;/b&gt;&lt;/h2&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;그럼에도 불구하고 우테코에 지원한 이유는 단순했다. 한 번 진득하게 다시 공부해 보고 싶었다. 그 과정에서 나와는 다른 개발자들을 만나고 그들로부터 배우고 싶었다. 그리고 무엇보다 자신이 맞다고 생각하는 개발 방식이 정말 맞는지 검증받고 싶었다. 5000자짜리 자소서를 쓸 때도 그런 진심이 충분히 담겨 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프리코스에서의 깨달음&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;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bELZrN/dJMcabP8MYf/TX7eROdKaKm6TkpN613Ta1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bELZrN/dJMcabP8MYf/TX7eROdKaKm6TkpN613Ta1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bELZrN/dJMcabP8MYf/TX7eROdKaKm6TkpN613Ta1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbELZrN%2FdJMcabP8MYf%2FTX7eROdKaKm6TkpN613Ta1%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;1142&quot; height=&quot;623&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;591&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YcxCx/dJMcab3GcfJ/asZcoxyQsztzVWE7LZDjXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YcxCx/dJMcab3GcfJ/asZcoxyQsztzVWE7LZDjXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YcxCx/dJMcab3GcfJ/asZcoxyQsztzVWE7LZDjXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYcxCx%2FdJMcab3GcfJ%2FasZcoxyQsztzVWE7LZDjXK%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;1143&quot; height=&quot;591&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;591&quot;/&gt;&lt;/span&gt;&lt;/figure&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;프리코스가 시작되고 첫 몇 주는 정말 바빴다. 특히 코드리뷰 과정에서 많은 걸 배웠는데, 1주차는 개인 일정 때문에 온전히 집중하지 못했다. 하지만 2주차부터는 정말 몰입했다. 자신이 쓴 코드에 대한 피드백을 받고, 남의 코드를 리뷰하면서 새로운 시각을 얻을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/335&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/335&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763729631266&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;[9oormthon 제주 버스 알림콜] 리메이크 및 조회 성능 최적화 하기&quot; data-og-description=&quot;https://balhae.tistory.com/219  구름톤(9oormthon) 11기 대상 후기  지원 동기와 과정대학교 4학년, 졸업을 앞둔 시점에서 문득 돌아보니 지금까지 만든 프로젝트들이 너무 의미없게 느껴졌습니다. 진&quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/335&quot; data-og-url=&quot;https://balhae.tistory.com/335&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sgX2p/hyZN66U5j3/f1h5Kjr78Nk926mK5Piju0/img.jpg?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/E9Wb5/hyZNEJAzGd/9qdhEppQKrlC1ADMy4TzPK/img.jpg?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/kFGcU/hyZNIeafkC/XRkcly57DKvftgmy5sDpk0/img.png?width=854&amp;amp;height=523&amp;amp;face=0_0_854_523&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/335&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/335&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sgX2p/hyZN66U5j3/f1h5Kjr78Nk926mK5Piju0/img.jpg?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/E9Wb5/hyZNEJAzGd/9qdhEppQKrlC1ADMy4TzPK/img.jpg?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/kFGcU/hyZNIeafkC/XRkcly57DKvftgmy5sDpk0/img.png?width=854&amp;amp;height=523&amp;amp;face=0_0_854_523');&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;[9oormthon 제주 버스 알림콜] 리메이크 및 조회 성능 최적화 하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://balhae.tistory.com/219  구름톤(9oormthon) 11기 대상 후기  지원 동기와 과정대학교 4학년, 졸업을 앞둔 시점에서 문득 돌아보니 지금까지 만든 프로젝트들이 너무 의미없게 느껴졌습니다. 진&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리코스를 하면서 비로소 깨달았다. &lt;b&gt;개발은 기능 구현이 아니라 읽을 수 있는 코드를 만드는 것&lt;/b&gt;이라는 걸. SOLID 원칙 중 단일 책임 원칙은 개념으로만 알고 있었는데, 코드리뷰를 통해 이게 얼마나 중요한지 체감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 메서드는 하나의 책임만 가져야 한다. 메서드의 라인 수는 15줄을 넘기지 않는 게 좋다. 기능을 쪼갤 수 있는 만큼 쪼개야 한다. 이런 원칙들이 단순한 규칙이 아니라 유지보수성을 위한 필수 조건이라는 걸 깨달았을 때, 나는 개발자로서 한 단계 성장한 것 같았다.&lt;/p&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 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;3주차 로또 과제를 끝내고 4주차가 되었을 때, 나는 충격을 받았다. 4주차는 이전과 완전히 달랐다. 프리코스 1~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;그 순간 나는 결정했다. 단순히 프리코스 요구사항을 충족시키는 프로젝트가 아니라, 내가 정말 만들고 싶었던 것을 만들자고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1년, 아니 2년 전부터 계속 마음에 담고 있던 게 있었다. 한국 문화재를 알려주는 애플리케이션이었다. 그리고 한 가지 더 추가하고 싶었다. 요즘 공부하고 있던 &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;그래서 결심했다. 남은 3주 동안 Android Studio와 Kotlin으로 앱을 만들고, Spring Boot와 Kotlin으로 백엔드 서버를 구축하겠다고. 우테코에서 배운 클린코드의 원칙들을 여기에 모두 적용해서.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;시간과의 싸움&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1843&quot; data-origin-height=&quot;905&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n5AlF/dJMcafSxSRk/9SsMg1KuesJ4995q54Vbbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n5AlF/dJMcafSxSRk/9SsMg1KuesJ4995q54Vbbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n5AlF/dJMcafSxSRk/9SsMg1KuesJ4995q54Vbbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn5AlF%2FdJMcafSxSRk%2F9SsMg1KuesJ4995q54Vbbk%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;1843&quot; height=&quot;905&quot; data-origin-width=&quot;1843&quot; data-origin-height=&quot;905&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;당연히 쉬운 길은 아니었다. 인프런에서 Android 강의를 구매했다. 앱 개발에 대한 기본기가 부족했기 때문에 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;그런데 세상일이란 게... 정확히 그 시기에 면접과 시험이 겹쳤다. 21일이라는 기간이 있었지만, 실제로 온전히 집중할 수 있었던 건 12일 정도였다. 절박했다. 남은 시간 안에 강의를 완료하고 앱을 완성해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 신기한 게, 제약이 오히려 나를 만들었다. 남은 12일 동안 나는 그 어느 때보다 집중했다. 강의를 끝내고, 실제로 앱을 만들어내야 한다는 긴박감이 나를 움직였다. 잠을 줄이고, 밥도 빨리 먹고, 오직 코딩에만 몰입했다.&lt;/p&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년을 기다렸던 그 앱을 만들어냈지만 이후 추가할 기능들은 산더미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Woowacourse-kotlin-master&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Woowacourse-kotlin-master&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763729511381&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;Woowacourse-kotlin-master&quot; data-og-description=&quot;Woowacourse-kotlin-master has 10 repositories available. Follow their code on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Woowacourse-kotlin-master&quot; data-og-url=&quot;https://github.com/Woowacourse-kotlin-master&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/pwEnj/hyZOfivrMb/F70KLIdFGzMaWfCKM2SDp1/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420,https://scrap.kakaocdn.net/dn/dkzdPc/hyZN24xzMD/VukppeVmr35Q7BfLwJNOk0/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420&quot;&gt;&lt;a href=&quot;https://github.com/Woowacourse-kotlin-master&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Woowacourse-kotlin-master&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/pwEnj/hyZOfivrMb/F70KLIdFGzMaWfCKM2SDp1/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420,https://scrap.kakaocdn.net/dn/dkzdPc/hyZN24xzMD/VukppeVmr35Q7BfLwJNOk0/img.png?width=420&amp;amp;height=420&amp;amp;face=0_0_420_420');&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;Woowacourse-kotlin-master&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Woowacourse-kotlin-master has 10 repositories available. Follow their code on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;이 프로젝트를 완성했을 때의 그 느낌은 정말 특별했다. 단순히 &quot;앱을 만들었다&quot;가 아니라, &quot;클린코드 원칙을 지키면서 앱을 만들었다&quot;는 게 자랑스러웠다. 우테코에서 배운 개발 철학을 실제로 구현해 낼 수 있었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 이미 취업을 했지만, 만약 우테코 최종 합격 통보를 받았다면 그곳을 택할 것이다. 배달의민족에서 함께 일하고 싶다는 마음은 여전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;p data-ke-size=&quot;size16&quot;&gt;마지막으로 이런 좋은 기회를 준 우테코 관계자님들께 정말 감사드린다. 앞으로도 배움을 멈추지 않겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>대외활동 및 자격증 후기</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/339</guid>
      <comments>https://balhae.tistory.com/339#entry339comment</comments>
      <pubDate>Fri, 21 Nov 2025 21:56:06 +0900</pubDate>
    </item>
    <item>
      <title>인덱스에 대해</title>
      <link>https://balhae.tistory.com/338</link>
      <description>&lt;h2 data-end=&quot;147&quot; data-start=&quot;0&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;147&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요.&lt;br /&gt;이번 시간에는 &lt;b&gt;인덱스(Index)&lt;/b&gt; 에 대해 확실히 정복하고자 한다.&lt;/p&gt;
&lt;p data-end=&quot;147&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;단순히 &amp;ldquo;검색을 빠르게 해주는 기능&amp;rdquo;으로만 알고 있었다면, 오늘을 기점으로 인덱스의 &lt;b&gt;구조, 동작 원리, 선택 기준&lt;/b&gt;까지 명확히 이해하게 될 것이다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;232&quot; data-start=&quot;149&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;232&quot; data-start=&quot;149&quot; data-ke-size=&quot;size16&quot;&gt;핵심은 &amp;ldquo;&lt;b&gt;왜 이 인덱스를 써야 하는가&lt;/b&gt;&amp;rdquo;를 스스로 설명할 수 있는 수준까지 도달하는 것이다.&lt;br /&gt;그럼 지금부터 인덱스의 세계로 함께 들어가보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/335&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/335&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760532434909&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;[9oormthon 제주 버스 알림콜] 리메이크 및 조회 성능 최적화 하기&quot; data-og-description=&quot;https://balhae.tistory.com/219  구름톤(9oormthon) 11기 대상 후기  지원 동기와 과정대학교 4학년, 졸업을 앞둔 시점에서 문득 돌아보니 지금까지 만든 프로젝트들이 너무 의미없게 느껴졌습니다. 진&quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/335&quot; data-og-url=&quot;https://balhae.tistory.com/335&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/BqggC/hyZJ6mD6EU/eSckrNlApijBc1TqumMZKK/img.jpg?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/bEBnnD/hyZLB0c8iF/lZZ4U3keOWwXHcNNPM6pH0/img.jpg?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/P6rYU/hyZLhfLMYC/mZmPxnWnVkl9KWbkFR1uBK/img.png?width=854&amp;amp;height=523&amp;amp;face=0_0_854_523&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/335&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/335&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/BqggC/hyZJ6mD6EU/eSckrNlApijBc1TqumMZKK/img.jpg?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/bEBnnD/hyZLB0c8iF/lZZ4U3keOWwXHcNNPM6pH0/img.jpg?width=800&amp;amp;height=319&amp;amp;face=0_0_800_319,https://scrap.kakaocdn.net/dn/P6rYU/hyZLhfLMYC/mZmPxnWnVkl9KWbkFR1uBK/img.png?width=854&amp;amp;height=523&amp;amp;face=0_0_854_523');&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;[9oormthon 제주 버스 알림콜] 리메이크 및 조회 성능 최적화 하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://balhae.tistory.com/219  구름톤(9oormthon) 11기 대상 후기  지원 동기와 과정대학교 4학년, 졸업을 앞둔 시점에서 문득 돌아보니 지금까지 만든 프로젝트들이 너무 의미없게 느껴졌습니다. 진&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.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;67&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;지난 &lt;b&gt;제주 버스 알림콜 프로젝트&lt;/b&gt;에서 조회 성능을 최적화하는 과정에서 인덱스에 대해 얕게 공부해 본 적이 있다.&lt;/p&gt;
&lt;p data-end=&quot;90&quot; data-start=&quot;69&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;90&quot; data-start=&quot;69&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;인덱스 A to Z&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;90&quot; data-start=&quot;69&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 &lt;b&gt;인덱스란 무엇일까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;219&quot; data-start=&quot;92&quot; data-ke-size=&quot;size16&quot;&gt;인덱스(Index)는 말 그대로 &lt;b&gt;&amp;lsquo;색인&amp;rsquo;&lt;/b&gt; 또는 &lt;b&gt;&amp;lsquo;목차&lt;/b&gt;&amp;rsquo;를 의미한다.&lt;/p&gt;
&lt;p data-end=&quot;219&quot; data-start=&quot;92&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;219&quot; data-start=&quot;92&quot; data-ke-size=&quot;size16&quot;&gt;책에서 원하는 내용을 빠르게 찾기 위해 목차를 보는 것처럼, 데이터베이스에서도 원하는 데이터를 더 효율적으로 찾기 위해 인덱스를 사용한다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;263&quot; data-start=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;263&quot; data-start=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 인덱스는 &lt;b&gt;왜 도입되었을까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;263&quot; data-start=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;263&quot; data-start=&quot;221&quot; data-ke-size=&quot;size16&quot;&gt;그 이유부터 차근히 살펴보자.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 37.7899%; height: 122px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; width: 16.4659%;&quot;&gt;fisrt_name&lt;/td&gt;
&lt;td style=&quot;height: 17px; width: 20.1898%;&quot;&gt;age&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 16.4659%;&quot;&gt;김씨&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 20.1898%;&quot;&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 16.4659%;&quot;&gt;이씨&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 20.1898%;&quot;&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 16.4659%;&quot;&gt;박씨&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 20.1898%;&quot;&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 16.4659%;&quot;&gt;김씨&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 20.1898%;&quot;&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px; width: 16.4659%;&quot;&gt;이씨&lt;/td&gt;
&lt;td style=&quot;height: 21px; width: 20.1898%;&quot;&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;a858dd84-753a-49f8-a743-298a5e83300a&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-end=&quot;69&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;위의 그림과 같이 people 테이블이 있다고 가정하자.&lt;br /&gt;이 테이블에서 &lt;b&gt;age = 20&lt;/b&gt;인 데이터를 찾으려면&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760538698713&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM people WHERE age = 20;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;135&quot; data-start=&quot;117&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;135&quot; data-start=&quot;117&quot; data-ke-size=&quot;size16&quot;&gt;이라는 쿼리를 실행해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;248&quot; data-start=&quot;137&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이때 &lt;b&gt;컴퓨터는 age가 20인 행을 찾기 위해 모든 데이터를 처음부터 끝까지 하나씩 확인&lt;/b&gt;해야 한다.&lt;br /&gt;만약 행이 &lt;b&gt;1억 개&lt;/b&gt;라면, &lt;b&gt;1억 번&lt;/b&gt;을 모두 확인해야 하는 셈이다.&lt;/p&gt;
&lt;p data-end=&quot;293&quot; data-start=&quot;250&quot; data-ke-size=&quot;size16&quot;&gt;이 방식이 바로 &lt;b&gt;풀 테이블 스캔(Full Table Scan)&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-end=&quot;298&quot; data-start=&quot;295&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;478&quot; data-start=&quot;300&quot; data-ke-size=&quot;size16&quot;&gt;풀 테이블 스캔은 데이터를 &lt;b&gt;순차적으로 접근&lt;/b&gt;하기 때문에 오히려 &lt;b&gt;작은 테이블&lt;/b&gt;에서는 효율적일 수 있다.&lt;br /&gt;또한 &lt;b&gt;적용 가능한 인덱스가 없거나&lt;/b&gt;, &lt;b&gt;인덱스를 적용하더라도 범위가 너무 넓어 성능 이점이 적을 때&lt;/b&gt;에도 사용된다.&lt;br /&gt;즉, &lt;b&gt;비용 대비 효과가 낮을 때&lt;/b&gt; 풀 테이블 스캔을 수행하는 것이다.&lt;/p&gt;
&lt;hr data-end=&quot;483&quot; data-start=&quot;480&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;485&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 컴퓨터가 데이터를 &lt;b&gt;더 빠르고 효율적으로 찾기 위해&lt;/b&gt; 도입된 것이 바로 &lt;b&gt;인덱스(Index)&lt;/b&gt;다.&lt;/p&gt;
&lt;hr data-end=&quot;555&quot; data-start=&quot;552&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;580&quot; data-start=&quot;557&quot; data-ke-size=&quot;size16&quot;&gt;인덱스를 이해하기 쉽게 예를 들어보자.&lt;/p&gt;
&lt;p data-end=&quot;633&quot; data-start=&quot;582&quot; data-ke-size=&quot;size16&quot;&gt;숫자 1부터 100까지 중에서 A가 생각한 숫자를 &lt;b&gt;B와 C가 맞혀야 한다&lt;/b&gt;고 하자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;817&quot; data-start=&quot;635&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;707&quot; data-start=&quot;635&quot;&gt;&lt;b&gt;B&lt;/b&gt;는 1이에요? 2에요? &amp;hellip; 이런 식으로 하나씩 물어본다.&lt;br /&gt;&amp;rarr; &lt;b&gt;최악의 경우 100번&lt;/b&gt;을 물어봐야 한다.&lt;/li&gt;
&lt;li data-end=&quot;817&quot; data-start=&quot;709&quot;&gt;&lt;b&gt;C&lt;/b&gt;는 &amp;ldquo;50보다 크나요?&amp;rdquo;, &amp;ldquo;75보다 작나요?&amp;rdquo;처럼 &lt;b&gt;중간값을 기준으로 절반씩 나누며 탐색&lt;/b&gt;한다.&lt;br /&gt;&amp;rarr; 이렇게 하면 &lt;b&gt;훨씬 적은 횟수로&lt;/b&gt; 원하는 값을 찾을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;888&quot; data-start=&quot;819&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 &lt;b&gt;인덱스는 데이터를 정렬한 후,&lt;/b&gt; &lt;b&gt;이진 탐색(Binary Search)&lt;/b&gt;처럼 빠르게 접근할 수 있도록 도와준다.&lt;/p&gt;
&lt;hr data-end=&quot;893&quot; data-start=&quot;890&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;1060&quot; data-start=&quot;895&quot; data-ke-size=&quot;size16&quot;&gt;즉, 인덱스를 age 컬럼에 적용하면&lt;br /&gt;age 값을 &lt;b&gt;정렬된 상태로 복사해 별도의 구조(B+Tree 등)&lt;/b&gt;로 저장한다.&lt;/p&gt;
&lt;p data-end=&quot;1060&quot; data-start=&quot;895&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1060&quot; data-start=&quot;895&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 DB는 &lt;b&gt;정렬된 사본(인덱스)을 통해 빠르게 특정 값을 찾고&lt;/b&gt;,&lt;br /&gt;그 인덱스가 가리키는 &lt;b&gt;원본 데이터(테이블 레코드)&lt;/b&gt;에 접근하게 된다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1129&quot; data-start=&quot;1062&quot; data-ke-size=&quot;size16&quot;&gt;정리하자면,&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1129&quot; data-start=&quot;1062&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;인덱스란 정렬된 데이터의 사본을 유지하여, 원하는 값을 훨씬 빠르게 찾을 수 있게 하는 기술이다.&lt;/b&gt;&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;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVLzUS/dJMb9LKJnaK/EAhrHJuR2laIICMdOPHlt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVLzUS/dJMb9LKJnaK/EAhrHJuR2laIICMdOPHlt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVLzUS/dJMb9LKJnaK/EAhrHJuR2laIICMdOPHlt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVLzUS%2FdJMb9LKJnaK%2FEAhrHJuR2laIICMdOPHlt1%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;455&quot; height=&quot;362&quot; data-origin-width=&quot;455&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&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;Array나 Linked List처럼 단순히 순차적으로 정렬된 구조가 아니라&lt;/b&gt;, &lt;b&gt;트리(Tree) 형태로 정렬된 구조&lt;/b&gt;를 가진다.&lt;br /&gt;이때 사용되는 방식이 바로 &lt;b&gt;이진 탐색 트리(Binary Search Tree)&lt;/b&gt; 구조이며, 탐색의 시간 복잡도는 &lt;b&gt;O(log N)&lt;/b&gt; 수준으로 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 인덱스가 없을 경우에는 &lt;b&gt;O(N)&lt;/b&gt; 시간이 걸리게 된다.&lt;br /&gt;즉, &lt;b&gt;이진 탐색 트리 기반의 트리 구조가 기본적인 테이블 인덱스의 핵심 원리&lt;/b&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vKXkf/dJMb9LDXIPz/LKkozIJGSfBqX1oyoX7NKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vKXkf/dJMb9LDXIPz/LKkozIJGSfBqX1oyoX7NKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vKXkf/dJMb9LDXIPz/LKkozIJGSfBqX1oyoX7NKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvKXkf%2FdJMb9LDXIPz%2FLKkozIJGSfBqX1oyoX7NKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;942&quot; height=&quot;530&quot; data-origin-width=&quot;942&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 노드를 &lt;b&gt;하나씩&lt;/b&gt;만 배치하는 것이 아니라, &lt;b&gt;두 개 이상씩&lt;/b&gt; 묶어서 배치하는 방식을 사용하면 이것이 바로 &lt;b&gt;B-Tree&lt;/b&gt; 구조이다.&lt;br /&gt;즉, 이러한 방식으로 구성하면 &lt;b&gt;1부터 13까지의 데이터&lt;/b&gt;를 효율적으로 탐색할 수 있으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;앞서 살펴본 &lt;b&gt;이진 탐색 트리(Binary Search Tree)&lt;/b&gt; 방식에서는 &lt;b&gt;1부터 7까지만&lt;/b&gt; 처리할 수 있었던 것과 비교해&lt;br /&gt;&lt;b&gt;더 많은 데이터를 한 번에 관리하고 탐색 효율을 높일 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;427&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZoErS/dJMb9XYDcJr/nX0xUlrAxNQNr45BFWYP51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZoErS/dJMb9XYDcJr/nX0xUlrAxNQNr45BFWYP51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZoErS/dJMb9XYDcJr/nX0xUlrAxNQNr45BFWYP51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZoErS%2FdJMb9XYDcJr%2FnX0xUlrAxNQNr45BFWYP51%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;892&quot; height=&quot;427&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;427&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-end=&quot;230&quot; data-start=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;위 그림처럼 &lt;b&gt;가이드라인이 제공되어 탐색이 훨씬 용이&lt;/b&gt;해진다.&lt;br /&gt;이때 &lt;b&gt;B-Tree와의 가장 큰 차이점&lt;/b&gt;은, 단순히 가이드라인만 있는 것이 아니라 &lt;b&gt;하위 노드들이 서로 연결(Linked)되어 있다는 점&lt;/b&gt;이다.&lt;br /&gt;즉, 노드 간 이동이 훨씬 간단하며, &lt;b&gt;특히 범위 검색(Range Scan)에 매우 효율적&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;230&quot; data-start=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;381&quot; data-start=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, &amp;lsquo;나이가 4부터 8까지인 데이터&amp;rsquo;를 찾는 경우를 보자.&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;381&quot; data-start=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;381&quot; data-start=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;B-Tree&lt;/b&gt;에서는 각 값을 찾기 위해 매번 노드를 타고 내려가야 하지만,&lt;br /&gt;&lt;b&gt;B+Tree&lt;/b&gt;에서는 한 번 4를 찾은 뒤, &lt;b&gt;연결된 노드를 따라 순차적으로 이동하기만 하면&lt;/b&gt; 된다.&lt;/p&gt;
&lt;p data-end=&quot;435&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;435&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;이러한 이유로 &lt;b&gt;최근 대부분의 데이터베이스는 인덱스를 B+Tree 구조로 구현&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;141&quot; data-start=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;즉, 정리하자면 &lt;b&gt;인덱스가 없는 경우&lt;/b&gt;에는 테이블에 존재하는 &lt;b&gt;모든 데이터를 전부 탐색(Full Table Scan)&lt;/b&gt; 해야 하므로&lt;br /&gt;검색 속도가 매우 느리다.&lt;/p&gt;
&lt;p data-end=&quot;141&quot; data-start=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;316&quot; data-start=&quot;143&quot; data-ke-size=&quot;size16&quot;&gt;반면, &lt;b&gt;인덱스가 존재하는 경우&lt;/b&gt;에는 &lt;b&gt;트리 구조를 따라 몇 번의 탐색만으로 원하는 데이터를 빠르게 찾을 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;316&quot; data-start=&quot;143&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;316&quot; data-start=&quot;143&quot; data-ke-size=&quot;size16&quot;&gt;또한 인덱스에는 &lt;b&gt;원본 테이블의 행 주소(Row Address)&lt;/b&gt; 가 함께 저장되어 있어,&lt;/p&gt;
&lt;p data-end=&quot;316&quot; data-start=&quot;143&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;316&quot; data-start=&quot;143&quot; data-ke-size=&quot;size16&quot;&gt;해당 주소를 참조함으로써 &lt;b&gt;실제 테이블의 행을 효율적으로 반환&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;368&quot; data-start=&quot;318&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;368&quot; data-start=&quot;318&quot; data-ke-size=&quot;size16&quot;&gt;결과적으로, 인덱스는 &lt;b&gt;검색 속도를 획기적으로 향상시키는 핵심 구조&lt;/b&gt;라고 할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;368&quot; data-start=&quot;318&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;368&quot; data-start=&quot;318&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원본 테이블&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-end=&quot;147&quot; data-start=&quot;49&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;id&lt;/td&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; data-end=&quot;111&quot; data-start=&quot;94&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;98&quot; data-start=&quot;94&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;104&quot; data-start=&quot;98&quot; data-col-size=&quot;sm&quot;&gt;김을용&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;111&quot; data-start=&quot;104&quot; data-col-size=&quot;sm&quot;&gt;서울대&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;129&quot; data-start=&quot;112&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;116&quot; data-start=&quot;112&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;122&quot; data-start=&quot;116&quot; data-col-size=&quot;sm&quot;&gt;박덕팔&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;129&quot; data-start=&quot;122&quot; data-col-size=&quot;sm&quot;&gt;연세대&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;147&quot; data-start=&quot;130&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;134&quot; data-start=&quot;130&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;140&quot; data-start=&quot;134&quot; data-col-size=&quot;sm&quot;&gt;이상구&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;147&quot; data-start=&quot;140&quot; data-col-size=&quot;sm&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;Index1&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;강사&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;김을용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;박덕팔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이상구&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;5a2dc25e-8f72-4749-a64e-561b05092e6a&quot; data-message-author-role=&quot;assistant&quot;&gt;&lt;b&gt;Index2&lt;/b&gt;&lt;br /&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;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;출신 대학&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;고려대&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서울대&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;연세대&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-end=&quot;162&quot; data-start=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;162&quot; data-start=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 &lt;b&gt;특정 컬럼을 복사해 정렬해 두는 구조&lt;/b&gt;이기 때문에,&lt;br /&gt;본래 테이블 외에도 위와 같이 &lt;b&gt;추가적인 저장 공간&lt;/b&gt;을 차지한다.&lt;/p&gt;
&lt;p data-end=&quot;162&quot; data-start=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 &lt;b&gt;검색에 사용되지 않는 컬럼에 불필요한 인덱스를 생성하는 것은 비효율적&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;326&quot; data-start=&quot;164&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;326&quot; data-start=&quot;164&quot; data-ke-size=&quot;size16&quot;&gt;또한 인덱스는 &lt;b&gt;조회 속도를 빠르게 해주는 대신&lt;/b&gt;,&lt;br /&gt;데이터를 &lt;b&gt;삽입(INSERT)&lt;/b&gt;&amp;middot;&lt;b&gt;수정(UPDATE)&lt;/b&gt;&amp;middot;&lt;b&gt;삭제(DELETE)&lt;/b&gt; 할 때마다&lt;/p&gt;
&lt;p data-end=&quot;326&quot; data-start=&quot;164&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;326&quot; data-start=&quot;164&quot; data-ke-size=&quot;size16&quot;&gt;인덱스도 함께 갱신되어야 한다.&lt;br /&gt;따라서 &lt;b&gt;쓰기 작업이 잦은 테이블에서는 오버헤드가 커지고, 성능이 저하될 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-end=&quot;331&quot; data-start=&quot;328&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;545&quot; data-start=&quot;333&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클러스터드 인덱스(Clustered Index)&lt;/b&gt; 는 일반적으로 &lt;b&gt;PRIMARY KEY로 선언된 컬럼에 자동 적용&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-end=&quot;545&quot; data-start=&quot;333&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;545&quot; data-start=&quot;333&quot; data-ke-size=&quot;size16&quot;&gt;보조 인덱스(Non-clustered Index)처럼 별도로 생성되는 구조가 아니라, &lt;b&gt;테이블 자체의 물리적 데이터가 인덱스 순서에 따라 정렬되는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;545&quot; data-start=&quot;333&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;545&quot; data-start=&quot;333&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;테이블 자체가 인덱스 구조로 저장되는 형태&lt;/b&gt;라고 볼 수 있다.&lt;/p&gt;
&lt;hr data-end=&quot;550&quot; data-start=&quot;547&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;일반적으로 &lt;b&gt;인덱스는 하나의 데이터베이스 객체로서 약 10% 정도의 추가 저장 공간을 필요로&lt;/b&gt; 한다.&lt;br /&gt;새로운 데이터를 삽입할 때 &lt;b&gt;페이지(Page)&lt;/b&gt; 내부에 여유 공간이 없으면,&lt;/p&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;DB는 공간을 나누어 저장하기 위해 &lt;b&gt;페이지 분할(Page Split)&lt;/b&gt; 을 수행한다.&lt;/p&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;754&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;이 과정은 &lt;b&gt;디스크 I/O 증가 및 성능 저하&lt;/b&gt;로 이어질 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;941&quot; data-start=&quot;756&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;941&quot; data-start=&quot;756&quot; data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;DELETE 작업 시 인덱스 데이터는 실제로 삭제되지 않고 &amp;lsquo;사용 안 함&amp;rsquo; 표시만 되며&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;UPDATE는 내부적으로 DELETE + INSERT 방식으로 처리&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-end=&quot;941&quot; data-start=&quot;756&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;941&quot; data-start=&quot;756&quot; data-ke-size=&quot;size16&quot;&gt;이러한 동작이 반복되면 &lt;b&gt;인덱스의 조각화(Fragmentation)&lt;/b&gt; 가 심해지고,&lt;br /&gt;결과적으로 &lt;b&gt;쿼리 성능이 점차 떨어지게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-end=&quot;946&quot; data-start=&quot;943&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1057&quot; data-start=&quot;948&quot; data-ke-size=&quot;size16&quot;&gt;정리하자면, 인덱스는 &lt;b&gt;조회 성능 향상에는 탁월하지만&lt;/b&gt;,&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1057&quot; data-start=&quot;948&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;1057&quot; data-start=&quot;948&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저장 공간 증가와 쓰기 성능 저하라는 대가&lt;/b&gt;를 수반한다.&lt;br /&gt;따라서 &lt;b&gt;읽기와 쓰기 비율을 고려한 인덱스 설계가 중요&lt;/b&gt;하다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1760601498420&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE member (
    id INT PRIMARY KEY,         -- 클러스터링 인덱스
    name VARCHAR(255),
    email VARCHAR(255) UNIQUE   -- 논-클러스터링 인덱스
);&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;237&quot; data-start=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;보조 인덱스(Non-Clustered Index)&lt;/b&gt; 는 우리가 앞서 살펴본 인덱스 구조를 의미한다.&lt;/p&gt;
&lt;p data-end=&quot;237&quot; data-start=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;237&quot; data-start=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;클러스터드 인덱스와 달리 테이블의 물리적 순서와는 별개로 관리되는 인덱스&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;237&quot; data-start=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;237&quot; data-start=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;이 인덱스는 &lt;b&gt;데이터 행의 주소(Row Address)&lt;/b&gt; 를 별도로 가지고 있어, 인덱스를 통해 해당 주소를 찾아가 실제 데이터를 조회하게 된다.&lt;/p&gt;
&lt;p data-end=&quot;369&quot; data-start=&quot;239&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;369&quot; data-start=&quot;239&quot; data-ke-size=&quot;size16&quot;&gt;또한, &lt;b&gt;UNIQUE 제약 조건(Constraint)&lt;/b&gt; 을 설정하면 해당 컬럼에 대해 &lt;b&gt;자동으로 보조 인덱스가 생성&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-end=&quot;369&quot; data-start=&quot;239&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;369&quot; data-start=&quot;239&quot; data-ke-size=&quot;size16&quot;&gt;이로써 중복된 값을 방지함과 동시에,해당 컬럼을 기준으로 한 &lt;b&gt;검색 성능이 향상&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-end=&quot;369&quot; data-start=&quot;239&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760601467976&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 보조 인덱스 생성 3가지 방법 정리

-- 방법 1: 제약조건으로 UNIQUE 인덱스 생성
ALTER TABLE member
ADD CONSTRAINT unq_name UNIQUE (name);

-- 방법 2: UNIQUE INDEX 직접 생성
CREATE UNIQUE INDEX unq_idx_name
ON member (name);

-- 방법 3: 일반 INDEX (중복 허용)
CREATE INDEX idx_name
ON member (name);&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;199&quot; data-start=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;추가로, &lt;b&gt;중복을 방지하고 싶다면 UNIQUE 제약 조건을 사용&lt;/b&gt;하면 된다.&lt;/p&gt;
&lt;p data-end=&quot;199&quot; data-start=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;데이터베이스에서는 일반적으로 &lt;b&gt;기본 인덱스(Default Index)&lt;/b&gt; 가 자동으로 생성되지만,&lt;br /&gt;&lt;b&gt;중복을 허용하지 않으려면 UNIQUE INDEX를 명시적으로 생성&lt;/b&gt;하면 된다.&lt;/p&gt;
&lt;p data-end=&quot;205&quot; data-start=&quot;201&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;265&quot; data-start=&quot;206&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;238&quot; data-start=&quot;206&quot;&gt;중복 허용 &amp;rarr; 일반 인덱스(Normal Index), UNIQUE 제약 조건&lt;/li&gt;
&lt;li data-end=&quot;265&quot; data-start=&quot;239&quot;&gt;중복 방지 &amp;rarr; &lt;b&gt;UNIQUE INDEX&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;286&quot; data-start=&quot;267&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 선택적으로 설정할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcZqWf/dJMb9NBMDX1/xcOpKLBckYen4ZlLrEvNgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcZqWf/dJMb9NBMDX1/xcOpKLBckYen4ZlLrEvNgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcZqWf/dJMb9NBMDX1/xcOpKLBckYen4ZlLrEvNgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcZqWf%2FdJMb9NBMDX1%2FxcOpKLBckYen4ZlLrEvNgK%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;788&quot; height=&quot;610&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 보조 인덱스 구조다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;추가로, &lt;/span&gt;&lt;b&gt;클러스터드 인덱스&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;와 &lt;/span&gt;&lt;b&gt;논클러스터드 인덱스&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;를 구성할 때는 &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;실제로 &lt;/span&gt;&lt;b&gt;보조 인덱스(Non-Clustered Index)&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 에는 &lt;/span&gt;&lt;b&gt;데이터 페이지의 물리적 주소값이 직접 저장되는 것이 아니라&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;해당 행을 식별할 수 있는 &lt;/span&gt;&lt;b&gt;클러스터드 인덱스의 키 값(ID 컬럼 값)&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 이 저장된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; data-start=&quot;943&quot; data-end=&quot;946&quot; /&gt;
&lt;p data-end=&quot;150&quot; data-start=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;150&quot; data-start=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;150&quot; data-start=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;id, 이름, 그룹, 이메일, 성별, 주민번호, 나이 등등&lt;/p&gt;
&lt;p data-end=&quot;150&quot; data-start=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;150&quot; data-start=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 인덱스 구조가 복잡하다 보니, &lt;b&gt;어떤 컬럼에 인덱스를 적용해야 할지&lt;/b&gt; 고민이 생긴다.&lt;/p&gt;
&lt;p data-end=&quot;150&quot; data-start=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;앞서 언급했듯이, 그 해답은 바로 &lt;b&gt;카디널리티(Cardinality)&lt;/b&gt; 에 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;291&quot; data-start=&quot;152&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;291&quot; data-start=&quot;152&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;카디널리티가 높은 컬럼&lt;/b&gt;, 다시 말해 &lt;b&gt;중복되는 값이 적은 컬럼&lt;/b&gt;에 인덱스를 적용하는 것이 효율적이다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;291&quot; data-start=&quot;152&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;291&quot; data-start=&quot;152&quot; data-ke-size=&quot;size16&quot;&gt;이런 컬럼일수록 &lt;b&gt;인덱스를 통한 데이터 선별 효과가 커지고&lt;/b&gt;, 결과적으로 &lt;b&gt;검색 성능 향상에 가장 큰 영향을 준다.&lt;/b&gt;&lt;/p&gt;
&lt;p 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;709&quot; data-start=&quot;27&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style7&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;이름&lt;/td&gt;
&lt;td&gt;그룹&lt;/td&gt;
&lt;td&gt;성별&lt;/td&gt;
&lt;td&gt;이메일&lt;/td&gt;
&lt;td&gt;사는 지역&lt;/td&gt;
&lt;td&gt;나이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;181&quot; data-start=&quot;134&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;138&quot; data-start=&quot;134&quot;&gt;1&lt;/td&gt;
&lt;td data-end=&quot;143&quot; data-start=&quot;138&quot; data-col-size=&quot;sm&quot;&gt;승호&lt;/td&gt;
&lt;td data-end=&quot;148&quot; data-start=&quot;143&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;152&quot; data-start=&quot;148&quot; data-col-size=&quot;sm&quot;&gt;남&lt;/td&gt;
&lt;td data-end=&quot;170&quot; data-start=&quot;152&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;169&quot; data-start=&quot;154&quot;&gt;test1@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;175&quot; data-start=&quot;170&quot; data-col-size=&quot;sm&quot;&gt;성남&lt;/td&gt;
&lt;td data-end=&quot;181&quot; data-start=&quot;175&quot; data-col-size=&quot;sm&quot;&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;230&quot; data-start=&quot;182&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;186&quot; data-start=&quot;182&quot;&gt;2&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;191&quot; data-start=&quot;186&quot;&gt;디코&lt;/td&gt;
&lt;td data-end=&quot;196&quot; data-start=&quot;191&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;200&quot; data-start=&quot;196&quot; data-col-size=&quot;sm&quot;&gt;여&lt;/td&gt;
&lt;td data-end=&quot;219&quot; data-start=&quot;200&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;218&quot; data-start=&quot;202&quot;&gt;test2@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;224&quot; data-start=&quot;219&quot; data-col-size=&quot;sm&quot;&gt;서울&lt;/td&gt;
&lt;td data-end=&quot;230&quot; data-start=&quot;224&quot; data-col-size=&quot;sm&quot;&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;279&quot; data-start=&quot;231&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;235&quot; data-start=&quot;231&quot;&gt;3&lt;/td&gt;
&lt;td data-end=&quot;240&quot; data-start=&quot;235&quot; data-col-size=&quot;sm&quot;&gt;세현&lt;/td&gt;
&lt;td data-end=&quot;245&quot; data-start=&quot;240&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;249&quot; data-start=&quot;245&quot; data-col-size=&quot;sm&quot;&gt;남&lt;/td&gt;
&lt;td data-end=&quot;268&quot; data-start=&quot;249&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;267&quot; data-start=&quot;251&quot;&gt;test3@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;273&quot; data-start=&quot;268&quot; data-col-size=&quot;sm&quot;&gt;수원&lt;/td&gt;
&lt;td data-end=&quot;279&quot; data-start=&quot;273&quot; data-col-size=&quot;sm&quot;&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;326&quot; data-start=&quot;280&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;284&quot; data-start=&quot;280&quot;&gt;4&lt;/td&gt;
&lt;td data-end=&quot;289&quot; data-start=&quot;284&quot; data-col-size=&quot;sm&quot;&gt;유성&lt;/td&gt;
&lt;td data-end=&quot;294&quot; data-start=&quot;289&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;298&quot; data-start=&quot;294&quot; data-col-size=&quot;sm&quot;&gt;남&lt;/td&gt;
&lt;td data-end=&quot;315&quot; data-start=&quot;298&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;314&quot; data-start=&quot;300&quot;&gt;test4@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;320&quot; data-start=&quot;315&quot; data-col-size=&quot;sm&quot;&gt;서울&lt;/td&gt;
&lt;td data-end=&quot;326&quot; data-start=&quot;320&quot; data-col-size=&quot;sm&quot;&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;373&quot; data-start=&quot;327&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;331&quot; data-start=&quot;327&quot;&gt;5&lt;/td&gt;
&lt;td data-end=&quot;336&quot; data-start=&quot;331&quot; data-col-size=&quot;sm&quot;&gt;용현&lt;/td&gt;
&lt;td data-end=&quot;341&quot; data-start=&quot;336&quot; data-col-size=&quot;sm&quot;&gt;FE&lt;/td&gt;
&lt;td data-end=&quot;345&quot; data-start=&quot;341&quot; data-col-size=&quot;sm&quot;&gt;여&lt;/td&gt;
&lt;td data-end=&quot;362&quot; data-start=&quot;345&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;361&quot; data-start=&quot;347&quot;&gt;test5@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;367&quot; data-start=&quot;362&quot; data-col-size=&quot;sm&quot;&gt;서울&lt;/td&gt;
&lt;td data-end=&quot;373&quot; data-start=&quot;367&quot; data-col-size=&quot;sm&quot;&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;421&quot; data-start=&quot;374&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;378&quot; data-start=&quot;374&quot;&gt;6&lt;/td&gt;
&lt;td data-end=&quot;383&quot; data-start=&quot;378&quot; data-col-size=&quot;sm&quot;&gt;민수&lt;/td&gt;
&lt;td data-end=&quot;388&quot; data-start=&quot;383&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;392&quot; data-start=&quot;388&quot; data-col-size=&quot;sm&quot;&gt;남&lt;/td&gt;
&lt;td data-end=&quot;409&quot; data-start=&quot;392&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;408&quot; data-start=&quot;394&quot;&gt;test6@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;415&quot; data-start=&quot;409&quot; data-col-size=&quot;sm&quot;&gt;남양주&lt;/td&gt;
&lt;td data-end=&quot;421&quot; data-start=&quot;415&quot; data-col-size=&quot;sm&quot;&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;468&quot; data-start=&quot;422&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;426&quot; data-start=&quot;422&quot;&gt;7&lt;/td&gt;
&lt;td data-end=&quot;431&quot; data-start=&quot;426&quot; data-col-size=&quot;sm&quot;&gt;관우&lt;/td&gt;
&lt;td data-end=&quot;436&quot; data-start=&quot;431&quot; data-col-size=&quot;sm&quot;&gt;FE&lt;/td&gt;
&lt;td data-end=&quot;440&quot; data-start=&quot;436&quot; data-col-size=&quot;sm&quot;&gt;남&lt;/td&gt;
&lt;td data-end=&quot;457&quot; data-start=&quot;440&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;456&quot; data-start=&quot;442&quot;&gt;test7@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;462&quot; data-start=&quot;457&quot; data-col-size=&quot;sm&quot;&gt;서울&lt;/td&gt;
&lt;td data-end=&quot;468&quot; data-start=&quot;462&quot; data-col-size=&quot;sm&quot;&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;518&quot; data-start=&quot;469&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;473&quot; data-start=&quot;469&quot;&gt;8&lt;/td&gt;
&lt;td data-end=&quot;479&quot; data-start=&quot;473&quot; data-col-size=&quot;sm&quot;&gt;경진&lt;/td&gt;
&lt;td data-end=&quot;484&quot; data-start=&quot;479&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;488&quot; data-start=&quot;484&quot; data-col-size=&quot;sm&quot;&gt;여&lt;/td&gt;
&lt;td data-end=&quot;507&quot; data-start=&quot;488&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;506&quot; data-start=&quot;490&quot;&gt;test8@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;512&quot; data-start=&quot;507&quot; data-col-size=&quot;sm&quot;&gt;서울&lt;/td&gt;
&lt;td data-end=&quot;518&quot; data-start=&quot;512&quot; data-col-size=&quot;sm&quot;&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;565&quot; data-start=&quot;519&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;523&quot; data-start=&quot;519&quot;&gt;9&lt;/td&gt;
&lt;td data-end=&quot;528&quot; data-start=&quot;523&quot; data-col-size=&quot;sm&quot;&gt;지현&lt;/td&gt;
&lt;td data-end=&quot;533&quot; data-start=&quot;528&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;537&quot; data-start=&quot;533&quot; data-col-size=&quot;sm&quot;&gt;남&lt;/td&gt;
&lt;td data-end=&quot;554&quot; data-start=&quot;537&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;553&quot; data-start=&quot;539&quot;&gt;test9@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;559&quot; data-start=&quot;554&quot; data-col-size=&quot;sm&quot;&gt;서울&lt;/td&gt;
&lt;td data-end=&quot;565&quot; data-start=&quot;559&quot; data-col-size=&quot;sm&quot;&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;614&quot; data-start=&quot;566&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;571&quot; data-start=&quot;566&quot;&gt;10&lt;/td&gt;
&lt;td data-end=&quot;577&quot; data-start=&quot;571&quot; data-col-size=&quot;sm&quot;&gt;건호&lt;/td&gt;
&lt;td data-end=&quot;582&quot; data-start=&quot;577&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;586&quot; data-start=&quot;582&quot; data-col-size=&quot;sm&quot;&gt;남&lt;/td&gt;
&lt;td data-end=&quot;603&quot; data-start=&quot;586&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;602&quot; data-start=&quot;588&quot;&gt;test10@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;608&quot; data-start=&quot;603&quot; data-col-size=&quot;sm&quot;&gt;서울&lt;/td&gt;
&lt;td data-end=&quot;614&quot; data-start=&quot;608&quot; data-col-size=&quot;sm&quot;&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;662&quot; data-start=&quot;615&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;620&quot; data-start=&quot;615&quot;&gt;11&lt;/td&gt;
&lt;td data-end=&quot;625&quot; data-start=&quot;620&quot; data-col-size=&quot;sm&quot;&gt;재민&lt;/td&gt;
&lt;td data-end=&quot;630&quot; data-start=&quot;625&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;634&quot; data-start=&quot;630&quot; data-col-size=&quot;sm&quot;&gt;여&lt;/td&gt;
&lt;td data-end=&quot;651&quot; data-start=&quot;634&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;650&quot; data-start=&quot;636&quot;&gt;test11@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;656&quot; data-start=&quot;651&quot; data-col-size=&quot;sm&quot;&gt;인천&lt;/td&gt;
&lt;td data-end=&quot;662&quot; data-start=&quot;656&quot; data-col-size=&quot;sm&quot;&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;709&quot; data-start=&quot;663&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;668&quot; data-start=&quot;663&quot;&gt;12&lt;/td&gt;
&lt;td data-end=&quot;673&quot; data-start=&quot;668&quot; data-col-size=&quot;sm&quot;&gt;원녕&lt;/td&gt;
&lt;td data-end=&quot;678&quot; data-start=&quot;673&quot; data-col-size=&quot;sm&quot;&gt;BE&lt;/td&gt;
&lt;td data-end=&quot;682&quot; data-start=&quot;678&quot; data-col-size=&quot;sm&quot;&gt;여&lt;/td&gt;
&lt;td data-end=&quot;698&quot; data-start=&quot;682&quot; data-col-size=&quot;sm&quot;&gt;&lt;a data-end=&quot;697&quot; data-start=&quot;684&quot;&gt;test12@gmail.com&lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td data-end=&quot;703&quot; data-start=&quot;698&quot; data-col-size=&quot;sm&quot;&gt;서울&lt;/td&gt;
&lt;td data-end=&quot;709&quot; data-start=&quot;703&quot; data-col-size=&quot;sm&quot;&gt;26&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-end=&quot;159&quot; data-start=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;159&quot; data-start=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;b&gt;ID&lt;/b&gt;, &lt;b&gt;이메일&lt;/b&gt;, &lt;b&gt;주민등록번호&lt;/b&gt;와 같은 컬럼이 대표적인 &lt;b&gt;카디널리티가 높은 컬럼&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;159&quot; data-start=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이러한 컬럼들은 값의 중복이 거의 없기 때문에 &lt;b&gt;인덱스를 적용하기에 가장 적합하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;374&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;374&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;또한, 인덱스에는 &lt;b&gt;복합 인덱스(Composite Index)&lt;/b&gt; 라는 개념도 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;374&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;374&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;복합 인덱스는 &lt;b&gt;여러 컬럼을 결합하여 하나의 인덱스로 구성한 형태&lt;/b&gt;로, &lt;b&gt;왼쪽(앞쪽) 컬럼부터 순서대로 조건에 사용할 때만 효율적으로 동작&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;374&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;374&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;즉, 인덱스의 &lt;b&gt;선두 컬럼(Leading Column)&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_1760538240610&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_user ON user (age, name, city);&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;age &amp;rarr; name &amp;rarr; city&lt;/b&gt; 순으로 정렬되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1760538232937&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE age = 25
WHERE age = 25 AND name = '승호'
WHERE age = 25 AND name = '승호' AND city = '서울'&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;이렇게는 where절이 가능하지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760538225137&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE name = '승호'           -- ❌ age가 빠져서 불가능
WHERE city = '서울'           -- ❌ age, name 다 빠짐
WHERE name = '승호' AND city = '서울'  -- ❌ age가 없으므로 불가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이는 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 인덱스는 항상 &lt;b&gt;맨 왼쪽 컬럼부터&lt;/b&gt; 차례로 사용될 때만 의미가 있다.&lt;/p&gt;
&lt;div&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;399&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;472&quot; data-start=&quot;399&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;복합 인덱스&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-end=&quot;472&quot; data-start=&quot;399&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1. 선택도(카디널리티, Cardinality)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;399&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;399&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 검색 범위를 좁히는 구조이기 때문에, 먼저 필터링되는 컬럼의 값이 다양할수록 인덱스 탐색 범위를 빠르게 줄일 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;399&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;399&quot; data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;값이 다양할수록(중복이 적을수록) 앞에 둔다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;399&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-end=&quot;629&quot; data-start=&quot;483&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody data-end=&quot;629&quot; data-start=&quot;543&quot;&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;td style=&quot;height: 21px;&quot;&gt;개수 비교&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;577&quot; data-start=&quot;543&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;549&quot; data-start=&quot;543&quot;&gt;age&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;567&quot; data-start=&quot;549&quot; data-col-size=&quot;sm&quot;&gt;100개 (20~60살 등)&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;577&quot; data-start=&quot;567&quot; data-col-size=&quot;sm&quot;&gt;다양성 중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;605&quot; data-start=&quot;578&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;585&quot; data-start=&quot;578&quot;&gt;name&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;595&quot; data-start=&quot;585&quot; data-col-size=&quot;sm&quot;&gt;50,000개&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;605&quot; data-start=&quot;595&quot; data-col-size=&quot;sm&quot;&gt;다양성 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;629&quot; data-start=&quot;606&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;613&quot; data-start=&quot;606&quot;&gt;city&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;619&quot; data-start=&quot;613&quot; data-col-size=&quot;sm&quot;&gt;10개&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;629&quot; data-start=&quot;619&quot; data-col-size=&quot;sm&quot;&gt;다양성 낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;691&quot; data-start=&quot;631&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 경우 name을 앞에 두는 것이 효율적. &lt;/b&gt;  (name, age, city)&lt;/p&gt;
&lt;p data-end=&quot;777&quot; data-start=&quot;693&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;777&quot; data-start=&quot;693&quot; data-ke-size=&quot;size16&quot;&gt;하지만, &lt;b&gt;대부분의 쿼리가 age 기준으로 시작한다면&lt;/b&gt;&amp;nbsp; &amp;rarr; (age, name, city)도 합리적 선택이 될 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;922&quot; data-start=&quot;844&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;922&quot; data-start=&quot;844&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;922&quot; data-start=&quot;844&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. WHERE 절 사용 빈도&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;922&quot; data-start=&quot;844&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 &lt;b&gt;WHERE 절의 조건 컬럼&lt;/b&gt;을 기준으로 탐색한다.&lt;/p&gt;
&lt;p data-end=&quot;922&quot; data-start=&quot;844&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;따라서 가장 자주 필터링되는 컬럼을 인덱스의 앞부분에 두는 게 핵심이다.&lt;/p&gt;
&lt;p data-end=&quot;922&quot; data-start=&quot;844&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;922&quot; data-start=&quot;844&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 WHERE 조건에 자주 등장하는 컬럼을 앞에 둔다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;930&quot; data-start=&quot;924&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760538207431&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM user WHERE age = 25 AND name = '승호';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1135&quot; data-start=&quot;994&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1068&quot; data-start=&quot;994&quot;&gt;대부분의 쿼리가 나이(age)를 조건으로 시작한다면&lt;br /&gt;-&amp;gt; (age, name, city) 순이 가장 효율적이다.&lt;/li&gt;
&lt;li data-end=&quot;1135&quot; data-start=&quot;1069&quot;&gt;반대로 이름으로 검색하는 경우가 훨씬 많다면&lt;br /&gt;-&amp;gt; &amp;nbsp;(name, age, city) 로 변경하는 게 낫다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1303&quot; data-start=&quot;1224&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1303&quot; data-start=&quot;1224&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 조건 유형 (=, &amp;lt;, &amp;gt;, LIKE 등)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1303&quot; data-start=&quot;1224&quot; data-ke-size=&quot;size16&quot;&gt;등호(=) 조건은 인덱스를 끝까지 사용할 수 있지만,&lt;/p&gt;
&lt;p data-end=&quot;1303&quot; data-start=&quot;1224&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1303&quot; data-start=&quot;1224&quot; data-ke-size=&quot;size16&quot;&gt;범위 조건(&amp;lt;, &amp;gt;, BETWEEN, LIKE)은 &lt;b&gt;그 지점에서 탐색이 끊긴다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1311&quot; data-start=&quot;1305&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1311&quot; data-start=&quot;1305&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉 &lt;b&gt;등호(=) 조건 컬럼은 앞에, 범위(&amp;lt;, &amp;gt;, LIKE)는 뒤쪽에 둔다.&lt;/b&gt; &lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760538184369&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 등호 조건만 있을 때
SELECT * FROM user
WHERE age = 25 AND name = '승호' AND city = '서울';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1440&quot; data-start=&quot;1405&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1440&quot; data-start=&quot;1405&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 인덱스 (age, name, city) 전부 사용&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760538195131&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- LIKE(범위 조건) 포함
SELECT * FROM user
WHERE age = 25 AND name LIKE '김%' AND city = '서울';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1602&quot; data-start=&quot;1541&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1602&quot; data-start=&quot;1541&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; age까지만 완전 탐색 가능&lt;/p&gt;
&lt;p data-end=&quot;1602&quot; data-start=&quot;1541&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1602&quot; data-start=&quot;1541&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; name에서 탐색이 끊기므로 city는 인덱스 미사용 ❌&lt;/p&gt;
&lt;p data-end=&quot;1656&quot; data-start=&quot;1604&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1656&quot; data-start=&quot;1604&quot; data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;등호 조건 컬럼을 앞에&lt;/b&gt;, &lt;b&gt;범위 조건 컬럼을 뒤로&lt;/b&gt; 두는 게 원칙이다.&lt;/p&gt;
&lt;p data-end=&quot;1656&quot; data-start=&quot;1604&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1656&quot; data-start=&quot;1604&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1835&quot; data-start=&quot;1730&quot; data-ke-size=&quot;size20&quot;&gt;4. &lt;b&gt;ORDER BY / GROUP BY 고려&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1835&quot; data-start=&quot;1730&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1835&quot; data-start=&quot;1730&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 이미 정렬된 구조이기 때문에, WHERE 조건 이후의 ORDER BY 컬럼이 인덱스 순서와 일치하면&lt;br /&gt;DB가 &lt;b&gt;추가 정렬(filesort)&lt;/b&gt; 을 하지 않아도 된다.&lt;/p&gt;
&lt;p data-end=&quot;1835&quot; data-start=&quot;1730&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1835&quot; data-start=&quot;1730&quot; data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;정렬이나 그룹화에 사용되는 컬럼은 뒤쪽에 둔다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760538140261&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM user
WHERE age = 25
ORDER BY name, city;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1992&quot; data-start=&quot;1910&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 인덱스 (age, name, city) 와 정렬 순서가 동일하므로&lt;br /&gt;&amp;rarr; &lt;b&gt;WHERE + ORDER BY를 모두 인덱스로 처리 가능&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1760538165543&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM user
WHERE age = 25
ORDER BY city, name;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2095&quot; data-start=&quot;2060&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 인덱스 순서와 다르기 때문에 &lt;b&gt;filesort 발생&lt;/b&gt; ❌&lt;/p&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;b&gt;문제&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 아래 문제를 풀어보자&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760598032163&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Book 테이블에
category 칼럼
author 칼럼(작가/식별성 높음/ 동명이인 가능)가 있다.

Book 테이블은 500만개
카테고리는 총 20개로 가정합니다

select * from book where category = ? and author = ?


인덱스
A (category)
B (author)
C (category, author)
D (author, category)

이 쿼리에 대한 인덱스 선택의 문제이다.&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;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;1. 해당 쿼리 기준으로 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;C(category, author)와 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;D(author, category)의 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;인덱스 동작의 차이를 설명하세요.&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;C는 category로 먼저 좁히고 author로 필터링한다.&lt;br /&gt;D는 author로 먼저 좁히기 때문에 선택도가 높아 더 효율적이다.&lt;br /&gt;두 쿼리 모두 인덱스 시크 가능하지만, 보통 D가 더 빠르&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;2. 해당 쿼리 기준 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;가장 느린 인덱스를 선택하세요.&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;A(category). 카디널리티가 낮아 한 category당 수십만 건을 스캔해야 한다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;3. 해당 쿼리 기준 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;B (author)와 D (author, category) 인덱스 동작 차이를 설명하세요.&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;B는 author까지만 인덱스로 찾고 category는 테이블에서 비교한다.&lt;br /&gt;D는 두 조건을 모두 인덱스로 처리해 범위가 훨씬 좁다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;4. &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;select * from book&amp;nbsp;&amp;nbsp;category = ? and author like '%단어%' 로 바뀐다면 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;가장 빠른 인덱스를 선택하세요.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;정답:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C(category, author).&lt;br /&gt;category로 먼저 좁히고 LIKE 검색을 그 범위 내에서 수행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;5. select * from book author like '%단어%' 로 바뀐다면 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;가장 빠른 인덱스를 선택하세요.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;정답:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B(author).&lt;br /&gt;LIKE 앞에 %가 있어 전체 스캔이지만, 단일 인덱스라 D보다 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;6. select * from book where author = ? 쿼리에서 &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #222222; text-align: left;&quot;&gt;C (category, author) 인덱스 동작 방식을 설명하세요.&amp;nbsp;&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;선두 컬럼(category)이 빠져 탐색이 불가능하다.&lt;br /&gt;결국 전체 인덱스를 스캔해야 한다.&lt;/p&gt;</description>
      <category>CS</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/338</guid>
      <comments>https://balhae.tistory.com/338#entry338comment</comments>
      <pubDate>Thu, 16 Oct 2025 17:24:51 +0900</pubDate>
    </item>
    <item>
      <title>[맞춤형 헤어 컨설팅 예약] 서킷 브레이커 패턴 연구하기</title>
      <link>https://balhae.tistory.com/337</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/252&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/252&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760019369517&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;2025 블레이버스 MVP 개발 해커톤 대회 테크 인사이트상 수상 후기&quot; data-og-description=&quot;어느덧 2025년&amp;hellip;!!본격적인 취업 준비에 들어가기 전에, 마지막으로 공모전에 참가하고 싶었다.그때 동아리 친구가 블레이버스 개발 해커톤에 나가자고 제안했다.덕분에 동아리 팀원들과 함께 &quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/252&quot; data-og-url=&quot;https://balhae.tistory.com/252&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/blpnXv/hyZKHFVePr/3NAyfcwr2czap7vHlEUjyK/img.jpg?width=318&amp;amp;height=159&amp;amp;face=0_0_318_159,https://scrap.kakaocdn.net/dn/fACIa/hyZLkb3KQk/wwSmxEb0zv715mlk9xmimK/img.jpg?width=318&amp;amp;height=159&amp;amp;face=0_0_318_159,https://scrap.kakaocdn.net/dn/jqJK4/hyZKa3dmbq/cszRQHGewbQwvf2BI0jrrK/img.png?width=4032&amp;amp;height=3024&amp;amp;face=139_175_731_608&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/252&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/252&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/blpnXv/hyZKHFVePr/3NAyfcwr2czap7vHlEUjyK/img.jpg?width=318&amp;amp;height=159&amp;amp;face=0_0_318_159,https://scrap.kakaocdn.net/dn/fACIa/hyZLkb3KQk/wwSmxEb0zv715mlk9xmimK/img.jpg?width=318&amp;amp;height=159&amp;amp;face=0_0_318_159,https://scrap.kakaocdn.net/dn/jqJK4/hyZKa3dmbq/cszRQHGewbQwvf2BI0jrrK/img.png?width=4032&amp;amp;height=3024&amp;amp;face=139_175_731_608');&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;2025 블레이버스 MVP 개발 해커톤 대회 테크 인사이트상 수상 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;어느덧 2025년&amp;hellip;!!본격적인 취업 준비에 들어가기 전에, 마지막으로 공모전에 참가하고 싶었다.그때 동아리 친구가 블레이버스 개발 해커톤에 나가자고 제안했다.덕분에 동아리 팀원들과 함께&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.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;290&quot; data-start=&quot;196&quot; data-ke-size=&quot;size16&quot;&gt;이번 연휴에는 평소에 미뤄뒀던 주제들을 하나씩 정리해보려 한다.&lt;br /&gt;그중 두 번째로 &lt;b&gt;카카오페이 API 장애 발생시 서킷브레이커 패턴&lt;/b&gt;&amp;nbsp;에 대해 깊이 다뤄볼 예정이다.&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개념&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;200&quot; data-start=&quot;85&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서킷브레이커(Circuit Breaker)&lt;/b&gt; 는 마이크로서비스나 외부 API 통신에서&lt;br /&gt;&lt;b&gt;연속적인 실패를 감지하고, 일정 기준 이상 실패가 누적되면 호출을 자동으로 차단&lt;/b&gt;하는 안정화 패턴이다.&lt;/p&gt;
&lt;p data-end=&quot;305&quot; data-start=&quot;202&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;305&quot; data-start=&quot;202&quot; data-ke-size=&quot;size16&quot;&gt;즉, 회로 차단기처럼 시스템이 과열되기 전에 전류를 끊듯이,&lt;br /&gt;API 요청이 계속 실패할 경우 &lt;b&gt;회로(요청 경로)를 &amp;lsquo;OPEN&amp;rsquo; 상태로 전환해 더 이상 요청을 보내지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;389&quot; data-start=&quot;307&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;389&quot; data-start=&quot;307&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 장애가 발생한 외부 서비스로의 &lt;b&gt;불필요한 요청과 리소스 낭비를 막고&lt;/b&gt;,&lt;/p&gt;
&lt;p data-end=&quot;389&quot; data-start=&quot;307&quot; data-ke-size=&quot;size16&quot;&gt;시스템 전체로 장애가 확산되는 것을 방지할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;477&quot; data-start=&quot;391&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;477&quot; data-start=&quot;391&quot; data-ke-size=&quot;size16&quot;&gt;회로는 일정 시간 후 자동으로 &lt;b&gt;HALF_OPEN &amp;rarr; CLOSED&lt;/b&gt; 상태로 복귀하면서,&lt;br /&gt;외부 서비스가 복구되었는지를 소량의 요청으로 테스트한다.&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개발 프로세스&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;308&quot; data-start=&quot;184&quot; data-ke-size=&quot;size16&quot;&gt;맞춤형 헤어 컨설팅 예약 시스템은 KakaoPay API에 장애가 발생할 경우, &lt;b&gt;일정 실패율 임계치&lt;/b&gt;에 도달하면 회로(Circuit Breaker)가 &lt;b&gt;자동으로 차단(Open)&lt;/b&gt; 되어 추가적인 장애 전파를 방지한다.&lt;/p&gt;
&lt;p data-end=&quot;403&quot; data-start=&quot;313&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;403&quot; data-start=&quot;313&quot; data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;Fail-Fast 전략&lt;/b&gt;을 적용하여, 회로가 열린(Open) 상태에서는 &lt;b&gt;즉시 실패 응답을 반환&lt;/b&gt;함으로써 사용자 대기 시간을 최소화한다.&lt;/p&gt;
&lt;p data-end=&quot;483&quot; data-start=&quot;408&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;483&quot; data-start=&quot;408&quot; data-ke-size=&quot;size16&quot;&gt;한편, &lt;b&gt;Retry 메커니즘&lt;/b&gt;을 통해 일시적인 네트워크 장애나 타임아웃이 발생했을 경우 최대 3회까지 재시도를 수행한다.&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;488&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;488&quot; data-ke-size=&quot;size16&quot;&gt;회로는 자동으로 &lt;b&gt;OPEN &amp;rarr; HALF_OPEN &amp;rarr; CLOSED&lt;/b&gt; 상태 전환을 거쳐 정상 복구되며,&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;488&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;488&quot; data-ke-size=&quot;size16&quot;&gt;만약 &lt;b&gt;CallNotPermittedException&lt;/b&gt;이 발생할 경우에는 보상 트랜잭션을 수행하여 시스템 안정성을 보장한다.&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;적용&lt;/b&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;resilience4j:
  circuitbreaker:
    instances:
      kakaoPayCB:
        registerHealthIndicator: true
        slidingWindowSize: 10                  #  최근 10회의 요청을 기준으로 상태를 판단
        minimumNumberOfCalls: 5                #  최소 5회 이상 호출되어야 통계 유효
        failureRateThreshold: 30               #  실패율 임계치 (30%) &amp;rarr; 초과 시 회로 OPEN
        waitDurationInOpenState: 10s           #  회로 OPEN 상태 유지 시간 (10초 후 HALF_OPEN으로 전환)
        permittedNumberOfCallsInHalfOpenState: 3 #  HALF_OPEN 시 테스트 호출 3회 허용
        automaticTransitionFromOpenToHalfOpenEnabled: true #  자동으로 HALF_OPEN &amp;rarr; CLOSED 상태 전환
        # Fail-Fast 전략: 회로가 OPEN 상태일 때는 CallNotPermittedException 즉시 발생 &amp;rarr; 대기 없이 실패 반환

  retry:
    instances:
      kakaoPayRetry:
        max-attempts: 3                        #  최대 재시도 횟수 (3회)
        wait-duration: 1s                      #  재시도 간 대기 시간 (1초)
        # 일시적 장애 시 자동 재시도 수행 (Timeout, Connection Error 등)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 application.yml에 설정을 추가했다.&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실패율 임계치를 30%로 설정한 이유는&lt;/b&gt;, 결제 요청은 네트워크나 외부 API 상태에 따라 일시적으로 실패할 수 있지만,&lt;br /&gt;3건 중 1건 이상 실패가 지속된다면 이는 단순한 일시 장애가 아니라 &lt;b&gt;시스템적 문제&lt;/b&gt;로 판단하기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;즉, 30% 이상에서 회로를 차단함으로써 &lt;b&gt;불필요한 외부 호출을 줄이고 서버 자원을 보호&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;427&quot; data-start=&quot;270&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;427&quot; data-start=&quot;270&quot; data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;재시도 횟수를 3회로 설정한 이유는&lt;/b&gt;,&lt;br /&gt;일시적인 네트워크 지연이나 타임아웃은 대부분 &lt;b&gt;1~2초 내 재요청 시 복구&lt;/b&gt;되기 때문이다.&lt;br /&gt;하지만 과도한 재시도는 오히려 API 서버 부하를 가중시킬 수 있어, &lt;b&gt;안정성과 효율성의 균형을 고려해 3회로 제한&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class ResilienceConfig {

    private final CircuitBreakerRegistry circuitBreakerRegistry;
    private final RetryRegistry retryRegistry;

    @Bean
    public CircuitBreaker kakaoPayCircuitBreaker() {
        return circuitBreakerRegistry.circuitBreaker(&quot;kakaoPayCB&quot;);
    }

    @Bean
    public Retry kakaoPayRetry() {
        return retryRegistry.retry(&quot;kakaoPayRetry&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;Resilience4j의 CircuitBreaker와 Retry를 &lt;b&gt;수동 빈(@Bean)&lt;/b&gt; 으로 등록한 이유는,&lt;br /&gt;단순히 설정 기반의 AOP 방식이 아니라 &lt;b&gt;비즈니스 로직 내부에서 직접 제어하기 위해서&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;결제 요청 로직은 CircuitBreaker와 Retry를 &lt;b&gt;조합해 감싸는 구조&lt;/b&gt;로,&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 자동 설정(AOP) 방식은 호출 흐름을 직접 제어할 수 없어, &lt;/b&gt;&lt;br /&gt;Retry와 CircuitBreaker를 조합하거나 조건별로 분리 적용하는 세밀한 제어가 불가능하다.&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;따라서 CircuitBreakerRegistry와 RetryRegistry를 주입받아&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;명시적으로 인스턴스를 생성하고 로직 단위에서 제어할 수 있도록 구성했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/**
 * 0️⃣ 결제 준비 전 로직 - 주문 생성 및 KakaoPay Ready 요청
 */
@Override
public KakaoPayReadyResponse payLogic(String userId, ReadyRequestDTO reqDto) {
    String orderId = UUID.randomUUID().toString();

    //   서킷브레이커 + 리트라이 데코레이션
    Supplier&amp;lt;KakaoPayReadyResponse&amp;gt; decoratedSupplier =
            CircuitBreaker.decorateSupplier(kakaoPayCircuitBreaker,
                    Retry.decorateSupplier(kakaoPayRetry,
                            () -&amp;gt; requestPayReady(orderId, userId, reqDto.amount())
                    ));

    try {
        .... 실행

    } catch (CallNotPermittedException e) {
        .... 보상 트랜잭션 실행
    } catch (Exception e) {
        .... 에러 실행
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-end=&quot;547&quot; data-start=&quot;412&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;547&quot; data-start=&quot;412&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 서킷브레이커 + 재시도 적용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;547&quot; data-start=&quot;412&quot; data-ke-size=&quot;size16&quot;&gt;위 코드는 결제 요청의 핵심은 requestPayReady() 메서드에서 &lt;b&gt;카카오페이 결제 API를 호출&lt;/b&gt;하는 부분이다.&lt;br /&gt;이 호출 구간을 단순히 실행하는 대신, Retry와 CircuitBreaker로 감싸서 안정성을 보강한다.&lt;/p&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;549&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;549&quot; data-ke-size=&quot;size16&quot;&gt;Retry는 일시적인 네트워크 문제나 타임아웃이 발생했을 때 &lt;b&gt;최대 3회까지 자동으로 재시도&lt;/b&gt;하도록 설정되어 있다.&lt;/p&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;549&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;549&quot; data-ke-size=&quot;size16&quot;&gt;반면, CircuitBreaker는 호출 실패율이 &lt;b&gt;30% 이상&lt;/b&gt;으로 올라가면 &lt;b&gt;회로를 자동으로 &amp;ldquo;OPEN&amp;rdquo; 상태로 전환&lt;/b&gt;하여&lt;br /&gt;더 이상 외부 API를 호출하지 않는다.&lt;/p&gt;
&lt;p data-end=&quot;727&quot; data-start=&quot;549&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;727&quot; data-start=&quot;549&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Fail-Fast 전략&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;999&quot; data-start=&quot;843&quot; data-ke-size=&quot;size16&quot;&gt;회로가 &lt;b&gt;OPEN 상태&lt;/b&gt;가 되면, Resilience4j는 외부 API를 호출하지 않고 즉시 CallNotPermittedException을 발생시킨다.&lt;/p&gt;
&lt;p data-end=&quot;999&quot; data-start=&quot;843&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;999&quot; data-start=&quot;843&quot; data-ke-size=&quot;size16&quot;&gt;이는 &amp;ldquo;Fail-Fast 전략&amp;rdquo;으로, 불필요한 대기 없이 사용자에게 바로 실패 응답을 반환하게 한다.&lt;/p&gt;
&lt;p data-end=&quot;1095&quot; data-start=&quot;1001&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1095&quot; data-start=&quot;1001&quot; data-ke-size=&quot;size16&quot;&gt;덕분에 카카오페이 쪽 서버가 장애를 일으키더라도 우리 서버의 스레드가 대기 상태로 묶이지 않고,&lt;br /&gt;&lt;b&gt;빠르게 실패를 감지하고 안정적으로 복구를 준비할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1095&quot; data-start=&quot;1001&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1095&quot; data-start=&quot;1001&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;예외 및 복구 처리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1408&quot; data-start=&quot;1291&quot; data-ke-size=&quot;size16&quot;&gt;만약 회로가 열린 상태에서 요청이 들어오면 CallNotPermittedException이 발생하고,&lt;br /&gt;우리는 이 예외를 잡아서 &lt;b&gt;보상 트랜잭션&lt;/b&gt;(예: 결제 취소, 재시도 안내 등)을 수행한다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;분석&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;K6 부하 테스트를 통해 서킷브레이커 적용 전후의 성능 변화를 분석하고, 이를 통해 시스템 효율성을 검증하고자 한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import http from 'k6/http';
import { check, sleep } from 'k6';

// 부하 설정
export const options = {
    vus: 50,                 // 동시 사용자 50명
    duration: '30s',         // 30초간 지속
    thresholds: {
        http_req_failed: ['rate&amp;lt;0.1'],          // 실패율 10% 이하 목표
        http_req_duration: ['p(95)&amp;lt;2000'],      // 95% 요청이 2초 이하
    },
};

// 테스트 대상 API
const BASE_URL = 'http://localhost:8080';
const PAY_URL = `${BASE_URL}/api/pay/ready`;

// 각 VU(가상 유저)에 고유 userId 부여 (2~51)
export default function () {
    const userId = `user${__VU + 1}`;  // __VU: k6 내장 전역변수 (1부터 시작)
    const payload = JSON.stringify({
        amount: 1000,
        userId: userId,
    });

    const params = {
        headers: { 'Content-Type': 'application/json' },
    };

    // POST 요청 실행
    const res = http.post(PAY_URL, payload, params);

    // 결과 검증
    check(res, {
        'status is 200': (r) =&amp;gt; r.status === 200,
    });

    // 각 요청 사이에 약간의 대기 (0.5초)
    sleep(0.5);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; 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;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;531&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf06Yq/btsQ6GP8G88/C766KHkBdc4THnSC9UvBj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf06Yq/btsQ6GP8G88/C766KHkBdc4THnSC9UvBj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf06Yq/btsQ6GP8G88/C766KHkBdc4THnSC9UvBj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf06Yq%2FbtsQ6GP8G88%2FC766KHkBdc4THnSC9UvBj0%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;845&quot; height=&quot;531&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;531&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;727&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djtmZo/btsQ4YRX098/HLKhPy4LTlR8rxbkGcvIS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djtmZo/btsQ4YRX098/HLKhPy4LTlR8rxbkGcvIS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djtmZo/btsQ4YRX098/HLKhPy4LTlR8rxbkGcvIS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjtmZo%2FbtsQ4YRX098%2FHLKhPy4LTlR8rxbkGcvIS1%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;861&quot; height=&quot;727&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;727&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;결과는 위와 같다. 왼쪽이 적용 전, 오른쪽이 적용 후 사진이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;904&quot; data-start=&quot;223&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;CircuitBreaker 미적용&lt;/td&gt;
&lt;td&gt;CircuitBreaker 적용&lt;/td&gt;
&lt;td&gt;변화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;412&quot; data-start=&quot;340&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;368&quot; data-start=&quot;340&quot;&gt;&lt;b&gt;실패율 (http_req_failed)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;378&quot; data-start=&quot;368&quot; data-col-size=&quot;sm&quot;&gt;100.00%&lt;/td&gt;
&lt;td data-end=&quot;388&quot; data-start=&quot;378&quot; data-col-size=&quot;sm&quot;&gt;100.00%&lt;/td&gt;
&lt;td data-end=&quot;412&quot; data-start=&quot;388&quot; data-col-size=&quot;sm&quot;&gt;동일 (Fail-Fast 정상 동작)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;485&quot; data-start=&quot;413&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;433&quot; data-start=&quot;413&quot;&gt;&lt;b&gt;평균 응답시간 (avg)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;449&quot; data-start=&quot;433&quot; data-col-size=&quot;sm&quot;&gt;&lt;b&gt;634.58 ms&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;464&quot; data-start=&quot;449&quot; data-col-size=&quot;sm&quot;&gt;106.15 ms&lt;/td&gt;
&lt;td data-end=&quot;485&quot; data-start=&quot;464&quot; data-col-size=&quot;sm&quot;&gt;  &lt;b&gt;&amp;minus;83.3 % 감소&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;545&quot; data-start=&quot;486&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;502&quot; data-start=&quot;486&quot;&gt;&lt;b&gt;중앙값 (med)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;514&quot; data-start=&quot;502&quot; data-col-size=&quot;sm&quot;&gt;617.33 ms&lt;/td&gt;
&lt;td data-end=&quot;524&quot; data-start=&quot;514&quot; data-col-size=&quot;sm&quot;&gt;2.75 ms&lt;/td&gt;
&lt;td data-end=&quot;545&quot; data-start=&quot;524&quot; data-col-size=&quot;sm&quot;&gt;  &lt;b&gt;&amp;minus;99.6 % 감소&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;617&quot; data-start=&quot;546&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;565&quot; data-start=&quot;546&quot;&gt;&lt;b&gt;95퍼센타일 (p95)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;581&quot; data-start=&quot;565&quot; data-col-size=&quot;sm&quot;&gt;&lt;b&gt;680.75 ms&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;596&quot; data-start=&quot;581&quot; data-col-size=&quot;sm&quot;&gt;&lt;b&gt;19.95 ms&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;617&quot; data-start=&quot;596&quot; data-col-size=&quot;sm&quot;&gt;  &lt;b&gt;&amp;minus;97.1 % 감소&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;686&quot; data-start=&quot;618&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;638&quot; data-start=&quot;618&quot;&gt;&lt;b&gt;최대 응답시간 (max)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;647&quot; data-start=&quot;638&quot; data-col-size=&quot;sm&quot;&gt;1.66 s&lt;/td&gt;
&lt;td data-end=&quot;656&quot; data-start=&quot;647&quot; data-col-size=&quot;sm&quot;&gt;9.72 s&lt;/td&gt;
&lt;td data-end=&quot;686&quot; data-start=&quot;656&quot; data-col-size=&quot;sm&quot;&gt;일시적 지연 존재 (Retry 재시도 영향)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;756&quot; data-start=&quot;687&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;707&quot; data-start=&quot;687&quot;&gt;&lt;b&gt;초당 요청 수 (RPS)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;721&quot; data-start=&quot;707&quot; data-col-size=&quot;sm&quot;&gt;42.73 req/s&lt;/td&gt;
&lt;td data-end=&quot;735&quot; data-start=&quot;721&quot; data-col-size=&quot;sm&quot;&gt;74.96 req/s&lt;/td&gt;
&lt;td data-end=&quot;756&quot; data-start=&quot;735&quot; data-col-size=&quot;sm&quot;&gt;  &lt;b&gt;+75.4 % 향상&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;836&quot; data-start=&quot;757&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;783&quot; data-start=&quot;757&quot;&gt;&lt;b&gt;데이터 송신량 (data_sent)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;803&quot; data-start=&quot;783&quot; data-col-size=&quot;sm&quot;&gt;228 kB (7.2 kB/s)&lt;/td&gt;
&lt;td data-end=&quot;822&quot; data-start=&quot;803&quot; data-col-size=&quot;sm&quot;&gt;422 kB (13 kB/s)&lt;/td&gt;
&lt;td data-end=&quot;836&quot; data-start=&quot;822&quot; data-col-size=&quot;sm&quot;&gt;시스템 처리량 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;904&quot; data-start=&quot;837&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;865&quot; data-start=&quot;837&quot;&gt;&lt;b&gt;평균 iteration_duration&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;874&quot; data-start=&quot;865&quot; data-col-size=&quot;sm&quot;&gt;1.13 s&lt;/td&gt;
&lt;td data-end=&quot;883&quot; data-start=&quot;874&quot; data-col-size=&quot;sm&quot;&gt;0.607 s&lt;/td&gt;
&lt;td data-end=&quot;904&quot; data-start=&quot;883&quot; data-col-size=&quot;sm&quot;&gt;  &lt;b&gt;&amp;minus;46.9 % 단축&lt;/b&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;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;7e5733fb-0c27-4383-9893-c8f39b1dbd2a&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;148&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;표로 정리하면 다음과 같다. Fast-Fail 전략의 영향으로 요청이 지연되지 않고 즉시 실패 응답을 반환하여, 도입 전보다 시스템 효율이 향상되었다. 다만 서킷브레이커는 임계값 설정이 적절하지 않을 경우 오히려 역효과를 초래할 수 있으므로 신중한 설계가 필요하다.&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;</description>
      <category>개발 팁</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/337</guid>
      <comments>https://balhae.tistory.com/337#entry337comment</comments>
      <pubDate>Fri, 10 Oct 2025 21:39:39 +0900</pubDate>
    </item>
    <item>
      <title>[맞춤형 헤어 컨설팅 예약] 동시성 충돌 연구해보기</title>
      <link>https://balhae.tistory.com/336</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/252&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/252&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760019369517&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;2025 블레이버스 MVP 개발 해커톤 대회 테크 인사이트상 수상 후기&quot; data-og-description=&quot;어느덧 2025년&amp;hellip;!!본격적인 취업 준비에 들어가기 전에, 마지막으로 공모전에 참가하고 싶었다.그때 동아리 친구가 블레이버스 개발 해커톤에 나가자고 제안했다.덕분에 동아리 팀원들과 함께 &quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/252&quot; data-og-url=&quot;https://balhae.tistory.com/252&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/blpnXv/hyZKHFVePr/3NAyfcwr2czap7vHlEUjyK/img.jpg?width=318&amp;amp;height=159&amp;amp;face=0_0_318_159,https://scrap.kakaocdn.net/dn/fACIa/hyZLkb3KQk/wwSmxEb0zv715mlk9xmimK/img.jpg?width=318&amp;amp;height=159&amp;amp;face=0_0_318_159,https://scrap.kakaocdn.net/dn/jqJK4/hyZKa3dmbq/cszRQHGewbQwvf2BI0jrrK/img.png?width=4032&amp;amp;height=3024&amp;amp;face=139_175_731_608&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/252&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/252&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/blpnXv/hyZKHFVePr/3NAyfcwr2czap7vHlEUjyK/img.jpg?width=318&amp;amp;height=159&amp;amp;face=0_0_318_159,https://scrap.kakaocdn.net/dn/fACIa/hyZLkb3KQk/wwSmxEb0zv715mlk9xmimK/img.jpg?width=318&amp;amp;height=159&amp;amp;face=0_0_318_159,https://scrap.kakaocdn.net/dn/jqJK4/hyZKa3dmbq/cszRQHGewbQwvf2BI0jrrK/img.png?width=4032&amp;amp;height=3024&amp;amp;face=139_175_731_608');&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;2025 블레이버스 MVP 개발 해커톤 대회 테크 인사이트상 수상 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;어느덧 2025년&amp;hellip;!!본격적인 취업 준비에 들어가기 전에, 마지막으로 공모전에 참가하고 싶었다.그때 동아리 친구가 블레이버스 개발 해커톤에 나가자고 제안했다.덕분에 동아리 팀원들과 함께&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.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;290&quot; data-start=&quot;196&quot; data-ke-size=&quot;size16&quot;&gt;이번 연휴에는 평소에 미뤄뒀던 주제들을 하나씩 정리해보려 한다.&lt;br /&gt;그중 첫 번째로 &lt;b&gt;동시성 충돌(concurrency issue)&lt;/b&gt; 에 대해 깊이 다뤄볼 예정이다.&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;당시 해커톤 프로젝트에서는 예약시스템이 있어서 sychronized 키워드를 사용해 스레드 간 동시성 문제를 제어했다.&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;하지만 시간이 지나면서 &lt;b&gt;공정성(fairness)&lt;/b&gt; 문제와 &lt;b&gt;무한 대기(deadlock)&lt;/b&gt; 가능성이 드러나,&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;보다 세밀한 제어가 가능한 &lt;b&gt;ReentrantLock&lt;/b&gt; 으로 개선했다.&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;550&quot; data-start=&quot;292&quot; data-ke-size=&quot;size16&quot;&gt;이로써 코드 단위의 락 해제와 재진입 제어가 유연해졌고, 상황에 따라 타임아웃 설정이나 공정 모드 적용도 가능해졌다.&lt;/p&gt;
&lt;p data-end=&quot;751&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;그러나 여기서 한 가지 중요한 점을 놓치고 있었다. 바로 &lt;b&gt;서버가 로드밸런싱 환경에서 3대의 인스턴스로 동작하고 있었다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;751&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;751&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;JVM 수준에서의 락(synchronized, ReentrantLock)은 인스턴스 간에 공유되지 않기 때문에&lt;/b&gt;,&lt;/p&gt;
&lt;p data-end=&quot;751&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;751&quot; data-start=&quot;552&quot; data-ke-size=&quot;size16&quot;&gt;멀티 인스턴스 환경에서는 여전히 &lt;b&gt;동시성 충돌이 완전히 해결되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;767&quot; data-start=&quot;753&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;767&quot; data-start=&quot;753&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 실험에서는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;894&quot; data-start=&quot;768&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;804&quot; data-start=&quot;768&quot;&gt;&lt;b&gt;ReentrantLock (단일 JVM 기반 락)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;846&quot; data-start=&quot;805&quot;&gt;&lt;b&gt;비관적 락(Pessimistic Lock, DB 수준 락)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;894&quot; data-start=&quot;847&quot;&gt;&lt;b&gt;Redis 분산락 (Redisson 기반, 다중 인스턴스 환경 대응)&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;962&quot; data-start=&quot;896&quot; data-ke-size=&quot;size16&quot;&gt;이 세 가지 방법을 적용해보고, 각각의 &lt;b&gt;처리 성능, 락 경쟁 상황, 대기 시간&lt;/b&gt; 등을 비교 분석해볼 예정이다.&lt;/p&gt;
&lt;p data-end=&quot;962&quot; data-start=&quot;896&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;962&quot; data-start=&quot;896&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기술 도입&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;154&quot; data-start=&quot;73&quot; data-ke-size=&quot;size16&quot;&gt;낙관적 락도 존재하지만, &lt;b&gt;동시에 동일 자원에 접근하는 상황이 빈번하게 발생&lt;/b&gt;하는 경우에는 &lt;b&gt;비관적 락을 사용하는 것이 더 적절하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;303&quot; data-start=&quot;156&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;b&gt;항공권 예약이나 영화관 예매처럼 비교적 여유 있는 시스템&lt;/b&gt;에서는 낙관적 락을 사용해도 문제가 없지만,&lt;br /&gt;&lt;b&gt;티켓팅이나 미용실 예약처럼 동일한 시간대에 단 한 명만 예약이 가능하고 요청이 동시에 몰리는 환경&lt;/b&gt;에서는 비관적 락이 더 안전하다.&lt;/p&gt;
&lt;hr data-end=&quot;308&quot; data-start=&quot;305&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;340&quot; data-start=&quot;310&quot; data-ke-size=&quot;size23&quot;&gt;  낙관적 락 (Optimistic Lock)&lt;/h3&gt;
&lt;p data-end=&quot;462&quot; data-start=&quot;341&quot; data-ke-size=&quot;size16&quot;&gt;낙관적 락은 &lt;b&gt;충돌이 거의 없을 것이라고 가정&lt;/b&gt;하고, 자원에 접근할 때 잠금을 걸지 않는다.&lt;br /&gt;마치 &lt;b&gt;회의실 예약표를 미리 잠그지 않고 작성한 뒤, 나중에 제출할 때 겹치면 다시 써야 하는 방식&lt;/b&gt;과 같다.&lt;/p&gt;
&lt;p data-end=&quot;565&quot; data-start=&quot;464&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;b&gt;트래픽이 적을 때는 효율적&lt;/b&gt;이지만,&lt;br /&gt;여러 사용자가 동시에 같은 자원을 수정하려 할 경우 &lt;b&gt;충돌 후 재시도와 롤백이 반복되어 성능 저하&lt;/b&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;hr data-end=&quot;570&quot; data-start=&quot;567&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;603&quot; data-start=&quot;572&quot; data-ke-size=&quot;size23&quot;&gt;  비관적 락 (Pessimistic Lock)&lt;/h3&gt;
&lt;p data-end=&quot;741&quot; data-start=&quot;604&quot; data-ke-size=&quot;size16&quot;&gt;반면, 비관적 락은 &lt;b&gt;충돌이 발생할 가능성이 높다고 가정&lt;/b&gt;한다.&lt;br /&gt;자원에 접근하는 순간 &lt;b&gt;잠금을 걸어 다른 사용자의 접근을 차단&lt;/b&gt;하는데,&lt;br /&gt;이는 &lt;b&gt;회의실 예약표를 직접 들고 가서 다른 사람이 볼 수 없게 하는 방식&lt;/b&gt;과 비슷하다.&lt;/p&gt;
&lt;p data-end=&quot;839&quot; data-start=&quot;743&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;b&gt;데이터 정합성을 확실히 보장&lt;/b&gt;할 수 있지만,&lt;br /&gt;한 사용자가 락을 잡고 있는 동안 &lt;b&gt;다른 요청이 대기 상태로 전환되어 응답 지연이 발생&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;hr data-end=&quot;844&quot; data-start=&quot;841&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;944&quot; data-start=&quot;846&quot; data-ke-size=&quot;size16&quot;&gt;따라서 어느 방식이 &quot;정답&quot;이라고 단정하기는 어렵다.&lt;br /&gt;다만 이번 테스트에서는 &lt;b&gt;DB 수준에서의 락 동작을 직접 확인하기 위해 비관적 락을 적용&lt;/b&gt;하여 실험을 진행했다.&lt;/p&gt;
&lt;hr data-end=&quot;949&quot; data-start=&quot;946&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;984&quot; data-start=&quot;951&quot; data-ke-size=&quot;size23&quot;&gt;  확장 환경에서의 한계와 Redis 분산 락 도입&lt;/h3&gt;
&lt;p data-end=&quot;1098&quot; data-start=&quot;985&quot; data-ke-size=&quot;size16&quot;&gt;한편, &lt;b&gt;단일 DB 환경&lt;/b&gt;에서는 비관적 락만으로도 충분하지만,&lt;br /&gt;&lt;b&gt;DB가 여러 대로 확장된 분산 환경&lt;/b&gt;에서는 각 서버의 &lt;b&gt;DB 연결 세션이 분리되어 있어 서버 간 락 공유가 불가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-end=&quot;1162&quot; data-start=&quot;1100&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 &lt;b&gt;Redis 기반의 분산 락(Redis Distributed Lock)&lt;/b&gt;을 도입했다.&lt;/p&gt;
&lt;p data-end=&quot;1319&quot; data-start=&quot;1164&quot; data-ke-size=&quot;size16&quot;&gt;Redis 분산 락은 &lt;b&gt;건물 전체의 회의실 예약표를 중앙 관리실에서 통합 관리하는 방식&lt;/b&gt;과 유사하다.&lt;br /&gt;여러 서버가 동시에 동일 자원에 접근하더라도, &lt;b&gt;중앙의 Redis가 &amp;ldquo;누가 먼저 예약했는지&amp;rdquo;를 관리&lt;/b&gt;하여&lt;br /&gt;&lt;b&gt;단 하나의 요청만 자원을 점유하도록 보장&lt;/b&gt;한다.&lt;/p&gt;
&lt;hr data-end=&quot;1324&quot; data-start=&quot;1321&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1355&quot; data-start=&quot;1326&quot; data-ke-size=&quot;size23&quot;&gt;⚙️ Redis 분산 락의 특성과 트레이드오프&lt;/h3&gt;
&lt;p data-end=&quot;1531&quot; data-start=&quot;1356&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 과정에서 각 서버가 Redis 서버와 통신해야 하므로,&lt;br /&gt;&lt;b&gt;락의 획득&amp;middot;해제 과정마다 네트워크 왕복(Round Trip)&lt;/b&gt;이 발생한다.&lt;br /&gt;또한 &lt;b&gt;SETNX, PEXPIRE, DEL&lt;/b&gt; 등의 명령을 통해 락이 관리되기 때문에&lt;br /&gt;&lt;b&gt;비관적 락보다 평균 응답 시간이 길어지는 단점&lt;/b&gt;이 존재한다.&lt;/p&gt;
&lt;p data-end=&quot;1594&quot; data-start=&quot;1533&quot; data-ke-size=&quot;size16&quot;&gt;결국, &lt;b&gt;Redis 분산 락은 속도보다는 전역 일관성을 보장하는 데 초점을 맞춘 방식&lt;/b&gt;이라고 할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1594&quot; data-start=&quot;1533&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1594&quot; data-start=&quot;1533&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;962&quot; data-start=&quot;896&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;단일 서버&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import http from 'k6/http';
import { check, sleep } from 'k6';

// 테스트 옵션 설정
export const options = {
    vus: 50,                // 가상 사용자 50명
    duration: '30s',        // 테스트 지속 시간 30초
    thresholds: {
        http_req_failed: ['rate&amp;lt;0.05'],   // 실패율 5% 이하
        http_req_duration: ['p(95)&amp;lt;500'], // 95% 요청이 500ms 이하
    },
};

// 테스트 대상 URL
const BASE_URL = 'http://localhost:8080/api/consulting';

// 공통 헤더
const params = {
    headers: {
        'Content-Type': 'application/json',
    },
};

export default function () {
    // 각 가상 사용자별로 userId 2~51 부여
    const userId = (__VU + 1).toString();

    // 요청 바디
    const payload = JSON.stringify({
        designerId: &quot;uuid01&quot;,
        meet: &quot;offline&quot;,
        pay: &quot;카카오페이&quot;,
        startTime: &quot;2025-10-09T15:30:00&quot;,
        userId: userId
    });

    // POST 요청
    const res = http.post(BASE_URL, payload, params);

    // 응답 검증
    check(res, {
        'status was 200': (r) =&amp;gt; r.status === 200,
        'response time &amp;lt; 500ms': (r) =&amp;gt; r.timings.duration &amp;lt; 500,
    });

    sleep(0.1); // 사용자 간 약간의 간격
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&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;1331&quot; data-start=&quot;1199&quot; data-ke-size=&quot;size16&quot;&gt;단일 서버에서 테스트를 진행해볼 K6 코드이다.&lt;br /&gt;이 코드를 통해 &lt;b&gt;여러 사용자가 동시에 동일한 API를 호출할 때 발생할 수 있는 동시성 문제(예: 중복 처리, 락 충돌, 트랜잭션 경합 등)&lt;/b&gt;를 &lt;b&gt;시뮬레이션 형태로 검증&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1433&quot; data-start=&quot;1338&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1433&quot; data-start=&quot;1338&quot; data-ke-size=&quot;size16&quot;&gt;다만, K6은 서버 외부에서 부하를 발생시키는 도구이므로, &lt;b&gt;동시성의 내부 원인(스레드, 락, 트랜잭션 경합)&lt;/b&gt; 은 별도의 로그나 모니터링 도구를 통해 분석해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1433&quot; data-start=&quot;1338&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ReentrantLock&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class ReentrantLockManager {

    private final ConcurrentHashMap&amp;lt;String, ReentrantLock&amp;gt; lockMap = new ConcurrentHashMap&amp;lt;&amp;gt;();
    private final ConcurrentHashMap&amp;lt;String, Boolean&amp;gt; reservedMap = new ConcurrentHashMap&amp;lt;&amp;gt;();

    public ReentrantLock getLock(String designerId, LocalDateTime startTime) {
        String lockKey = createKey(designerId, startTime);
        return lockMap.computeIfAbsent(lockKey, key -&amp;gt; new ReentrantLock(true));
    }

    public boolean isReserved(String designerId, LocalDateTime startTime) {
        String lockKey = createKey(designerId, startTime);
        return reservedMap.getOrDefault(lockKey, false);
    }

    public void markAsReserved(String designerId, LocalDateTime startTime) {
        String lockKey = createKey(designerId, startTime);
        reservedMap.put(lockKey, true);
    }

    private String createKey(String designerId, LocalDateTime startTime) {
        return designerId + &quot;:&quot; +
                startTime.truncatedTo(ChronoUnit.MINUTES)
                        .format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd'T'HH:mm&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public ConsultingResponseDTO executeV1(ConsultingRequestDTO req, String userId) {

    . . . . 객체 불러오기
    
    ReentrantLock lock = reentrantLockManager.getLock(req.designerId(), req.startTime());
    boolean acquired = false;

    try {
        acquired = lock.tryLock(5, TimeUnit.SECONDS);
        if (!acquired) {
            throw new BlaybusException(HttpStatus.CONFLICT, &quot;현재 예약 요청이 많습니다. 잠시 후 다시 시도해주세요.&quot;);
        }

        // 락 안에서 메모리 캐시로 중복 체크
        if (reentrantLockManager.isReserved(req.designerId(), req.startTime())) {
            throw new BlaybusException(HttpStatus.CONFLICT, &quot;이미 해당 시간에 예약 시간이 존재합니다.&quot;);
        }

        // DB에서도 한 번 더 체크 (혹시 모를 경우 대비)
        if (consultingRepository.existsByDesignerAndStartTime(designer, req.startTime())) {
            throw new BlaybusException(HttpStatus.CONFLICT, &quot;이미 해당 시간에 예약 시간이 존재합니다.&quot;);
        }

        	. . . . . . . 저장 로직

        return ConsultingResponseDTO.from(consulting);
        
        // 저장 성공 후 메모리에 마킹
        reentrantLockManager.markReserved(req.designerId(), req.startTime());

    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new BlaybusException(HttpStatus.INTERNAL_SERVER_ERROR, &quot;락 획득 중 인터럽트 발생&quot;);
    } finally {
        if (acquired) {
            lock.unlock();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;br /&gt;여기서 락은 finally 블록에서 즉시 해제되지만, 실제 DB 커밋은 @Transactional 메서드가 완전히 종료된 뒤에 이루어진다.&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;즉, 락이 풀린 순간 다른 스레드가 같은 자원에 대한 락을 획득해 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;이러한 타이밍 이슈를 방지하기 위해, &lt;b&gt;락 해제 직전에 메모리 캐시에 예약 상태를 미리 반영&lt;/b&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;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(&quot;SELECT c FROM Consulting c WHERE c.designer = :designer AND c.startTime = :startTime&quot;)
Optional&amp;lt;Consulting&amp;gt; findWithPessimisticLock(@Param(&quot;designer&quot;) Designer designer,
                                             @Param(&quot;startTime&quot;) LocalDateTime startTime);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public ConsultingResponseDTO executeV2(ConsultingRequestDTO req, String userId) {
  
    . . . . 객체 불러오기

    // 동일 (designer, startTime) 조합에 대해 DB가 직접 Lock 잡음
    boolean exists = consultingRepository.findWithPessimisticLock(designer, req.startTime()).isPresent();
    if (exists) {
        throw new BlaybusException(HttpStatus.CONFLICT, &quot;이미 해당 시간에 예약 시간이 존재합니다.&quot;);
    }
  
  			. . . . . . . 저장 되는 코드
  
   return new ConsultingResponseDTO(
                consulting.getId(),
                consulting.getUser().getId(),
                consulting.getDesigner().getId(),
                consulting.getMeeting(),
                type,
                ConsultingStatus.fromString(status),
                consulting.getPay(),
                consulting.getStartTime()
        );
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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 data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;58d5288b-defa-419d-8aa6-b04fb127cb12&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;46&quot; data-start=&quot;0&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;&amp;nbsp;&lt;/div&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;Redis 분산락&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt; public ConsultingResponseDTO execute(ConsultingRequestDTO req, String userId) {

      	. . . . 객체 불러오기 
        
        String normalizedTime = req.startTime()
                .truncatedTo(ChronoUnit.MINUTES)
                .format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd'T'HH:mm&quot;));

        String lockKey = &quot;lock:consulting:&quot; + req.designerId() + &quot;:&quot; + normalizedTime;
        String reservedKey = &quot;reserved:consulting:&quot; + req.designerId() + &quot;:&quot; + normalizedTime;

        RLock lock = redissonClient.getLock(lockKey);

        boolean isLocked = false;
        try {
            // waitTime=5s, leaseTime=10s
            isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new BlaybusException(HttpStatus.CONFLICT, &quot;현재 예약 요청이 많습니다. 잠시 후 다시 시도해주세요.&quot;);
            }

            // Redis 캐시에서 먼저 체크
            RBucket&amp;lt;String&amp;gt; reservedBucket = redissonClient.getBucket(reservedKey);
            if (reservedBucket.isExists()) {
                throw new BlaybusException(HttpStatus.CONFLICT, &quot;이미 해당 시간에 예약 시간이 존재합니다.&quot;);
            }

            // DB에서도 체크
            if (consultingRepository.existsByDesignerAndStartTime(designer, req.startTime())) {
                throw new BlaybusException(HttpStatus.CONFLICT, &quot;이미 해당 시간에 예약 시간이 존재합니다.&quot;);
            }

            . . . . . . . 저장되는 코드

            // Redis에 예약 완료 마킹 (TTL 1시간)
            reservedBucket.set(&quot;reserved&quot;, 1, TimeUnit.HOURS);

            return new ConsultingResponseDTO(
                    consulting.getId(),
                    consulting.getUser().getId(),
                    consulting.getDesigner().getId(),
                    consulting.getMeeting(),
                    type,
                    ConsultingStatus.fromString(status),
                    consulting.getPay(),
                    consulting.getStartTime()
            );

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BlaybusException(HttpStatus.INTERNAL_SERVER_ERROR, &quot;락 획득 중 인터럽트 발생&quot;);
        } finally {
            if (isLocked &amp;amp;&amp;amp; lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;154&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;앞서 명시적 락을 사용한 경우와 동일한 상황이다. 이쪽 역시 별도의 캐시를 두어 예약 상태를 관리하였다.&lt;/p&gt;
&lt;p data-end=&quot;154&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;154&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;예약 완료 마킹을 &lt;b&gt;1시간&lt;/b&gt;으로 설정한 이유는,&lt;br /&gt;&lt;b&gt;해당 시간대의 예약이 끝난 이후에는 동일한 시간 슬롯에 대한 중복 요청이 더 이상 의미가 없기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;260&quot; data-start=&quot;156&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;260&quot; data-start=&quot;156&quot; data-ke-size=&quot;size16&quot;&gt;즉, 예약 시작 시점부터 1시간이 지나면 자연스럽게 해당 시간대의 예약 가능 여부가 초기화되도록 하여,&lt;br /&gt;&lt;b&gt;불필요한 캐시 데이터가 오래 남지 않게 하고 메모리 효율성을 유지하기 위함이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;260&quot; data-start=&quot;156&quot; 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;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1DiUb/btsQ3IVypZ8/e88Z0uxlMbT0nVkBqo5WCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1DiUb/btsQ3IVypZ8/e88Z0uxlMbT0nVkBqo5WCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1DiUb/btsQ3IVypZ8/e88Z0uxlMbT0nVkBqo5WCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1DiUb%2FbtsQ3IVypZ8%2Fe88Z0uxlMbT0nVkBqo5WCK%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;867&quot; height=&quot;741&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zsN5I/btsQ5RwNlEr/CR1a6uDHjJuoIKdyuq5bm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zsN5I/btsQ5RwNlEr/CR1a6uDHjJuoIKdyuq5bm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zsN5I/btsQ5RwNlEr/CR1a6uDHjJuoIKdyuq5bm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzsN5I%2FbtsQ5RwNlEr%2FCR1a6uDHjJuoIKdyuq5bm1%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;746&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;774&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFrHzN/btsQ3ApKxGL/HLYvu42bUDQpQscoAlPG91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFrHzN/btsQ3ApKxGL/HLYvu42bUDQpQscoAlPG91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFrHzN/btsQ3ApKxGL/HLYvu42bUDQpQscoAlPG91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFrHzN%2FbtsQ3ApKxGL%2FHLYvu42bUDQpQscoAlPG91%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;872&quot; height=&quot;774&quot; data-origin-width=&quot;872&quot; data-origin-height=&quot;774&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-end=&quot;150&quot; data-start=&quot;107&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;150&quot; data-start=&quot;107&quot; data-ke-size=&quot;size16&quot;&gt;앞서 코드를 기반으로 K6 부하 테스트를 진행했다.&lt;/p&gt;
&lt;p data-end=&quot;150&quot; data-start=&quot;107&quot; data-ke-size=&quot;size16&quot;&gt;단순히 테스트 코드로 검증할 수도 있었지만, &lt;b&gt;응답 지연(ms)&lt;/b&gt; 을 함께 확인하기 위해 K6를 활용했다.&lt;br /&gt;왼쪽부터 순서대로 &lt;b&gt;ReentrantLock&lt;/b&gt;, &lt;b&gt;비관적 락&lt;/b&gt;, &lt;b&gt;Redis 분산 락&lt;/b&gt; 결과이다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 291px;&quot; border=&quot;1&quot; data-end=&quot;1044&quot; data-start=&quot;131&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 63px;&quot;&gt;
&lt;td style=&quot;height: 63px; width: 4.18605%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 13.1395%;&quot;&gt;락 종류&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 9.65117%;&quot;&gt;평균 응답 시간&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 7.67447%;&quot;&gt;최대 응답 시간&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 9.53487%;&quot;&gt;95% 응답 시간&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 6.86049%;&quot;&gt;요청 수&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 9.65116%;&quot;&gt;HTTP 실패율&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 39.186%;&quot;&gt;동시성 관점 분석&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 85px;&quot; data-end=&quot;597&quot; data-start=&quot;359&quot;&gt;
&lt;td style=&quot;height: 85px; width: 4.18605%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;367&quot; data-start=&quot;359&quot;&gt;&lt;b&gt;①&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 85px; width: 13.1395%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;398&quot; data-start=&quot;367&quot;&gt;&lt;b&gt;ReentrantLock (JVM 내부 락)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 85px; width: 9.65117%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;412&quot; data-start=&quot;398&quot;&gt;&lt;b&gt;9.11 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 85px; width: 7.67447%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;428&quot; data-start=&quot;412&quot;&gt;&lt;b&gt;565.87 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 85px; width: 9.53487%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;443&quot; data-start=&quot;428&quot;&gt;&lt;b&gt;12.32 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 85px; width: 6.86049%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;461&quot; data-start=&quot;443&quot;&gt;&lt;b&gt;&amp;asymp; 455 req/s&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 85px; width: 9.65116%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;475&quot; data-start=&quot;461&quot;&gt;&lt;b&gt;99.99 %&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 85px; width: 39.186%;&quot; data-col-size=&quot;lg&quot; data-end=&quot;597&quot; data-start=&quot;475&quot;&gt;단일 인스턴스 내에서만 동기화되므로 처리 속도는 빠르지만, 외부 요청 충돌 시 일부 스레드가 락을 획득하지 못해 &lt;b&gt;요청 실패율이 매우 높음&lt;/b&gt;. 서버 내부에서는 동시성 제어가 잘 되지만, 분산 환경에는 부적합.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 63px;&quot; data-end=&quot;821&quot; data-start=&quot;598&quot;&gt;
&lt;td style=&quot;height: 63px; width: 4.18605%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;606&quot; data-start=&quot;598&quot;&gt;&lt;b&gt;②&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 13.1395%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;645&quot; data-start=&quot;606&quot;&gt;&lt;b&gt;비관적 락 (Pessimistic Lock / DB 레벨)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 9.65117%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;660&quot; data-start=&quot;645&quot;&gt;&lt;b&gt;11.36 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 7.67447%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;676&quot; data-start=&quot;660&quot;&gt;&lt;b&gt;322.23 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 9.53487%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;690&quot; data-start=&quot;676&quot;&gt;&lt;b&gt;17.2 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 6.86049%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;708&quot; data-start=&quot;690&quot;&gt;&lt;b&gt;&amp;asymp; 443 req/s&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 9.65116%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;722&quot; data-start=&quot;708&quot;&gt;&lt;b&gt;99.99 %&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 39.186%;&quot; data-col-size=&quot;lg&quot; data-end=&quot;821&quot; data-start=&quot;722&quot;&gt;트랜잭션 단위의 DB 락으로 인해 &lt;b&gt;정합성은 확보되지만&lt;/b&gt;, 동시에 다수의 요청이 들어오면 DB 락 대기로 인해 응답이 지연됨. &lt;b&gt;락 경합이 심화될수록 병목 발생&lt;/b&gt;. 즉 시스템 전체 성능 마비로 이어짐.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 63px;&quot; data-end=&quot;1044&quot; data-start=&quot;822&quot;&gt;
&lt;td style=&quot;height: 63px; width: 4.18605%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;830&quot; data-start=&quot;822&quot;&gt;&lt;b&gt;③&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 13.1395%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;858&quot; data-start=&quot;830&quot;&gt;&lt;b&gt;Redis 분산 락 (Redisson)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 9.65117%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;874&quot; data-start=&quot;858&quot;&gt;&lt;b&gt;105.07 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 7.67447%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;890&quot; data-start=&quot;874&quot;&gt;&lt;b&gt;594.73 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 9.53487%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;906&quot; data-start=&quot;890&quot;&gt;&lt;b&gt;259.48 ms&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 6.86049%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;924&quot; data-start=&quot;906&quot;&gt;&lt;b&gt;&amp;asymp; 242 req/s&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 9.65116%;&quot; data-col-size=&quot;sm&quot; data-end=&quot;938&quot; data-start=&quot;924&quot;&gt;&lt;b&gt;99.98 %&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 63px; width: 39.186%;&quot; data-col-size=&quot;lg&quot; data-end=&quot;1044&quot; data-start=&quot;938&quot;&gt;네트워크 기반 분산 락으로 인해 응답 시간이 크게 증가했으며, &lt;b&gt;락 획득&amp;middot;해제 과정의 네트워크 오버헤드가 명확&lt;/b&gt;히 드러남. 그러나 여러 인스턴스 간의 일관성 유지에는 가장 적합함.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;표를 정리하면 위와 같다.&lt;/div&gt;
&lt;div&gt;단일 서버 환경에서 테스트를 진행했기 때문에, &lt;b&gt;모든 경우에서 성공은 1건, 나머지는 모두 실패&lt;/b&gt;한 것은 당연한 결과다.&lt;/div&gt;
&lt;div&gt;또한 Redis의 응답 지연 시간이 가장 긴 이유 역시 &lt;b&gt;네트워크를 통한 통신이 필요하기 때문&lt;/b&gt;이다.&lt;/div&gt;
&lt;div&gt;표만 놓고 보면, &lt;b&gt;비관적 락과 ReentrantLock 모두 안정적으로 동작한 것으로 보인다.&lt;/b&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-end=&quot;962&quot; data-start=&quot;896&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;다중 서버&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    vus: 50,
    duration: '30s',
    thresholds: {
        http_req_failed: ['rate&amp;lt;0.05'],
        http_req_duration: ['p(95)&amp;lt;5000'],
    },
};

// 공통 헤더
const params = {
    headers: {
        'Content-Type': 'application/json',
    },
};

const ports = [8080, 8081, 8082];

export default function () {
    // 매 요청마다 랜덤 포트 선택
    const port = ports[Math.floor(Math.random() * ports.length)];
    const BASE_URL = `http://localhost:${port}/api/consulting/create`;

    const userId = (__VU + 1).toString();

    const payload = JSON.stringify({
        designerId: &quot;uuid01&quot;,
        meet: &quot;offline&quot;,
        pay: &quot;카카오페이&quot;,
        startTime: &quot;2025-10-09T15:30:00&quot;,
        userId: userId
    });

    const res = http.post(BASE_URL, payload, params);

    check(res, {
        'status was 200': (r) =&amp;gt; r.status === 200,
        'response time &amp;lt; 5000ms': (r) =&amp;gt; r.timings.duration &amp;lt; 5000,
    });

    sleep(0.1);
}
&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;랜덤하게 선택&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;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# ================================
# Blaybus 다중 인스턴스 실행 스크립트
# ================================

# 프로젝트 루트 경로
$PROJECT_ROOT = &quot;C:\Users\chltm\Github\Blaybus-Haertz-Server&quot;

# JAR 파일 경로
$JAR_PATH = &quot;$PROJECT_ROOT\build\libs\Blaybus-0.0.1-SNAPSHOT.jar&quot;

# 환경변수 파일 경로
$ENV_PATH = &quot;$PROJECT_ROOT\env\prod.env&quot;

# 복사 대상 경로 (JAR 실행 위치)
$TARGET_ENV_DIR = &quot;$PROJECT_ROOT\build\libs\env&quot;

# env 폴더가 없으면 생성하고 prod.env 복사
if (-Not (Test-Path $TARGET_ENV_DIR)) {
    New-Item -ItemType Directory -Path $TARGET_ENV_DIR | Out-Null
    Copy-Item $ENV_PATH -Destination $TARGET_ENV_DIR
    Write-Host &quot;env/prod.env 복사 완료&quot;
}

# 실행 포트 목록
$ports = @(8080, 8081, 8082)

foreach ($port in $ports) {
    Write-Host &quot;  포트 $port 인스턴스 실행 중...&quot;
    Start-Process powershell -ArgumentList &quot;-NoExit&quot;, &quot;-Command&quot;, &quot;cd '$($PROJECT_ROOT)\build\libs'; java -jar Blaybus-0.0.1-SNAPSHOT.jar --server.port=$port&quot;
    Start-Sleep -Seconds 2
}

Write-Host &quot;&quot;
Write-Host &quot;==========================================&quot;
Write-Host &quot;모든 인스턴스 실행 완료!&quot;
Write-Host &quot;8080 / 8081 / 8082 포트에서 서버 확인 가능&quot;
Write-Host &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;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;eced555b-d324-487e-b248-111e65b42742&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;53&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 &lt;b&gt;.ps1 스크립트 파일을 작성해&lt;/b&gt;, 여러 인스턴스를 동시에 실행하도록 설정했다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;53&quot; data-start=&quot;0&quot; 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;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;871&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ok90y/btsQ4VAO7XO/O0BuoCrkFsgO6QNGvwlCX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ok90y/btsQ4VAO7XO/O0BuoCrkFsgO6QNGvwlCX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ok90y/btsQ4VAO7XO/O0BuoCrkFsgO6QNGvwlCX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fok90y%2FbtsQ4VAO7XO%2FO0BuoCrkFsgO6QNGvwlCX0%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;949&quot; height=&quot;871&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;871&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;873&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kTLWb/btsQ4Wma1ar/nAWilc0ICOPRLvKkc6qruk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kTLWb/btsQ4Wma1ar/nAWilc0ICOPRLvKkc6qruk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kTLWb/btsQ4Wma1ar/nAWilc0ICOPRLvKkc6qruk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkTLWb%2FbtsQ4Wma1ar%2FnAWilc0ICOPRLvKkc6qruk%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;976&quot; height=&quot;873&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;873&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;725&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F5I3o/btsQ3WfG787/k1VCiGyJstPNT54l1ShqV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F5I3o/btsQ3WfG787/k1VCiGyJstPNT54l1ShqV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F5I3o/btsQ3WfG787/k1VCiGyJstPNT54l1ShqV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF5I3o%2FbtsQ3WfG787%2Fk1VCiGyJstPNT54l1ShqV1%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;975&quot; height=&quot;725&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;725&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;진행한 결과다. 순서도 아까랑 동일하다.&amp;nbsp;&lt;/div&gt;
&lt;/div&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-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 5.34884%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 14.8837%;&quot;&gt;락 종류&lt;/td&gt;
&lt;td style=&quot;width: 10.814%;&quot;&gt;평균 응답 시간&lt;/td&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;최대 응답 시간&lt;/td&gt;
&lt;td style=&quot;width: 18.721%;&quot;&gt;95% 응답시간&lt;/td&gt;
&lt;td style=&quot;width: 12.5581%;&quot;&gt;요청 수&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 11.0465%;&quot;&gt;실패율&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 16.279%;&quot;&gt;생성 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 5.34884%;&quot;&gt;①&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 14.8837%;&quot;&gt;&lt;b&gt;ReentrantLock&lt;/b&gt;(JVM 내부 락)&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 10.814%;&quot;&gt;13.26 ms&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 10.3488%;&quot;&gt;1.09 s&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.721%;&quot;&gt;20.62 ms&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 12.5581%;&quot;&gt;&amp;asymp; 438 req/s&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 11.0465%;&quot;&gt;99.97%&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.279%;&quot;&gt;✅ &lt;b&gt;3건 생성&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 5.34884%;&quot;&gt;②&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 14.8837%;&quot;&gt;&lt;b&gt;비관적 락&lt;/b&gt;(DB 기반)&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 10.814%;&quot;&gt;19.2 ms&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 10.3488%;&quot;&gt;1.05 s&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.721%;&quot;&gt;53.87 ms&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 12.5581%;&quot;&gt;&amp;asymp; 415 req/s&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 11.0465%;&quot;&gt;99.99%&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.279%;&quot;&gt;✅ &lt;b&gt;1건 생성&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center; width: 5.34884%;&quot;&gt;③&lt;/td&gt;
&lt;td style=&quot;text-align: left; width: 14.8837%;&quot;&gt;&lt;b&gt;Redis 분산 락&lt;/b&gt;(Redisson)&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 10.814%;&quot;&gt;87.38 ms&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 10.3488%;&quot;&gt;1.17 s&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 18.721%;&quot;&gt;331.9 ms&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 12.5581%;&quot;&gt;&amp;asymp; 265 req/s&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 11.0465%;&quot;&gt;99.98%&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 16.279%;&quot;&gt;✅ &lt;b&gt;1건 생성&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;468c23e3-1ecc-46e7-afcd-055b29d1de61&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;264&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 위 표와 같다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;264&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;264&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;다중 서버 환경으로 설정하니 &lt;b&gt;응답 시간이 확연히 감소&lt;/b&gt;했다.&lt;br /&gt;특히 ReentrantLock의 경우 &lt;b&gt;총 3건이 생성&lt;/b&gt;되었는데, 직접 테스트해보며 &lt;b&gt;JVM 수준의 락으로는 다중 서버 간 동시 접근을 막을 수 없다는 점&lt;/b&gt;을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;264&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;264&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;반면 &lt;b&gt;비관적 락과 Redis 분산 락은 모두 1건으로 동일한 결과&lt;/b&gt;를 보였으며,&lt;br /&gt;단 비관적 락은 &lt;b&gt;DB 인스턴스가 3대라면 각각에서 동일하게 3건이 생성될 가능성&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;264&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;334&quot; data-start=&quot;63&quot; data-ke-size=&quot;size16&quot;&gt;무엇보다 &lt;b&gt;비관적 락(Pessimistic Lock)&lt;/b&gt; 은 대용량 트래픽 환경에서 &lt;b&gt;DB 커넥션 고갈&lt;/b&gt;을 유발해 시스템 전체를 마비시킬 수 있으며, 동시에 &lt;b&gt;데드락(Deadlock)&lt;/b&gt; 이 발생할 위험도 존재한다.&lt;/p&gt;
&lt;p data-end=&quot;334&quot; data-start=&quot;63&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;비관적 락은 일종의 &amp;ldquo;회의실 문을 잠그고 들어가는 방식&amp;rdquo;으로,&lt;br /&gt;예를 들어 너는 회의실 A를 점유 중이고 나는 회의실 B를 점유 중인데,&lt;br /&gt;서로 상대방 방의 열쇠가 필요하다면 둘 다 움직이지 못한 채 대기하게 된다.&lt;br /&gt;이것이 바로 &lt;b&gt;데드락&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;526&quot; data-start=&quot;336&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;526&quot; data-start=&quot;336&quot; data-ke-size=&quot;size16&quot;&gt;반면 &lt;b&gt;낙관적 락(Optimistic Lock)&lt;/b&gt; 은 회의실 문을 잠그지 않고, 나중에 겹쳤는지만 확인하는 방식이다.&lt;br /&gt;따라서 충돌이 발생해도 단순히 &amp;ldquo;다시 예약해&amp;rdquo; 하고 끝나므로 대기나 데드락이 발생하지 않는다.&lt;br /&gt;다만 대규모 요청이 동시에 몰리는 상황에서는 &lt;b&gt;충돌 후 재시도 횟수가 폭증&lt;/b&gt;해 시스템 부하로 이어질 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;760&quot; data-start=&quot;528&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;760&quot; data-start=&quot;528&quot; data-ke-size=&quot;size16&quot;&gt;이러한 이유로, Redis 분산락은 DB 트랜잭션과 별개로 &lt;b&gt;애플리케이션 레벨에서 락을 관리&lt;/b&gt;하기 때문에&lt;br /&gt;DB 커넥션을 점유하지 않으면서도 자원 접근 순서를 제어할 수 있다.&lt;br /&gt;즉, 비관적 락의 &lt;b&gt;병목과 데드락 문제를 완화&lt;/b&gt;하고,&lt;br /&gt;낙관적 락의 &lt;b&gt;과도한 재시도 문제&lt;/b&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;h2 data-end=&quot;264&quot; data-start=&quot;0&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;최종&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 테스트를 통해 얻은 인사이트들은 단순한 성능 비교를 넘어,&lt;br /&gt;&lt;b&gt;실제 서비스 환경에서 발생할 수 있는 동시성 문제의 본질을 깊이 이해하는 계기&lt;/b&gt;가 되었다.&lt;br /&gt;이 경험을 바탕으로, 새로운 사이드 프로젝트에서는 &lt;b&gt;더 안정적이고 확장 가능한 구조를 설계&lt;/b&gt;하고자 한다.&lt;br /&gt;나아가, &lt;b&gt;현장에서 마주칠 수 있는 다양한 문제 상황을 직접 연구하고 개선하는 개발자&lt;/b&gt;로 성장하고 싶다.&lt;/p&gt;</description>
      <category>개발 팁</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/336</guid>
      <comments>https://balhae.tistory.com/336#entry336comment</comments>
      <pubDate>Fri, 10 Oct 2025 18:25:00 +0900</pubDate>
    </item>
    <item>
      <title>[9oormthon 제주 버스 알림콜] 리메이크 및 조회 성능 최적화 하기</title>
      <link>https://balhae.tistory.com/335</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/219&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/219&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1759929715455&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; 구름톤(9oormthon) 11기 대상 후기&quot; data-og-description=&quot;  지원 동기와 과정대학교 4학년, 졸업을 앞둔 시점에서 문득 돌아보니 지금까지 만든 프로젝트들이 너무 의미없게 느껴졌습니다. 진짜 의미있는 무언가를 만들어보고 싶다는 생각이 들었고, &quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/219&quot; data-og-url=&quot;https://balhae.tistory.com/219&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/rh0Gv/hyZKo8Fyz7/lvWprn4uA1QJqmeSG9RMD1/img.png?width=583&amp;amp;height=328&amp;amp;face=0_0_583_328,https://scrap.kakaocdn.net/dn/RV7lz/hyZKnBXu7B/0jmeULE3MVqnCEGUvIsNu0/img.png?width=583&amp;amp;height=328&amp;amp;face=0_0_583_328,https://scrap.kakaocdn.net/dn/caieR8/hyZKv7NFF0/HCfiytE0hXBtC9jsnN2Kb1/img.png?width=1050&amp;amp;height=1400&amp;amp;face=0_0_1050_1400&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/219&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/219&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/rh0Gv/hyZKo8Fyz7/lvWprn4uA1QJqmeSG9RMD1/img.png?width=583&amp;amp;height=328&amp;amp;face=0_0_583_328,https://scrap.kakaocdn.net/dn/RV7lz/hyZKnBXu7B/0jmeULE3MVqnCEGUvIsNu0/img.png?width=583&amp;amp;height=328&amp;amp;face=0_0_583_328,https://scrap.kakaocdn.net/dn/caieR8/hyZKv7NFF0/HCfiytE0hXBtC9jsnN2Kb1/img.png?width=1050&amp;amp;height=1400&amp;amp;face=0_0_1050_1400');&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; 구름톤(9oormthon) 11기 대상 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  지원 동기와 과정대학교 4학년, 졸업을 앞둔 시점에서 문득 돌아보니 지금까지 만든 프로젝트들이 너무 의미없게 느껴졌습니다. 진짜 의미있는 무언가를 만들어보고 싶다는 생각이 들었고,&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.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;73&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;이번 연휴 10일 동안은 작년 11월에 참여했던 &lt;b&gt;9oormthon 제주 버스 알림콜 프로젝트&lt;/b&gt;를 다시 리메이크하기로 했다.&lt;/p&gt;
&lt;p data-end=&quot;187&quot; data-start=&quot;75&quot; data-ke-size=&quot;size16&quot;&gt;리메이크를 결심한 이유는, 당시에는 백엔드 개발 역량이 아직 미숙해서 아쉬운 점이 많았기 때문이다.&lt;br /&gt;이번에는 그때의 부족함을 보완하고, 특히 &lt;b&gt;조회 성능 최적화&lt;/b&gt;에 집중해 학습해볼 예정이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로젝트 리펙터링&amp;nbsp;&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;40be3e7d-d871-4200-a27c-3b8129df488d&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-end=&quot;42&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;왼쪽은 &lt;b&gt;리팩터링 이전&lt;/b&gt;, 오른쪽은 &lt;b&gt;리팩터링 이후&lt;/b&gt;의 모습이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;118&quot; data-start=&quot;44&quot; data-ke-size=&quot;size16&quot;&gt;이전 코드에서는 전반적으로 구조가 복잡하고 일관성이 부족했지만,&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;프로젝트 구조&amp;nbsp;&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;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;322&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhhGe0/btsQ2fT1IGQ/URsmCQFBuZEi66EwlMMoUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhhGe0/btsQ2fT1IGQ/URsmCQFBuZEi66EwlMMoUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhhGe0/btsQ2fT1IGQ/URsmCQFBuZEi66EwlMMoUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhhGe0%2FbtsQ2fT1IGQ%2FURsmCQFBuZEi66EwlMMoUK%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;322&quot; height=&quot;367&quot; data-origin-width=&quot;322&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oc8Z3/btsQ5XXV5aR/V6CChbJrACw7PBgzbKbzak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oc8Z3/btsQ5XXV5aR/V6CChbJrACw7PBgzbKbzak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oc8Z3/btsQ5XXV5aR/V6CChbJrACw7PBgzbKbzak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOc8Z3%2FbtsQ5XXV5aR%2FV6CChbJrACw7PBgzbKbzak%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;334&quot; height=&quot;621&quot; data-origin-width=&quot;334&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 훨씬 깔끔해졌다.&lt;br /&gt;이전에는 구조가 제대로 잡혀 있지 않아, 코드 흐름을 따라가기가 너무 헷갈렸다.&lt;br /&gt;이번에 DDD 도메인 구조를 적용하면서 &lt;b&gt;역할이 명확해지고, 유지보수도 훨씬 수월해졌다.&lt;/b&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;DB 테이블&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 319px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 319px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 319px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbOrKU/btsQ3arbW3I/bSGgK2P2szWubZKGdBMqa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbOrKU/btsQ3arbW3I/bSGgK2P2szWubZKGdBMqa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbOrKU/btsQ3arbW3I/bSGgK2P2szWubZKGdBMqa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbOrKU%2FbtsQ3arbW3I%2FbSGgK2P2szWubZKGdBMqa1%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;682&quot; height=&quot;522&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 319px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4fU9l/btsQ4BuNhwL/mY9fhKWUmiP16MhpDYsen0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4fU9l/btsQ4BuNhwL/mY9fhKWUmiP16MhpDYsen0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4fU9l/btsQ4BuNhwL/mY9fhKWUmiP16MhpDYsen0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4fU9l%2FbtsQ4BuNhwL%2FmY9fhKWUmiP16MhpDYsen0%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;854&quot; height=&quot;523&quot; data-origin-width=&quot;854&quot; data-origin-height=&quot;523&quot;/&gt;&lt;/span&gt;&lt;/figure&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-end=&quot;92&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;이전에는 엔티티와 컬럼 이름이 제각각이라 일관성이 없었고,&lt;br /&gt;Note 엔티티에는 정규화가 제대로 이루어지지 않아 한 엔티티가 너무 많은 역할을 맡고 있었다.&lt;/p&gt;
&lt;p data-end=&quot;278&quot; data-start=&quot;94&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;278&quot; data-start=&quot;94&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번 리팩터링에서는 Note 엔티티를 &lt;b&gt;BusLog&lt;/b&gt;로 변경하고,&lt;br /&gt;&lt;b&gt;알림(BusAlarm)과 즐겨찾기(BusFavorite) 기능은 별도의 엔티티로 분리&lt;/b&gt;해 관리하도록 했다.&lt;/p&gt;
&lt;p data-end=&quot;278&quot; data-start=&quot;94&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;278&quot; data-start=&quot;94&quot; data-ke-size=&quot;size16&quot;&gt;또한 Member 테이블에는 컬럼이 너무 적어서, 최소한의 정보라도 담기 위해 name 컬럼을 추가했다&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-end=&quot;105&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;이전에는 코드 스타일이 정말 극악이었다&amp;hellip;&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;지금 다시 보니까, 그때는 진짜 내가 얼마나 미숙했는지 느껴진다...&lt;/p&gt;
&lt;p data-end=&quot;105&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;105&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;이걸 리팩터링하는 데만 &lt;b&gt;3일&lt;/b&gt;이 걸렸으니 말 다 했다.&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;107&quot; data-ke-size=&quot;size16&quot;&gt;거의 전부 새로 짰다고 봐도 무방하다.&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;107&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;107&quot; data-ke-size=&quot;size16&quot;&gt;그래서 여기서는 대표적으로 &lt;b&gt;버스 관련 컨트롤러와 서비스 로직&lt;/b&gt;만 가져와서&lt;br /&gt;리팩터링 전후를 비교해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;lt;리펙터링 전&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@Tag(name = &quot;사용자의 버스 기록 정보&quot;)
@RequestMapping(&quot;/api/bus&quot;)
@Slf4j
public class NoteController {

    private final NoteService noteService;

    private final StationService stationService;

    private final NoteRepository noteRepository;

    @GetMapping(&quot;/list&quot;)
    @Operation(summary = &quot;즐겨찾기 버스 기록 저장 보여주기&quot;)
    public SuccessResponse&amp;lt;ListResult&amp;lt;NoteResponse&amp;gt;&amp;gt; readAll(@RequestAttribute(&quot;id&quot;) String userId) {
        ListResult&amp;lt;NoteResponse&amp;gt; note = noteService.findAll(userId);
        return SuccessResponse.ok(note);
    }



    @PostMapping(&quot;/save&quot;)
    @Operation(summary = &quot;버스 데이터 정보들이 쭉 넘어옴 여기서 전화 콜 및 일시 저장해줘야됨 즉 즐격찾기 false&quot;)
    public SuccessResponse&amp;lt;NoteSaveResponse&amp;gt; create(@Valid
                                                          @RequestAttribute(&quot;id&quot;) String userId,
                                                    @RequestBody NoteRequest req
    ) {
        SingleResult&amp;lt;Note&amp;gt; note = noteService.save(req,userId);



        String stationId = req.stationId();
        int station = req.station();
        String busId =req.notionId();
        // 5초마다 API 호출 시작
        stationService.scheduleBusApiCall(userId,busId, stationId,station);

        // Note 객체를 NoteSaveResponse로 변환
        NoteSaveResponse response = NoteSaveResponse.of(note.getData());

        // 변환된 NoteSaveResponse를 응답으로 반환
        return SuccessResponse.ok(response);
        // 즉시 note 객체 응답 반환
    }


    @PostMapping(&quot;/favorite&quot;)
    @Operation(summary = &quot;즐겨 찾기 API&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; delete(@Valid @RequestBody NoteFavoriteRequest req){
        Optional&amp;lt;Note&amp;gt; findNote =noteRepository.findById(req.id());
        if (findNote.isPresent()) {
            Note note = findNote.get();
            if(req.favorite()){
                noteRepository.updelete(req,2);
            }else{
                if(note.getFavorite_pre()!=1){
                    noteRepository.delete(req);
                }
            }

        } else {
            // 값이 없을 때의 처리 로직
            throw new NoSuchElementException(&quot;해당하는 사용자가 없습니다.&quot;);
        }



        // 즉시 응답 반환
        return ResponseEntity.ok(&quot;설정 완료&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&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;101&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;컨트롤러에 서비스 로직이 섞여 있고&amp;hellip; 진짜 &lt;b&gt;최악이었다.&lt;/b&gt;&lt;br /&gt;그때는 레이어드 아키텍처 개념도 제대로 잡혀 있지 않아서,&lt;br /&gt;비즈니스 로직이 컨트롤러 안에 뒤섞여 있었다.&lt;/p&gt;
&lt;p data-end=&quot;204&quot; data-start=&quot;103&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;204&quot; data-start=&quot;103&quot; data-ke-size=&quot;size16&quot;&gt;게다가 &lt;b&gt;커스텀 응답 방식&lt;/b&gt;도 제각각이라 일관성이 없었고,&lt;/p&gt;
&lt;p data-end=&quot;204&quot; data-start=&quot;103&quot; data-ke-size=&quot;size16&quot;&gt;SpringSecurityConfig 역시 구조가 정리되어 있지 않아 여러모로 &lt;b&gt;불완전한 코드&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@EnableScheduling
@Slf4j
@RequiredArgsConstructor
public class StationService {
    private final MemberRepository memberRepository;

    private final RestTemplate restTemplate = new RestTemplate();

    // 사용자별 상태를 저장하기 위한 Map
    private final Map&amp;lt;String, String&amp;gt; userBusIdMap = new ConcurrentHashMap&amp;lt;&amp;gt;();
    private final Map&amp;lt;String, String&amp;gt; userStationIdMap = new ConcurrentHashMap&amp;lt;&amp;gt;();
    private final Map&amp;lt;String, Integer&amp;gt; userStationMap = new ConcurrentHashMap&amp;lt;&amp;gt;();
    private final Map&amp;lt;String, Boolean&amp;gt; userStopCallingMap = new ConcurrentHashMap&amp;lt;&amp;gt;();
    private final Map&amp;lt;String, AtomicInteger&amp;gt; userCntMap = new ConcurrentHashMap&amp;lt;&amp;gt;();
    private final Map&amp;lt;String, Set&amp;lt;Integer&amp;gt;&amp;gt; userSeenBusesMap = new ConcurrentHashMap&amp;lt;&amp;gt;();
    private static final long RUNNING_DURATION_HOURS = 3; // 3시간
    private final LocalDateTime startTime = LocalDateTime.now();

    @Value(&quot;${twilio.account-sid}&quot;)
    private String accountSid;

    @Value(&quot;${twilio.auth-token}&quot;)
    private String authToken;

    // 사용자별 API 호출 상태 설정 메서드
    @Async
    public void scheduleBusApiCall(String userId, String busId, String stationId, int station) {
        userBusIdMap.put(userId, busId);
        userStationIdMap.put(userId, stationId);
        userStationMap.put(userId, station);
        userStopCallingMap.put(userId, false);  // 호출 중단 플래그 초기화
        userSeenBusesMap.put(userId, new HashSet&amp;lt;&amp;gt;());  // 중복 체크 목록 초기화
        userCntMap.put(userId, new AtomicInteger(0));  // 카운터 초기화
    }

    // 5초마다 실행되는 메서드 - 사용자별로 독립적으로 동작
    @Scheduled(fixedRate = 5000)
    public void callBusApi() {
        // 현재 시간이 시작 시간으로부터 3시간 경과했는지 확인
        if (ChronoUnit.HOURS.between(startTime, LocalDateTime.now()) &amp;gt;= RUNNING_DURATION_HOURS) {
            log.info(&quot;3시간이 경과하여 스케줄링을 중단합니다.&quot;);
            return;  // 스케줄링 중단
        }

        userStationIdMap.forEach((userId, stationId) -&amp;gt; {
            String busId = userBusIdMap.get(userId);
            Integer station = userStationMap.get(userId);
            Boolean stopCalling = userStopCallingMap.getOrDefault(userId, true);
            AtomicInteger cnt = userCntMap.getOrDefault(userId, new AtomicInteger(0));
            Set&amp;lt;Integer&amp;gt; seenBuses = userSeenBusesMap.getOrDefault(userId, new HashSet&amp;lt;&amp;gt;());

            if (stationId != null &amp;amp;&amp;amp; !stopCalling) {
                String url = &quot;https://bus.jeju.go.kr/api/searchArrivalInfoList.do?station_id=&quot; + stationId;
                ResponseEntity&amp;lt;BusInfo[]&amp;gt; response = restTemplate.getForEntity(url, BusInfo[].class);
                BusInfo[] buses = response.getBody();
                log.info(&quot;Twilio Account SID: {}&quot;, accountSid);
                log.info(&quot;Twilio Auth Token: {}&quot;, authToken);

                if (buses != null) {
                    log.info(&quot;API 응답 데이터: {}&quot;, Arrays.toString(buses));
                    log.info(&quot;현재 {} 사용자의 cnt 값: {}&quot;, userId, cnt);

                    Arrays.stream(buses)
                            .filter(bus -&amp;gt; busId.equals(bus.getRouteNum()) &amp;amp;&amp;amp; bus.getRemainStation() == station)
                            .forEach(bus -&amp;gt; {
                                if (!seenBuses.contains(bus.getVhId())) {
                                    log.info(&quot;{} 사용자에게 조건을 만족하는 새로운 버스를 찾았습니다: {}&quot;, userId, bus);
                                    seenBuses.add(bus.getVhId());
                                    cnt.incrementAndGet();  // 새로운 버스일 경우 카운터 증가
                                    userCntMap.put(userId, cnt);
                                    log.info(&quot;현재 {} 사용자의 cnt 값: {}&quot;, userId, cnt);
                                    Optional&amp;lt;Member&amp;gt; byPhone = memberRepository.findByPhone2(userId);
                                    if (byPhone.isPresent()) {
                                        Member member = byPhone.get();
                                        bus_call(member);
                                        // member에 대한 로직 처리
                                    } else {
                                        // 값이 없을 때의 처리 로직
                                        throw new NoSuchElementException(&quot;해당하는 사용자가 없습니다.&quot;);
                                    }

                                    if (cnt.get() &amp;gt;= 2) {
                                        log.info(&quot;{} 사용자의 cnt가 2에 도달하여 호출을 중단합니다.&quot;, userId);
                                        userStopCallingMap.put(userId, true);
                                    }
                                } else {
                                    log.info(&quot;{} 사용자는 이미 확인된 버스입니다: {}&quot;, userId, bus.getVhId());
                                }
                            });

                    if (cnt.get() &amp;lt; 2) {
                        log.info(&quot;{} 사용자에게 조건을 만족하는 새로운 버스를 찾지 못했습니다.&quot;, userId);
                    }
                } else {
                    log.warn(&quot;API 응답에서 버스 데이터가 비어 있습니다.&quot;);
                }

                // 상태 업데이트
                userSeenBusesMap.put(userId, seenBuses);
                userCntMap.put(userId, cnt);
            }
        });
    }

    private void bus_call(Member member) {
        Twilio.init(accountSid, authToken);
        log.info(&quot;버스 콜 실행&quot;);
        String phone = member.getPhone();
        String substring = phone.substring(1);
        String from = &quot;+16232992975&quot;;
        String to = &quot;+82&quot; + substring;
        log.info(to);

        // 사용자에게 전달할 음성 메시지 작성
        Say say = new Say.Builder(&quot;안녕하세요, 이것은 당신을 위한 음성 메시지입니다.&quot;)
                .language(Say.Language.KO_KR)
                .voice(Say.Voice.ALICE)
                .build();

        VoiceResponse response = new VoiceResponse.Builder()
                .say(say)
                .build();

        // VoiceResponse를 XML 문자열로 변환
        String twiml = response.toXml();

        // TwiML XML 문자열을 Twiml 객체로 감싸기
        com.twilio.type.Twiml twimlObject = new com.twilio.type.Twiml(twiml);

        // Twiml 객체를 사용하여 전화 걸기
        Call call = Call.creator(
                new com.twilio.type.PhoneNumber(to),
                new com.twilio.type.PhoneNumber(from),
                twimlObject
        ).create();

        log.info(&quot;Twilio Call SID: {}&quot;, call.getSid());
    }



}

// BusInfo 클래스는 API 응답에 맞게 필드 정의
class BusInfo {

    @JsonProperty(&quot;ARRV_STATION_ID&quot;)
    private int arrvStationId;

    @JsonProperty(&quot;ROUTE_SUB_NM&quot;)
    private String routeSubNm;

    @JsonProperty(&quot;VH_ID&quot;)
    private int vhId;

    @JsonProperty(&quot;ROUTE_ID&quot;)
    private int routeId;

    @JsonProperty(&quot;ROUTE_NUM&quot;)
    private String routeNum;

    @JsonProperty(&quot;PLATE_NO&quot;)
    private String plateNo;

    @JsonProperty(&quot;CURR_STATION_ID&quot;)
    private int currStationId;

    @JsonProperty(&quot;PREDICT_TRAV_TM&quot;)
    private int predictTravTm;

    @JsonProperty(&quot;REMAIN_STATION&quot;)
    private int remainStation;

    @JsonProperty(&quot;CURR_STATION_NM&quot;)
    private String currStationNm;

    @JsonProperty(&quot;LOW_PLATE_TP&quot;)
    private String lowPlateTp;

    // Getters and Setters
    public int getVhId() {
        return vhId;
    }

    public String getRouteNum() {
        return routeNum;
    }

    public int getRemainStation() {
        return remainStation;
    }

    @Override
    public String toString() {
        return &quot;ROUTE_NUM=&quot; + routeNum + &quot;, REMAIN_STATION=&quot; + remainStation;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;148&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;와&amp;hellip; 진짜 &lt;b&gt;최악 중의 최악&lt;/b&gt;이다.&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;지금 보면 딱 &amp;ldquo;비전공자가 ChatGPT 코드 베껴서 붙인 것 같은&amp;rdquo; 느낌이다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;148&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;148&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인터페이스도 없고, 모듈화도 안 되어 있고, 주석도 엉망&lt;/b&gt;이다.&lt;br /&gt;그냥 전부 다 뒤섞여 있는 &lt;b&gt;짬뽕 코드 그 자체&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-end=&quot;242&quot; data-start=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;242&quot; data-start=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;아무리 해커톤이라 급하게 만들었다고 해도, 이건 진짜 너무 했다 싶다.&lt;br /&gt;그때는 돌아볼 여유도 없었지만, 지금 보니까 &lt;b&gt;구조부터 완전 잘못된 설계&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;리펙터링 후&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@Tag(name = &quot;사용자의 버스 기록 정보&quot;, description = &quot;버스 로그, 즐겨찾기 관련 API&quot;)
@RequestMapping(&quot;/api/bus&quot;)
@Slf4j
public class BusLogController {

    private final BusLogService busLogService;

    /**
     * 버스 로그 저장 API
     * - 사용자 요청 시 새로운 버스 로그를 저장함
     * - 즐겨찾기는 기본적으로 false로 설정
     */
    @Operation(summary = &quot;버스 로그 저장&quot;, description = &quot;버스 데이터 정보를 저장합니다. (즐겨찾기 기본값 false)&quot;)
    @ApiResponses(value = {
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;버스 로그 저장 성공&quot;),
            @ApiResponse(responseCode = &quot;400&quot;, description = &quot;요청 데이터가 유효하지 않음&quot;),
            @ApiResponse(responseCode = &quot;404&quot;, description = &quot;사용자를 찾을 수 없음&quot;),
            @ApiResponse(responseCode = &quot;500&quot;, description = &quot;서버 내부 오류&quot;)
    })
    @PostMapping
    public ResponseEntity&amp;lt;Void&amp;gt; saveBusLog(@Valid @RequestBody BusLogSaveReq req,
                                           @AuthenticationPrincipal String userId) {
        busLogService.postBusLogSave(req, userId);
        return ResponseEntity.ok().build();
    }

    /**
     * 버스 로그 전체 조회 API
     * - 사용자의 모든 버스 로그를 조회
     * - 각 로그마다 알림 여부, 즐겨찾기 여부 포함
     */
    @Operation(summary = &quot;버스 로그 전체 조회&quot;, description = &quot;사용자의 모든 버스 기록을 조회합니다. (알림 여부, 즐겨찾기 여부 포함)&quot;)
    @ApiResponses(value = {
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;버스 로그 조회 성공&quot;),
            @ApiResponse(responseCode = &quot;404&quot;, description = &quot;사용자 또는 버스 로그가 존재하지 않음&quot;),
            @ApiResponse(responseCode = &quot;500&quot;, description = &quot;서버 내부 오류&quot;)
    })
    @GetMapping
    public ResponseEntity&amp;lt;List&amp;lt;BusLogAllRes&amp;gt;&amp;gt; getBusLogAll(@RequestParam String memberId,@AuthenticationPrincipal String userId) {
        return ResponseEntity.ok(busLogService.getBusLogAll(memberId));
    }

    /**
     * 즐겨찾기 상태 토글 API
     * - true &amp;harr; false 상태 전환
     */
    @Operation(summary = &quot;버스 즐겨찾기 상태 변경&quot;, description = &quot;해당 버스 로그의 즐겨찾기 상태를 토글합니다. (활성/비활성)&quot;)
    @ApiResponses(value = {
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;즐겨찾기 상태 변경 성공&quot;),
            @ApiResponse(responseCode = &quot;400&quot;, description = &quot;요청 데이터가 유효하지 않음&quot;),
            @ApiResponse(responseCode = &quot;404&quot;, description = &quot;버스 로그 또는 즐겨찾기 정보 없음&quot;),
            @ApiResponse(responseCode = &quot;500&quot;, description = &quot;서버 내부 오류&quot;)
    })
    @PostMapping(&quot;/favorite&quot;)
    public ResponseEntity&amp;lt;Void&amp;gt; updateBusFavorite(@Valid @RequestBody BusFavoriteReq req) {
        busLogService.updateBusFavorite(req);
        return ResponseEntity.ok().build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&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;83&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;훨씬 깔끔하다!! ✨&lt;br /&gt;이제는 &lt;b&gt;주석도 명확하게 정리되어 있고&lt;/b&gt;, &lt;b&gt;Swagger 문서화도 잘 되어 있어서&lt;/b&gt;&lt;br /&gt;코드가 한눈에 들어온다.&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;85&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;85&quot; data-ke-size=&quot;size16&quot;&gt;컨트롤러에는 &lt;b&gt;입출력 로직만 남겨서 역할이 명확해졌고&lt;/b&gt;, 서비스 단에서 비즈니스 로직을 깔끔하게 처리하니까&lt;br /&gt;읽기에도, 유지보수하기에도 훨씬 편하다. 이게 바로 내가 원하던 구조다&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;85&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;/**
 *   BusLogServiceImpl
 *
 * &amp;lt;p&amp;gt;버스 이동 기록(BusLog) 및 즐겨찾기, 알림 관련 비즈니스 로직을 담당하는 구현체입니다.&amp;lt;/p&amp;gt;
 * &amp;lt;ul&amp;gt;
 *   &amp;lt;li&amp;gt;사용자의 버스 이용 내역 저장&amp;lt;/li&amp;gt;
 *   &amp;lt;li&amp;gt;버스 로그 전체 조회 (알림, 즐겨찾기 포함)&amp;lt;/li&amp;gt;
 *   &amp;lt;li&amp;gt;즐겨찾기 상태 토글&amp;lt;/li&amp;gt;
 * &amp;lt;/ul&amp;gt;
 */
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class BusLogServiceImpl implements BusLogService {

    private final BusLogRepository busLogRepository;
    private final BusFavoriteRepository busFavoriteRepository;
    private final BusAlarmRepository busAlarmRepository;
    private final MemberRepository memberRepository;


    /**
     *   버스 로그 저장
     *
     * &amp;lt;p&amp;gt;새로운 버스 이용 로그를 생성 및 저장합니다.
     * 사용자가 존재하지 않으면 예외를 발생시킵니다.&amp;lt;/p&amp;gt;
     *
     * &amp;lt;p&amp;gt;기본적으로 생성 시 즐겨찾기는 false, 알림은 별도의 로직에서 관리됩니다.&amp;lt;/p&amp;gt;
     *
     * @param req    버스 로그 저장 요청 DTO
     * @param userId 현재 로그인된 사용자 ID
     * @throws GoormBusException 사용자가 존재하지 않을 경우
     */
    @Override
    public void postBusLogSave(BusLogSaveReq req, String userId) {
        Member findMember = memberRepository.findById(userId).orElse(null);
        if (findMember == null)
            throw new GoormBusException(ErrorCode.USER_NOT_EXIST);

        BusLog busLog = BusLog.builder()
                .member(findMember)
                .departure(req.departure())
                .destination(req.destination())
                .station(req.station())
                .notionId(req.notionId())
                .stationId(req.stationId())
                .build();

        BusAlarm busAlarm = BusAlarm.builder()
                .busLog(busLog)
                .build();

        BusFavorite busFavorite = BusFavorite.builder()
                .busLog(busLog)
                .build();
        busLogRepository.save(busLog);
        busFavoriteRepository.save(busFavorite);
        busAlarmRepository.save(busAlarm);
    }

    /**
     *   버스 로그 전체 조회
     *
     * &amp;lt;p&amp;gt;특정 사용자의 모든 버스 로그를 조회합니다.
     * 각 로그마다 알림 활성화 여부 및 즐겨찾기 여부를 함께 반환합니다.&amp;lt;/p&amp;gt;
     *
     * &amp;lt;p&amp;gt;사용자 또는 관련 데이터가 없을 경우 예외를 발생시킵니다.&amp;lt;/p&amp;gt;
     *
     * @param memberId 사용자 ID
     * @return {@link BusLogAllRes} 리스트
     * @throws GoormBusException 사용자, 알림, 즐겨찾기 데이터가 존재하지 않을 경우
     */
    @Override
    public List&amp;lt;BusLogAllRes&amp;gt; getBusLogAll(String memberId) {
        Member findMember = memberRepository.findById(memberId).orElse(null);
        if (findMember == null)
            throw new GoormBusException(ErrorCode.USER_NOT_EXIST);

        List&amp;lt;BusLogAllRes&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
        result = selectV2(findMember, result);

        log.info(&quot;BusLog 전체 조회 완료: memberId={}, count={}&quot;, memberId, result.size());
        return result;
    }

    /**
     * ⭐ 즐겨찾기 상태 변경
     *
     * &amp;lt;p&amp;gt;버스 로그에 연결된 즐겨찾기 상태를 토글(활성/비활성)합니다.
     * 이미 true 상태면 비활성화하고, false면 활성화합니다.&amp;lt;/p&amp;gt;
     *
     * @param req 즐겨찾기 상태 변경 요청 DTO
     * @throws GoormBusException 버스 로그 또는 즐겨찾기 정보가 존재하지 않을 경우
     */
    @Override
    public void updateBusFavorite(BusFavoriteReq req) {
        BusLog findBusLog = busLogRepository.findById(req.busLogId()).orElse(null);
        if (findBusLog == null)
            throw new GoormBusException(ErrorCode.BUS_LOG_NOT_EXIST);

        BusFavorite findBusFavorite = busFavoriteRepository.findByBusLog(findBusLog)
                .orElseThrow(() -&amp;gt; new GoormBusException(ErrorCode.BUS_FAVORITE_NOT_EXIST));

        if (findBusFavorite.isFavoriteFlag()) {
            findBusFavorite.deactivateIsFavoriteFlag();
        } else {
            findBusFavorite.activateIsFavoriteFlag();
        }
    }


    private List&amp;lt;BusLogAllRes&amp;gt; selectV1(Member findMember, List&amp;lt;BusLogAllRes&amp;gt; result) {
        List&amp;lt;BusLog&amp;gt; busLogs = busLogRepository.findByMember(findMember);

        for (BusLog busLog : busLogs) {
            BusAlarm findBusAlarm = busAlarmRepository.findByBusLog(busLog)
                    .orElseThrow(() -&amp;gt; new GoormBusException(ErrorCode.BUS_ALARM_NOT_EXIST));

            BusFavorite findBusFavorite = busFavoriteRepository.findByBusLog(busLog)
                    .orElseThrow(() -&amp;gt; new GoormBusException(ErrorCode.BUS_FAVORITE_NOT_EXIST));

            result.add(BusLogAllRes.of(
                    busLog,
                    findBusAlarm.isAlarmFlag(),
                    findBusFavorite.isFavoriteFlag()
            ));
        }

        return result;
    }


    private List&amp;lt;BusLogAllRes&amp;gt; selectV2(Member findMember, List&amp;lt;BusLogAllRes&amp;gt; result) {
        List&amp;lt;BusLog&amp;gt; busLogs = busLogRepository.findAllWithAlarmAndFavorite(findMember);

        return busLogs.stream()
                .map(busLog -&amp;gt; BusLogAllRes.of(
                        busLog,
                        busLog.getBusAlarm().isAlarmFlag(),
                        busLog.getBusFavorite().isFavoriteFlag()
                ))
                .toList();

    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;/**
 *   BusAlarmServiceImpl
 *
 * &amp;lt;p&amp;gt;버스 알림 관련 비즈니스 로직을 구현한 클래스입니다.&amp;lt;/p&amp;gt;
 * &amp;lt;ul&amp;gt;
 *   &amp;lt;li&amp;gt;버스 알림 플래그 토글&amp;lt;/li&amp;gt;
 *   &amp;lt;li&amp;gt;Twilio API를 이용한 전화 알림 발송&amp;lt;/li&amp;gt;
 * &amp;lt;/ul&amp;gt;
 */
@Service
@Transactional
@Slf4j
@RequiredArgsConstructor
public class BusAlarmServiceImpl implements BusAlarmService {

    private final BusAlarmRepository busAlarmRepository;
    private final BusLogRepository busLogRepository;

    @Value(&quot;${twilio.account-sid}&quot;)
    private String accountSid;

    @Value(&quot;${twilio.auth-token}&quot;)
    private String authToken;

    @Value(&quot;${twilio.from-number}&quot;)
    private String fromNumber;

    /**
     *   버스 알림 상태 토글
     *
     * &amp;lt;p&amp;gt;해당 버스 로그의 알림 상태를 활성화 &amp;harr; 비활성화로 전환합니다.&amp;lt;/p&amp;gt;
     *
     * @param req 알림 상태 변경 요청 DTO
     * @throws GoormBusException 버스 로그 또는 알림 정보가 존재하지 않을 경우
     */
    @Override
    public void updateBusAlarm(BusAlarmReq req) {
        BusLog findBusLog = busLogRepository.findById(req.busLogId()).orElse(null);
        if (findBusLog == null)
            throw new GoormBusException(ErrorCode.BUS_LOG_NOT_EXIST);

        BusAlarm findBusAlarm = busAlarmRepository.findByBusLog(findBusLog).orElse(null);
        if (findBusAlarm == null)
            throw new GoormBusException(ErrorCode.BUS_ALARM_NOT_EXIST);

        if (findBusAlarm.isAlarmFlag()) {
            findBusAlarm.deactivateIsAlarmFlag();
            log.info(&quot;버스 알림 비활성화: busLogId={}&quot;, req.busLogId());
        } else {
            findBusAlarm.activateIsAlarmFlag();
            log.info(&quot;버스 알림 활성화: busLogId={}&quot;, req.busLogId());
        }
    }

    /**
     *   버스 도착 음성 알림 발송
     *
     * &amp;lt;p&amp;gt;Twilio API를 통해 버스 도착 안내 음성 전화를 사용자에게 발송합니다.&amp;lt;/p&amp;gt;
     *
     * @param member 알림을 받을 사용자
     * @param busLog 버스 운행 로그
     */
    @Override
    public void sendBusArrivalVoiceNotification(Member member, BusLog busLog) {
        Twilio.init(accountSid, authToken);

        String recipientNumber = formatRecipientPhone(member.getPhone());
        String message = buildArrivalMessage(member, busLog);
        String twimlXml = buildTwimlXml(message);

        makeVoiceCall(recipientNumber, twimlXml);
        log.info(&quot;버스 도착 알림 발송 완료: member={}, phone={}&quot;, member.getName(), recipientNumber);
    }

    /**
     * ☎ 전화번호를 국제 표준 형태(+82...)로 변환
     *
     * @param phone 사용자 전화번호
     * @return 국제 표준 형태의 전화번호
     * @throws IllegalArgumentException 전화번호 형식이 잘못된 경우
     */
    private String formatRecipientPhone(String phone) {
        if (phone == null || phone.length() &amp;lt; 2) {
            throw new IllegalArgumentException(&quot;잘못된 전화번호 형식입니다: &quot; + phone);
        }
        return &quot;+82&quot; + phone.substring(1); // 예: 010 &amp;rarr; +8210
    }

    /**
     *   안내 음성 메시지 문자열 생성
     *
     * @param member 사용자 정보
     * @param busLog 버스 운행 로그
     * @return 음성 메시지 내용
     */
    private String buildArrivalMessage(Member member, BusLog busLog) {
        return String.format(
                &quot;안녕하세요, %s님. %s에서 %s로 가는 %s번 버스가 현재 %s 정류장 남았습니다. 서둘러 주세요.&quot;,
                member.getName(),
                busLog.getDeparture(),
                busLog.getDestination(),
                busLog.getNotionId(),
                busLog.getStation()
        );
    }

    /**
     *   Twilio용 TwiML XML 생성
     *
     * @param message 음성 안내 문장
     * @return TwiML XML 문자열
     */
    private String buildTwimlXml(String message) {
        Say say = new Say.Builder(message)
                .language(Say.Language.KO_KR)
                .voice(Say.Voice.ALICE)
                .build();

        VoiceResponse response = new VoiceResponse.Builder()
                .say(say)
                .build();

        return response.toXml();
    }

    /**
     *   Twilio API를 통해 실제 전화를 발신
     *
     * @param recipientNumber 수신자 전화번호
     * @param twimlXml Twilio용 XML 메시지
     */
    private void makeVoiceCall(String recipientNumber, String twimlXml) {
        Twiml twiml = new Twiml(twimlXml);
        Call.creator(
                new PhoneNumber(recipientNumber),
                new PhoneNumber(fromNumber),
                twiml
        ).create();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;172&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;진짜 이전보다 훨씬 깔끔해졌다!!!&amp;nbsp;&lt;br /&gt;&lt;b&gt;모듈화도 잘 되어 있고, 인터페이스도 적절히 활용했으며, 주석 처리도 깔끔해서&lt;/b&gt;&lt;br /&gt;이제는 코드를 한눈에 파악하기가 훨씬 쉽다.&lt;/p&gt;
&lt;p data-end=&quot;172&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;물론 아직 더 세분화할 수 있는 부분도 있지만&amp;hellip;!&lt;br /&gt;시간상 이번에는 여기까지만 하고, 나중에 다시 다듬어볼 예정이다.&lt;/p&gt;
&lt;p data-end=&quot;361&quot; data-start=&quot;174&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;361&quot; data-start=&quot;174&quot; data-ke-size=&quot;size16&quot;&gt;무엇보다 인상 깊었던 건 &lt;b&gt;스케줄러 로직의 개선&lt;/b&gt;이다.&lt;br /&gt;이전에는 &lt;b&gt;버스 로그 생성 API가 완료되면 비동기로 스케줄러를 호출&lt;/b&gt;하도록 짜놨는데,&lt;br /&gt;지금 보니 왜 그렇게 비효율적인 구조를 만들었는지 모르겠다.&lt;/p&gt;
&lt;p data-end=&quot;361&quot; data-start=&quot;174&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;361&quot; data-start=&quot;174&quot; data-ke-size=&quot;size16&quot;&gt;당시엔 스케줄러 코드가 서비스 로직이랑 완전히 뒤섞여 있어서&lt;br /&gt;유지보수가 어렵고, 확장성도 전혀 없었다.&lt;/p&gt;
&lt;p data-end=&quot;440&quot; data-start=&quot;363&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;지금은 그 구조를 완전히 개선해서, &lt;b&gt;스케줄러를 서비스 로직과 분리&lt;/b&gt;하고 아래처럼 훨씬 &lt;b&gt;깔끔하고 직관적인 코드&lt;/b&gt;로 바꿨다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;/**
 *   StationService
 *
 * &amp;lt;p&amp;gt;버스 도착 정보와 알림 발송을 담당하는 스케줄링 서비스입니다.&amp;lt;/p&amp;gt;
 * &amp;lt;ul&amp;gt;
 *   &amp;lt;li&amp;gt;30초마다 버스 도착 여부를 확인하고 음성 알림 발송&amp;lt;/li&amp;gt;
 *   &amp;lt;li&amp;gt;24시간이 지난 알림의 잔여 횟수를 자동으로 초기화&amp;lt;/li&amp;gt;
 * &amp;lt;/ul&amp;gt;
 */
@Service
@EnableScheduling
@Slf4j
@RequiredArgsConstructor
public class StationService {

    private final BusLogRepository busLogRepository;
    private final JejuBusClient jejuBusClient;
    private final BusAlarmRepository busAlarmRepository;
    private final BusAlarmService busAlarmService;

    /**
     *   30초마다 실행되는 버스 도착 알림 스케줄러
     *
     * &amp;lt;p&amp;gt;모든 BusLog를 순회하면서:
     * &amp;lt;ul&amp;gt;
     *     &amp;lt;li&amp;gt;알림이 비활성화된 경우 제외&amp;lt;/li&amp;gt;
     *     &amp;lt;li&amp;gt;잔여 알림 횟수가 0인 경우 제외&amp;lt;/li&amp;gt;
     *     &amp;lt;li&amp;gt;JejuBus API를 통해 도착 정보를 조회 후,
     *     해당 정류장에 도착한 경우 음성 알림 발송 및 잔여 횟수 차감&amp;lt;/li&amp;gt;
     * &amp;lt;/ul&amp;gt;
     */
    @Scheduled(fixedRate = 30000)
    public void callBusScheduler() {
        List&amp;lt;BusLog&amp;gt; findBusLogAll = busLogRepository.findAll();
        for (BusLog busLog : findBusLogAll) {
            BusAlarm findBusAlarm = busAlarmRepository.findByBusLog(busLog)
                    .orElseThrow(() -&amp;gt; new GoormBusException(ErrorCode.BUS_ALARM_NOT_EXIST));

            // 비활성화된 알림은 건너뛰기
            if (!findBusAlarm.isAlarmFlag()) continue;

            // 잔여 횟수가 0이면 알림 스킵
            if (findBusAlarm.getAlarmRemaining() == 0L) continue;

            checkBusArrivalAndNotify(busLog, findBusAlarm);
        }
    }

    /**
     * ⏰ 1분마다 실행되는 알림 초기화 스케줄러
     *
     * &amp;lt;p&amp;gt;버스 로그 생성 이후 24시간이 지난 경우,
     * 알림 잔여 횟수를 2로 초기화합니다.&amp;lt;/p&amp;gt;
     */
    @Scheduled(fixedRate = 60000)
    public void reactivateBusNotification() {
        List&amp;lt;BusLog&amp;gt; findBusLogAll = busLogRepository.findAll();
        for (BusLog busLog : findBusLogAll) {
            BusAlarm findBusAlarm = busAlarmRepository.findByBusLog(busLog)
                    .orElseThrow(() -&amp;gt; new GoormBusException(ErrorCode.BUS_ALARM_NOT_EXIST));

            LocalDateTime createdAt = findBusAlarm.getCreatedAt();
            LocalDateTime now = LocalDateTime.now();

            // 24시간 이상 경과 확인
            if (Duration.between(createdAt, now).toHours() &amp;gt;= 24) {
                if (findBusAlarm.getAlarmRemaining() &amp;lt; 2) {
                    findBusAlarm.initAlarmRemaining();
                    log.info(&quot;버스 알림 잔여 횟수 초기화 완료: busLogId={}&quot;, busLog.getId());
                }
            }
        }
    }

    /**
     *   버스 도착 여부 확인 후 알림 발송
     *
     * &amp;lt;p&amp;gt;JejuBus API로 버스 도착 정보를 조회하고,
     * 현재 사용자의 목표 정류장과 일치하면 음성 알림을 발송합니다.&amp;lt;/p&amp;gt;
     *
     * @param busLog  버스 운행 로그
     * @param busAlarm 해당 로그의 알림 엔티티
     * @throws GoormBusException 제주 버스 API 응답이 유효하지 않을 경우
     */
    private void checkBusArrivalAndNotify(BusLog busLog, BusAlarm busAlarm) {
        ArrivalResponse response = jejuBusClient.getArrivalInfo(busLog.getStationId());
        if (response == null || response.getResultList() == null)
            throw new GoormBusException(ErrorCode.JEJU_RESPONSE_NOT_EXIST);

        response.getResultList().forEach(arrival -&amp;gt; {
            int remainStation = Integer.parseInt(arrival.getRemainStation());
            int busLogStation = Integer.parseInt(String.valueOf(busLog.getStation()));

            // 현재 정류장 도착 시 알림 발송
            if (remainStation == busLogStation) {
                busAlarm.minusAlarmRemaining(); // 잔여 횟수 -1 감소
                busAlarmService.sendBusArrivalVoiceNotification(busLog.getMember(), busLog);
                log.info(&quot;버스 도착 알림 발송 완료: busLogId={}, member={}&quot;, busLog.getId(), busLog.getMember().getName());
            }
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt; 전체적으로 보면 이번 리팩터링은 단순한 코드 수정이 아니라 &lt;b&gt;프로젝트를 완전히 새로 세운 작업&lt;/b&gt;이었다.&lt;/span&gt;&lt;br /&gt;엔티티 설계부터 프로젝트 구조, 코드 구성, 주석 스타일, 응답 포맷까지 전면적으로 손봤다.&lt;br /&gt;특히 ResponseEntity로 통일하고, yml 설정과 PR/이슈 템플릿을 추가하면서 협업 환경도 훨씬 체계적으로 바뀌었다.&lt;br /&gt;그렇게 해서, &lt;b&gt;제주 버스 알림콜 프로젝트는 새로운 시작을 맞이했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;약 1년 전에는 기술적으로 조금이라도 점수를 얻고 싶어서 버스 정보 조회 시 발생할 수 있는 N+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;&lt;br /&gt;그래서 이번엔 처음부터 다시 설계하면서, N+1 문제를 가정한 상황을 직접 재현하고,&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;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;jpa:
  hibernate:
    ddl-auto: none
  properties:
    hibernate:
      show_sql: false              # 기본적으로 SQL 로그는 찍지 않음
      format_sql: true             # 보기 좋게 포매팅 (켜질 때용)
      use_sql_comments: false      # JPQL 주석 비활성화
      generate_statistics: true   # 통계 비활성화 (Hibernate 내부 로그 제거)
      dialect: org.hibernate.dialect.MySQLDialect&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드는 쿼리 로그를 보기 위해 설정을 해두었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package goorm.global.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Slf4j
@Aspect
@Component
public class PerformanceAspect {

    @Around(&quot;execution(* goorm.domain.buslog.application.service..*(..))&quot;)
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Object result = joinPoint.proceed();  // 실제 메서드 실행

        stopWatch.stop();

        String methodName = joinPoint.getSignature().toShortString();
        log.info(&quot;⏱️ [성능측정] {} 실행 시간: {} ms&quot;, methodName, stopWatch.getTotalTimeMillis());

        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드는 Aop 기반의 메서드별 성능 측정 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;package goorm.global.infra.monitoring;

import jakarta.persistence.EntityManagerFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class HibernateStatisticsLogger {

    private final EntityManagerFactory entityManagerFactory;

    /**
     * 버스 관련 API 요청 후 Hibernate 쿼리 통계 출력
     */
    @AfterReturning(&quot;execution(* goorm.domain.buslog.presentation..*(..)) || execution(* goorm.domain.busalarm.presentation..*(..))&quot;)
    public void logHibernateStatistics() {
        SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
        Statistics stats = sessionFactory.getStatistics();

        long selectCount = stats.getQueryExecutionCount();   // 실행된 SELECT 문 수
        long insertCount = stats.getEntityInsertCount();     // INSERT 문 수
        long updateCount = stats.getEntityUpdateCount();     // UPDATE 문 수
        long deleteCount = stats.getEntityDeleteCount();     // DELETE 문 수
        long totalCount = selectCount + insertCount + updateCount + deleteCount;

        log.info(&quot;  [Hibernate 쿼리 통계 요약]&quot;);
        log.info(&quot;───────────────────────────────&quot;);
        log.info(&quot;SELECT 실행 횟수 : {}&quot;, selectCount);
        log.info(&quot;INSERT 실행 횟수 : {}&quot;, insertCount);
        log.info(&quot;UPDATE 실행 횟수 : {}&quot;, updateCount);
        log.info(&quot;DELETE 실행 횟수 : {}&quot;, deleteCount);
        log.info(&quot;총 쿼리 실행 횟수 : {}&quot;, totalCount);
        log.info(&quot;───────────────────────────────&quot;);

        // 요청별 측정을 위해 통계 초기화
        stats.clear();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&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;70&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;각각의 요청마다 &lt;b&gt;최종 실행된 쿼리 수를 확인하기 위해&lt;/b&gt;,&lt;br /&gt;직접 &lt;b&gt;수동으로 빈(Bean)을 추가&lt;/b&gt;해서 구현했다.&lt;/p&gt;
&lt;p data-end=&quot;195&quot; data-start=&quot;72&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;195&quot; data-start=&quot;72&quot; data-ke-size=&quot;size16&quot;&gt;참고로, 이 로직은 &lt;b&gt;Hibernate에서 기본적으로 제공하는 기능&lt;/b&gt;을 활용한 것이다.&lt;/p&gt;
&lt;p data-end=&quot;195&quot; data-start=&quot;72&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;195&quot; data-start=&quot;72&quot; data-ke-size=&quot;size16&quot;&gt;즉, 별도의 라이브러리를 추가하지 않아도&lt;br /&gt;Hibernate의 내부 로깅 기능을 통해 쿼리 실행 횟수를 추적할 수 있다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/**
     * 버스 로그 전체 조회 API
     * - 사용자의 모든 버스 로그를 조회
     * - 각 로그마다 알림 여부, 즐겨찾기 여부 포함
     */
    @Operation(summary = &quot;버스 로그 전체 조회&quot;, description = &quot;사용자의 모든 버스 기록을 조회합니다. (알림 여부, 즐겨찾기 여부 포함)&quot;)
    @ApiResponses(value = {
            @ApiResponse(responseCode = &quot;200&quot;, description = &quot;버스 로그 조회 성공&quot;),
            @ApiResponse(responseCode = &quot;404&quot;, description = &quot;사용자 또는 버스 로그가 존재하지 않음&quot;),
            @ApiResponse(responseCode = &quot;500&quot;, description = &quot;서버 내부 오류&quot;)
    })
    @GetMapping
    public ResponseEntity&amp;lt;List&amp;lt;BusLogAllRes&amp;gt;&amp;gt; getBusLogAll(@RequestParam String memberId,@AuthenticationPrincipal String userId) {
        return ResponseEntity.ok(busLogService.getBusLogAll(memberId));
    }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;422&quot; data-start=&quot;299&quot; data-ke-size=&quot;size16&quot;&gt;이번에 테스트할 API다.&lt;/p&gt;
&lt;p data-end=&quot;422&quot; data-start=&quot;299&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;422&quot; data-start=&quot;299&quot; data-ke-size=&quot;size16&quot;&gt;위 주석에서도 볼 수 있듯이, 버스 로그를 불러오면서&lt;br /&gt;각 버스에는 &lt;b&gt;알림(BusAlarm)과 즐겨찾기(BusFavorite)&lt;/b&gt; 두 개의 연관 엔티티가 매핑되어 있다.&lt;/p&gt;
&lt;p data-end=&quot;528&quot; data-start=&quot;429&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;528&quot; data-start=&quot;429&quot; data-ke-size=&quot;size16&quot;&gt;참고로 @RequestParam String memberId는 &lt;b&gt;k6 부하 테스트를 위해 임시로 추가한 코드&lt;/b&gt;다.&lt;br /&gt;실제 서비스 코드에는 포함되지 않는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;/**
 *   버스 로그 전체 조회
 *
 * &amp;lt;p&amp;gt;특정 사용자의 모든 버스 로그를 조회합니다.
 * 각 로그마다 알림 활성화 여부 및 즐겨찾기 여부를 함께 반환합니다.&amp;lt;/p&amp;gt;
 *
 * &amp;lt;p&amp;gt;사용자 또는 관련 데이터가 없을 경우 예외를 발생시킵니다.&amp;lt;/p&amp;gt;
 *
 * @param memberId 사용자 ID
 * @return {@link BusLogAllRes} 리스트
 * @throws GoormBusException 사용자, 알림, 즐겨찾기 데이터가 존재하지 않을 경우
 */
@Override
public List&amp;lt;BusLogAllRes&amp;gt; getBusLogAll(String memberId) {
    Member findMember = memberRepository.findById(memberId).orElse(null);
    if (findMember == null)
        throw new GoormBusException(ErrorCode.USER_NOT_EXIST);

    List&amp;lt;BusLogAllRes&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
    List&amp;lt;BusLog&amp;gt; busLogs = busLogRepository.findByMember(findMember);

    for (BusLog busLog : busLogs) {
        BusAlarm findBusAlarm = busAlarmRepository.findByBusLog(busLog)
                .orElseThrow(() -&amp;gt; new GoormBusException(ErrorCode.BUS_ALARM_NOT_EXIST));

        BusFavorite findBusFavorite = busFavoriteRepository.findByBusLog(busLog)
                .orElseThrow(() -&amp;gt; new GoormBusException(ErrorCode.BUS_FAVORITE_NOT_EXIST));

        result.add(BusLogAllRes.of(
                busLog,
                findBusAlarm.isAlarmFlag(),
                findBusFavorite.isFavoriteFlag()
        ));
    }

    log.info(&quot;BusLog 전체 조회 완료: memberId={}, count={}&quot;, memberId, result.size());
    return result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;letter-spacing: 0px;&quot;&gt;테스트를 진행할 서비스 코드다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로 보기에도 이미 성능적으로 문제가 많아 보이지만, 직접 실행해보면서 어느 정도의 쿼리 부하가 발생하는지 확인해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;p data-ke-size=&quot;size16&quot;&gt;일단 실제 트래픽은 아니지만, 문제 상황을 가정하자.&lt;br /&gt;사용자 한 명이 &lt;b&gt;버스 로그 1,000건을 조회&lt;/b&gt;한다고 했을 때,&lt;/p&gt;
&lt;pre id=&quot;code_1759795804919&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-10-07T09:09:00.412+09:00 DEBUG 37124 --- [GoormBus-prod] [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        bl1_0.id,
        bl1_0.created_at,
        bl1_0.departure,
        bl1_0.destination,
        bl1_0.member_id,
        bl1_0.notion_id,
        bl1_0.station,
        bl1_0.station_id 
    from
        bus_log bl1_0 
    where
        bl1_0.member_id=?&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;버스 로그 데이터 1,000개를 가져오는 1개의 쿼리&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1759795839456&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-10-07T09:09:00.504+09:00 DEBUG 37124 --- [GoormBus-prod] [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        ba1_0.id,
        ba1_0.alarm_remaining,
        ba1_0.bus_log_id,
        ba1_0.created_at,
        ba1_0.is_alarm_flag 
    from
        bus_alarm ba1_0 
    where
        ba1_0.bus_log_id=?
2025-10-07T09:09:00.504+09:00 TRACE 37124 --- [GoormBus-prod] [nio-8080-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) &amp;lt;- [1]
2025-10-07T09:09:00.517+09:00 DEBUG 37124 --- [GoormBus-prod] [nio-8080-exec-2] org.hibernate.SQL                        : 
    select
        bf1_0.id,
        bf1_0.bus_log_id,
        bf1_0.created_at,
        bf1_0.is_favorite_flag 
    from
        bus_favorite bf1_0 
    where
        bf1_0.bus_log_id=?
2025-10-07T09:09:00.517+09:00 TRACE 37124 --- [GoormBus-prod] [nio-8080-exec-2] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) &amp;lt;- [1]&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;352&quot; data-start=&quot;205&quot; data-ke-size=&quot;size16&quot;&gt;binding parameter를 참고하면 위 로그가 1000개씩 2세트로 있다.&lt;/p&gt;
&lt;p data-end=&quot;352&quot; data-start=&quot;205&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;352&quot; data-start=&quot;205&quot; data-ke-size=&quot;size16&quot;&gt;최종적으로 현재 구조에서는 &lt;b&gt;위 로그처럼 연관된 알림(BusAlarm)과 즐겨찾기(BusFavorite)를 개별 조회&lt;/b&gt;하게 된다.&lt;/p&gt;
&lt;p data-end=&quot;358&quot; data-start=&quot;354&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;424&quot; data-start=&quot;359&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;381&quot; data-start=&quot;359&quot;&gt;&lt;b&gt;버스 로그 조회 쿼리 1회&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;424&quot; data-start=&quot;382&quot;&gt;&lt;b&gt;각 로그마다 연관 엔티티 조회 쿼리 2회 (알림 + 즐겨찾기)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1 + 2N 문제가 발생하게 된다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1617&quot; data-origin-height=&quot;273&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDrQHB/btsQ33ENtMx/NhGvfGFhA82HyfdllQT840/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDrQHB/btsQ33ENtMx/NhGvfGFhA82HyfdllQT840/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDrQHB/btsQ33ENtMx/NhGvfGFhA82HyfdllQT840/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDrQHB%2FbtsQ33ENtMx%2FNhGvfGFhA82HyfdllQT840%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;145&quot; data-origin-width=&quot;1617&quot; data-origin-height=&quot;273&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt; 터미널 로그를 확인해보니, 쿼리 개수가 무려 &lt;b&gt;2,001개&lt;/b&gt;나 찍혔다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;실행 시간은 &lt;b&gt;10,835ms&lt;/b&gt;, 즉 거의 &lt;b&gt;10초&lt;/b&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;단순히 데이터를 불러오는 데 이 정도 시간이 걸린다는 건, N+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;a href=&quot;https://balhae.tistory.com/307&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://balhae.tistory.com/307&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1759933139270&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;N+1문제&quot; data-og-description=&quot;https://balhae.tistory.com/156 섹션1 영속성 관리 - 내부 동작 방식최코딩의 개발 섹션1 영속성 관리 - 내부 동작 방식 본문 JPA/JPA 기본 핵심 원리 섹션1 영속성 관리 - 내부 동작 방식 seung_ho_choi.s 2024. 1. 2&quot; data-og-host=&quot;balhae.tistory.com&quot; data-og-source-url=&quot;https://balhae.tistory.com/307&quot; data-og-url=&quot;https://balhae.tistory.com/307&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fpQ4N/hyZKB6sBAz/i5GPGdz7jFvkEjpyNAiZLK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bDs05v/hyZKfcisPa/NkgpfNvCSapl3TWe0108uk/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/b8RS3R/hyZKwFEsp6/CuAPMs18u76VnJB6xr47H0/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440&quot;&gt;&lt;a href=&quot;https://balhae.tistory.com/307&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://balhae.tistory.com/307&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fpQ4N/hyZKB6sBAz/i5GPGdz7jFvkEjpyNAiZLK/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/bDs05v/hyZKfcisPa/NkgpfNvCSapl3TWe0108uk/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/b8RS3R/hyZKwFEsp6/CuAPMs18u76VnJB6xr47H0/img.jpg?width=1080&amp;amp;height=1440&amp;amp;face=0_0_1080_1440');&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;N+1문제&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://balhae.tistory.com/156 섹션1 영속성 관리 - 내부 동작 방식최코딩의 개발 섹션1 영속성 관리 - 내부 동작 방식 본문 JPA/JPA 기본 핵심 원리 섹션1 영속성 관리 - 내부 동작 방식 seung_ho_choi.s 2024. 1. 2&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;balhae.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-end=&quot;94&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;94&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;필자의 CS 블로그에서도 다뤘듯이, &lt;b&gt;N+1 문제를 해결하는 방법은 여러 가지&lt;/b&gt;가 있다.&lt;br /&gt;그중에서도 가장 대표적인 방식이 바로 &lt;b&gt;Fetch Join&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;172&quot; data-start=&quot;96&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 이 &lt;b&gt;Fetch Join을 적용해서&lt;/b&gt;&lt;br /&gt;앞서 발생했던 과도한 쿼리 실행 문제를 어떻게 개선할 수 있는지 직접 확인해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    @Query(&quot;&quot;&quot;
    SELECT DISTINCT b
    FROM BusLog b
    LEFT JOIN FETCH b.busAlarm
    LEFT JOIN FETCH b.busFavorite
    WHERE b.member = :member
&quot;&quot;&quot;)
    List&amp;lt;BusLog&amp;gt; findAllWithAlarmAndFavorite(@Param(&quot;member&quot;) Member member);

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch Join을 활용해 &lt;b&gt;조인과 동시에 연관된 엔티티를 함께 조회&lt;/b&gt;하는 방식을 사용했다.&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;LEFT JOIN FETCH 구문을 통해 &lt;b&gt;BusLog와 연관된 BusAlarm, BusFavorite 엔티티를 한 번에 가져오도록&lt;/b&gt; 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private List&amp;lt;BusLogAllRes&amp;gt; selectV1(Member findMember, List&amp;lt;BusLogAllRes&amp;gt; result) {
    List&amp;lt;BusLog&amp;gt; busLogs = busLogRepository.findByMember(findMember);

    for (BusLog busLog : busLogs) {
        BusAlarm findBusAlarm = busAlarmRepository.findByBusLog(busLog)
                .orElseThrow(() -&amp;gt; new GoormBusException(ErrorCode.BUS_ALARM_NOT_EXIST));

        BusFavorite findBusFavorite = busFavoriteRepository.findByBusLog(busLog)
                .orElseThrow(() -&amp;gt; new GoormBusException(ErrorCode.BUS_FAVORITE_NOT_EXIST));

        result.add(BusLogAllRes.of(
                busLog,
                findBusAlarm.isAlarmFlag(),
                findBusFavorite.isFavoriteFlag()
        ));
    }

    return result;
}


private List&amp;lt;BusLogAllRes&amp;gt; selectV2(Member findMember, List&amp;lt;BusLogAllRes&amp;gt; result) {
    List&amp;lt;BusLog&amp;gt; busLogs = busLogRepository.findAllWithAlarmAndFavorite(findMember);

    return busLogs.stream()
            .map(busLog -&amp;gt; BusLogAllRes.of(
                    busLog,
                    busLog.getBusAlarm().isAlarmFlag(),
                    busLog.getBusFavorite().isFavoriteFlag()
            ))
            .toList();

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;selectV1이 기존 코드이고, selectV2가 &lt;b&gt;Fetch Join을 적용한 서비스 코드&lt;/b&gt;다.&lt;br /&gt;이제 두 버전의 성능 차이가 어떻게 나타날지 결과가 정말 기대된다.&lt;/p&gt;
&lt;pre id=&quot;code_1759796534302&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-10-07T09:21:27.074+09:00 DEBUG 22516 --- [GoormBus-prod] [nio-8080-exec-3] org.hibernate.SQL                        : 
    select
        distinct bl1_0.id,
        ba1_0.id,
        ba1_0.alarm_remaining,
        ba1_0.created_at,
        ba1_0.is_alarm_flag,
        bf1_0.id,
        bf1_0.created_at,
        bf1_0.is_favorite_flag,
        bl1_0.created_at,
        bl1_0.departure,
        bl1_0.destination,
        bl1_0.member_id,
        bl1_0.notion_id,
        bl1_0.station,
        bl1_0.station_id 
    from
        bus_log bl1_0 
    left join
        bus_alarm ba1_0 
            on bl1_0.id=ba1_0.bus_log_id 
    left join
        bus_favorite bf1_0 
            on bl1_0.id=bf1_0.bus_log_id 
    where
        bl1_0.member_id=?
2025-10-07T09:21:27.075+09:00 TRACE 22516 --- [GoormBus-prod] [nio-8080-exec-3] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) &amp;lt;- [ea45ebca-ce2a-4abe-8b8c-470699637be5]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1613&quot; data-origin-height=&quot;256&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFagmR/btsQ4oPryy7/6ehhPBXvc8JKkQyOBNs1f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFagmR/btsQ4oPryy7/6ehhPBXvc8JKkQyOBNs1f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFagmR/btsQ4oPryy7/6ehhPBXvc8JKkQyOBNs1f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFagmR%2FbtsQ4oPryy7%2F6ehhPBXvc8JKkQyOBNs1f1%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;1613&quot; height=&quot;256&quot; data-origin-width=&quot;1613&quot; data-origin-height=&quot;256&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;쿼리 로그와 터미널 결과를 보면, &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이번에는 &lt;/span&gt;&lt;b&gt;쿼리가 단 1회만 실행&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;된 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;34c6533c-8c2d-4e3f-b89f-b1b944222b99&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;133&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;무엇보다 실행 시간도 &lt;b&gt;이전 10,835ms에서 50ms로&lt;/b&gt;, 비교할 수 없을 만큼 &lt;b&gt;압도적인 성능 향상&lt;/b&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;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-origin-width=&quot;1629&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zI3gi/btsQ4VTNm4H/x8u3ibem9eCIZHUPA61kT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zI3gi/btsQ4VTNm4H/x8u3ibem9eCIZHUPA61kT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zI3gi/btsQ4VTNm4H/x8u3ibem9eCIZHUPA61kT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzI3gi%2FbtsQ4VTNm4H%2Fx8u3ibem9eCIZHUPA61kT1%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;126&quot; data-origin-width=&quot;1629&quot; data-origin-height=&quot;239&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;10,000개를 기준으로 조회하면 475ms가 소요된다.&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-end=&quot;378&quot; data-start=&quot;271&quot; data-ke-size=&quot;size16&quot;&gt;Fetch Join으로 &lt;b&gt;쿼리 수를 최적화&lt;/b&gt;하긴 했지만, 여전히 &lt;b&gt;조회 속도&lt;/b&gt; 측면에서는 개선의 여지가 있었다.&lt;br /&gt;특히 버스 로그가 수백만 건인 경우, member_id를 기준으로 검색할 때 &lt;b&gt;탐색 범위가 너무 넓어지는&lt;/b&gt; 문제가 있었다.&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;271&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759934293613&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Table(
    name = &quot;bus_log&quot;,
    indexes = {
        @Index(name = &quot;idx_member_id&quot;, columnList = &quot;member_id&quot;)
    }
)
public class BusLog {
    ...
}&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가 member_id 컬럼을 기준으로 &lt;b&gt;B-Tree 구조로 데이터를 정렬하여 관리&lt;/b&gt;하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 MySQL은 B+Tree 으로 구현되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세히 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버스&amp;nbsp;로그&amp;nbsp;테이블에&amp;nbsp;데이터가&amp;nbsp;쭉&amp;nbsp;쌓여&amp;nbsp;있다고&amp;nbsp;하자.&amp;nbsp;이때&amp;nbsp;특정&amp;nbsp;memberId에&amp;nbsp;해당하는&amp;nbsp;로그를&amp;nbsp;찾아야&amp;nbsp;하는데,&amp;nbsp;인덱스가&amp;nbsp;없다면&amp;nbsp;데이터&amp;nbsp;전체를&amp;nbsp;훑는&amp;nbsp;풀&amp;nbsp;스캔(Full&amp;nbsp;Scan)&amp;nbsp;이&amp;nbsp;발생해&amp;nbsp;성능이&amp;nbsp;저하된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이를&amp;nbsp;해결하기&amp;nbsp;위해&amp;nbsp;인덱스(index)&amp;nbsp;를&amp;nbsp;도입하면,&amp;nbsp;memberId&amp;nbsp;기준으로&amp;nbsp;정렬된&amp;nbsp;별도의&amp;nbsp;탐색&amp;nbsp;구조가&amp;nbsp;만들어지고,&amp;nbsp;&lt;b&gt;DB는&amp;nbsp;이진&amp;nbsp;탐색(Binary&amp;nbsp;Search)&amp;nbsp;기법을&amp;nbsp;사용해&amp;nbsp;중간에서&amp;nbsp;데이터를&amp;nbsp;빠르게&amp;nbsp;찾아간다.&lt;/b&gt; &lt;br /&gt;&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;증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를&amp;nbsp;개선한&amp;nbsp;구조가&amp;nbsp;B-Tree다. &lt;br /&gt;&lt;b&gt;B-Tree는&amp;nbsp;한&amp;nbsp;노드에&amp;nbsp;여러&amp;nbsp;개의&amp;nbsp;키를&amp;nbsp;저장하여,&amp;nbsp;탐색&amp;nbsp;시&amp;nbsp;데이터를&amp;nbsp;반씩이&amp;nbsp;아니라&amp;nbsp;3분의&amp;nbsp;2씩&amp;nbsp;줄여나가는&amp;nbsp;식으로&amp;nbsp;더&amp;nbsp;빠르게&amp;nbsp;찾을&amp;nbsp;수&amp;nbsp;있다. &lt;/b&gt;&lt;br /&gt;&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;&lt;b&gt;상위&amp;nbsp;노드에는&amp;nbsp;탐색을&amp;nbsp;위한&amp;nbsp;가이드&amp;nbsp;역할만&amp;nbsp;남긴&amp;nbsp;구조가&amp;nbsp;바로&amp;nbsp;B+Tree다.&lt;/b&gt; &lt;br /&gt;이&amp;nbsp;구조는&amp;nbsp;&lt;b&gt;범위&amp;nbsp;검색(range&amp;nbsp;scan)&lt;/b&gt;&amp;nbsp;에&amp;nbsp;매우&amp;nbsp;효율적이다. &lt;br /&gt;&lt;br /&gt;즉,&amp;nbsp;인덱스를&amp;nbsp;생성하면&amp;nbsp;테이블과는&amp;nbsp;별도로&amp;nbsp;memberId만&amp;nbsp;모아둔&amp;nbsp;인덱스&amp;nbsp;공간이&amp;nbsp;생기고, &lt;br /&gt;DB는&amp;nbsp;먼저&amp;nbsp;인덱스에서&amp;nbsp;해당&amp;nbsp;키를&amp;nbsp;찾은&amp;nbsp;뒤,&amp;nbsp;거기에&amp;nbsp;연결된&amp;nbsp;실제&amp;nbsp;테이블의&amp;nbsp;행(row)을&amp;nbsp;가져오는&amp;nbsp;방식으로&amp;nbsp;데이터를&amp;nbsp;탐색한다. &lt;br /&gt;&lt;br /&gt;정리하자면, &lt;br /&gt;&lt;br /&gt;pk가 primary로 선언된 기본 키는 자동으로 클러스터드 인덱스(Clustered Index) 가 된다. &lt;br /&gt;&lt;br /&gt;이를&amp;nbsp;보조하기&amp;nbsp;위해&amp;nbsp;만든&amp;nbsp;인덱스가&amp;nbsp;비클러스터&amp;nbsp;인덱스(Non-Clustered&amp;nbsp;Index,&amp;nbsp;보조&amp;nbsp;인덱스)&amp;nbsp;다. &lt;br /&gt;&lt;br /&gt;이번&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bduHTw/btsQ7RcgHQE/ecWKtBs35R2kqNnrmaBC81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bduHTw/btsQ7RcgHQE/ecWKtBs35R2kqNnrmaBC81/img.png&quot; data-alt=&quot;클러스터 인덱스와 보조 인덱스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bduHTw/btsQ7RcgHQE/ecWKtBs35R2kqNnrmaBC81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbduHTw%2FbtsQ7RcgHQE%2FecWKtBs35R2kqNnrmaBC81%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;1468&quot; height=&quot;286&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클러스터 인덱스와 보조 인덱스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 덕분에 특정 회원의 로그를 조회할 때, &lt;b&gt;전체 테이블을 스캔하지 않고도 필요한 데이터만 빠르게 탐색&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-5&quot; data-message-id=&quot;e2a0e2a1-647d-401a-8e9c-43919262ebd6&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-end=&quot;73&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;인덱스는 필요할 때만 사용하는 것이 중요하다.&lt;/b&gt;&lt;br /&gt;무분별하게 남발하면 오히려 &lt;b&gt;성능 저하&lt;/b&gt;를 초래할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;73&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;182&quot; data-start=&quot;75&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 조회 속도를 빠르게 해주는 대신, &lt;b&gt;데이터 삽입&amp;middot;수정&amp;middot;삭제 시마다 인덱스도 함께 갱신&lt;/b&gt;되어야 하기 때문에&lt;/p&gt;
&lt;p data-end=&quot;182&quot; data-start=&quot;75&quot; data-ke-size=&quot;size16&quot;&gt;쓰기 작업이 잦은 테이블에서는 &lt;b&gt;오버헤드가 커질 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;182&quot; data-start=&quot;75&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;254&quot; data-start=&quot;184&quot; data-ke-size=&quot;size16&quot;&gt;즉, 자주 조회되는 컬럼이나 검색 조건으로 자주 쓰이는 컬럼에만 &lt;b&gt;선별적으로 인덱스를 적용&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;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-origin-width=&quot;1620&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fk7Xw/btsQ2dohTa8/Whz6TIbSQ6BXVO874Zjj70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fk7Xw/btsQ2dohTa8/Whz6TIbSQ6BXVO874Zjj70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fk7Xw/btsQ2dohTa8/Whz6TIbSQ6BXVO874Zjj70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFk7Xw%2FbtsQ2dohTa8%2FWhz6TIbSQ6BXVO874Zjj70%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;133&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;250&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-end=&quot;80&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;인덱스를 적용한 후 실행 결과는 &lt;b&gt;429ms&lt;/b&gt;로, 이전의 &lt;b&gt;475ms&lt;/b&gt;보다 약간 개선되긴 했지만&lt;br /&gt;체감할 만큼 큰 차이는 아니었다.&lt;/p&gt;
&lt;p data-end=&quot;80&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;171&quot; data-start=&quot;82&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 &lt;b&gt;K6을 활용해 동시 사용자 부하 테스트&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;K6 부하 테스트&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import http from 'k6/http';
import { check, sleep } from 'k6';

// 실제 DB에 저장된 member UUID 리스트 (새로운 100개)
const memberIds = [
    100개 있음 (생략)
];

export const options = {
    vus: 100, // 동시 접속 사용자 수
    duration: '30s', // 테스트 시간
    thresholds: {
        http_req_duration: ['p(95)&amp;lt;500'], // 95% 요청이 0.5초 이내에 끝나야 함
    },
};

export default function () {
    const memberId = memberIds[Math.floor(Math.random() * memberIds.length)];
    const url = `http://localhost:8080/api/bus?memberId=${memberId}`;
    const res = http.get(url);

    check(res, {
        'status is 200': (r) =&amp;gt; r.status === 200,
    });

    sleep(1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-end=&quot;74&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;choco로 K6을 설치한 뒤, 프로젝트 내에 per.js 파일을 작성하고 k6 run per.js 명령어로 실행했다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;241&quot; data-start=&quot;76&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;266&quot; data-start=&quot;179&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;시나리오&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;266&quot; data-start=&quot;179&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot + JPA 기반 프로젝트의 /api/bus API에&lt;br /&gt;DB 인덱스를 적용하기 전후로 K6를 사용해 부하 테스트를 진행했습니다.&lt;/p&gt;
&lt;p data-end=&quot;285&quot; data-start=&quot;268&quot; data-ke-size=&quot;size16&quot;&gt;테스트 환경은 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;423&quot; data-start=&quot;287&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;312&quot; data-start=&quot;287&quot;&gt;&lt;b&gt;동시 사용자(VUs):&lt;/b&gt; 100명&lt;/li&gt;
&lt;li data-end=&quot;334&quot; data-start=&quot;313&quot;&gt;&lt;b&gt;테스트 지속시간:&lt;/b&gt; 30초&lt;/li&gt;
&lt;li data-end=&quot;359&quot; data-start=&quot;335&quot;&gt;&lt;b&gt;총 요청 수:&lt;/b&gt; 약 3,000건&lt;/li&gt;
&lt;li data-end=&quot;377&quot; data-start=&quot;360&quot;&gt;&lt;b&gt;테스트 툴:&lt;/b&gt; k6&lt;/li&gt;
&lt;li data-end=&quot;423&quot; data-start=&quot;378&quot;&gt;&lt;b&gt;API 엔드포인트:&lt;/b&gt; /api/bus?memberId={uuid}&lt;/li&gt;
&lt;li data-end=&quot;423&quot; data-start=&quot;378&quot;&gt;&lt;b&gt;DB Connection Pool&lt;/b&gt;: 10&lt;/li&gt;
&lt;li data-end=&quot;423&quot; data-start=&quot;378&quot;&gt;&lt;b&gt;Connection Timeout&lt;/b&gt;: 2000ms&lt;/li&gt;
&lt;li data-end=&quot;423&quot; data-start=&quot;378&quot;&gt;&lt;b&gt;측정 환경&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;: 로컬 환경(&lt;b&gt;원래는 실제 배포 환경에서 해야된다...!&lt;/b&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;241&quot; data-start=&quot;76&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;749&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9bDRq/btsQ2wOO7EZ/eFpzETKnalkX7j6NXMiUzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9bDRq/btsQ2wOO7EZ/eFpzETKnalkX7j6NXMiUzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9bDRq/btsQ2wOO7EZ/eFpzETKnalkX7j6NXMiUzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9bDRq%2FbtsQ2wOO7EZ%2FeFpzETKnalkX7j6NXMiUzK%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;1118&quot; height=&quot;749&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;749&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IIki0/btsQ2iDhD4a/kM5YmUyJ2vCF1WYykdOmKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IIki0/btsQ2iDhD4a/kM5YmUyJ2vCF1WYykdOmKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IIki0/btsQ2iDhD4a/kM5YmUyJ2vCF1WYykdOmKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIIki0%2FbtsQ2iDhD4a%2FkM5YmUyJ2vCF1WYykdOmKK%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;1115&quot; height=&quot;780&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진은 &lt;b&gt;K6 부하 테스트 결과&lt;/b&gt;를 보여준다.&lt;br /&gt;왼쪽은 &lt;b&gt;인덱스 도입 전&lt;/b&gt;, 오른쪽은 &lt;b&gt;인덱스 도입 후&lt;/b&gt;의 모습이다.&lt;br /&gt;두 결과를 비교하면, 인덱스 적용이 실제 트래픽 환경에서 &lt;b&gt;요청 처리 속도와 안정성에 어떤 영향을 주었는지&lt;/b&gt; 한눈에 확인할 수 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;787&quot; data-start=&quot;446&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;평균 응답시간(avg)&lt;/td&gt;
&lt;td&gt;95% 응답시간(p95)&lt;/td&gt;
&lt;td&gt;최대 응답시간(max)&lt;/td&gt;
&lt;td&gt;요청 수&lt;/td&gt;
&lt;td&gt;성공률&lt;/td&gt;
&lt;td&gt;초당 처리량(RPS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;698&quot; data-start=&quot;627&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;642&quot; data-start=&quot;627&quot;&gt;&lt;b&gt;인덱스 적용 전&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;652&quot; data-start=&quot;642&quot;&gt;32.21ms&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;662&quot; data-start=&quot;652&quot;&gt;25.27ms&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;673&quot; data-start=&quot;662&quot;&gt;685.49ms&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;681&quot; data-start=&quot;673&quot;&gt;2,955&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;688&quot; data-start=&quot;681&quot;&gt;100%&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;698&quot; data-start=&quot;688&quot;&gt;95.3/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;787&quot; data-start=&quot;699&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;714&quot; data-start=&quot;699&quot;&gt;&lt;b&gt;인덱스 적용 후&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;728&quot; data-start=&quot;714&quot;&gt;&lt;b&gt;30.48ms&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;742&quot; data-start=&quot;728&quot;&gt;&lt;b&gt;23.05ms&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;757&quot; data-start=&quot;742&quot;&gt;&lt;b&gt;662.02ms&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;765&quot; data-start=&quot;757&quot;&gt;2,982&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;772&quot; data-start=&quot;765&quot;&gt;100%&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;787&quot; data-start=&quot;772&quot;&gt;&lt;b&gt;96.17/s&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과를 정리해보면 위 표와 같다.&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1083&quot; data-start=&quot;807&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;변화&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;895&quot; data-start=&quot;847&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;857&quot; data-start=&quot;847&quot;&gt;평균 응답속도&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;874&quot; data-start=&quot;857&quot;&gt;⬇️ &lt;b&gt;5.4% 개선&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;895&quot; data-start=&quot;874&quot;&gt;전체 쿼리 처리속도가 더 빨라짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;949&quot; data-start=&quot;896&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;907&quot; data-start=&quot;896&quot;&gt;95% 응답속도&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;924&quot; data-start=&quot;907&quot;&gt;⬇️ &lt;b&gt;8.8% 개선&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;949&quot; data-start=&quot;924&quot;&gt;대부분의 요청이 더 짧은 시간 내 완료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;999&quot; data-start=&quot;950&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;960&quot; data-start=&quot;950&quot;&gt;최대 응답시간&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;977&quot; data-start=&quot;960&quot;&gt;⬇️ &lt;b&gt;3.4% 개선&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;999&quot; data-start=&quot;977&quot;&gt;부하 상황에서도 응답 안정성 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1049&quot; data-start=&quot;1000&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1012&quot; data-start=&quot;1000&quot;&gt;초당 요청 처리량&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1029&quot; data-start=&quot;1012&quot;&gt;⬆️ &lt;b&gt;0.9% 증가&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1049&quot; data-start=&quot;1029&quot;&gt;서버의 동시 처리 여유가 생김&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1083&quot; data-start=&quot;1050&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1056&quot; data-start=&quot;1050&quot;&gt;성공률&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1070&quot; data-start=&quot;1056&quot;&gt;✅ 동일 (100%)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1083&quot; data-start=&quot;1070&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;분석은 위와 같다.&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_1759935107639&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;응답속도(ms)
|
|       인덱스 전  avg 32.21 ────────
|       인덱스 후  avg 30.48 ──────
|
|----|----|----|----|----|----|----|----&amp;gt;
      0   10   20   30   40   50 (ms)&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;시각화를 해본 그래프이다.&amp;nbsp;&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;b&gt;최종&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;83&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;5~10% 정도의 개선은 겉보기에는 큰 변화가 아닌 것처럼 느껴질 수 있다. 하지만 &lt;b&gt;시스템 레벨에서 보면 이는 매우 의미 있는 차이&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-end=&quot;212&quot; data-start=&quot;85&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;212&quot; data-start=&quot;85&quot; data-ke-size=&quot;size16&quot;&gt;단일 요청 기준으로 &lt;b&gt;2ms가 단축&lt;/b&gt;되었다고 가정하더라도, &lt;b&gt;초당 100건의 요청&lt;/b&gt;, &lt;b&gt;하루 800만 건의 트래픽&lt;/b&gt;이 처리되는 시스템에서는&lt;br /&gt;결국 하루 기준 &lt;b&gt;약 4시간 이상이 절약되는 효과&lt;/b&gt;가 발생한다.&lt;/p&gt;
&lt;p data-end=&quot;381&quot; data-start=&quot;214&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;381&quot; data-start=&quot;214&quot; data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;DB 인덱스의 효율은 선형적으로 증가하지 않는다. &lt;/b&gt;데이터가 쌓일수록 인덱스를 사용한 &lt;b&gt;탐색 속도 이득은 지수적으로 커진다.&lt;/b&gt;&lt;br /&gt;지금은 데이터량이 적어 체감이 크지 않지만, &lt;b&gt;100만 건 이상의 데이터가 누적되는 시점부터는 10배 이상의 성능 차이&lt;/b&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;561&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;561&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;인덱스는 단순히 &lt;b&gt;조회 속도만 개선하는 것이 아니다. &lt;/b&gt;&lt;b&gt;CPU 효율 향상&lt;/b&gt;과 &lt;b&gt;Lock 경합 완화&lt;/b&gt;에도 직접적인 영향을 준다.&lt;br /&gt;전체 테이블 스캔(Table Scan)을 피하면서 &lt;b&gt;DB Connection 점유 시간이 감소&lt;/b&gt;하고, 그만큼 더 많은 요청을 &lt;b&gt;병렬로 처리할 수 있게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;696&quot; data-start=&quot;563&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;696&quot; data-start=&quot;563&quot; data-ke-size=&quot;size16&quot;&gt;결론적으로, 인덱스 적용을 통해 &lt;b&gt;평균 5~9%의 응답 속도 향상&lt;/b&gt;과 &lt;b&gt;처리 안정성 확보&lt;/b&gt;가 이루어졌다. 지금은 미세한 수치 변화처럼 보이지만, &lt;b&gt;데이터가 커질수록 쿼리 탐색 성능 향상 폭은 훨씬 커질 것&lt;/b&gt;으로 기대된다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;700&quot; data-start=&quot;581&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;이번 테스트를 통해 얻은 인사이트들은 &lt;b&gt;붐빔(Boombim) 프로젝트&lt;/b&gt;에도 꼭 적용해봐야겠다.&lt;br /&gt;현재 붐빔에서는 &lt;b&gt;서울 실시간 인구 데이터 OpenAPI&lt;/b&gt;를 통해 약 &lt;b&gt;120개의 장소 정보를 조회&lt;/b&gt;하고 있는데,&lt;br /&gt;이 데이터를 &lt;b&gt;지도를 열 때마다 매번 호출&lt;/b&gt;하다 보니 트래픽이 증가할수록 &lt;b&gt;성능 저하&lt;/b&gt;가 발생할 가능성이 크다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;285&quot; data-start=&quot;199&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;285&quot; data-start=&quot;199&quot; data-ke-size=&quot;size16&quot;&gt;다음에는 이 부분에 &lt;b&gt;캐싱 로직을 도입&lt;/b&gt;해서 API 호출 빈도를 줄이고, 실제 사용자 환경에서의 &lt;b&gt;반응 속도 개선&lt;/b&gt;에도 도전해볼 생각이다.&lt;/p&gt;</description>
      <category>개발 팁</category>
      <author>seung_ho_choi.s</author>
      <guid isPermaLink="true">https://balhae.tistory.com/335</guid>
      <comments>https://balhae.tistory.com/335#entry335comment</comments>
      <pubDate>Thu, 9 Oct 2025 00:02:30 +0900</pubDate>
    </item>
  </channel>
</rss>