<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Good Developer</title>
    <link>https://seungwontech.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 14 Jun 2026 17:39:35 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>seungwonlee</managingEditor>
    <image>
      <title>Good Developer</title>
      <url>https://tistory1.daumcdn.net/tistory/5408353/attach/e26430ad3f6e4d959acdd6a6e5d775e8</url>
      <link>https://seungwontech.tistory.com</link>
    </image>
    <item>
      <title>왜 변경이 잦은 컬럼에는 인덱스가 독이 될 수 있을까</title>
      <link>https://seungwontech.tistory.com/128</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데이터베이스 성능 최적화를 공부하다 보면 &quot;수정(UPDATE)과 삭제(DELETE)가 잦은 컬럼에는 인덱스를 신중하게 걸어야 한다&quot;는 조언을 자주 듣게 됩니다. 왜 그럴까요? 단순히 인덱스를 관리하는 비용 때문일까요?&lt;/span&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;오늘은 MySQL InnoDB 엔진의 내부 동작 방식을 통해, 실제 데이터는 10만 건인데 인덱스만 비대해져 성능이 갉아먹히는 이유를 '책의 색인' 비유와 함께 파헤쳐 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 핵심 개념: DB는 즉시 삭제하지 않는다 (Ghost Record)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데이터베이스는 DELETE나 UPDATE 명령이 들어왔을 때, 해당 데이터를 즉시 물리적으로 지우지 않습니다. 대신 &quot;삭제됨(Deleted Mark)&quot; 표시만 해둡니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;왜 바로 삭제하지 않을까?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;속도 때문입니다: 즉시 물리적으로 삭제하면 인덱스 구조(B-Tree)를 그 즉시 재조정해야 하므로 작업이 느려집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;효율성: 표시만 해두고 나중에 백그라운드 스레드(Purge Thread)가 한꺼번에 정리하는 것이 훨씬 효율적입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 책의 '색인'으로 이해하기 (비유)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;책 뒷면의 색인(Index)을 상상해 보세요. 책 본문은 100페이지, 색인은 2페이지 분량입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;상황: 1,000번의 데이터 삭제/수정이 일어났습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결과: 실제 본문 내용은 여전히 100페이지 분량이지만, 색인 페이지는 취소선이 그어진 정보들로 가득 차서 20페이지로 늘어났습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;문제: 예전에는 '김치'라는 단어를 찾기 위해 2페이지만 보면 됐지만, 이제는 불필요한 취소선이 가득한 20페이지 전체를 다 뒤져야 합니다. 당연히 조회 속도가 느려질 수밖에 없습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. DELETE vs UPDATE 내부 동작 비교&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;DELETE의 경우: &quot;공간은 그대로, 데이터만 줄어듦&quot;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;17&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;초기: 데이터 100개 (100GB) &amp;rarr; Data_length: 100GB&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;20개 삭제 후: 80개 생존, 20개 삭제 표시 &amp;rarr; Data_length: 여전히 100GB&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;문제점: SELECT 시 100GB 공간을 전부 스캔하며 삭제 표시된 20GB를 건너뜁니다. 즉, 불필요한 스캔 범위가 남습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;UPDATE의 경우: &quot;데이터는 그대로, 공간은 늘어남&quot; (더 위험!)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;InnoDB에서 UPDATE는 내부적으로 Delete + Insert 방식으로 동작합니다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;기존 레코드: 삭제 표시 (Old)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;새 레코드: 새로운 공간에 삽입 (New)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결과: 데이터 100개 중 20개를 수정하면, 전체 점유 공간은 120GB로 늘어납니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위험성: 수정이 반복될수록 실제 데이터 양은 같은데 스캔해야 할 범위(쓰레기 데이터 공간)만 기하급수적으로 커집니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 86px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;구분&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;DELETE&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;UPDATE&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;실제 데이터&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;80개(줄어듬)&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;100개(그대로)&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;전체 공간&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;100GB(그대로)&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;120GB(늘어남)&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;쓰레기 공간&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;20GB&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;20GB&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;스캔 범위&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;100GB&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;120GB&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;수정이 더 심각한 이유는 데이터는 그대로인데 공간만 계속 늘어나며 스캔해야 할 범위가 계속 커집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이런 걸 고려했을 때 수정이 반복된다면 얼마나 심각해질까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;100개의 레코드를 100번 update 하면 실제 데이터는 100GB인데 10TB를 스캔해야 하므로 성능은 최악이 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. 내 DB 상태 진단하기&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;MySQL에서는 간단한 쿼리로 우리 테이블에 '쓰레기 데이터(Fragment)'가 얼마나 쌓였는지 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1769669452397&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SHOW TABLE STATUS LIKE '테이블명';&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Data_length: 전체 데이터가 차지하는 공간&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Data_free: 빈 공간(쓰레기)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Index_length: 인덱스 크기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;최적화가 필요한 시점&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Data_free 비율 = Data_free &amp;divide; Data_length이&amp;nbsp; 20% 이상이면 OPTIMIZE TABLE 명령을 통해 파편화된 공간을 정리해 주는 것이 좋습니다. (AI 생각)&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5. 실무에서의 판단기준&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인덱스를 걸어야 하는 경우&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;조회가 빈번한 컬럼&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;where절에 자주 사용되는 컬럼&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;INSERT위주이고 수정/삭제가 거의 없는 컬럼&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;33&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인덱스를 신중해야 하는 경우&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;34&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;수정/삭제가 실시간으로 대량 발생하는 컬럼&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데이터의 중복도(선택도)가 낮은 컬럼 (예: 성별, 진행상태 등)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인덱스 크기가 실제 데이터 크기보다 비대해진 경우&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;6. 실제 사례 분석&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;제가 확인한 실제 테이블 상태입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Data_length: 6.47GB&lt;/li&gt;
&lt;li&gt;Data_free: 7MB (0.1%)&lt;/li&gt;
&lt;li&gt;Index_length: 6.97GB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Data_free 비율이 0.1%으로 쓰레기가 거의 없습니다. 최적화 불필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;주의할 점&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인덱스(6.97GB)가 데이터(6.47GB) 보다 큼&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;불필요한 인덱스가 있는지 점검 필요&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-path-to-node=&quot;36&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;6. 결론&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;37&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;DB는 삭제/수정 시 즉시 지우지 않고 표시만 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;UPDATE는 Delete + Insert이므로 삭제보다 더 빈번하게 공간을 낭비한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;쓰레기 데이터가 쌓이면 인덱스 스캔 범위가 넓어져 조회 성능이 떨어진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;정기적으로 Data_free 비율을 모니터링하고 최적화(Optimize)를 진행한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결국 좋은 DB 설계란, 무조건 빠른 조회를 위해 인덱스를 남발하는 것이 아니라 &lt;b data-index-in-node=&quot;45&quot; data-path-to-node=&quot;38&quot;&gt;데이터의 생애 주기(C.R.U.D)를 고려하여 적절한 균형&lt;/b&gt;을 찾는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/128</guid>
      <comments>https://seungwontech.tistory.com/128#entry128comment</comments>
      <pubDate>Thu, 29 Jan 2026 19:02:35 +0900</pubDate>
    </item>
    <item>
      <title>데드락과 갱신 유실을 겪고 배운 송금 시스템 설계 회고</title>
      <link>https://seungwontech.tistory.com/127</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;금융 서비스의 핵심은 데이터 무결성이라고 생각합니다. 단순히 잔액을 옮기는 기능을 넘어, 수천 건의 요청이 동시에 발생해도 데이터가 꼬이지 않고 네트워크 오류가 있어도 중복 거래가 발생하지 않도록 안정적인 송금 시스템을 설계&amp;middot;구현하며 겪은 과정을 기록합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;본 글에서 &amp;lsquo;송금&amp;rsquo;은 타행 이체가 아닌 동일 시스템 내 계좌 간 이체를 의미합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;1. 프로젝트 목표 및 핵심 설계&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프로젝트는 확장 가능하고 신뢰할 수 있는 송금 시스템을 구축을 목표로 하며 아래 4가지 원칙을 준수했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데이터 정합성: 모든 금융 거래의 원자성 보장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시성 제어: 동일 계좌에 대한 경합 상황 해결&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;멱등성 보장: 동일 요청 재시도 시 중복 처리 방지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;성능 최적화: 대량 거래 발생 시에도 빠른 한도 조회 및 잔액 갱신&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;2. DB 모델링&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 92px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 16.5116%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테이블&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.1395%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #2780d4; color: #ffffff; text-align: start; font-family: 'Nanum Gothic';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.3489%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;비고&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 16.5116%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Account&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.1395%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;계좌 테이블&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.3489%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재 잔액과 계좌 상태를 관리하며, 삭제 시 Soft Delete 방식을 사용합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 16.5116%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;DailyLimitUsage&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.1395%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;일일 한도 테이블&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.3489%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;일자별 누적 합계 테이블을 별도로 관리합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 16.5116%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;LimitSetting&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.1395%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;한도 설정 테이블&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.3489%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;계좌별 한도 설정 정보를 저장하는 테이블입니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 16.5116%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Transaction&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.1395%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;거래 내역 테이블&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.3489%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;모든 거래의 최종 내역을 저장합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;2.1 DailyLimitUsage 설계 의도&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;매번 Transaction 테이블을 합산(SUM)하여 한도 체크하는 방식은 데이터가 늘어날수록 성능이 저하됩니다. 이를 방지하고자 일자별 누적 합계 테이블을 별도로 관리하여 조회 성능을 최적화했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;3. 트러블 슈팅&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;3.1 동시성 제어와 데드락 방지&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;금융 데이터는 정합성이 최우선입니다. 충돌 발생 시 롤백과 재시도가 잦은 낙관적 락 대신, 데이터 수정 시점에 즉시 락을 거는 비관적 락을 선택했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하지만&amp;nbsp;이체는 두 계좌의 락을 동시에 획득해야 하므로, A &amp;rarr; B, B &amp;rarr; A 송금이 동시에 발생할 경우 서로의 락 해제를 기다리는 데드락에 빠질 수 있습니다. 해결 방법으로 계좌 ID를 기준으로 정렬된 순서에 따라 락을 획득하도록 설계했습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768275794885&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 코드 핵심 로직 예시
Long firstId = Math.min(fromAccountId, toAccountId);
Long secondId = Math.max(fromAccountId, toAccountId);

