<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>자바시작 워니</title>
    <link>https://oneny.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 5 Aug 2026 19:00:54 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>oneny</managingEditor>
    <item>
      <title>가상 스레드와 ForkJoinPool - 1</title>
      <link>https://oneny.tistory.com/146</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;가상&amp;nbsp;스레드와&amp;nbsp;ForkJoinPool&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 스레드는 고성능의 동시성 애플리케이션을 지원합니다.&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 가상 스레드 구현을 이해하기 위해서는 Work-Stealing 알고리즘을 사용하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;포크조인 풀(ForkJoinPool)&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;과 JDK 19에 가상 스레드와 함께 JDK 내부에 추가된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;컨티뉴에이션(Continuation)&lt;/b&gt;을 알아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 글에서는 포크조인 풀에 대해 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Virtual Thread 스케줄러 아키텍처&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1882&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1vH1E/dJMcadXfGBo/CXOB3PV4QtzNvR8ESBRN10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1vH1E/dJMcadXfGBo/CXOB3PV4QtzNvR8ESBRN10/img.png&quot; data-alt=&quot;가상 스레드는 ForkJoinPool에 제출 -&amp;amp;gt; 캐리어에 마운트 -&amp;amp;gt; 블로킹하면 unmount되어 힙에서 대기하다 준비되면 다시 제출&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1vH1E/dJMcadXfGBo/CXOB3PV4QtzNvR8ESBRN10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1vH1E%2FdJMcadXfGBo%2FCXOB3PV4QtzNvR8ESBRN10%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;782&quot; height=&quot;875&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1882&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가상 스레드는 ForkJoinPool에 제출 -&amp;gt; 캐리어에 마운트 -&amp;gt; 블로킹하면 unmount되어 힙에서 대기하다 준비되면 다시 제출&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 가상 스레드 스케줄러 아키텍처로, 스케줄러가 두 층으로 나뉘어 있음을 보여줍니다. JDK 스케줄러는 가상 스레드를 캐리어에 배정하고, 운영체제 스케줄러는 캐리어를 코어에 배정합니다. 캐리어는 평범한 플랫폼 스레드라 운영체제와 1:1로 대응하고, M:N 매핑은 맨 윗단 JVM 상에서 일어납니다. 그것을 가능하게 하는 것이 unmount로 블로킹 지점에서 가상 스레드가 캐리어를 반납하기 때문에 적은 수의 캐리어로 동시에 많은 가상 스레드를 소화할 수 있습니다.&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;플랫폼 스레드(Platform Thread)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바가 처음 만들어졌을 때부터 존재하던 네이티브 스레드(native thread)입니다. JDK에서 공싱적인 이름은 플랫폼 스래드로 운영체제에 의해 실행되는 무거운 스레드이며 스케줄링과 관리를 운영체제에 의존하고, 자바 스레드와 커널 스레드 사이에 1:1 관계를 유지합니다. 즉, 자바 코드는 기반이 되는 OS 스레드 위에 실행되어 운영체제의 스케줄링과 컨텍스트 전환 메커니즘을 활용합니다. 사용 가능한 플랫폼 스레드의 수는 OS 스레드의 수에 의해 제한됩니다. 또한, 플랫폼 스레드는 일반적으로 큰 스레드 스택과 운영체제가 관리하는 기타 리소스를 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 스레드는 위 그림처럼 이 플랫폼 스레드 내부에서 실행되며, 이렇게 플랫폼 스레드가 가상 스레드를 실행해 주는 역할을 '캐리어 스레드(carrier thread)'라고 합니다.&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;가상 스레드(Virtual Thread)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/openjdk/loom&quot;&gt;프로젝트 룸(Loom)&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;은 자바 및 JVM 생태계의 최신 프로젝트로 기존 자바 동시성 모델의 한계를 해결하기 위해 가상 스레드가 JDK 21부터 새로 도입된 자바의 동시성 모델로 도입되었습니다. 전적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;JVM에 의해 관리&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;되어 직접적으로 커널 스레드와 매핑되지 않고 다수의 가상 스레드가 플랫폼 스레드(캐리어 스레드) 풀을 공유합니다. 이 덕분에&lt;span&gt;&amp;nbsp;JVM이 상대적으로 적은 운영체제 자원으로도 많은 수의 가상 스레드를 효율적으로 다중화(multiplex)할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 스레드는 플랫폼 스레드와 아래와 같은 차이를 가지고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가벼움&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수백만 개의 가상 스레드를 생성하더라도 시스템 자원이 고갈되지 않을 정도로 플랫폼 스레드에 비해 훨씬 적은 양의 메모리를 사용하고 시스템 자원을 더 적게 소모합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스케줄링&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JVM에 의해 스케줄링되므로 CPU 사이클을 낭비 없이 더 잘 사용할 수 있고, 운영체제 스레드 스케줄링에서 발생하는 오버헤드를 피할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;블로킹 허용 능력&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;블로킹 연산 수행 시 시스템 자원을 점유하지 않고 제어권을 캐리어 스레드에게 넘겨 다른 가상 스레드들이 계속 실행될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ForkJoinPool&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포크조인풀은 Java 7에 도입된 특수한 스레드 풀로, 일반 스레드 풀과 달리 &lt;b&gt;워커마다 개인 작업 큐(데크, deque)를 두고&lt;/b&gt;, 할 일이 없는 워커가 바쁜 워커의 큐에서 &lt;b&gt;작업을 훔쳐오는(work-stealing) 방식으로 동작&lt;/b&gt;합니다. 원래는 분할 정복 병렬 연산용으로 설계되었지만, 현재는 병렬 스트림, CompletableFuture를 넘어 Virtual Thread 스케줄러의 구현체를 담당하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ThreadPoolExecutor 같은 일반 풀은 모든 워커가 하나의 공유 큐에서 작업을 꺼내 워커가 많아지면 락 경합 지점이 될 수 있는 구조적 문제를 해결하기 위해 설계되었습니다.&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;ForkJoinPool 동작 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워커마다 개인 데크(deque, 양방향 큐)를 가집니다.&lt;/b&gt; 워커가 작업을 실행하다가 fork()로 하위 작업을 만들면 자기 데크의 한쪽 끝(head)에 넣고, 다음 작업도 같은 끝(head)에서 꺼냅니다(LIFO 방향). 자기 큐를 자기만 만지는 동안에는 경합이 거의 없고, 방금 넣은 작업을 바로 꺼내므로 &lt;b&gt;CPU 캐시 지역성도 좋습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데크가 빈 워커는 놀지 않고 &lt;b&gt;다른 워커의 작업을 &lt;/b&gt;훔치는 Work-Stealing 기반으로 동작합니다.&lt;/b&gt; 다른 워커의 데크 &lt;b&gt;반대쪽 끝(tail)&lt;/b&gt;에서 작업을 가져갑니다(FIFO 방향). 이 덕분에 충돌할 확률을 최소화할 수 있고, tail에 있는 작업은 가장 먼저 fork된 &quot;큰 덩어리&quot;일 가능성이 높아, 한 번 훔치면 오래 일할 수 있어 훔치는 빈도 자체가 줄어듭니다. 부하 분산과 낮은 경합을 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부 제출은 별도 제출 큐로 들어옵니다.&lt;/b&gt; 풀 밖에서 submit()된 작업은 공유 큐(실제로는 해시 분산된 여러 큐)에 들어가고, 워커들이 자기 데크가 비면 여기서도 가져갑니다.&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;ForkJoinPool 사용&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1785324572110&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class FibonacciNumberWithForkJoinPool {

    private static final Map&amp;lt;Integer, Long&amp;gt; cache = new ConcurrentHashMap&amp;lt;&amp;gt;(
            Map.of(0, 0L, 1, 1L));

    static class FibonacciTask extends RecursiveTask&amp;lt;Long&amp;gt; { // 1
        private final int n;

        public FibonacciTask(int n) {
            this.n = n;
        }

        @Override
        protected Long compute() {
            if (cache.containsKey(n)) {
                return cache.get(n);
            }

            FibonacciTask f1 = new FibonacciTask(n - 1);
            f1.fork(); // 2
            FibonacciTask f2 = new FibonacciTask(n - 2);
            long result = f2.compute() + f1.join(); // 3
            cache.put(n, result);
            return result;
        }
    }

    public static void main(String[] args) {
        try (var pool = new ForkJoinPool(2)) {
            Long result = pool.invoke(new FibonacciTask(5));
            System.out.println(&quot;Fibonacci number is: &quot; + result);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ForkJoinPool은 결과가 있는 경우 RecursiveTask&amp;lt;V&amp;gt;, 결과가 없는 경우 RecursiveAction을 상속해 &lt;b&gt;compute() 안에서 충분히 작으면 직접 계산, 크면 반으로 쪼개 fork() 후 join()&lt;/b&gt;을 구현합니다. 위 코드는 ForkJoinPool의 분할(fork) / 직접 계산과 합류(compute + join) / work-stealing을 하나로 확인하면서 피보나치 계산을 수행하는 대표 예시 중 하나입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785402833836&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static class FibonacciTask extends RecursiveTask&amp;lt;Long&amp;gt; {&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RecursiveTask&amp;lt;V&amp;gt;는 위에서 설명했듯이 결과를 반환하는 ForkJoinTask입니다. 태스크는 스레드가 아닌 상태를 가진 객체이고, 어떤 워커가 이 객체의 compute()를 자기 콜 스택에서 호출하게 됩니다. 그래서 FibonacciTask(4)(줄여서 F(4)) 태스크 객체는 worker-1가 만들었지만 worker-2의 스택에서 실행될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785402978836&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FibonacciTask f1 = new FibonacciTask(n - 1);
f1.fork(); // F(4)를 내 데크 head에 push&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fork()는 현재 워커의 데크 head(top)에 push하고 바로 리턴하는 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785403040588&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FibonacciTask f2 = new FibonacciTask(n - 2);
long result = f2.compute() + f1.join();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 하위 작업을 모두 fork할 수도 있지만, 그러면 워커 자신은 push만 하고 놀게 되기 떄문에 큐를 거치는 오버헤드 없이 하나는 즉시 처리하는 방식입니다. 또한, compute과 join 순서가 중요한데 f1.join() + f2.compute()으로 진행하게 되면 자기 일을 시작하기 전에 join 대기부터 하게 되어 병렬성을 스스로 깎아먹게 됩니다.&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;스크린샷 2026-08-02 오전 9.42.58.png&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;1736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caTIMA/dJMcadbMaKR/hLz1S9fUMZD0BD88gzYhLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caTIMA/dJMcadbMaKR/hLz1S9fUMZD0BD88gzYhLK/img.png&quot; data-alt=&quot;F(5) 실행 시 worker-1의 덱이 채워지고, 유휴 worker-2가 tail에서 가장 큰 태스크를 훔쳐가는 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caTIMA/dJMcadbMaKR/hLz1S9fUMZD0BD88gzYhLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaTIMA%2FdJMcadbMaKR%2FhLz1S9fUMZD0BD88gzYhLK%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;1842&quot; height=&quot;1736&quot; data-filename=&quot;스크린샷 2026-08-02 오전 9.42.58.png&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;1736&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;F(5) 실행 시 worker-1의 덱이 채워지고, 유휴 worker-2가 tail에서 가장 큰 태스크를 훔쳐가는 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 실행 시의 시퀀스 다이어그램으로,&amp;nbsp;pool.invoke(new FibonacciTask(5))가 호출되면 F(5)라는 하나의 큰 작업이 worker-1에게 배정되고, worker-1은 이를 F(4)와 F(3)으로 분할합니다. 이때, worker-1은 fork()를 호출해서 자기 데크에 F(4)를 push하고, &lt;b&gt;유휴worker-2 워커 스레드가 그것을 훔쳐&lt;/b&gt;갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-07-30 오후 5.49.40.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UmF3Y/dJMcai5dBgK/1XUCLbaVXTl3Gx6eHo37u0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UmF3Y/dJMcai5dBgK/1XUCLbaVXTl3Gx6eHo37u0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UmF3Y/dJMcai5dBgK/1XUCLbaVXTl3Gx6eHo37u0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUmF3Y%2FdJMcai5dBgK%2F1XUCLbaVXTl3Gx6eHo37u0%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;688&quot; height=&quot;364&quot; data-filename=&quot;스크린샷 2026-07-30 오후 5.49.40.png&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;638&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;pre id=&quot;code_1785401787579&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;while (taskB.status != DONE) {
    ① taskB가 아직 내 데크에 있나? &amp;rarr; 있으면 내가 직접 꺼내서(exec) 실행
    ② 도둑맞았나? &amp;rarr; 도둑의 데크에서 관련 태스크를 가져와 대신 실행 (helping)
    ③ 도울 것도 없나? &amp;rarr; 그때 비로소 보상 후 진짜 park
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 join()을 실행 시 join 대상을 도둑 맞았다고 하더라도 해당 메서드는 &quot;스레드를 재우는 명령&quot;이 아니라 상태 확인 루프가 있는 일반 메서드 호출입니다. 태스크의 실행이란 결국 워커 스레드 스택 위에서 exec() 메서드를 호출하는 것뿐이므로, &lt;b&gt;join 지점에서 워커는 자기 스택 위에 다른 태스크의 프레임을 중첩으로 쌓아 실행(helping)&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, ForkJoinTask는 상태 필드(volatile status)를 가진 객체이고, join을 대기 중인 태스크는 위 다이어그램 및 루프처럼 &lt;b&gt;스택 아래쪽 프레임에 정지된 채 그대로 보존&lt;/b&gt;되고, 중첩 실행이 반환되어 내려오면 완료 여부를 재확인하고 재개됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Virtual Thread와의 관계&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1785235890762&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// java.lang.VirtualThread.java
final class VirtualThread extends BaseVirtualThread {
    private static final Unsafe U = Unsafe.getUnsafe();
    private static final ContinuationScope VTHREAD_SCOPE = new ContinuationScope(&quot;VirtualThreads&quot;);
    private static final ForkJoinPool DEFAULT_SCHEDULER = createDefaultScheduler();
    
    // ...
    
    private static ForkJoinPool createDefaultScheduler() {
        ForkJoinWorkerThreadFactory factory = pool -&amp;gt; new CarrierThread(pool);
        int parallelism, maxPoolSize, minRunnable;
        String parallelismValue = System.getProperty(&quot;jdk.virtualThreadScheduler.parallelism&quot;);
        String maxPoolSizeValue = System.getProperty(&quot;jdk.virtualThreadScheduler.maxPoolSize&quot;);
        String minRunnableValue = System.getProperty(&quot;jdk.virtualThreadScheduler.minRunnable&quot;);
        if (parallelismValue != null) {
            parallelism = Integer.parseInt(parallelismValue);
        } else {
            parallelism = Runtime.getRuntime().availableProcessors();
        }
        if (maxPoolSizeValue != null) {
            maxPoolSize = Integer.parseInt(maxPoolSizeValue);
            parallelism = Integer.min(parallelism, maxPoolSize);
        } else {
            maxPoolSize = Integer.max(parallelism, 256);
        }
        if (minRunnableValue != null) {
            minRunnable = Integer.parseInt(minRunnableValue);
        } else {
            minRunnable = Integer.max(parallelism / 2, 1);
        }
        Thread.UncaughtExceptionHandler handler = (t, e) -&amp;gt; { };
        boolean asyncMode = true; // FIFO
        return new ForkJoinPool(parallelism, factory, handler, asyncMode,
                     0, maxPoolSize, minRunnable, pool -&amp;gt; true, 30, SECONDS);
    }

    // ...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 JDK 25의 &lt;a href=&quot;https://github.com/openjdk/jdk25u/blob/master/src/java.base/share/classes/java/lang/VirtualThread.java#L1429&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.VirtualThread.java&lt;/a&gt;로 가상 스레드의 기본 스케줄러는 ForkJoinPool 인스턴스를 사용하고 있는 것을 확인할 수 있다. 하지만, JVM 전체가 공유하는 기본 인스턴스인 ForkJoinPoo.commonPool()과 달리 별개의 ForkJoinPool 전용 인스턴스로 두 가지 중요한 차이가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FIFO 모드(asyncMode)로 동작합니다.&lt;/b&gt; 일반적인 ForkJoinPool은 방금 쪼갠 하위 작업 먼저 진행하는 분할 정복용 LIFO 방식이지만, 수십만 개의 독립적인 요청을 처리하는 서버에서 LIFO를 쓰면 먼저 온 요청이 한없이 밀리는 불공정이 생길 수 있습니다. 따라서 도착 순서 공정성을 제공하기 위해 FIFO 방식으로 자기 데크에서도 오래된 것부터 꺼내 대기 시간을 고르게 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워커(carrier thread) 수가 기본적으로 CPU 코어 수와 같습니다.&lt;/b&gt; 가상 스레드는 위에서 살펴본 RecuriveTask와 달리 Continuation이라는 실행 태스크(다음 블로킹 지점까지 진행시키는 단위)를 가지고 가상 스레드가 시작되거나 unpark될 때마다 태스크가 하나 제출되고, 실행 중 블로킹을 만나면 태스크는 unmount되어 carrier를 반납하고, 힙에서 대기하게 됩니다. 이 덕분에 적은 수의 carrier로도 방대한 가상 스레드를 소화할 수 있습니다.&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;기존 ThreadPoolExecutor의 한계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 스레드는 1:1 매핑이 아닌 다수의 경량 실행 단위를 소수의 OS 스레드 위에서 돌리는 M:N 모델을 택했습니다. 그러면 다른 ThreadPoolExecutor가 있는데도 불구하고 ForkJoinPool을 선택했을까요? 이는 기존 스레드 스케줄러가 가진 한계를 알아봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 ThreadPoolExecutor는 모든 워커가 하나의 BlockingQueue에서 take()를 경쟁합니다. 즉, 테스크를 공유 큐(shared queue)에 넣고, 유휴 스레드가 태스크를 가져가기 위해 경쟁하게 되고 오버헤드가 발생할 수 있습니다. 요청 수천 개 수준에서는 문제가 없을 수 있지만, 가상 스레드처럼 mount/unmount가 초당 수백만 번 일어나는 환경에서는 그 큐의 head가 전 시스템의 병목 지점이 될 수 있습니다.&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;ForkJoinPool을 선택한 이유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;M:N 모델을 택하기 위해서는 &lt;b&gt;누가 어떤 코어에서 실행할지&lt;/b&gt;를 커널 대신 런타임이 직접 결정해야 합니다. 이 배정을 하나의 공유 큐로 처리하면 그 큐의 head가 전 시스템의 경합 지점이 되므로, 배정 자체를 코어별로 분산해야 합니다. 따라서 분산, 균형, 저비용을 동시에 만족시키는 답이 코어별 로컬 큐 + work-stealing이며, 자바에는 이미 그 자료구조를 20여 년간 다듬어온 ForkJoinPool이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로, Go, Kotlin 코루틴, Rust Tokio 등 주요 런타임들도 M:N 모델을 지원하기 위해 서로 독립적으로 코어별 로컬 큐 + work-stealing이라는 같은 자료구조를 사용하고 있습니다. 마찬가지로 Loom은 그 자료구조를 취하고 위에서 살펴봤듯이 공정성을 위해 FIFO로 바뀌었고, 워커는 CarrierThread로 교체되었으며, 캐리어 보상이라는 서버 워크로드용 정책만 새로 얹었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://senoritadeveloper.medium.com/javas-virtual-vs-platform-threads-and-what-s-new-in-jdk-24-22de93f51a74&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java's Virtual vs. Platform Threads and What's New in JDK 24&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-DC4306FC-D6C1-4BCC-AECE-48C32C1A8DAA&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Virtual Threads 가상 스레드&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000219305989&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;모던 자바 동시성 프로그래밍&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://xpmxf4.tistory.com/119&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java ForkJoinPool의 Work-Stealing 알고리즘 완벽 가이드: 고성능 병렬 처리의 비밀&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://upcurvewave.tistory.com/653&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Java 병렬 처리 ForkJoinPool 기본 작동 원리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/@cs.vivekgupta/overview-of-fork-join-framework-core-of-parallelism-in-java-35f4a4cc8c3b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Overview Of Fork-Join Framework - Core of Parallelism in Java&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-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>Java/Java</category>
      <category>FJP</category>
      <category>ForiJoinPool</category>
      <category>virtual thread</category>
      <category>VT</category>
      <category>work-stealing</category>
      <category>가상 스레드</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/146</guid>
      <comments>https://oneny.tistory.com/146#entry146comment</comments>
      <pubDate>Sun, 2 Aug 2026 18:44:32 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch 성능 최적화 기법</title>
      <link>https://oneny.tistory.com/144</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Reader 성능 개선&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JpaPagingItemReader를 사용하면 쿼리 조회 시에 LIMIT OFFSET 방식을 사용하기 때문에 OFFSET 수가 커질수록 읽어야할 데이터 수가 많아져 성능에 문제가 생길 수 있다. Spring Batch의 ItemReader는 대량의 데이터를 읽을 때 LIMIT OFFSET 방식의 속도 개선 방법으로는 No-Offset 기반과 Cursor 기반 두 가지 접근 방식을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;No-Offset 방식은 '&lt;a href=&quot;https://oneny.tistory.com/110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;페이징 성능 개선: offset vs no offset vs covering index&lt;/a&gt;' 글에 작성한 적이 있듯이 Offset을 사용하지 않고 인덱스를 활용해 효율적으로 페이지 단위 조회를 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor 방식은 DB 커넥션을 유지한 채, 결과 집합(ResultSet)을 스트리밍 형태로 순차 조회하는 방식을 말한다. Spring Batch에서는 JdbcCursorItemReader나 JpaCursorItemReader가 이 방식을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;No-Offset 방식&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f6f8fa; color: #24292e; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;AND id &amp;lt; 마지막조회ID # 직전 조회 결과의 마지막 id
ORDER BY id DESC
LIMIT 페이지사이즈&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;No-Offset 방식은 기존 Offset 기반 페이징의 성능 문제를 해결하기 위해 마지막 조회된 id를 기준으로 다음 페이지를 조회하는 방식이다. 위 쿼리처럼 &quot;AND id &amp;lt; 마지막조회ID&quot; 조건으로 이미 읽은 데이터를 건너뛰며, 인덱스를 활용한 빠른 탐색이 가능하다.&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;ItemStreamReader 구현&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/**
 * No-Offset 기법을 사용하여 페이징 처리를 하는 ItemReader 구현체.
 * 대용량 데이터 조회 시 성능 저하를 방지하기 위해 id 값을 기준으로 다음 페이지를 조회한다.
 * @param &amp;lt;T&amp;gt; 조회할 엔티티 타입
 */
public class NoOffsetItemReader&amp;lt;T&amp;gt; implements ItemStreamReader&amp;lt;T&amp;gt; {

    private final EntityManagerFactory entityManagerFactory;
    private final String queryString;
    private final Map&amp;lt;String, Object&amp;gt; parameterValues;
    private final int chunkSize;
    private final Function&amp;lt;T, Long&amp;gt; idExtractor; // 조회된 엔티티에서 ID를 추출하는 함수
    private final Class&amp;lt;T&amp;gt; targetType; // 조회할 엔티티의 클래스 타입
    private final String name;
    private final Queue&amp;lt;T&amp;gt; buffer = new LinkedList&amp;lt;&amp;gt;(); // 조회된 데이터를 임시 저장하는 버퍼
    private EntityManager entityManager;
    private boolean isEnd = false; // 모든 데이터를 다 읽었는지 여부
    private Long firstId; // 현재 페이지의 시작 id

    /**
     * @param entityManagerFactory JPA EntityManagerFactory
     * @param queryString 조회 JPQL 쿼리
     * @param parameterValues 쿼리 파라미터
     * @param chunkSize 페이지 사이즈
     * @param idExtractor 엔티티에서 ID를 추출하는 함수
     * @param targetType 조회할 엔티티의 클래스 타입
     * @param name ItemReader의 이름을 설정한다. Spring Batch 메타데이터에 저장될 이름이다.
     */
    @Builder
    NoOffsetItemReader(
          EntityManagerFactory entityManagerFactory,
          String queryString,
          Map&amp;lt;String, Object&amp;gt; parameterValues,
          int chunkSize,
          Function&amp;lt;T, Long&amp;gt; idExtractor,
          Class&amp;lt;T&amp;gt; targetType,
          String name
    ) {
       this.entityManagerFactory = entityManagerFactory;
       this.queryString = queryString;
       this.parameterValues = parameterValues;
       this.chunkSize = chunkSize;
       this.idExtractor = idExtractor;
       this.targetType = targetType;
       this.name = name;
    }

    @Override
    public void open(ExecutionContext executionContext) throws ItemStreamException {
       this.entityManager = entityManagerFactory.createEntityManager();
       if (executionContext.containsKey(&quot;fistId&quot;)) {
          // Job 재시작 시, 이전에 저장된 firstId를 가져와서 중단된 지점부터 다시 시작
          this.firstId = (Long) executionContext.get(&quot;fistId&quot;);
       } else {
          // Job이 처음 시작될 때, 쿼리에서 가장 큰 ID 값을 가져와 firstId에 저장한다.
          // 이 ID는 다음 페이지 조회의 시작점이 된다.
          TypedQuery&amp;lt;T&amp;gt; query = entityManager
                .createQuery(queryString, this.targetType)
                .setMaxResults(1);
          parameterValues.forEach(query::setParameter);
          List&amp;lt;T&amp;gt; results = query.getResultList();

          if (results.isEmpty()) {
             // 조회 결과가 없으면 fistId를 0으로 설정
             this.firstId = 0L;
          } else {
             // 가장 큰 ID + 1을 시작점으로 설정하여 모든 데이터를 포함하도록 한다.
             this.firstId = idExtractor.apply(results.get(0)) + 1;
          }
       }
    }

    @Override
    public T read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
       if (buffer.isEmpty() &amp;amp;&amp;amp; !isEnd) {
          fillBuffer();
       }

       return buffer.poll();
    }

    private void fillBuffer() {
       // 외부에서 받은 기본 쿼리(queryString)에 No-Offset 조건을 동적으로 추가한다.
       // 사용자 쿼리에 WHERE 절이 있다는 전제 하에 AND로 연결한다.
       String queryWithNoOffset = queryString.replace(&quot;WHERE&quot;, &quot;WHERE id &amp;lt; :firstId AND&quot;);

       TypedQuery&amp;lt;T&amp;gt; query = entityManager
             .createQuery(queryWithNoOffset, this.targetType) // 최종적으로 조립된 쿼리를 사용한다.
             .setMaxResults(chunkSize);

       // 외부에서 주입된 파라미터 설정
       parameterValues.forEach(query::setParameter);
       // 내부 상태인 firstId 파라미터 설정
       query.setParameter(&quot;firstId&quot;, this.firstId);

       List&amp;lt;T&amp;gt; result = query.getResultList();
       if (result.isEmpty()) {
          this.isEnd = true;
       } else {
          buffer.addAll(result);
          this.firstId = idExtractor.apply(result.get(result.size() - 1));
       }
    }

    @Override
    public void update(ExecutionContext executionContext) throws ItemStreamException {
       executionContext.put(&quot;firstId&quot;, this.firstId);
    }

    @Override
    public void close() throws ItemStreamException {
       if (entityManager != null) {
          entityManager.close();
       }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemStreamReader는 ItemReader와 ItemStream을 결합한 인터페이스로, 읽기 기능과 저장/복구(open, update, close)를 함께 제공하여 대용량 배치에서 예외 발생 시 안전한 이어 읽기를 가능하게 한다. ExecutionContext에 진행 상태를 기록/복원함으로써 실패 후 재시작 시 중단 지점부터 이어서 처리할 수 있다. ItemStreamReader의 생명주기 관련 메서드는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;open(ExecutionContext):&lt;/b&gt; 스텝 시작 시 호출된다. 외부 자원(파일, DB 커서, 네트워크 연결 등)을 준비하고, 이전 실행에서 저장해 둔 상태(예: 마지막 처리 위치)를 복원한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;update(ExecutionContext):&lt;/b&gt; 청크가 커밋될 때마다 주기적으로 호출된다. 현재 진행 상태(예: 오프셋, lastId, 파일 라인 번호 등)를 저장하여 중간 실패에 대비한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;close():&lt;/b&gt; 스텝 종료 시 자원을 정리하며 자동으로 호출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 JPA 기반 ItemStreamReader를 커스터마이징하여 No-Offset 조회 방식을 구현한 예로, id &amp;lt; :firstId 조건과 ExecutionContext의 체크포인트를 이용해 대용량 조회 성능 저하 문제를 해결한다. 흐름은 open -&amp;gt; read -&amp;gt; update -&amp;gt; close로 진행되며, 마지막으로 읽은 ID를 이어받아 재시작이 가능하다.&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;No-Offset 기반 Job 구현&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class NoOffsetReportJobConfig {

    private final EntityManagerFactory entityManagerFactory;
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final int chunkSize = 200;

    @Bean
    public Job noOffsetPaymentReportJob(Step noOffsetPaymentReportStep) {
       return new JobBuilder(&quot;noOffsetPaymentReportJob&quot;, jobRepository)
             .incrementer(new RunIdIncrementer())
             .start(noOffsetPaymentReportStep)
             .build();
    }

    @Bean
    public Step noOffsetPaymentReportStep(
          NoOffsetItemReader&amp;lt;PaymentSource&amp;gt; noOffsetItemReader
    ) {
       return new StepBuilder(&quot;noOffsetPaymentReportStep&quot;, jobRepository)
             .&amp;lt;PaymentSource, Payment&amp;gt;chunk(chunkSize, transactionManager)
             // Step 소요 시간 측정
             .listener(new StepDurationTrackerListener())
             .reader(noOffsetItemReader)
             .processor(paymentReportProcessor())
             .writer(noOffsetPaymentReportWriter())
             // Chunk 소요 시간 측정
             .listener(new ChunkDurationTrackerListener())
             .build();
    }

    @Bean
    @StepScope
    public NoOffsetItemReader&amp;lt;PaymentSource&amp;gt; noOffsetItemReader(
          @Value(&quot;#{jobParameters['paymentDate']}&quot;) LocalDate paymentDate
    ) {
       return NoOffsetItemReader.&amp;lt;PaymentSource&amp;gt;builder()
             .entityManagerFactory(entityManagerFactory)
             .queryString(&quot;SELECT ps FROM PaymentSource ps WHERE ps.paymentDate = :paymentDate ORDER BY ps.id DESC&quot;)
             .parameterValues(Collections.singletonMap(&quot;paymentDate&quot;, paymentDate))
             .chunkSize(chunkSize)
             .name(&quot;noOffsetItemReader&quot;)
             .idExtractor(PaymentSource::getId)
             .targetType(PaymentSource.class)
             .build();  // Lombok 빌더의 build() - NoOffsetItemReaderBuilder 인스턴스 반환
    }

    private ItemProcessor&amp;lt;PaymentSource, Payment&amp;gt; paymentReportProcessor() {
       return paymentSource -&amp;gt; new Payment(
             null,
             paymentSource.getFinalAmount(),
             paymentSource.getPaymentDate(),
             paymentSource.getPartnerCorpName(),
             &quot;PAYMENT&quot;
       );
    }

    @Bean
    public ItemWriter&amp;lt;Payment&amp;gt; noOffsetPaymentReportWriter() {
       return chunk -&amp;gt; {
       };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 Job 설정 클래스로, No-Offset 기법을 적용한 리포트 배치 잡을 정의했다. 위에서 살펴본 NoOffsetItemReader로 PaymentSource 데이터를 ID 기준으로 순차 조회하며, 처리 후 Payment 엔티티로 변환한다. 스텝과 청크 단위로 실행 시간을 측정하는 리스너도 추가하여 성능 모니터링도 함께 수행한다. 리스너에 대한 자세한 내용을 &lt;a href=&quot;https://oneny.tistory.com/143&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Batch 정리한 블로그 글&lt;/a&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-origin-width=&quot;3292&quot; data-origin-height=&quot;1212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qLoJv/dJMcaacv8Uz/UTp4jOUNrPrTsylINtsIM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qLoJv/dJMcaacv8Uz/UTp4jOUNrPrTsylINtsIM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qLoJv/dJMcaacv8Uz/UTp4jOUNrPrTsylINtsIM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqLoJv%2FdJMcaacv8Uz%2FUTp4jOUNrPrTsylINtsIM0%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;3292&quot; height=&quot;1212&quot; data-origin-width=&quot;3292&quot; data-origin-height=&quot;1212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 조건이 id &amp;lt; ? AND payment_date &amp;gt;? ORDER BY id DESC LIMIT ?을 통해 인덱스 기반 빠른 페이지 조회를 하는 것을 확인할 수 있다. 그리고 50만 건의 결제 데이터를 1초 만에 처리한 배치 실행 결과를 통해 성능이 개선된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Cursor 방식&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor 방식은 DB와 커넥션을 맺은 후 Cursor를 한 칸씩 옮기면서 데이터를 Streaming해서 보내는 방식을 말한다. 커서가 어디에 존재하는지와 데이터를 언제 가져오느냐에 따라 클라이언트와 서버 방식으로 나눌 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Server-Side Cursor:&lt;/b&gt; DB 서버가 커서를 관리하며, 클라이언트는 한 행씩(fetch) 데이터를 가져온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Client-Side Cursor:&lt;/b&gt; 커서가 아니라 결과셋 전체를 한 번에 클라이언트 메모리에 가져와서, 거기서 순차적으로 읽은 방식&lt;/li&gt;
&lt;/ul&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;Client-side Cursor Streaming&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-03 오후 8.29.13.png&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mXfbm/dJMcaaXTojq/zcyw6TAx9zxEtNQbkutUuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mXfbm/dJMcaaXTojq/zcyw6TAx9zxEtNQbkutUuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mXfbm/dJMcaaXTojq/zcyw6TAx9zxEtNQbkutUuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmXfbm%2FdJMcaaXTojq%2Fzcyw6TAx9zxEtNQbkutUuK%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;731&quot; height=&quot;335&quot; data-filename=&quot;스크린샷 2025-11-03 오후 8.29.13.png&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client-size Cursor Streaming 방식은 데이터베이스 쿼리의 모든 결과를 한 번에 애플리케이션의 메모리로 가져와서 처리하는 방식으로, MySQL JDBC 드라이버의 기본 동작 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 SELECT 쿼리를 실행하면, 데이터베이스는 조회된 모든 데이터를 클라이언트(애플리케이션)로 즉시 전송한다. JDBC 드라이버는 전송받은 모든 데이터를 애플리케이션의 메모리(JVM Heap)에 저장하고, 이후 애플리케이션이 ResultSet.next()를 호출하면, 데이터베이스와 추가 통신 없이 메모리에 저장된 데이터를 빠르게 읽어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 모든 데이터를 메모리에 가져온 후에는 데이터베이스 통신 없이 메모리에서 직접 읽으므로, 데이터 순회 속도가 매우 빠르다는 장점이 있지만, 조회할 데이터가 많은 경우 메모리 사용량이 폭증하여 OOM(OutOfMemory)를 유발하는 주된 원인이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class CursorReportJobConfig {

    private final EntityManagerFactory entityManagerFactory;
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final int chunkSize = 200;

    // ...

    @Bean
    @StepScope
    public JpaCursorItemReader&amp;lt;PaymentSource&amp;gt; cursorItemReader(
          @Value(&quot;#{jobParameters['paymentDate']}&quot;) LocalDate paymentDate
    ) {
       return new JpaCursorItemReaderBuilder&amp;lt;PaymentSource&amp;gt;()
             .name(&quot;cursorItemReader&quot;)
             .entityManagerFactory(entityManagerFactory)
             .queryString(&quot;SELECT ps FROM PaymentSource ps WHERE ps.paymentDate = :paymentDate&quot;)
             .parameterValues(Collections.singletonMap(&quot;paymentDate&quot;, paymentDate))
             .build();
    }

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2034&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HigZT/dJMcafrmyxQ/8LzGdVT1n3KJTpjkESv8zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HigZT/dJMcafrmyxQ/8LzGdVT1n3KJTpjkESv8zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HigZT/dJMcafrmyxQ/8LzGdVT1n3KJTpjkESv8zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHigZT%2FdJMcafrmyxQ%2F8LzGdVT1n3KJTpjkESv8zK%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;2034&quot; height=&quot;240&quot; data-origin-width=&quot;2034&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-03 오후 8.52.08.png&quot; data-origin-width=&quot;3366&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xJ16b/dJMcac9feby/tkYtKRRRAv46Mm5gTt4ZN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xJ16b/dJMcac9feby/tkYtKRRRAv46Mm5gTt4ZN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xJ16b/dJMcac9feby/tkYtKRRRAv46Mm5gTt4ZN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxJ16b%2FdJMcac9feby%2FtkYtKRRRAv46Mm5gTt4ZN0%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;3366&quot; height=&quot;578&quot; data-filename=&quot;스크린샷 2025-11-03 오후 8.52.08.png&quot; data-origin-width=&quot;3366&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 JpaCursorItemReader를 활용한 배치 처리 구현 예시이다. 커서 방식으로 PaymentSource 엔티티를 읽어 Payment 객체를 변환한다. 그리고 처음에 데이터를 메모리에 올리기까지 모든 데이터를 조회해야 하기 때문에 시간이 걸린 것을 확인할 수 있다. 그 뒤 데이터를 chunk 단위로 순회할 때 일정한 시간을 가진다.&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;Server-side Cursor Streaming&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-03 오후 8.36.45.png&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MemwA/dJMb99LrUIl/9zZm3bzWey48rQM3SCQC3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MemwA/dJMb99LrUIl/9zZm3bzWey48rQM3SCQC3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MemwA/dJMb99LrUIl/9zZm3bzWey48rQM3SCQC3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMemwA%2FdJMb99LrUIl%2F9zZm3bzWey48rQM3SCQC3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;734&quot; height=&quot;333&quot; data-filename=&quot;스크린샷 2025-11-03 오후 8.36.45.png&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Server-side Cursor Streaming은 데이터베이스 쿼리의 결과를 서버에 유지한 채, 애플리케이션이 필요할 때마다 데이터를 조금씩 스트리밍으로 가져와 처리하는 방식을 말한다. 대용량 데이터를 메모리 문제 없이 안정적으로 처리하기 위해 Client-side 방식에서 명시적인 설정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 스트리밍 옵션과 함께 SELECT 쿼리를 실행하면, 데이터베이스는 결과 전체를 서버에 준비해두고 실제 데이터는 이때 전송되지 않고, 결과 집합을 가리키는 포인터(커서)만 즉시 반환한다. 이후 애플리케이션이 ResultSet.next()를 호출할 때마다, 데이터베이스에 '다음 데이터 한 건'을 요청하는 네트워크 통신이 발생한다. 데이터베이스는 요청받은 데이터 한 건만 응답으로 보내주며, 이 과정이 모든 데이터를 다 읽을 때까지 반복된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회할 데이터가 아무리 많아도 애플리케이션의 메모리 사용량이 매우 낮고 일정하게 유지되므로 OOM(OutOfMemory)를 원천적으로 방지할 수 있다. 모든 데이터를 읽을 때까지 데이터베이스 커넥션을 계속 유지해야 하므로 DB에 부담을 줄 수 있으며, 데이터 건당 발생하는 네트워크 통신으로 인해 전체 처리 속도가 상대적으로 느릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
@StepScope
public JpaCursorItemReader&amp;lt;PaymentSource&amp;gt; cursorItemReader(
       @Value(&quot;#{jobParameters['paymentDate']}&quot;) LocalDate paymentDate
) {
    return new JpaCursorItemReaderBuilder&amp;lt;PaymentSource&amp;gt;()
          .name(&quot;cursorItemReader&quot;)
          .entityManagerFactory(entityManagerFactory)
          .queryString(&quot;SELECT ps FROM PaymentSource ps WHERE ps.paymentDate = :paymentDate&quot;)
          .parameterValues(Collections.singletonMap(&quot;paymentDate&quot;, paymentDate))
          .hintValues(Collections.singletonMap(HibernateHints.HINT_FETCH_SIZE, Integer.MIN_VALUE))
          .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 Server-Side Cursor 방식으로 동작시키기 위한 설정으로 Hibernate Hint로 HINT_FETCH_SIZE를 조정했다. 이 옵션을 데이터베이스에서 데이터를 가져올 때 서버 측 커서(Server-Side Cursor)를 사용할지 결정하는 중요한 역할을 한다. Integer.MIN_VALUE로 설정하면 JDBC 드라이버에게 결과를 스트리밍 방식으로 받아오도록 요청하여 데이터베이스 서버에 커서를 유지한 채로, 애플리케이션에서 next()를 호출할 때마다 한 건씩 데이터를 가져온다. 이를 통해 대용량의 데이터셋을 처리하더라도 애플리케이션의 메모리 사용량을 최소화할 수 있어 OOM 에러를 방지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Processor&amp;amp;Writer 성능 개선&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Processor는 데이터를 가공하고 변환하는 단계다. 이 과정에서 외부 API 호출이나 파일 접근 같은 I/O 작업이 들어가면 병목이 발생하기 쉽다. 배치의 본질은 &quot;대량 처리&quot;인데, Processor에서 I/O 대기 시간이 길어지면 전체 파이프라인이 느려질 수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 가장 먼저 해야 할 일은 Processor를 최대한 순수한 변환 로직만 수행하도록 단순화하는 것이다. 예를 들어, 외부 API를 호출하는 로직이 Processor에서 들어 있다면 이를 Writer로 이동시키고, Processor는 단순히 데이터를 구조화하거나 필요한 필드를 계산하는 역할만 맡도록 분리하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, Reactive Stream, RxJava, CompletableFuture 등을 사용하여 병렬 처리를 통해 동시에 여러 요청을 처리하도록 개선할 수 있다. 이 접근은 단일 스레드 기반의 동기 호출을 비동기화하여 처리량을 극대화하며, Processor 단계에서는 불필요한 대기 시간을 없애 배치 전체의 흐름을 끊김 없이 유지시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Writer는 배치의 마지막 단계로, 데이터베이스나 외부 시스템에 실제 결과를 반영하는 역할을 한다. 이 단계에서 가장 큰 성능 저하의 원인은 과도한 I/O 호출이다. 이를 해결하기 위한 가장 효과적인 방법은 쿼리의 batch update이다. 예를 들어, 한 건씩 UPDATE를 날리 대신 WEHRE id IN (...) 형태로 여러 데이터를 한 번에 갱신하면, DB에 접근하는 횟수를 크게 줄일 수 있다.&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;회원등급 Batch Flow&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE users
(
    id    BIGINT AUTO_INCREMENT PRIMARY KEY,
    grade VARCHAR(50) NOT NULL -- INIT, BASIC, PREMIUM, VIP
);
CREATE INDEX idx_grade ON users (grade);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 테이블처럼 user 테이블에 등급(grade)이 있고, 5만 명의 INIT 등급인 회원들을 BASIC, PREMINUM, VIP 등급으로 업데이트 배치를 진행한다고 가정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Service
public class OrderClient {

    public Grade getGrade(Long userId) {
        try {
            // 150ms 대기, 외부 API 호출하는 것처럼 응답 지연
            Thread.sleep(150);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        if (userId % 3 == 0) {
            return Grade.VIP;
        } else if (userId % 2 == 0) {
            return Grade.PREMIUM;
        } else {
            return Grade.BASIC;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 배치에서 주문 서비스에 각 유저에 해당하는 등급을 조회한다고 가정한 코드로 OrderClient에서는 유저의 ID를 받아 적절한 등급을 반환하는 로직을 사용한다. 이때, 150ms을 대기하도록 하여 외부 APi를 호출하는 것처럼 응답을 지연시켰다.&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;TO-BE&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class UserGradleApplyJobConfiguration {

    private final EntityManagerFactory entityManagerFactory;
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final OrderClient orderClient;
    private static final int CHUNK_SIZE = 1_000;

    @Bean
    public Job userGradleApplyJob(Step userGradleApplyStep) {
       return new JobBuilder(&quot;userGradleApplyJob&quot;, jobRepository)
             .incrementer(new RunIdIncrementer())
             .start(userGradleApplyStep)
             .build();
    }

    @Bean
    public Step userGradleApplyStep(
          JpaCursorItemReader&amp;lt;User&amp;gt; cursorUserItemReader,
          ItemProcessor&amp;lt;User, User&amp;gt; userItemProcessor,
          JpaItemWriter&amp;lt;User&amp;gt; userItemWriter
    ) {
       return new StepBuilder(&quot;userGradleApplyStep&quot;, jobRepository)
             .&amp;lt;User, User&amp;gt;chunk(CHUNK_SIZE, transactionManager)
             .listener(new StepDurationTrackerListener())
             .reader(cursorUserItemReader)
             .processor(userItemProcessor)
             .writer(userItemWriter)
             .listener(new ChunkDurationTrackerListener())
             .build();
    }

    @Bean
    @StepScope
    public JpaCursorItemReader&amp;lt;User&amp;gt; cursorUserItemReader() {
       return new JpaCursorItemReaderBuilder&amp;lt;User&amp;gt;()
             .name(&quot;cursorItemReader&quot;)
             .entityManagerFactory(entityManagerFactory)
             .queryString(&quot;SELECT u FROM User u WHERE u.grade = 'INIT'&quot;)
             .build();
    }

    @Bean
    public ItemProcessor&amp;lt;User, User&amp;gt; userItemProcessor() {
       return user -&amp;gt; {
          Grade grade = orderClient.getGrade(user.getId());
          user.setGrade(grade);
          return user;
       };
    }

    @Bean
    public JpaItemWriter&amp;lt;User&amp;gt; userItemWriter() {
       return new JpaItemWriterBuilder&amp;lt;User&amp;gt;()
             .entityManagerFactory(entityManagerFactory)
             .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-04 오후 10.05.01.png&quot; data-origin-width=&quot;2374&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYoIe2/dJMcajN4usS/GKyvC5IQ7VqlBEzLvLHaWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYoIe2/dJMcajN4usS/GKyvC5IQ7VqlBEzLvLHaWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYoIe2/dJMcajN4usS/GKyvC5IQ7VqlBEzLvLHaWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYoIe2%2FdJMcajN4usS%2FGKyvC5IQ7VqlBEzLvLHaWK%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;2374&quot; height=&quot;808&quot; data-filename=&quot;스크린샷 2025-11-04 오후 10.05.01.png&quot; data-origin-width=&quot;2374&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 User 엔티티의 등급(grade)를 외부 시스템으로부터조회하여 갱신하는 Spring Batch Job 클래스이다. 5만건의 데이터를 조회하기 때문에 JpaCursorItemReader로 INIT 등급인 사용자들을 커서 방식으로 스트리밍 조회하고, ItemProcessor에서 OrderClient를 통해 실제 등급을 받아와 갱신한 후, JpaItemWriter로 DB에 반영한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 위에서 살펴본 OrderClient는 각 하나의 아이템당 150ms을 지연시키므로 150ms * 1000로 대략 2분 30초가 걸리는 것을 확인할 수 있다. 추가로 각각의 아이템을 JpaItemWriter를 사용하여 update 쿼리를 요청한 것도 확인할 수 있다.&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;AS-IS&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public Step userGradleApplyStepV2(
       JpaCursorItemReader&amp;lt;User&amp;gt; cursorUserItemReaderV2,
       ItemWriter&amp;lt;User&amp;gt; userItemWriterV2
) {
    return new StepBuilder(&quot;userGradleApplyStepV2&quot;, jobRepository)
          .&amp;lt;User, User&amp;gt;chunk(CHUNK_SIZE, transactionManager)
          .listener(new StepDurationTrackerListener())
          .reader(cursorUserItemReaderV2)
          .writer(userItemWriterV2)
          .listener(new ChunkDurationTrackerListener())
          .build();
}

@Bean
public ItemWriter&amp;lt;User&amp;gt; userItemWriterV2() {
    return users -&amp;gt; {
       var appliedGradeUsers = Flowable.fromIterable(users.getItems())
             .parallel()
             .runOn(Schedulers.io())
             .map(user -&amp;gt; {
                final var grade = orderClient.getGrade(user.getId());
                user.setGrade(grade);
                return user;
             })
             .sequential()
             .toList()
             .blockingGet();

       appliedGradeUsers.stream()
             .collect(Collectors.groupingBy(User::getGrade))
             .forEach((grade, targetUsers) -&amp;gt; {
                System.out.println(&quot;grade = &quot; + grade);
                      final var userIds = targetUsers.stream().map(User::getId).toList();
                      userRepository.updateGrade(grade, userIds);
                   }
             );
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-04 오후 10.24.13.png&quot; data-origin-width=&quot;2666&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FFE3Q/dJMcagX7bU2/x7ogsIXGQFlqoM46ancwx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FFE3Q/dJMcagX7bU2/x7ogsIXGQFlqoM46ancwx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FFE3Q/dJMcagX7bU2/x7ogsIXGQFlqoM46ancwx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFFE3Q%2FdJMcagX7bU2%2Fx7ogsIXGQFlqoM46ancwx0%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;2666&quot; height=&quot;376&quot; data-filename=&quot;스크린샷 2025-11-04 오후 10.24.13.png&quot; data-origin-width=&quot;2666&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-04 오후 10.23.55.png&quot; data-origin-width=&quot;2634&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Okl1V/dJMcacuD0u7/g85vB0kDkN9UJSxGlDkWQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Okl1V/dJMcacuD0u7/g85vB0kDkN9UJSxGlDkWQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Okl1V/dJMcacuD0u7/g85vB0kDkN9UJSxGlDkWQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOkl1V%2FdJMcacuD0u7%2Fg85vB0kDkN9UJSxGlDkWQk%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;2634&quot; height=&quot;426&quot; data-filename=&quot;스크린샷 2025-11-04 오후 10.23.55.png&quot; data-origin-width=&quot;2634&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 User 데이터를 읽는 것까지는 같지만, Processor 단계를 과감히 없애고 RxJava Flowable로 병렬 처리하여 외부 API를 호출하는 배치 패턴이다. Writer에서 각 User의 Grade를 외부 시스템(OrderClient)에서 조회하여 순차 처리 대신 병렬 스트림을 활용한 외부 API 호출 시간을 대폭 단축시킬 수 있다. 또한, Grade별로 그룹화하여 같은 등급으로 업데이트할 사용자들을 묶어 배치 업데이트를 수행하여 위에서 살펴본 UPDATE 쿼리 보다 훨씬 효율적으로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Partition Step을 사용한 성능 개선&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티셔닝은 하나의 일감(단일 Step)을 여러 개의 작은 일감(파티션)으로 잘게 쪼갠 뒤, 여러 명의 작업자(스레드)에게 나누어주고 동시에 병렬로 처리하게 하는 스프링 배치의 가장 강력한 성능 최적화 기능이다. 결과적으로 시스템이 가진 멀티코어 CPU 자원을 100% 활용하여 대용량 데이터 처리 시간을 획기적으로 단축시킬 수 있다. 수백만건의 데이터를 처리할 때, 메모리 문제를 피하면서도 가장 빠르게 작업을 끝낼 수 있는 방법 중 하나이다. 스프링배치에서 파티셔닝을 활용한 사례는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기간별 데이터 처리:&lt;/b&gt; 가장 흔한 예로 '1년치 매출 데이터를 집계하라'는 거대한 작업 대신, &quot;1월 데이터는 1번 작업자가, 2월 데이터는 2월 작업자가...&quot;와 같이 12개의 작업으로 나누어 12개의 스레드가 동시에 처리하게 할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 속성별 분할:&lt;/b&gt; 데이터의 특정 속성을 기준으로 나눌 수도 있다. 예를 들어, '모든 사용자의 등급을 갱신하라'는 작업을 &quot;VIP 등급은 1번 작업자, PREMIUM 등급은 2번 작업자가..&quot;와 같이 사용자 등급별로 작업을 분할하여 병렬 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대용량 파일 처리:&lt;/b&gt; 수십 기가바이트(GB)에 달하는 거대한 텍스트 파일이 있다면, 이 파일을 행(line) 번호를 기준으로 여러 조각으로 나눌 수 있다. &quot;1번 작업자는 1~100만 번째 줄, 2번 작업자는 100만 1~200만 번째 줄...&quot;과 같이 나누어 동시에 파일을 읽고 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Batch의 파티셔닝&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 배치의 파티셔닝은 크게 두 가지 역할로 구성된다. 하나는 전체 작업을 관리하는 매니저 스텝(Manager Step), 다른 하나는 실제 데이터를 처리하는 워커 스텝(Worker Step)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매니저 스텝(Manager Step)은 파티셔닝의&amp;nbsp; 직접 데이터를 처리하지 않고, 전체 작업을 분할하고 워커 스텝들을 관리 및 실행하는 책임을 가진다. 매니저 스텝의 역할을 자세히 살펴보면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파티션 생성(Partitioner):&lt;/b&gt; Partitioner는 전체 데이터 범위를 어떻게 나눌지 결정하는 로직을 담은 컴포넌트이다. JobParameters를 참고하여, 각 워커가 처리할 데이터의 범위(예: 날짜, ID 범위)를 ExecutionContext에 담아 여러 개의 파티션을 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;워커 스텝 실행:&lt;/b&gt; 생성된 파티션의 수만큼 워커 스텝을 실행하도록 스케줄링한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;병렬 처리(TaskExecutor):&lt;/b&gt; TaskExecutor(스레드 풀)를 사용하여 워커 스텝들을 병렬로 동시에 실행시킨다. gridSize 옵션으로 동시에 실행할 워커의 최대 개수를 제어할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워커 스텝(Worker Step)은 매니저로부터 특정 데이터 처리 임무를 할당받은 실제 작업 단위이다. 각 워커는 자신에게 주어진 범위의 데이터만 독립적으로 처리한다. 워커 스텝의 주요 책임을 살펴보면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 수신:&lt;/b&gt; 각 워커 스텝은 시작될 때, 매니저가 파티셔너를 통해 생성한 StepExecutionContext를 전달받는다. 코드에서는 @StepScope와 @Value 어노테이션을 사용하여 이 컨텍스트에 담긴 파라미터(예: 처리할 날짜)를 주입받는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;독립적 실행:&lt;/b&gt; 각 워커는 독립적인 Reader, Processor, Writer 인스턴스를 가진다. 이를 통해 다른 워커의 작업에 전혀 영향을 주지 않고 자신만의 데이터를 수행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 관리:&lt;/b&gt; 각 워커 스텝은 자신만의 트랜잭션 범위 내에서 동작한다. 따라서 특정 워커에서 오류가 발생하여 롤백되더라도, 다른 워커들의 트랜잭션에는 영향을 미치지 않아 작업의 격리성을 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;파티셔닝(Partitioning) 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-06 오전 7.13.56.png&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBXMC1/dJMcagcKsba/9nRHWzcyXFulGSzkRFprHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBXMC1/dJMcagcKsba/9nRHWzcyXFulGSzkRFprHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBXMC1/dJMcagcKsba/9nRHWzcyXFulGSzkRFprHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBXMC1%2FdJMcagcKsba%2F9nRHWzcyXFulGSzkRFprHk%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;2570&quot; height=&quot;644&quot; data-filename=&quot;스크린샷 2025-11-06 오전 7.13.56.png&quot; data-origin-width=&quot;2570&quot; data-origin-height=&quot;644&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 다이어그램은 Spring Batch의 파티셔닝 아키텍처를 보여준다. ManagerStep이 날짜 범위를 여러 파티션으로 분할하고, TaskExecutor의 스레드 풀(gridSize=10)을 통해 각 Worker Step이 병렬로 실행되어 각자 할당받은 날짜 범위의 데이터를 처리할 수 있다. 각 Worker는 @StepScope의 ItemReader를 통해 독립적인 파티션 데이터를 조회하여 DB에 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gridSize는 생성할 파티션의 개수를 의미하고, threadPoolSize는 실제 동시에 실행 가능한 스레드 개수를 의미한다. gridSize가 threadPool의 사이즈보다 크면 일부 파티션은 대기 후 순차적으로 실행되며, 같으면 모든 파티션이 동시에 실행된다. 만약 gridSize가 30이더라도 taskExecutor의 maxPoolSize가 10이라면 실제 실행할 수 있는 스레드는 10개 뿐이다. 따라서 10개 워커만 동시에 실행되고 나머지는 대기하기 때문에 보통 gridSize와 taskExecutor의 maxPoolSize를 맞춰 대기없이 스레드가 원활히 동작하는 것이 이상적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;LocalDatePartitioner&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
public class LocalDatePartitioner implements Partitioner {
    // 파티션 번호를 식별하기 위한 키
    private static final String PARTITION_KEY = &quot;partition&quot;;
    // StepExecutionContext에 저장될 날짜 데이터의 키
    private static final String EXECUTION_CONTEXT_KEY_PAYMENT_DATE = &quot;paymentDate&quot;;

    private final LocalDate startDate; // 파티션 생성 시작일
    private final LocalDate endDate; // 파티녓 생성 종료일

    public LocalDatePartitioner(LocalDate startDate, LocalDate endDate) {
       this.startDate = startDate;
       this.endDate = endDate;
    }

    @Override
    public Map&amp;lt;String, ExecutionContext&amp;gt; partition(int gridSize) {
       HashMap&amp;lt;String, ExecutionContext&amp;gt; partitions = new HashMap&amp;lt;&amp;gt;();
       LocalDate currentDate = this.startDate;
       int partitionIndex = 0;

       // 시작일부터 종료일까지 반복하여 파티션 생성
       while (!currentDate.isAfter(this.endDate)) {
          ExecutionContext context = new ExecutionContext();
          // paymentDate 키에 현재 날짜를 ISO_LOCAL_DATE 형식의 문자열로 저장
          context.putString(EXECUTION_CONTEXT_KEY_PAYMENT_DATE, currentDate.format(DateTimeFormatter.ISO_LOCAL_DATE));
          // &quot;partition&quot; + 인덱스 형태의 키로 파티션 추가
          partitions.put(PARTITION_KEY + partitionIndex, context);

          log.info(&quot;Created Partition: {}, Date: {}&quot;, PARTITION_KEY + partitionIndex, context.get(EXECUTION_CONTEXT_KEY_PAYMENT_DATE));

          // 다음 날짜로 이동
          currentDate = currentDate.plusDays(1);
          partitionIndex++;
       }

       return partitions;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalDatePartitioner는 Partitioner 인터페이스를 구현하여 날짜 범위를 일 단위로 분할하는 커스텀 파티셔너이다. startDate부터 endDate까지 하루씩 증가하며 각 날짜에 대한 ExecutionContext를 생성하고, partition0, partition1 형태의 키로 Map에 저장한다. 각 ExecutionContext에는 paymentDate 키로 해당 날짜 문자열이 저장되어 Worker Step에서 @StepScope를 통해 주입받아 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@RequiredArgsConstructor
public class PartitionJobConfig {

    private final EntityManagerFactory entityManagerFactory;
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final int chunkSize = 1_000;

    @Bean
    public Job partitionJob(Step managerStep) {
       return new JobBuilder(&quot;partitionJob&quot;, jobRepository)
             .incrementer(new RunIdIncrementer())
             .start(managerStep)
             .build();
    }

    @Bean
    public Step managerStep(Step workerStep) {
       return new StepBuilder(&quot;managerStep&quot;, jobRepository)
             // &quot;workerStep&quot;이라는 이름으로 파티셔너를 설정한다.
             .partitioner(&quot;workerStep&quot;, partitioner(null, null))
             // 각 파티션에서 실행될 스텝을 지정한다.
             .step(workerStep)
             // 동시에 실행할 파티션(워커)의 수를 설정한다.
             .gridSize(10)
             // 파티션을 병렬로 처리하기 위한 TaskExecutor를 설정한다.
             .taskExecutor(taskExecutor())
             .build();
    }

    @Bean
    @StepScope
    public LocalDatePartitioner partitioner(
          @Value(&quot;#{jobParameters['startDate']}&quot;) LocalDate startDate,
          @Value(&quot;#{jobParameters['endDate']}&quot;) LocalDate endDate
    ) {
       return new LocalDatePartitioner(startDate, endDate);
    }

    @Bean
    public TaskExecutor taskExecutor() {
       ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
       taskExecutor.setCorePoolSize(10); // 기본 스레드 수
       taskExecutor.setMaxPoolSize(10); // 최대 스레드 수
       taskExecutor.setThreadNamePrefix(&quot;partition-thread-&quot;); // 스레드 이름 접두사
       taskExecutor.setWaitForTasksToCompleteOnShutdown(true); // 종료 시 모든 태스크가 온료될 때까지 대기
       taskExecutor.initialize();
       return taskExecutor;
    }

    @Bean
    public Step workerStep(JpaCursorItemReader&amp;lt;PaymentSource&amp;gt; workerCursorItemReader) {
       return new StepBuilder(&quot;workerStep&quot;, jobRepository)
             .&amp;lt;PaymentSource, Payment&amp;gt;chunk(chunkSize, transactionManager)
             .listener(new StepDurationTrackerListener())
             .reader(workerCursorItemReader)
             .processor(workerPaymentReportProcessor())
             .writer(workerPaymentReportWriter())
             .listener(new ChunkDurationTrackerListener())
             .build();
    }

    @Bean
    @StepScope
    public JpaCursorItemReader&amp;lt;PaymentSource&amp;gt; workerCursorItemReader(
          @Value(&quot;#{stepExecutionContext['paymentDate']}&quot;) String paymentDateStr
    ) {
       LocalDate paymentDate = LocalDate.parse(paymentDateStr, DateTimeFormatter.ISO_LOCAL_DATE);
       return new JpaCursorItemReaderBuilder&amp;lt;PaymentSource&amp;gt;()
             .name(&quot;cursorItemReader&quot;)
             .entityManagerFactory(entityManagerFactory)
             .queryString(&quot;SELECT ps FROM PaymentSource ps WHERE ps.paymentDate = :paymentDate&quot;)
             .parameterValues(Collections.singletonMap(&quot;paymentDate&quot;, paymentDate))
             .build();
    }

    private ItemProcessor&amp;lt;PaymentSource, Payment&amp;gt; workerPaymentReportProcessor() {
       return paymentSource -&amp;gt; new Payment(
             null,
             paymentSource.getFinalAmount(),
             paymentSource.getPaymentDate(),
             paymentSource.getPartnerCorpName(),
             &quot;PAYMENT&quot;
       );
    }

    private ItemWriter&amp;lt;Payment&amp;gt; workerPaymentReportWriter() {
       return chunk -&amp;gt; {

          var item = chunk.getItems().stream().findFirst().get();
          System.out.println(item.getPaymentDate());

       };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 파티셔닝을 활용한 결제 데이터 배치 처리 설정이다. ManagerStep이 LocalDatePartitioner를 통해 날짜별로 파티션을 생성한다. 각 WorkerStep을 병렬 실행하여 PaymentSource를 조회해 Payment로 변환한다. 그리고 JpaCursorItemReader를 사용하여 대용량 데이터를 효율적으로 처리하며, 각 파티션은 @StepScope를 통해 독립적인 paymentDate 파라미터를 주입받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3160&quot; data-origin-height=&quot;846&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lSaUO/dJMcagqhCJB/QVNKF374l84gBUkStr7gBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lSaUO/dJMcagqhCJB/QVNKF374l84gBUkStr7gBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lSaUO/dJMcagqhCJB/QVNKF374l84gBUkStr7gBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlSaUO%2FdJMcagqhCJB%2FQVNKF374l84gBUkStr7gBK%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;3160&quot; height=&quot;846&quot; data-origin-width=&quot;3160&quot; data-origin-height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 결과에서 확인할 수 있듯이 parition-thread-1부터 partition-thread-10까지 10개의 스레드에서 독립적인 워커스텝을 병렬로 실행하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jojoldu.tistory.com/473&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Batch와 QuerydslItemReader&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jgrammer.tistory.com/entry/Spring-Batch-%EC%8B%A4%ED%8C%A8%EB%A5%BC-%EB%8B%A4%EB%A3%A8%EB%8A%94-%EA%B8%B0%EC%88%A0-ItemStream&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Batch 실패를 다루는 기술 - ItemStream&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/connector-j/en/connector-j-reference-implementation-notes.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;6.4 JDBC API Implementation Notes&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://fastcampus.co.kr/dev_online_springbatch&quot;&gt;실무를 위한 Spring Batch의 모든 것 : 성능 개선부터 프로젝트까지&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <category>ItemProcessor 성능 개선</category>
      <category>ItemStreamReader</category>
      <category>ItemWriter 성능 개선</category>
      <category>Spring 배치 성능 개선</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/144</guid>
      <comments>https://oneny.tistory.com/144#entry144comment</comments>
      <pubDate>Thu, 6 Nov 2025 20:48:55 +0900</pubDate>
    </item>
    <item>
      <title>Spring Batch</title>
      <link>https://oneny.tistory.com/143</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring Batch&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch는 대용량 데이터 처리를 위한 경량 프레임워크로, Job과 Step으로 구성된 배치 작업을 정의하고 실행한다. 로깅/추적, 트랜잭션 관리, 작업 처리 통계, 작업 재시작, 스킵, 리소스 관리 등 대용량 레코드 처리에 필수적인 기능을 제공한다. Spring Batch의 주요 사용 사례는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대용량 데이터 ETL(Extract, Transform, Load)&lt;/li&gt;
&lt;li&gt;정기적인 대량 데이터 처리(매출 집계, 통계 생성)&lt;/li&gt;
&lt;li&gt;외부 시스템과의 데이터 동기화&lt;/li&gt;
&lt;li&gt;파일 import/export wkrdjq&lt;/li&gt;
&lt;li&gt;대량 메일 발송 및 알림 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Batch 특징 및 장점&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;풍부한 필수 기능 제공&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 관리, 로깅/추적, 체크포인트/재시작, 통계 집계, 예외 처리(Skip), 재시도(Retry) 등의 기능을 간편히 활용 가능&lt;/li&gt;
&lt;li&gt;실패한 배치 Job을 중단 지점부터 재시작하거나 동일 파라미터로 중복 실행 방지 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring 생태계와 쉬운 통합&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Framework 기반으로 DI, AOP 활용 가능&lt;/li&gt;
&lt;li&gt;Spring Boot 환경에서 Starter 의존성 추가만으로 자동 설정(DataSource, JobReposiroty 등) 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증된 성능과 안정성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;금융권, 이커머스 등 다양한 산업에서 검증된 신뢰성과 성능 제공&lt;/li&gt;
&lt;li&gt;청크 기반 처리, 멀티스레딩, 파티셔닝을 통한 효율적인 대용량 데이터 처리 지원&lt;/li&gt;
&lt;li&gt;청크 크기, 스레드 개수 등 세부적인 성능 튜닝 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다양한 Reader/Writer 지원&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일(CSV, XML, JSON), DB(JDBC, JPA), JMS, MongoDB 등의 다양한 데이터 소스를 기본적으로 지원&lt;/li&gt;
&lt;li&gt;커스텀 ItemReader/Writer로 새로운 데이터 소스에 쉽게 대응 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;메타데이터 스키마&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Uw5f/btsRaSI9LTf/aWjQtyKkEOCjP8NMmPITY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Uw5f/btsRaSI9LTf/aWjQtyKkEOCjP8NMmPITY1/img.png&quot; data-alt=&quot;출처: https://docs.spring.io/spring-batch/reference/schema-appendix.html#page-title&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Uw5f/btsRaSI9LTf/aWjQtyKkEOCjP8NMmPITY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Uw5f%2FbtsRaSI9LTf%2FaWjQtyKkEOCjP8NMmPITY1%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;585&quot; height=&quot;484&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://docs.spring.io/spring-batch/reference/schema-appendix.html#page-title&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch는 배치 실행 정보를 관리하기 위해 위 스키마에서 확인할 수 있듯 6개의 메타데이터 테이블을 사용한다. 각 테이블은 Java 도메인 객체(JobInstance, JobExecution 등)와 1:1로 매핑되어 실행 이력과 상태를 저장한다. 이들은 JobRepository가 내부적으로 관리하며, 배치 실행 중 발생한 모든 정보를 기록하고 추적한다. 테이블 구조 및 역할은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BATCH_JOB_INSTANCE&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job의 논리적 인스턴스를 의미하며, 동일한 Job 이름과 파라미터 조합이 하나의 인스턴스로 식별된다.&lt;/li&gt;
&lt;li&gt;Java 객체: JobInstance&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BATCH_JOB_EXECUTION&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 JobInstance의 실제 실행 이력(성공, 실패, 시작 및 종료 시간 등)을 저장한다.&lt;/li&gt;
&lt;li&gt;Java 객체: JobExecution&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BATCH_JOB_EXECUTION_PARAMS&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Job 실행 시 전달된 파라미터를 관리한다.&lt;/li&gt;
&lt;li&gt;Java 객체: JobParameters&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BATCH_STEP_EXECUTION&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JobExecution 내 각 Step의 실행 상태, 처리 건수, 실패 사유 등을 저장한다.&lt;/li&gt;
&lt;li&gt;Java 객체: StepExecution&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BATCH_JOB_EXECUTION_CONTEXT&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JobExecution 수준의 컨텍스트 정보를 직렬화하여 저장하여 실행 도중 필요한 상태 값이나 중간 데이터 유지에 사용한다.&lt;/li&gt;
&lt;li&gt;Java 객체: ExecutionContext&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BATCH_STEP_EXECUTION_CONTEXT&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StepExecution별로 독립적인 컨텍스트를 저장하여 Step 단위의 상태 복원 및 재시작 시 활용한다.&lt;/li&gt;
&lt;li&gt;Java 객체: ExecutionContext&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Job&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch의 Job은 배치 처리의 최상위 단위로, 특정 비즈니스 목적을 달성하기 위한 배치 프로세스의 시작점이다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Job은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;구성(Configuration)&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;실행(Execution)&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;두 역할을 동시에 수행한다. 즉, &lt;/span&gt;하나의 Job은 여러 Step을 순차적 또는 조건부로 조합하여 복잡한 배치 워크플로우를 구성하여 &lt;span style=&quot;color: #333333; text-align: start;&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;b&gt;Job&amp;nbsp; 구현체 종류&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch는 아래와 같이 Job 인터페이스의 대표적인 두 가지 구현체를 제공하고, 각각의 Job 구현체는 Step을 어떤 방식으로 실행할지, 그리고 흐름을 어떻게 제어할지에 따라 구분된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SimpleJob:&lt;/b&gt; 가장 기본적인 Job 구현체
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부에 정의된 Step들을 &lt;b&gt;순차적&lt;/b&gt;으로 한 줄 흐름으로 실행한다.&lt;/li&gt;
&lt;li&gt;추가적인 조건 분기 없이 &lt;b&gt;선형적인 작업 흐름&lt;/b&gt;을 처리할 때 적합한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FlowJob:&lt;/b&gt; 복잡한 흐름 제어(조건 분기, 병렬 흐름 등)가 필요한 경우 사용되는 구현체
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flow 객체를 기반으로 실행 흐름을 구성하며, StepA 성공 시 StepB, 실패 시 StepC 실행 등으로 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조건에 따라 분기하거나,&lt;/b&gt; 특정 Step을 재시도, 병렬 실행, skip하는 등 보다 &lt;b&gt;정교한 시나리오 설계&lt;/b&gt;에 적합한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JobInstance&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1760533670154&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;create table BATCH_JOB_INSTANCE
(
    JOB_INSTANCE_ID bigint       not null primary key, -- pk
    VERSION         bigint       null, -- 버전
    JOB_NAME        varchar(100) not null, -- Job을 구성할 떄 부여하는 Job의 이름
    JOB_KEY         varchar(32)  not null, -- jobName과 jobParameters를 합쳐 해시값
    constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JobInstance는 특정 Job이 특정 파라미터로 실행된 &lt;b&gt;논리적 실행 단위 객체&lt;/b&gt;를 말한다. 즉, 같은 Job이라도 실행 파라미터(예: date)가 다르다면 Job명 + JobParameters 조합(jobKey 해시값)이 다르기 때문에 새로운 JobInstance로 간주하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;중복 실행을 방지&lt;/b&gt;한다. JobInstance 기준으로 실행 이력을 관리하기 때문에 실패/재시작을 위한&amp;nbsp;여러 번의 실행(JobExecution)과 연결될 수 있어 &quot;2025-10-11자 집계 작업을 실패 후 재시작할 때&quot;와 같은 논리적 묶음 관리가 가능하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JobExecution&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1760535369565&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE BATCH_JOB_EXECUTION
(
    JOB_EXECUTION_ID BIGINT      NOT NULL PRIMARY KEY, -- 식별키
    VERSION          BIGINT, -- 버전
    JOB_INSTANCE_ID  BIGINT      NOT NULL, -- JOB_INSTANCE 식별키
    CREATE_TIME      DATETIME(6) NOT NULL, -- JobExecution이 생성된 시점
    START_TIME       DATETIME(6) DEFAULT NULL, -- JobExecution이 시작된 시점 
    END_TIME         DATETIME(6) DEFAULT NULL, -- JobExecution이 끝난 시점, 실패나 중단 시 저장되지 않을 수 있음
    STATUS           VARCHAR(10), -- 실행 상태(COMPLETED, FAILED, STOPPED, ...)
    EXIT_CODE        VARCHAR(2500), -- 실행 종료코드(COMPLETED, FAILED, NOOP, ...)
    EXIT_MESSAGE     VARCHAR(2500), -- Job 실행 중 발생한 오류 메시지나 사용자 정의 종료 메시지 저장
    LAST_UPDATED     DATETIME(6), -- JobExecution 업데이트 시점, STATUS 바뀔 때마다 갱신
    constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
        references BATCH_JOB_INSTANCE (JOB_INSTANCE_ID)
) ENGINE = InnoDB;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JobExecution은 JobInstance 한 번의 실제 이력을 말한다. 하나의 JobInstance(특정 JobName + JobParameters)에 대해 여러 번 실행(재시작, 실패 후 재시도 등)이 있을 수 있는데 실행마다 별도의 JobExecution이 생성되고, 각각 status(성공/실패), 시작/종료 시각, 에러메시지, 종료 코드 등을 저장한다. 즉, JobExecution이 쌓이면 &lt;b&gt;&quot;언제, 누가, 어떤 조건으로 실행했고, 성공/실패했는지&quot;&lt;/b&gt;를 전부 기록하고, 배치 실패 원인 추적, SLA(처리시간 보장), 장애 대응, 이중실행 방지 등에서 매우 중요한 역할을 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JobParameters&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1760534289693&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;CREATE TABLE BATCH_JOB_EXECUTION_PARAMS
(
    JOB_EXECUTION_ID BIGINT       NOT NULL, -- JobExecution 식별키, JOB_EXECUTION과는 일대다 관계
    PARAMETER_NAME   VARCHAR(100) NOT NULL, -- 파라미터명
    PARAMETER_TYPE   VARCHAR(100) NOT NULL, -- 파라미터 타입
    PARAMETER_VALUE  VARCHAR(2500), -- 파라미터 값
    IDENTIFYING      CHAR(1)      NOT NULL, -- 식별여부(Y, N)
    constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID) references BATCH_JOB_EXECUTION (JOB_EXECUTION_ID)
) ENGINE = InnoDB&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JobParameters는 Job 실행 시 입력되는 파라미터 집합으로 JobInstance의 유일성을 결정짓는 핵심 요소이다. 실행 시점, 대상 일자, 파일명 등 작업을 구분하는데 보통 쓰인다. 위에서 설명했듯이 JobName + JobParameters의 조합이 기존에 존재하면, 동일 JobInstance로 인식하고, 파라미터 하나라도 다르다면 새로운 JobInstance를 생성한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760534542680&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;JobParameters params1 = new JobParametersBuilder()
    .addString(&quot;requestDate&quot;, &quot;2025-10-15&quot;, true)   // 식별 파라미터
    .addString(&quot;filePath&quot;, &quot;/tmp/data.csv&quot;, false)  // 단순 실행 파라미터
    .toJobParameters();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 IDENTIFYING 컬럼은 JobParamter가 JobInstance를 식별하는 기준에 포함되는지 여부를 나타내어 위 코드처럼 JobParameters 객체를 생성 시 지정할 수 있다.&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;Job 실행 흐름&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-15 오전 9.07.41.png&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;1264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTD9w4/btsQ9UHNuqN/FrDrS6l3gtKAwIDNmW8gcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTD9w4/btsQ9UHNuqN/FrDrS6l3gtKAwIDNmW8gcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTD9w4/btsQ9UHNuqN/FrDrS6l3gtKAwIDNmW8gcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTD9w4%2FbtsQ9UHNuqN%2FFrDrS6l3gtKAwIDNmW8gcK%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;594&quot; height=&quot;516&quot; data-filename=&quot;스크린샷 2025-10-15 오전 9.07.41.png&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;1264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 다이어그램은 Spring Batch에서 JobLauncher가 Job을 실행할 때 JobRepository가 내부적으로 JobInstance를 어떻게 판별하고 관리하는지를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JobLauncher는 배치 실행을 요청하는 진입점으로 run을 호출하여 Job과 JobParameters를 받아 JobRepository로 전달하는 역할을 한다. JobRepository는 모든 실행 이력을 관리하는 컴포넌트로 중복 실행을 방지하기 위해 BATCH_JOB_INSTNACE 테이블의 JobName과 JobKey(JobParameters의 해시)로 기존 JobInstance가 있는지 검사하고, 존재하지 않으면 새 JobInstance를 생성 후 DB에 저장한다. 이후 JobExecution(BATCH_JOB_EXECUTION)을 생성하고 실행을 기록한다. 실행 시 전달된 파라미터는 BATCH_JOB_EXECUTION_PARAMS에 함께 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1760572394346&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
public class HelloBatchConfiguration {

    @Bean
    public Job helloJob(JobRepository jobRepository, Step helloStep1, Step helloStep2) {
        return new JobBuilder(&quot;helloJob&quot;, jobRepository)
                .start(helloStep1)
                .next(helloStep2)
                .build();
    }

    @Bean
    public Step helloStep1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder(&quot;helloStep&quot;, jobRepository)
                .tasklet((StepContribution contribution, ChunkContext chunkContext) -&amp;gt; {
                    System.out.println(&quot;hello spring batch111&quot;);

                    Thread.sleep(5_000L);

                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }

    @Bean
    public Step helloStep2(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
        return new StepBuilder(&quot;helloStep&quot;, jobRepository)
                .tasklet((StepContribution contribution, ChunkContext chunkContext) -&amp;gt; {
                    System.out.println(&quot;hello spring batch222&quot;);
                    throw new IllegalArgumentException(&quot;에러!!&quot;);

//                    return RepeatStatus.FINISHED;
                }, transactionManager)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 Spring Batch에서 두 개의 Step으로 구성된 helloJob 배치를 정의한다. 첫 번째 Step은 단순히 메시지를 출력하고 5초 대기한 후 종료되고, 두 번째 Step은 예외를 발생시켜서 실패 상태를 유도한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-16 오전 8.59.15.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boAIFf/btsRcFiNKIB/n7JAN0wbwbHEhejY3tunvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boAIFf/btsRcFiNKIB/n7JAN0wbwbHEhejY3tunvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boAIFf/btsRcFiNKIB/n7JAN0wbwbHEhejY3tunvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboAIFf%2FbtsRcFiNKIB%2Fn7JAN0wbwbHEhejY3tunvk%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;741&quot; height=&quot;187&quot; data-filename=&quot;스크린샷 2025-10-16 오전 8.59.15.png&quot; data-origin-width=&quot;1980&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 작업이 실행되면 helloJob 배치는 BATCH_JOB_INSTANCE에 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3420&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSb0cX/btsRb6AZPOD/2YXK6YY4BPN8J1RmJ670OK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSb0cX/btsRb6AZPOD/2YXK6YY4BPN8J1RmJ670OK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSb0cX/btsRb6AZPOD/2YXK6YY4BPN8J1RmJ670OK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSb0cX%2FbtsRb6AZPOD%2F2YXK6YY4BPN8J1RmJ670OK%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;3420&quot; height=&quot;468&quot; data-origin-width=&quot;3420&quot; data-origin-height=&quot;468&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;3488&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9aBAn/dJMb9NIx3xD/UUPeFOoDktRGSvo4b7E321/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9aBAn/dJMb9NIx3xD/UUPeFOoDktRGSvo4b7E321/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9aBAn/dJMb9NIx3xD/UUPeFOoDktRGSvo4b7E321/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9aBAn%2FdJMb9NIx3xD%2FUUPeFOoDktRGSvo4b7E321%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;3488&quot; height=&quot;502&quot; data-origin-width=&quot;3488&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 실행 시마다 BATCH_JOB_EXECUTION이 추가되며, 그 하위에 여러 Step에 여러 Step이 실행되면 BATCH_STEP_EXECUTION이 연계되어 저장된다. 그리고 각 실행마다 관련된 파라미터(BATCH_JOB_EXECUTION_PARAMS)와 컨텍스트(BATCH_JOB_EXECUTION_CONTEXT, BATCH_STEP_EXECUTION_CONTEXT)가 기록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블에 저장된 데이터와 함께 Job 실행 흐름을 살펴보면 처음에 helloStep2에서 실패로 Job과 Step에 대한 상태를 FAILED로 저장된 것을 확인할 수 있다. 그리고 helloStep2를 FINISHED 상태로 끝낼 수 있도록 주석을 해제하면 JobInstance를 새로 생성하지 않고, 기존 JobInstance를 사용하여 잡 실행을 처리한 것을 확인할 수 있다. 마지막으로 같은 잡을 실행할 수는 있지만 NOOP 상태 즉, 아무런 데이터를 처리하지 않은 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Step&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step은 Spring Batch에서 '실제 데이터 처리'가 일어나는 최소 단위를 말한다. 즉, Job을 구성하는 독립적인 하나의 단계로서 실제 배체 처리를 정의하고 컨트롤하는데 필요한 정보를 가지고 있는 도메인 객체이다. Step 내부에서는 Reader -&amp;gt; Processor -&amp;gt; Writer의 구조로 데이터를 읽고(Reader), 가공하고(Processor), 저장(Writer)한다. Step이 실행될 때마다 처리 건수, 성공/실패, 상태 등이 메타테이블에 기록되어 모니터링, 장애 대응, 재시작에 활용된다.&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;Step 구현체 종류&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step은 아래와 같이 Tasklet 기반(단일 작업) 또는 Chunk 기반(데이터 묶음 단위 반복 처리)으로 구현할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TaskletStep&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 Step에서 '특정 로직'을 단일 작업으로 실행할 때 사용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) 파일 삭제, 외부 시스템 알림 전송, 단건 처리 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Tasklet 인터페이스를 구현해서 원하는 로직만 작성하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ChunkOrientedTasklet&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대용량 데이터를 'Chunk'(묶음) 단위로 반복 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;내부적으로 ItemReader -&amp;gt; ItemProcessor -&amp;gt; ItemWriter 구조를 따른다.&lt;/li&gt;
&lt;li&gt;데이터 집계, 이관, 대량 파일 처리 등 대부분의 배치 처리에 사용하며, 실무에서 가장 많이 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;StepExecution&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepExecution은 Step 한 번의 실행 이력 단위를 말하며, 언제, 어떤 상태로 실행됐고, 성공/실패/중단 등 결과가 어떻게 됐는지 모두 기록한다. StepExecution의 주요 속성은 실행 시작/종료 시간, 상태(성공, 실패 등), 처리/읽은/쓰여진 건수, 에러 메시지 등이 있으며 각 StepExecution 별 처리 건수 모니터링하며, 장애 발생 시, 어느 Step에서 문제가 발생했는지 추적할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepExecution과 관련한 이력은 BATCH_STEP_EXECUTION 테이블에 저장한다.&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;StepContribution&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepContribution은 StepExecution 내부에서 한 Chunk(묶음)의 처리 결과를 누적 및 집계하는 역할을 담당한다. 청크 기반 처리에서 각 Chunk별로 Reader/Processor/Writer의 처리 결과를 기록하고, StepExecution에 반영한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepContribution의 주요 속성은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽은/처리한/쓰여진/스킨된 아이템 수&lt;/li&gt;
&lt;li&gt;예외 발생, 필터링, 실패 건수 등 상세 통계&lt;/li&gt;
&lt;li&gt;성능 튜닝, 장애 분석, 청크 사이즈 조정 시 StepContribution 로그가 매우 유용함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 청크 사이즈마다 ItemReader -&amp;gt; ItemProcessor -&amp;gt; ItemWriter가 실행되고, 그 결과가 StepContribution에 저장된다. 모든 Chunk가 끝나면 StepContribution의 통계 정보가 StepExecution에 합산된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;br /&gt;Chunk 기반 처리&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-30 오후 10.54.20.png&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3R3Gc/dJMcacH9IiQ/9olgy2rFs5KO07wpJJiiQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3R3Gc/dJMcacH9IiQ/9olgy2rFs5KO07wpJJiiQK/img.png&quot; data-alt=&quot;출처: 실무를 위한 Spring Batch의 모든 것 : 성능 개선부터 프로젝트까지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3R3Gc/dJMcacH9IiQ/9olgy2rFs5KO07wpJJiiQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3R3Gc%2FdJMcacH9IiQ%2F9olgy2rFs5KO07wpJJiiQK%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;767&quot; height=&quot;371&quot; data-filename=&quot;스크린샷 2025-10-30 오후 10.54.20.png&quot; data-origin-width=&quot;1586&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 실무를 위한 Spring Batch의 모든 것 : 성능 개선부터 프로젝트까지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Chunk는 &lt;b&gt;대용량 데이터를 '덩어리(Chunk, 묶음)' 단위로 잘라서 일정 개수씩 반복 처리하는 방식&lt;/b&gt;을 말한다. Step 내부에서 ItemReader -&amp;gt; ItemProcessor -&amp;gt; ItemWriter 구조로 Reader에서 데이터를 Chunk 사이즈만큼 읽고, Processor에서 가공/필터링한 뒤, Writer에서 일괄 저장한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Chunk 기반으로 처리하면 다음과 같은 장점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메모리 효율성:&lt;/b&gt; 한 번에 모든 데이터를 올리지 않고, Chunk 단위로 나눠서 처리하므로 메모리 부담이 적다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 향상:&lt;/b&gt; Chunk 단위로 데이터를 일괄로 처리하면, 매 건마다 DB에 접근하거나 트랜잭션을 커밋하는 경우보다 빠르다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 안정성:&lt;/b&gt; 각 Chunk를 하나의 트랜잭션으로 간주하여 Chunk 단위로 커밋 및 롤백이 가능하고, 실패한 Chunk만 재시도하거나 스킵할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;장애 복구 용이:&lt;/b&gt; JobRepository와 연계되어 각 Chunk 단위의 처리 상태(읽은 건수, 커밋된 건수 등)이 저장되어 장애가 발생했을 때 중단된 지점부터 재시작하는데 사용된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성:&lt;/b&gt; Chunk 단위 구조는 Partitioning와 같은 병렬 실행과 결합할 때 각각의 스레드나 워커 스텝에서 독립적으로 실행되어 대용량 데이터를 빠르게 병렬 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Chunk 사이즈 조정, 주의해야 할 점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터베이스 트랜잭션 커밋 문제&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chunk가 커질수록 한 번에 커밋되는 데이터가 많아짐&lt;/li&gt;
&lt;li&gt;대량 커밋 시 DB Lock, Deadlock, Undo/Redo 로그 폭증 등으로 오히려 전체 성능 저하&lt;/li&gt;
&lt;li&gt;장애 발생 시 롤백 비용 커짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 서비스 연동 시 네트워크 한계&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 번에 대용량 데이터를 외부 시스템에 전송하면 상대 서버가 수신 거부, 타임아웃 발생 가능&lt;/li&gt;
&lt;li&gt;네트워크 대역폭을 초과해 실패하거나, 처리 지연&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 사용량 폭증&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chunk 사이즈가 클수록 한 번에 처리할 데이터를 메모리에 모두 올려야 함&lt;/li&gt;
&lt;li&gt;JVM OutOfMemory 등, 서버 메모리 한계 초과로 배치 실패 위험&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;각 시스템 환경에 맞는 튜닝 필요&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB, 네트워크, 메모리 등 시스템 환경마다 최적값 다름&lt;/li&gt;
&lt;li&gt;실무에서는 보통 2,000 이상의 Chunk는 피하는 편&lt;/li&gt;
&lt;li&gt;직접 실험 및 모니터링을 통해 적정 Chunk Size 산정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ItermReader&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ItemReader는 Step에서 데이터를 읽어오는 역할을 하며, 아래와 같은 다양한 데이터 소스(JPA, JDBC, 파일, API 등)를 사용한 구현체를 사용하여 데이터를 추출할 수 있다. 대량 데이터 처리 시 한 번에 너무 많은 데이터를 읽으면 OutOfMemory 등 메모리 이슈가 발생할 수 있기 때문에 주의해야 한다. ItemReader 주요 구현체는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JpaPagingItemReader&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA 기반으로 페이징 처리로 대용량 데이터를 안전하게 읽을 수 있다.&lt;/li&gt;
&lt;li&gt;JPQL 쿼리 사용으로 한 번에 메모리 과부화를 방지할 수 있다.&lt;/li&gt;
&lt;li&gt;주의: flush/clear 타이밍, 영속성 컨텍스트 관리 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JdbcCursorItemReader&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB Cursor 기반으로 한 줄씩 직접 읽어올 수 있다.&lt;/li&gt;
&lt;li&gt;주의: 트랜잭션 길어질 경우 DB 부하 발생 및 커넥션 장시간 점유&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FlatFileItemReader&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CSV/텍스트 파일 등 외부 파일 데이터를 읽을 수 있다.&lt;/li&gt;
&lt;li&gt;주의: 파일 용량, 포맷, 인코딩 이슈 및 구분자, 줄바꿈 문자 등으로 오류 발생 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ItemProcessor&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ItemProcessor는 Reader에서 읽은 데이터를 가공, 변환, 필터링(제외)하는 역할을 한다. 즉, 입력값을 받아 가공 결과를 반환하거나, 필요 없는 데이터는 null로 반환해 Writer 단계로 전달하지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ItemProcessor에서 날짜 포맷이나 문자열 변환 등 데이터 포맷을 변경하거나 조건에 맞지 않는 데이터를 제외 또는 복잡한 비즈니스 로직을 거쳐 도메인 변환(엔티티 &amp;lt;-&amp;gt; DTO)하는 과정을 사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ItemProcessor를 사용할 떄는 외부 서비스 호출, 복잡한 연산이 많으면 전체 배치 성능 저하가 있을 수 있기 때문에 무거운 연산에 주의해야 한다. 그리고 데이터 정합성을 위해 필터링/가공 후 Writer에 넘기는 데이터와 실제 저장될 데이터의 정합성을 항상 확인하는 것이 좋다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;ItemWriter&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ItemWriter는 Processor에서 넘어온 데이터를 실제로 DB, 파일, 외부 시스템 등 다양한 대상에 저장(쓰기)하는 역할을 한다. ItemWriter를 사용할 때 한 번에 너무 많은 Chunk만큼의 데이터를 저장하면 트랜잭션 부담, 너무 적으면 I/O 오버헤드가 발생할 수 있다. 그리고 저장 실패 시 배치 전체가 중단될 수 있어 예외에 대한 롤백, 재시도 등 전략이 필요하다. ItemWriter 주요 구현체는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JpaItemWriter, JdbcBatchItemWriter&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB에 일괄 저장, 대량 데이터 처리에 적합하다.&lt;/li&gt;
&lt;li&gt;주의: 트랜잭션, 커밋 타이밍, 영속성 컨텍스트 고나리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FlatFileItemWriter&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일(CSV, 텍스트 등)로 저장, 대량 로그/리포트 파일 생성에 유용&lt;/li&gt;
&lt;li&gt;주의: 파일 포맷, 인코딩, 줄바꿈 문자 등 체크&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Custom Writer&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 API, 메일, 메시지 큐 등 다양한 방식 구현 가능&lt;/li&gt;
&lt;li&gt;주의: 외부 시스템 장애, 네트워크 타임아웃 등 예외처리 필수&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Fault-Tolerant 설정&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1761310915337&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;return new StepBuilder(&quot;faultTolerantStep&quot;, jobRepository)
	.&amp;lt;PaymentSource, Payment&amp;gt;chunk(100, transactionManager)
	.reader(paymentReader())
	.processor(paymentProcessor())
	.writer(paymentWriter())
	// 내결함성 활성화
	// Skip/Retry/BackOff 등의 내결함성 옵션을 사용할 수 있도록 StepBuilder를 FaultTolerantStepBuilder로 전환한다.
	.faultTolerant()

	// Skip 설정
	.skip(IllegalArgumentException.class) // 지정한 예외 타입이 발생했을 때 해당 아이템을 건너뛰도록 등록한다.
	.skip(IllegalStateException.class)
	.skipLimit(10) // Skip 허용 최대 건수를 설정한다. 이 횟수를 초과하면 Step이 실패한다.
        // noSkip을 사용해서 예외 상속 계층 구조에서 특정 하위 예외를 명시적으로 제외할 수 있다.
	.noSkip(NullPointerException.class) // Skip 대상에서 제외할 예외 타입을 지정하여 건너뛰지 않고 Step을 실패시킨다.
	.skipPolicy(customSkipPolicy()) // 사용자 정의 또는 기본 SkipPolicy를 등록해, 복합 기준(예: 예외 타입 + 횟수)을 적용할 수 있다.

	//  Retry 설정
	.retry(PessimisticLockException.class) // 지정한 예외 타입이 발생했을 때 재시도를 수행하도록 동작한다.
	.retry(OptimisticLockException.class)
	.retry(QueryTimeoutException.class)
	.retryLimit(3) // Retry 허용 최대 횟수를 설정한다. 이 횟수를 초과하면 재시도를 중단한다.
	.retryPolicy(retryPolicy()) // 사용자 정의 또는 기본 RetryPolicy를 등록해, 복합 기준(예: 예외 타입 + 횟수)을 적용할 수 있다.
	.backOffPolicy(backOffPolicy()) // 재시도 간격(고정/지수 등)을 조정하는 BackOff 정책을 등록한다.
	.noRetry(IllegalArgumentException.class) // Retry 대상에서 제외할 예외 타입을 지정한다.

	// 롤백 제어
	.noRollback(IllegalStateException.class) // 지정한 예외 타입이 발생해도 트랜잭션을 롤백하지 않고 커밋된 데이터를 유지한다.

	.build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 데이터 처리 시 일부 데이터 오류나 외부 시스템 장애는 항상 발생할 수 있다. 전체 Job이 단 한 건의 오류로 멈춘다면 생산성 및 가용성이 크게 저하하기 때문에 faultTolerant()를 사용하면 배치 처리 중 작은 데이터 오류나 일시적 장애가 발생해도 Step 전체를 곧바로 실패시키지 않고, 건너뛰기(Skip) 또는 재시도(Retry) 로직을 적용해 계속 진행할 수 있다.&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;Skip&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class FaultTolerantStepBuilder {

	// ...
    
	protected SkipPolicy createSkipPolicy() {
		SkipPolicy skipPolicy = this.skipPolicy;
		Map&amp;lt;Class&amp;lt;? extends Throwable&amp;gt;, Boolean&amp;gt; map = new HashMap&amp;lt;&amp;gt;(skippableExceptionClasses);
		map.put(ForceRollbackForWriteSkipException.class, true);
		LimitCheckingItemSkipPolicy limitCheckingItemSkipPolicy = new LimitCheckingItemSkipPolicy(skipLimit, map);
		if (skipPolicy == null) {
			if (skippableExceptionClasses.isEmpty() &amp;amp;&amp;amp; skipLimit &amp;gt; 0) {
				logger.debug(String.format(
						&quot;A skip limit of %s is set but no skippable exceptions are defined. Consider defining skippable exceptions.&quot;,
						skipLimit));
			}
			skipPolicy = limitCheckingItemSkipPolicy;
		}
		else if (limitCheckingItemSkipPolicy != null) {
			skipPolicy = new CompositeSkipPolicy(new SkipPolicy[] { skipPolicy, limitCheckingItemSkipPolicy });
		}
		return skipPolicy;
	}
}

public class CompositeSkipPolicy implements SkipPolicy {

	private SkipPolicy[] skipPolicies;

	// ...

	@Override
	public boolean shouldSkip(Throwable t, long skipCount) throws SkipLimitExceededException {
		for (SkipPolicy policy : skipPolicies) {
			if (policy.shouldSkip(t, skipCount)) {
				return true;
			}
		}
		return false;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSV 파일의 주문 데이터 한 줄씩 읽어서 처리하는 Job이 있다고 가정하자. 파일에 한 줄이 잘못된 포맷(예: 2025-13-01) 혹은 음수 금액으로 잘못 들어가 있는 경우 해당 데이터는 무조건 수동으로 수정해야 할 '영구적 오류'이므로 재시도해도 절대 올바르게 변환되지 않는다. 이러한 상황에 .skip(InvalidFormatException).skipLimit(100)을 사용하여 최대 100건까지 이런 오류 라인을 건너뛰며 나머지 정상 데이터를 계속 처리하도록 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;faultTolerant()를 적용하는 경우 FaultTolerantStepBuilder를 통해 StepBuilder 내부에서 스킵 정책(SkipPolicy)을 결정하고 생성하는 createSkipPolicy가 실행된다. 해당 메서드는 LimitCheckingItemSkipPolicy를 기본으로 사용하면서, 필요할 경우 사용자 정의 정책(this.skipPolicy)과 결합하여 CompositeSkipPolicy로 묶어 shouldSkip 메서드에서 각 스킵정책을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 배치에서 제공하는 SkipPolicy 클래스에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;LimitCheckingItemSkipPolicy&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public Step step() {
    return new StepBuilder(&quot;faultTolerantStep&quot;, jobRepository)
          .&amp;lt;I, O&amp;gt;chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .skip(MyBusinessException.class) // 스킵 허용 예외 등록
          .skipLimit(5)
          .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LimitCheckingItemSkipPolicy는 가장 기본적으로 제공되는 스킵 정책이다. 이 정책은 지정된 예외 타입일 경우에만 스킵을 허용하며, 동시에 스킵 횟수가 설저오딘 한도(skipLimit)를 초과하지 않는 경우에만 스킵을 가능하도록 동작한다. 예외가 발생할 때마다 현재까지의 스킵 횟수(skipCount)를 확인하여, 만약 skipCount가 skipLimit보다 작다면 해당 아이템을 스킵하고 다음 아이템으로 넘어가지만, 한도를 초과하면 더 이상 스킵을 허용하지 않고 해당 Skip이 실패로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제는 MyBusinessException이 발생할 경우 최대 5번까지는 스킵이 허용되며, 6번째 예외부터는 Step이 실패로 종료된다. 해당 스킵 정책은 비즈니스 예외에 유용하고, 횟수 기반으로 안전하게 스킵을 제어할 수 있다. 대부분의 간단한 skip 시나리오에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;AlwaysSkipItemSkipPolicy&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Step step() {
    return new StepBuilder(&quot;faultTolerantStep&quot;, jobRepository)
          .&amp;lt;I, O&amp;gt;chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .skipPolicy(new AlwaysSkipItemSkipPolicy())
          .build();
}

public class AlwaysSkipItemSkipPolicy implements SkipPolicy {

	@Override
	public boolean shouldSkip(Throwable t, long skipCount) {
		return true;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AlwaysSkipItemSkipPolicy는 모든 예외를 무조건 스킵하도록 허용하는 정책이다. 이 정책은 예외의 종류나 발생 횟수와 상관없이 항상 true를 반환하므로 배치 수행 중 어떤 오류가 발생하더라도 작업이 중단되지 않고 끝가지 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 내부적으로 shouldSkip(Throwable t, int skipCount) 메서드가 호출될 때마다 예외 타입이나 현재 스킵 횟수와 관계없이 항상 true를 반환하기 때문에, 배치 수행 도중 예외가 발생하더라도 해당 아이템은 단순히 건너뛰고 다음 아이템으로 진행된다. 이로 인해 전체 Step이 실패하지 않고 모든 아이템 처리를 끝까지 시도할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;NeverSkipItemSkipPolicy&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Step step() {
    return new StepBuilder(&quot;faultTolerantStep&quot;, jobRepository)
          .&amp;lt;I, O&amp;gt;chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .skipPolicy(new NeverSkipItemSkipPolicy())
          .build();
}

public class NeverSkipItemSkipPolicy implements SkipPolicy {

	@Override
	public boolean shouldSkip(Throwable t, long skipCount) {
		return false;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NeverSkipItemSkipPolicy는 어떠한 예외도 스킵하지 않도록 하는 정책이다. 즉, 배치 수행 중 하나의 아이템이라도 예외가 발생하면 해당 Step은 즉시 실패로 간주되어 롤백이 수행된다. 이 정책은 내부적으로 shouldSkip(Throwable t, int skipCount) 메서드가 호출될 때 항상 false를 반환하기 때문에, 어떤 예외가 발생하더라도 스킵 없이 바로 실패 처리를 진행한다.&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;Retry&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retry 기능은 일시적인 장애(Transient Error)에 대해 자동으로 재시도하도록 지원하는 내장 복구 메커니즘이다. 이는 일회성 네트워크 오류나 DB 데드락 등, 일정 시간 후 재요청하면 성공할 가능성이 높은 상황에서 알시 장애 때문에 전체 Job을 중단하지 않고, 정상적으로 다음 Step으로 이어질 수 있도록 할 수 있어 매우 유용하다. 주의해야할 점으로 Retry 전략은 ItemProcessor와 ItemWriter 둘 다에 적용되지만, ItemReader에는 적용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch에서 재시도 설정 주요 메서드는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;retry(Class&amp;lt;? extends Throwable&amp;gt; type)&lt;/b&gt;: 지정한 예외가 발생하면 재시도를 수행하도록 등록한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;retryLimit(int retryLimit)&lt;/b&gt;: 최대 재시도 횟수를 설정한다. 이 횟수를 초과하면 더 이상 재시도하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;noRetry(Class&amp;lt;? extends Throwable&amp;gt; type)&lt;/b&gt;: 재시도 대상에서 제외할 예외 타입을 지정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;retryPolicy(RetryPolicy)&lt;/b&gt;: 사용자 정의 또는 기본 제공 RetryPolicy를 등록해, 예외 타입/횟수/기타 조건으로 재시도를 세밀하게 제어할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;backOffPolicy(BackOffPolicy)&lt;/b&gt;: 재시도 사이의 간격(고정/지수 등)을 조정하는 BackOffPolicy를 등록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761696217370&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class FaultTolerantStepBuilder&amp;lt;I, O&amp;gt; extends SimpleStepBuilder&amp;lt;I, O&amp;gt; {

    protected BatchRetryTemplate createRetryOperations() {

        RetryPolicy retryPolicy = this.retryPolicy;
        SimpleRetryPolicy simpleRetryPolicy = null;

        Map&amp;lt;Class&amp;lt;? extends Throwable&amp;gt;, Boolean&amp;gt; map = new HashMap&amp;lt;&amp;gt;(retryableExceptionClasses);
        map.put(ForceRollbackForWriteSkipException.class, true);
        simpleRetryPolicy = new SimpleRetryPolicy(retryLimit, map);

        if (retryPolicy == null) {
            Assert.state(!(retryableExceptionClasses.isEmpty() &amp;amp;&amp;amp; retryLimit &amp;gt; 0),
                &quot;If a retry limit is provided then retryable exceptions must also be specified&quot;);
            retryPolicy = simpleRetryPolicy;
        }
        else if ((!retryableExceptionClasses.isEmpty() &amp;amp;&amp;amp; retryLimit &amp;gt; 0)) {
            CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
            compositeRetryPolicy.setPolicies(new RetryPolicy[] { retryPolicy, simpleRetryPolicy });
            retryPolicy = compositeRetryPolicy;
        }
        
        // ...
    }
}

public class SimpleRetryPolicy implements RetryPolicy {

	@Override
	public boolean canRetry(RetryContext context) {
		Throwable t = context.getLastThrowable();
		boolean can = (t == null || retryForException(t)) &amp;amp;&amp;amp; context.getRetryCount() &amp;lt; getMaxAttempts();
		if (!can &amp;amp;&amp;amp; t != null &amp;amp;&amp;amp; !this.recoverableClassifier.classify(t)) {
			context.setAttribute(RetryContext.NO_RECOVERY, true);
		}
		else {
			context.removeAttribute(RetryContext.NO_RECOVERY);
		}
		return can;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;faultTolerant() 활성화 시 사용되는 FaultTolerantStepBuilder에서 createRetryOperations 메서드는 재시도 정책(RetryPolicy)을 구성하여 BatchRetryTemplate에 주입하는 진입점이다. 기본 정책으로 SimpleRetryPolicy(retryLimit, retryableExceptionMap)을 만들고, 필요 시 기존 사용자 정책과 Composite으로 묶어 예외 발생 시 정책을 순차적으로 적용이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시도 가능 여부는 canRetry(context) 호출 시 순서대로 각 정책의 canRetry(...)를 검사 하나라도 true를 반환하면 즉시 true를 반환한다. 아래 SimpleRetryPolicy.canRetry()는 &quot;재시도 대상 예외 + 시도 횟수 &amp;lt; 최대 시도&quot;를 만족할 때만 재시도하며, 복구 불가 예외는 컨텍스트에 NO_RECOVERY 플래그를 남긴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 다양한 재시도 규칙을 한 번에 적용 가능하고, 복잡한 운영 환경에서 정교한 장애 대응 시나리오로 확장할 수 있다. 그리고 사용자가 .retryPolicy(...)로 커스텀 정책을 주입하더라도, Spring Batch가 기본 생성하는 SimpleRetryPolicy를 그대로 유지해 누락 없이 예외를 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SimpleRetryPolicy&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private Step paymentRetryStep() {
    Map&amp;lt;Class&amp;lt;? extends Throwable&amp;gt;, Boolean&amp;gt; retryableExceptions = Map.of(
          PessimisticLockException.class, true,
          IllegalStateException.class, false
    );
    SimpleRetryPolicy policy = new SimpleRetryPolicy(3, retryableExceptions, true);

    return new StepBuilder(&quot;retryStep&quot;, jobRepository)
          .&amp;lt;I, O&amp;gt;chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .retryPolicy(policy)
          .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정된 예외 타입에 대해 최대 횟수 기반으로 재시도를 허용하는 기본 정책이다. 내부에 Map&amp;lt;Class&amp;lt;? extends Throwable&amp;gt;, Boolean&amp;gt;을 설정해, 어떤 예외를 재시도할지 등록한다. 재시도 횟수가 retryLimit을 초과하지 않았으면 canRetry(...)가 true를 반환하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 세 번까지 PessimistickLockException에 대한 재시도를 허용하고, IllegalArgumentException은 즉시 실패하도록 설정한 예시이다. 해당 정책은 횟수 기반 가장 간단한 재시도 정책으로 &quot;잠시 기다렸다가 다시 시도하면 성공할 가능성이 높은 일시적 장애에 대한 예외만, 최대 N번&quot; 같은 기본 시나리오에 적합한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TimeoutRetryPolicy&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private Step retryStep() {
    return new StepBuilder(&quot;retryStep&quot;, jobRepository)
          .&amp;lt;I, O&amp;gt;chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .retryPolicy(new TimeoutRetryPolicy(5_000))
          .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정된 시간(timeout) 동안만 재시도를 허용하는 정책이다. 첫 시도가 시작된 시점을 기준으로 누적 경과시간을 체크하고, 경과시간이 timeout 미만이면 open(...) 이후 canRetry(..)가 true이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 5초 이내라면 동일 예외가 발생해도 재시도를 하도록 설정한 예시이다. &quot;몇 초 안에 해결될 일시적 오류&quot;에 적합하며, 네트워크 지연, 외부 API 응답 대기 같은 시나리오에 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AlwaysRetryPolicy&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private Step retryStep() {
    return new StepBuilder(&quot;retryStep&quot;, jobRepository)
          .&amp;lt;I, O&amp;gt;chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .retryPolicy(new AlwaysRetryPolicy())
          .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 예외를 무조건 재시도하도록 허용하는 정책이다. canRetry(...) 호출 시 발생 횟수나 예외 타입에 상관없이 항상 true를 반환하여 무조건 재시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 어떠한 예외가 발생하더라도 무제한으로 재시도하도록 설정한 예시로 PoC 단계에서 &quot;일단 다 재시도해보자&quot;할 때 유용하고, 실무에서는 잘 쓰지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NeverRetryPolicy&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private Step retryStep() {
    return new StepBuilder(&quot;retryStep&quot;, jobRepository)
          .&amp;lt;I, O&amp;gt;chunk(10)
          .reader(reader()).processor(processor()).writer(writer())
          .faultTolerant()
          .retryPolicy(new NeverRetryPolicy())
          .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전혀 재시도하지 않음을 선언하는 정책이다. canRetry(...)가 항상 false를 반환하여 예외 발생 즉시 실패하여 롤백 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 어떤한 예외도 재시도 없이 바로 Step을 실패 처리한다. 재시도를 완전히 비활성화하고 싶을 때나 재시도 자체가 비즈니스에 부정적일 때 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Listener&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-30 오후 10.28.05.png&quot; data-origin-width=&quot;3742&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMoERC/dJMcaeTvvGw/prOuD3XdtMwzK6Et0WKR20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMoERC/dJMcaeTvvGw/prOuD3XdtMwzK6Et0WKR20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMoERC/dJMcaeTvvGw/prOuD3XdtMwzK6Et0WKR20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMoERC%2FdJMcaeTvvGw%2FprOuD3XdtMwzK6Et0WKR20%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;3742&quot; height=&quot;960&quot; data-filename=&quot;스크린샷 2025-10-30 오후 10.28.05.png&quot; data-origin-width=&quot;3742&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch의 Listener는 배치 작업의 생명주기에서 &lt;b&gt;특정 시점에 실행되는 콜백 인터페이스&lt;/b&gt;이다. Job, Step, Chunk 각 계층마다 Before/After Listener가 존재하며, 로깅, 모니터링, 예외 처리 등 횡단 관심사를 구현할 수 있다.&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;JobExecutionListener&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface JobExecutionListener {

    // 잡 시작 직전 호출
    default void beforeJob(JobExecution jobExecution) {
    }

    // 모든 스텝 종료 후 호츌
    default void afterJob(JobExecution jobExecution) {
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JobExecutionListener는 배치 Job의 실행 전후 시점에 개입할 수 있는 콜백 인터페이스이다. beforeJob()으로 Job 시작 전 초기화 작업을, afterJob()으로 완료 후 리소스 정리나 알림 처리를 수행한다. JobExecution 객체를 통해 실행 시간, 상태, 메타데이터 등을 조회하여 모니터링 및 알림 시스템을 구축할 수 있다.&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;StepExecutionListener&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface StepExecutionListener extends StepListener {

    // 각 스텝 시작 직전 호출
    default void beforeStep(StepExecution stepExecution) {
    }

    // 각 스텝 종료 후 호출
    @Nullable
    default ExitStatus afterStep(StepExecution stepExecution) {
       return null;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StepExecutionListener는 개발 Step의 실행 전후 시점에 개입하는 리스너로, beforeStep()과 afterStep() 두 개의 콜백을 제공한다. 특히 afterStep()은 ExitStatus를 반환하여 다음 Step으로의 흐름을 동적으로 제어할 수 있으며, StepExecution 객체를 통해 읽기/쓰기 통계(getReadCount(), getWriteCount(), getSkipCount() 등)에 접근하여 상세한 모니터링이 가능하다. 또한, ExecutionContext를 활용하면 Step 간 데이터 공유도 구현할 수 있다.&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;ChunkListener&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface ChunkListener extends StepListener {

    /**
     * The key for retrieving the rollback exception.
     */
    String ROLLBACK_EXCEPTION_KEY = &quot;sb_rollback_exception&quot;;

    // 트랜잭션이 시작된 직후, 청크 단위 처리 직전에
    default void beforeChunk(ChunkContext context) {
    }

    // 청크 처리가 성공적으로 커밋된 직후
    default void afterChunk(ChunkContext context) {
    }

    // 청크 처리 중 예외가 발생해 롤백된 직후
    default void afterChunkError(ChunkContext context) {
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChunkListener는 Chunk 단위 처리의 시작과 완료, 에러 시점에 개입하는 리스너로, 트랜잭션 경계와 정확히 일치하다. Chunk 레벨의 세밀한 타이밍 측정, 메모리 관리, afterChunkError에서 롤백 원인 로깅, 알림 또는 별도 리커버리 로직 수행하거나 성공한 청크 수, 실패된 청크 수 집계하여 모니터링 시스템에 전송하는 등을 구현할 수 있어 대용량 배치 작업의 안정성과 성능 모니터링에 필수적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ItemReadListener&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface ItemReadListener&amp;lt;T&amp;gt; extends StepListener {

    // ItemReader.read() 호출 직전
    default void beforeRead() {
    }

    // read()가 정상적으로 객체를 반환한 직후
    default void afterRead(T item) {
    }

    // read() 도중 예외 발생 시 바로
    default void onReadError(Exception ex) {
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemReaderListener는 Spring Batch의 리스너 계층 구조에서 가장 세밀한 단위로 동작한다. beforeRead()는 read() 호출 직전, afterRead()는 실제 아이템이 반환된 직후, onReadError()는 읽기 실패 시 호출된다. 중요한 점은 afterRead()가 null 반환 시에는 호출되지 않으며, 실제 객체가 반환될 때만 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemReadListener의 주요 활용 포인트는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;읽기 성능 모니터링:&lt;/b&gt; 각 레코드별 읽기 시간 측정 -&amp;gt; 병목 구간 탐지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 품질 검사:&lt;/b&gt; afterRead에서 값 검증, 이상치 필터링 로직 삽입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 대비 로깅/알림:&lt;/b&gt; onReadError에서 예외 정보 로깅 또는 관리자 알림&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ItemProcessListener&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface ItemProcessListener&amp;lt;T, S&amp;gt; extends StepListener {

    // ItemProcessor.process(item) 호출 직전
    default void beforeProcess(T item) {
    }

    // process()가 정상적으로 결과를 반환한 직후
    default void afterProcess(T item, @Nullable S result) {
    }

    // process() 도중 예외 발생 시
    default void onProcessError(T item, Exception e) {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemProcessListener는 개별 아이템의 변환 과정 전후와 에러 시점에 개입하는 리스너로, Processor의 비즈니스 로직 성능을 측정하고, 필터링된 아이템을 추적하는데 최적화되어 있다. 중요한 특징은 afterProcess()가 result 파라미터로 처리 결과를 받으며, Processor가 null을 반환해도 호출하기 때문에 필터링된 아이템도 모니터링할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemProcessListener의 주요 활용 포인트는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로세싱 타이밍 측정:&lt;/b&gt; 각 아이템별 처리 시간 기록 -&amp;gt; CPU 집약 로직 최적화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필터링 알림:&lt;/b&gt; afterProcess에서 result == null인 필터링된 아이템 로깅&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예외 세부 처리:&lt;/b&gt; onProcessError에서 특정 항목 재처리 또는 스킵 전략 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ItemWriteListener&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface ItemWriteListener&amp;lt;S&amp;gt; extends StepListener {

    // ItemWriter.write(item) 호출 직전
    default void beforeWrite(Chunk&amp;lt;? extends S&amp;gt; items) {
    }

    // write()가 정상적으로 커밋(트랜잭션 커밋)된 이후
    default void afterWrite(Chunk&amp;lt;? extends S&amp;gt; items) {
    }

    // write() 도중 예외 발생 시
    default void onWriteError(Exception exception, Chunk&amp;lt;? extends S&amp;gt; items) {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemWriteListener는 Chunk 단위로 아이템들을 쓰는 과정의 전후와 에러 시점에 개입하는 리스너이다. beforWrite()는 write() 호출 직전, afterWrite()는 트랜잭션이 커밋된 후, onWriteError()는 에러 발생 시 롤백 전에 호출된다. 특히, afterWrite()는 ChunkListener.afterChunk()보다 먼저 실행되며, DB I/O 성능 측정, 배치 사이즈 동적 조정, 실패한 아이템 추적 등에 활용할 수 있다. onWriteError()에서 별도 트랜잭션 작업이 필요하면 PROPAGATION_REQUIRES_NEW를 반드시 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ItemWriteListener의 주요 활용 포인트는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쓰기 타이밍 측정:&lt;/b&gt; 쓰기 호출 전후 시간 기록 -&amp;gt; DB I/O 최적화 방안 도출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배치 커밋 조정:&lt;/b&gt; afterWrite에서 커밋 간격 조정 또는 배치 사이즈 재계산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오류 아이템 로깅:&lt;/b&gt; onWriteError에서 실패한 레코드 상세 로깅 -&amp;gt; 재처리 스크립트 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Chunk Reader-Processor-Writer 예제&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #282c34; color: #bbbbbb;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
@AllArgsConstructor
public class PaymentReportJobConfig {

    private static final int CHUNK_SIZE = 200;

    private final EntityManagerFactory entityManagerFactory;
    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;

    @Bean
    public Job paymentReportJob(Step paymentReportStep) {
       return new JobBuilder(&quot;paymentReportJob&quot;, jobRepository)
             .incrementer(new RunIdIncrementer())
             .start(paymentReportStep)
             .build();
    }

    @Bean
    public Step paymentReportStep(
          JpaPagingItemReader&amp;lt;PaymentSource&amp;gt; paymentReportReader
    ) {
       return new StepBuilder(&quot;paymentReportStep&quot;, jobRepository)
             .&amp;lt;PaymentSource, Payment&amp;gt;chunk(CHUNK_SIZE, transactionManager)
             // Step 소요 시간 측정
             .listener(new StepDurationTrackerListener())
             .reader(paymentReportReader)
             .processor(paymentReportProcessor())
             .writer(paymentReportWriter())
             // Chunk 소요 시간 측정
             .listener(new ChunkDurationTrackerListener())
             .build();
    }

    @Bean
    @StepScope
    public JpaPagingItemReader&amp;lt;PaymentSource&amp;gt; paymentReportReader(
          @Value(&quot;#{jobParameters['paymentDate']}&quot;) LocalDate paymentDate
    ) {
       return new JpaPagingItemReaderBuilder&amp;lt;PaymentSource&amp;gt;()
             .name(&quot;paymentSourceItemReader&quot;)
             .entityManagerFactory(entityManagerFactory)
             .queryString(&quot;SELECT ps FROM PaymentSource ps WHERE ps.paymentDate = :paymentDate&quot;)
             .parameterValues(Collections.singletonMap(&quot;paymentDate&quot;, paymentDate))
             .pageSize(10)
             .build();
    }

    private ItemProcessor&amp;lt;PaymentSource, Payment&amp;gt; paymentReportProcessor() {
       return paymentSource -&amp;gt; {
          if (paymentSource.getFinalAmount().equals(BigDecimal.ZERO)) {
             return null;
          }

          return Payment.builder()
                .amount(paymentSource.getFinalAmount())
                .paymentDate(paymentSource.getPaymentDate())
                .partnerCorpName(paymentSource.getPartnerCorpName())
                .status(&quot;PAYMENT&quot;)
                .build();
       };
    }

    @Bean
    public JpaItemWriter&amp;lt;Payment&amp;gt; paymentReportWriter() {
       return new JpaItemWriterBuilder&amp;lt;Payment&amp;gt;()
             .entityManagerFactory(entityManagerFactory)
             .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드는 특정 날짜의 결제 원천 데이터(PaymentSource)를 읽어서, 금액이 0이 아닌 건만 필터링하여 결제 데이터(Payment)로 변환한 후 저장하는 Job이다. JpaPagingItemReader로 데이터를 페이징 조회하고, chunk size 200으로 트랜잭션을 관리하며, Listener를 통해 Step과 Chunk의 실행 시간을 측정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;실행 결과&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4004&quot; data-origin-height=&quot;1154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/poYd9/dJMcahW0rB5/Lileb1OCKjfhtpolkjJpk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/poYd9/dJMcahW0rB5/Lileb1OCKjfhtpolkjJpk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/poYd9/dJMcahW0rB5/Lileb1OCKjfhtpolkjJpk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpoYd9%2FdJMcahW0rB5%2FLileb1OCKjfhtpolkjJpk1%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;4004&quot; height=&quot;1154&quot; data-origin-width=&quot;4004&quot; data-origin-height=&quot;1154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 로그는 PaymentReportJob의 실행 결과이다. 총 5만 건의 데이터를 처리했으면, 251번의 청크로 나누어 실행되었다. 마지막 청크(#251)에서 ItemReader에서 read()가 null을 반환하여 빈 청크로 인해 종료된 것을 알 수 있다. 또한,&amp;nbsp;JpaPagingItemReader을 사용하여 데이터를 조회할 때 LIMIT OFFSET 방식으로 조회한 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진은 마지막 로그에 대해서만 캡쳐했지만, 청크 수가 증가할수록 LIMIT OFFSET 방식은 읽어야하는 데이터가 많아지므로 조회가 점점 느리게 된다. 따라서 #250 청크는 751ms가 걸리고, 총 1분 31초가 소요된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/schema-appendix.html#page-title&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Meta-Data Schema - 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://fastcampus.co.kr/dev_online_springbatch&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;실무를 위한 Spring Batch의 모든 것 : 성능 개선부터 프로젝트까지&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring Batch</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/143</guid>
      <comments>https://oneny.tistory.com/143#entry143comment</comments>
      <pubDate>Thu, 6 Nov 2025 20:48:49 +0900</pubDate>
    </item>
    <item>
      <title>MongoDB 인덱싱</title>
      <link>https://oneny.tistory.com/142</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱싱&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스를 사용하면 효율적으로 쿼리할 수 있다. 인덱스를 사용하지 않는 쿼리를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;컬렉션 스캔(collection scan)&lt;/b&gt;이라 하며, 서버가 쿼리 결과를 찾으려면 '전체 내용을 살펴봐야 함'을 의미한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759745583390&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;&amp;gt; db.col3.find({ a: 3 }).explain(&quot;executionStats&quot;)

{
  explainVersion: '1',
  queryPlanner: {
    namespace: 'test.col3',
    parsedQuery: {
      a: {
        '$eq': 3
      }
    },
    // ...
    
  executionStats: {
    executionSuccess: true,
    nReturned: 59, // 반환받은 결과의 개수
    executionTimeMillis: 82, // 걸린 시간
    totalKeysExamined: 0,
    totalDocsExamined: 105166, // 도큐먼트를 읽은 개수
    executionStages: {
      isCached: false,
      stage: 'COLLSCAN',
      filter: {
        a: {
          '$eq': 3
        }
      },
      
	// ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컬렉션을 쿼리할 때 explain 함수를 이용해 쿼리가 실행될 몽고DB가 무엇을 하는지 확인할 수 있다. explain는 명령을 감싸는 커서 보조자 메서드(cursor helper method)와 사용하면 좋다. executionStats 모드는 인덱스를 이용한 쿼리의 효과를 이해하는 데 도움이 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위처럼 &quot;executionStats&quot; 필드의 값인 중첩된 도큐먼트를 살펴보면 &quot;totalDocsExamined&quot;를 통해 몽고DB가 쿼리를 실행하면서 살펴본 도큐먼트 개수를 알 수 있다. 인덱스를 사용하지 않았기 때문에 컬렉션에 들어있는 모든 도큐먼트 개수와 같다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 몽고DB가 쿼리에 효율적으로 응답하려면 애플리케이션이 요구하는 모든 쿼리 패턴(query pattern)에 인덱스를 사용해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759745583391&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;&amp;gt; db.col3.createIndex({ a: 1 })
&amp;gt; db.col3.createIndex({ a: 1, b: 1 }) // 2개 이상의 필드로 구성된 복합 인덱스 가능

&amp;gt; db.col3.find({ a: 3 }).explain(&quot;executionStats&quot;)
{
  executionStats: {
    executionSuccess: true,
    nReturned: 59,
    executionTimeMillis: 0,
    totalKeysExamined: 59,
    totalDocsExamined: 59,
	// ...
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;a 필드에 인덱스를 만들면 위처럼 totalDocsExamined, executionTImeMilles가 확연히 줄어든 것을 확인할 수 있다. 하지만 인덱싱된 필드를 변경하는 쓰기(삽입, 갱신, 삭제) 작업은 더 오래 걸리기 때문에 어떤 필드가 인덱싱하기에 적합한지 신중히 파악해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;몽고DB가 인덱스를 선택하는 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-26 오후 7.23.57.png&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;1216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMYsWb/btsQRpWCPYZ/hYwWYT3S9Ptp9kHEbGbbX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMYsWb/btsQRpWCPYZ/hYwWYT3S9Ptp9kHEbGbbX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMYsWb/btsQRpWCPYZ/hYwWYT3S9Ptp9kHEbGbbX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMYsWb%2FbtsQRpWCPYZ%2FhYwWYT3S9Ptp9kHEbGbbX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;521&quot; height=&quot;574&quot; data-filename=&quot;스크린샷 2025-09-26 오후 7.23.57.png&quot; data-origin-width=&quot;1104&quot; data-origin-height=&quot;1216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 쿼리 플래너 로직처럼&amp;nbsp;&lt;b&gt;쿼리 플래너는 주어진 쿼리에 대해 사용 가능한 인덱스를 고려하여 가장 효율적인 쿼리 계획을 선택하고 캐시&lt;/b&gt;한다. 쿼리 계획의 효율성을 평가하기 위해 쿼리 플래너는 쿼리를 충족하는데 사용할 인덱스 후보 집합을 식별하고 각 인덱스 후보에 쿼리 플랜(query plan)을 만들고, 각각 다른 인덱스를 사용하는 병렬 스레드에서 쿼리를 실행한다. 쿼리 스레드가 레이스에서 이기려면, 모든 쿼리 결과를 가장 먼저 반환하거나 결과에 대한 시범 횟수를 정렬 순서로 가장 먼저 반환해야 한다. 인메모리 정렬을 하면 비용이 많이 들기 때문에 정렬 순서는 중요한 부분이다. 즉, 평가 기간 동안 가장 적은 양의 작업을 수행하면서 가장 많은 결과를 생성하는 쿼리 계획을 채택한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버는 쿼리 플랜의 캐시를 유지하는데, 승리한 플랜은 차후 모양이 같은 쿼리에 사용하기 위해 캐시에 저장한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;복합 인덱스 사용&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1759745583392&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db.students.find({ student_id: { $gt: 500000 }, class_id: 54 })
        .sort({ final_grade: 1 })
        .explain(&quot;executionStats&quot;)
        
db.students.createdIndex({ class_id: 1, final_grade: 1, student_id: 1 })&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2개 이상의 키로 구성된 복합 인덱스를 올바르기 설계하기 위해 인덱스의 선택성(selectivity)를 고려할 수 있다.&amp;nbsp;특정 쿼리 패턴에서 스캔할 레코드 개수를 인덱스가 얼마나 최소화하는지 중요하다. 즉, 복합 인덱스를 설계할 때 일반적으로 트레이프오프가 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 쿼리는 class_id가 54이면서 student_id가 500000보다 크고, 최종 성적을 기준으로 정렬하는 모든 레코드를 요청한다. 새로운 복합 인덱스의 키는 [class_id, final_grade, student_id]와 같이 정렬되어야 한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정렬 구성 요소는 동등 필터 바로 뒤, 다중값 필터 앞에 포함&lt;/b&gt;한다. 이 인덱스는 쿼리에서 고려하는 키 집합을 매우 선택적으로 좁힌다. 그런 다음 몽고DB는 인덱스의 동등 필터와 일치하는 키 3개를 통해 다중값 필터와 일치하는 레코드를 식별한다. 해당 레코드는 최종 성적에 따라 오름차순으로 정렬된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 복합 인덱스는 몽고DB가 결과 셋에 포함될 도큐먼트보다 더 많은 도큐먼트의 키를 검사하지만 인덱스를 사용해 도큐먼트를 정렬함으로써 실행 시간을 절약할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;복합 인덱스에 대한 설계를 설계할 때 고려할 점을 요약하면 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동등 필터에 대한 키를 맨 앞에 표시해야 한다.&lt;/li&gt;
&lt;li&gt;정렬에 사용된느 키는 다중값 필드 앞에 표시해야 한다.&lt;/li&gt;
&lt;li&gt;다중값 필터에 대한 키는 마지막에 표시해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;객체 및 배열 인덱싱&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1759745583393&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{
    &quot;username&quot;: &quot;sid&quot;,
    &quot;loc&quot;: {
        &quot;ip&quot;: &quot;1.2.3.4&quot;,
        &quot;city&quot;: &quot;Springfield&quot;,
        &quot;state&quot;: &quot;NY&quot;
    }
}

db.users.createIndex({&quot;loc.city&quot;: 1})&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;몽고DB는 도큐먼트 내부에 도달해서 내장 필드와 배열에 인덱스를 생성하도록 허용한다. 내장 객체와 배열 필드는 복합 인덱스에서 최상위 필드와 결합될 수 있으며, 다소 특수한 경우를 제외하면 대부분 '일반적인' 인덱스 필드와 같은 방식으로 동작한다. 위 도큐먼트처럼 &quot;loc&quot;의 서브필드(subfield)에 인덱스를 만들어 해당 필드를 이용하는 쿼리의 속도를 높일 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;카디널리티&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;카디널리티(cardinality)는 컬렉션의 한 필드에 대해 고윳값(distinct value)이 얼마나 많은지 나타낸다. 일반적으로 필드의 카디널리티가 높을수록 인덱싱이 더욱 도움이 된다. 인덱스가 검색 범위를 훨씬 작은 결과 셋으로 빠르게 좁힐 수 있기 때문이다. 일반적으로, 낮은 카디널리티 필드에서 인덱스는 높은 카디널리티 필드에서만큼 일치하는 항목을 많이 제거할 수 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인덱스를 생성하지 않는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스가 항상 쿼리 성능을 높여주는 것은 아니다. 실제 효과는 데이터 크기, 인덱스 크기, 도큐먼트의 크기, 결과 집합의 평균 크기 등 여러 요소에 따라 달라진다. 일반적으로 매칭되는 도큐먼트의 비율이 낮을수록(=선택도가 높을수록) 인덱스가 유리하고, 반대로 컬렉션의 상당 부분을 반환하는 쿼리라면 단순 컬렉션 스캔이 더 효율적일 수 있다.인덱스 사용 여부는 explain()을 통해 실제 실행 계획을 확인하면서 판단하는 것이 좋다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱스 종류&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스를 구축할 떄 인덱스 옵션을 지정해 동작 방식을 바꿀 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;고유 인덱스&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-30 오전 7.22.16.png&quot; data-origin-width=&quot;1978&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eiCjJo/btsQSQz6CT7/NprsZhjDJ933W6QAMq9Irk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eiCjJo/btsQSQz6CT7/NprsZhjDJ933W6QAMq9Irk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eiCjJo/btsQSQz6CT7/NprsZhjDJ933W6QAMq9Irk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeiCjJo%2FbtsQSQz6CT7%2FNprsZhjDJ933W6QAMq9Irk%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;1978&quot; height=&quot;584&quot; data-filename=&quot;스크린샷 2025-09-30 오전 7.22.16.png&quot; data-origin-width=&quot;1978&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;고유 인덱스는 각 값이 인덱스에 최대 한 번 나타나도록 보장한다. 예를 들어, 여러 도큐먼트에 firstname 키에 동일한 값을 가질 수 없도록 하려면 firstname 필드가 있는 도큐먼트에 대해서만 partialFilterExpression으로 고유 인덱스를 만들면 된다. 위처럼 users 컬렉션에 도큐먼트를 삽입하면 중복 키 예외(duplicate key exception)를 발생시킨다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;_id의 인덱스는 컬렉션을 생성하면 항상 자동으로 생성되고, 다른 고유 인덱스와 달리 삭제할 수 없다는 점을 제외하면 일반적인 고유 인덱스이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;부분 인덱스&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1814&quot; data-origin-height=&quot;1108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n4XCq/btsQVsLssqN/N7H1wh5n5NYxKbFV9Jv8xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n4XCq/btsQVsLssqN/N7H1wh5n5NYxKbFV9Jv8xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n4XCq/btsQVsLssqN/N7H1wh5n5NYxKbFV9Jv8xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4XCq%2FbtsQVsLssqN%2FN7H1wh5n5NYxKbFV9Jv8xk%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;1814&quot; height=&quot;1108&quot; data-origin-width=&quot;1814&quot; data-origin-height=&quot;1108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;고유 인덱스는 null을 값으로 취급하므로, 키가 없는 도큐먼트가 여러 개인 고유 인덱스를 만들 수 없다. 하지만 오직 키가 존재할 때만 고유 인덱스가 적용되도록 할 때가 많다. 부분 인덱스를 만들려면 &quot;partialFilterExpression&quot; 옵션을 포함시킨다. 부분 인덱스를 생성하려면 필터 표현식(filter expression)을 나타내는 도큐먼트와 함께 희소 인덱스가 제공하는 기능의 슈퍼셋(superset)을 나타낸다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리는 부분 인덱스 사용 여부에 따라 다른 결과를 반환할 수 있다. 위처럼 컬렉션에 대부분의 도큐먼트가 x 필드를 가지지만, 한 도큐먼트만 x를 가지지 않을때 x에 부분 인덱스를 생성하여 인덱스를 사용하면 { _id: 0 } 도큐먼트를 반환하지 않는다.&lt;/p&gt;</description>
      <category>DB/MongoDB</category>
      <category>MongoDB index</category>
      <category>몽고db 인덱스</category>
      <category>몽고디비 인덱스</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/142</guid>
      <comments>https://oneny.tistory.com/142#entry142comment</comments>
      <pubDate>Mon, 6 Oct 2025 20:59:46 +0900</pubDate>
    </item>
    <item>
      <title>MongoDB</title>
      <link>https://oneny.tistory.com/141</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MongoDB&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 초기 설계 당시에는 어떤 DB를 사용하는지 결정하게 된다. 이때 시스템의 특성과 요구사항에 따라 관계형 데이터베이스 대신 NoSQL 데이터베이스를 채택하는 주된 이유는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 데이터셋이나 매우 높은 쓰기 처리량 달성을 관계형 데이터베이스보다 쉽게 할 수 있는 뛰어난 확장성의 필요&lt;/li&gt;
&lt;li&gt;관계형 모델에서 지원하지 않는 특수 질의 동작&lt;/li&gt;
&lt;li&gt;관계형 스키마의 제한에 대한 불만과 더욱 동적이고 표현력이 풍부한 데이터 모델에 대한 바람 &lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoSQL 데이터베이스는 이러한 요구사항에 따라 아래와 같이 크게 4가지 유형으로 구분된다. 이 중에서 MongoDB는 문서 지향(Document-oriented) 데이터베이스의 대표적인 예로 다양한 형태의 데이터를 유연하게 저장하고 확장할 수 있는 기능을 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Key-Value Store(키-값 저장소)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 고유한 키(Key)와 그에 대응하는 값(Value)의 쌍으로 저장하는 단순한 구조로 단순한 조회/캐시에 적합하다.&lt;/li&gt;
&lt;li&gt;대표적으로 Redis, AWS DynamoDB 등이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Document Store(문서 지향 데이터베이스)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 문서(Document) 단위로 저장하며 각 문서는 JSON, BSON, XML 등의 스키마리스(Schemaless) 구조를 가진다.&lt;/li&gt;
&lt;li&gt;대표적으로 MongoDB, ElasticSearch 등이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Column-Family Store(열 기반 데이터베이스)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 행(Row)이 아닌 열(Column) 중심으로 저장하는 방식으로 열 단위로 데이터를 묶어 저장하여 분석/집계 작업에 유리하다.&lt;/li&gt;
&lt;li&gt;대표적으로 Apache Cassandra, HBase 등이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Graph Database(그래프 데이터베이스)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 데이터를 노드(Node)는 엔터티(사람, 상품 등)으로, 엣지(Edge)는 관계(친구, 구매 등) 형태로 저장하여 관계 중심 질의(Relationship Query)가 매우 빠르다.&lt;/li&gt;
&lt;li&gt;대표적으로 Neo4j, ArangoDB 등이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;MongoDB 특징&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB는 관계형 데이터베이스(relational database)가 아니라 문서 지향 NoSQL 데이터베이스(document-oriented database)로, JSON과 유사하지만 효율적이고 더 많은 타입을 지원하는 BSON 문서에 데이터를 저장하여 스키마 유연성과 개발 생산성을 제공한다. 이러한 몽고DB는 다음 세 가지 큰 특징이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개발 친화적인 데이터베이스&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쉬운 분산 확장&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연한 스키마 구조&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB는 &lt;b&gt;내장 도큐먼트와 배열을 허용함으로써 문서 지향 모델은 복잡한 계층 관계(hierarchical relationship)를 하나의 레코드로 표현&lt;/b&gt;할 수 있다. 이 방식은 최신 객체 지향 언어를 사용하는 개발자의 관점에 매우 적합한 개발 친화적인 데이터베이스라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB에서는 도큐먼트의 키와 값을 미리 정의하지 않는다. 따라서 &lt;b&gt;고정된 스키마가 없으므로 필요할 때마다 쉽게 필드를 추가하거나 제거&lt;/b&gt;할 수 있다. 덕분에 개발 과정을 빠르게 반복할 수 있어 개발 속도가 향상되고, 서비스 요구사항 변화나 기능 확장에도 유연하게 대응할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB는 분산 확장을 염두에 두고 설계됐다. &lt;b&gt;도큐먼트 지향 데이터 모델은 데이터를 여러 서버에 더 쉽게 분산&lt;/b&gt;하게 해준다. 도큐먼트를 자동으로 재분배하고 사용자 요청을 올바른 장비에 라우팅함으로써 클러스터 내 데이터 양과 부하를 조절할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;몽고DB 기본 개념&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB의 기본 개념을 간략히 설명하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;몽고DB 데이터의 기본 단위는 &lt;b&gt;도큐먼트&lt;/b&gt;이며, 이는 관계형 데이터베이스의 행과 유사하다(하지만 더 다양한 자료 표현이 가능하다).&lt;/li&gt;
&lt;li&gt;같은 맥락에서 &lt;b&gt;컬렉션(collection)&lt;/b&gt;은 동적 스키마(dynamic schema)가 있는 테이블과 같다.&lt;/li&gt;
&lt;li&gt;몽고DB의 단일 인스턴스는 자체적인 컬렉션을 갖는 여러 개의 독립적인 데이터베이스를 데이터베이스를 호스팅한다.&lt;/li&gt;
&lt;li&gt;모든 도큐먼트는 컬렉션 내에서 고유한 특수키인 &lt;b&gt;&quot;_id&quot;&lt;/b&gt;를 가진다.&lt;/li&gt;
&lt;li&gt;몽고DB는 몽고 셸(The mongo Shell)이라는 간단하지만 강력한 도구와 함께 배포된다. mongo 셸은 몽고DB 인스턴스를 관리하고 몽고DB 쿼리 언어로 데이터를 조작하기 위한 내장 지원을 제공한다. 또한 사용자가 다양한 목적으로 자신의 스크립트를 만들고 로드(load)할 수 있는 완전한 기능의 자바스크립트 해석기(Javascript Interpreter)다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;도큐먼트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB의 핵심은 정렬된 키와 연결된 값의 집합으로 이뤄진 도큐먼트다. 도큐먼트 표현 방식은 프로그래밍 언어마다 다르지만 대부분의 언어는 맵(map), 해시(hash), 딕셔너리(dictionary)와 같이 도큐먼트를 자연스럽게 표현하는 자료구조를 가진다. 도큐먼트의 키는 문자열이다. 다음 예외 몇 가지를 제외하면 어떤 UTF-8 문자든 쓸 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키는 \0(null 문자)을 포함하지 않는다. \0은 키의 끝을 나타내는 데 사용된다.&lt;/li&gt;
&lt;li&gt;.과 $ 문자는 몇 가지 특별한 속성을 가지며 특정 상황에만 사용해야 한다. 이 문자들은 보통 예약어(reserved word)로 취급해야 하며 부적절하게 사용하면 드라이버에서 경고가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;컬렉션&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션은 &lt;b&gt;도큐먼트의 모음&lt;/b&gt;이다. 몽고DB의 도큐먼트가 관계형 데이터베이스의 행에 대응된다면 컬렉션은 테이블에 대응된다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬렉션은 동적 스키마를 가진다. 하나의 컬렉션 내 도큐먼트들이 모두 다른 구조를 가질 수 있다는 의미다. 하지만 인덱스를 만들려면 도큐먼트는 특정 구조를 가져야 한다. 이러한 인덱스는 컬렉션별로 정의한다. 같은 유형의 도큐먼트를 하나의 컬렉션에 넣음으로써 컬렉션을 효율적으로 인덱싱할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브컬렉션(subcollection)의 네임스페이스(namespace)에&amp;nbsp; &lt;b&gt;.&lt;/b&gt;(마침표) 문자를 사용해 컬렉션을 체계화한다. 예를 들어, 블로그 기능이 있는 애플리케이션은 blog.posts와 blog.authors라는 컬렉션을 가질 수 있다. 이는 단지 체계화를 위함이며 blog 컬렉션이나 자식 컬렉션(child collection)과는 아무런 관계가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;스키마 설계와 데이터 모델링&lt;/b&gt;&lt;/h3&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;RDBMS 한계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 주로 MySQL, Oracle, PostgreSQL과 같은 관계형 데이터베이스, 즉 RDBMS를 사용했다. 이런 데이터베이스들은 테이블 형태를 구성하고 데이터를 저장한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소셜미디어의 급격한 성장으로 관리해야 되는 데이터가 급증하게 되면서 다양한 데이터를 관리하고 , 구조가 매우 복잡해지게 구성이 될 수 밖에 없어졌다. 사용자마다 다른 형태의 정보를 가져와야 하는 경우도 있고, 데이터 간의 관계도 되게 복잡해지기 때문에 RDBMS와 같은 구조화된 형식은 한계가 되게 명확했다. 또한, &lt;b&gt;정해진 틀에서 데이터가 저장되는 형태이기 때문에 스키마 변경이 어려워 다양한 데이터들을 수용하기에는 큰 어려움&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;수평적인 확장이 어렵다.&lt;/b&gt; MySQL 같은 경우에는 클러스터 형태로 관리되는 형태가 아니기 때문에 데이터의 확장에 있어 좀 더 큰 어려움이 있다. 마지막으로 복잡한 데이터 구조를 표헌하기에 적합하지 않다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;몽고DB를 위한 스키마 설계&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755952942727&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 사용자 컬렉션의 사용자 문서 예시
{
    &quot;_id&quot;: &quot;5ef2d4b45b7f11b6d7a&quot;,
    &quot;user_id&quot;: &quot;Sherlock Holmes&quot;,
    &quot;age&quot;: 40,
    &quot;address&quot;: {
        &quot;Country&quot;: &quot;England&quot;,
        &quot;City&quot;: &quot;London&quot;,
        &quot;Street&quot;: &quot;221B Baker St.&quot;
    },
    &quot;Hobbies&quot;: [&quot;violin&quot;, &quot;crime-solving&quot;]
}

{
    &quot;_id&quot;: &quot;6ef8d4b32c9f12b6d4a&quot;,
    &quot;user_id&quot;: &quot;John Waston&quot;,
    &quot;age&quot;: 45,
    &quot;address&quot;: {
        &quot;Country&quot;: &quot;England&quot;,
        &quot;Street&quot;: &quot;221B Baker St.&quot;
    },
    &quot;Medical license&quot;: &quot;Active&quot;,
    &quot;Job&quot;: &quot;Software Engineer&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB는 기존 관계형 데이터베이스의 접근 방식과는 다른 모델링 전략이 요구된다. 이러한 전략적 변화는 각 애플리케이션의 고유한 데이터 활용 패턴을 고려하여 이루어져야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;효과적인 몽고DB 설계의 핵심은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;사용자의 데이터 조회 요구사항을 정확히 파악&lt;/b&gt;&lt;/span&gt;하는 것이다. 이는 시스템 엔티티의 구조를 결정하는 데 결정적인 역할을 한다. 관계형 데이터베이스가 정규화를 중시하고 데이터 중복을 지양하는 것과 달리, &lt;b&gt;몽고DB는 성능 최적화와 유연성 확보를 위해 의도적으로 데이터 중복과 비정규화를 활용&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB의 문서 모델은 독특한 강점을 제공한다. 하나의 컬렉션 내에서도 각 문서는 서로 다른 구조를 가질 수 있으며, 같은 필드명이라도 다른 데이터 타입을 포함할 수 있다. 위 사용자 컬렉션의 두 문서를 확인하면 기본적인 구조는 유사하지만, 세부적인 내용에서 차이를 보이는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;몽고DB의 데이터 타입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본 데이터형&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB는 도큐먼트의 값으로 기본 데이터형부터 날짜, 배열 등 다양한 데이터형을 지원한다.&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;타입&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;&lt;b&gt;표현&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;null&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;{&quot;x&quot;: null}&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;null 값과 존재하지 않는 필드를 표현하는 데 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;불리언&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;{&quot;x&quot;: true}&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;참과 거짓 값에 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;숫자&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;{&quot;x&quot;: 3.14}, {&quot;x&quot;: 3}&lt;br /&gt;{&quot;x&quot;: NumberInt(&quot;3&quot;)}&lt;br /&gt;{&quot;x&quot;: NumberLong(&quot;3&quot;)}&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;셸은 64비트 부동소수점 수를 기본으로 사용한다.&lt;br /&gt;4바이트 혹은 6바이트의 부호 정수(signed integer)는 각각 NumberInt 혹은 Number 클래스를 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;문자열&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;{&quot;x&quot;: &quot;foobar&quot;}&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;어떤 UTF-8 문자열이든 문자열형으로 표현할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;날짜&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;{&quot;x&quot;: new Date()}&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;몽고DB는 1970년 1월 1일부터의 시간을 1/1000초 단위로 64비트 정수로 날짜를 저장한다. 표준 시간대(time zone)은 저장하지 않는다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;정규 표현식&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;{&quot;x&quot;: /foobar/i}&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;쿼리는 자바스크립트의 정규 표현식 문법을 사용할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;배열&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;{&quot;x&quot;: [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]}&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;값의 셋(set)이나 리스트(list)를 배열로 표현할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;내장 도큐먼트&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;{&quot;x&quot;: {&quot;foo&quot;: &quot;bar&quot;}}&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;도큐먼트는 부모 도큐먼트의 값으로 내장된 도큐먼트 전체를 포함할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;객체 ID&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;{&quot;x&quot;: ObjectId()}&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;객체 ID는 도큐먼트용 12바이트 ID다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;이진 데이터&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td colspan=&quot;3&quot;&gt;이진 데이터는 임의의 바이트 문자열이며 셸에서는 조작이 불가능하다. 이진 데이터는 데이터베이스에 UTF-8이 아닌 문자열을 저장하는 유일한 방법이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%;&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3334%;&quot; colspan=&quot;2&quot;&gt;{&quot;x&quot;: function() { /* ... */ }}&lt;/td&gt;
&lt;td style=&quot;width: 50.0001%;&quot; colspan=&quot;3&quot;&gt;쿼리와 도큐먼트는 임의의 자바스크립트 코드를 포함할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB에서 도큐먼트는 자바스크립트 객체와 개념적으로 닮았다는 점에서 JSON과 닮았다고 할 수 있다. 하지만 데이터형이 null, 불리언, 숫자, 문자열, 배열, 객체만 지원하기 떄문에 JSON의 표현력은 제한적이여서 몽고DB는 JSON의 키/값 쌍 성질을 유지하면서 추가적인 데이터형을 지원한다. 각 데이터형 값을 표시하는 방식은 언어에 따라 다르지만, 위 목록은 흔히 지원되는 데이터형이 셸에서 어떻게 도큐먼트의 일부로 표현되는지 나타낸다.&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;_id와 ObjectId&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB에 저장된 모든 도큐먼트는 &quot;_id&quot; 키를 가진다. &quot;_id&quot; 키 값은 어떤 데이터형이어도 상관없지만 &quot;ObjectId&quot;가 기본 데이터형이다. 하나의 컬렉션에서 모든 도큐먼트는 고유한 &quot;_id&quot; 값을 가지며, 이 값은 컬렉션 내 모든 도큐먼트가 고유하게 식별되게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ObjectId 클래스는 몽고DB의 분산 데이터베이스를 고려하여 설계&lt;/b&gt;됐다. 여러 서버에 걸쳐 자동으로 증가하는 기본 키를 동기화하는 작업은 어렵고 시간이 걸리기 때문에 샤딩된 환경에서 12 바이트 고유 식별자(unique identifier)로 생성된다. 생성 시점, 서버, 프로세스, 카운터 값을 조합해 전역적으로 중복이 거의 없도록 설계되어 정렬, 샤딩, 분산 환경에서 빠르고 안정적으로 유니크성을 보장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ObjctId의 앞 9바이트는 1초 동안 여러 장비와 프로세스에 걸쳐 유일성을 보장한다. 마지막 3바이트는 단순히 증분하는 숫자로, 1초 내 단일 프로세스의 유일성을 보장한다. 고유한 ObjectId는 프로세스당 1초에 256^3(1677만7216)개까지 생성된다.&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;JSON과 BSON 차이와 활용&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;JSON&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;BSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Encoding&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;UTF-8 String&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Readability&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Humans and Machines&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Machines Only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;Data Types&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;String, Boolean, Number, Array&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;String, Boolean, Number (Integer, Float, Long, Decimal128, ...), Array, Date, Raw Binary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON은 Key-Value 형태로 구성되어 일반적으로 사람이 읽고 쓰기 쉽게 구성이 되어 있고 기계가 해석하고 생성하기 쉬운 UTF-8 기반 데이터 교환 포맷이다. 문자열, 숫자, 객체 및 배열 형태 등 기본적인 타입들을 모두 지원하여 언어나 플랫폼에 관계없이 대중적으로 많이 사용하는 것이 장점이라고 할 수 있다. 하지만 JSON은 텍스트 기반이라는 특성상, 성능과 효율성 측면에서 한계가 존재하기 때문에 이러한 한계를 보완하기 위해 몽고DB는 BSON(Binary JSON)이라는 이진 형태의 직렬화 포맷을 내부적으로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BSON(Binary JSON)은 &lt;b&gt;이진 형태로 직렬화된 데이터 포맷을 지원&lt;/b&gt;하여 몽고DB가 데이터를 저장하고 네트워크를 통해 전송할 때 사용하는 기본 데이터 표현 방식이다. 또한, 텍스트 기반의 JSON 보다 날짜, 정수, 이진 데이터 등 다양한 타입을 지원하면서 바이너리 포맷이기 때문에 JSON에 비해 직렬화/역직렬화가 더 빠르고 전송 데이터 크기 감소로 네트워크 전송 효율성이 향상된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;샤딩(Sharding)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB에서 샤딩(Sharding)은 분할이라는 뜻으로 대용량의 데이터를 여러 노드로 분산해서 저장하는 기법을 말한다. 단일 서버에서 모든 데이터가 저장되면 서버 자체가 부담이 되기 때문에 읽기 및 쓰기 처리량에 대해 반드시 한계치가 찾아올 수 밖에 없다. 따라서 몽고DB는 샤딩을 활용하여 데이터를 동시에 여러 노드에서 읽고 쓸 수 있도록 하여 효율적으로 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-06 오후 8.22.24.png&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rKfvM/btsQ2upOtNt/KCxQmt1kFLr81qg5BMS7w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rKfvM/btsQ2upOtNt/KCxQmt1kFLr81qg5BMS7w0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rKfvM/btsQ2upOtNt/KCxQmt1kFLr81qg5BMS7w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrKfvM%2FbtsQ2upOtNt%2FKCxQmt1kFLr81qg5BMS7w0%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;525&quot; height=&quot;370&quot; data-filename=&quot;스크린샷 2025-10-06 오후 8.22.24.png&quot; data-origin-width=&quot;1158&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB 샤딩 클러스터는 위 그림과 같은 컴포넌트로 구성되고, 컬렉션 수준에서 데이터를 샤딩하여 클러스터의 샤드 전체에 컬렉션 데이터를 분산한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;샤드&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 데이터를 저장하는 서버로 각 샤드에는 샤드 데이터의 하위 집합이 포함된다.&lt;/li&gt;
&lt;li&gt;각 샤드는 레플리카 셋으로 배포되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mongos 사용한 라우팅&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mongos는 클라이언트 애플리케이션과 샤딩된 클러스터 간의 인터페이스를 제공하는 쿼리 라우터 역할을 한다.&lt;/li&gt;
&lt;li&gt;클라이언트에 요청이 들어오면 어떤 샤드에 데이터가 있는지를 판단하고 해당 요청을 데이터가 존재하는 샤드로 라우팅해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;config 서버&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;config 서버는 클러스터에 대한 메타데이터와 구성 설정을 저장한다. config 서버는 레플리카 셋으로 배포되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB는 샤드 키를 사용하여 샤드에 대해 컬렉션 문서를 분산하고, 샤드 키는 문서의 필드 하나 또는 여러 필드로 구성될 수 있다. 샤드 키를 통해 데이터 저장 용량이 수평적으로 나눠 저장될 수 있고, 처리량을 향상시킬 수 있다. 즉, 샤드 키를 잘못 선택하는 경우 부하가 한 샤드에 집중될 수도 있기 때문에 알맞는 샤드 키를 선택하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB 완벽 가이드 : 실전 예제로 배우는 NoSQL 데이터베이스 기초부터 활용까지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.mongodb.com/ko-kr/docs/manual/sharding/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;샤딩 - 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.mongodb.com/ko-kr/docs/manual/core/replica-set-architecture-three-members/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;3개 노드로 구성된 복제본 세트&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/MongoDB</category>
      <category>ㄷ</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/141</guid>
      <comments>https://oneny.tistory.com/141#entry141comment</comments>
      <pubDate>Mon, 6 Oct 2025 20:59:23 +0900</pubDate>
    </item>
    <item>
      <title>MongoDB 트랜잭션</title>
      <link>https://oneny.tistory.com/140</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트랜잭션&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 &lt;b&gt;읽기나 쓰기 작업이 가능한 데이터베이스 작업을 하나 이상 포함하는 데이터베이스의 논리적 작업 단위&lt;/b&gt;를 의미한다. 데이터베이스 운영 시 여러 도큐먼트에 관한 읽기과 쓰기 작업이 필요한 경우가 많고, 이러한 도큐먼트들은 하나의 컬렉션에 있을 수도 있고, 여러 컬렉션에 분산되어 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB는 비관계형 데이터베이스이므로 전통적인 관계형 데이터베이스의 트랜잭션 방식을 그대로 적용할 수 없다. &lt;a href=&quot;https://oneny.tistory.com/139&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;몽고DB CRUD&lt;/a&gt; 블로그에서 설명했듯이 updateOne, &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;findOneAndDelete, findOneAndReplace, findOneAndUpdate 등 Single Document에 대해서는 원자성을 보장한다. 몽고DB의 경우 insertMany처럼 전체 쿼리에 대해 일부 실패하면 그 시점까지만 반영되고 나머지는 반영되지 않는 것이 기본동작이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;몽고DB는 2018년 버전 4.0의 출시와 함꼐 다중 문서 ACID 트랜잭션 지원을 시작했다. Replica Set의 경우 4.0부터, Sharding의 경우 4.2부터 Multi Document Transaction을 지원하고 있어 몽고DB도 전체 트랜잭션이 가능하다.&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;b&gt;ACID 정의&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ACID는 데이터베이스 트랜잭션의 핵심 특성을 나타내는 것으로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;원자성, 일관성, 격리성, 지속성&lt;/b&gt;을 의미한다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원자성(Atomicity)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션의 완전성을 보장하는 특성으로 트랜잭션에 포함된 모든 작업은 전부 성공하거나 전부 실패한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성(Consistency)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 무결성을 보장하는 특성으로 트랜잭션이 완료된 후에도 모든 데이터는 정의된 규칙과 제약을 준수하는 일관된 상태를 유지해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고립성(Isolation)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 실행되는 트랜잭션 간의 독립성을 보장한다. 각 트랜잭션은 다른 트랜잭션의 중간 상태를 볼 수 없으며, 마치 자신만이 실행되고 있는 것처럼 작동해야 한다.&lt;/li&gt;
&lt;li&gt;몽고DB의 WiredTiger 스토리지 엔진은 스냅샷 격리(Snapshot Isolation)를 기본적으로 제공하여 하나의 트랜잭션에서 쿼리는 항상 같은 결과를 반환하고, readConcern과 writeConcern 조합으로 조절할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;영속성(Durability)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션이 성공적으로 완료된 후의 데이터 보존을 보장한다. 한 번 커밋된 트랜잭션의 결과는 시스템 장애가 발생하더라도 영구적으로 보존되어야 한다.&lt;/li&gt;
&lt;li&gt;WiredTiger 스토리지 엔진을 통해 WAL(Write-ahead Log) 메커니즘을 구현하여 100밀리초 간격으로 트랜잭션을 디스크 저널에 자동으로 커밋함으로써 데이터의 영구성을 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;일관성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스의 상태를 관리하는 일관성에 대해 자세히 알아보자. 데이터베이스의 일관성 모델은 크게 두 가지로 나눌 수 있고, 몽고DB는 이 두 가지 모델의 중간 특성에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, &lt;b&gt;최종 일관성(eventual consistency)&lt;/b&gt;은 분산 데이터 시스템에서 가장 널리 사용되는 모델이다. 이 모델에서는 데이터가 업데이트되면 시간이 지남에 따라 모든 후속 읽기 작업에서 최신 커밋된 값을 확인할 수 있다. 분산 시스템에서 데이터는 서버 네트워크를 통해 복제되므로, 성능 측면에서 최종 일관성 모델이 가장 실용적인 선택이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음, &lt;b&gt;강한 일관성(strong consistency)&lt;/b&gt; 모델은 모든 후속 읽기 작업에서 항상 최근에 커밋된 쓰기 값을 확인할 수 있도록 보장한다. 이를 위해서는 다음 읽기 작업이 시작되기 전에 모든 업데이트가 전체 서버에 전파되고 커밋되어야 한다. 그러나 이러한 특성은 분산 시스템에서는 성능 저하가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;몽고DB의 ACID 구현&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB의 데이터 모델링 방식은 전통적인 관계형 데이터베이스와는 차이가 있다. 관계형 데이터베이스가 데이터를 여러 테이블로 분리하는 반면, 몽고DB는 문서 내에 하위 문서와 배열을 활용하여 관련 데이터를 계층적으로 구조화한다.&amp;nbsp;&lt;a href=&quot;https://www.mongodb.com/resources/products/capabilities/mongodb-multi-document-acid-transactions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;몽고DB의 공식 백서&lt;/a&gt;에 따르면 대다수의 애플리케이션(약 80~90%)은 멀티 도큐먼트 트랜잭션이 필요하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 도큐먼트 ACID 트랜잭션이 필요한 이유 중 하나로 데이터의 규모가 몽고DB의 단일 문서 크기 제한인 16MB를 초과하는 경우가 있다. 데이터를 하나의 주 도큐먼트 내 하위 도큐먼트 및 배열로 저장하는 것이 불가능하여, 여러 문서에 걸친 트랜잭션 처리가 필수적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Replica Set와 Sharding 클러스터에서의 몽고DB 트랜잭션 구성은 세심한 주의가 필요하다. 모든 트랜잭션을 시작할 때는 반드시 읽기 및 쓰기 보장 수준을 적절히 설정해야 하고, 몽고DB 4.4부터는 클러스터 전체에 걸쳐 읽기와 쓰기 보장 수준을 통합적으로 구성할 수 있다.&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;Write Concern&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 146px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;b&gt;Write Concern&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 37px;&quot;&gt;ACKNOWLEDGED&lt;/td&gt;
&lt;td style=&quot;height: 37px;&quot; colspan=&quot;2&quot;&gt;프라이머리 노드가 쓰기 작업을 메모리에 적용 후 바로 응답&lt;/td&gt;
&lt;td style=&quot;height: 37px;&quot; colspan=&quot;2&quot;&gt;빠른 응답 속도를 제공하지만, 장애 시 데이터 일관성이 낮을 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;UNACKNOWLEDGED&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot; colspan=&quot;2&quot;&gt;요청을 보내면, 저장 유무 상관없이 성공을 응답&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot; colspan=&quot;2&quot;&gt;매우 빠른 응답을 제공하지만 데이터 손실 가능성이 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;MAJORITY&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot; colspan=&quot;2&quot;&gt;세컨더리 노드까지 과반수의 합의가 된다면 응답&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot; colspan=&quot;2&quot;&gt;높은 데이터 일관성을 보장하며, 과반수에서 기록이 완료되므로 성능이 저하될 수도 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 37px;&quot;&gt;W1, W2&lt;/td&gt;
&lt;td style=&quot;height: 37px;&quot; colspan=&quot;2&quot;&gt;세컨더리 노드에서 성공을 합의하는 갯수가 충족된다면 응답&lt;/td&gt;
&lt;td style=&quot;height: 37px;&quot; colspan=&quot;2&quot;&gt;&lt;span&gt;특정 개수의 노드에 기록되었는지 제어 가능하며, 안정성과 성능 사이를 조절할 수 있다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;JOURNALED&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 18px;&quot; colspan=&quot;2&quot;&gt;쓰기 작업이 디스크 저널에 기록이 된다면 응답&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 18px;&quot; colspan=&quot;2&quot;&gt;장애 시 복구가 가능하며 안전한 기록을 보장하지만, 성능 저하가 있을 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;wrtieConcern은 몽고DB에서 레플리카 셋이나 샤딩된 클러스터에 대한 쓰기(write) 작업에 대해 확인 수준을 말한다. 몽고DB 5.0 이상의 버전에서는 클라이언트 수준의 majority 쓰기 보장을 기본값으로 사용한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-05 오후 11.00.16.png&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rj3y6/btsQ3cB8oWZ/hnUMwHmQ8uk5kUuMuNU9s0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rj3y6/btsQ3cB8oWZ/hnUMwHmQ8uk5kUuMuNU9s0/img.png&quot; data-alt=&quot;출처: https://www.mongodb.com/ko-kr/docs/manual/core/replica-set-write-concern/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rj3y6/btsQ3cB8oWZ/hnUMwHmQ8uk5kUuMuNU9s0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frj3y6%2FbtsQ3cB8oWZ%2FhnUMwHmQ8uk5kUuMuNU9s0%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;475&quot; height=&quot;493&quot; data-filename=&quot;스크린샷 2025-10-05 오후 11.00.16.png&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.mongodb.com/ko-kr/docs/manual/core/replica-set-write-concern/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 writeConcern: { w: &quot;majority&quot; } 설정일 때, 몽고DB에 쓰기 요청을 할 때 응답이 오기까지의 과정을 나타낸다. 프라이머리 노드가 데이터를 먼저 메모리에 적용(Apply)한 뒤, 세컨더리 노드 과반수에 복제가 완료되면 클라이언트(Driver)에게 성공 응답을 보낸다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Read Concern&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;&lt;b&gt;Read Concern&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;local&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;현재 연결된 노드(Primary 또는 Secondary)의 메모리에 반영된 최신 데이터 읽기&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;&lt;span&gt;롤백된 가능성이 있는 데이터를 읽어 데이터 일관성이 낮다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;majority&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;레플리카 셋의 과반수 노드에 커밋된 데이터 읽기&lt;/td&gt;
&lt;td colspan=&quot;2&quot;&gt;&lt;span&gt;local 수준보다 안정적이며, 데이터가 장애 시에도 롤백되지 않는다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%;&quot;&gt;snapshot&lt;/td&gt;
&lt;td style=&quot;width: 40%;&quot; colspan=&quot;2&quot;&gt;트랜잭션 내에서 일관된 스냅샷 기준으로 읽기&lt;/td&gt;
&lt;td style=&quot;width: 40%;&quot; colspan=&quot;2&quot;&gt;&lt;span&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;Read Concern은 '읽은 데이터가 어느 시점 기준으로 보장되는가', 즉 데이터의 일관 성 수준을 정의한다. 몽고DB 클라이언트는 기본적으로 local 수준을 사용하여 해당 노드의 메모리에 반영된 최신 데이터를 읽어온다. local 수준보다 안정적으로 읽을 필요가 있는 경우에는 majority 수준을, 데이터 격리가 필요한 경우에는 snapshot 수준을 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mjority 쓰기 보장은 커밋된 데이터의 일관된 스냅숏을 제공한다. majority와 snapshot 읽기 수준을 보장하기 위해서는 해당 트랜잭션이 반드시 majority 쓰기 보장 수준으로 커밋되어야 한다. 만약 트랜잭션이 이보다 낮은 수준의 쓰기 보장으로 커밋되면, 시스템은 읽기 작업이 과반수의 승인을 받은 데이터에 접근한다는 것을 보장할 수 없다.&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;Read Preference&lt;/b&gt;&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 102px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 17px;&quot;&gt;&lt;b&gt;Read Preference&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 17px;&quot;&gt;primary&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;항상 프라이머리 노드에서 읽기&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;기본값으로 가장 최신 데이터를 읽을 수 있지만 부하가 집중될 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 17px;&quot;&gt;primaryPreferred&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;프라이머리 우선, 없으면 세컨더리 노드에서 읽기&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;primary 수준보다 높은 가용성을 보장한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 17px;&quot;&gt;secondary&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;항상 세컨더리에서 읽기&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;읽기 부하 분산이 가능하지만 복제 지연이 발생할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 17px;&quot;&gt;secondaryPreferred&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;세컨더리 우선, 없으면 프라이머리 노드에서 읽기&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot; colspan=&quot;2&quot;&gt;읽기 부하 분산이 가능하지만 프라이머리에서 읽을 때보다는 일관성 보장 가능성이 낮을 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 17px;&quot;&gt;nearest&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 17px;&quot; colspan=&quot;2&quot;&gt;여러 세컨더리 노드 중 네트워크 지연시간이 가장 적은 노드를 선택하여 읽기&lt;/td&gt;
&lt;td style=&quot;width: 40%; height: 17px;&quot; colspan=&quot;2&quot;&gt;네트워크 대기시간을 줄여 빠른 응답 시간을 제공하지만 일관성 보장이 안된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read Concern이 '데이터를 어느 시점 기준으로 읽을 것인가'로 데이터의 일관성 수준을 결정한다면 Read Preference는 '어떤 노드에서 읽을 것인가'인 데이터의 출처 위치를 결정하여 레플리카 셋 환경에서 중요한 역할을 한다. 즉, readConcern이 local이고, readPreference가 secondary로 설정되었다면 쿼리는 세컨더리 노드로 보내지고, 세컨더리 노드의 로컬 메모리 상태를 기준으로 데이터를 읽어 프라이머리 노드보다 복제 지연(Replication Lag)으로 인해 조금 이전 시점의 데이터를 읽어올 수 있다는 점을 주의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;몽고DB의 Replication Lag 이슈&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-06 오후 6.45.14.png&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;1082&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brGBE0/btsQ2qA0qIo/odMGpmOYMvo3WsanXsj631/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brGBE0/btsQ2qA0qIo/odMGpmOYMvo3WsanXsj631/img.png&quot; data-alt=&quot;출처: https://oliveyoung.tech/2024-12-17/catalog-mongo-transaction-2/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brGBE0/btsQ2qA0qIo/odMGpmOYMvo3WsanXsj631/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrGBE0%2FbtsQ2qA0qIo%2FodMGpmOYMvo3WsanXsj631%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;697&quot; height=&quot;479&quot; data-filename=&quot;스크린샷 2025-10-06 오후 6.45.14.png&quot; data-origin-width=&quot;1576&quot; data-origin-height=&quot;1082&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://oliveyoung.tech/2024-12-17/catalog-mongo-transaction-2/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레플리카 셋이나 샤딩된 클러스터 환경에서는 Write Concern과 Read Preference 설정이 큰 영향을 미친다. 위 그림처럼 WriteConcern.ACKNOWLEDGED와 ReadPreference.secondaryPreferred 설정이 데이터 조회에 어떤 방식으로 영향을 미치는지 보여준다. 아래처럼 프라이머리와 세컨더리 간의 복제 지연 때문에 세컨더리 노드에서 데이터를 찾지 못하는 문제가 발생한 것을 확인할 수 있다. 이를 코드로 표현하면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759744117851&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class RequestAddService {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Transactional(&quot;mongoTransactionManager&quot;)
    public ResponseDto addRequest(RequestDto requestDto) {
        mongoTemplate.setWriteConcern(WriteConcern.ACKNOWLEDGED);
        mongoTemplate.save(requestDto.toEntity());
        // -&amp;gt; Primary 노드까지 진행이 된다면 응답을 받는다. -&amp;gt; 세컨더리 노드에는 없을수도 있다.

        Query query = Query.query(Criteria.where(&quot;_id&quot;).is(requestDto.getId()));
        ReadPreference readPreference = ReadPreference.secondaryPreferred();
        Document doc = mongoTemplate.getDb()
                .getCollection(&quot;request&quot;)
                .withReadPreference(readPreference)
                .find(new Document(&quot;_id&quot;, requestDto.getId()))
                .first();

        if (doc == null) {
            System.out.println(&quot;Lag 발생: Secondary에서 데이터 조회 실패&quot;);
        }

        return new ResponseDto(requestDto.getId());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드처럼 프라이머티와 세컨더리 간 복제가 완료되지 않은 상황에서 일관성이 중요하다면 기대한 결과를 얻지 못할 가능성이 생길 수 있다. 즉, 세컨더리 노드에서 복제 지연이 발생하면 데이터를 찾지 못하는 상황으로 이어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Read Preference 조정으로 일관성 보장&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1759744379427&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// addRequest 비즈니스 로직 내 설정 수정
ReadPreference readPreference = ReadPreference.primary();

// 트랜잭션 시 설정 수정
@Configuration
public class MongoConfig {
    
    @Bean(name = &quot;mongoTransactionManager&quot;)
    public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
        TransactionOptions transactionOptions = TransactionOptions.builder()
                // 트랜잭션이 선언되어 있다면, PRIMARY에서 읽어야 한다.
                .readPreference(ReadPreference.primary())
                .build();
        
        return new MongoTransactionManager(dbFactory, transactionOptions);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드처럼 서비스 비즈니스 로직 또는 빈으로 트랜잭션 상황에서만 Read Preference 설정 Primary로 설정하여 프라이머리 노드에서 항상 최신의 데이터를 읽을 수 있도록 보장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;애플리케이션을 위한 트랜잭션 제한 조정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB 트랜잭션에는 두 가지 주요 제한 범주가 있다. 첫 번쨰는 트랜잭션이 실행될 수 있는 시간, 트랜잭션이 락을 획득하려고 대기하는 시간, 모든 트랜잭션이 실행될 최대 길이를 제어하는 것과 관련 있다. 두 번째 범주는 몽고DB oplog 항목과 개별 항목에 대한 크기 제한과 관련 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;시간 제한&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 최대 실행 시간은 기본적으로 1분 이하다. 이는 mongod 인스턴스 레벨에서 transactionLifetimeLimitSeconds에 의해 제어되는 제한을 수정해 증가시킬 수 있다. 샤드 클러스터(sharded cluster)의 경우 모든 샤드 레플리카 셋 멤버에 매개변수를 설정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션의 작업에 필요한 락을 획득하기 위해 트랜잭션이 대기하는 최대 시간은 기본적으로 &lt;b&gt;5밀리세컨드&lt;/b&gt;다. 이 시간 내에 락을 획득할 수 없으면 중단되고, maxTransactionLockRequestTimeoutMillis에 의해 제어되는 제한을 수정해 늘릴 수 있다. 해당 설정값은 0, -1 또는 0보다 큰 숫자로 설정할 수 있다. 0으로 설정한 경우 필요한 모든 락을 즉시 획득할 수 없으면 트랜잭션이 중단된다. -1로 설정하면 작업별 제한 시간이 maxTimeMS에 지정된대로 사용된다. 0보다 큰 숫자는 트랜잭션이 필요한 락을 획득하려고 시도하는 (지정된) 기간으로 해당 시간까지의 대기 시간(wait time)(초)을 구성한다.&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;Oplog 크기 제한&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB는 트랜잭션의 쓰기 작업에 필요한만큼 oplog 항목을 생성한다. 그러나 각 oplog 항목은 BSON 도큐먼트 크기 제한인 16메가바이트 이하어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB 완벽 가이드: 실전 예제로 배우는 NoSQL 데이터베이스 기초부터 활용까지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마스터링 몽고DB 7.0 : 고급 쿼리 및 아틀라스 등 MongoDB 전문 지식으로 데이터 엑셀런스 달성하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-12-17/catalog-mongo-transaction-2/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Boot MongoDB 트랜잭션 도입 실전 가이드&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.mongodb.com/ko-kr/docs/manual/core/transactions/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MongoDB Transactions - 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.mongodb.com/ko-kr/docs/manual/core/wiredtiger/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;WiredTiger Storage Engine - 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mozi.tistory.com/652?category=1427227&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;잠금과 트랜잭션(1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hoing.io/archives/77095&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MongoDB - 트랜잭션(Transaction) - Isolation Level(격리 수준)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-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>DB/MongoDB</category>
      <category>mongodb transaction</category>
      <category>몽고db 트랜잭션</category>
      <category>몽고디비 트랜잭션</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/140</guid>
      <comments>https://oneny.tistory.com/140#entry140comment</comments>
      <pubDate>Mon, 6 Oct 2025 19:11:41 +0900</pubDate>
    </item>
    <item>
      <title>몽고DB CRUD</title>
      <link>https://oneny.tistory.com/139</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;몽고DB CRUD&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfeZLI/btsQbY5kosI/BNFxMtLwmrnnzMAAu4m4Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfeZLI/btsQbY5kosI/BNFxMtLwmrnnzMAAu4m4Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfeZLI/btsQbY5kosI/BNFxMtLwmrnnzMAAu4m4Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfeZLI%2FbtsQbY5kosI%2FBNFxMtLwmrnnzMAAu4m4Wk%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;690&quot; height=&quot;166&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;몽고DB는 위 명령행처럼 몽고DB 인스턴스와 상호작용하는 자바스크립트 셸을 제공한다. 셸은 관리 기능이나, 실행 중인 인스턴스를 점검하거나 간단한 기능을 시험하는데 매우 유용하다. 셸은 시작할 때 몽고DB 서버의 test 데이터베이스에 연결하고, 데이터베이스 연결을 전역 변수 db에 할당한다. 따라서 데이터베이스 선택하기 위해서는 use 명령어를 통해 선택할 수 있다. 그리고 셸에서 데이터를 조작하거나 보려면 생성(create), 읽기(read), 갱신(update), 삭제(delete)의 네 가지 기본적인 작업(CRUD)을 할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;삽입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;삽입&lt;/b&gt;은 몽고DB에 데이터를 추가하는 기본 방법이다.&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;insertOne&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bewQJp/btsQab5HEi3/SRk4ULn1DigkP9KYd29Xok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bewQJp/btsQab5HEi3/SRk4ULn1DigkP9KYd29Xok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bewQJp/btsQab5HEi3/SRk4ULn1DigkP9KYd29Xok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbewQJp%2FbtsQab5HEi3%2FSRk4ULn1DigkP9KYd29Xok%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;676&quot; height=&quot;209&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;insertOne 함수는 컬렉션에 도큐먼트를 추가한다. 예를 들어, 영화를 저장한다고 가정하자. 우선 도큐먼트를 나타내는 자바스크립트 객체인 movie라는 지역변수(local variable)를 생성한다. movie 변수는 &quot;title&quot;, &quot;director&quot;, &quot;year&quot;와 같은 키를 가진다. 위 객체는 유효한 몽고DB 도큐먼트이며 insertOne 함수를 이용해 movies 컬렉션에 저장할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;insertMany&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1078&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E6HO1/btsQfEefvw4/VP5sk5dBkEnV7TB0i3HkD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E6HO1/btsQfEefvw4/VP5sk5dBkEnV7TB0i3HkD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E6HO1/btsQfEefvw4/VP5sk5dBkEnV7TB0i3HkD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE6HO1%2FbtsQfEefvw4%2FVP5sk5dBkEnV7TB0i3HkD1%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;661&quot; height=&quot;376&quot; data-origin-width=&quot;1896&quot; data-origin-height=&quot;1078&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러 도큐먼트를 컬렉션에 삽입하려면 insertMany로 도큐먼트 배열을 데이터베이스에 전달한다. 코드가 삽입된 각 도큐먼트에 대해 데이터베이스로 왕복하지 않고 도큐먼트를 대량 삽입(bulk insert)하므로 훨씬 더 효율적이고 매우 빨라진다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;몽고DB의 4버전은 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;48MB보다 큰 메시지를 허용하지 않으므로 한 번에 일괄 삽입할 수 있는 데이터의 크기에는 제한&lt;/b&gt;&lt;/span&gt;이 있다. 48MB보다 큰 삽입을 시도하면 많은 드라이버는 삽입된 데이터를 &lt;b&gt;48MB 크기의 일괄 삽입 여러 개로 분할&lt;/b&gt;한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;몽고DB는 한 번의 호출로 여러 유형의 작업을 일괄 처리하는 대량 쓰기 API(Bulk Write API)를 지원한다. 확인하고 싶으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.mongodb.com/ko-kr/docs/manual/&quot;&gt;몽고DB 공식&lt;/a&gt;문서에서 확인하면 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;일괄 삽입 오류&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;insertMany를 사용해 대량 삽입할 때 배열 중간에 있는 도큐먼트에서 특정 유형의 오류가 발생하는 경우, &lt;b&gt;정렬 및 비정렬 연산 선택에 따라 발생하는 상황이 달라진다&lt;/b&gt;. insertMany 두 번째 매개변수로 옵션 도큐먼트(option document)에 &quot;ordered&quot; 키로 지정할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-15 오후 9.23.06.png&quot; data-origin-width=&quot;2044&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0IiYi/btsQzdg9lo0/mwcUEOEsi56LKKQlkiz3C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0IiYi/btsQzdg9lo0/mwcUEOEsi56LKKQlkiz3C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0IiYi/btsQzdg9lo0/mwcUEOEsi56LKKQlkiz3C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0IiYi%2FbtsQzdg9lo0%2FmwcUEOEsi56LKKQlkiz3C1%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;748&quot; height=&quot;281&quot; data-filename=&quot;스크린샷 2025-09-15 오후 9.23.06.png&quot; data-origin-width=&quot;2044&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본값인 true(정렬된 삽입, ordered insert)를 지정한 경우 도큐먼트가 제공된 순서대로 삽입된다. 정렬된 삽입(ordered intert)의 경우 전달된 배열이 삽입 순서를 정의하여 삽입 도중 오류가 발생하면, 배열에서 해당 지점을 벗언나 도큐먼트는 삽입되지 않는다. 위 예제처럼 정렬된 삽입이 기본값이므로 처음 두 개의 도큐먼트만 삽입된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-15 오후 9.27.38.png&quot; data-origin-width=&quot;1926&quot; data-origin-height=&quot;970&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EZkeu/btsQzGJ94Qr/lUsC7LKQjk9WwmMj3a1en0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EZkeu/btsQzGJ94Qr/lUsC7LKQjk9WwmMj3a1en0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EZkeu/btsQzGJ94Qr/lUsC7LKQjk9WwmMj3a1en0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEZkeu%2FbtsQzGJ94Qr%2FlUsC7LKQjk9WwmMj3a1en0%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;727&quot; height=&quot;366&quot; data-filename=&quot;스크린샷 2025-09-15 오후 9.27.38.png&quot; data-origin-width=&quot;1926&quot; data-origin-height=&quot;970&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;false를 지정하면 몽고DB가 성능을 개선하려고 삽입을 재배열할 수 있다. 정렬되지 않은 삽입(unordered insert)의 경우 몽고DB는 일부 삽입이 오류를 발생시키는지 여부에 관계없이 모든 도큐먼트 삽입을 시도한다. 위 예제처럼 정렬되지 않은 삽입을 지정하면 배열의 첫 번째, 두 번째, 네 번째 도큐먼트가 삽입된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;삭제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에 있는 데이터를 삭제하기 위해서는 deleteOne과 deleteMany를 제공한다. 두 메서드 모두 필터 도큐먼트를 첫 번째 매개변수로 사용한다. 예를 들어, 방금 생성한 영화 도큐먼트를 삭제하려고 할 때 필터와 일치하는 모든 도큐먼트를 삭제하려면 deleteMany를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 컬렉션을 삭제하려면 db.movies.drop()처럼 drop을 사용하는 편이 더 빠르고 빈 컬렉션에 인덱스를 재생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w7OlQ/btsQghpxlBQ/ArH0lVvtXkqzDkLSjF3M40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w7OlQ/btsQghpxlBQ/ArH0lVvtXkqzDkLSjF3M40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w7OlQ/btsQghpxlBQ/ArH0lVvtXkqzDkLSjF3M40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw7OlQ%2FbtsQghpxlBQ%2FArH0lVvtXkqzDkLSjF3M40%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;679&quot; height=&quot;150&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터는 도큐먼트를 제거할 때 비교할 일련의 기준을 지정한다. deleteOne을 사용하면서 컬렉션 내 여러 도큐먼트와 일치하는 필터를 지정하는 경우에는 필터와 일치하는 첫 번째 도큐먼트를 삭제한다. 어떤 도큐먼트가 먼저 발견되는지는 도큐먼트가 삽입된 순서, 도큐먼트에 어떤 갱신이 이뤄졌는지(일부 스토리지 엔진의 경우), 어떤 인덱스를 지정하는지 등 몇 가지 요인에 따라 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;갱신&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도큐먼트를 데이터베이스에 저장한 후에는 &lt;b&gt;updateOne, updateMany, replaceOne&lt;/b&gt;과 같은 갱신 메서드를 사용해 변경할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;갱신은 원자적으로 이뤄진다.&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;갱신 요청 두 개가 동시에 발생하면 서버에 먼저 도착한 요청이 적용된 후 다음 요청이 적용되어 도큐먼트는 변질 없이 안전하게 처리된다. 기본 동작을 원치 않으면 도큐먼트 버저닝 패턴(The Document Versioning Pattern)을 고려할 수 있다.&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-origin-width=&quot;1524&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rCSLW/btsQfg5KJGo/D9RaW7RkYOciy9FAvGbEF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rCSLW/btsQfg5KJGo/D9RaW7RkYOciy9FAvGbEF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rCSLW/btsQfg5KJGo/D9RaW7RkYOciy9FAvGbEF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrCSLW%2FbtsQfg5KJGo%2FD9RaW7RkYOciy9FAvGbEF0%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;622&quot; height=&quot;318&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;updateOne과 updateMany는 필터 도큐먼트를 첫 번째 매개변수로, 변경 사항을 설명하는 수정자 도큐먼트를(modifier document)를 두 번째 매개변수로 사용한다. replaceOne도 첫 번째 매개변수로 필터를 사용하지만 두 번째 매개변수는 필터와 일치하는 도큐먼트를 교체할 도큐먼트이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 예제는 도큐먼트에 새 키 값으로 리뷰 배열을 추가한다. 갱신하려면 갱신 연산자(update operator)인 &lt;b&gt;$set&lt;/b&gt;을 이용한다. find를 호출해 도큐먼트에서 reviews 키가 생긴 것을 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;도큐먼트 치환&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-15 오후 10.08.29.png&quot; data-origin-width=&quot;2712&quot; data-origin-height=&quot;1438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pofzP/btsQydINcmg/tKltt0XtWLSbh3GpG6fatK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pofzP/btsQydINcmg/tKltt0XtWLSbh3GpG6fatK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pofzP/btsQydINcmg/tKltt0XtWLSbh3GpG6fatK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpofzP%2FbtsQydINcmg%2FtKltt0XtWLSbh3GpG6fatK%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;705&quot; height=&quot;374&quot; data-filename=&quot;스크린샷 2025-09-15 오후 10.08.29.png&quot; data-origin-width=&quot;2712&quot; data-origin-height=&quot;1438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;replaceOne은 도큐먼트를 새로운 것으로 완전히 치환한다. 첫 번째 매개변수로 필터를 사용하지만 두 번째 매개변수는 필터와 일치하는 도큐먼트를 교체할 도큐먼이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이는 대대적인 스키마 마이그레이션(schema migration)에 유용하다. &quot;friends&quot;와 &quot;enemis&quot; 필드를 relationships라는 서브도큐먼트(subdocument)로 옮겨보자. 셸에서 도큐먼트의 구조를 수정한 후 replaceOne을 사용해 데이터베이스의 버전을 교체할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;조건절로 2개 이상의 도큐먼트가 일치하는 경우 중복된 &quot;_id&quot; 값을 갖는 도큐먼트를 생성할 수도 있다. 데이터베이스는 오류를 반환하고 아무것도 변경하지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;갱신 연산자&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;도큐먼트의 부분 갱신에는 원자적&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;갱신 연산자(update operator)&lt;/b&gt;를 사용한다. 갱신 연산자는 키를 변경, 추가, 제거하고, 심지어 배열과 내장 도큐먼트를 조작하는 복잡한 갱신 연산을 지정하는 데 사용하는 특수키다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&quot;$set&quot; 제한자 사용하기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 예제에서 확인했던 것처럼 &lt;b&gt;&quot;$set&quot;은 필드 값을 설정한다. 필드가 존재하지 않으면 새 필드가 생성된다.&lt;/b&gt; 이 기능은 스키마를 갱신하거나 사용자 정의 키(user-defined key)를 추가할 때 편리하다. &quot;$set&quot;은 키의 데이터형도 변경할 수 있고, 반대로 &quot;$unset&quot;으로 키와 값을 모두 제거할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-16 오전 8.14.56.png&quot; data-origin-width=&quot;2590&quot; data-origin-height=&quot;1640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s3AR2/btsQzMRdyjk/SmEv6O3cw7mcyd9pE38fy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s3AR2/btsQzMRdyjk/SmEv6O3cw7mcyd9pE38fy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s3AR2/btsQzMRdyjk/SmEv6O3cw7mcyd9pE38fy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs3AR2%2FbtsQzMRdyjk%2FSmEv6O3cw7mcyd9pE38fy0%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;784&quot; height=&quot;496&quot; data-filename=&quot;스크린샷 2025-09-16 오전 8.14.56.png&quot; data-origin-width=&quot;2590&quot; data-origin-height=&quot;1640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;도큐먼트가 생성될 때 필드가 설정되어야 할 때가 종종 있는데, 이후 갱신에서는 변경되지 않아야 한다. 이때 &lt;b&gt;$setOnInsert&lt;/b&gt;를 사용한다. $setOnInsert는 도큐먼트가 삽입될 때 필드값을 설정하는 데만 사용하는 연산자다. 위 명령어처럼 다시 갱신하면 기존 도큐먼트를 찾고, 아무것도 입력되지 않으며, createdAt 필드는 변경되지 않는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;증가와 감소&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-15 오후 10.12.02.png&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PmhlA/btsQxMq9LjX/QgGQtOJkb5TpjRdBkbitEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PmhlA/btsQxMq9LjX/QgGQtOJkb5TpjRdBkbitEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PmhlA/btsQxMq9LjX/QgGQtOJkb5TpjRdBkbitEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPmhlA%2FbtsQxMq9LjX%2FQgGQtOJkb5TpjRdBkbitEK%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;800&quot; height=&quot;385&quot; data-filename=&quot;스크린샷 2025-09-15 오후 10.12.02.png&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;$inc&quot; 연산자는 이미 존재하는 키의 값을 변경하거나 새 키를 생성하는 데 사용한다.&lt;/b&gt; 분석, 분위기, 투표 등과 같이 자주 변하는 수치 값을 갱신하는 데 매우 유용하다. &quot;$inc&quot;는 int, long, double, decimal 타입 값에만 사용할 수 있다. 숫자가 아닌 값으로 증감을 시도하면 'Modifier &quot;$inc&quot; allowed for numbers only'라는 오류 메시지가 뜬다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;누군가가 페이지를 방문할 때마다 카운터가 증가한다고 가정해보자. 갱신 연산자로 카운터를 원자적으로 증가시킨다. 페이지마다 URL은 &quot;url&quot;키로, 조회수는 &quot;pageviews&quot; 키로 도큐먼트에 저장되어 있다. 누군가가 페이지를 방문할 때마다 URL로 페이지를 찾고 &quot;pageviews&quot; 키의 값을 증가시키려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;$inc&quot; 제한자(modifier)&lt;/b&gt;를 사용한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;배열 연산자&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;배열을 다루는 데 갱신 연산자를 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-09-15 오후 10.24.45.png&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSybok/btsQBxZMNkD/pW21JE9AIb7EbqhIedfVP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSybok/btsQBxZMNkD/pW21JE9AIb7EbqhIedfVP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSybok/btsQBxZMNkD/pW21JE9AIb7EbqhIedfVP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSybok%2FbtsQBxZMNkD%2FpW21JE9AIb7EbqhIedfVP1%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;808&quot; height=&quot;408&quot; data-filename=&quot;스크린샷 2025-09-15 오후 10.24.45.png&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&quot;$push&quot;는 배열이 이미 존재하면 배열 끝에 요소를 추가하고, 존재하지 않으면 새로운 배열을 생성한다. 위 명령어처럼 배열 형태의 &quot;reviews&quot; 키에 삽입하면 리뷰가 추가된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&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-09-15 오후 10.26.34.png&quot; data-origin-width=&quot;2362&quot; data-origin-height=&quot;1206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q5TEY/btsQxE7Je4G/6Gqgml4sHYtGM1SFwQXvW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q5TEY/btsQxE7Je4G/6Gqgml4sHYtGM1SFwQXvW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q5TEY/btsQxE7Je4G/6Gqgml4sHYtGM1SFwQXvW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq5TEY%2FbtsQxE7Je4G%2F6Gqgml4sHYtGM1SFwQXvW0%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;803&quot; height=&quot;410&quot; data-filename=&quot;스크린샷 2025-09-15 오후 10.26.34.png&quot; data-origin-width=&quot;2362&quot; data-origin-height=&quot;1206&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&quot;$push&quot;에 &quot;$each&quot; 제한자를 사용하면 작업 한 번으로 값을 여러 개 추가할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757941030150&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db.movies.updateOne(&quot;genre&quot;: &quot;horror&quot;,
	{&quot;$push&quot;: {&quot;top10&quot;: {&quot;$each&quot;: [{&quot;name&quot;: &quot;Nigtmare on Elm Street&quot;,
    								&quot;rating&quot;: 6.6],
                                    {&quot;name&quot;: &quot;Saw&quot;, &quot;rating&quot;: 4.3}],
                         &quot;$slice&quot;: 10,
                         &quot;$sort&quot;: {&quot;rating&quot;: -1}}}})&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&quot;$slice&quot;를 &quot;$push&quot;와 결합해 사용하여 배열이 특정 크기 이상으로 늘어나지 않게 효과적으로 'top N' 목록을 만들 수 있다. 위 예제처럼 rating 필드로 배열의 모든 요소를 정렬한 후 처음 10개 요소를 유지하기 위해서 &quot;$sort&quot;도 같이 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&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-09-16 오전 7.42.23.png&quot; data-origin-width=&quot;2034&quot; data-origin-height=&quot;1412&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LRJqF/btsQARdA6LS/AZWWWvKHEci44K9atWkXK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LRJqF/btsQARdA6LS/AZWWWvKHEci44K9atWkXK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LRJqF/btsQARdA6LS/AZWWWvKHEci44K9atWkXK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLRJqF%2FbtsQARdA6LS%2FAZWWWvKHEci44K9atWkXK1%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;684&quot; height=&quot;475&quot; data-filename=&quot;스크린샷 2025-09-16 오전 7.42.23.png&quot; data-origin-width=&quot;2034&quot; data-origin-height=&quot;1412&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;특정 값이 배열에 존재하지 않을 때 해당 값을 추가하면서, 배열을 집합처럼 처리하려면 $addToSet을 사용할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;요소 제거하기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;배열을 큐나 스택처럼 사용하려면 배열의 양 쪽 끝에서 요소를 제거하는 $pop을 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;{ $pop: { key: 1 } }:&lt;/b&gt; 배열의 마지막부터 요소를 제거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;{ $pop: { key: -1 } }:&amp;nbsp;&lt;/b&gt;배열의 처음부터 요소를 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주어진 조건에 맞는 배열 요소를 모두 제거할 때는 $pull을 사용한다. 예를 들어, [1, 1, 2, 1]과 같은 배열에서 1을 뽑아내면 배열에는 [2] 하나만 남는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;갱신 입력(upsert)&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1757977722771&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db.analytics.updateOne({ url: &quot;/blog&quot; }, { $inc: { pageviews: 1 } }, { upsert: true })&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;갱신 조건에 맞는 도큐먼트가 존재하지 않을 때는 쿼리 도큐먼트와 갱신 도큐먼트를 합쳐서 새로운 도큐먼트를 생성한다. 조건에 맞는 도큐먼트가 발견되면 일반적인 갱신을 수행한다. &lt;b&gt;갱신 입력을 사용하면 코드를 줄이고 원자적이므로 경쟁 상태를 피할 수 있다.&lt;/b&gt; updateOne과 updateMany의 세 번째 매개변수는 옵션 도큐먼트로, 갱신 입력을 지정할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;갱신한 도큐먼트 반환&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2632&quot; data-origin-height=&quot;1340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkVbau/btsQAgSsYLw/UTwAJ1vCFk2TwrrDjicKV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkVbau/btsQAgSsYLw/UTwAJ1vCFk2TwrrDjicKV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkVbau/btsQAgSsYLw/UTwAJ1vCFk2TwrrDjicKV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkVbau%2FbtsQAgSsYLw%2FUTwAJ1vCFk2TwrrDjicKV0%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;728&quot; height=&quot;371&quot; data-origin-width=&quot;2632&quot; data-origin-height=&quot;1340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;findOneAndDelete, findOneAndReplace, findOneAndUpdate는 updateOne과 달리 사용자가 &lt;b&gt;수정된 도큐먼트의 값을 원자적으로 얻을 수 있다는 점&lt;/b&gt;이다. 즉, 경쟁 상태를 만들지 않는다. 기본적으로 도큐먼트의 상태를 수정하기 전에 반환하는데 옵션 도큐먼트의 returnNewDocument 필드를 true로 설정하면 갱신된 도큐먼트를 반환한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;쿼리&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;읽기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757981386946&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db.users.find({}, { username: 1, email: 1 })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;find와 findOne 함수는 컬렉션을 쿼리하는데 사용한다. 쿼리 도큐먼트에 여러 키/값 쌍을 추가해 검색을 제한할 수 있다. 그리고 조회할 도큐먼트 내 키/값 정보가 모두 필요하지 않는 경우에는 두 번째 매개변수에 원하는 키를 지정하면 된다. 이는 네트워크상의 데이터 전송량과 클라이언트 측에서 도큐먼트를 &lt;b&gt;디코딩(decoding)&lt;/b&gt;하는 데 드는 시간과 메모리를 줄여준다. username과 email 키 값만 원할 때는 위처럼 쿼리한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;쿼리 조건&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿼리는 완전 일치(exact match) 외에도 범위, OR절, 부정 조건(negation) 등 더 복잡한 조건으로 검색할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;쿼리 조건절&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1758025988420&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 18세에서 30세 사이의 사용자 조회
db.users.find({ age: { $gte: 18, $lte: 30 } })

// 2007년 1월 1일 이전에 등록한 사람 조회
start = new Date(&quot;01/01/2007&quot;)
db.users.find({ registered: { $lt: start } })

// oneny가 아닌 사용자 조회
db.users.find({ username: { $ne: &quot;oneny&quot; } })&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;, &amp;lt;=, &amp;gt;, &amp;gt;=에 해당하는 비교 연산자는 각각 &lt;b&gt;&quot;$lt&quot;, &quot;$lte&quot;, &quot;$gt&quot;, &quot;$gte&quot;&lt;/b&gt;다. 키 값이 특정 값과 일치하지 않는 도큐먼트를 찾는 데는 &quot;not equal&quot;을 나타내는 &quot;$ne&quot;를 사용한다.&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;OR 쿼리&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1758026320911&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// 725, 542, 390 번호인 당첨자 조회
db.raffle.find({ ticket_no: { $in: [725, 542, 390] } })

// 당첨 번호를 가지지 않는 사람 조회
db.raffle.find({ ticket_no: { $nin: [725, 542, 390] } })

// 사용자 id에 번호 대신 이름을 쓰도록 점진적으로 이전하고 있다면, 두 조건 중 하나라도 맞는 도큐먼트 조회
db.users.find({ user_id: { $in: [12345, &quot;oneny&quot;] } })

// ticket_no가 세 번호 중 적어도 하나와 일치하거나 winner가 true인 경우 조회
db.raffle.find({ $or: [{ ticket_no: { $nin: [725, 542, 390] } }, { winner: true }] })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB에서 OR 쿼리에는 두 가지 방법이 있다. &lt;b&gt;&quot;$in&quot;&lt;/b&gt;은 하나의 키를 다양한 값과 비교하는 쿼리에 사용한다. &quot;&lt;b&gt;$or&quot;&lt;/b&gt;은 더 일반적이며, 여러 키를 주어진 값과 비교하는 쿼리에 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;$in&lt;/b&gt;은 매우 유연해 여러 개의 값을 쓸 수 있을 뿐 아니라 서로 다른 데이터형도 쓸 수 있다. 쿼리 옵티마이저는 &lt;b&gt;$in&lt;/b&gt;을 더 효율적으로 다루기 때문에 &lt;b&gt;$in&lt;/b&gt;을 사용하는 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;$not&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1758026697156&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// $mod: 키의 값을 첫 번째 값으로 나눈 후 그 나머지 값의 조건을 두 번째 값으로 기술하는 연산자
// 1, 6, 11, 16 등인 사용자 반환
db.users.find({ id_num: { $mod: [5, 1] } })

// 반대로 2, 3, 4, 5, 7, 8, 9, 10, 12 등인 사용자를 받으려면 $not 사용
db.users.find({ id_num: { $not: { $mod: [5, 1] } } })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;$not&lt;/b&gt;은 메타 조건절(metaconditional)이며 어떤 조건에도 적용할 수 있다. &lt;b&gt;$not&lt;/b&gt;은 정규 표현식과 함께 사용해 주어진 패턴과 일치하지 않는 도큐먼트를 찾을 때 특히 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;형 특정 쿼리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB에서는 도큐먼트 내에서 다양한 데이터형을 사용할 수 있다. 일부 데이터형은 쿼리 시 형에 특정하게(type-specific) 작동하다.&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;null&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1758026890775&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db.c.find({ z: { $eq: null, $exists: true } })&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;null은 '존재하지 않음'과도 일치한다. 따라서 키가 null인 값을 쿼리하면 해당 키를 갖지 않는 도큐먼트도 반환한다. 값이 null인 키만 찾고 싶으면 키가 null인 값을 쿼리하고, &lt;b&gt;$exists&lt;/b&gt; 조건절을 사용해 null 존재 여부를 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;배열에 쿼리&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1758027267677&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// apple과 banana 요소를 가진 도큐먼트 쿼리
db.food.find({ fruits: { $all: [&quot;apple&quot;, &quot;banana&quot;] } })

// 전체 배열과 정확하게 일치하는 도큐먼트 쿼리
db.food.find({ fruits: [&quot;apple&quot;, &quot;banana&quot;, &quot;peach&quot;] })

// 배열 내 특정 요소 쿼리(key.index 구문)
db.food.find({ fruit.2: &quot;peach&quot; })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개 이상의 배열 요소가 일치하는 배열을 찾으려면 &lt;b&gt;$all&lt;/b&gt;을 사용한다. 이는 배열 내 여러 요소와 일치하는지 확인하게 해주고, 순서는 중요하지 않다. 정확히 배열 요소가 일치하는지 확인하려면 두 번째처럼 쿼리하면 되고, 순서가 다르면 조회되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758028239184&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;db.food.find({ fruits: { $size: 3 } })

// 크기 범위 쿼리용으로 도큐먼트에 size 키 추가
db.food.update(criteria, { $push: { fruit: &quot;strawberry&quot; }, $inc: { size: 1 } })

db.food.find({ size: { $gt: 3 } })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;$size&lt;/b&gt;는 특정 크기의 배열을 쿼리하는 유용한 조건절이다. 하지만 $gt 같은 다른 조건절과 결합할 수 없으므로 위처럼 size 키를 추가해 크기의 범위를 쿼리할 수 있다. 하지만 $addToSet 연산자와는 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758028612456&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 블로그 게시물에서 먼저 달린 댓글 10개 조회
db.blog.posts.findOne(criteria, { comments: { $slice: 10 } })

// 반대로 나중에 달린 댓글 조회
db.blog.posts.findOne(criteria, { comments: { $slice: -10 } })

// 처음 23개를 건너뛰고, 24번째 요소부터 33번째 요소까지 조회
db.blog.posts.findOne(criteria, { comments: { $slice: [23, 10] } })

// 각 도큐먼트에서 Bob이 쓴 댓글 중 첫 번째로 일치하는 댓글 조회
db.blog.posts.find({ comments.name: &quot;bob&quot; }, { comments.$ : 1 })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;$slice&lt;/b&gt;는 배열 요소의 부분집합을 반환받을 수 있다. 또한, 오프셋(offset)광 요소 개수를 지정해 원하는 범위 안에 있는 결과를 반환할 수 있다. 마지막&amp;nbsp;&lt;b&gt;$&lt;/b&gt; 연산자를 사용하면 특정 기준과 일치하는 요소를 반환받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758029177237&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{ x: 5 }
{ x: 15 }
{ x: 25 }
{ x: [5, 25] }

// 두 도큐먼트 조회
&amp;gt; db.test.find({ x: { $gt: 10, $lt: 20 } })
{ x: 15 }
{ x: [5, 25] }

// 배열 비교, 비배열 요소는 일치시키지 않음
&amp;gt; db.test.find({ x: { $elemMatch: { $gt: 10, $lt: 20 } } })
&amp;gt; // 결과 없음

// { x: 15 }만 조회
db.test.find({ x: { $gt: 10, $lt: 20 } }).min({ x: 10 }).max({ x: 20 })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 쿼리는 조회하면 두 개의 도큐먼트가 조회된다. 5와 25 둘 다 10과 20 사이는 아니지만, 25는 첫 번째 절과 일치하고, 5는 두 번째 절과 일치하기 때문에 반환되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;$elemMatch&lt;/b&gt; 연산자를 사용하면 몽고DB는 두 절을 하나의 배열 요소와 비교한다. 하지만 비배열 요소를 일치시키지 않는다는 함정이 있다. 따라서 $elemMatch는 배열 요소에 대한 범위 쿼리에만 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리하는 필드에 인덱스가 있다면 &lt;b&gt;min 함수&lt;/b&gt;와 &lt;b&gt;max 함수&lt;/b&gt;를 사용해 $gt와 $lt 값 사이로 인덱스 범위를 제한해 쿼리할 수 있다. 그러면 쿼리는 5와 25는 누락시키고 10과 20 사이의 인덱스만 통과시켜 {&amp;nbsp;x:&amp;nbsp;15&amp;nbsp;}만 조회된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;커서(cursor)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1758096822263&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 100개 데이터 입력
for(x=0; x&amp;lt;100;x++) { db.col3.insertOne({&quot;a&quot;:x}) }

// 기본 커서 생성
const cursor = db.col3.find()

// 커서 탐색 방법들
cursor.forEach(doc =&amp;gt; console.log(doc));
cursor.toArray(); // 모든 결과를 배열로 변환
cursor.hasNext(); // 다음 문서 존재 여부 확인
cursor.next(); // 다음 문서 반환
cursor.limit(10); // 결과 개수 제한
cursor.skip(20); // 처음 20개 건너뛰기
cursor.sort({createdAt: -1}); // 정렬&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 &lt;b&gt;커서&lt;/b&gt;를 사용해 find의 결과를 반환한다. 일반적으로 &lt;b&gt;클라이언트 측의 커서 구현체는 쿼리의 최종 결과를 강력히 제어&lt;/b&gt;하게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB 커서는 쿼리 결과를 효율적으로 탐색하기 위한 포인터 객체로, 대량의 데이터를 메모리에 한 번에 로드하지 않고 순차적으로 접근할 수 있게 해준다. 커서는 배치(batch) 단위로 도큐먼트를 반환하며, 자동으로 서버와 클라이언트 간의 메모리 사용량을 최적화한다. 또한, 결과 개수를 제한하거나, 결과 중 몇 개를 건너뛰거나, 여러 키를 조합한 결과를 어떤 방향으로든 정렬하는 등 다양하게 조작할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 측 커서는 메모리와 리소스를 점유한다. 커서가 더는 가져올 결과가 없거나 클라이언트로부터 종료 요청을 받으면 데이터베이스는 점유하고 있던 리소스를 해제한다. 또는, 사용자가 아직 결과를 다 살펴보지 않았고, 커서가 여전히 유효 영역 내에 있더라도 일정 시간(기본 10분) 후 자동으로 만료되어 서버 리소스를 보호한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;많은 수의 건너뛰기 피하기&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1758098379856&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// date을 내림차순으로 정렬하고 첫 페이지 조회
const page1 = db.collection
        .find({})
        .sort({ date: -1 })
        .limit(100);

// 다음 페이지 가져오기
const lastDate = page1[page1.length - 1]._id;
const page2 = db.collection
        .find({ date: { $lt: lastDate })
        .sort({ date: -1 })
        .limit(100);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도큐먼트 수가 적을 때는 skip을 사용해도 무리가 없지만 skip을 100만건으로 지정하면 100만 건의 도큐먼트를 모두 읽고 버려야 하므로 오프셋이 클수록 성능이 기하급수적으로 저하된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 커서 기반 페이지네이션을 사용하면 100만 개의 도큐먼트 중 마지막 페이지를 조회할 때도 첫 페이지와 동일한 속도로 실행된다. 인덱스를 통해 직접 시작점을 찾아가므로 중간 도큐먼트를 스캔할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-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>DB/MongoDB</category>
      <category>MongoDB</category>
      <category>MongoDB CRUD</category>
      <category>몽고db</category>
      <category>몽고DB CRUD</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/139</guid>
      <comments>https://oneny.tistory.com/139#entry139comment</comments>
      <pubDate>Wed, 17 Sep 2025 17:49:57 +0900</pubDate>
    </item>
    <item>
      <title>Terraform</title>
      <link>https://oneny.tistory.com/137</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Terraform&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-11 오후 6.27.44.png&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;1208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mFrgb/btsPPLEWmhj/9nbhonCIkE8m1iZHm8Ing0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mFrgb/btsPPLEWmhj/9nbhonCIkE8m1iZHm8Ing0/img.png&quot; data-alt=&quot;출처: https://k21academy.com/terraform-iac/terraform-providers-overview/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mFrgb/btsPPLEWmhj/9nbhonCIkE8m1iZHm8Ing0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmFrgb%2FbtsPPLEWmhj%2F9nbhonCIkE8m1iZHm8Ing0%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;526&quot; height=&quot;521&quot; data-filename=&quot;스크린샷 2025-08-11 오후 6.27.44.png&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;1208&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://k21academy.com/terraform-iac/terraform-providers-overview/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼(Terraform)은 &lt;b&gt;하시코프(HashiCorp)&lt;/b&gt;에서 공개한 IaC 도구다. 테라폼을 통해 리소스를 코드로 손쉽게 배포 및 삭제하며, 다수의 리소스를 일괄적으로 관리하는 구조로 특정 클라우드나 인프라에 종속적이지 않고 AWS, Azure, Google Cloud와 같은 클라우드 서비스나 온프레미스 시스템과 연동될 수 있도록 지원하는 프로바이더 기반 아키텍처이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼은 HCL를 사용하여 간결하고 읽기 쉬운 문법을 통해 인프라를 정의할 수 있다. 각 인프라와 서비스는 고유의 API를 가지고 있고 프로바이더는 각 API 명세를 테라폼 코드로 호출해 동작한다. 즉, terraform apply와 같은 명령으로 만들어진 코드를 실행하고 배포할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테라폼 장점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼을 사용하면 다음과 같은 장점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자동화 및 일관성 있는 인프라 관리 환경 구축&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인프라와 서비스 구성 및 변경 작업인 프로비저닝을 위해 워크플로우를 제공하여 코드로 자동화된 환경을 구축할 수 있다.&lt;/li&gt;
&lt;li&gt;여러 환경(개발, 테스트, 프로덕션)에 동일한 인프라를 배포하고, 코드로 모든 구성을 관리하여 높은 일관성을 확보할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경 추적 및 버전 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 형태로 되어 있어 Git 같은 VCS(Version Control System)들을 통해 프로비저닝 작업 일관성을 보장할 수 있다.&lt;/li&gt;
&lt;li&gt;작업 수행 전에 실행 계획을 작성하여 변경 사항을 미리 확인하므로 안전하고 빠른 프로비저닝을 수행한다.&lt;/li&gt;
&lt;li&gt;VPC나 로드 밸런서와 같은 리소스를 모듈로 작성하여 다양한 프로젝트와 환경에서 동일한 환경을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테라폼 워크플로우&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-10 오후 6.49.55.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGOZB6/btsPLHDSPG2/NhDdyToNcNlcBYjLnRxAdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGOZB6/btsPLHDSPG2/NhDdyToNcNlcBYjLnRxAdk/img.png&quot; data-alt=&quot;출처: https://k21academy.com/terraform-iac/terraform-workflow-and-its-use-case/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGOZB6/btsPLHDSPG2/NhDdyToNcNlcBYjLnRxAdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGOZB6%2FbtsPLHDSPG2%2FNhDdyToNcNlcBYjLnRxAdk%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;1400&quot; height=&quot;230&quot; data-filename=&quot;스크린샷 2025-08-10 오후 6.49.55.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://k21academy.com/terraform-iac/terraform-workflow-and-its-use-case/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 워크플로우는 Write -&amp;gt; Init -&amp;gt; Plan -&amp;gt; Apply -&amp;gt; Destroy 순으로 진행되고, 각 단계에 대한 자세한 설명은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;write:&lt;/b&gt;&amp;nbsp;프로비저닝하려는 목적에 따라 인프라를 작성하는 단계로 리소스, 모듈, 변수 등을 정의한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;init:&lt;/b&gt; 프로바이더 플러그인 설치, 모듈 다운로드, .terraform.lock.hcl 생성 등을 수행하여 작업 디렉터리를 초기화한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;plan:&lt;/b&gt; 현재 상태와 코드의 차이를 계산해 실행 계획을 보여준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;apply:&lt;/b&gt;&amp;nbsp;계획을 실행해 인프라를 생성/수정/삭제한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;destroy:&lt;/b&gt; 모든 관리 리소스를 제거한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;프로바이더(Provider)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-20 오후 7.28.45.png&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d30VeO/btsPYBKfbyE/ywZImdX61lLWzGKpOx6zS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d30VeO/btsPYBKfbyE/ywZImdX61lLWzGKpOx6zS0/img.png&quot; data-alt=&quot;출처: https://jayendrapatil.com/terraform-cheat-sheet/#google_vignette&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d30VeO/btsPYBKfbyE/ywZImdX61lLWzGKpOx6zS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd30VeO%2FbtsPYBKfbyE%2FywZImdX61lLWzGKpOx6zS0%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;567&quot; height=&quot;276&quot; data-filename=&quot;스크린샷 2025-08-20 오후 7.28.45.png&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://jayendrapatil.com/terraform-cheat-sheet/#google_vignette&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼은 terraform 바이너리 파일을 시작으로 원격 환경에서 원하는 대상을 호출하는 방식으로 실행된다. '원하는 대상'은 호출하는 방식이 서로 다르지만 대상의 공급자, 즉 프로바이더가 제공하는 API를 호출해 상호작용을 한다. 여기서 테라폼이 대상과의 상호작용을 할 수 있도록 하는 것이 '프로바이더'다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로바이더 구성에 대한 요구사항은 위 사진처럼 공식 레지스트리 사이트인 테라폼 레지스트리에 공개되어 있는 각 프로바이더의 구성 방식을 참고하는 것이 올바른 방법이다. AWS와 같은 클라우드 프로바이더의 경우는 AWS 자격증명 정보와 리전 이름 등을 정의하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;프로바이더 종류&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에는 다음과 같은 다양한 프로바이더들을 제공하여 각각의 플랫폼에서 리소르를 관리할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;aws: Amazon Web Service(AWS)의 리소스(예: EC2, S3 등)를 관리하기 위한 Terrafor의 주요 프로바이더&lt;/li&gt;
&lt;li&gt;azurerm: Microsoft Azure 리소스를 관리하는 프로바이더로, VM, 네트워크, 데이터베이스 등의 Azure 서비스에 접근 가능&lt;/li&gt;
&lt;li&gt;google: Google Cloud Platform(GCP) 리소스를 관리하며, Compute Engine, GKE 등 GCP 서비스와 통합됨&lt;/li&gt;
&lt;li&gt;kubernetes: Kubernetes 클러스터 및 리소스(Pod, Service, Deployment 등)를 관리하는 프로바이더&lt;/li&gt;
&lt;li&gt;helm: Helm 패키지 관리자를 통해 Kubernetes 애플리케이션을 설치하고 관리하는 Terraform 프로바이더&lt;/li&gt;
&lt;li&gt;kubectl: kubectl 명령을 사용하여 Kubernetes 리소스를 직접 조작하거나 관리할 수 있도록 지원하는 프로바이더&lt;/li&gt;
&lt;li&gt;docker: Docker 컨테이너와 네트워크를 관리하며, 로컬 또는 원격 Docker 엔진과 통합할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;HCL(HashiCorp Configuration Language)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HCL은 하시코프에서 IaC와 구성 정보를 명시하기 위해 개발된 오픈소스 도구로 간결하고 직관적인 문법을 제공한다. 그리고 다양한 클라우드 프로바이더와 서비스에서 사용 가능한 리소스들을 간편하게 정의하고, 각 리소스 간의 의존 관계를 자동으로 관리해주기 때문에 복잡한 인프라도 쉽게 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼은 인프라를 구성하기 위한 다음과 같은 선언 블록이 존재한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;terraform 블록&lt;/li&gt;
&lt;li&gt;resource 블록&lt;/li&gt;
&lt;li&gt;data 블록&lt;/li&gt;
&lt;li&gt;variable 블록&lt;/li&gt;
&lt;li&gt;local 블록&lt;/li&gt;
&lt;li&gt;output 블록&lt;/li&gt;
&lt;/ul&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;terraform 블록&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1754905816537&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;terraform {
  required_version = &quot;~&amp;gt; 1.12.2&quot; # 테라폼 버전
  
  required_providers {
    # 각 프로바이더의 이름에 소스 경로와 버전을 명시
    aws = {
      source = &quot;hashicorp/aws&quot;
      version = &quot;~&amp;gt; 6.7.0&quot; # 지정한 버전에서 가장 자릿수가 낮은 구성 요소만 증가하는 것 허용
    }
  }
  
  backend &quot;local&quot; { # state를 보관하는 위치를 지정
    path = &quot;relative/path/to/terraform.tfstate&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼 버전이나 프로바이더 버전과 같은 값들은 자동으로 설정되지만 다른 사람과 함께 작업할 때는 버전을 명시적으로 선언하고 필요한 조건을 입력하여 실행 오류를 최소화하는 것이 좋다. 테라폼 내에서 버전이 명시되는 terraform, module에서 사용 가능하며 버전에 대한 제약을 둠으로써 테라폼, 프로바이더, 모듈이 항상 의도한 정의대로 실행되는 것을 목적으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;terraform 블록의 required_providers 블록 내에 &amp;lt;로컬 이름&amp;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;resource 블록&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1754907762025&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resource &quot;&amp;lt;리소스 유형&amp;gt;&quot; &quot;&amp;lt;이름&amp;gt;&quot; {
  &amp;lt;인수&amp;gt; = &amp;lt;값&amp;gt;
}

resource &quot;aws_instance&quot; &quot;web&quot; {
  ami           = &quot;ami-0897f20d7e803af8f&quot;
  instance_type = &quot;t3.micro&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-11 오후 7.25.53.png&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CdC47/btsPMPoM1cH/AEwav4KkCxfrJqFSINqgYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CdC47/btsPMPoM1cH/AEwav4KkCxfrJqFSINqgYk/img.png&quot; data-alt=&quot;aws 프로바이더도 같이 설치 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CdC47/btsPMPoM1cH/AEwav4KkCxfrJqFSINqgYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCdC47%2FbtsPMPoM1cH%2FAEwav4KkCxfrJqFSINqgYk%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;1606&quot; height=&quot;208&quot; data-filename=&quot;스크린샷 2025-08-11 오후 7.25.53.png&quot; data-origin-width=&quot;1606&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;aws 프로바이더도 같이 설치 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스(resource)는 테라폼이 프로비저닝 도구라는 측면에서 가장 중요한 요소다. 리소스 블록은 선언된 항목을 생성하는 동작을 수행한다. 리소스에서 사용되는 유형들은 프로바이더에 종속성을 갖는다. aws_instance 유형처럼 특정 프로바이더의 유형만 추가해도 terraform init을 수행하면 해당 프로바이더를 설치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 블록을 정의할 때 사용 가능한 메타인수를 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;depends_on:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;종속성을 선언하며, 선언된 구성 요소와의 생성 시점에 대해 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;count:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;선언된 개수에 따라 여러 데이터 소스를 선언&lt;/li&gt;
&lt;li&gt;&lt;b&gt;for_each:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;map 또는 set 타입의 데이터 배열의 값을 기준으로 여러 리소스를 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;provider:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;동일한 프로바이더가 다수 정의되어 있는 경우 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;lifecycle:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;데이터 소스의 수명주기 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;provisioner:&lt;/b&gt; 리소스 생성 후 추가 작업 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;timeouts:&lt;/b&gt; 프로바이더에서 정의한 일부 리소스 유형에서는 create, update, delete에 대한 허용 시간을 정의 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754908113618&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resource &quot;local_file&quot; &quot;abc&quot; {
  content  = &quot;123!&quot;
  filename = &quot;${path.module}/abc.txt&quot;
}

resource &quot;local_file&quot; &quot;edf&quot; {
  content  = local_file.abc.content # &amp;lt;-- local_file.abc의 속성 값을 대신 넣어줌
  filename = &quot;${path.module}/abc.txt&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-11 오후 7.31.20.png&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dajweX/btsPMRGwIq4/Vbos2BuIA0wPLNiNuyKmw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dajweX/btsPMRGwIq4/Vbos2BuIA0wPLNiNuyKmw1/img.png&quot; data-alt=&quot;프로비저닝 순서 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dajweX/btsPMRGwIq4/Vbos2BuIA0wPLNiNuyKmw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdajweX%2FbtsPMRGwIq4%2FVbos2BuIA0wPLNiNuyKmw1%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;1348&quot; height=&quot;180&quot; data-filename=&quot;스크린샷 2025-08-11 오후 7.31.20.png&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로비저닝 순서 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼의 종속성은 resource, module 선언으로 프로비저닝되는 각 요소의 생성 순서를 구분짓는다. 위 코드처럼 다른 리소스에서 값을 참조해 불러올 경우 자동으로 연관 관계가 정의되는 암시적 종속성을 갖게 되고, 강제로 리소스 간 명시적 종속성을 부여할 경우에는 메타인수인 depends_on을 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;리소스 속성 참조&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754909501392&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resource &quot;kubernetes_namespace&quot; &quot;example&quot; {
  metadata {
    annotations = {
      name = &quot;example-annotation&quot;
    }
    name = &quot;terraform-example-namespace&quot;
  }
}

resource &quot;kubernetes_secret&quot; &quot;example&quot; {
  metadata {
    namespace = kubernetes_namespace.example.metadata.0.name # namespace 리소스 인수 참조
    name      = &quot;terraform-example&quot;
  }
  data = {
    password = &quot;P4ssw0rd&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 구성에서 참조 가능한 값은 인수와 속성으로 &amp;lt;리소스 유형&amp;gt;.&amp;lt;이름&amp;gt;.&amp;lt;인수&amp;gt; 또는 &amp;lt;리소스 유형&amp;gt;.&amp;lt;이름&amp;gt;.&amp;lt;속성&amp;gt;으로 참조 가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인수:&lt;/b&gt; 리소스 생성 시 사용자가 선언하는 값&lt;/li&gt;
&lt;li&gt;&lt;b&gt;속성:&lt;/b&gt; 사용자가 설정하는 것은 불가능하지만 리소스 생성 이후 획득 가능한 리소스 고유 값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드처럼 쿠버네티스 프로바이더의 Namespace 리소스를 생성하고 그 이후 Secret을 해당 Namespace에 생성하는 종속성을 리소스 인수 값으로 생성하면 Namesapce의 이름이 변경되어도 해당 Namespace를 참조하는 모든 리소스도 같이 업데이트할 수 있다.&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;data 블록&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1754960249242&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Declare the data source
data &quot;aws_availability_zones&quot; &quot;available&quot; {
  state = &quot;available&quot;
}

resource &quot;aws_subnet&quot; &quot;primary&quot; {
  availability_zone = data.aws_availability_zones.available.names[0]
  # e.g. ap-northeast-2a
}

resource &quot;aws_subnet&quot; &quot;secondary&quot; {
  availability_zone = data.aws_availability_zones.available.names[1]
  # e.g. ap-northeast-2b
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 소스는 테라폼으로 정의되지 않은 외부 리소스 또는 저장된 정보를 테라폼 내에서 참조할 때 사용한다. 그리고 데이터 소스로 읽은 대상을 참조하는 방식은 리소스와 구별되게 data가 앞에 붙는다. &lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Data Source : aws_availability_zones 블로그의 예시 코드&lt;/a&gt;처럼 AWS 프로바이더의 가용영역을 작업자가 수동으로 입력하지 않고 프로바이더로 접근한 환경에서 제공되는 데이터 소스를 활용해 subnet의 가용영역 인수를 정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 소스를 정의할 때 사용 가능한 메타인수는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;depends_on:&lt;/b&gt; 종속성을 선언하며, 선언된 구성 요소와의 생성 시점에 대해 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;count:&lt;/b&gt; 선언된 개수에 따라 여러 데이터 소스를 선언&lt;/li&gt;
&lt;li&gt;&lt;b&gt;for_each:&lt;/b&gt; map 또는 set 타입의 데이터 배열의 값을 기준으로 여러 리소스를 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;provider:&lt;/b&gt; 동일한 프로바이더가 다수 정의되어 있는 경우 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;lifecycle:&lt;/b&gt; 데이터 소스의 수명주기 관리&lt;/li&gt;
&lt;/ul&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;variable 블록&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1754962054219&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;variable &quot;my_password&quot; {}

resource &quot;local_file&quot; &quot;abc&quot; {
  content = var.my_password
  filename = &quot;${path.module}/abc.txt&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 변수(variable)는 인프라를 구성하는 데 필요한 속성 값을 정의해 코드의 변경 없이 여러 인프라를 생성하는 데 목적이 있다. 테라폼에서는 이것을 입력 변수(Input Variables)로 정의하고, variable로 시작되는 블록으로 구성된다. 변수 참조가 필요한 경우에는 var.&amp;lt;이름&amp;gt;으로 참조할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수 정의 시 사용 가능한 메타인수는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;default:&lt;/b&gt; 변수에 할당되는 기본값 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;type:&lt;/b&gt; 변수에 허용되는 값 유형 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;description:&lt;/b&gt; 입력 변수의 설명&lt;/li&gt;
&lt;li&gt;&lt;b&gt;vaildation:&lt;/b&gt; 변수 선언의 제약조건을 추가해 유효성 검사 규칙을 정의
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;condition에 지정된 규칙이 true 또는 false를 반환해야 하며, error_message로 false인 경우 출력되는 메시지 정의 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sensitive:&lt;/b&gt; 민감한 변수 값임을 알리고 테라폼의 출력문에서 값 노출을 제한&lt;/li&gt;
&lt;li&gt;&lt;b&gt;nullable:&lt;/b&gt; 변수에 값이 없어도 됨을 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;변수 유형&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원되는 변수의 범주와 형태는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 유형
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;string&lt;/b&gt;: 글자 유형&lt;/li&gt;
&lt;li&gt;&lt;b&gt;number&lt;/b&gt;: 숫자 유형&lt;/li&gt;
&lt;li&gt;&lt;b&gt;bool&lt;/b&gt;: true 또는 false&lt;/li&gt;
&lt;li&gt;&lt;b&gt;any&lt;/b&gt;: 명시적으로 모든 유형이 허용됨을 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;집합 유형
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;list&lt;/b&gt;(&amp;lt;유형&amp;gt;): 인덱스 기반 집합&lt;/li&gt;
&lt;li&gt;&lt;b&gt;map&lt;/b&gt;(&amp;lt;유형&amp;gt;): 값 = 속성 기반 집합이며 키값 기준 정렬&lt;/li&gt;
&lt;li&gt;&lt;b&gt;set&lt;/b&gt;(&amp;lt;유형&amp;gt;): 값 기반 집합이며 정렬 키값 기준 정렬&lt;/li&gt;
&lt;li&gt;&lt;b&gt;object&lt;/b&gt;({&amp;lt;인수 이름&amp;gt; = &amp;lt;유형&amp;gt;, ... })&lt;/li&gt;
&lt;li&gt;&lt;b&gt;tuple&lt;/b&gt;([&amp;lt;유형&amp;gt;, ...])&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;변수 입력 우선순위&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;variable은 선언되는 방식에 따라 변수의 우선순위가 있으므로, 이를 적절히 사용해 로컬 환경과 빌드 서버 환경에서의 정의를 다르게 하거나, 프로비저닝 파이프라인을 구성하는 경우 외부 값을 변수에 지정할 수 있다. 입력 변수의 우선순위는 다음과 같고, 우선순위 숫자가 작을수록 우선순위도 낮다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실행 후 입력(변수에 값이 선언되지 않아 CLI에서 입력)&lt;/li&gt;
&lt;li&gt;variable 블록의 default 값&lt;/li&gt;
&lt;li&gt;환경 변수(TF_VAR_변수 이름)&lt;/li&gt;
&lt;li&gt;terraform.tfvars에 정의된 변수 선언&lt;/li&gt;
&lt;li&gt;*.auto.tfvars에 정의된 변수 선언&lt;/li&gt;
&lt;li&gt;*.auto.tfvars.json에 정의된 변수 선언&lt;/li&gt;
&lt;li&gt;CLI 실행 시 -var 인수에 지정 또는 -var-file로 파일 지정&lt;/li&gt;
&lt;/ol&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;local 블록&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1754984719199&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;variable &quot;prefix&quot; {
  default = &quot;hello&quot;
}

locals {
  name    = &quot;terraform&quot;
  content = &quot;${var.prefix} ${local.name}&quot;
  my_info = {
    age    = 20
    region = &quot;KR&quot;
  }
  my_nums = [1, 2, 3, 4, 5]
}

locals {
  content = &quot;centent2&quot; # 중복 선언되었으므로 오류가 발생한다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 내에서 사용자가 지정한 값 또는 속성 값을 가공해 참조 가능한 local(지역 값)은 외부에서 입력되지 않고, 코드 내에서만 가공되어 동작하는 값을 선언한다. local은 입력 변수와 달리 선언된 모듈 내에서만 접근 가능하고, 변수처럼 실행 시에 입력받을 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬이 선언되는 블록은 locals로 시작하고, 선언된 local 값은 local.&amp;lt;이름&amp;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;output 블록&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 내에서 생성되는 속성 값들은 output 블록에 정의된다. 출력되는 값은 value의 값이며 테라폼이 제공하는 조합과 프로그래밍적인 기능들에 의해 원하는 값을 출력할 수 있다. output 정의 시 사용 가능한 메타인수는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;description:&lt;/b&gt; 출력 값 설명&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sensitive:&lt;/b&gt; 민감한 출력 값임을 알리고 테라폼의 출력문에서 값 노출을 제한&lt;/li&gt;
&lt;li&gt;&lt;b&gt;depends_on:&lt;/b&gt; value에 담길 값이 특정 구성에 종속성이 있는 경우 생성되는 순서를 임의로 조정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;precondition:&lt;/b&gt; 출력 전에 지정된 조건을 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테라폼 반복문&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라를 코드로 정의할 때, 동일한 유형의 리소스를 여러 개 생성/설정하는 경우가 많다. 이럴 때는 반복문을 사용해 코드를 더 간결하고 효율적으로 작성할 수 있다. 테라폼은 count, for_each, for, dynamic이라는 다양한 반복문 구조를 지원한다.&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;count&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1754996884325&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;example1&quot; {
  count         = 3                       # 3개의 인스턴스를 생성
  ami           = &quot;ami-0897f20d7e803af8f&quot;  # 사용할 AMI ID
  instance_type = &quot;t2.micro&quot;              # EC2 인스턴스 유형 설정

  tags = {
    Name = &quot;Example-Instance-${count.index}&quot; # 각 인스턴스에 고유한 이름 태그 지정
    # (예: &quot;Example-Instance-0&quot;, &quot;Example-Instance-1&quot;)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 또는 모듈 블록에 count값이 정수인 인수가 포함된 경우 선언된 정수값만큼 리소스나 모듈을 생성하게 된다. count로 생성되는 리소스의 경우 &amp;lt;리소스 타입&amp;gt;.&amp;lt;이름&amp;gt;[&amp;lt;인덱스 번호&amp;gt;], 모듈의 경우 module.&amp;lt;모듈 이름&amp;gt;[&amp;lt;인덱스 번호&amp;gt;]로 해당 리소스의 값을 참조한다. 위 코드처럼 테라폼으로 AWS EC2 인스턴스를 한 번에 3개 생성할 수 있고, count.index를 이용해 이름 태그를 고유하게 만들 수 있다.&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;for_each&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755037036589&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;example2&quot; {
  for_each      = toset([&quot;dev&quot;, &quot;staging&quot;, &quot;prod&quot;]) # 환경별(dev, staging, prod)로 인스턴스를 생성
  ami           = &quot;ami-0897f20d7e803af8f&quot;  # 사용할 AMI ID
  instance_type = &quot;t2.micro&quot;                        # EC2 인스턴스 유형 설정

  tags = {
    Name = &quot;Example-Instance-${each.key}&quot; # 각 환경에 맞는 고유 이름 태그 지정
    # (예: &quot;Example-Instance-dev&quot;)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리소스 또는 모듈 블록에서 for_each에 입력된 데이터 형태가 map 또는 set 이면, 선언된 key 값 개수만큼 리소스를 생성한다. for_each가 설정된 블록에서는 each 속성을 사용해 구성을 수정할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;each.key:&lt;/b&gt; 인스턴스에 해당하는 map 또는 set 타입의 key값&lt;/li&gt;
&lt;li&gt;&lt;b&gt;each.value:&lt;/b&gt; 인스턴스에 해당하는 map의 value 값&lt;/li&gt;
&lt;/ul&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;for&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755038070455&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;locals {
  name_tags = [for name in var.instance_names : &quot;Name-${name}&quot;]
  # instance_names 목록의 각 항목에 &quot;Name-&quot; 접두사를 추가해 태그 이름을 생성
  # (예: [&quot;Name-web1&quot;, &quot;Name-web2&quot;, &quot;Name-db1&quot;])
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for 문은 복합 형식 값의 형태를 변환하는 데 사용된다. 예를 들어, list 값의 포맷을 변경하거나 특정 접두사(prefix)를 추가할 수도 있고, output에 원하는 형태로 반복적인 결과를 표현할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;list 타입의 경우 값 또는 인덱스와 값을 반환&lt;/li&gt;
&lt;li&gt;map 타입의 경우 키 또는 키와 값에 대해 반환&lt;/li&gt;
&lt;li&gt;set 타입의 경우 키 값에 대해 반환&lt;/li&gt;
&lt;/ul&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;dynamic&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1755038637528&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;variable &quot;ingress_rules&quot; {
  description = &quot;보안 그룹을 위한 인그레스 규칙 목록&quot;
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = &quot;TCP&quot;
      cidr_blocks = [&quot;0.0.0.0/0&quot;]
    },
    {
      from_port   = 443
      to_port     = 443
      protocol    = &quot;TCP&quot;
      cidr_blocks = [&quot;0.0.0.0/0&quot;]
    }
  ]
}

resource &quot;aws_security_group&quot; &quot;example&quot; {
  name = &quot;example-sg&quot; # 보안 그룹 이름 설정

  dynamic &quot;ingress&quot; {
    for_each = var.ingress_rules # ingress_rules 변수의 각 항목을 순회하며 인그레스 규칙 생성
    content {
      from_port   = ingress.value.from_port   # 시작 포트 번호 설정
      to_port     = ingress.value.to_port     # 종료 포트 번호 설정
      protocol    = ingress.value.protocol    # 프로토콜 설정 (예: TCP, UDP)
      cidr_blocks = ingress.value.cidr_blocks # CIDR 형식의 허용 IP 블록 설정
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;count나 for_each 구문을 사용한 리소스 전체를 여러 개 생성하는 것 외에도 리소스 내에 선언되는 구성 블록을 다중으로 작성해야 하는 경우가 있다. 위 코드처럼 AWS의 Security Group 리소스 구성에 ingress 요소가 리소스 선언 내부에서 블록 형태로 여러 번 정의되는 경우 dynamic을 사용하여 동적인 블록을 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;테라폼 조건식&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1755075437009&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;locals {
  instance_type = var.environment == &quot;prod&quot; ? &quot;m5.large&quot; : &quot;t2.micro&quot;
}

locals {
  ami_map = {
    &quot;us-east-1&quot; = &quot;ami-063d43db0594b521b&quot; # us-east-1에서 찾은 amazon linux 2023 ami
    &quot;us-west-2&quot; = &quot;ami-066a7fbea5161f451&quot; # us-west-2에서 찾은 amazon linux 2023 ami
  }

  selected_ami = local.ami_map[var.region != &quot;&quot; ? var.region : &quot;us-east-1&quot;] # 리전에 따라 ami 선택
}

resource &quot;aws_instance&quot; &quot;example&quot; {
  ami           = local.selected_ami
  instance_type = local.instance_type
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼에서의 조건식은 3항 연산자 형태를 갖는다. 조건은 true 또는 false로 확인되는 모든 표현식을 사용할 수 있다. 위 코드처럼 locals 블록에서 조건문과 맵을 사용해 설정값을 미리 정의하고, 리소스 생성 시 이를 참조하면 환경과 리전에 따라 EC2 인스턴스의 유형과 AMI를 동적으로 선택하도록 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;함수&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1755089919188&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 컬렉션 함수 예제
variable &quot;fruit_list&quot; {
  default = [&quot;apple&quot;, &quot;banana&quot;, &quot;cherry&quot;]
}

output &quot;joined_fruit&quot; {
  value = join(&quot;, &quot;, var.fruit_list) # 리스트를 콤마로 구분하여 문자열로 결합
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼은 프로그래밍 언어적인 특성을 갖추고 있는데, 값의 유형을 변경하거나 조합할 수 있는 내장 함수들이 그 예다. 단, 내장된 함수 외에 사용자가 구현하는 별도의 사용자 정의 함수를 지원하지는 않는다. 함수 종류에는 숫자, 문자열, 컬렉션, 인코딩, 파일 시스템, 날짜/시간, 해시/암호화, IP 네트워크, 유형 변환이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자 함수: abs, ceil, floor, max, min 등 숫자 관련 함수&lt;/li&gt;
&lt;li&gt;문자열 함수: join, split, replace, trimspace, length 등 문자열 처리 함수&lt;/li&gt;
&lt;li&gt;컬렉션 함수: merge, contains, lookup, flatten, keys 등 리스트와 맵을 처리하는 함수&lt;/li&gt;
&lt;li&gt;변환 함수: toset, tolist, tostring 등 타입 변환 함수&lt;/li&gt;
&lt;li&gt;네트워크 함수: cidrsubnet, cidrhost, cidrnetmask 등 네트워크 관련 함수&lt;/li&gt;
&lt;li&gt;파일 및 디렉터리 함수: file, filebase64, templatefile 등 파일 처리를 위한 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;모듈&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테라폼으로 인프라와 서비스를 관리하면 시간이 지날수록 구성이 복잡해지고 관리하는 리소스가 늘어나게 된다. 테라폼의 구성 파일과 디렉터리 구성에는 제약이 없기 때문에 단일 파일 구조상에서 지속적으로 업데이트할 수 있지만, 다음과 같은 문제가 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테라폼 구성에서 원하는 항목을 찾고 수정하는 것이 점점 어려워짐&lt;/li&gt;
&lt;li&gt;리소스들 간의 연관 관계가 복잡해질수록 변경 작업의 영향도를 분석하기 위한 노력이 늘어남&lt;/li&gt;
&lt;li&gt;개발/스테이징/프로덕션 환경으로 구분된 경우 비슷한 형태의 구성이 반복되어 업무 호율이 줄어듦&lt;/li&gt;
&lt;li&gt;새로운 프로젝트를 구성하는 경우 기존 구성에서 취해야 할 리소스 구성과 종속성 파악이 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 문제처럼 테라폼으로 관리하는 대상의 규모가 커지고 복잡해져 생긴 문제를 보완하고 관리 작업을 수월하게 하기 위한 방안으로 테라폼 구성의 집합인 모듈을 사용할 수 있다. 모듈은 대부분의 프로그래밍 언어에서 쓰이는 라이브러리나 패키지와 역할이 비슷하다고 할 수 있고, 다음과 같은 기본 작성 원칙을 제안한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 디렉터리 형식은 terraform-&amp;lt;프로바이더 이름&amp;gt;-&amp;lt;모듈 이름&amp;gt; 형식으로 제안한다. 이 형식은 HCP Terraform, Terraform Enterprise에서도 사용되는 방식으로 디렉터리 또는 레지스트리 이름이 테라폼을 위한 것인지, 어떤 프로바이더의 리소스를 포함하고 있는지, 부여된 이름이 무엇인지 판별할 수 있도록 한다.&lt;/li&gt;
&lt;li&gt;공개된 테라폼 레지스트리의 모듈을 참고하는 것을 권장한다. 대다수의 테라폼 모듈은 공개된 모듈이 존재하고 거의 모든 인수에 대한 변수 처리, 반복문 적용 리소스, 조건에 따른 리소스 활성/비활성 등을 모범 사례로 공개해 두었다.&lt;/li&gt;
&lt;li&gt;작성된 모듈은 공개 또는 비공개로 게시해 팀 또는 커뮤니티와 공유하기를 제안한다. 모듈의 사용성을 높이고 피드백을 통해 더 발전된 모듈을 구성할 수 있는 자극이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;모듈 사용 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈에서 사용되는 모든 리소스는 관련 프로바이더의 정의가 필요하다. 여기서 사용자는 프로바이더 정의를 모듈 안에 두어야 할지 밖에 두어야 할지 구성을 고민할 수 있다. 두 유형에 관한 내용은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자식 모듈에서 프로바이더 정의:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;동일한 프로바이더가 루트와 자식 양쪽에서 또는 서로 다른 자식 모듈에 버전 조건 합의가 안 되면, 오류가 발생하고 모듈에 반복문을 사용할 수 없다는 단점이 있으므로 잘 사용하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;루트 모듈에서 프로바이더 정의:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;자식 모듈 루트 모듈의 프로바이더 구성에 종속되는 방식이다. 자식 모듈에 특정 프로바이더 구성의 종속성은 반영할 수 없으므로 자식 모듈을 테스트한 프로바이더 조건에 대해 기록하고, 자식 모듈을 사용하는 루트 모듈에서 정의하는 프로바이더에 맞게 업데이트해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755252593283&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# modules/vpc/variables.tf
variable &quot;cidr_block&quot; {
  description = &quot;VPC의 CIDR 블록&quot;
  type        = string
  default     = &quot;192.168.0.0/16&quot;
}

variable &quot;vpc_name&quot; {
  description = &quot;VPC의 이름&quot;
  type        = string
  default     = &quot;your-vpc&quot;
}

# modules/vpc/main.tf
resource &quot;aws_vpc&quot; &quot;this&quot; {
  cidr_block = var.cidr_block # VPC의 CIDR 블록을 변수로 설정하여 유연성 제공
  tags = {
    Name = var.vpc_name # VPC의 이름 태그를 변수로 설정
  }
}

# modules/vpc/outputs.tf
output &quot;vpc_id&quot; {
  description = &quot;VPC의 ID&quot;
  value       = aws_vpc.this.id
}

# main.tf
terraform {
  required_version = &quot;&amp;gt;=1.9.8&quot;

  required_providers {
    aws = {
      source  = &quot;hashicorp/aws&quot;
      version = &quot;&amp;gt;=5.81.0&quot;
    }
  }
}
provider &quot;aws&quot; {
  region = &quot;ap-northeast-2&quot;
  profile = &quot;my-profile&quot;
}

module &quot;my_vpc&quot; { # module 사용
  source = &quot;./modules/vpc&quot;

  # variables
  vpc_name = &quot;my-vpc&quot;
  cidr_block = &quot;10.0.0.0/16&quot;
}

# outputs.tf
output &quot;vpc_id&quot; {
  value = module.my_vpc.vpc_id
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0npEE/btsPUhEjP2n/CvpBeO53h5c0YxX4Z45uYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0npEE/btsPUhEjP2n/CvpBeO53h5c0YxX4Z45uYk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;432&quot; data-filename=&quot;스크린샷 2025-08-16 오전 8.44.20.png&quot; style=&quot;width: 22.9393%; margin-right: 10px;&quot; data-widthpercent=&quot;23.21&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0npEE/btsPUhEjP2n/CvpBeO53h5c0YxX4Z45uYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0npEE%2FbtsPUhEjP2n%2FCvpBeO53h5c0YxX4Z45uYk%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;714&quot; height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB7Rui/btsPUrUGZK0/VKRtFROj2ZrEAR9WaPJkD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB7Rui/btsPUrUGZK0/VKRtFROj2ZrEAR9WaPJkD0/img.png&quot; data-origin-width=&quot;4506&quot; data-origin-height=&quot;824&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;76.79&quot; style=&quot;width: 75.8979%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB7Rui/btsPUrUGZK0/VKRtFROj2ZrEAR9WaPJkD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB7Rui%2FbtsPUrUGZK0%2FVKRtFROj2ZrEAR9WaPJkD0%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;4506&quot; height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;모듈 폴더 구조 / VPC 생성 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 modules/vpc에 VPC 모듈을 만들고, 루트에서 그 모듈을 호출해 VPC를 생성한다. 모듈은 cidr_block, vpc_name 변수를 받아 aws_vpc.this를 만들고 vpc_id를 출력한다. 루트는 AWS 프로바이더/버전 제약을 설정하고, module &quot;my_vpc&quot;롭 ㅕㄴ수를 덮어써 서울 리전에 VPC를 만들도록 했다. 이렇게 모듈로 묶여진 리소스는 module이라는 정의를 통해 단순하게 재활용하고 반복사용할 수 있다. 모듈의 결과 참조 형식은 module.&amp;lt;모듈 이름&amp;gt;.&amp;lt;output 이름&amp;gt;으로 정의된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;테라폼 모듈 레지스트리&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테라폼은 모듈을 불러올 때 source 값의 형식을 분석해 이를 HTTP, Git, 로컬 파일, 레지스트리 등 해당 프로토콜 방식으로 해석해서 가져온다. 이떄,&amp;nbsp;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1;&quot; href=&quot;https://registry.terraform.io/browse/modules&quot;&gt;테라폼 모듈 레지스트리&lt;/a&gt;는 Terraform이 정의한 모듈 소스 프로토콜을 통해 모듈을 불러는 방식으로 공개된 테라폼 모듈을 사용하거나 HCL Terraform, Terraform Enterprise에서 제공되는 비공개 테라폼 모듈을 사용할 때 설정하는 소스 지정 방식이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1755302158476&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;module &quot;vpc&quot; {
  source = &quot;terraform-aws-modules/vpc/aws&quot;
  version = &quot;~&amp;gt;6.0&quot;
  
  # ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드는 HashiCorp의 공식 registry.terraform.io인 공개 레지스트리를 사용한 것으로 terraform-aws-modules는 네임스페이스, vpc는 모듈 이름, aws는 프로바이더를 의미한다. terraform init 시 terraform-aws/modules/vpc/aws 모듈 코드를 레지스트리에서 내려받아 .terraform/modules에 캐싱한다. terraform plan에서 모듈 내 리소스 정의를 로드하고, 현재 상태와 비교해 변경 계획을 생성한다. 그리고 terraform apply에서 AWS API를 호출해 VPC를 생성한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://k21academy.com/terraform-iac/terraform-workflow-and-its-use-case/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Terraform Workflow: Terraform Plan &amp;amp; AWS Create VPC Overview&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000202478097?utm_source=google&amp;amp;utm_medium=cpc&amp;amp;utm_campaign=googleSearch&amp;amp;gt_network=g&amp;amp;gt_keyword=&amp;amp;gt_target_id=aud-901091942354:dsa-435935280379&amp;amp;gt_campaign_id=9979905549&amp;amp;gt_adgroup_id=132556570510&amp;amp;gad_source=1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;테라폼으로 시작하는 IaC : 테라폼 입문부터 실무 적용, 생성형 AI 활용까지 올인원 가이드&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%ED%85%8C%EB%9D%BC%ED%8F%BC-%EA%B0%9C%EC%9A%94-%EA%B8%B0%EB%B3%B8%EB%AC%B8%EB%B2%95/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(기본)테라폼 개요와 기본 문법&lt;/a&gt;&lt;/p&gt;</description>
      <category>기타/AWS</category>
      <category>terraform</category>
      <category>테라폼</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/137</guid>
      <comments>https://oneny.tistory.com/137#entry137comment</comments>
      <pubDate>Fri, 15 Aug 2025 20:08:29 +0900</pubDate>
    </item>
    <item>
      <title>분산락과 낙관적 락을 사용한 데이터 정합성 지키기</title>
      <link>https://oneny.tistory.com/136</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;분산락과&amp;nbsp;낙관적&amp;nbsp;락을&amp;nbsp;사용한&amp;nbsp;데이터&amp;nbsp;정합성&amp;nbsp;지키기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oneny.tistory.com/132&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;보상 트랜잭션을 사용한 데이터 정합성 유지하기&lt;/a&gt; 블로그 글에서 주문 비즈니스 로직에서 분산 트랜잭션을 처리 시스템을 구축했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 상품 서비스에서 재고를 관리하고 있으며, 다수의 사용자가 동시에 한정 수량의 상품을 구매할 경우 UPDATE inventory SET quantity = quantity - 1와 같은 단순 쿼리만으로는 경쟁 조건(Race Condition)이 발생해 데이터 정합성이 쉽게 깨질 수 있다. 따라서 이커머스에서 재고의 정확성은 곧 서비스 신뢰도와 직결되므로, 정합성을 보장하기 위한 아키텍처 설계와 동시에 제어 전략이 필수적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 상품 서비스에서 일반적으로 비관적 락(Pessimistic Lock)과 낙관적 락(Optimistic Lock)을 활용해 정합성을 확보할 수 있다. 재고에 대한 경쟁 조건을 방지하기 위한 작업 시나리오에 대해 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;분산락(Distributed Lock)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산락은 물리적으로 분산된 여러 서버 인스턴스에서 동시에 동일한 자원에 접근하려는 경쟁 상황이 발생할 때, 이를 서로 배타적으로 제어할 수 있도록 보장해주는 동기화 수단이다. 즉, 비관적 락의 개념을 분산 환경에서 확장된 구현 방식이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;분산락을 위한 Java 레디스 클라이언트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에서 분산락을 구현하는 방법은 주로 SETNX(&lt;b&gt;SET&lt;/b&gt; if &lt;b&gt;N&lt;/b&gt;ot e&lt;b&gt;X&lt;/b&gt;ists) 명령어와 EXPIRE 명령어를 조합하는 방식으로 Java 환경에서 Redis와 통신할 수 있는 대표적인 클라이언트는 Lettuce와 Redisson이 있다. Lettuce는 스핀락(Spin-Lock) 방식으로 락을 구현하기 때문에 락을 얻기 위해 계속해서 반복적으로 시도하기 때문에 락을 얻기 전까지 CPU를 지속적으로 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mwP22/btsPExA0Mm0/JacD01GYOpEnmIDbQbgMt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mwP22/btsPExA0Mm0/JacD01GYOpEnmIDbQbgMt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mwP22/btsPExA0Mm0/JacD01GYOpEnmIDbQbgMt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmwP22%2FbtsPExA0Mm0%2FJacD01GYOpEnmIDbQbgMt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;366&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, Redisson은 &lt;b&gt;레드락(RedLock) 알고리즘을 공식적으로 지원&lt;/b&gt;하고, &lt;b&gt;Redis의 Pub/Sub 메커니즘을 사용&lt;/b&gt;해 락을 관리하며 더 효율적이고 자원 소모가 적은 방식으로 락을 제어한다. 위 사진에서 채널을 통해 메시지를 보내는 것처럼 하나의 채널을 만들고 하나의 스레드가 락을 점유하면 해제했을 때 채널에 메시지를 보내줌으로써 구독하고 있는(락을 획득해야 하는) 스레드들이 메시지를 받아 락 획득을 시도하는 방식이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Redisson 구현&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1754095240931&quot; class=&quot;livescript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAop {

    private final RedissonReactiveClient redissonClient;
    private final AopForTransaction aopForTransaction;

    @Around(&quot;@annotation(com.idolu.product.global.annotation.DistributedLock)&quot;)
    public Mono&amp;lt;Object&amp;gt; lock(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        String baseKey = distributedLock.lockName();
        String dynamicKey = RedissonKeyGenerator.generateDynamicKey(
                distributedLock.identifier(),
                joinPoint.getArgs(),
                distributedLock.paramClassType(),
                signature.getParameterNames());
        long threadId = ThreadLocalRandom.current().nextLong();

        return redissonClient.getLock(baseKey + &quot;:&quot; + dynamicKey)
                .tryLock(
                        distributedLock.waitTime(),
                        distributedLock.leaseTime(),
                        distributedLock.timeUnit(),
                        threadId)
                .handle((lockAcquired, sink) -&amp;gt; {
                    log.info(&quot;락 획득: {}, threadId: {}&quot;, lockAcquired, threadId);
                    if (lockAcquired) sink.next(true);
                    else sink.error(new IllegalArgumentException(&quot;락을 획득하지 못했습니다.&quot;));
                })
                .flatMap(lockAcquired -&amp;gt; {
                    try {
                        return aopForTransaction.proceed(joinPoint);
                    } catch (Throwable e) {
                        return Mono.error(e);
                    }
                })
                .flatMap(result -&amp;gt; {
                    log.info(&quot;락 해제 시도 threadId: {}&quot;, threadId);
                    return redissonClient.getLock(baseKey + &quot;:&quot; + dynamicKey)
                            .unlock(threadId)
                            .thenReturn(result);
                })
                .onErrorResume(ex -&amp;gt; {
                    log.info(&quot;락 해제 시도 threadId: {}&quot;, threadId);
                    return redissonClient.getLock(baseKey + &quot;:&quot; + dynamicKey)
                            .unlock(threadId)
                            .then(Mono.error(ex)); // 예외 다시 전달
                });
    }
}

@Slf4j
@Component
public class AopForTransaction {

    @Transactional
    public Mono&amp;lt;Object&amp;gt; proceed(ProceedingJoinPoint joinPoint) throws Throwable {
        return (Mono&amp;lt;Object&amp;gt;) joinPoint.proceed();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 @DistributedLock 어노테이션이 붙은 메서드 실행 시 Redisson 기반의 분산락을 획득한 후 락 안에서 트랜잭션을 시작하여 비즈니스 로직을 실행하고, 정상 또는 예외 상황에서도 반드시 락을 해제하도록 보장하는 AOP이다. 전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@DistributedLock 어노테이션을 감지하여 AOP가 동작한다.&lt;/li&gt;
&lt;li&gt;lockName과 identifier 값을 기반으로 동적 락 키를 생성한다.&lt;/li&gt;
&lt;li&gt;RedissonReactiveClient를 이용하여 비동기로 락 획득을 시도한다.&lt;/li&gt;
&lt;li&gt;락을 획득하면 aopForTransaction.proceed()를 실행하여 트랜잭션을 시작한다.&lt;/li&gt;
&lt;li&gt;본래의 메서드를 실행하여 비즈니스 로직을 수행한다.&lt;/li&gt;
&lt;li&gt;트랜잭션을 커밋 또는 롤백한다.&lt;/li&gt;
&lt;li&gt;락을 해제한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;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-08-03 오후 4.46.15.png&quot; data-origin-width=&quot;3242&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7ge5c/btsPDYsi34u/VD3vYRJGQ8XoqJAgfwmbr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7ge5c/btsPDYsi34u/VD3vYRJGQ8XoqJAgfwmbr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7ge5c/btsPDYsi34u/VD3vYRJGQ8XoqJAgfwmbr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7ge5c%2FbtsPDYsi34u%2FVD3vYRJGQ8XoqJAgfwmbr0%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;3242&quot; height=&quot;510&quot; data-filename=&quot;스크린샷 2025-08-03 오후 4.46.15.png&quot; data-origin-width=&quot;3242&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oneny.tistory.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;분산 락 사용 시 상위 트랜잭션이 있으면 안되는 이유&lt;/a&gt; 블로그 글에서 설정한 적이 있듯이 트랜잭션이 먼저 시작한 상태에서 락을 획득할 경우 MySQL의 REPEATABLE READ 격리 수준에 따라 각 트랜잭션의 변경사항을 인지하지 못한 채 데이터를 읽고 수정하게 되어 Lost Update와 같은 정합성 문제가 발생할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 안정적인 동시성 제어를 위해서는 반드시 분산 락을 먼저 획득한 후 트랜잭션을 시작해야 하며, 로그에서도 이러한 순서 보장하고 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;낙관적 락 적용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락은 강력한 동시성 제어 수단이지만, 네트워크 지연이나 클라이언트의 비정상 동작 등 예기치 못한 상황에서는 완벽하게 동작하지 않을 수 있다. 대부분의 경우에는 문제없이 운영되지만, 이러한 극히 드문 예외 상황에서도 데이터 정합성을 보장하기 위해 낙관적 락을 보조적으로 함께 사용하여 재고 처리를 안정적으로 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754213027557&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Configuration
public class RetryConfig {

    // 재시도 전략 설정
    @Bean
    public Retry optimisticLockingBackoffRetry() {
        return Retry.backoff(3, Duration.ofSeconds(2)).jitter(0.1)
                .filter(ex -&amp;gt; ex instanceof OptimisticLockingFailureException)
                .doBeforeRetry(retrySignal -&amp;gt; log.error(&quot;retryCount: {}, errorCode: {}&quot;, retrySignal.totalRetries(), retrySignal.failure().getMessage()))
                .onRetryExhaustedThrow((spec, retrySignal) -&amp;gt; retrySignal.failure());
    }
}

// 재시도 전략 적용
public Mono&amp;lt;Boolean&amp;gt; updateProductStock(ProductStockUpdateCommand command) {
    return productAdapter.updateProductStock(command)
            .retryWhen(optimisticLockingBackoffRetry);
}

// 분산락 및 낙관적 락 적용
@DistributedLock(lockName = &quot;productStock&quot;, identifier = &quot;productId&quot;, paramClassType = ProductStockUpdateCommand.class)
public Mono&amp;lt;Boolean&amp;gt; updateProductStock(ProductStockUpdateCommand command) {
    return inventoryUpdateLogRepository.findInventoryUpdateLogByOrderNoAndType(command.getOrderNo(), command.getStockType()) // 이미 처리된 주문 재고 처리건인지 확인
            .handle((__, sink) -&amp;gt; sink.error(new ProductBadRequestException(ALREADY_STOCK_UPDATE))) // 이미 처리된 주문 재고 변경건
            .switchIfEmpty(Mono.just(true))
            .flatMap(__ -&amp;gt; inventoryUpdateLogRepository.save(InventoryUpdateLog.builder()
                    .productId(command.getProductId())
                    .orderNo(command.getOrderNo())
                    .quantity(command.getStock())
                    .type(command.getStockType())
                    .build()))
            .flatMap(__ -&amp;gt; productRepository.findById(command.getProductId()))
            .flatMap(product -&amp;gt; productRepository.save(product.updateStock(command.getStock(), command.getStockType()))) // 재고 처리
            .thenReturn(true);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 재고 변경 요청을 처리할 때 데이터 정합성을 보장하기 위해 Redisson 기반의 분산 락과 낙관적 락을 함께 사용했다. 락을 획득한 이후에는 해당 주문 번호와 재고 타입에 대한 중복 처리를 방지하기 위해 InventoryUpdateLog 테이블을 먼저 조회하고, 기존에 동일한 요청이 처리된 적이 있다면 작업을 중단한다. 중복된 작업이 아니라면, 처리 로그를 저장하고 해당 상품을 조회한 뒤 재고를 변경한다. 이때, 재고 변경 메서드인 updateStock()은 내부적으로 @Version 필드를 활용해 낙관적 락을 적용하고 있으며, 이로 인해 동시성 충돌이 발생할 경우 OptimisticLockingFailureException 예외가 발생한다. 이를 보완하기 위해 RetryConfig 클래스에서 OptimisticLockingFailureException 예외에 대한 재시도 전략을 정의하고, 예외가 발생할 때마다 재시도 횟수와 실패 원인을 로그로 기록할 수 있게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Redisson의 분산 락으로 서비스 간 동시성 문제를 제어하고, 낙관적 락과 재시도 로직으로 데이터베이스 수준의 충돌을 제어하여 재고 정합성을 지킬 수 있도록 로직을 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://medium.com/musinsa-tech/%EB%AC%B4%EC%A7%84%EC%9E%A5-%ED%9E%98%EB%93%A4%EC%97%88%EC%A7%80%EB%A7%8C-%EB%AC%B4%EC%A7%84%EC%9E%A5-%EC%84%B1%EC%9E%A5%ED%95%9C-%EA%B0%9C%EB%B0%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-e445888579a9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;무진장 힘들었지만 무진장 성장한 개발 이야기&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/311&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Redis] 레디스가 제공하는 분산락(RedLock)의 특징과 한계&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@rkdalstj4505/Redis-%EB%B6%84%EC%82%B0-%EB%9D%BDLettuce-Redisson&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Redis 분산 락(Lettuce, Redisson)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=UOWy6zdsD-c&amp;amp;t=456s&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;토스 | SLASH 22 - 애플 한 주가 고객에게 전달 되기까지&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/트러블 슈팅</category>
      <category>Redisson</category>
      <category>WebFlux Redisson</category>
      <category>낙관적 락</category>
      <category>분산락</category>
      <category>분산락과 낙관적 락</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/136</guid>
      <comments>https://oneny.tistory.com/136#entry136comment</comments>
      <pubDate>Sun, 3 Aug 2025 18:41:50 +0900</pubDate>
    </item>
    <item>
      <title>K8S - 서비스 API 카테고리</title>
      <link>https://oneny.tistory.com/135</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스 API 카테고리&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서비스 API 카테고리로 분류된 리소스는 클러스터상의 컨테이너에 대한 엔드포인트를 제공하거나 레이블과 일치하는 컨테이너의 디스커버리에 사용되는 리소스이다. 내부적으로 사용되는 리소스를 제외하고 사용자가 직접 사용하는 것은 L4 로드 밸런싱을 제공하는 서비스 리소스와 L7 로드 밸런싱을 제공하는 인그레스 리소스, 두 종류가 있다. 또한, 서비스 리소스에는 제공 목적에 따라 클러스터 내부나 클러스터 외부에서 접속할 수 있는 가상 IP 등의 엔드포인트를 제공하고 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;쿠버네티스 클러스터 네트워크와 서비스&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oneny.tistory.com/133&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;EKS 기본 아키텍처 게시글&lt;/a&gt;에서 확인할 수 있는데 EKS 클러스터를 생성하면 노드별로 Amazon VPC CNI가 설치되어 내부 네트워크가 자동으로 구성되기 때문에 파드는 서비스를 사용하지 않고도 파드 간 통신이 가능하지만 서비스를 사용하면 다음 두 가지 큰 장점을 얻을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파드에 대한 트래픽 로드 밸런싱&lt;/li&gt;
&lt;li&gt;서비스 디스커버리와 클러스터 내부 DNS&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;파드에 대한 트래픽 로드 밸런싱&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디플로이먼트를 사용하여 여러 파드를 기동할 수 있는데, 파드는 기동될 때마다 각기 다른 IP 주소를 할당받기 때문에 로드 밸런싱하는 구조를 자체적으로 구현하려면 각 파드의 IP 주소를 매번 조회하거나 전송 대상의 목적지를 설정해야 한다. 또한, 서비스는 로드 밸런싱의 접속 창구가 되는 엔드포인트도 제공한다. 엔드포인트는 외부 로드 밸런서가 할당하는 가상 IP 주소(Virtual IP 주소)나 클러스터 내부에서만 사용할 수 있는 가상 IP 주소(ClusterIP) 등 여러 가지 종류를 제공한다.&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;서비스 디스커버리와 클러스터 내부 DNS&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 디스커버리는 특정 조건의 대상이 되는 멤버를 보여주거나 이름에 엔드포인트를 판별하는 기능으로 서비스(Service)가 해당 기능을 제공하고 있다. 즉, 서비스에 속해 있는 파드를 보여주거나 서비스명에서 엔드포인트 정보를 반환하는 것을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS를 사용한 서비스 디스커버리는 클러스터 내부 DNS 서버에 자동으로 등록되는 서비스 엔드포인트 정보를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1753347527336&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - name: nginx-container
        image: amsy810/echo-nginx:v2.0
---
apiVersion: v1
kind: Service
metadata:
  name: sample-clusterip
spec:
  type: ClusterIP
  ports:
  - name: &quot;http-port&quot;
    protocol: &quot;TCP&quot;
    port: 8080
    targetPort: 80
  selector:
    app: sample-app&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;2600&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bj0Etw/btsPqU9WLM4/cyVvHGxjPwGTX3nlNi0cEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bj0Etw/btsPqU9WLM4/cyVvHGxjPwGTX3nlNi0cEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bj0Etw/btsPqU9WLM4/cyVvHGxjPwGTX3nlNi0cEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbj0Etw%2FbtsPqU9WLM4%2FcyVvHGxjPwGTX3nlNi0cEK%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;2600&quot; height=&quot;300&quot; data-origin-width=&quot;2600&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 파드에서 서비스로 할당되는 엔드포인트에 접속하려면 당연히 목적지가 필요하지만, 할당된 IP 주소를 사용하는 방법 외에도 자동 등록된 DNS 레코드를 사용할 수 있다. 위 사진에서 확인할 수 있듯이 자동 할당된 IP 주소에 연결된 DNS명을 사용하여 같은 HTTP 요청을 보내도 같은 결과를 얻을 수 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ClusterIP&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bveejf/btsPpixBO1N/J6hg3t4erCWV3AJZdK9kLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bveejf/btsPpixBO1N/J6hg3t4erCWV3AJZdK9kLK/img.png&quot; data-alt=&quot;출처: https://zesty.co/finops-glossary/kubernetes-clusterip/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bveejf/btsPpixBO1N/J6hg3t4erCWV3AJZdK9kLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbveejf%2FbtsPpixBO1N%2FJ6hg3t4erCWV3AJZdK9kLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;398&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://zesty.co/finops-glossary/kubernetes-clusterip/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClusterIP 서비스는 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;쿠버네티스 클러스터 내부에서만 통신 가능&lt;/span&gt;한, Internal Network에 생성되는 가상 IP가 할당&lt;/b&gt;된다. 따라서 해당 유형의 서비스에 접근하는 클라이언트도 클러스터 내부에 위치해야 하며, 외부에서 접속이 필요한 경우에는 별도의 NodePort나 LoadBalancer, 혹은 Ingress 설정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClusterIP로 트래픽이 전달되는 방식은 각 노드에 상주하는 kube-proxy가 핵심 역할을 한다. 클라이언트가 서비스의 ClusterIP로 요청을 보내면, &lt;b&gt;kube-proxy는 노드의 iptables에 설정된 분산 규칙을 통해 요청을 서비스에 연결된 파드 중 하나로 무작위로 전달&lt;/b&gt;한다. 이 과정을 통해 로드 밸런싱이 자동으로 수행되며, 파드가 어느 노드에 위치해 있는지와 관계없이 요청은 적절한 파드로 분산된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752846905144&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: sample-clusterip
spec:
  type: ClusterIP
  ports:
  - name: &quot;http-port&quot;
    protocol: &quot;TCP&quot;
    port: 8080
    targetPort: 80
  selector:
    app: sample-app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;type: ClusterIP를 지정하면 ClusterIP 서비스를 생성할 수 있고, 설정 항목은 다음과 같다. 그리고 기본적으로 쿠버네티스 서비스에 등록된 클러스터 내부 DNS 레코드를 사용하여 호스트(sample-clusterip)를 지정하는 것이 바람직하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&amp;nbsp;spec.ports[].port:&lt;/b&gt; ClusterIP에서 수신할 포트 번호를 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;spec.ports[].targetPort:&lt;/b&gt; 목적지 컨테이너 포트 번호를 지정&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;NodePort&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NdbQj/btsPrem7igB/4BZADIqivMrAe3w6OI6iLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NdbQj/btsPrem7igB/4BZADIqivMrAe3w6OI6iLK/img.png&quot; data-alt=&quot;출처: https://zesty.co/finops-glossary/nodeport-in-kubernetes-a-glossary-overview/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NdbQj/btsPrem7igB/4BZADIqivMrAe3w6OI6iLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNdbQj%2FbtsPrem7igB%2F4BZADIqivMrAe3w6OI6iLK%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;519&quot; height=&quot;332&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://zesty.co/finops-glossary/nodeport-in-kubernetes-a-glossary-overview/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodePort는 &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;모든 쿠버네티스 노드의 IP 주소:포트에서 수신한 트래픽을 컨테이너에 전송&lt;/span&gt;하는 형태로 외부와 통신할 수 있는 서비스&lt;/b&gt;다. 즉, Listen할 때 0.0.0.0:포트를 사용하여 모든 IP 주소로 바인드하는 형태다.&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;NodePort 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1752925399905&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: sample-nodeport
spec:
  type: NodePort
  ports:
  - name: &quot;http-port&quot;
    protocol: &quot;TCP&quot;
    port: 8080
    targetPort: 80
    nodePort: 30000
  selector:
    app: sample-app&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-23 오후 8.23.47.png&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UkQEG/btsPv2ujVV1/zmOYipxGR6JQScfWzgIt6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UkQEG/btsPv2ujVV1/zmOYipxGR6JQScfWzgIt6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UkQEG/btsPv2ujVV1/zmOYipxGR6JQScfWzgIt6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUkQEG%2FbtsPv2ujVV1%2FzmOYipxGR6JQScfWzgIt6k%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;1316&quot; height=&quot;178&quot; data-filename=&quot;스크린샷 2025-07-23 오후 8.23.47.png&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매니페스트를 사용하여 NodePort 서비스를 생성할 수 있고, 설정 항목은 다음과 같다. 그리고 생성한 NodePort 서비스를 확인해보면 컨테이너 내부에서의 통신에서 ClusterIP를 사용하기 위해 ClusterIP도 자동으로 할당된 것을 확인할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;spec.ports[].port:&lt;/b&gt; ClusterIP에서 수신할 포트 번호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;spec.ports[].targetPort:&lt;/b&gt; 목적지 컨테이너 포트 번호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;spec.ports[].nodePort:&lt;/b&gt; 모든 쿠버네티스 노드 IP 주소에서 수신할 포트 번호&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodePort에서 사용할 수 있는 포트 범위는 많은 쿠버네티스 환경에서 30000 ~ 32767(쿠버네티스 기본값)이며, 범위 외의 포트를 지정하려고 하면 에러가 발생할 수 있다. 또한, 자동으로 할당되는 NodePort도 같은 범위 안에서 사용 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2326&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vS7KR/btsPwuDPUEV/E9UkDkyHi7ftnFJknKn740/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vS7KR/btsPwuDPUEV/E9UkDkyHi7ftnFJknKn740/img.png&quot; data-alt=&quot;NodePort 생성 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vS7KR/btsPwuDPUEV/E9UkDkyHi7ftnFJknKn740/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvS7KR%2FbtsPwuDPUEV%2FE9UkDkyHi7ftnFJknKn740%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;2326&quot; height=&quot;600&quot; data-origin-width=&quot;2326&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NodePort 생성 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NodePort에서 사용하고 있는 쿠버네티스 노드에서 포트 상태를 확인해보면, 모든 노드의 30000/TCP 포트로 Listen하고 있는 상태가 되어 모든 쿠버네티스 노드의 IP 주소로 쿠버네티스 클러스터 외부에서도 통신이 가능하다. 따라서 노드에 curl -s http:{IP 주소}:30000으로 요청하면 통신이 가능할 것을 확인할 수 있다.&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;NodePort 주의점&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복수의 포트로 Listen 상태를 가질 수 없기 때문에 여러 NodePort 서비스에서 같은 포트를 사용하는 것은 불가능하며, &lt;b&gt;클라이언트가 노드 IP와 포트를 직접 알아야 하고, 클러스터 내부 네트워크가 외부에 노출&lt;/b&gt;될 수 있다는 단점이 있다. 또한, NodePort를 사용하면 하나의 쿠버네티스 &lt;b&gt;노드에 할당된 IP 주소로 통신해야 하기 때문에 그 노드가 단일 장애점(Single Point of Failure, SPoF)가 되어 버린다.&lt;/b&gt; 이러한 보안적으로 취약한 부분을 LoadBalancer 서비스를 사용하여 보완할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;LoadBalancer 서비스&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-23 오후 9.09.40.png&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFsny/btsPvCbup0C/o3yh4HHUEiE2JuMUL958w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFsny/btsPvCbup0C/o3yh4HHUEiE2JuMUL958w0/img.png&quot; data-alt=&quot;출처: https://www.inflearn.com/course/amazon-eks-%EA%B8%B0%EB%B3%B8-%EA%B0%95%EC%9D%98/dashboard&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFsny/btsPvCbup0C/o3yh4HHUEiE2JuMUL958w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFsny%2FbtsPvCbup0C%2Fo3yh4HHUEiE2JuMUL958w0%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;759&quot; height=&quot;322&quot; data-filename=&quot;스크린샷 2025-07-23 오후 9.09.40.png&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.inflearn.com/course/amazon-eks-%EA%B8%B0%EB%B3%B8-%EA%B0%95%EC%9D%98/dashboard&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoadBalancer 서비스는 &lt;b&gt;쿠버네티스 클러스터 외부의 로드 밸런서에 외부 통신이 가능한 가상 IP를 할당하여 외부와 트래픽을 송수신할 때 사용하는 서비스&lt;/b&gt;이다. LoadBalancer 타입을 사용하면 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;쿠버네티스 노드와 별도로 외부 로드 밸런서를 사용&lt;/b&gt;&lt;/span&gt;하기 때문에 노드 장애가 발생해도 가용성을 확보한 덕분에 크게 문제가 않는다. 클러스터 내부 구조를 외부에 노출하지 않도록 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 LoadBalancer에 접근하면 &lt;b&gt;LoadBalancer를 통해 노드의 IP와 포트를 통해 NodePort로 인입되고, ClusterIP를 통해 iptables 분산 룰로 파드에 전달&lt;/b&gt;된다. 즉, 클라이언트는 로드밸런서의 주소만 알면 되기 때문에 클러스터 내부 구조를 외부에 노출하지 않도록 구성할 수 있다. 이때, &lt;b&gt;LoadBalancer 타입은 OSI 4계층 수준에서 분산 처리&lt;/b&gt;가 이루어진다. AWS 환경에서 보면 NLB에 대응된다.&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;LoadBalancer 서비스 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1753274936143&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: sample-clusterip
spec:
  type: ClusterIP
  ports:
  - name: &quot;http-port&quot;
    protocol: &quot;TCP&quot;
    port: 8080
    targetPort: 80
    nodePort: 30000
  selector:
    app: sample-app&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoadBalancer 서비스를 생성하면 컨테이너 내부에서의 통신은 &lt;b&gt;ClusterIP를 사용하기 위해 ClusterIP도 자동 할당&lt;/b&gt;되고, NodePort도 자동적으로 할당되기 때문에 spec.ports[].nodePort[] 지정도 가능하다. 할당된 가상 IP는 쿠버네티스 노드에 분산되기 때문에 쿠버네티스 노드를 스케일링할 때 변경하지 않아도 된다. 그리고 클러스터 외부에서 Load Balancing(ELB)에 할당된 가상 IP를 통해 통신이 가능하다. 설정 항목은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;spec.ports[].port:&lt;/b&gt; LoadBalancer에 할당되는 가상 IP와 ClusterIP에서 수신할 포트 번호 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;spec.ports[].targetPort:&lt;/b&gt; 목적지 컨테이너 포트 번호 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;spec.ports[].nodePort:&lt;/b&gt; 모든 쿠버네티스 워커 노드 IP 주소에서 수신할 포트 번호 지정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;NodePort도 자동 할당되므로&amp;nbsp;&lt;/span&gt;지정하지 않는 경우 랜덤 포트 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2386&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxFBFo/btsPxiP6CvL/Ls3HXyff9xYe2P9W8MOAM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxFBFo/btsPxiP6CvL/Ls3HXyff9xYe2P9W8MOAM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxFBFo/btsPxiP6CvL/Ls3HXyff9xYe2P9W8MOAM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxFBFo%2FbtsPxiP6CvL%2FLs3HXyff9xYe2P9W8MOAM0%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;2386&quot; height=&quot;554&quot; data-origin-width=&quot;2386&quot; data-origin-height=&quot;554&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;2436&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yRU2V/btsPwYEsK8X/32uNi0VFXh9Xk1a4emyVfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yRU2V/btsPwYEsK8X/32uNi0VFXh9Xk1a4emyVfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yRU2V/btsPwYEsK8X/32uNi0VFXh9Xk1a4emyVfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyRU2V%2FbtsPwYEsK8X%2F32uNi0VFXh9Xk1a4emyVfK%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;2436&quot; height=&quot;190&quot; data-origin-width=&quot;2436&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;watch -d kubectl get pod,svc,ep&lt;/b&gt; 명령어를 사용하여 LoadBalancer 서비스와 엔드포인트가 생성된 것을 확인할 수 있다. sample-lb 엔드포인트의 IP 주소 3개는 실제 트래픽이 전달될 파드 IP 주소이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-23 오후 11.10.10.png&quot; data-origin-width=&quot;3374&quot; data-origin-height=&quot;1614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEsLav/btsPvUJ6iQZ/g5Q9B0QWIkmrz6PIMYm3vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEsLav/btsPvUJ6iQZ/g5Q9B0QWIkmrz6PIMYm3vK/img.png&quot; data-alt=&quot;로드 밸런서&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEsLav/btsPvUJ6iQZ/g5Q9B0QWIkmrz6PIMYm3vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEsLav%2FbtsPvUJ6iQZ%2Fg5Q9B0QWIkmrz6PIMYm3vK%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;3374&quot; height=&quot;1614&quot; data-filename=&quot;스크린샷 2025-07-23 오후 11.10.10.png&quot; data-origin-width=&quot;3374&quot; data-origin-height=&quot;1614&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로드 밸런서&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-23 오후 11.10.35.png&quot; data-origin-width=&quot;3338&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doFWHl/btsPwYdo33z/46pqccaVMihwzyOgFZT191/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doFWHl/btsPwYdo33z/46pqccaVMihwzyOgFZT191/img.png&quot; data-alt=&quot;로드 밸런서 - 대상 인스턴스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doFWHl/btsPwYdo33z/46pqccaVMihwzyOgFZT191/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoFWHl%2FbtsPwYdo33z%2F46pqccaVMihwzyOgFZT191%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;3338&quot; height=&quot;768&quot; data-filename=&quot;스크린샷 2025-07-23 오후 11.10.35.png&quot; data-origin-width=&quot;3338&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로드 밸런서 - 대상 인스턴스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 LoadBalancer 서비스를 생성하면 AWS에서&lt;b&gt; CNB가 생성&lt;/b&gt;된 것을 확인할 수 있다. 리스터는 외부로부터 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;TCP:8080을 받아서 각 내부 서브넷에 등록된 인스턴스의 TCP:30000 포트로 포워딩&lt;/b&gt;&lt;/span&gt;한다. 그러면 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;파드 내 컨테이너가 리스닝 중인 실제 80번 포트로 라우팅하여 트래픽이 전달&lt;/b&gt;&lt;/span&gt;되는 과정을 거치게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, AWS에서는 이제 CLB보다는 ALB와 NLB로 목적에 맞게 구분해서 사용하는 것을 권장하고, 이를 위해서는 &lt;b&gt;AWS Load Balancer Controller&lt;/b&gt;를 설치하여 Kubernetes 리소스(Service, Ingress 등)와 연동해 자동으로 ALB 또는 LNB를 프로비저닝하고 관리할 수 있도록 구성해야 한다.&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;AWS Load Balancer Controller&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon Load Balancer Controller는 Kubernetes 클러스터에서에서 AWS ELB를 관리하도록 도와주는 Add-on 도구(&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;클러스터를 운영하는데 필수적인 기본 컴포넌트들(coredns, kube-proxy, vpc-cni 등)을 Amazon EKS가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;관리형 서비스 형태로 제공&lt;/b&gt;하는 기능)로 크게 두 가지 기능을 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Kuberenetes Ingress 리소스에서 Application Load Balancer를 프로비저닝할 수 있도록 만들어준다.&lt;/li&gt;
&lt;li&gt;Kubernetes Service 리소스에서 Network Load Balancer를 프로비저닝할 수 있도록 만들어준다.&lt;/li&gt;
&lt;/ol&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-07-24 오후 8.02.37.png&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSlkyB/btsPy2fwuKl/ljpDYalPB1J2J2dTiiF0z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSlkyB/btsPy2fwuKl/ljpDYalPB1J2J2dTiiF0z0/img.png&quot; data-alt=&quot;출처: https://www.inflearn.com/course/amazon-eks-%EA%B8%B0%EB%B3%B8-%EA%B0%95%EC%9D%98/dashboard&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSlkyB/btsPy2fwuKl/ljpDYalPB1J2J2dTiiF0z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSlkyB%2FbtsPy2fwuKl%2FljpDYalPB1J2J2dTiiF0z0%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;1732&quot; height=&quot;602&quot; data-filename=&quot;스크린샷 2025-07-24 오후 8.02.37.png&quot; data-origin-width=&quot;1732&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.inflearn.com/course/amazon-eks-%EA%B8%B0%EB%B3%B8-%EA%B0%95%EC%9D%98/dashboard&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터에 AWS Load Balancer Controller를 설치하면 컨트롤 플레인과 상호 작용하면서 &lt;b&gt;파드 정보를 확인하고, 이벤트를 모니터링&lt;/b&gt;하여 클러스터 내부에 어떤 파드가 있는지를 파악한다. 이 정보를 가지고 AWS ELB의 &lt;b&gt;대상 그룹(Target Group) 내 파드를 등록하거나 제거하는 작업을 수행&lt;/b&gt;한다. 그러면 ELB는 대상 그룹에 파드를 직접 매핑해서 중간 단계(NodePort, ClusterIP)를 거치지 않고 바로 파드로 통신할 수 있어 conntrack이나 iptables에 의한 처리 과정이 생략되고, 효율적인 통신이 가능하다. 이러한 동작이 가능한 이유는 노드와 파드가 같은 VPC IP 대역을 사용하고 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;IRSA 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754283714141&quot; class=&quot;dsconfig&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# IAM Policy json 파일 다운로드
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/refs/heads/main/docs/install/iam_policy.json

# AWSLoadbBalancerControllerIAMPolicy 생성
aws iam create-policy \
  --policy-name AWSLoadBalancerControllerIAMPolicy \
  --policy-document file://iam_policy.json
  
  # IRSA 생성 - 위에서 만든 IAM 정책을 연결
  eksctl create iamserviceaccount \
  --cluster=$CLUSTER_NAME \
  --namespace=kube-system \
  --name=aws-load-balancer-controller \
  --attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
  --override-existing-serviceaccounts \
  --approve
  
  # IRSA 정보 확인
  eksctl get iamserviceaccount \
  --cluster $CLUSTER_NAME
  
  # Kubernetes 서비스 어카운트 확인
  kubectl get serviceaccounts \
  -n kube-system aws-load-balancer-controller \
  -o yaml | yh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Load Balancer가 ALB 또는 NLB와 같은 ELB 리소스를 제어하려면 AWS API를 호출할 수 있는 적절한 IAM 권한이 필요하다. 이를 위해 Kubernetes의 ServiceAccount와 IAM Role을 연결하는 IRSA(IAM Role for Service Account) 방식을 사용한다. IRSA는 클러스터 내 리소스가 AWS 리소스에 안전하게 접근할 수 있도록 도와주는 메커니즘으로 이를 위해 EKS 클러스터에는 OIDC(OpenID Connect) 공급자가 설정되어 있어야 한다. 이 OIDC 공급자는 Kubernetes에서 발급한 서비스 계정의 토큰을 신뢰할 수 있도록 AWS가 검증하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oneny.tistory.com/134&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS CloudFormation으로 EKS 클러스터&lt;/a&gt; 블로그 글을 통해 EKS 클러스터를 생성한 경우, 해당 템플릿에서 OIDC 공급자도 함께 설정되어 있다. 이때 IRSA를 완성하기 위해 OIDC 공급자와 함께 AWS Load Balancer Controller에 필요한 IAM 정책을 연결한 IAM Role을 생성하고, 이를 컨트롤러의 ServiceAccount에 바인딩해야 한다. 이 과정을 통해 Load Balancer Controller는 쿠버네티스 환경 안에서 IAM 권한을 위임받아 안전하게 AWS 리소스를 제어할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wK7EK/btsPHlflBHE/6mPf32CUUI43nn41ZyXO7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wK7EK/btsPHlflBHE/6mPf32CUUI43nn41ZyXO7K/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2748&quot; data-origin-height=&quot;1806&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.25.02.png&quot; style=&quot;width: 39.3072%; margin-right: 10px;&quot; data-widthpercent=&quot;39.77&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wK7EK/btsPHlflBHE/6mPf32CUUI43nn41ZyXO7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwK7EK%2FbtsPHlflBHE%2F6mPf32CUUI43nn41ZyXO7K%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;2748&quot; height=&quot;1806&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTDyIV/btsPFXlDPfr/gY1pM6yRaqdhuFkGZpWgC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTDyIV/btsPFXlDPfr/gY1pM6yRaqdhuFkGZpWgC1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2710&quot; data-origin-height=&quot;1176&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.27.07.png&quot; style=&quot;width: 59.53%;&quot; data-widthpercent=&quot;60.23&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTDyIV/btsPFXlDPfr/gY1pM6yRaqdhuFkGZpWgC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTDyIV%2FbtsPFXlDPfr%2FgY1pM6yRaqdhuFkGZpWgC1%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;2710&quot; height=&quot;1176&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;AWS LoadBalancerControllerIAMPolicy 권한 / IRSA의 IAM 역할&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진에서 정의된 권한을 보면 EC2나 ELB 리소르를 읽고 쓰는 권한 등이 포함되어 있는 것을 확인할 수 있다. 이런 IAM 정책을 AWS Load Balancer Controller의 권한에 위힘하면 컨트롤러가 필요한 리소스를 직접 생성하거나 관리할 수 있다. 그리고 생성된 IRSA에서도 IAM 역할과 연결된 IAM 정책을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;AWS Load Balancer Controller 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754286637161&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Helm Chart Repository 추가 및 업데이트
helm repo add eks https://aws.github.io/eks-charts
helm repo update

# AWS Load Balancer Controller 설치
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.create=false \
  --set serviceAccount.name=aws-load-balancer-controller
  
# AWS Load Balancer Controller 확인
kubectl get deployment -n kube-system aws-load-balancer-controller

kubectl describe deploy -n kube-system aws-load-balancer-controller&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;helm install 명령어를 사용해 AWS Load Balancer Controller를 설치한다. 설치할 때 클러스터 이름을 지정하고, 서비스 어카운트 생성을 비활성화하고, 대신 위에서 생성해둔 IRSA를 직정 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Service NLB 배포&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754287242616&quot; class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: akos-websrv
        image: k8s.gcr.io/echoserver:1.5
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: svc-nlb-ip-type
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: &quot;8080&quot;
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: &quot;true&quot;
spec:
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
  selector:
    app: deploy-websrv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl apply를 통해 Deployment를 통해 파드 2대를 생성하고, 서비스의 LoadBalancer 타입으로 NLB를 생성한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754287263108&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Pod의 IP 확인
kubectl get pod -owide

# NLB에 연결된 타겟그룹 바인딩 정보 확인
kubectl get targetgroupbindings&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;2342&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k9Gzo/btsPFtSJP1U/sKwv7wmIaNFUs1IKvT9Xo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k9Gzo/btsPFtSJP1U/sKwv7wmIaNFUs1IKvT9Xo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k9Gzo/btsPFtSJP1U/sKwv7wmIaNFUs1IKvT9Xo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk9Gzo%2FbtsPFtSJP1U%2FsKwv7wmIaNFUs1IKvT9Xo0%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;2342&quot; height=&quot;268&quot; data-origin-width=&quot;2342&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 명령어를 통해 파드 두 개와 NLB에 연결된 타겟그룹 바인딩 정보를 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;AWS 리소스 확인&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.56.20.png&quot; data-origin-width=&quot;2944&quot; data-origin-height=&quot;1546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DRfit/btsPFhkLUR5/mYMA4bDXrXaW1yEo7JKDKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DRfit/btsPFhkLUR5/mYMA4bDXrXaW1yEo7JKDKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DRfit/btsPFhkLUR5/mYMA4bDXrXaW1yEo7JKDKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDRfit%2FbtsPFhkLUR5%2FmYMA4bDXrXaW1yEo7JKDKk%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;2944&quot; height=&quot;1546&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.56.20.png&quot; data-origin-width=&quot;2944&quot; data-origin-height=&quot;1546&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.56.57.png&quot; data-origin-width=&quot;2960&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mcYeR/btsPE883lbe/SLsSAOKPJhkHK9SaTnKQkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mcYeR/btsPE883lbe/SLsSAOKPJhkHK9SaTnKQkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mcYeR/btsPE883lbe/SLsSAOKPJhkHK9SaTnKQkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmcYeR%2FbtsPE883lbe%2FSLsSAOKPJhkHK9SaTnKQkk%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;2960&quot; height=&quot;1100&quot; data-filename=&quot;스크린샷 2025-08-04 오후 2.56.57.png&quot; data-origin-width=&quot;2960&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AWS의 로드 밸런서를 확인하면 AWS Load Balancer Controller가 자동으로 NLB가 생성하고, TCP 80 포트가 열려있는 것을 확인할 수 있다. 그리고 연결된 대상 그룹 정보와 TargetGroupBindings 정보가 일치한 것을 확인할 수 있다. 마지막으로 등록된 대상 IP가 파드의 IP와 일치한 것도 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인그레스(Ingress)&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-23 오후 9.10.03.png&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL2fIn/btsPwIV7JyX/8sunFO0RVVba2n5qvZu5uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL2fIn/btsPwIV7JyX/8sunFO0RVVba2n5qvZu5uk/img.png&quot; data-alt=&quot;출처: https://www.inflearn.com/course/amazon-eks-%EA%B8%B0%EB%B3%B8-%EA%B0%95%EC%9D%98/dashboard&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL2fIn/btsPwIV7JyX/8sunFO0RVVba2n5qvZu5uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL2fIn%2FbtsPwIV7JyX%2F8sunFO0RVVba2n5qvZu5uk%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;1700&quot; height=&quot;500&quot; data-filename=&quot;스크린샷 2025-07-23 오후 9.10.03.png&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://www.inflearn.com/course/amazon-eks-%EA%B8%B0%EB%B3%B8-%EA%B0%95%EC%9D%98/dashboard&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인그레스는 L7 로드 밸런싱을 제공하는 리소스로, 클러스터 외부에서 내부 서비스로 &lt;b&gt;HTTP와 HTTPS 경로를 외부에 노출&lt;/b&gt;하는데 사용할 수 있다. AWS 환경에서 보면 &lt;b&gt;ALB에 대응&lt;/b&gt;된다고 할 수 있다. &lt;b&gt;Ingress를 활용하면 HTTP나 HTTPS 기반의 URL 라우팅이 가능해지기 때문에 하나의 로드 밸런서로 여러 서비스를 처리&lt;/b&gt;할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림처럼 K8S의 Service와 Ingress는 공통적으로 NodePort를 통해 인입되고, ClusterIP를 통해 파드를 분산한다. 이 과정에서 리눅스 커널의 conntrack과 iptables가 사용하여 CPU나 메모리 자원을 소모된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/amazon-eks-%EA%B8%B0%EB%B3%B8-%EA%B0%95%EC%9D%98/dashboard&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CloudNet@와 함께하는 Amazon EKS 기본 강의&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nauco.tistory.com/89&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;EKS에 CLB, NLB, ALB 만들기, AWS Load Balancer Controller 사용&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/lbc-helm.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Helm을 사용하여 AWS Load Balancer Controller 설치&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/102847901&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;쿠버네티스 완벽 가이드&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기타/AWS</category>
      <category>EKS NLB</category>
      <category>EKS Service</category>
      <category>k8s service</category>
      <author>oneny</author>
      <guid isPermaLink="true">https://oneny.tistory.com/135</guid>
      <comments>https://oneny.tistory.com/135#entry135comment</comments>
      <pubDate>Thu, 24 Jul 2025 21:00:45 +0900</pubDate>
    </item>
  </channel>
</rss>