<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>hola 개발</title>
    <link>https://do-it-zero.tistory.com/</link>
    <description>개발 계발!</description>
    <language>ko</language>
    <pubDate>Thu, 11 Jun 2026 12:00:34 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>hola.</managingEditor>
    <image>
      <title>hola 개발</title>
      <url>https://tistory1.daumcdn.net/tistory/7363575/attach/e068dd5e3c6446cea01622871aca1a3f</url>
      <link>https://do-it-zero.tistory.com</link>
    </image>
    <item>
      <title>[ 선착순 쿠폰 발급 시스템 ] 10,000건 동시 요청 시 latency를 47초에서 2초로 개선하기</title>
      <link>https://do-it-zero.tistory.com/76</link>
      <description>&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;&amp;nbsp;지난 글에서는 GCP 서버 한대로 10,000건의 동시 요청을 처리하도록 만들었습니다. 하지만 latency가 너무 느렸기 때문에 이번 글은 latency를 개선하는 과정이 되겠습니다.&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;[ Redis 도입 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;현재 시스템은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;8&quot; data-path-to-node=&quot;3&quot;&gt;요청당 하나의 워커 쓰레드(Worker Thread)를 할당&lt;/b&gt;하는 구조입니다. 이 방식은 DB 쿼리 실행 시 해당 쓰레드가 응답을 받을 때까지 멈춰있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;103&quot; data-path-to-node=&quot;3&quot;&gt;블로킹(Blocking) 발생하여&lt;/b&gt; latency가 느려지는 가장 큰 원인입니다. 설정된 200개의 쓰레드가 모두 DB 응답을 기다리며 묶이게 되면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;maxConnections를 늘려 연결에 성공한 201번째 이후의 요청들은 일할 쓰레드를 할당받지 못해 대기하게 되고, 결과적으로 시스템 전체의&lt;b&gt;&lt;span&gt; l&lt;/span&gt;atency이 급격히 증가&lt;/b&gt;하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이를 해결하기 위해 DB 요청 부분을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b data-index-in-node=&quot;21&quot; data-path-to-node=&quot;4&quot;&gt;비동기 처리&lt;/b&gt;하여 쓰레드의 점유 시간을 최소화하는 방안을 생각하였습니다. 특히 쿠폰 발급 여부 확인과 잔여 수량 차감이라는 핵심 로직을 반드시 DB에서 처리할 필요가 없다고 생각하였고, DB 대신 메모리 기반으로 동작하며&lt;b&gt;&lt;span&gt; ra&lt;/span&gt;ce condition을 원자적으로 제어할 수 있는 Redis의 Lua 스크립트&lt;/b&gt;를 도입을 하였습니다. Redis가 앞단에서 발급 가능 여부를 빠르게 검증하고 수량을 먼저 차감한 뒤, 실제 DB 저장은 비동기로 처리함으로써 응답 속도를 개선하고자 하고자 하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 아래는 앞서 정의한 아키텍처를 기준으로 주요 유스케이스의 처리를 나타낸 시퀀스 다이어그램입니다.&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-05-03 오전 12.49.42.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMgaM7/dJMcag6rA0d/VwWe8LlCbGzcE8emmyN60K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMgaM7/dJMcag6rA0d/VwWe8LlCbGzcE8emmyN60K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMgaM7/dJMcag6rA0d/VwWe8LlCbGzcE8emmyN60K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMgaM7%2FdJMcag6rA0d%2FVwWe8LlCbGzcE8emmyN60K%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;1514&quot; height=&quot;352&quot; data-filename=&quot;스크린샷 2026-05-03 오전 12.49.42.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;# Service&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777138222284&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public String issueCouponRedis(IssueCouponDto issueCouponDto) {

        /** 반환 값에 따른 의미
         *  1 : 쿠폰 발급 성공
         * -1 : 이미 발급된 유저
         * -2 : 쿠폰 소진
         * */
        String script = &quot;&quot;&quot;
                if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
                    return -1
                end
                local count = tonumber(redis.call('GET', KEYS[1]))
                if not count or count &amp;lt;= 0 then
                    return -2
                end
                redis.call('DECR', KEYS[1])
                redis.call('SADD', KEYS[2], ARGV[1])
                return 1
            &quot;&quot;&quot;;

        Long result = redisTemplate.execute(
                new DefaultRedisScript&amp;lt;&amp;gt;(script,Long.class),
                List.of(&quot;coupons&quot;,&quot;issued_users&quot;),
                issueCouponDto.getUserId()
        );

        if(result &amp;gt; 0) {
            // 쿠폰발급매니저가 가진 queue에 넘겨주기
            manager.asyncIssueCouponRedis(issueCouponDto);
            return &quot;쿠폰 발급 성공&quot;;
        } else if(result == -1) {
            return &quot;이미 발급됨&quot;;
        } else {
            return &quot;발급 실패&quot;;
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;# AsyncCouponIssueManager&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777139631143&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    private final BlockingQueue&amp;lt;IssueCouponDto&amp;gt; queue = new LinkedBlockingQueue&amp;lt;&amp;gt;(10000);
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    private final CouponRepository couponRepository;
    private final JdbcTemplate jdbcTemplate;
    
    @PostConstruct
    public void start() {
        executor.submit(this::processBatch);
    }

    private void processBatch() {
        log.info(&quot;processBatch 실행&quot;);
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 1. 해당 큐에서 데이터 하나를 대기하며 가져옴
                IssueCouponDto firstElement = queue.poll(1, TimeUnit.SECONDS);
                if (firstElement == null) continue;

                // 2. 나머지 데이터들을 한꺼번에 긁어옴 (Batch 단위)
                List&amp;lt;IssueCouponDto&amp;gt; batch = new ArrayList&amp;lt;&amp;gt;(2000);
                batch.add(firstElement);
                queue.drainTo(batch, 1999);

                // 3. JPA saveAll 대신 JdbcTemplate 실전 Bulk Insert
                String sql = &quot;INSERT INTO coupon_issue_history (user_id, coupon_id, issued_quantity) VALUES (?, ?, 1)&quot;;

                jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
                    @Override
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        ps.setString(1, batch.get(i).getUserId());
                        ps.setString(2, batch.get(i).getCouponId());
                    }
                    @Override
                    public int getBatchSize() {
                        return batch.size();
                    }
                });

                // 4. 재고 차감 (이미 메모리에서 깎았으므로 DB 반영은 한 번에)
                couponRepository.decreaseCounts(batch.get(0).getCouponId(), batch.size());

                log.info(&quot;쿠폰 {}개 차감 실행 완료&quot;,batch.size());

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                log.error(&quot;Batch 처리 중 에러 발생&quot;, e);
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-27 오후 9.32.46.png&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0zLa6/dJMcafTWTCZ/viMV0BAEuGSk38EgLwb1u0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0zLa6/dJMcafTWTCZ/viMV0BAEuGSk38EgLwb1u0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0zLa6/dJMcafTWTCZ/viMV0BAEuGSk38EgLwb1u0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0zLa6%2FdJMcafTWTCZ%2FviMV0BAEuGSk38EgLwb1u0%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;1036&quot; height=&quot;440&quot; data-filename=&quot;스크린샷 2026-04-27 오후 9.32.46.png&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;202&quot; data-start=&quot;141&quot; data-ke-size=&quot;size16&quot;&gt;테스트 결과, 요청의 95% latency가 약 23초에서 4.7초로 크게 개선되었음을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;202&quot; data-start=&quot;141&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;204&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;하지만 여전히 사용자 경험 측면에서는 추가적인 개선이 필요한 수준입니다. 왜냐하면 &lt;span style=&quot;background-color: #ffffff; color: #191d21; text-align: start;&quot;&gt;구글 리서치 자료에 따르면 모바일 웹 사이트의 로딩 시간이 3초 이상일 때 32%, 5초 이상은 90%, 6초 이상은 106% 마지막으로 10초가 넘으면 123%의 이탈률이 발생한다하기 때문입니다.&lt;/span&gt; 따라서사용자 경험을 실질적으로 개선하기 위해 &lt;b&gt;95% latency를 3초 이내로 단축하는 것을 목표로 설정&lt;/b&gt;하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;latency 개선을 위해 가장 먼저 was 스케일 업을 검토하였습니다. 왜냐하면 cpu사용률이 100%를 초과하는 구간이 확인되었고, 이를 통해 cpu가 주요 병목 지점이라고 판단하였기 때문입니다. 이에 따라 cpu 코어를 1개 추가한 후 테스트를 진행하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1. was 스케일 업(코어 1개 추가)&lt;/span&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;스크린샷 2026-04-29 오후 8.58.08.png&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QGp7Q/dJMcacXd2K6/8Pbnk34kvk38fVzBi6x8h0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QGp7Q/dJMcacXd2K6/8Pbnk34kvk38fVzBi6x8h0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QGp7Q/dJMcacXd2K6/8Pbnk34kvk38fVzBi6x8h0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQGp7Q%2FdJMcacXd2K6%2F8Pbnk34kvk38fVzBi6x8h0%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;1048&quot; height=&quot;488&quot; data-filename=&quot;스크린샷 2026-04-29 오후 8.58.08.png&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;410&quot; data-start=&quot;266&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;스케일 업을 통해 latency를 4.7초에서 3.93초까지 단축할 수 있었으나, 목표로 설정한 2초 이내에는 여전히 도달하지 못했습니다. 또한 CPU 사용률이 여전히 높은 수준을 유지하고 있어, 단일 WAS의 성능 개선만으로는 한계가 있다고 판단하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;572&quot; data-start=&quot;412&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;572&quot; data-start=&quot;412&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이에 따라 트래픽을 분산하기 위한 수평 확장(Scale-out)을 적용하기로 하였으며, 앞단에 L4 로드밸런서를 구성하였습니다. 이를 위해 &lt;span&gt;&lt;span&gt;Nginx&lt;/span&gt;&lt;/span&gt;의 stream 모듈을 활용하여 TCP 레벨에서 요청을 분산하도록 구성하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;668&quot; data-start=&quot;574&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;668&quot; data-start=&quot;574&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;해당 방식은 애플리케이션 레벨(L7) 처리 없이 TCP 연결 단에서 트래픽을 분산할 수 있어, 추가적인 처리 오버헤드 없이 빠른 로드밸런싱이 가능하다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;768&quot; data-start=&quot;670&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;768&quot; data-start=&quot;670&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;구성은 Nginx를 L4 로드밸런서로 두고, 스케일 업 전과 동일한 스펙의 was인스턴스를 두 대로 확장하여 트래픽을 분산하도록 하였으며, 해당 환경에서 성능 테스트를 진행하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Nginx L4 로드밸런서 + was 두 대(스케일 업 전)&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;스크린샷 2026-05-06 105438.png&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pEE7u/dJMcadolkI8/QYYaiXA8V7DElyqxhXo7L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pEE7u/dJMcadolkI8/QYYaiXA8V7DElyqxhXo7L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pEE7u/dJMcadolkI8/QYYaiXA8V7DElyqxhXo7L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpEE7u%2FdJMcadolkI8%2FQYYaiXA8V7DElyqxhXo7L1%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;578&quot; height=&quot;353&quot; data-filename=&quot;스크린샷 2026-05-06 105438.png&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-29 오후 9.14.28.png&quot; data-origin-width=&quot;1031&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgPofp/dJMcaipCR0g/3JXmeAiZKRh0vw2wIWOGd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgPofp/dJMcaipCR0g/3JXmeAiZKRh0vw2wIWOGd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgPofp/dJMcaipCR0g/3JXmeAiZKRh0vw2wIWOGd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgPofp%2FdJMcaipCR0g%2F3JXmeAiZKRh0vw2wIWOGd0%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;1031&quot; height=&quot;446&quot; data-filename=&quot;스크린샷 2026-04-29 오후 9.14.28.png&quot; data-origin-width=&quot;1031&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;212&quot; data-start=&quot;152&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;하지만 테스트 결과, latency가 3.93초에서 4.04초로 오히려 소폭 증가하는 현상이 확인되었습니다.&lt;/p&gt;
&lt;p data-end=&quot;377&quot; data-start=&quot;214&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;377&quot; data-start=&quot;214&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;스케일 아웃 이후에도 성능이 개선되지 않고 오히려 악화된 점을 고려했을 때, was가 아닌 앞단의 로드밸런서 구간에서 병목이 발생하고 있을 가능성을 의심하였습니다. 특히 트래픽이 증가한 상황에서 Nginx를 통한 TCP 레벨 분산 과정에서 추가적인 오버헤드가 발생할 수 있다고 판단하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;488&quot; data-start=&quot;379&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;488&quot; data-start=&quot;379&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이에 따라 로드밸런서 자체의 처리 성능이 영향을 미치는지 확인하기 위해 &lt;span&gt;&lt;span&gt;Nginx&lt;/span&gt;&lt;/span&gt; 인스턴스를 스케일 업하여 테스트를 추가로 수행하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Nginx 스케일 업(core 1개 추가)&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;스크린샷 2026-04-29 오후 9.51.38.png&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nXMKJ/dJMcacppwyd/N1AL1hSBDmChlgFkQ1VjmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nXMKJ/dJMcacppwyd/N1AL1hSBDmChlgFkQ1VjmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nXMKJ/dJMcacppwyd/N1AL1hSBDmChlgFkQ1VjmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnXMKJ%2FdJMcacppwyd%2FN1AL1hSBDmChlgFkQ1VjmK%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;1020&quot; height=&quot;441&quot; data-filename=&quot;스크린샷 2026-04-29 오후 9.51.38.png&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;235&quot; data-start=&quot;134&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;Nginx&lt;/span&gt;&lt;/span&gt; 스케일 업 이후 latency는 3.31초까지 개선되었으나, 여전히 목표치인 3초 이내에는 도달하지 못하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;313&quot; data-start=&quot;237&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;313&quot; data-start=&quot;237&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이에 따라 로드밸런서 단의 최적화 효과는 제한적이라고 판단하였고, 남아있는 병목이 WAS 처리 성능에 있을 가능성을 다시 검토하였습니다. 따라서 was는 기존과 동일하게 2대 구성을 유지한 상태에서, 각 인스턴스의 CPU 코어를 1개씩 추가하여 스케일 업을 수행한 뒤 성능 테스트를 진행하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Nginx &amp;amp; was 2대 스케일 업(각각 core 1개씩 추가)&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;스크린샷 2026-05-03 오전 12.43.08.png&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2NkQE/dJMcaiJUAse/BKcMIcd3Ec8gK0ukoYBPO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2NkQE/dJMcaiJUAse/BKcMIcd3Ec8gK0ukoYBPO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2NkQE/dJMcaiJUAse/BKcMIcd3Ec8gK0ukoYBPO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2NkQE%2FdJMcaiJUAse%2FBKcMIcd3Ec8gK0ukoYBPO1%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;2102&quot; height=&quot;862&quot; data-filename=&quot;스크린샷 2026-05-03 오전 12.43.08.png&quot; data-origin-width=&quot;2102&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;106&quot; data-start=&quot;59&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;드디어 95% latency는 2.08초까지 개선되는 결과를 얻을 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;272&quot; data-start=&quot;108&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;272&quot; data-start=&quot;108&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;추가적으로 WAS 확장 및 캐시 계층 스케일 업 등을 통해 더 많은 성능 개선 여지를 검증하고자 하였으나, &lt;span&gt;&lt;span&gt;Google&lt;/span&gt;&lt;/span&gt; Cloud 무료 크레딧 환경에서 제공되는 CPU quota 제한으로 인해 추가적인 부하 테스트는 진행하지 못하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;375&quot; data-start=&quot;274&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;375&quot; data-start=&quot;274&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그럼에도 불구하고, 여러 단계의 구조 개선과 스케일 조정을 통해 latency를 초기 대비 큰 폭으로 개선하였으며, 유의미한 성능 개선을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;375&quot; data-start=&quot;274&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;375&quot; data-start=&quot;274&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 회고 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;277&quot; data-start=&quot;139&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;최근 채용 공고들을 보면 &amp;ldquo;대규모 트래픽 처리 경험&amp;rdquo;이 우대 사항으로 자주 등장합니다. 하지만 현재 재직 중인 환경에서는 이러한 규모의 트래픽을 직접 경험하기 어려워, 대규모 트래픽을 어떻게 처리하는지에 대한 궁금증을 지속적으로 가지고 있었습니다. 이번 개인 프로젝트를 통해 대규모 트래픽 처리는 단순히 서버 성능을 높이는 것만으로 해결되는 문제가 아니라는 점을 직접 경험할 수 있었습니다.&lt;/p&gt;
&lt;p data-end=&quot;537&quot; data-start=&quot;360&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;537&quot; data-start=&quot;360&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;2월 초부터 약 3개월 동안 프로젝트에 깊이 몰입하며 성능 개선을 시도했지만, 예상하지 못한 다양한 병목과 문제들을 마주하게 되었습니다. 이 과정에서 여러 번 한계에 부딪히며 포기하고 싶은 순간도 있었지만, 문제는 반드시 해결할 수 있다는 믿음을 바탕으로 지속적으로 학습하고 원인을 분석하며 하나씩 해결해 나갔습니다.&lt;/p&gt;
&lt;p data-end=&quot;602&quot; data-start=&quot;539&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;602&quot; data-start=&quot;539&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;</description>
      <category>프로젝트/대규모 트래픽 - 쿠폰 발급 시스템</category>
      <category>Akamai 연구결과</category>
      <category>Lua 스크립트</category>
      <category>redis</category>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/76</guid>
      <comments>https://do-it-zero.tistory.com/76#entry76comment</comments>
      <pubDate>Wed, 6 May 2026 11:16:39 +0900</pubDate>
    </item>
    <item>
      <title>[ 선착순 쿠폰 발급 시스템 ] GCP 기본 서버 1대로 10,000 동시 요청 처리 해보기(dial timout 처리 방법)</title>
      <link>https://do-it-zero.tistory.com/75</link>
      <description>&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;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;선착순 쿠폰 발급 시, 대규모 트래픽이 동시에 발생하여 다양한 문제가 발생할 수 있습니다. &lt;/span&gt;&lt;span style=&quot;color: #000000; 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;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;[&amp;nbsp; Workflow &amp;amp; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;Architecture&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt; ]&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNCB6L/dJMcahDEVir/AKrg9TUfOGSkpw730hCZ1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNCB6L/dJMcahDEVir/AKrg9TUfOGSkpw730hCZ1K/img.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;353&quot; data-filename=&quot;스크린샷 2026-03-16 150201.png&quot; data-is-animation=&quot;false&quot; width=&quot;421&quot; height=&quot;209&quot; data-widthpercent=&quot;51.78&quot; style=&quot;width: 51.1756%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNCB6L/dJMcahDEVir/AKrg9TUfOGSkpw730hCZ1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNCB6L%2FdJMcahDEVir%2FAKrg9TUfOGSkpw730hCZ1K%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;711&quot; height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FmZoP/dJMcacI9cQr/1CTKU9iiXTWcmpk19qK3Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FmZoP/dJMcacI9cQr/1CTKU9iiXTWcmpk19qK3Tk/img.png&quot; width=&quot;533&quot; height=&quot;284&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;290&quot; data-filename=&quot;blob&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;48.22&quot; style=&quot;width: 47.6616%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FmZoP/dJMcacI9cQr/1CTKU9iiXTWcmpk19qK3Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFmZoP%2FdJMcacI9cQr%2F1CTKU9iiXTWcmpk19qK3Tk%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;544&quot; height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;위와 같은 Workflow이며 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;외장 tomcat 과 Spring Boot , Mysql로 쿠폰 발급 시스템을 만들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;&amp;nbsp;외장 tomcat을 쓴 이유는 jar파일을 만들어 GCP 인스턴스에 배포할 경우, ssh로 접속해서 jar파일을 올려야하는데 이는 시간이 너무 오래걸렸기 때문입니다. 따라서 외장 tomcat을 설치 후 초기 애플리케이션 클래스 파일들을 올린 후 수정이 있을 경우 수정된 클래스 파일을 그 경로에 맞게 배포하는 방식으로 배포 시간을 줄였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 선착순 쿠폰 발급 특성 상&amp;nbsp;대규모 트래픽이 발생하여 동시에 쿠폰 재고 차감 시 정합성 문제가 발생할 수 있기에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿠폰 발급 이력 확인&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr;&lt;/span&gt;&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; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;darr;&lt;/span&gt;&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; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr;&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 data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 과정이 하나의 트랜잭션 안에서 실행하도록 작성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;인프라 구성 및 서버 스펙&lt;/span&gt;&amp;nbsp;]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;이번 테스트에서는 &lt;b&gt;GCP(Google Cloud Platform)&lt;/b&gt;환경에서 &lt;b&gt;VPC 기반 네트워크&lt;/b&gt;를 구성하고, 각 VM 서버는 동일한 사양으로 설정하여 진행하였습니다. 테스트에 사용된 서버 사양은 아래와 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 51.046511%; height: 36px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 38.865546%; height: 18px;&quot;&gt;os&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 78.955013%; height: 18px;&quot;&gt;cpu 및 memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 38.865546%; height: 18px;&quot;&gt;debian&lt;/td&gt;