// 낮은 ID부터 순차적으로 락 획득 (순환 대기 방지)
accountRepository.findLockedByAccountId(firstId);
accountRepository.findLockedByAccountId(secondId);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;3.2 갱신 유실 방지와 식별자 설계&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;비관적 락을 적용했음에도 불구하고, 객체 조회 순서에 따라 데이터 최신성이 보장되지 않아 잔액이 유실되는 문제를 경험했습니다. 이는 설계 당시 영속성 컨텍스트의 동작 방식을 간과하여 발생한 실수였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;락을 획득하기 전 계좌 엔티티를 먼저 조회하여 메모리에 올리는 것이 문제였습니다. 이후 findLockedByAccountId로 비관적 락을 시도하더라도, JPA는 이미 영속성 컨텍스트에 존재하는 과거 상태의 엔티티를 그대로 반환합니다. 결과적으로 락은 걸었지만 데이터는 락을 걸기 전의 과거 스냅샷을 사용하게 되며, 다른 트랜잭션의 변경 사항을 덮어쓰는 갱신 유실이 발생했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;문제를 해결하기 위해 락을 걸기 전에는 계좌의 ID만 최소한으로 조회하고, 실제 비관적 락을 획득하는 시점에 최신 엔티티를 새롭게 로드하도록 로직을 개선했습니다. 락 획득 시점과 엔티티 로드 시점을 일치시켜 항상 최신 데이터를 바탕으로 잔액 계산이 이루어지게 설계했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;또한 API 파라미터 설계 시 시스템 내부 식별자인 AccountId 대신 AccountNo를 사용한 것 역시 의도적인 선택이었습니다. 이는 보안상 내부 PK 구조를 외부에 노출하지 않기 위함이었습니다. 내부 로직에서는 계좌번호를 통해 ID를 먼저 식별한 후, 해당 ID를 기반으로 데이터에 접근함으로써 시스템 내부 구조를 은닉하고 식별자 노출로 인한 보안 리스크를 최소화했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768286374833&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 개선된 로직: ID 식별 후, 락 획득 시점에 최신 엔티티 로드
Long fromAccountId = accountRepository.findIdByAccountNo(fromAccountNo);
Long toAccountId = accountRepository.findIdByAccountNo(toAccountNo);

// PK 기반 락 획득 시점에 최신 상태의 엔티티를 영속성 컨텍스트에 로드
Account firstLockedAccount = accountRepository.findLockedByAccountId(firstId);
Account secondLockedAccount = accountRepository.findLockedByAccountId(secondId);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;3.3 멱등성 이중 방어&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;네트워크 지연으로 인한 중복 요청은 금전적 사고로 이어집니다. 이를 막기 위해 이중 방어막을 구축했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 72px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.6744%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 77.3256%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;width: 22.6744%; height: 36px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Application Level(Redis)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 77.3256%; height: 36px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;SETNX를 사용하여 동일한 transaction_request_id로 들어오는 요청은 1초간 락킹하여 1차 차단합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.6744%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Database Level&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 77.3256%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;유니크 제약 조건을 통해 동일 요청이 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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;멱등성은 중복 요청을 막는 것을 넘어 여러 번 요청해도 일관된 결과를 받는 것까지 포함해야 한다고 판단했습니다. 락 획득 실패 시 에러를 내지 않고 DB를 조회해 이미 완료된 요청이면 기존 결과를 반환하고 진행 중이면 '처리 중' 응답을 내려주었습니다. 프론트엔드는 이를 기반으로 로딩 UI를 노출하거나 폴링을 수행하여, 중복 클릭 시에도 최종적으로 동일한 성공 화면을 보게 설계했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;4. 기술적 트레이드오프&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;4.1 Redis vs MySQL Native&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;멱등성을 보장하기 위한 설계 과정에서&amp;nbsp;Redis 분산 락과 MySQL의 INSERT IGNORE&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 방식 사이에서 고민이 있었습니다&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Nanum Gothic';&quot;&gt;MySQL Native 방식을 활용하면 별도의 Redis 인프라 구축 없이도 충분히 멱등성을 보장할 수 있다고 생각했습니다. 사전 조회 과정 없이 바로 삽입을 시도해 네트워크 왕복 횟수를 줄일 수 있고, 중복 시에도 예외 대신 단순히 0을 반환받아 처리하는 방식이 매우 효율적인 전략이라고 판단했기 때문입니다. 추가 인프라 운영 부담 없이 DB가 제공하는 기능만으로 문제를 해결할 수 있다는 점도 큰 장점이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Nanum Gothic';&quot;&gt;하지만 고민 끝에 인프라 복잡도가 높아지더라도 Redis를 활용한 이중 방어 전략을 선택했습니다. 그 이유는 핵심 자원인 DB의 부하를 낮추는 것이 더 중요하다고 보았기 때문입니다. 송금 서비스처럼 트래픽이 집중되는 환경에서 DB 커넥션과 락은 매우 한정적인 자원입니다. 중복 요청이 왔을 때, DB 트랜잭션을 시작하기 전 단계인 Redis에서 요청을 선제적으로 차단함으로써 원천 DB를 보호하고 싶었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결국 시스템의 복잡도는 다소 높아지더라도, DB의 안정성을 최우선으로 고려하여 부하를 분산시키는 구조를 갖추는 것이 금융 시스템에 더 적합한 선택이라고 판단하여 Redis 기반의 설계를 최종적으로 채택하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;4.2 다중 락 (계좌 락 + 일일 한도 락)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;계좌 락에 이어, 일일 한도(AccountDailyLimitUsage)를 조회&amp;middot;생성(없으면 생성)하는 과정에서도 비관적 락을 적용할지 여부는 이번 설계에서 가장 큰 고민 중 하나였습니다. 이체 로직은 이미 두 계좌에 대해 락을 획득한 상태에서 진행되기 때문에, 여기에 한도 테이블까지 추가로 락을 잡으면 DB 커넥션 점유 시간과 락 대기 시간이 늘어나 전체 처리량이 떨어질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1124&quot; data-start=&quot;914&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;성능만 고려한다면 한도 조회를 락 없이 수행하는 편이 유리합니다. 하지만 금융 서비스에서 &amp;lsquo;일일 한도&amp;rsquo;는 보안 및 규제와 직결된 핵심 비즈니스 규칙이며, 한 번이라도 초과되면 심각한 사고로 이어질 수 있습니다. 락을 포기하면 짧은 순간에 동시 요청들이 한도 체크를 동시에 통과하는 레이스 컨디션이 발생할 수 있고, 그 결과 설정된 한도를 초과한 이체가 실행될 위험이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1124&quot; data-start=&quot;914&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1258&quot; data-start=&quot;1126&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결국 저는 성능 비용을 감수하더라도 한도 조회 단계에서도 비관적 락을 적용하기로 결정했습니다. 처리 속도보다 &amp;ldquo;한도는 반드시 지켜져야 한다&amp;rdquo;는 원칙을 우선했고, 락을 통해 한도 누적과 이체 실행이 항상 원자적으로 일어나도록 보장했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768296418767&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private DailyLimitUsage getOrCreateUsage(Long accountId, LocalDate today) {
    return dailyLimitUsageRepository.findLockedByAccountIdAndLimitDate(accountId, today)
            .orElseGet(() -&amp;gt; dailyLimitUsageRepository.save(dailyLimitUsage.init(accountId, today)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;5. 검증 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;5.1 멱등성 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;321&quot; data-start=&quot;198&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시에 중복 출금 요청이 와도, 실제 출금은 단 한 번만 반영되어 잔액과 거래 내역이 중복 생성되지 않음을 검증한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;321&quot; data-start=&quot;198&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시에 출금 요청 10건을 발생해 계좌 잔액이 100,000원인 계좌에 5,000원을 출금한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768282906781&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;트랜잭션요청아이디가 같으면 한번만 출금한다.&quot;)
void idempotent_test() throws Exception {
 
    int requestCount = 10;
    ExecutorService executorService = Executors.newFixedThreadPool(requestCount);

    CountDownLatch readyLatch = new CountDownLatch(requestCount);
    CountDownLatch startLatch = new CountDownLatch(1);
    CountDownLatch endLatch = new CountDownLatch(requestCount);

    AtomicInteger success = new AtomicInteger();
    AtomicInteger fail = new AtomicInteger();

    String transactionRequestId = &quot;c1e12a85-46c8-4bp4-cc9b-2482b9f214et&quot;;

    for (int i = 0; i &amp;lt; requestCount; i++) {
        executorService.submit(() -&amp;gt; {
            try {
                readyLatch.countDown();
                startLatch.await();
                withdrawUseCase.execute(accountNo, withdrawAmount, transactionRequestId);
                success.incrementAndGet();
            } catch (Exception e) {
                fail.incrementAndGet();
            } finally {
                endLatch.countDown();
            }
        });
    }
    readyLatch.await();
    startLatch.countDown();
    endLatch.await();
    executorService.shutdown();

    Account result = accountRepository.findById(accountId).orElseThrow();
    Long historyCount = accountTransactionRepository.countByAccountId(accountId);
    Transaction transaction = accountTransactionRepository.findByAccountIdAndTransactionRequestId(accountId, sameTransactionId).orElseThrow();

    assertThat(result.getBalance()).isEqualTo(balance - withdrawAmount);

    assertThat(fail.get()).isEqualTo(0);
    assertThat(historyCount).isEqualTo(1);
    assertThat(accountTransactionRepository.findByAccountIdAndTransactionRequestId(accountId, transactionRequestId)).isPresent();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZSl7D/dJMcadOdu9p/0rg35joc2ZypbHToMRbCnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZSl7D/dJMcadOdu9p/0rg35joc2ZypbHToMRbCnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZSl7D/dJMcadOdu9p/0rg35joc2ZypbHToMRbCnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZSl7D%2FdJMcadOdu9p%2F0rg35joc2ZypbHToMRbCnk%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;1306&quot; height=&quot;318&quot; data-origin-width=&quot;1306&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;5.2. 동시성 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다수의 송금 요청이 동시에 발생해도 잔액 정합성이 깨지지 않고, 모든 요청이 정확히 한 번씩 반영되는지를 검증한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시에 이체 요청 100건을 발생해 잔액이 각각 1,000,000원인 계좌 A에서 계좌 B로 10,000원을 송금한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;모든 요청이 성공했을 때 계좌 A의 잔액은 0원, 계좌 B의 잔액은 2,000,000원이 되어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768283465868&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;A에서 B로 100명이 동시에 1만원씩 송금하면 A는 0원, B는 200만원이 된다.&quot;)
void transfer_test() throws Exception {
    int threadCount = 100;
    long transferAmount = 10_000L;

    ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
    CountDownLatch latch = new CountDownLatch(threadCount);
    AtomicInteger successCount = new AtomicInteger();

    for (int i = 0; i &amp;lt; threadCount; i++) {
        executorService.submit(() -&amp;gt; {
            try {
                transferUseCase.execute(accountNoA, accountNoB, transferAmount, UUID.randomUUID().toString());
                successCount.incrementAndGet();
            } catch (Exception e) {
                System.err.println(&quot;Transfer failed: &quot; + e.getMessage());
            } finally {
                latch.countDown();
            }
        });
    }

    latch.await();
    executorService.shutdown();

    Account finalA = accountRepository.findById(accountIdA).orElseThrow();
    Account finalB = accountRepository.findById(accountIdB).orElseThrow();

    assertThat(successCount.get()).isEqualTo(threadCount);
    assertThat(finalA.getBalance()).isEqualTo(0L);
    assertThat(finalB.getBalance()).isEqualTo(2_000_000L);
    // 트랜잭션 기록은 출금/입금 쌍으로 총 200건이어야 함
    assertThat(transactionRepository.count()).isEqualTo(threadCount * 2);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz3QFn/dJMcabv9P7P/vyz2gLQKaf5JM0shW2hXbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz3QFn/dJMcabv9P7P/vyz2gLQKaf5JM0shW2hXbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz3QFn/dJMcabv9P7P/vyz2gLQKaf5JM0shW2hXbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz3QFn%2FdJMcabv9P7P%2Fvyz2gLQKaf5JM0shW2hXbk%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;1396&quot; height=&quot;158&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;5.3 데드락 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;A&amp;rarr;B, B&amp;rarr;A 교차 송금 시나리오를 통해 데드락 방지 로직을 검증한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;계좌 A, B (잔액 1,000,000원)으로 A &amp;rarr; B로 10,000원 송금 (50회), B &amp;rarr; A로 10,000원 송금 (50회) 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;최종 잔액은 각각 1,000,000원(보낸 돈 50만, 받은 돈 50만)으로 유지되어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1768291906424&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;A&amp;rarr;B와 B&amp;rarr;A 송금이 동시에 100건 발생해도 데드락 없이 정합성이 유지된다.&quot;)
void deadlock_prevention_test() throws Exception {
    // given
    int threadCount = 100; // 총 100번의 송금 (A-&amp;gt;B 50번, B-&amp;gt;A 50번)
    long transferAmount = 10_000L;

    ExecutorService executorService = Executors.newFixedThreadPool(32);
    CountDownLatch latch = new CountDownLatch(threadCount);

    AtomicInteger successCount = new AtomicInteger();
    AtomicInteger failCount = new AtomicInteger();

    // when
    for (int i = 0; i &amp;lt; threadCount; i++) {
        boolean isAtoB = (i % 2 == 0); // 절반은 A-&amp;gt;B, 절반은 B-&amp;gt;A
        String fromNo = isAtoB ? accountNoA : accountNoB;
        String toNo = isAtoB ? accountNoB : accountNoA;

        executorService.submit(() -&amp;gt; {
            try {
                // 각 요청마다 고유한 requestId(UUID) 생성
                transferUseCase.execute(fromNo, toNo, transferAmount, UUID.randomUUID().toString());
                successCount.incrementAndGet();
            } catch (Exception e) {
                failCount.incrementAndGet();
                System.err.println(&quot;Transfer failed: &quot; + e.getMessage());
            } finally {
                latch.countDown();
            }
        });
    }

    latch.await();
    executorService.shutdown();

    // then
    Account finalA = accountRepository.findById(accountIdA).orElseThrow();
    Account finalB = accountRepository.findById(accountIdB).orElseThrow();

    // 1. 모든 요청이 성공했는지 확인 (데드락 발생 시 일부 타임아웃/실패 발생)
    assertThat(successCount.get()).isEqualTo(threadCount);
    assertThat(failCount.get()).isEqualTo(0);

    // 2. 최종 잔액 검증
    // A: 100만 - (50번 * 1만) + (50번 * 1만) = 100만
    // B: 100만 - (50번 * 1만) + (50번 * 1만) = 100만
    assertThat(finalA.getBalance()).isEqualTo(1_000_000L);
    assertThat(finalB.getBalance()).isEqualTo(1_000_000L);

    // 트랜잭션 기록은 출금/입금 각 100건이어야 함
    assertThat(transactionRepository.countByAccountId(accountIdA)).isEqualTo(threadCount);
    assertThat(transactionRepository.countByAccountId(accountIdB)).isEqualTo(threadCount);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biUW1o/dJMcaiPxy8m/mQk3gDsVDO205rc44hM6E1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biUW1o/dJMcaiPxy8m/mQk3gDsVDO205rc44hM6E1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biUW1o/dJMcaiPxy8m/mQk3gDsVDO205rc44hM6E1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiUW1o%2FdJMcaiPxy8m%2FmQk3gDsVDO205rc44hM6E1%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;1054&quot; height=&quot;108&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아래의 Math.min, max 정렬 로직을 주석 처리한 뒤 테스트를 수행했을 때, 다음과 같은 에러 로그를 목격할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwHHTn/dJMcajt8z8X/4lutFb0mL7rMU5590IhJ60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwHHTn/dJMcajt8z8X/4lutFb0mL7rMU5590IhJ60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwHHTn/dJMcajt8z8X/4lutFb0mL7rMU5590IhJ60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwHHTn%2FdJMcajt8z8X%2F4lutFb0mL7rMU5590IhJ60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;956&quot; height=&quot;379&quot; data-origin-width=&quot;956&quot; data-origin-height=&quot;379&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1643&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmnA6a/dJMcabbQYHR/kpkN52a0RrPjxCzVdNx2w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmnA6a/dJMcabbQYHR/kpkN52a0RrPjxCzVdNx2w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmnA6a/dJMcabbQYHR/kpkN52a0RrPjxCzVdNx2w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmnA6a%2FdJMcabbQYHR%2FkpkN52a0RrPjxCzVdNx2w1%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;1643&quot; height=&quot;86&quot; data-origin-width=&quot;1643&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;6. 마치며&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333;&quot;&gt;이번 송금 시스템을 구현하며 가장 크게 느낀 점은 &lt;b&gt;기술은 단순한 도구일 뿐이고, 비즈니스의 성격과 우선순위에 따라 최선의 선택은 달라진다&lt;/b&gt;는 것입니다. 금융 서비스에서는 기능을 빠르게 완성하는 것보다 &lt;b&gt;데이터 무결성을 끝까지 지켜내는 설계&lt;/b&gt;가 더 중요하다는 것을 다시 한 번 확인했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;444&quot; data-start=&quot;179&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333;&quot;&gt;동시성 제어를 위해 비관적 락을 선택하고, 이체 시에는 락 순서를 고정해 데드락을 방지했으며, 영속성 컨텍스트로 인해 발생할 수 있는 갱신 유실 문제까지 직접 겪으며 &lt;b&gt;락 획득 시점과 엔티티 로드 시점을 일치시키는 설계의 중요성&lt;/b&gt;을 체득했습니다. 또한 멱등성은 중복을 막는 것을 넘어, 여러 번 요청하더라도 항상 동일한 결과로 수렴하도록 만드는 UX까지 포함되어야 한다고 판단했고, 이를 위해 Redis 선차단과 DB 유니크 제약을 결합한 이중 방어 구조를 적용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;444&quot; data-start=&quot;179&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;573&quot; data-start=&quot;446&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333;&quot;&gt;이 과정을 통해 설계 관점이 한층 더 성장한 기분이며, 앞으로는 Outbox 패턴, 타임아웃/백오프 정책, 장애&amp;middot;동시성 등 다양한 시나리오 테스트까지 확장해 보면서 운영 관점에서도 더 신뢰할 수 있는 구조로 발전시켜보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/127</guid>
      <comments>https://seungwontech.tistory.com/127#entry127comment</comments>
      <pubDate>Tue, 13 Jan 2026 16:17:34 +0900</pubDate>
    </item>
    <item>
      <title>비관적 락은 대기지옥, 낙관적 락은 재시도지옥이었다</title>
      <link>https://seungwontech.tistory.com/126</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;선착순 100명에게 쿠폰을 지급하는 이벤트. 단순해 보이지만, 순간적으로 수만 명이 &amp;ldquo;광클&amp;rdquo;을 시작하면 서버는 아수라장이 된다. 이 문제를 해결하기 위해 고민했던 과정과 내가 선택한 해결책을 정리해 본다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 요구사항&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;선착순으로 100명에게 할인 쿠폰을 제공한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;101개 이상이 지급되면 안 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;순간적으로 몰리는 트래픽을 감당해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;&lt;b&gt;2. 동시성 제어, 어떤 도구를 쓸 것인가?&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;가장 먼저 떠오른 3가지 방법의 한계를 정리해 봤다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1️⃣ synchronized&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;synchronized는 JVM 내부에서만 동작하는 락이다. 단일 서버에서는 동시성 문제를 막을 수 있지만, 서버가 여러 대로 확장되면 락이 공유되지 않는다. 그 결과 여러 서버에서 발급 로직이 동시에 실행되어 중복 발급(경쟁 조건)이 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2️⃣ 비관적 락 (대기지옥)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;락으로 모든 요청을 순차 처리하면 정합성은 보장된다. 하지만 트래픽이 몰리면 대기 시간이 급격히 증가하고, 지연/타임아웃이 발생해 사용자 경험이 크게 나빠질 수 있다. 소규모라면 가능하지만 대규모 선착순에는 병목이 되기 쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3️⃣ 낙관적 락 (무한지옥)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;version으로 동시 수정 충돌을 감지해 충돌 시 업데이트를 실패시키는 방식이며, 보통 재시도와 함께 사용된다. 하지만 선착순처럼 같은 재고를 동시에 갱신하면 충돌이 잦아져 재시도 지옥으로 빠질 수 있다. 서버 재시도를 하지 않는 전략도 가능하지만, 사용자의 연타/새로고침으로 트래픽이 다시 폭발할 수 있어 별도 제어가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-size: 1.44em; letter-spacing: -1px;&quot;&gt;3. 선착순 이벤트에서 정말 중요한 것&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿠폰이 수량을 초과해 발급되지 않는 것이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;선착순에서 핵심은 &amp;ldquo;누가 1ms 먼저 눌렀는지&amp;rdquo;가 아니라 수량 정합성이다. 시스템 관점에서는 쿠폰 수량이 초과 발급되지 않도록 보장하는 것이 최우선이며, 모든 요청을 순차 처리할 필요는 없다. 결국 &amp;ldquo;총 발급 수가 한도를 넘지 않도록&amp;rdquo;만 보장하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;&lt;b&gt;4. Redis INCR를 선택한 이유&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;Redis는 싱글 스레드로 동작하며 명령어가 원자적이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000; text-align: left;&quot;&gt;여러 요청이 동시에 INCR를 호출해도 Redis가 순서대로 처리하므로 값이 꼬이지 않는다. 또한 메모리 기반이라 DB보다 빠르고, 복잡한 분산 락 없이도 IINCR 한 번(원자적 증가)으로 수량 제어가 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;&lt;b&gt;5. Redis 기반 방식의 주의점&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;Redis를 쓴다고 끝은 아니다. 예외 케이스를 고려해야 한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis에서는 발급이 성공했지만 DB 저장이 실패하면 수량과 발급 상태가 어긋날 수 있다. 이 경우 메시지 큐를 활용해 비동기로 저장하거나 실패 재처리 전략이 필요하다. 그리고&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;한 사용자가 연타로 여러 장을 가져가면 안 된다. SADD 같은 Set 기반 중복 체크로 &amp;ldquo;사용자당 1회&amp;rdquo;를 보장하는 멱등 설계가 필요하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;6. 요약&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000;&quot;&gt;요청이 들어오면 &lt;b&gt;Redis에서 SADD(중복 체크) + INCR(수량 제어)로&lt;/b&gt; 먼저 컷 한다. 이후 실제 발급 데이터 저장은 메시지 큐로 분리해 DB 부하를 조절하며 안전하게 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/126</guid>
      <comments>https://seungwontech.tistory.com/126#entry126comment</comments>
      <pubDate>Fri, 9 Jan 2026 17:22:02 +0900</pubDate>
    </item>
    <item>
      <title>1000만 건 테이블 13초를 0.2초로 줄인 페이징 성능 최적화</title>
      <link>https://seungwontech.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;대규모 트래픽을 처리하는 게시판 서비스에서 게시글 목록 조회는 가장 빈번하게 호출되는 핵심 API입니다. 특히 페이지 번호 기반 페이징에서 offset이 커질수록 느려지는 문제는 많은 개발자가 겪는 이슈입니다. 해당 내용은 인프런 강의를 듣고 정리한 것으로, 실제 제 프로젝트에도 도움이 많이 되어 글로 남깁니다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;DB는 miaradb 11입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Nanum Gothic';&quot;&gt;1. 페이지 번호 방식의 문제점&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Nanum Gothic';&quot;&gt;총 게시글 1000만 건, 페이지당 30개, 4페이지 조회 (offset = 90)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1763020010916&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from article
where board_id = 1
order by created_at desc
limit 30 offset 90;

조회 속도: 13s 456ms
EXPLAIN: type = ALL, Extra = Using where; Using filesort&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Nanum Gothic';&quot;&gt;즉 전체 테이블 스캔이 발생했다. create_at에 인덱스가 없어 DB는 모든 데이터를 읽은 뒤 디스크 기반 정렬(filesort)을 수행하게 되어 조회 속도가 크게 느려졌다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Nanum Gothic';&quot;&gt;2. 인덱스 추가&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1763020435597&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create index idx_board_id_article_id on article(board_id asc, article_id desc);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1763020455610&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from article
where board_id = 1
order by article_id desc
limit 30 offset 90;

조회 속도: 132ms 
EXPLAIN: type = range, Extra = Using index condition, key = idx_board_id_article_id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;즉 인덱스를 사용했다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;정렬 기준을 created_at 대신 article_id로 변경한 이유는 분산 환경에서는 여러 서버에서 동시에 게시글 생성 시 동일 timestamp가 발생하며 정렬 순서가 불안정해진다. 목록 조회 시 순서가 바뀌거나 누락되는 문제가 생길 수 있다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Snowflake ID는 시간 기반 + 일련번호 조합, 전역 오름차순 보장, 출동 가능성이 희박하므로 article_id로 정렬하는 것이 더 안전하다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Nanum Gothic';&quot;&gt;3. offset이 커지면 다시 느려진다&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Nanum Gothic';&quot;&gt;예를 들어 offset 1,455,570를 조회한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1763021095424&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select * from article
where board_id = 1
order by article_id desc
limit 30 offset 1455570;

조회 속도: 1s 410ms
EXPLAIN: type = range, Extra = Using index condition, key = idx_board_id_article_id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Nanum Gothic';&quot;&gt;여전히 인덱스를 사용 중이지만 속도는 느려졌다. 왜 이런 일이 발생했을까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;먼저 offset 방식은 아래처럼 동작한다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;offset + limit 만큼의 인덱스 데이터를 읽고 offset개는 버리고 마지막 limit개만 반환한다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;즉 offset = 90 &amp;rarr; 120개 스캔, offset = 1,455,570 &amp;rarr; 1,455,600개 스캔 그리고 여기서 중요한 개념이 아래 등장한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Clustered Index와 Secondary Index 개념&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;Clustered Index&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;primary key 기반으로 생성되는 인덱스이다.&amp;nbsp;구조적으로 인덱스의 가장 말단 노드인 leaf node에 실제 행 데이터 전체가 저장된다. 즉 PK를 이용해 데이터를 조회하는 것은 인덱스 경로를 따라 최종적으로 데이터 파일 자체를 직접 읽는 것과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Secondary Index(&lt;/span&gt;보조 인덱스)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;PK가 아닌 다른 컬럼에 대해 사용자가 별도로 생성한 인덱스이다. 보조 인덱스의 리프 노드에는 다음 세 가지 정보만 저장한다. 인덱스를 구성하는 컬럼 값 여기선 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;board_id, article_id, 해당 행의 primary key(PK) 값 여기서는 article_id을 말한다. &lt;/span&gt;where 조건에 맞는 행을 찾기 위해 보조 인덱스 트리를 탐색한다. 보조 인덱스 리프 노드에서 해당&amp;nbsp; 행의 PK추출한다. 추출한 PK 값을 이용하여 클러스터드 인덱스 트리를 다시 한번 탐색하고 최종적을 실제 행 데이터 전체를 가져옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 94px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개념&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Secondary Index&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Covering Index&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;PK 외 컬럼에 추가한 인덱스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;쿼리에 필요한 모든 컬럼이 들어 있는 인덱스&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;목적&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;빠르게 PK를 찾기 위한 보조 인덱스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;PK 재조회 없이 인덱스만으로 쿼리 해결&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데이터 조회&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;반드시 PK를 따라가야 함&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;PK 찾아갈 필요 없음(더 빠름)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;leaf node&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인덱스 컬럼 + PK만 포함&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Secondary Index는 인덱스 '종류'이고, Covering Index는 인덱스 사용 '패턴' 또는 '상태'이다. 커버링 인덱스는 보조 인덱스를 어떻게 활용하여 성능을 극대화했는지에 대한 정의이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;커버링 인덱스 활용으로 성능 개선&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인덱스의 데이터만으로 조회하면 secondary index만 보고 결과를 만들 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1763025917556&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select board_id, article_id 
from article
where board_id = 1
order by article_id desc
limit 30 offset 1455570;

조회 속도: 292ms
Extra = Using index (커버링 인덱스)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;secondary index에 이미 board_id, article_id가 있으므로 clustered index를 읽지 않아도 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;서브쿼리 + 조인&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1763026178806&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select *
from (
        select board_id, article_id
        from article
        where board_id = 1
        order by article_id desc
        limit 30 offset 1455570
     ) t
left join article a on t.article_id = a.article_id;

조회 속도: 약 278ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;서브쿼리가 커버링 인덱스로 빠르게 article_id 30개만 추출하고&amp;nbsp;그 30개에 대해서만 PK 기반으로 테이블을 읽는다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;(북마크 룩업 1,455,600번 &amp;rarr; 30번으로 감소) 따라서 select * 이지만 매우 빠르게 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;OFFSET 은 근본적으로 느려진다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1763026261553&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;offset = 1455570  &amp;rarr; 278ms
offset = 6455570  &amp;rarr; 886ms&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;커버링 인덱스를 사용해도 스캔 개수에 비례하여 느려지는 구조는 변하지 않습니다.&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;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;북마크 룩업이란, 보조 인덱스를 통해 원하는 레코드의 위치를 찾은 후 실제 행 데이터를 가져오기 위해 클러스터드 인덱스(Primary Key)를 다시 한번 찾아가는 과정을 말한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;대규모 서비스에서의 실무적 해결책&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테이블 분리 또는 &quot;정상적인 사용자는 300,000페이지를 조회하지 않는다&quot; 정책으로 최대 10,000페이지까지만 허용해 어뷰저 트래픽 방지하는 등 여러 방법으로 문제를 해결할 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인프런 강의&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a title=&quot;인프런 강의&quot; href=&quot;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%EB%A1%9C-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%84%A4%EA%B3%84-%EA%B2%8C%EC%8B%9C%ED%8C%90/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8%EB%A1%9C-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%84%A4%EA%B3%84-%EA%B2%8C%EC%8B%9C%ED%8C%90/dashboard&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/125</guid>
      <comments>https://seungwontech.tistory.com/125#entry125comment</comments>
      <pubDate>Thu, 13 Nov 2025 18:26:51 +0900</pubDate>
    </item>
    <item>
      <title>AWS NAT 인스턴스와 NAT 게이트웨이 어떤 걸 써야 할까?</title>
      <link>https://seungwontech.tistory.com/124</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;NAT 인스턴스&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;퍼블릭 서브넷에 배치된 EC2 인스턴스가 프라이빗 서브넷의 인터넷 요청을 대신 전달(SNAT) 하는 구조로&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;즉 인터넷으로 나가는 프록시 서버 역할을 하는 EC2&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;특징&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;직접 관리, EC2 크기와 설정에 따라 성능이 달라진다. 비용이 매우 저렴하지만 운영 부담 존재한다. t3.micro 기준 시간당 0.013달러의 비용이 발생한다. 더 저렴한 t4g.nano은 0.0052달러이다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;NAT 게이트웨이&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;AWS에서 완전 관리형으로 제공하는 NAT 서비스로 퍼블릭 서브넷에 생성 후 Elastic ip를 연결하면 프라이빗 서브넷 인스턴스들이 외부로 나갈 수 있다.&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;특징&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;관리 불필요하고 비용은 비싸지만 안정적이고 유지보수 부담이 적다. 서울 기준 NAT 게이트웨이 요금 0.059 달러의 비용이 발생한다. 24시간이면 큰 비용이 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;요약 및 생각&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NAT 인스턴스는 직접 관리해야하고 장애 시 복구도 수동으로 해야 하지만 비용이 10배 저렴하다는 점에서 토이 프로젝트나 개발용 환경에서 매우 적합하다고 생각됩니다. NAT 게이트웨이는 자동 복구와 높은 성능을 제공해 안정성을 보장하지만 24시간 가동 시 상당한 비용이 발생할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/124</guid>
      <comments>https://seungwontech.tistory.com/124#entry124comment</comments>
      <pubDate>Wed, 5 Nov 2025 13:45:54 +0900</pubDate>
    </item>
    <item>
      <title>아직도 DB에 JWT 저장하세요?</title>
      <link>https://seungwontech.tistory.com/122</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JWT란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JWT(json web token)는 클라이언트와 서버 간 인증 정보를 토큰 형태를 안전하게 전달하기 위한 표준이다. 기존 세선/쿠키 방식과 달리 서버는 상태를 저장하지 않고 토큰 자체만으로 정보의 유효성을 검증할 수 있어 서버 자원 소모를 줄일 수 있다. 즉 서버가 세션을 들고 있지 않아도 클라이언트가 JWT만 가지고 있다면 인증이 가능하다. 그래서 Stateless 인증 방식이라고 부른다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3가지 구성 요소&lt;/span&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 80px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 23.2558%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.7442%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 23.2558%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Header(헤더)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.7442%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;토큰의 타입과 해싱 알고리즘(HS256, HS512) 정보가 담김&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 23.2558%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Payload(정보)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.7442%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;유저 정보, 권한, 만료시간 등이 포함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 23.2558%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Signature(서명)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.7442%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위 두 정보를 서버의 비밀키로 서명하여 생성, 서버는 이 서명을 통해 토큰의 위변조 여부를 확인&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JWT를 관리하는 대표적인 방법 두 가지&lt;/span&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;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;방식&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.7442%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;DB 저장&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.7442%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;AccessToken/ RefreshToken을 RDB에 저장하고 상태를 관리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.2558%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis 저장&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.7442%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인메모리 캐시(Redis)로 토큰, TTL, 블랙리스트를 관리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;DB로 관리하는 경우&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데이터가 영구적으로 저장되어 서버가 재시작해도 토큰 정보가 유지된다는 장점이 있지만 단점으로는 토큰 검증 및 관리를 위해 매번 디스크 I/O 발생하는 DB에 접근해야 하고 TTL 처리에 별도 배치나 쿼리가 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis로 관리하는 경우&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인메모리 기반이라 읽기/쓰기가 매우 빠르다, 토큰 검증 시 부하가 적어 대규모 트래픽에 유리하다, TTL 기능을 활용하여 토큰 만료를 자동으로 관리할 수 있다는 장점이 있지만 단점으로는 메모리에 데이터가 저장되므로 레디스 서버가 다운되거나 재시작되면 모든 토큰 정보가 삭제됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;블랙리스트&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;유효 기간이 남았음에도 강제로 무효화해야 하는 토큰 목록을 의미하며, 사용자가 로그아웃하면 액세스 토큰과 리프레시 토큰 모두 즉시 무효화되어야 한다. 보안상의 이유로 비밀번호 변경 시 기존의 모든 토큰을 무효화할 때, 서버에서 특정 토큰이 해킹되었다고 판단하여 강제로 만료시켜야 할 때 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JTI(JWT ID)의 역할&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;블랙리스트를 관리할 때 토큰 문자열 전체 대신 JTI라는 클레임을 사용한다. JTI는 토큰에 부여하는 고유 식별자로 서버는 토큰 전체를 저장하는 대신 로그아웃된 토큰의 JTI만 Redis에 저장한다. 이후 어떤 요청이 들어오든 토큰의 JTI를 추출해 Redis에서 해당 JTI가 존재하는지 확인하고 존재하면 유효 기간과 상관없이 무효 토큰으로 처리한다. 장점으로는 데이터 저장 공간을 절약한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;액세스 토큰을 생성할 때 페이로드에 jti 클레임을 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761713483265&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;redisTemplate.opsForValue().set(
    &quot;bl:access:&quot; + jti,
    &quot;logout&quot;,
    remainingTime,
    TimeUnit.SECONDS
);
// 키 구조 bl:access:{jti} = &quot;logout&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis의 진짜 강점은 TTL&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;토큰마다 만료 시간을 설정해 두면 만료 시점이 지나면 Redis가 자동으로 키를 삭제한다. 이제 배치나 스케줄러 없이도 TTL이 지나면 자동으로 데이터가 정리됩니다. 메모리 누수나 &quot;고아 토큰&quot; 걱정이 없어진다. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761713888565&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RedisHash(&quot;token&quot;)
public class Token {
    @Id
    private String userId;

    private String token;

    @TimeToLive(unit = TimeUnit.SECONDS)
    private Long expiration;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;@EnableRedisRepositories(enableKeyspaceEvents = ON_STARTUP)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Data Redis에서 @RedisHash를 사용해 엔티티를 저장할 때 Redis 내부에는 &lt;b&gt;두 가지 키&lt;/b&gt;가 함께 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 70px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 23.0233%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;키 이름&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.9767%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.8837%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;TTL 여부&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;width: 23.0233%; height: 25px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;token:{userId}&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.9767%; height: 25px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;실제 토큰 데이터 (엔티티 본문)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.8837%; height: 25px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;✅ TTL 적용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 25px;&quot;&gt;
&lt;td style=&quot;width: 23.0233%; height: 25px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;token&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 36.9767%; height: 25px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;엔티티의 ID 목록을 담은 인덱스(Set 형태)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.8837%; height: 25px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;❌ TTL 미적용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;문제 상황&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;TTL이 설정된 키는 시간이 지나면 레디스가 자동으로 삭제한다. 하지만 인덱스용 Set은 TTL이 없기 때문에 삭제된 ID가 그대로 남게 되어 phantom key 현상이 발생한다. 즉 실제 데이터는 사라졌지만 인덱스에는 여전히 존재하는 불일치 상태가 생긴다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;해결 방법&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Data Redis는 이 문제를 해결하기 위해 설정을 제공합니다. 이 설정은 레디스에서 TTL이 만료되거나 키가 삭제될 때 발생하는 이벤트를 감지하고 Spring이 해당 이벤트를 받아 내부 인덱스를 자동 정리해 준다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설정하지 않을 경우&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-28 오후 4.38.25.png&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nOLWf/dJMcadAjoV1/FPL77vj9o0LqM0Hxy4rMFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nOLWf/dJMcadAjoV1/FPL77vj9o0LqM0Hxy4rMFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nOLWf/dJMcadAjoV1/FPL77vj9o0LqM0Hxy4rMFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnOLWf%2FdJMcadAjoV1%2FFPL77vj9o0LqM0Hxy4rMFK%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;1456&quot; height=&quot;304&quot; data-filename=&quot;스크린샷 2025-10-28 오후 4.38.25.png&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설정했을 경우 자동으로 phantom key 생성&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-29 오후 3.42.28.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ywBLN/dJMcacg6jOH/ptP8Bgrv495YKoCv4y1fGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ywBLN/dJMcacg6jOH/ptP8Bgrv495YKoCv4y1fGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ywBLN/dJMcacg6jOH/ptP8Bgrv495YKoCv4y1fGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FywBLN%2FdJMcacg6jOH%2FptP8Bgrv495YKoCv4y1fGK%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;743&quot; height=&quot;275&quot; data-filename=&quot;스크린샷 2025-10-29 오후 3.42.28.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결과적으로 TTL 만료와 함께 인덱스까지 자동 정리되므로 &quot;고아 인덱스 없이&quot; 항상 정합성 있는 Redis Repository 상태를 유지할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RedisTemplate vs RedisRepository&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis에 데이터를 저장하고 접근하는 Spring Data Redis의 두 가지 주요 방법은 RedisTemplate과 RedisRepository입니다.&amp;nbsp;&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RedisTemplate&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; Redis 명령어를 실행하기 위해 제공하는 핵심 클래스입니다. Redis와의 통신을 하여 개발자가 데이터 타입별 연산을 쉽게 수행할 수 있도록 한다. 사용법은 opsForValue().set(), opsForHash().put()) 코드를 직접 작성하여 데이터를 조작합니다. 모든 데이터 조작을 수동으로 처리해야 한다.&amp;nbsp;&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RedisRepository&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; Spring Data의 Repository 추상화를 Redis에 적용한 방식이다. JPA처럼 메서드 이름 규칙을 따르면 Spring이 자동으로 CRUD 구현체를 생성해 준다. TokenRepository extends CrudRepository&amp;lt;Token, String&amp;gt;와 같이 인터페이스를 선언하고 tokenRepository.findById 메서드를 호출하여 사용한다. 개발 편의성과 생산성이 매우 높다.&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RedisTemplate 타입 권장 사항 &amp;lt;String, Object&amp;gt; vs. &amp;lt;String, String&amp;gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;String, Object 보다 String, String이 권장되는 이유는 직렬화 문제와 데이터 호환성 때문이다. &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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;직렬화 문제 및 데이터 비효율성&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Object 사용 시 Spring은 기본적으로 JDK 직렬화를 사용한다. JDK 직렬화는 Java 객체의 클래스 정보까지 바이트로 저장한다. 레디스에 저장되는 데이터 크기가 불필요하게 커지고(비효율적) 메모리를 낭비한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;String 사용 시 Java객체를 레디스에 저장하기 전에 json 문자열로 명시적으로 변환하여 저장하게 됩니다. 레디스는 기본적으로 문자열 기반 저장소입니다. 저장된 데이터가 json이라는 표준 포맷이 되므로 자바 외의 다른 언어로 구성한 MSA 환경에서 데이터를 쉽게 읽고 처리할 수 있어 호환성을 극대화한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결론적으로 JWT 관리는 DB보다 Redis가 빠르고 TTL 기반 자동 만료로 유지보수가 쉽다고 생각됩니다. 서비스 규모나 비용 등을 고려하면 DB로 관리하는 방식 또한 여러 정책을 가지고 사용한다면 굳이 비용을 들여서 사용할 필요가 없을 수 있다고 생각됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/122</guid>
      <comments>https://seungwontech.tistory.com/122#entry122comment</comments>
      <pubDate>Wed, 29 Oct 2025 15:20:22 +0900</pubDate>
    </item>
    <item>
      <title>클릭 대신 테라폼으로 인프라 관리하기</title>
      <link>https://seungwontech.tistory.com/121</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼(Terraform)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하시코프에서 개발한 오픈소스 코드형 인프라(IaC) 도구입니다. 코드를 사용하여 인프라를 선언적으로 정의하고 프로비저닝(구축 및 관리)할 수 있게 해 준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼은 사용자가 원하는 인프라의 최종 상태를 HCL이라는 전용 설정 언어를 사용하여 파일(.tf)로 작성한다. 이 코드는 단순히 명령을 순차적으로 나열하는 것이 아니라 어떤 리소스가 필요하다 고 선언하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;핵심 특징&lt;/span&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;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.8372%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이름&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.1628%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.8372%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;IaC(Infrastructure as Code)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.1628%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;인프라를 수동으로 설정하는 대신 코드로 정의하고 관리하는 자동화, 일관성, 반복 가능성을 확보한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.8372%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;선언적&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.1628%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;사용자는 무엇을 원하는지를 코드로 선언하면 테라폼이 어떻게 해당 상태에 도달할지(생성, 수정, 삭제)를 결정하고 실행한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.8372%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;멀티 클라우드 지원&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.1628%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;AWS, GCP 같은 주요 클라우드 서비스뿐만 아니라 쿠버네티스, 도커 기타 등 다양한 인프라 환경을 Provider 개념을 통해 단일 도구로 관리할 수 있다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼을 사용하는 이유&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인프라 관리의 효율성, 안정성, 자동화를 극대화하기 위해서입니다. 수동으로 콘솔을 클릭 필요 없이 코드로 실행하는 것만으로 복잡한 인프라를 자동으로 구축하고 변경할 수 있다. 새로운 환경을 구축하거나 인프라 변경이 필요할 때 미리 작성된 코드를 재사용하여 빠르게 반복적으로 배포도 가능하다. 인프라 정의가 코드로 존재하기 때문에 Git과 같은 버전 관리 시스템에 저장하여 변경 이력을 추적하고 팀원과 안전하게 협업할 수 있다. 이것만으로도 충분히 큰 장점이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼의 기본 동작 방식&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;HCL 언어로 원하는 인프라 리소스를 정의한 구성 파일을 작성하고 계획(plan) 명령을 실행하여 작성된 코드와 현재 인프라 상태를 비교하고 최종 상태에도 도달하기 위해 필요한 변경 사항을 미리 확인한다. 적용(apply) 명령을 실행하여 계획된 변경 사항을 실제 클라우드 인프라에 적용한다. 상태 파일(.tfstate)을 사용하여 실제 인프라의 현황과 코드에 정의된 원하는 상태를 매핑하고 추적한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위 내용처럼 인프라 관리에선 테라폼은 필수적인 요소이다. 테라폼 기본 개념과 간단한 실습을 통해 공부한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼 기본 개념&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 148px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 23.7209%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이름&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.2791%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.7209%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;resource&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.2791%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;실제로 생성할 인프라 자원을 의미한다 EC2, VPC, RDS 등 리소스를 블록을 사용하여 파일에 정의한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.7209%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;provider&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.2791%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼과 특정 인프라 서비스( AWS Azure 등)를 API와 상호작용할 수 있도록 연결해주는 플러그인이다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.7209%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;output&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.2791%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;배포 완료 후 외부에 노출하고 싶은 특정 정보를 정의한다. 생성된 EC2의 퍼플릭 IP주소 등을 다른 시스템이나 사용자에게 보여주기 위해 사용한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.7209%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;backend&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.2791%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼이 인프라의 현재 상태를 기록하는 상태 파일(.tfstate)을 저장할 위치를 설정한다. S3와 같은 원격 저장소에 저장하여 팀원간의 협업 및 안전성을 확보하는데 사용한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.7209%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;module&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.2791%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;재사용 가능하도록 여러 개의 리소스를 묶어 놓은 논리적 그룹이자 캡슐화 단위이다. 복잡한 인프라를 구조화하고 동일한 구성을 여러 환경에 반복적으로 배포할 때 유용하다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.7209%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;state file(tfstate)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.2791%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼이 현재까지 관리하고 배포한 인프라 자원의 실제 상태를 기록하는 파일이다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 23.7209%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;remote state&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.2791%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼에서 tfstate를 로컬이 아닌 원격 저장소에 보관하고 관리하는 방식을 의미한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;remote state의 필요성 및 역할&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;여러 팀원이 하나의 인프라를 동시에 관리할 때 각자의 로컬 상태 파일이 아닌 공유된 중앙 상태 파일을 사용해야 일관성을 유지하고 충돌을 방지할 수 있다. tfstate파일에는 인프라에 대한 중요한 정보(민감정보)가 포함되어 있다. 이를 안전한 클라우드 스토리지(S3)에 저장하여 로컬 유출 위험을 줄이고 백업 및 복구를 용이하게 한다. 여러 사용자가 동시에 apply를 실행하는 것을 방지하기 위해 원격 백엔드는 잠금 기능을 지원한다. 일관성을 잃는 것을 막아 준다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼 명령어&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;terraform init(초기화)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테라폼 작업 디렉터리를 초기화하고 구성 파일에 정의된 대로 작업을 수행할 준비를 마치는 명령어로 구성 파일에 명시된 Provider 플러그인과 그 버전을 검색하고 로컬에 다운로드한다. 백엔드 설정을 확인하고 상태 파일을 저장할 원격 저장소를 설정한다. 소스 코드를 참조하는 module을 준비한다 이 명령어는 테라폼 코드를 처음 작성하거나 새로운 Provider 또는 백엔드 설정 추가 변경했을 때 반드시 먼저 수행한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;terraform plan(계획 수립)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재 작성된 테라폼 코드와 실제 인프라 상태(tfstate) 파일 및 클라우드 서비스를 비교하여 앞으로 어떤 변경이 발생할지 실행 계획을 보여준다. 실제 인프라에 변경을 가하기 전에 오류가 없는지 미리 검토할 수 있는 안전한 기회를 제공한다. apply 실행 전에 plan을 실행하여 변경 사항을 확인하는 것은 인프라 운영의 필수 적이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;terraform apply(적용)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;plan명령으로 확인했던 실행 계획을 실제로 인프라에 적용하여 리소스를 생성 수정 또는 삭제한다. yes을 입력하여 적용을 승인하도록 요청합니다. 변경 사항이 성공적으로 적용되면 state file(tfstate)을 업데이트하여 인프라의 최종상태를 기록한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VPC를 테라폼을 이용해 코드로 구축 예제&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 123px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.6047%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이름&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.3953%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.6047%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;VPC&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.3953%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;AWS 클라우드에서 논리적으로 분리된 나만의 가상 네트워크 공간입니다. IP 주소 대역으 지정하여 생성하며 모든 AWS 리소스는 이 VPC내부에 존재합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.6047%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;서브넷&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.3953%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;VPC의 IP 주소 대역을 더 작게 나눈 네트워크 영역이다. 각 서브넷은 하나의 가용 영역에 속하며 용도에 따라 퍼플릭과 프라이빗으로 나눈다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.6047%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;라우팅 테이블&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.3953%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;서브넷 내부에서 발생하는 네트워크 트래픽이 어디로 향해야 하는지 규칙을 정의하는 테이블이다. 각 서브넷은 반드시 하나의 라우팅 테이블과 연결되어야한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 15px;&quot;&gt;
&lt;td style=&quot;width: 23.6047%; height: 15px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;인터넷 게이트웨이 IGW&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.3953%; height: 15px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;VPC와 외부 인터넷 간의 통신을 가능하게 하는 게이트웨이이다. 퍼블릭 서브넷은 IGW를 통해 직접 인터넷과 통신할 수 있다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.6047%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;NAT Gateway NGW&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.3953%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;프라이빗 서브넷에 있는 리소스가 외부 인터넷으로의 아웃바운드 통신은 가능하지만 외부에서 해당 리소스로 인바운드 접근은 불가능하도록 해주는 서비스이다. 퍼블릭 서브넷에 위치하며 외부 통신을 위해 EIP와 연경된다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 23.6047%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;EIP&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.3953%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;AWS에서 제공하는 고정된 공인 IP 주소로 NAT GW에 할당되어 외부 통신의 고정 IP역할을 한다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;main.tf&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1760925621649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resource &quot;aws_vpc&quot; &quot;test_vpc&quot; {
  cidr_block = &quot;10.0.0.0/16&quot;

  tags = {
    Name = &quot;test-101&quot;
  }
}

resource &quot;aws_subnet&quot; &quot;public_subnet&quot; {
  vpc_id     = aws_vpc.test_vpc.id
  cidr_block = &quot;10.0.1.0/24&quot;

  availability_zone = &quot;ap-northeast-2a&quot;

  tags = {
    Name = &quot;terraform-101-public-subnet&quot;
  }
}

resource &quot;aws_subnet&quot; &quot;private_subnet&quot; {
  vpc_id     = aws_vpc.test_vpc.id
  cidr_block = &quot;10.0.10.0/24&quot;

  tags = {
    Name = &quot;terraform-101-private-subnet&quot;
  }
}

resource &quot;aws_internet_gateway&quot; &quot;igw&quot; {
  vpc_id = aws_vpc.test_vpc.id

  tags = {
    Name = &quot;terraform-101-igw&quot;
  }
}

resource &quot;aws_eip&quot; &quot;nat&quot; {
  lifecycle {
    create_before_destroy = true
  }
}

resource &quot;aws_nat_gateway&quot; &quot;ngw&quot; {
  allocation_id = aws_eip.nat.id

  subnet_id = aws_subnet.public_subnet.id

  tags = {
    Name = &quot;terraform-101-ngw&quot;
  }
}

resource &quot;aws_route_table&quot; &quot;public&quot; {
  vpc_id = aws_vpc.test_vpc.id

  tags = {
    Name = &quot;terraform-101-rt-public&quot;
  }
}

resource &quot;aws_route_table_association&quot; &quot;route_table_association_public&quot; {
  subnet_id = aws_subnet.public_subnet.id
  route_table_id = aws_route_table.public.id
}

resource &quot;aws_route&quot; &quot;public_nat&quot; {
  route_table_id = aws_route_table.public.id
  destination_cidr_block = &quot;0.0.0.0/0&quot;
  gateway_id = aws_internet_gateway.igw.id
}

resource &quot;aws_route_table&quot; &quot;private&quot; {
  vpc_id = aws_vpc.test_vpc.id

  tags = {
    Name = &quot;terraform-101-rt-private&quot;
  }
}

resource &quot;aws_route_table_association&quot; &quot;route_table_association_private&quot; {
  subnet_id = aws_subnet.private_subnet.id
  route_table_id = aws_route_table.private.id
}

resource &quot;aws_route&quot; &quot;private_nat&quot; {
  route_table_id = aws_route_table.private.id
  destination_cidr_block = &quot;0.0.0.0/0&quot;
  nat_gateway_id = aws_nat_gateway.ngw.id
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;네트워크의 기본 틀&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 91px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25.0775%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드 리소스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.0775%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.8449%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;상세 설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;width: 25.0775%; height: 38px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;aws_vpc&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.0775%; height: 38px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;가상 네트워크 전체(VPC)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.8449%; height: 38px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;모든 AWS 리소스가 위치할 나만의 격리된 사설 네이트워크 공간 10.0.0.0/16&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.0775%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;aws_subnet.public_subnet&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.0775%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;퍼블릭 구역&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.8449%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;외부 인터넷과 직접 통신이 가능한 영역으로 웹 서버의 로드 밸런서 등이 위치 10.0.1.0/24&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25.0775%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;aws_subnet.private_subnet&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.0775%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프라이빗 구역&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.8449%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;외부에서 직접 접근할 수 없도록 보호되는 영역으로 DB 서버등 중요한 리소스가 위치 10.0.10.0/24&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;퍼블릭 서브넷을 위한 인터넷 게이트웨이&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.1938%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드 리소스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.31%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.4961%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;상세 설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.1938%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;aws_internet_gateway&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.31%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인터넷 정문&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.4961%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VPC와 AWS외부 인터넷을 연경해주는 통로&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.1938%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;aws_route(public_nat)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.31%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;퍼블릭 경로&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.4961%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;퍼블릭 서브넷의 모든 외부 트래픽(0.0.0.0/0)이 IGW를 통해 인터넷으로 나가도록 규칙을 만듬&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프라이빗 서브넷을 위한 NAT 게이트웨이&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.0774%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드 리소스&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.3101%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.6124%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;상세 설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.0774%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;aws_eip&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.3101%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;고정 공인 IP&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.6124%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;NAT GW에 할당할 고정 IP 주소&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.0774%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;aws_nat_gatway&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.3101%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프라이빗의 출구&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.6124%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;private_subnet의 서버들이 보안은 유지한 채 외부 인터넷으로 나갈수 있도록 돕는 장치&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.0774%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;aws_route(private_nat)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.3101%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프라이빗 경로&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.6124%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프라이빗 서브넷의 모든 외부 트래픽이 NAT GW를 통해서만 나가도록 규칙을 만든다. 외부에서는 내부로의 직접 접근은 불가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;aws_route_table는 트래픽 경로 규칙을 담는 그릇(컨테이너)이며 route는 그 컨테이너 안에 들어가는 특정 트래픽을 전송 방향을 정의하는 개별 규칙이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;variables.tf와 providers.tf은 AWS 환경에 리소스를 배포하는 데 필요한 기본 환경&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;provider.tf는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;hashicorp/aws 프로바이더를 사용하여 AWS 리소스를 관리하며, 리전은 정의된 변수(var.region)를 따른다.&lt;/span&gt;&lt;/span&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;terraform {
  required_version = &quot;&amp;gt;= 1.6.0&quot;
  required_providers {
    aws = {
      source  = &quot;hashicorp/aws&quot;
      version = &quot;~&amp;gt; 5.55&quot;
    }
  }
}

provider &quot;aws&quot; {
  region = var.region
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;variables.tf는 AWS 리전 변수를 정의하고, ap-northeast-2를 기본값으로 설정하여 한국 리전에 인프라가 구축되도록 한다.&lt;/span&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;variable &quot;region&quot; {
  description = &quot;AWS 리전&quot;
  type        = string
  default     = &quot;ap-northeast-2&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Backend</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/121</guid>
      <comments>https://seungwontech.tistory.com/121#entry121comment</comments>
      <pubDate>Fri, 17 Oct 2025 18:06:15 +0900</pubDate>
    </item>
    <item>
      <title>JPA에서 JDBC로 전환했더니 성능이 10배 빨라졌다</title>
      <link>https://seungwontech.tistory.com/120</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Batch로 배치 적재 기능을 개발하면서 Writer를 JPA로 구현할지, JDBC로 할지를 고민하게 되었습니다. 프로젝트에서 데이터 조회는 이미 JPA를 기반으로 하고 있었고 Spring Batch에서도 JpaItemWriter를 기본적으로 제공하므로 처음에는 자연스럽게 JPA를 선택했습니다. 실제로 코드도 간결했고 데이터량도 적다 보니 성능도 충분하다고 판단했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;872&quot; data-start=&quot;761&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하지만 Writer를 JDBC 기반으로 변경하면 어떤 차이가 있을지 궁금해져 JdbcBatchItemWriter로 동일한 작업을 수행해 봤고 결과적으로 예상 이상의 성능 향상을 확인했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;두 Writer의 성능을 비교하기 위해 청크 단위의 실행 시간을 측정하는 WriterTimingListener를 추가로 구현했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760419361770&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class WriterTimingListener implements ItemWriteListener&amp;lt;Weather&amp;gt; {

    private Instant startTime;

    @Override
    public void beforeWrite(Chunk&amp;lt;? extends Weather&amp;gt; items) {
        startTime = Instant.now();
        log.info(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Writer - 청크 쓰기 시작. 아이템 수: {}&quot;, items.size());
    }

    @Override
    public void afterWrite(Chunk&amp;lt;? extends Weather&amp;gt; items) {
        if (startTime == null) {
            log.warn(&quot;Writer - 시작 시간이 기록되지 않았습니다. afterWrite가 먼저 호출되었습니다.&quot;);
            return;
        }
        var endTime = Instant.now();
        var duration = Duration.between(startTime, endTime);
        var itemCount = items.size();

        log.info(&quot;&amp;lt;&amp;lt;&amp;lt;&amp;lt; Writer - 청크 쓰기 완료. 소요 시간: {} ms, 아이템 수: {}&quot;,
                duration.toMillis(), itemCount);

        if (itemCount &amp;gt; 0) {
            double avgTime = (double) duration.toMillis() / itemCount;
            log.info(&quot;---- Writer - 아이템당 평균 처리 시간: {} ms&quot;, String.format(&quot;%.3f&quot;, avgTime));
        }
    }

    @Override
    public void onWriteError(Exception exception, Chunk&amp;lt;? extends Weather&amp;gt; items) {
        log.error(&quot;!!!! Writer - 청크 쓰기 오류 발생. 아이템 수: {}, 오류: {}&quot;,
                items.size(), exception.getMessage());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;먼저 API 호출 후 가공한 뒤 insert를 합니다. 데이터는 290개 중 97개만 적재합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3번 반복 테스트를 통해 평균을 구해봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JpaItemWriter 코드입니다. 비교적 깔끔하다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760408808969&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public Step weatherStep(WeatherItemReader reader, WeatherItemProcessor processor, JpaItemWriter&amp;lt;Weather&amp;gt; weatherJpaWriter, WriterTimingListener writerTimingListener) {
    return new StepBuilder(JOB_NAME + &quot;weatherStep&quot;, jobRepository)
            .&amp;lt;WeatherItem, Weather&amp;gt;chunk(300, transactionManager)
            .reader(reader)
            .processor(processor)
            .writer(weatherJpaWriter)
            .listener(writerTimingListener)
            .faultTolerant()
            .retryLimit(3)
            .retry(CoreException.class)
            .build();

}

@Bean
public JpaItemWriter&amp;lt;Weather&amp;gt; weatherJpaWriter(EntityManagerFactory emf) {
    JpaItemWriter&amp;lt;Weather&amp;gt; w = new JpaItemWriter&amp;lt;&amp;gt;();
    w.setEntityManagerFactory(emf);
    w.setUsePersist(false);
    return w;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;평균 소요 시간은 121.67ms입니다. 아이템당 평균 처리 시간은 약 1.254ms&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;소요 시간: 122ms, 아이템 수: 97&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아이템당 평균 처리 시간: 1.258ms&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;소요 시간: 124ms, 아이템 수: 97&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아이템당 평균 처리 시간: 1.278ms&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;소요 시간: 119ms, 아이템 수: 97&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아이템당 평균 처리 시간: 1.227ms&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JdbcBatchItemWriter 코드입니다. SQL을 직접 작성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760415466462&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public Step weatherStep(WeatherItemReader reader, WeatherItemProcessor processor, JdbcBatchItemWriter&amp;lt;Weather&amp;gt; weatherJdbcWriter, WriterTimingListener writerTimingListener) {
    return new StepBuilder(JOB_NAME + &quot;weatherStep&quot;, jobRepository)
            .&amp;lt;WeatherItem, Weather&amp;gt;chunk(300, transactionManager)
            .reader(reader)
            .processor(processor)
            .writer(weatherJdbcWriter)
            .listener(writerTimingListener)
            .faultTolerant()
            .retryLimit(3)
            .retry(CoreException.class)
            .build();
}

@Bean
public JdbcBatchItemWriter&amp;lt;Weather&amp;gt; weatherJdbcWriter() {
    String sql = &quot;INSERT INTO OUTDOOR_WEATHER (COLLECT_DT, NX, NY, OUTDOOR_TYPE, VALUE, CREATE_DT) &quot; +
                 &quot;VALUES (:collectDt, :nx, :ny, :outdoorType, :value, NOW())&quot;;

    return new JdbcBatchItemWriterBuilder&amp;lt;Weather&amp;gt;()
            .dataSource(dataSource)
            .sql(sql)
            .itemSqlParameterSourceProvider(item -&amp;gt; new MapSqlParameterSource()
                    .addValue(&quot;collectDt&quot;, item.getCollectDt())
                    .addValue(&quot;nx&quot;, item.getNx())
                    .addValue(&quot;ny&quot;, item.getNy())
                    .addValue(&quot;outdoorType&quot;, item.getOutdoorType().name())
                    .addValue(&quot;value&quot;, item.getValue()))
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;평균 소요 시간은 11.33ms입니다. 아이템당 평균 처리 시간은 약 0.117ms&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;소요 시간: 13ms, 아이템 수: 97&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아이템당 평균 처리 시간: 0.134ms&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;소요 시간: 12ms, 아이템 수: 97&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아이템당 평균 처리 시간: 0.124ms&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;소요 시간: 9ms, 아이템 수: 97&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아이템당 평균 처리 시간: 0.093ms&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JdbcBatchItemWriter 사용 시, JpaItemWriter 대비 &lt;b&gt;전체 처리 시간이 약 90% 감소&lt;/b&gt;했고 &lt;b&gt;성능이 약 10배 이상 향상&lt;/b&gt;되었습니다. 성능 차이가 발생한 이유는 JPA가 엔티티 상태 관리, 영속성 컨텍스트 등 부가 작업을 동반하기 때문입니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;반면 JDBC는 ORM 관련 비용이 전혀 발생하지 않아 순수하게 데이터를 적재하는 작업에만 집중하기 때문입니다. Batch Writer에서는 JDBC 방식이 매우 효율적일 수 있다는 것을 이번 테스트를 통해 확인했습니다. JPA와 JDBC를 적절히 선택하는 전략이 필요하다는 걸 다시 한번 느끼게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;193&quot; data-start=&quot;161&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/120</guid>
      <comments>https://seungwontech.tistory.com/120#entry120comment</comments>
      <pubDate>Tue, 14 Oct 2025 10:44:56 +0900</pubDate>
    </item>
    <item>
      <title>가면사배 11장 뉴스 피드 시스템 설계</title>
      <link>https://seungwontech.tistory.com/119</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;뉴스 피드 시스템&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용자가 팔로우하거나 친구로 등록한 사람들의 최신 활동을 시간순 또는 추천 방식으로 보여주는 기능으로 대표적인 서비스로는 페이스북, 인스타 등이 있습니다. 11장은 이 뉴스 피드 시스템의 설계 방법을 다룹니다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;뉴스 피드 시스템의 주요 기능과 요구사항 정리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 웹 및 모바일 지원&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 피드 발행&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. 피드 읽기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. 최신순으로 표시&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5. 한 명의 사용자는 최대 5000명의 친구를 가질 수 있고 매일 천만 명이 방문&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;6. 이미지, 비디오 등의 미디어 파일도 포함&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;두 가지 핵심 부분&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. &lt;b&gt;피드 발행&lt;/b&gt;은 사용자가 스토리를 포스팅하면 데이터를 캐시와 DB에 기록하고 새 포스팅은 친구의 뉴스 피드에도 전송한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. &lt;b&gt;뉴스 피드 생성&lt;/b&gt;은 사용자가 뉴스 피드 화면을 요청했을 때 사용자가 최신 포스팅 목록을 보여주는 작업을 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;포스팅 전송(팬 아웃) 서비스&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용자의 새 포스팅을 친구 관계에 있는 모든 사용자에게 전달하는 과정으로 팬 아웃에는 2가지 모델이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;쓰기 시점에 팬아웃과 읽기 시점에 팬아웃이다.&amp;nbsp;&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;쓰기 시 팬아웃 (푸시 모델)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; 새로운 포스팅을 발행할 때 친구들의 뉴스 피드 캐시에 미리 저장한다. 친구들이 뉴스 피드를 가져오는 응답시간이 짧아진다. 하지만 친구가 많은 사용자인 경우 사용자 모두의 뉴스 피드를 갱신하는데 많은 시간이 소요될 수 있어 특정 서버에 부하가 집중될 수 있다(핫키 문제). 그리고 서비스를 자주 이용하지 않은 사용자의 피드까지 갱신하기에 자원이 낭비된다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;읽기 시 팬아웃 (풀 모델)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용자가 뉴스 피드를 요청할 때 친구들의 포스팅을 DB나 캐시에서 실시간으로 가져와 취합한다. 데이터를 미리 친구에 푸시하는 작업이 필요 없어 핫키 문제도 생기지 않고 비활성한 사용자에는 미리 데이터를 저장할 필요가 없다.(저장 공간도 절약) 하지만 요청 시마다 복잡한 조회 및 정렬 작업으로 인해 응답 시간이 길어질 수 있다.&amp;nbsp;&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;절충안&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;책에서는 두 가지 방법을 결합하여 장점은 취하고 단점은 버리는 전략으로 구성한다. 먼저 대부분의 일반 사용자에 대해서는 푸시 모델을 사용한다. 팔로워 수가 매우 많은 사용자의 경우에는 풀 모델을 사용하여 시스템 과부하를 방지한다. 그리고 안정 해시를 통해 요청과 데이터를 보다 고르게 분산하여 핫키 문제를 줄여준다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;1684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b02UFe/dJMb8WZJVq2/YUCXnuwHDMPJLfzhkKYFwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b02UFe/dJMb8WZJVq2/YUCXnuwHDMPJLfzhkKYFwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b02UFe/dJMb8WZJVq2/YUCXnuwHDMPJLfzhkKYFwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb02UFe%2FdJMb8WZJVq2%2FYUCXnuwHDMPJLfzhkKYFwk%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;1744&quot; height=&quot;1684&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;1684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;- Dispatcher:글을 누가 썼는지 보고&amp;nbsp;푸시 방식으로 뿌릴지, 그냥 저장만 할지 결정하는 역할&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;-&amp;nbsp;Aggregator:&amp;nbsp;캐시에 없으면 DB/샤드에서 글을 가져와서&amp;nbsp;정렬해서 보여주는 역할&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;비유 설명) 인스타 사용자 유형 Dispatcher 판단 처리 방식&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 60px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용자 유형&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #2780d4; color: #ffffff; text-align: left; font-family: 'Nanum Gothic';&quot;&gt;Dispatcher 판단&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #2780d4; color: #ffffff; text-align: left; font-family: 'Nanum Gothic';&quot;&gt;처리 방식&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: left; font-family: 'Nanum Gothic';&quot;&gt;일반 사용자 (100명 팔로워)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&quot;바로 팬들에게 저장하자&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;푸시 모델&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #efefef; color: #333333; text-align: left; font-family: 'Nanum Gothic';&quot;&gt;연예인 (300만 팔로워)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: left; font-family: 'Nanum Gothic';&quot;&gt;&quot;미리 저장하지 말고 요청 때 보여주자&quot;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;풀 모델&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;핫키와 안정해시 대한 보충 이해&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;핫키 문제&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;특정 데이터나 서버에 트래픽이 비정상적으로 집중되어 해당 서버가 과부하로 느려지거나 다운되는 현상을 말한다. 예시로 팔로워가 수백만 명인 유명인이 포스팅을 올렸고 이 포스팅 데이터를 서버 1에 저장, 수백만 명의 팔로워들이 동시에 이 포스팅을 보려고 서버 1에 요청을 보내며 다른 서버들은 한가한데 서버 1만 갑자기 엄청난 부하를 받아 시스템 전체의 병목 현상이 발생한다. 이때 유명인의 데이터가 핫키가 된다.&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;안정해시&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;안정 해시는 요청이나 데이터를 서버에 할당할 때 서버가 추가되거나 제거되더라도 데이터의 재분배를 최소화하면서 부하를 비교적 고르게 분산시키는 기술이다&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;안정 해시의 기본 개념&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데이터를 서버에 저장하거나 요청을 서버로 보낼 때 보통 해시 함수를 사용한다.&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 일반적인 해시 방식의 문제&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용자 ID를 해시하여 서버 N대에 분산한다고 가정한다. 만약 서버 N 중 하나가 고장나서 N-1대가 되면 N-1대의 서버에 있는 거의 모든 데이터를 재배치해야 한다. 엄청난 시간과 자원이 낭비된다.&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 안정 해시의 해결책&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;안정 해시는 서버와 데이터를 가상의 원 위에 매핑한다. 데이터 K는 원 위에서 K의 위치로부터 시계 방향으로 가장 가까운 서버에 할당한다. 서버가 추가되거나 제거되더라도 주변의 서버에 있는 데이터만 재분배하면 되고 전체 데이터의 재분배는 발생하지 않는다. 즉 100개의 데이터가 N1 ~ N5 노드에 20개씩 분산되어 저장되어 있다고 가정한다면 1번 노드가 죽으면 가까운 2번 노드에 다시 매핑하면 2번 노드는 40개의 데이터를 담고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발서적</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/119</guid>
      <comments>https://seungwontech.tistory.com/119#entry119comment</comments>
      <pubDate>Thu, 9 Oct 2025 22:19:00 +0900</pubDate>
    </item>
    <item>
      <title>다이어그램으로 보는 AWS VPC 통신 정리</title>
      <link>https://seungwontech.tistory.com/118</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;AWS VPC&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VPC는 AWS라는 거대한 대지 위에 세우는 나만의 개인 단지입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; 누구나 들어올 수 있는 광장이 아니라, 내가 허락한 사람만, 내가 만든 규칙대로만 움직일 수 있는 가상 네트워크 공간입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;여기에 EC2, RDS 등을 올려 보안적으로 통제된 환경을 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;퍼블릭/프라이빗 서브넷 구분&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 55px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인터넷 접속&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예시 용도&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;필요 구성요소&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;퍼블릭 서브넷&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;외부에서 직접 접속 가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;웹 서버/로드밸런서&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인터넷 게이트웨이 + 퍼블릭IP&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프라이빗 서브넷&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;외부에서 직접 접속 불가&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;DB/내부 API/ 백엔드 작업 서버&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;NAT 게이트웨이(나갈때만)&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;인터넷 게이트웨이, NAT, 라우팅 정리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.3488%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 74.6512%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설명&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.3488%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;Internet Gateway(IGW)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 74.6512%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;양방향으로 퍼블릭 서브넷이 인터넷과 연결되게 해 줍니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.3488%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;NAT Gateway&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 74.6512%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;단방향으로 내부에서 외부로 프라이빗 서브넷이 나갈 수 있게 해 줍니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25.3488%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Route Table&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 74.6512%;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;퍼블릭, 프라이빗에 애플리케이션 서버 구성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;AWS 권장은 ALB는 퍼블릭, 애플리케이션 서버는 프라이빗입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하지만 소규모는 퍼블릭 서브넷에 넣어서 해도 된다. NAT GW가 비용이기 때문입니다,&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;서버 흐름도&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사용자 &amp;rarr; IGW &amp;rarr; ALB(퍼블릭) &amp;rarr; 서버(프라이빗) &amp;rarr; RDS(프라이빗) &amp;rarr; 응답 반환(같은 경로 반대로)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;여기서 NAT GW는 ALB가 프록시 역할을 해서 통신을 중개해 주기 때문에 NAT 필요 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;IGW는 오직 퍼블릭 IP가 있는 리소스만 인터넷과 통신하게 해 줍니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프라이빗 서버는 IGW를 직접 사용할 수 없고 외부로 나가야 할 경우 반드시 NAT GW를 거쳐야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bctLSs/btsQ3hKqo4g/WDoyIwFfkrrGFwcpHkKh70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bctLSs/btsQ3hKqo4g/WDoyIwFfkrrGFwcpHkKh70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bctLSs/btsQ3hKqo4g/WDoyIwFfkrrGFwcpHkKh70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbctLSs%2FbtsQ3hKqo4g%2FWDoyIwFfkrrGFwcpHkKh70%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;1188&quot; height=&quot;613&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;NAT GW 사용&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;애플리케이션 서버에서 외부 API 호출할 때 필요합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예를 들면 스케줄러(Cron)로 실행된 배치가 외부 API 호출할 때 필요합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;즉 애플리케이션이 직접 외부로 나가야 하기 때문이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;SNAT은 NAT GW가 프라이빗 서버의 사설 IP를 자신의 공인 IP로 바꿔서 인터넷으로 대신 보내줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프라이빗 &amp;rarr; NAT &amp;rarr; IGW &amp;rarr; 인터넷&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1085&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnp3Vl/btsQ4VTUHHh/KY8lbknKGVZ0JnVB6WwUZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnp3Vl/btsQ4VTUHHh/KY8lbknKGVZ0JnVB6WwUZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnp3Vl/btsQ4VTUHHh/KY8lbknKGVZ0JnVB6WwUZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbnp3Vl%2FbtsQ4VTUHHh%2FKY8lbknKGVZ0JnVB6WwUZk%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;1085&quot; height=&quot;507&quot; data-origin-width=&quot;1085&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Route 테이블&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;어떤 IP 대역으로 가는 트래픽을 어디로 보낼지 결정하는 네트워크의 길안내 지도이며 퍼블릭/ 프라이빗 서브넷은 CIDR 대역으로 구분하는 것이 아니라, 라우터 테이블에서 0.0.0.0/0이 어디로 향하느냐 IGW, NAT에 따라 구분됩니다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;라우터 테이블이 하는 일&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 75px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 32.9845%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;상황&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;라우터 테이블 역할&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;비유&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 32.9845%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;퍼블릭 서브넷에서 인터넷으로 나가야 할 때&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;0.0.0.0/0을 IGW로 보냄&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;모르는 주소는 다 정문으로&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 32.9845%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;프라이빗 서브넷에서 외부 API 요청&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;0.0.0.0/0에서 NAT GW로 보냄&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;밖으로 나갈 건 주방 뒷문으로&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 32.9845%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;내부 통신( 같은 VPC안 EC2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.6821%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VPC CIDR에서 Local로 유지&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 18px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;만약 프라이빗 서버에 퍼블릭 IP를 할당한다면?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;비용으로 인해 NAT GW를 사용하지 않기 위해 프라이빗 서버에 퍼블릭 IP를 추가로 할당해 NAT GW를 사용하지 않다면 프라이빗 서버가 아닌 퍼블릭 서버입니다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;프라이빗 서버를 사용하는 이유&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;인터넷 어디서든 접속 시도가 가능해집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; 보안 그룹으로 막아도 포트 스캐닝 공격이 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; 설계상 프라이빗 서브넷은 외부에서 절대 접근 불가능해야 하는데 퍼블릭 IP를 붙이면 컨셉 자체가 붕괴됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; 모든 외부 요청이 직접 해당 서버로 들어오니 로드밸런싱이나 방화벽 통제가 불가능합니다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;CIDR 대역란&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;IP 주소를 범위로 표현하는 방식&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 57px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 27.9845%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;표현&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.6821%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;의미&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;범위&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 27.9845%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;10.0.0.0/16&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.6821%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;앞의 16비트(10.0) 고정,나머지는 변경가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;10.0.0.0 ~ 10.0.255.255(약 65,536개 IP)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 27.9845%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;10.0.1.0/24&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.6821%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;앞의 24비트(10.0.1) 고정, 마지막만 변경 가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;10.0.1.0 ~ 10.0.1.255(256개 IP)&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;VPC와 서브넷의 관계(CIDR)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VPC는 사설 네트워크 공간 전체 서브넷은 그 공간을 분할한 더 작은 네트워크입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; VPC CIDR을 10.0.0.0/16으로 지정하면 이 VPC에서는 10.0.0.0 ~ 10.0.255.255 범위의 IP만 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; 서브넷은 이 범위안에서 /24, /20등으로 더 잘게 쪼개어 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>seungwonlee</author>
      <guid isPermaLink="true">https://seungwontech.tistory.com/118</guid>
      <comments>https://seungwontech.tistory.com/118#entry118comment</comments>
      <pubDate>Wed, 8 Oct 2025 02:38:25 +0900</pubDate>
    </item>
  </channel>
</rss>