&lt;td style=&quot;width: 78.955013%; height: 18px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; text-align: start;&quot;&gt;코어 1개 vCPU 2개, 메모리 4GB&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ K6를 활용한 부하테스트 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다양한 테스트 툴과 테스트 방법이 있지만, 아래와 같은 이유로 K6와 Spike 테스트 방법을 선택했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- K6를 사용한 이유&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;K6는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;JavaScript로 테스트 시나리오를 작성할 수 있으며&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;HTTP 기반 웹 서비스에 대해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;동시 사용자(virtual users) 시뮬레이션&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;과 요청 패턴을 쉽게 구성할 수 있기에 테스트 툴로 K6를 사용하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;- Spike 테스트&lt;/span&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;span style=&quot;color: #000000; text-align: start;&quot;&gt;선착순 쿠폰 발급이라는 서비스 특성상,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;특정 순간에 동시 접속 유저가 폭발적으로 증가&lt;/b&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;할 가능성이 높기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이번 테스트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Spike 테스트&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식으로 진행하였습니다.&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;# 부하테스트 진행 하면서 마주친 문제들&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[ dial: i/o timeout ]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;테스트 중 1000번 , 5000번의 동시 요청 시 정합성 문제 없이 100% 쿠폰 발급 성공 하였으나, 10000번의 동시 요청 시 k6로그에 &lt;b&gt;dial: i/o timeout 이 발생&lt;/b&gt;하였고 80% 정도의 쿠폰 발급만 성공&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;아래는 k6 부하테스트 후 결과 일부분을 가져온 것입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; █ TOTAL RESULTS&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;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_total.......:&amp;nbsp;10000&amp;nbsp;&amp;nbsp;162.249583/s&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_succeeded...:&amp;nbsp;83.95%&amp;nbsp;8395&amp;nbsp;out&amp;nbsp;of&amp;nbsp;10000&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_failed......:&amp;nbsp;16.05%&amp;nbsp;1605&amp;nbsp;out&amp;nbsp;of&amp;nbsp;10000&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HTTP&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;http_req_duration..............:&amp;nbsp;avg=27.77s&amp;nbsp;min=0s&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;med=29.67s&amp;nbsp;max=58.99s&amp;nbsp;p(90)=46.93s&amp;nbsp;p(95)=49.04s&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;- dial I/O timeout이란?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;TCP 연결을 시도할 때, &lt;b&gt;정해진 시간 안에 연결(TCP handshake)이 완료되지 않으면 발생하는 에러&lt;/b&gt; 입니다&lt;b&gt;.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;148&quot; data-start=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;이 문제의 원인은 &lt;b&gt;OS의 backlog가 꽉 차서 이후 들어오는 요청을 수락하지 못하는 것 &lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-end=&quot;148&quot; data-start=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;148&quot; data-start=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 backlog가 가득 차게 되었을까요?&lt;/p&gt;
&lt;p data-end=&quot;319&quot; data-start=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청은 아래와 같은 과정을 거치게 됩니다.&lt;/p&gt;
&lt;p data-end=&quot;319&quot; data-start=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;319&quot; data-start=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;Tomcat에서 Acceptor thread가 accept()를 호출&lt;/p&gt;
&lt;p data-end=&quot;319&quot; data-start=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;319&quot; data-start=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;OS backlog에 있는 소켓을 꺼내 JVM의 Connection Queue에 넣습니다.&lt;/p&gt;
&lt;p data-end=&quot;319&quot; data-start=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;319&quot; data-start=&quot;150&quot; data-ke-size=&quot;size16&quot;&gt;Tomcat의 worker thread가 이 Connection Queue에서 소켓을 가져가 요청을 처리&lt;/p&gt;
&lt;p data-end=&quot;536&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;하지만 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;worker thread&lt;/span&gt;가 모두 바쁘면 Connection Queue에 소켓이 계속 쌓이게 되고&lt;/p&gt;
&lt;p data-end=&quot;536&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;536&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;Queue가 포화되면 Acceptor Thread는 더 이상 accept()를 호출하지 않습니다.&lt;/p&gt;
&lt;p data-end=&quot;536&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;이로 인해 OS backlog가 계속 점유되어 새로운 연결을 받을 수 없게 되고, 결국 클라이언트에서 I/O timeout(dial timeout)이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 먼저는 worker thread가 db요청 처리하는 부분에 병목이 생길 수 있을 것 같다고 예상하여, spring 로그를 확인해보니 아래와 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection&amp;nbsp;not&amp;nbsp;added,&amp;nbsp;stats&amp;nbsp;(total=10/10,&amp;nbsp;idle=0/10,&amp;nbsp;active=10,&amp;nbsp;waiting=190)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;현재 tomcat의 db connection pool은 최대 10개였으며, 모든 커넥션이 사용 중인 상태에서 190개의 worker thread가 db connection을 얻기 위해 대기하고 있었습니다. 이 상황을 파악하고 db cpu 사용률을 보니 약 20% 정도였습니다. db cpu 사용률이 낮으므로 더 많은 요청을 처리할 수 있다고 판단하여, &lt;b&gt;tomcat의 db connection pool를 최소 100, 최대 200으로 조정&lt;/b&gt;하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;문제 해결 시도 #1-1. tomcat의 db connection pool 사이즈 조정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;█&amp;nbsp;TOTAL&amp;nbsp;RESULTS&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_total.......:&amp;nbsp;10000&amp;nbsp;&amp;nbsp;163.178115/s&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_succeeded...:&amp;nbsp;82.97%&amp;nbsp;8297&amp;nbsp;out&amp;nbsp;of&amp;nbsp;10000&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_failed......: 17.03% 1703 out of 10000&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HTTP&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;http_req_duration..............: avg=28.77s min=0s&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;med=30.02s max=58.98s p(90)=52.34s p(95)=54.19s&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;waiting 로그는 사라졌지만, 큰 변화는 없었습니다. 또한 아래와 같은 로그가 생기기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java.sql.SQLNonTransientConnectionException:&amp;nbsp;Connection&amp;nbsp;exception,&amp;nbsp;SQL-server&amp;nbsp;rejected&amp;nbsp;establishment&amp;nbsp;of&amp;nbsp;SQL-connection,&amp;nbsp;&amp;nbsp;message&amp;nbsp;from&amp;nbsp;server:&amp;nbsp;&quot;Too&amp;nbsp;many&amp;nbsp;connections&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 예외는 db의 기본 connection보다 많은 connection 요청이 발생하여 생긴 예외입니다. connection 요청이 너무 많아서 cpu에서 처리하지 못하는 것일까 싶어 db cpu 사용률을 확인해보니 대략 20% 정도였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;db cpu 사용률 여유가 있으므로 &lt;b&gt;db의&amp;nbsp; maximum connection을 151 &amp;rarr; 250으로 조정&lt;/b&gt;하여 테스트를 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;문제 해결 시도 #1-2. db의 maximum connection 사이즈 조정&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;█&amp;nbsp;TOTAL&amp;nbsp;RESULTS&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_total.......:&amp;nbsp;10000&amp;nbsp;&amp;nbsp;175.586703/s&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_succeeded...:&amp;nbsp;83.01%&amp;nbsp;8301&amp;nbsp;out&amp;nbsp;of&amp;nbsp;10000&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_failed......:&amp;nbsp;16.98%&amp;nbsp;1699&amp;nbsp;out&amp;nbsp;of&amp;nbsp;10000&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HTTP&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;http_req_duration..............:&amp;nbsp;avg=26.29s&amp;nbsp;min=0s&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;med=29.54s&amp;nbsp;max=54.05s&amp;nbsp;p(90)=45.46s&amp;nbsp;p(95)=47.33s&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 결과 Too many connections 예외는 사라지고 db cpu 사용률 약 5% 상승했습니다. 하지만 전체적으로 결과는 기존과 크게 다르지 않았으며 여전히 dial i/o timeout이 발생했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;문제 해결 시도 #1-3. 조건부 update로 로직 변경&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 테스트들 결과를 보면 평균 latency는 대부분 26~28초 이며 전체 요청 중 90%는 45.46초 이하인 매우 느린 결과입니다. 그 이유는 쿠폰 발급 로직이라 추측을 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존 쿠폰 발급 로직에서 db 요청 처리하는 과정은 아래와 같습니다.&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;쿠폰 발급 이력 확인&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&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; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;darr;&lt;/span&gt;&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; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿠폰 발급 내역 저장&lt;/p&gt;
&lt;pre id=&quot;code_1777126589741&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Transactional
    public String issueCouponPes(IssueCouponDto issueCouponDto) {
        // 쿠폰 발급 이력 확인
        boolean isIssued = historyRepository.existsByCouponIdAndUserId(issueCouponDto.getCouponId(), issueCouponDto.getUserId());

        if(isIssued){
            return &quot;이미 쿠폰이 발급 되었습니다.&quot;;
        } else {
            // findByIdsPes 시점부터 락이 걸림
            return couponRepository.findByIdPes(issueCouponDto.getCouponId())
                    .filter(Coupon::hasStock)
                    .map(coupon -&amp;gt; {
                        int updated = couponRepository.decreaseStock(coupon.getCouponId());

                        if (updated == 1) {
                            CouponIssueHistory history = CouponIssueHistory.builder()
                                    .couponId(issueCouponDto.getCouponId())
                                    .userId(issueCouponDto.getUserId())
                                    .build();
                            historyRepository.save(history);

                            return &quot;쿠폰 발급 완료&quot;;
                        }
                        return &quot;쿠폰 발급 실패&quot;;
                    })
                    .orElse(&quot;쿠폰 발급 실패&quot;);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; 위와 같은 경우 발급 여부 및 재고 확인을 위해 요청 수 만큼 select 쿼리를 수행하고 각 요청마다 락 점유 시간이 쌓여서 전체 응답 시간 자체가 느려진다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 정합성에서도 문제가 발생했었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp;이를 개선하기 위해 조건부 update를 사용하여, &lt;b&gt;재고 확인과 차감을 하나의 쿼리로 처리&lt;/b&gt;하고 &lt;b&gt;발급 이력은 unique제약을 활용한 insert로 검증&lt;/b&gt;하도록 변경하였습니다. &amp;nbsp;따라서 db에 쿼리를 요청하는 횟수가 4번에 2번으로 줄어들게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 로직은 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠폰 재고 조건부 차감 (update)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr;&lt;br /&gt;쿠폰 발급 이력 insert (userId,couponId를 unique 제약으로 중복 방지)&lt;/p&gt;
&lt;pre id=&quot;code_1777127009137&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Transactional
    public String issueCouponV2(IssueCouponDto dto){

        int update = couponRepository.decreaseStock(dto.getCouponId());

        if(update == 0){
            return &quot;쿠폰 재고 부족&quot;;
        }

        int result = historyRepository.insertIgnore(dto.getCouponId(), dto.getUserId());

        if(result &amp;lt;= 0){
            // 재고 없음 &amp;rarr; 롤백
            return &quot;쿠폰 이미 발급 됨&quot;;
        }
        return &quot;쿠폰 발급 성공&quot;;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후 테스트 결과는 아래와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&amp;nbsp;█&amp;nbsp;TOTAL&amp;nbsp;RESULTS&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_total.......:&amp;nbsp;10000&amp;nbsp;&amp;nbsp;161.530052/s&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_succeeded...:&amp;nbsp;86.26%&amp;nbsp;8626&amp;nbsp;out&amp;nbsp;of&amp;nbsp;10000&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_failed......: 13.74% 1374 out of 10000&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HTTP&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;http_req_duration..............:&amp;nbsp;avg=16.62s&amp;nbsp;min=0s&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;med=16.6s&amp;nbsp;&amp;nbsp;max=58.99s&amp;nbsp;p(90)=24.59s&amp;nbsp;p(95)=26.89s&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 latency가 26초에서 16초로, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;전체 요청 중 90%는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 45초에서 24초로 처리 속도가 향상 되었고 정합성 문제 또한 해결이 되었습니다. 하지만 여전히 dial i/o timeout 발생하고 있었습니다.&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;문제 해결 시도 #1-4. tomcat의 maxConnections 조정&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;156&quot; data-start=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;지금까지는 tomcat의 worker thread 작업 처리 시간이 길어져 OS backlog에서 요청을 거부한 것이라고 추정했으나, 작업 처리 시간을 개선해도 timeout은 여전히 발생했습니다.&lt;/p&gt;
&lt;p data-end=&quot;156&quot; data-start=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;292&quot; data-start=&quot;158&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;작업 처리 시간을 개선했음에도 공통적으로 나타나는 현상이 있었는데, &lt;b&gt;latency의 max 값이 일정하게 나타난다는 것&lt;/b&gt;이었습니다. latency max 값은 테스트 동안 관찰된 가장 느린 요청의 응답 시간을 의미하는데, 이를 힌트 삼아 아래와 같이 추측을 하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;tomcat JVM에 생성되는 socket 갯수는 tomcat의 server.xml에 maxConnections으로 정할 수 있으며 기본값은 8192개 입니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 테스트 현상을 보면 K6에서 약 8,100개 요청까지 가면 멈추는 현상이 공통적으로 있었습니다. 이는 JVM에&amp;nbsp; socket 객체 수가 maxConnections 만큼 생성되었고, 나머지 요청은 Acceptor가 accept 를 하지 못해서 발생한 것이라고 예측할 수 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;예측이 맞는지 확인하기 위해 was서버에서 socket이 얼마나 생성되는지 확인하였습니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;먼저 was 서버를 모니터링 하기 위한 스크립트 입니다.&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;while true;&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;do &lt;span&gt;&amp;nbsp; &lt;/span&gt;clear; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;date;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;echo &quot;=====================================================&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;echo &quot; 1. LISTEN QUEUE (Backlog Check - 8080)&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;echo &quot;=====================================================&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp; &lt;/span&gt;printf &quot;%-10s %-10s %-10s\n&quot; &quot;PORT&quot; &quot;Recv-Q&quot; &quot;Send-Q&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; ss -lnt | grep :8080 | awk '{printf &quot;%-10s %-10s %-10s\n&quot;, $4, $2, $3}';&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;=====================================================&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot; 2. SYSTEM WIDE SUMMARY (ss -s)&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;=====================================================&quot;; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;ss -s | grep -E &quot;Total:|TCP:&quot;;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;=====================================================&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot; 3. DETAILED STATE (PORT 8080)&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;=====================================================&quot;;&lt;span&gt;&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;EST=$(ss -ant | grep :8080 | grep -c ESTAB); &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;SYN=$(ss -ant | grep :8080 | grep -c SYN-RECV); &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;TIME_WAIT=$(ss -ant | grep :8080 | grep -c TIME-WAIT); &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;CLOSE_WAIT=$(ss -ant | grep :8080 | grep -c CLOSE-WAIT); &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;LISTEN=$(ss -ant | grep :8080 | grep -c LISTEN);&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;ESTABLISHED : $EST&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;(JVM&lt;span&gt;이&lt;/span&gt; &lt;span&gt;처리&lt;/span&gt; &lt;span&gt;중인&lt;/span&gt; &lt;span&gt;연결&lt;/span&gt;)&quot;; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;echo &quot;SYN_RECV&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;: $SYN&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;(OS &lt;span&gt;대기열에도&lt;/span&gt; &lt;span&gt;못&lt;/span&gt; &lt;span&gt;들어온&lt;/span&gt; &lt;span&gt;시도&lt;/span&gt;)&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;TIME_WAIT &lt;span&gt;&amp;nbsp; &lt;/span&gt;: $TIME_WAIT (&lt;span&gt;종료&lt;/span&gt; &lt;span&gt;절차&lt;/span&gt; &lt;span&gt;중&lt;/span&gt;)&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;CLOSE_WAIT&lt;span&gt;&amp;nbsp; &lt;/span&gt;: $CLOSE_WAIT (&lt;span&gt;비정상&lt;/span&gt; &lt;span&gt;종료&lt;/span&gt; &lt;span&gt;확인&lt;/span&gt; &lt;span&gt;필요&lt;/span&gt;)&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;LISTEN&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;: $LISTEN&quot;; &lt;span&gt;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &quot;=====================================================&quot;;&lt;span&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;sleep 0.5; done&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-26 오전 12.57.23.png&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/berSBE/dJMcajhFLNi/WKeIswI9eUaDmEVBnTwnKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/berSBE/dJMcajhFLNi/WKeIswI9eUaDmEVBnTwnKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/berSBE/dJMcajhFLNi/WKeIswI9eUaDmEVBnTwnKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FberSBE%2FdJMcajhFLNi%2FWKeIswI9eUaDmEVBnTwnKK%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;1084&quot; height=&quot;730&quot; data-filename=&quot;스크린샷 2026-04-26 오전 12.57.23.png&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-end=&quot;532&quot; data-start=&quot;294&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;저의 추정이 맞는지 확인하기 위해&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Tomcat이&lt;/span&gt; 최대 유지할 수 있는 TCP connection 수인&amp;nbsp;&lt;b&gt;maxConnections 값을 10,000 &lt;/b&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-04-26 오전 1.04.42.png&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmpQ6d/dJMcahRIsmp/LFUIOOUOfyTqikJkdxSXkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmpQ6d/dJMcahRIsmp/LFUIOOUOfyTqikJkdxSXkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmpQ6d/dJMcahRIsmp/LFUIOOUOfyTqikJkdxSXkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmpQ6d%2FdJMcahRIsmp%2FLFUIOOUOfyTqikJkdxSXkK%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;1296&quot; height=&quot;748&quot; data-filename=&quot;스크린샷 2026-04-26 오전 1.04.42.png&quot; data-origin-width=&quot;1296&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;█&amp;nbsp;TOTAL&amp;nbsp;RESULTS&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_total.......:&amp;nbsp;10000&amp;nbsp;&amp;nbsp;&amp;nbsp;367.699242/s&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_succeeded...:&amp;nbsp;100.00%&amp;nbsp;10000&amp;nbsp;out&amp;nbsp;of&amp;nbsp;10000&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checks_failed......:&amp;nbsp;0.00%&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;out&amp;nbsp;of&amp;nbsp;10000&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;✓&amp;nbsp;status&amp;nbsp;is&amp;nbsp;200&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HTTP&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;http_req_duration..............: avg=15.85s min=1.61s med=16.5s&amp;nbsp;&amp;nbsp;max=24.36s p(90)=23.25s p(95)=23.73s&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 10,000건의 요청 모두 성공하게 되었습니다. Tomcat 기본 maxConnections(8,192) 한계로 인해 10,000 동시 요청 시 TCP conneciotn을 수립하지 못하며 dial timeout 발생한 문제였던 것입니다. 따라서 maxConnections 10,000으로 증가시켜 모든 요청 정상 처리하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 1대로 10,000건의 요청을 처리하였지만 요청의 95%의 latency가 23초나 됩니다. 이는 너무 느린 값으로 다음에는 latency 개선하는 것을 목표로 글을 써보겠습니다.&lt;/p&gt;
&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;# Tomcat은 어떤 방식으로 OS backlog 에서 요청을 가져와 처리하는걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b data-index-in-node=&quot;14&quot; data-path-to-node=&quot;0&quot;&gt;&amp;nbsp;Tomcat의 Acceptor 스레드&lt;/b&gt;가 OS의 Accept Queue(Backlog)에 대기 중인 연결을 가져와서 &lt;b data-index-in-node=&quot;78&quot; data-path-to-node=&quot;0&quot;&gt;NIO 방식&lt;/b&gt;으로 처리할 수 있도록 Poller에게 넘겨주는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Tomcat 8.5 버전 이후부터는 BIO(Blocking I/O) 방식이 삭제되고 &lt;b data-index-in-node=&quot;47&quot; data-path-to-node=&quot;1&quot;&gt;NIO(Non-blocking I/O)가 기본&lt;/b&gt;이 되었기 때문에, 현재 사용하시는 대부분의 환경에서는 이 메커니즘으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버 OS 간의 &lt;b data-index-in-node=&quot;16&quot; data-path-to-node=&quot;4,0,0&quot;&gt;TCP 3-Way Handshake&lt;/b&gt;가 완료되면, 해당 연결은 OS의 &lt;b&gt;Accept Queue(Backlog)&lt;/b&gt;에 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;darr;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat 내부의 Acceptor라는 별도의 스레드가 serverSocket.accept()를 호출합니다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이때 OS 큐에서 완성된 연결(Socket)을 하나씩 꺼내옵니다. &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;이 때 Acceptor가 OS의 Accept Queue에서 연결을 꺼내올 때, 가장 먼저 확인하는 것이 바로 &lt;b data-index-in-node=&quot;57&quot; data-path-to-node=&quot;4&quot;&gt;현재 활성화된 연결 수&lt;/b&gt;입니다. 현재 연결된 수 &amp;ge; maxConnections 이면, &lt;b data-index-in-node=&quot;38&quot; data-path-to-node=&quot;5,1,0&quot;&gt;Acceptor는 더 이상 accept()를 호출하지 않고 잠시 대기(Block) 상태&lt;/b&gt;에 빠집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;nbsp; &amp;darr;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Acceptor가 소켓을 꺼내면, 이를 그대로 처리하지 않고 &lt;b data-index-in-node=&quot;34&quot; data-path-to-node=&quot;8,0,0&quot;&gt;Poller 스레드&lt;/b&gt;에게 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;darr;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Poller는 NIO의 핵심인 &lt;b data-index-in-node=&quot;17&quot; data-path-to-node=&quot;8,1,0&quot;&gt;Selector&lt;/b&gt;를 가지고 있으며 읽기 준비가 된 소켓을 발견하면, 실제 비즈니스 로직을 수행할 &lt;b data-index-in-node=&quot;47&quot; data-path-to-node=&quot;10,0,0&quot;&gt;Worker 스레드&lt;/b&gt;에게 작업을 할당합니다. 이 때 Worker 스레드의 수는 maxThreads 설정으로 정해집니다. 따로 설정하지 않을 시 기본값은 200입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로젝트/대규모 트래픽 - 쿠폰 발급 시스템</category>
      <category>dial timeout</category>
      <category>dialtimeout</category>
      <category>tomcat튜닝</category>
      <category>동시 요청 처리</category>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/75</guid>
      <comments>https://do-it-zero.tistory.com/75#entry75comment</comments>
      <pubDate>Sun, 26 Apr 2026 01:30:58 +0900</pubDate>
    </item>
    <item>
      <title>[ Tomcat ] Tomcat의 네트워크 스택</title>
      <link>https://do-it-zero.tistory.com/74</link>
      <description>&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;&amp;nbsp;현재 회사에서는 서버에 외장 Tomcat을 설치 후 디렉토리 배포를 하는 방식입니다. 취업 전에 Spring boot로만 작업을 했었기에 따로 Tomcat에 대해 깊이 공부한 적이 없었습니다. 하지만 이번에 스케쥴러의 마이그레이션 업무를 담당하게 되었고, 이에 따라 Tomcat도 공부하게 되었습니다.&amp;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;[ Tomcat의 핵심 : &lt;b&gt;Connector &amp;rarr; Coyote &amp;rarr; Catalina&lt;/b&gt;로 이어지는 파이프라인 ]&amp;nbsp;&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;Client &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;br /&gt;[OS&amp;nbsp;TCP&amp;nbsp;stack] &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;br /&gt;&lt;b&gt;Connector&lt;/b&gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;br /&gt;&lt;b&gt;Coyote&amp;nbsp;(ProtocolHandler)&lt;/b&gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;br /&gt;Endpoint &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;Acceptor &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;Poller &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;Worker&amp;nbsp;(Executor) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;br /&gt;Adapter &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;br /&gt;&lt;b&gt;Catalina&amp;nbsp;(Servlet&amp;nbsp;Container)&lt;/b&gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;Engine &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;├─&amp;nbsp;Host &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;Context &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;br /&gt;Servlet&lt;/p&gt;
&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;# Connector&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- server.xml 설정에 따라 Coyote의 ProtocolHandler 구현체를 생성하고 관리하는 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Coyote와 Catalina를 연결하는 브릿지 역할&lt;/p&gt;
&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;# Coyote&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 실제 TCP/HTTP 처리를 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Coyote의 각 레이어에서는 아래와 같이 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청이 서버와 TCP 3-way handshake 완료되면 TCP 연결 성립이 되고 OS socket 생성이 되고 kernel에 등록됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Coyote 내부의 Acceptor가 serverSocket.accept()를 수행하여 OS socket을 가져와 JVM에 socket 인스턴스를 생성 후 Poller queue에 넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Poller 내부에 Selector가 있는데, kernel network stack에 segment가 일부라도 kernel buffer에 도착하면 read event가 발생하며, selector가 이를 감지해 읽을 준비된 socket 객체는 Poller를 통해 worker thread에게 넘기게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 이 때 사용되는 worker thread는 server.xml에서 &amp;lt;Connector&amp;gt; 에서 설정한 worer thread pool에 있는 worker thread 입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;worker thread는 SocketProcessor를 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SocketProcessor는 socket을 읽은 후 Http11Processor를 호출 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Http11Processor는 HTTP를 해석하여 request line,header 파싱하고 body를 처리하여 Coyote Request를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 Mapper가 실행되어 Coyote Request로부터 데이터 파싱하여 Host,Context,Wrapper를 결정합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoyoteAdapter가 Coyote에서 만든 request와 Mapper 결과를 받아 Catalina의 Engine Pipline 실행을 시작하면서 Catalina 레이어로 진입하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( Engine Pipline이란 Tomcat에서 요청이 Catalina로 들어온 뒤, Engine &amp;rarr; Host&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr; Context&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&amp;rarr; Wrapper 순서로 Valve 체인을 통해 요청을 하위 컨테이너로 전달하는 실행 구조입니다.)&lt;/p&gt;
&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;# Catalina&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Catalina 구조는 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Engine &lt;br /&gt;&amp;nbsp;└─&amp;nbsp;Host &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;Context &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└─&amp;nbsp;Wrapper&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Engine은 전역 정책, Host는 도메인 정책, Context는 웹앱 실행 준비를 담당하는 Valve들이 각각 Pipeline에 존재하며 단계별로 요청을 처리합니다. 모든 Valve(정책 단계)를 통과하면, 최종적으로 Wrapper에서 Servlet으로 넘어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부터 coyote,catalina 에 대해서 정확히 알지 못했지만, 이렇게 Tomcat의 네트워크 스택을 공부하게 되면서 클라이언트 요청이 어떻게 구체적으로 처리가 되는지 알게 되었습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>자바</category>
      <category>catalina</category>
      <category>Coyote</category>
      <category>Tomcat</category>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/74</guid>
      <comments>https://do-it-zero.tistory.com/74#entry74comment</comments>
      <pubDate>Tue, 21 Apr 2026 15:59:11 +0900</pubDate>
    </item>
    <item>
      <title>[ JVM ] 왜 같은 API인데 시간이 지날수록 빨라질까? (Spirng)</title>
      <link>https://do-it-zero.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;개인 프로젝트로 대규모 쿠폰 발급 시스템을 만들어서 부하 테스트를 하다보면 최초 부하 테스트 보다 그 이후에 부하 테스트 시 응답 속도 결과가 빨라진 것을 자주 목격 했습니다. 단순히 최적화가 되서 그런가보다 라고 생각했지만, 어떻게 어떤 이유로 최적화를 시키는지 JVM관점에서 공부 해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ JVM 입장에서 보는 Spring 생성과 요청 처리 과정&amp;nbsp; ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;# Spring 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;spring 애플리케이션 실행 시, JVM은 classloader를 통해 .class 파일들을 가져와 바이트코드 분석, 클래스 구조 생성을 한 후 MetaSpace에 클래스 메타 데이터를 저장합니다. 그리고 검증,준비,참조 연결 단계를 거친 후 static 변수 초기화 및 static block을 실행합니다. 이후 JVM은 main thread를 생성되면서 main thread 전용 stack/Pc Register/Native Stack이 만들어지고 main() 메서드가 실행되면서 springApplication.run()이 실행됩니다 .&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;main() 안에서 SpringApplication.run()이 실행되면 Spring Boot가 시작되고, ApplicationContext가 생성되며 refresh()가 호출된다. 이 과정에서 Component Scan이 수행되어 @Component, @Service, @Controller 같은 클래스들이 탐색되고, 이를 기반으로 BeanDefinition이라는 &amp;ldquo;객체 설계도&amp;rdquo;가 만들어져 BeanFactory에 등록됩니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그 다음 BeanFactory가 createBean()을 호출하면서 실제 객체를 생성하는데, 이때 new가 실행되어 Heap에 Bean 객체가 생성되고, 이후 의존성 주입(@Autowired), 초기화(@PostConstruct), AOP 프록시 적용이 순차적으로 이루어집니다.&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 []&quot; 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;클라이언트 요청이 오면 worker thread의 stack에는 controller/service/repository 호출 프레임이 쌓이고, Execution Engine은 그 프레임 안의 bytecode를 interpreter/JIT으로 실행하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;JVM은 처음엔 interpreter로 bytecode를 실행하다가 실행 빈도가 높은 메서드를 JIT이 native code로 컴파일하여 이후에는 더 빠르게 실행하게 됩니다. 즉,초기 부하 테스트가 느린 이유는 Interpreter 기반 실행 + JIT profiling 부족 상태이고, 이후 테스트가 빨라지는 이유는 JIT이 hot method를 native code로 최적화했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>자바</category>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/73</guid>
      <comments>https://do-it-zero.tistory.com/73#entry73comment</comments>
      <pubDate>Fri, 17 Apr 2026 10:18:34 +0900</pubDate>
    </item>
    <item>
      <title>[ Linux ] 유용한 명령어</title>
      <link>https://do-it-zero.tistory.com/72</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;- tail &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; 파일명&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;파일을 감시하다가 변경이 생기면 바로 화면에 출력함, 주로 실시간 로그 확인, 모니터링 할 때 씀&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- logrotate 수정 후 테스트로 강제 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;su로 접근하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;logrotate -f /etc/logrotate.d/파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 파일 카피 cp [원본] [복사]&lt;/p&gt;</description>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/72</guid>
      <comments>https://do-it-zero.tistory.com/72#entry72comment</comments>
      <pubDate>Wed, 15 Apr 2026 09:33:08 +0900</pubDate>
    </item>
    <item>
      <title>[ Tomcat ] 외장 Tomcat 로그 설정 관련</title>
      <link>https://do-it-zero.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;# CATALINA_BASE 설정은 start.sh에서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# 로그 경로 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- GC log 설정 : start.sh 에서 디렉토리 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- catalina , localhost, manager, host-manager 관련 : etc/tomcat/logging.properties&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- spring 실행 로그 : tomcat/bin/catatlin.sh&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# spring 실행 로그 주기 설정&amp;nbsp; : /etc/logrotate.d/tomcat&lt;/p&gt;</description>
      <category>자바</category>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/71</guid>
      <comments>https://do-it-zero.tistory.com/71#entry71comment</comments>
      <pubDate>Tue, 14 Apr 2026 11:04:59 +0900</pubDate>
    </item>
    <item>
      <title>[ Tomcat ] Spring Boot 디렉토리 배포 방법</title>
      <link>https://do-it-zero.tistory.com/70</link>
      <description>&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;&amp;nbsp;Spring Boot는 내장 Tomcat이 있기에 따로 Tomcat을 설치할 필요가 없다는 장점이 있습니다. 그래서 서버에 배포시 jar 파일만 배포 후 실행하면 됩니다. 하지만 이런 점이 때로는 불편함을 만들 수 있다고 생각합니다. 왜냐하면 아주 작은 변경 했을 뿐인데, jar로 만들어서 배포해야 하기 때문입니다. 서버의 파일 전송 속도가 느린 환경에서 2kb 밖에 안되는 변경 class를 적용시키기 위해 mb단위의 jar를 만들어서 배포해야 하는 경우도 있기 때문입니다.&amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위와 같은 문제를 해결하기 위해 Tomcat 디렉토리로 Spring 애플리케이션을 배포하여 변경한 클래스 파일만 붙여 넣는 방법이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&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;1. 먼저 tomcat을 다운 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tomcat.apache.org/download-11.cgi&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tomcat.apache.org/download-11.cgi&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;2.&amp;nbsp; conf/server.xml 열어&amp;nbsp; &amp;lt;Host 를 검색해 확인합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Host name=&quot;localhost&quot; appBase=&quot;webapps&quot; unpackWARs=&quot;true&quot; autoDeploy=&quot;true&quot;&amp;gt; 이렇게 되어 있을텐데, 각 속성은 아래와 같은 목적을 가집니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;name&lt;/td&gt;
&lt;td&gt;localhost&lt;/td&gt;
&lt;td&gt;호스트 이름(도메인)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;appBase&lt;/td&gt;
&lt;td&gt;webapps&lt;/td&gt;
&lt;td&gt;애플리케이션 배포 폴더&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unpackWARs&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;WAR 파일을 풀어서 배포&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autoDeploy&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;실행 중 새 앱을 자동 배포/삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. webapps로 들어가 기존에 있던 모든 폴더를 삭제 후 ROOT 폴더를 새로 만듭니다. URL이 /로 시작하려면 반드시 ROOT에 있어야 합니다. URL을 최초 시작을 /로 하기 위해서는 ROOT에 배포되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 배포 파일을 만들 차례입니다. IDE는 이클립스이며, 빌드 자동화 도구는 Maven으로 설명하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Spring Boot 기본은 jar이므로, 먼저 WAR로 바꿔야 합니다. &amp;lt;packaging&amp;gt;war&amp;lt;/packaging&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- WAR로 배포하려면 &lt;b&gt;Tomcat 같은 외부 서버에서 실행 가능&lt;/b&gt;해야 하므로, SpringBootServletInitializer 상속하도록 메인 애플리케이션 클래스를 이렇게 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;예시 )&lt;/p&gt;
&lt;pre id=&quot;code_1774584001465&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
public class MyApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(MyApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 외부 Tomcat에 배포할 경우, 내장 Tomcat은 필요 없으므로 provided로 변경합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;dependency&amp;gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;artifactId&amp;gt;spring-boot-starter-tomcat&amp;lt;/artifactId&amp;gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt; &lt;br /&gt;&amp;lt;/dependency&amp;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;- 배포할 파일을 만들기 위해 Maven 빌드 설정을 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 우클릭 -&amp;gt; Run As -&amp;gt; Run Configuration 클릭 -&amp;gt; 왼쪽에 maven build 우클릭 후 New Configuration 클릭 -&amp;gt; work space에서 프로젝트 경로 추가 -&amp;gt; Goals 에 clean package를 작성 후 하단에 Run을 클릭합니다.&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-03-27 130151.png&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cE4uRO/dJMcajhi19B/rf9ZxOEwPgL6siQ8kdWlB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cE4uRO/dJMcajhi19B/rf9ZxOEwPgL6siQ8kdWlB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cE4uRO/dJMcajhi19B/rf9ZxOEwPgL6siQ8kdWlB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcE4uRO%2FdJMcajhi19B%2Frf9ZxOEwPgL6siQ8kdWlB0%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;730&quot; data-filename=&quot;스크린샷 2026-03-27 130151.png&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;730&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-27 130213.png&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yvrhP/dJMcajhi19R/cN4ed1piYyn1JzfHBfr1l0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yvrhP/dJMcajhi19R/cN4ed1piYyn1JzfHBfr1l0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yvrhP/dJMcajhi19R/cN4ed1piYyn1JzfHBfr1l0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyvrhP%2FdJMcajhi19R%2FcN4ed1piYyn1JzfHBfr1l0%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;321&quot; height=&quot;164&quot; data-filename=&quot;스크린샷 2026-03-27 130213.png&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-27 130245.png&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsnXkU/dJMcaiJtdNI/GNfugZi4g1CkofLBGYWsEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsnXkU/dJMcaiJtdNI/GNfugZi4g1CkofLBGYWsEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsnXkU/dJMcaiJtdNI/GNfugZi4g1CkofLBGYWsEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsnXkU%2FdJMcaiJtdNI%2FGNfugZi4g1CkofLBGYWsEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;722&quot; height=&quot;193&quot; data-filename=&quot;스크린샷 2026-03-27 130245.png&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프로젝트에 target을 누르면 프로젝트이름 + snapshot 폴더가 있습니다. WEB-INF 전체를 복사해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복사한 WEB-INF를 Tomcat 의 webapps/ROOT 폴더에 복사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cmd를 켠 후 Tomcat 의 bin 폴더 경로로 이동 후 startup.bat을 실행합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-27 131008.png&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;24&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYGViB/dJMcaf0iC8h/XWKstEg0KdY454fqauKRx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYGViB/dJMcaf0iC8h/XWKstEg0KdY454fqauKRx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYGViB/dJMcaf0iC8h/XWKstEg0KdY454fqauKRx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYGViB%2FdJMcaf0iC8h%2FXWKstEg0KdY454fqauKRx1%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;352&quot; height=&quot;24&quot; data-filename=&quot;스크린샷 2026-03-27 131008.png&quot; data-origin-width=&quot;352&quot; data-origin-height=&quot;24&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 클래스 변경이 있을 경우 WEB-INF/classes 로 들어가 해당 클래스만 변경 후 Tomcat을 재시작 하시면 됩니다.&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-end=&quot;113&quot; data-start=&quot;56&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;개발 일을 하면서 느낀 점은, &lt;b&gt;모든 선택에는 반드시 trade-off가 존재한다&lt;/b&gt;는 것입니다.&lt;/p&gt;
&lt;p data-end=&quot;226&quot; data-start=&quot;115&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;226&quot; data-start=&quot;115&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;예를 들어, &lt;b&gt;JAR로 배포&lt;/b&gt;하면 외부 Tomcat 설정 없이 간단하게 배포할 수 있다는 장점이 있습니다. 하지만 작은 변경 사항이 생겨도 전체 JAR를 다시 배포해야 한다는 단점이 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;342&quot; data-start=&quot;228&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;342&quot; data-start=&quot;228&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;반면, &lt;b&gt;디렉토리(WAR) 배포&lt;/b&gt;는 일부 클래스나 리소스만 교체하면 되므로 작은 변경 사항을 빠르게 반영할 수 있다는 장점이 있습니다. 그러나 외부 Tomcat을 설정해야 하는 번거로움이 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;421&quot; data-start=&quot;344&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;421&quot; data-start=&quot;344&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;결국, 어느 한 가지 방법만 고집하기보다는 &lt;b&gt;환경과 상황에 맞춰 다양한 방법을 유연하게 고려하는 것이 중요&lt;/b&gt;하다는 점을 느꼈습니다.&lt;/p&gt;</description>
      <category>자바</category>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/70</guid>
      <comments>https://do-it-zero.tistory.com/70#entry70comment</comments>
      <pubDate>Fri, 27 Mar 2026 13:20:29 +0900</pubDate>
    </item>
    <item>
      <title>[ 문제 해결 ] 대용량 데이터 요청 개선기</title>
      <link>https://do-it-zero.tistory.com/69</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 문제 상황 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;기존 시스템을 업데이트 후 200만건 정도의 조회 요청(대략 1.5G 정도)&amp;nbsp; 들어와 jvm에서 oom이 떠버렸다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보니 tomcat에 할당된 heap 메모리 사이즈가 업데이트 하기 전 서버보다 작게 잡혀 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;부장님께 보고 드리니 heap 사이즈를 늘려주신다고는 하나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'과연 heap 사이즈를 올린다고 추후 같은 문제가 발생하지 않을까?' 하는 의문이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 200만건 정도의 조회 요청을 한 번 정도 왔으니 다행이나 여러 명의 직원들이 동시에 요청을 하게 된다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 대용량 요청을 여러명이 동시에 요청하게 되면 그만큼 jvm에서 heap사이즈를 차지하게 될것이고 결국에는 oom이 발생할 수 밖에 없는 여지를 남겨둔다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 그렇다면 대체 어디서 왜 oom이 발생한 것일까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;heap dump를 통해 보니&amp;nbsp; &lt;span style=&quot;background-color: #f0f2f5; color: #0a0a0a; text-align: start;&quot;&gt;com.mysql.jdbc.JDBC4ResultSet&amp;rdquo; 인스턴스 하나가 1,641,591,440바이트(76.74%)를 차지합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC4ResultSet은 뭘까? DB에서 조회한 데이터가 최초로 담기는 객체이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;heap 메모리가 최대 2기가 밖에 할당되지 않았는데(추후 늘림) 저렇게 큰 데이터가 객체에 할당되다 보니 oom이 발생한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 봤을 땐 첫번째로는 당연히 heap 메모리만 늘리면 되겠다! 라고 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 더 나아가 여러 직원이 해당 요청을 동시에 한다면? 무조건 서버가 뻑갈 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 메모리를 늘리는 것이 아닌 다른 방식으로 요청을 처리할 필요가 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;두가지 방식을 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 해당 기능은 조회한 데이터를 브라우저에 보여주는 것이니 페이지 나눠서 보여주는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;2. Streaming 형식으로 db에서 받은 데이터를 바로 브라우저에 보내는 것&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;첫번째 방식은 기존에 있던 쿼리문들을 다 수정해야 한다는 번거로움이 있어서 제외했다. 그리고 해당 기능을 이용하는 직원들의 이야기를 들어보니 데이터를 조회 후에 엑셀로 다운 받아서 2차 작업을 하는 경우가 있다고 했다. 페이지를 나눌 경우 페이지를 눌러가며 각각의 엑셀을 다운 받아야하는 번거로움까지 생기니, 개발하는 관점이나 실무적으로나 첫번째 방식은 합당하지 않다고 생각하여 제외하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 선택한 것이 두번째 방식이였다. Streaming 방식으로 데이터를 보내기 위해서는 db 조회 데이터를 한 번에 가져오는 것이 아닌 일부분씩 가져와야하는데, 이것이 가능한지 조사하였고 ResultHandler를 이용하면 될 것 같다고 생각하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResultHandler 동작은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResultSet에서 row 하나 읽을 때마다 handleResult()가 호출되는 것이다. 가져온 데이터는 handleResult 메서는 내부에서 outpuyStream을 통해 브라우저에 보내면 되겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 뭐든지 트레이드 오프가 있는 법!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;ResultHandler를 쓰는 동안 Connection을 점유한다는 점, 이 부분은 oom이 발생하여 서버가 다운되는 것보다 connection 점유가 되어 혹시라도 요청이 늦거나 못 받는 경우가 생기는게 더 낫다고 생각했고 개발을 시작하였다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 사용자 입장에서 다시 생각하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 개발을 완료하였다! 테스트 해보니 서버에서는 oom이 발생하지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 문제는 사용자 입장에서 발생했다. 50만건 정도 넘어가니 브라우저에서 버벅이며 스크롤하는데도 느렸다. 또한 100만건 정도의 데이터를 받으니 프론트에서 JSON 파싱 에러가 나는 경우도 있었다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐음...oom 발생하지 않으니 괜찮은걸까? 생각했다. 하지만 실무자들 입장에서는 분명히 불편할 것이라고 생각했고, 그렇다면 다른 직원들이 어떻게 이 기능을 사용했는지 다시 생각해보았다. 보통 데이터 조회 후 엑셀로 다운 받아서 2차 작업을 했다는 것이 생각이 이 났고, 부장님과 얘기하여 조회 데이터가 30만건 이상인 경우에는 Streaming 방식으로 바꾸어 엑셀로 다운 받게 하자는 쪽으로 결정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 개발 완료 후 여러 차례 테스트를 진행하며 oom이 발생하지 않은 것을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 회고 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이번 글은 현재 회사의 환경에 맞추어 해결했던 방식일 뿐이니 내 방법이 무조건 정답은 아니라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 개발은 개발 환경과 실무자들의 사용성에 맞춰서 최선책을 찾아가는 것이라고 생각이 들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 대용량 데이터 요청을 어떻게 메모리에 최대한 부담 없이 처리할 수 있을지 기술적으로 풀어낸 좋은 경험이였다.&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;늘 그렇지만 한번에 해결되지 않는다. 계속 생각하고 생각하고 테스트 해보고 다시 해보는 모든 과정을 통해 해결 방법이 찾아진다는 것을 다시 한 번 느꼈다. 난 그래서 개발이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고록/문제 해결</category>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/69</guid>
      <comments>https://do-it-zero.tistory.com/69#entry69comment</comments>
      <pubDate>Wed, 25 Feb 2026 15:03:33 +0900</pubDate>
    </item>
    <item>
      <title>[ mariadb ] EXPLAIN 실행 계획 조회 명령어 공부 #1</title>
      <link>https://do-it-zero.tistory.com/68</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리문의 조회 속도를 파악할 일이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 쓰는 mariadb에서 어떻게 쿼리문을 분석하고 조회 속도를 확인할지에 대한 공부 기록이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 생각나는 것은 EXPLAIN 실행계획 명령어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mariadb 공식 문서에서 가서 EXPALIN이 뭔지 확인해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mariadb.com/docs/server/reference/sql-statements/administrative-sql-statements/analyze-and-explain-statements/explain&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mariadb.com/docs/server/reference/sql-statements/administrative-sql-statements/analyze-and-explain-statements/explain&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1767573240029&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;EXPLAIN | Server | MariaDB Documentation&quot; data-og-description=&quot;Good night I'm here to help you with the docs. What is this page about?What should I read next?Can you give an example?&quot; data-og-host=&quot;mariadb.com&quot; data-og-source-url=&quot;https://mariadb.com/docs/server/reference/sql-statements/administrative-sql-statements/analyze-and-explain-statements/explain&quot; data-og-url=&quot;https://mariadb.com/docs/server/reference/sql-statements/administrative-sql-statements/analyze-and-explain-statements/explain&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://mariadb.com/docs/server/reference/sql-statements/administrative-sql-statements/analyze-and-explain-statements/explain&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mariadb.com/docs/server/reference/sql-statements/administrative-sql-statements/analyze-and-explain-statements/explain&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;EXPLAIN | Server | MariaDB Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Good night I'm here to help you with the docs. What is this page about?What should I read next?Can you give an example?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mariadb.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;#얻은 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- EXPLAIN 문은 DESCRIBE 문과 동의어로 사용될 수도 있고, MariaDB가 SELECT, UPDATE 또는 DELETE 문을 실행하는 방식에 대한 정보를 얻는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- EXPLAIN EXTENDED를 사용하면 추가정보를 얻을 수 있다고 되어 있다. 실제 해보니 기존 EXPLAIN 명령어로 나온 결과에 fitered 열만 추가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- filtered란? row값으로 나온 행의 갯수에서 where 조건을 통과한 행의 비율&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;row가 100이고 filtered가 100.0 이라면 100개의 row를 예상했고 where 조건을 통과한 후에 행이 100% 즉 100개라는 뜻.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>데이터베이스</category>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/68</guid>
      <comments>https://do-it-zero.tistory.com/68#entry68comment</comments>
      <pubDate>Mon, 5 Jan 2026 17:28:07 +0900</pubDate>
    </item>
    <item>
      <title>[ 문제 해결 ]Set-cookie 가 있는데  브라우저 application 탭에 쿠키가 만들어지지 않을 때 (https 사용 없이 http로만 하는 방법)</title>
      <link>https://do-it-zero.tistory.com/67</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;[ 문제 ]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 refreshToken이 담긴 cookie를 만들고 응답헤더에도 Set-Cookie가 있음에도 실제 쿠키가 만들어지지 않는 문제가 발생했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;[ 원인 ]&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;front 서버는 localhost:5173&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;back 서버는 localshot:8080&lt;span&gt;&amp;nbsp;&lt;/span&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;브라우저는 페이지를 제공하는 서버를 기준으로 Origin을 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 localhost:5173 서버에서 리소스를 받았으므로, 해당 리소스의 Origin은 localhost:5173이다. 그런데 api 요청시 bacck 서버의 Origin은 localhost:8080이다. 즉 Origin이 다르므로 CORS 가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 백엔드 서버에서 CORS 헤더를 보내도록 설정하므로 해결 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 설정도 할 수 있지만 나는 간단하게 다음과 같이 controller에 설정하였다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@RestController
@Slf4j
@RequestMapping(&quot;/users&quot;)
@CrossOrigin(origins = &quot;http://localhost:5173&quot;, allowCredentials = &quot;true&quot;)
@RequiredArgsConstructor
public class UserController&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 문제가 해결되어 console 창에 에러 로그가 사라졌지만, 서버로부터 응답 받은 헤더에 set-cookie가 있음에도 cookie가 생성되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 SameSite=None + Secure 조건이 만족되지 않았기 때문에 브라우저는 쿠키를 생성하지 않았던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 조건이 만족하고 브라우저가 쿠키를 생성하기 위해서는 http가 아닌 https로 통신을 해야한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;[ 해결 ]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나는 로컬에서만 테스트 중이였으므로 https로 전환은 따로 할 필요성이 없어 http로도 쿠키가 생성시키는 방법을 모색했다.&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;vite.config.js 설정을 바꿔서&lt;/b&gt; localhost:5173을 프록시 서버로 사용하는 방법을 채택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localhost:5173 프록시 서버로 사용했기에 브라우저는 프록시 서버와 통신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 서버는 브라우저가 보낸 요청들을 localhost:8080인 서버와 통신을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 프록시 서버와만 통신하기 때문에 CORS 자체가 발생하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vite.config.js 설정 변경&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1f1f1f; color: #cccccc;&quot;&gt;
&lt;div&gt;&lt;span style=&quot;color: #ce92a4;&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce92a4;&quot;&gt;default&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;defineConfig&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;({&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;plugins&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;vue&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;()],&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;server&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; {&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;port&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #b5cea8;&quot;&gt;5173&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;,&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;proxy&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; {&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;'/api'&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; {&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;target&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;'http://localhost:8080'&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;,&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;changeOrigin&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;,&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;secure&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;,&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;rewrite&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;path&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #d16969;&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color: #d7ba7d;&quot;&gt;\/&lt;/span&gt;&lt;span style=&quot;color: #d16969;&quot;&gt;api/&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color: #ce9178;&quot;&gt;''&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;)&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; }&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;})&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 서버를 쓰는 것으로 대체하였으므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@CrossOrigin(origins = &quot;http://localhost:5173&quot;, allowCredentials = &quot;true&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;해당 설정으로 바꾼 후 브라우저 application 탭에 쿠키가 생성되어 refreshToken이 있는 것을 확인하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고록/문제 해결</category>
      <category>Cookie</category>
      <category>cookie refreshToke</category>
      <category>Set-Cookie</category>
      <category>set-cookie 안만들어짐</category>
      <author>hola.</author>
      <guid isPermaLink="true">https://do-it-zero.tistory.com/67</guid>
      <comments>https://do-it-zero.tistory.com/67#entry67comment</comments>
      <pubDate>Tue, 18 Nov 2025 17:08:14 +0900</pubDate>
    </item>
  </channel>
</rss>