<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ecsimsw</title>
    <link>https://ecsimsw.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 15 May 2026 22:18:41 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JinHwan Kim</managingEditor>
    <image>
      <title>ecsimsw</title>
      <url>https://tistory1.daumcdn.net/tistory/3020396/attach/6ea6ec42e2d94a249622363f4c9c2a65</url>
      <link>https://ecsimsw.tistory.com</link>
    </image>
    <item>
      <title>데이터 레이크 구축 : 초당 6천 개의 이벤트를 담는 똑똑한 호수</title>
      <link>https://ecsimsw.tistory.com/entry/datalake-s3</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 IoT 서비스 회사의 백엔드 개발자이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;약 260만 개의 기기에서 초당 6천 개에 달하는 기기 상태 이벤트가 발생하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 이를 수신하여 가공하고, 적재 또는 다음 서비스로 전달하는 파이프라인을 개발한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수많은 기기 상태 이벤트를 가공 없이 날것 그대로 저장하는 것은 매우 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;적재된 데이터는 기기가 과거 특정 시점에 어떤 상태였는지 확인하는 히스토리가 되고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;고객의 기기 동작 문의를 확인하거나 외부 플랫폼의 이벤트 처리 여부를 증명하는 중요한 증적이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;데이터 저장소 비교&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1차 시도: CloudWatch&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처음에는 단순 로그로 취급하여 CloudWatch Logs에 모든 이벤트를 기록했고, 비용 문제를 만났다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CloudWatch Logs는 수집, 저장, 분석 세 가지 영역에서 비용이 발생하고, 그중 수집 비용의 비중이 가장 크다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서울 리전 기준 GB당 약 $0.76인데, 하루에 수백 GB가 발생했고 단순 적재를 위한 비용으로는 감당하기는 어려웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2차 시도: Loki 또는 ELK&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비용 문제를 해결하기 위해 Promtail + Loki으로 오픈소스 조합으로 전환했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Loki의 백엔드 저장소를 S3로 설정하여 파일 시스템의 크기에 구애받지 않고, 순수 파일 저장 비용만 낼 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문제는 속도였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Loki는 로그 메타데이터(레이블)만 인덱싱 하고 내용은 그대로 저장하는 구조이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 때문에 '최근 1시간 이내의 기기 이벤트 검색'처럼 인덱싱 된 레이블 기반의 좁은 범위 검색은 빠르지만,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;'3일 동안 특정 기기의 모든 이벤트 검색'과 같은 전체 데이터 스캔 쿼리에는 매우 취약했고, 타임아웃이 잦아 사용이 어려웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다고 검색 속도가 빠른 ELK는, 그렇게 빠를 필요 없고, 가끔 사용하는 팀 상황에 비해선 과한 비용과 관리 부담이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;최종 결정 : S3 / 데이터 레이크 구성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결국 S3를 중심으로 한 데이터 레이크를 직접 구성하기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;S3는 저장 비용이 매우 저렴하고, 용량 확장에 신경 쓸 필요가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특히 기준 시간이 지난 데이터는 Glacier로 자동 이동시켜, 증적관리를 위한 저렴한 장기 보관이라는 목적에도 적합했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Athena 와 검색 속도&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 조회는 서버리스 쿼리 엔진인 Athena를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Athena는 검색을 위해 스캔한 데이터 양에 따라 속도와 비용이 결정되기에, '어떻게 스캔량을 줄이는가'가 중요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래는 이를 위해 신경 썼던 세 가지이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. Parquet 형식&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Parquet은 열 기반으로 데이터를 저장하는 파일 형식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;조회 쿼리를 실행하면, Athena는 전체가 아닌 출력이 필요한 컬럼 영역만 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;저장되는 데이터 중 실제 조회에 사용되는 컬럼은 소수인 팀의 상황에 특히 더 적합했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또, 파일 내부에 메타데이터를 가지고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대표적으로 최댓값, 최솟값 같은 통계 정보를 함께 기록하여 검색 조건에 포함되지 않는 파일은 무시하고 넘길 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;조건이 포함된 파일만 확인하니 스캔량이 줄고, 이는 곧 쿼리 속도 향상과 비용 절감으로 이어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. 적절한 파일 분리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;너무 파일이 많으면 각 파일의 메타데이터를 확인하기 위해 파일을 읽고 닫으며 오버헤드가 발생하고, 이는 성능이 저하로 이어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반대로 너무 큰 파일은 쿼리에 필요 없는 데이터까지 함께 읽을 확률이 높아져 비효율적일 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 해결하기 위해 이벤트를 배치로 모아 한 파일로 묶는 것으로, 최적 크기 파일 하나로 묶어서 저장한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. 파티션 기반 파일 경로&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;S3에 파일을 저장할 때 year=2026/month=03/day=28/hour=15/ 와 같이 시간 단위로 경로를 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이것을 파티셔닝이라 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Athena가 시간을 조건으로 한 검색을 수행할 때, 이 파티션 기준으로 범위 내 경로와 파일만을 스캔 대상으로 삼는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Athena는 Glue 카탈로그로 테이블 정보 (스키마, 파일 형식, 위치, 파티션 정보)를 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿼리를 실행하기 전 &lt;span style=&quot;text-align: start;&quot;&gt;Glue 카탈로그를 확인하여, 스캔할 경로와 파일 형식을 미리 파악해 두게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;많은 양, 빠를 필요는 없는 검색, 저렴한 비용이라는 팀에 맞는 이벤트 조회 시스템을 만들 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;AWS Glue ETL , 통계 자료 스케줄링&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS Glue를 사용하면 매시간 통계를 내는 ETL 작업을 쉽게 정의할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ETL은 데이터를 추출하고, 가공한 뒤, 다른 서비스에 전달 또는 다음 저장소에 적재하는 파이프라인을 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AWS Glue ETL에선 Spark + Python을 사용한 대용량 데이터 처리 로직과 자동 스케줄링을 정의할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한 가지 예시로, 수집한 기기 이벤트에서 제품별 기기 수, 각 제품에서 발생하는 이벤트 추이를 집계하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 그 작업을 매시각 20분에 실행하여, 한 시간 단위로 통계 자료를 저장하게 만들고,&amp;nbsp;이를 다시 일별, 월별로 반복했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 만들어진 통계는 특정 제품의 비정상적인 이벤트 발생을 감지, 제품팀에 전달되어 펌웨어 개선에 근거가 되었고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순 판매 수가 아닌, 시간대 별 활성화 기기 수, 자주 사용되는 기기 제어가 집계되어 마케팅 팀에 전달되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 작업 스케줄링이 모두 서버 리스 + Full managed 로 구성된다는 것도 큰 장점이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스케줄링 정의도 쉽고 명확하며, 작업이 실행되는 컴퓨팅 시간 동안의 비용만 지불하면 되기에 유지비 부담도 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞선 통계의 경우 2 DPU로 실행했을 때 약 2분이 소요되고, 이는 월 3 ~ 4만 원 수준의 비용이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최소한의 운영 부담으로 강력한 데이터 플랫폼을 구축할 수 있었다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순 데이터 보관에서 시작하여, 사용자들의 기기 사용 추이를 집계하고 추출하는, 데이터 관리 시스템으로 발전시킬 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;942&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AyEbW/dJMcaibItDm/0OaqF3SSVL3ATu3jVNkkb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AyEbW/dJMcaibItDm/0OaqF3SSVL3ATu3jVNkkb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AyEbW/dJMcaibItDm/0OaqF3SSVL3ATu3jVNkkb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAyEbW%2FdJMcaibItDm%2F0OaqF3SSVL3ATu3jVNkkb0%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;642&quot; height=&quot;419&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;942&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>KimJinHwan/Project</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1191</guid>
      <comments>https://ecsimsw.tistory.com/entry/datalake-s3#entry1191comment</comments>
      <pubDate>Sun, 29 Mar 2026 03:59:34 +0900</pubDate>
    </item>
    <item>
      <title>인증 서버 리팩토링 : OAuth 인증 시나리오와 PKCE</title>
      <link>https://ecsimsw.tistory.com/entry/%EC%9D%B8%EC%A6%9D-%EC%84%9C%EB%B2%84-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-OAuth-%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4-%EC%A0%95%EB%A6%AC-PKCE</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최근 국내 서비스들에 보안 이슈가 많았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리 회사도 보안 점검을 진행했고, 내가 맡고 있는 서비스의 인증 서버 역시 주요 대상이 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청 수 제한, 애플리케이션 키 관리, 로그 마스킹, 개인정보 보관 방법 등을 점검하고 개선했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기능적으로는 '구글 연동 로그인'과 '2FA 강제'를 위한 추가 개발이 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가 기능을 개발하고 보안을 점검하기 위해선, OAuth 2.0으로 구현된 인증 서버&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;를 먼저 이해해야 했다.&lt;/span&gt;&lt;/span&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;OAuth 2.0 가 필요했던 이유&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지금까지의 경우와는 달리, 이번엔 내가 OAuth 서버를 운영하는 입장이 되었다.&lt;/span&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;&quot;&gt;연동 서비스에서 OAuth 서버를 운영하는 이유부터 이해해야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;우리 회사의 연동 서비스는 삼성, LG와 같은 외부 IoT 플랫폼에서 자사 기기 연동 기능을 지원한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; 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;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;인증을 위해 연동 플랫폼에서 사용자 ID/PW를 직접 입력받아선 안된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;유저 정보가 외부 서비스에 직접 노출된다는 보안 문제가 있고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;이번 정책 강화처럼 2FA나 로그인 방법을 추가해야 할 때 외부 서비스에 직접 의존된다는 제어권의 문제가 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;OAuth는 계정 관리 측에서 제공한 페이지에서 유저 정보를 입력하게 하면서도,&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;인증된 외부 사용처로 토큰을 안전하게 전달할 수 있는 틀을 제공한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;대략적인 흐름&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;내가 이해한 OAuth의 전체 흐름은 아래 4단계이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;처음엔 헷갈렸는데, 한 두 번 그려보니 이해된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;1. &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;외부 클라이언트에 고유한 ID와 Secret을 발급하고, 인증 성공 후 코드를 전달받을 경로를 미리 등록한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;2. &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;사용자가 로그인 페이지에서 인증에 성공하면 임시 인가 코드를 발급하고, 미리 등록된 리다이렉트 경로로 전달한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;3. 코드를 전달받은&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;외부 서비스는 코드와 함께 자신의 Client ID, Client Secret을 인증 서버로 다시 제출한다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;4. 인증 서버는 이 정보들을 검증하고, 서비스 접근 토큰을 발급한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;1168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqvTAq/dJMcabp12zs/QKmLlpjjqLa4rU11Dqqwok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqvTAq/dJMcabp12zs/QKmLlpjjqLa4rU11Dqqwok/img.png&quot; data-alt=&quot;OAuth2.0 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqvTAq/dJMcabp12zs/QKmLlpjjqLa4rU11Dqqwok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqvTAq%2FdJMcabp12zs%2FQKmLlpjjqLa4rU11Dqqwok%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;627&quot; height=&quot;402&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;1168&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OAuth2.0 플로우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인증 코드 탈취 공격 / PKCE&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위 처리 흐름에서도 놓칠 수 있는 위험 포인트가 남아 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OAuth 서버에서 사용자로 전달한 인증 Code 코드가 탈취되는 경우이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 코드가 탈취당한다면 공격자는 클라이언트 서버에 그 코드를 전달하여 토큰을 요청할 수 있게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 방어하기 위해 OAuth 코드 요청자와 토큰 요청자가 동일한지 여부를 검증하는 키를 숨겨둔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 최초 로그인 요청 시에 클라이언트는 임의의 값을 생성, 저장한다.&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;2. OAuth 코드를 요청할 때, 이 값을 해싱하여 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 후에 토큰을 요청할 때는 앞선 임의의 값을 원본으로 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. OAuth 서버는 똑같은 방법으로 해싱하여 전과 비교하는 것으로, 코드 요청자와 토큰 요청자가 동일한지 검증한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;1176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEx3Ne/dJMcaiWXV9M/fCRk5UYsQwnAMRHP9ywiFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEx3Ne/dJMcaiWXV9M/fCRk5UYsQwnAMRHP9ywiFK/img.png&quot; data-alt=&quot;앞선 OAuth2.0 플로우에 PKCE 방어 기법 4단계를 표시했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEx3Ne/dJMcaiWXV9M/fCRk5UYsQwnAMRHP9ywiFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEx3Ne%2FdJMcaiWXV9M%2FfCRk5UYsQwnAMRHP9ywiFK%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;597&quot; height=&quot;378&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;1176&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앞선 OAuth2.0 플로우에 PKCE 방어 기법 4단계를 표시했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이런 방어 방식을 PKCE (Proof Key for Code Exchange)라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 해커가 해싱된 값을 탈취한다고 하더라도, 원본 값은 알 수 없으니 동일 사용자인 척하기는 것은 불가능한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인가 코드를 요청할 때는 해싱한 값으로 원본 값을 노출하지 않은 채 소통을 오가고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최종적으로 토큰을 요청할 때는 원본 값을 전달하여 OAuth 서버에서 그 값을 검증하는 것으로, 두 요청자가 동일한지 증명한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;연동 서비스의 OAuth 인증 서버 점검&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 보안 점검 : &lt;span style=&quot;text-align: start;&quot;&gt;요청 수 제한, &lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;애플리케이션 키 관리, 로그 마스킹, 개인정보 보관 방법 확인 등&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;- 기능 개발 : &lt;/span&gt;구글 연동 로그인, 2FA 강제&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;OAuth2.0 개념 학습&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 연동 서비스에서 OAuth 서버 운영이 필요한 배경&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- OAuth 2.0 인증 흐름 이해&lt;/span&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;&quot;&gt;- 인증 코드 탈취 공격과 PKCE를 사용한 방어&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;</description>
      <category>KimJinHwan/Project</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1186</guid>
      <comments>https://ecsimsw.tistory.com/entry/%EC%9D%B8%EC%A6%9D-%EC%84%9C%EB%B2%84-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-OAuth-%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4-%EC%A0%95%EB%A6%AC-PKCE#entry1186comment</comments>
      <pubDate>Sat, 21 Mar 2026 20:51:08 +0900</pubDate>
    </item>
    <item>
      <title>이벤트 파이프라인 성능 개선 : 처리량 향상과 리소스 과부하 대비</title>
      <link>https://ecsimsw.tistory.com/entry/event-pipeline-refactor</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 IoT 서비스를 운영하는 백엔드 개발자이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;회사 기기는 삼성의 Smartthings, LG의 ThinQ, Naver Clova, KT, Kakao 등 국내 IoT 플랫폼 연동을 지원한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 플랫폼의 기기 제어 요청을 처리하고, 반대로 기기의 이벤트를 플랫폼으로 전달하는 중간 다리의 역할을 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이벤트 파이프라인은 기기 상태 변화를 수신하고, 기기가 연동된 플랫폼으로 그 이벤트를 전달하는 역할을 한다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;화재 감지, 강제 문 열림 알림, 동작 감지 등, 이벤트는&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;단순히 앱 상 UI 변경만이 아닌, &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;사용자 안전 문제와 직결된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;그렇기에 지연 시간과 유실률은 서비스 신뢰도 문제이며, &lt;/span&gt;안정적인 리소스 관리, 꼼꼼한 처리 증적 관리가 특히 더 중요했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;처리량 증가&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;서비스가 성장하며 이벤트가 크게 늘었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;2년 전까지만 해도 초당 2,000건이었던 기기 이벤트가, 이제는 초당 6,000건을 훌쩍 넘는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;아래는 한 달 동안의 이벤트 수 변화이다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;그 수위가 점점 높아지는 것을 볼 수 있다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; 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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OVQQT/dJMcafMBBPp/YaKmHgCH0thcp2d0z1TTbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OVQQT/dJMcafMBBPp/YaKmHgCH0thcp2d0z1TTbk/img.png&quot; data-alt=&quot;한 달 동안의 이벤트 수 변화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OVQQT/dJMcafMBBPp/YaKmHgCH0thcp2d0z1TTbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOVQQT%2FdJMcafMBBPp%2FYaKmHgCH0thcp2d0z1TTbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;289&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;289&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;한 달 동안의 이벤트 수 변화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;병렬&lt;/b&gt;&lt;b&gt; &lt;/b&gt;&lt;b&gt;처리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이벤트 수가 꾸준히 증가했지만, 파티션 수를 유연하게 늘리기 어려웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또 많은 양의 이벤트 수신을 위해 CPU 성능이 중요했고, 인프라 비용이 많이 나가 컨슈머를 마구 늘릴 수도 없는 노릇이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리 순서 역시 중요하기에 메시지를 단순히 병렬 처리하는 것은 방법이 되지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한 파티션에 수많은 기기의 이벤트가 적재되었고, 기기별 순서&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;보장을&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;위해서&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;파티션별 순차&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;처리해야 하는&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;비효율에 집중했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배치 수신한 이벤트를 기기 ID로 묶고, 같은 기기의 이벤트끼리는 순차 처리, 서로 다른 기기의 이벤트끼리는 병렬 처리를 구현했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;'파티션 단위의 단일 스레드 순차 처리 모델에서 벗어나, 애플리케이션 레벨에서의 병렬 처리를 구현했습니다.'&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;LLM이 멋진 표현으로&amp;nbsp;바꿔주었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기기별 이벤트 순서 보장은 보장하면서도, 초당 처리량을 기존 약 4,200개에서 18,000개까지 높일 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대신 '그렇게 늘어난 처리량을 기존 리소스로 버틸 수 있는가' 를 풀어야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순히 비용을 들인 스케일 아웃으로, 언젠가는 만날 근본적인 문제를 넘기고 싶지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;쿼리, 인덱스 튜닝&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리량은 크게 올랐지만, 정작 플랫폼으로 전달되는 이벤트 단건의 처리 시간은 늘었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;들쭉 날쭉한 DB 쿼리 시간과 CP Pending 수를 모니터링하여 DB 커넥션 풀의 커넥션 부족을 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동시 처리를 늘린 만큼, DB 액세스가 늘어 CP가 부족해졌고 커넥션 대기에 시간이 필요했던 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커넥션 풀을 조정을 위해 처리량과 함께 DB 서버의 CPU 사용률과 커넥션 수를 모니터링해야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리 팀의 경우엔 지금 다루는 애플리케이션의 감당 수준보다, 데이터베이스 자체의 가용률 체크가 더 중요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커넥션을 오래 잡고 있는 경우는 없을까 락과 놓친 인덱스를 점검했고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리 후 해당 서비스에서 2ms 이상 걸리는 쿼리는 없도록 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;빈도는 줄었지만 그럼에도 커넥션 부족과 &lt;span style=&quot;text-align: start;&quot;&gt;CP Pending을&lt;/span&gt;&amp;nbsp;완전히 피할 순 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;캐시 처리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일부 기기의 이벤트는 처리를 위해 다른 서비스와의 통신이 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;당장 응답 시간만큼의 블록킹이 발생했고, 외부 서비스의 상태에 직접 의존되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB 쿼리와 API 요청으로 확인하는 데이터를 &lt;span style=&quot;text-align: start;&quot;&gt;캐싱하기 시작했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Look Aside로 캐시 조회 후 DB를 확인했고, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;데이터 변경 시엔&amp;nbsp;&lt;/span&gt;Evict Only로 캐시 업데이트 없이 무효화했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;트랜잭션 커밋 후 캐시 업데이트 강제는 @TransactionalEventListener를 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;쓰기와 조회가 서로 다른 서비스에서 발생하기에, 데이터 변경 후 재조회되는 경우가 많지 않다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또 조회 성능이 중요한 서비스이기 때문에, 캐시 업데이트 처리에 다른 조회 요청들이 받는 영향을 최소화하고 싶었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;실제 조회가 발생했을 때 캐싱하여, &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&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;&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;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이 서비스에선 히트율이 약 60% 일 때, 캐시를 적용하지 않을 때와 같은 처리량을 갖는 것을 테스트했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실제 운영에선 DB 쿼리에서 90%, API 요청에선 95% 이상의 히트율을 갖으며 캐시 적용에 의의를 확인할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB, API 블록킹 시간이 제거되어, 이벤트 단건 평균 처리 시간이 87.2% 감소했다. (86ms &amp;rarr; 2ms)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또 그만큼 DB, 네트워크 커넥션 사용이 줄어 커넥션 풀 부족 문제가 해결되었다.&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배치 처리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이벤트 종류에 따라&amp;nbsp;기록이 필요한 데이터도 존재했다.&lt;/span&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;&quot;&gt;예를 들어 센서 기기의 이벤트는 DB에 저장되어, 특정 기간 동안의 상태 이력 조회 기능에 사용된다.&lt;/span&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;&quot;&gt;처리량을 높이면서 이 데이터 삽입을 위한 병목도 늘었다.&lt;/span&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;&quot;&gt;처음엔 쿼리 응답 대기를 제거해 보자는 생각으로&amp;nbsp;Reactive MongoDB 도입, 논-블록킹 삽입으로 처리량을 개선했다.&lt;/span&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;&quot;&gt;대신 DB CPU 사용률이 증가해, 사용 중인 DB 인스턴스 타입을 늘려야 하는 경계에 도달했다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;이에 배치 삽입을 도입했다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&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;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;이벤트를 모아 Bulk insert를 사용하면 네트워크 통신 비용, DB의 연산량, 커넥션 점유 경합에 개선이 있을 것이라 생각했다.&lt;/span&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;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;DB에 저장해야 하는 이벤트를 즉시 삽입하지 않고, Thread-safe 한 큐에 저장한다.&lt;/span&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;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;일정한 시간 간격으로 큐를 비우고, 배치 사이즈만큼씩 bulk insert를 처리하는 꼴로 구현했다.&lt;/span&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;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;만약 배치 삽입이 실패하면 재시도가 아닌, DLT로 전달, 빠르게 알림 받고 삽입 실패 이유를 분석하는 방법으로 예외 처리하였다.&lt;/span&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;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;Full-managed DB를 사용하고 있어, 문제는 DB 보단 데이터 자체나 로직에 문제가 있는 경우일 확률이 높고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;빠른 재시도의 실시간성보다는 조금 늦게 적재되더라도, 유실에 안전한, 언젠가는 기록됨이 더 중요해서였다.&lt;/span&gt;&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;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;다른 로직들을 제거하고, 단순 이벤트 저장 흐름으로 처리량을 테스트했을 때, &lt;/span&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;&quot;&gt;초당 3.5천 건 &amp;rarr; 2만 건으로 처리량 4배 이상 개선하면서도, 이전 Reactive 방식의 DB CPU 사용률 문제 안정화할 수 있었다.&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;버퍼와 백 프레셔&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스케줄링 기능이 있는 서비스 특성상, 매 정각마다 일시적인 이벤트 급증이 발생한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일시적인 이벤트 몰림에 맞춰 리소스를 급히 늘린다면,&amp;nbsp;과부하와 리소스 부족으로 인한 서비스 장애로 전파될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;카프카 메시지 수신 스레드 풀과 별개로, 메시지 처리를 위한 워커 스레드 풀을 구성했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스레드 수를 미리 지정하여, 처리할 스레드 개수를 제한하고 리소스 과부하를 막았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다고 평소 스레드 수로 처리되지 못한 메시지를 쉽게 버리지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스레드 풀의 대기열을 버퍼로 사용하여, 처리되지 못한 메시지를 담을 임시 공간을 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 대기열 사이즈를 지정하지 않으면, 처리되지 못한 태스크가 쌓였을 때 메모리를 모두 차지, OOM으로 이어질 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사이즈를 명확하게 하되, 가득 차면 메시지 수신 스레드에서 직접 메시지를 처리하여, 공간이&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;생길&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;때까지&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;다음 메시지 수신을 대기한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 처리량에 따라 메시지 수신 양을&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;수신처에서 &lt;/span&gt;조절하는 구조를 '백프레셔'라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;미리 설정한 수로 사용할 리소스를 제한하고, 대기열 상태에 따라 다음 메시지 수신을 조절할 수 있는 시스템을 만들 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SOAnX/dJMcagEKYy7/81jKVKFyv30BXKvBMzWAbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SOAnX/dJMcagEKYy7/81jKVKFyv30BXKvBMzWAbK/img.png&quot; data-alt=&quot;하루 동안의 이벤트 수 변화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SOAnX/dJMcagEKYy7/81jKVKFyv30BXKvBMzWAbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSOAnX%2FdJMcagEKYy7%2F81jKVKFyv30BXKvBMzWAbK%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;864&quot; height=&quot;241&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하루 동안의 이벤트 수 변화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;외부 장애 대응&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;잠깐의 캐시 비정상 동작, 외부 API 응답 속도 저하에도 크게 영향을 받는다.&lt;/span&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;&quot;&gt;이에 꼼꼼한 예외처리, 장애 지속 판별과 회로 차단이 중요했다.&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;span style=&quot;color: #000000;&quot;&gt;먼저 캐시 서버에 예외가 발생하면 DB를 직접 호출한다.&lt;/span&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;&quot;&gt;캐시에 반영하지 못한 더러워진 데이터는 따로 큐에 보관하여, 복구 시 캐시 무효화를 처리할 수 있도록 했다.&lt;/span&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;&quot;&gt;캐시 업데이트에 실패한 데이터가 일정 개수 이상이면, 관련 캐시 엔트리 전체를 제거하는 꼴로 구현했다.&lt;/span&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;&quot;&gt;서버 종료 시 큐에 남은 더러워진 데이터가 유실되지 않도록, Graceful shutdown으로 일정 시간 동안 캐시 업데이트를 반복 시도한다.&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;span style=&quot;color: #000000;&quot;&gt;API 호출, 레디스 장애 상황에선 그 호출 자체도 비용이다.&lt;/span&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;&quot;&gt;특히 느린 응답의 반복은 전체 처리 지연으로 이어질 수 있다.&lt;/span&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;&quot;&gt;API 서버와 캐시 요청에 서킷 브레이커를 적용해서, 장애 지속을 판단하고 일정 시간 접근 시도를 피한다.&lt;/span&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;&quot;&gt;테스트를 반복하며 외부 API 응답 시간에 따른 리소스 변화, 처리량 변화를 미리 파악하였고, 장애 판단의 기준을 잡을 수 있었다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;테스트 환경 구성&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부 API 응답 속도에 따른 리소스 변화를 확인하기 위한 테스트 환경 구성이 필요했다.&lt;/span&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;&quot;&gt;외부 서비스를 Wiremock으로 대체, &lt;span style=&quot;text-align: start;&quot;&gt;서버 응답 속도와 값을 제어하여 상황별 리소스 변화와 장애 판단 기준을 만들 수 있었다.&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&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;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;실제 운영보다 많은 수의 기기 이벤트와 DB 로우를 가정하여 환경을 구성했다.&lt;/span&gt;&lt;/span&gt;&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;span style=&quot;color: #000000;&quot;&gt;아래는 외부 API 응답 속도에 따른 변화를 테스트했을 때의 대시보드이다.&lt;/span&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;&quot;&gt;Scrap 시간 단위마다 Prometheus에 직접 요청하여 실시간 값을 반영하는 꼴로 만들었다.&lt;/span&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;&quot;&gt;그라파나 대시보드는 평소 전체 상태 확인을 위한다면, 이는 원하는 매트릭만 골라 값 변화를 명확하게 확인하기 좋았다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;아래는 동일한 DB CP, 네트워크 커넥션 풀, 워커 스레드 풀 설정에, 외부 API 속도만 변경했을 때이다.&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&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;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;200ms까지 단건 처리 시간이 약간 늘긴 했지만, 처리량이 줄거나 &lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;워커 스레드 풀에서 대기가 발생하는 경우가 없었다.&lt;/span&gt;&lt;/span&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;반면 250ms에서 워커 스레드 풀에서 대기(버퍼링)가 발생하면서 단건 처리 시간이 늘고, 처리량이 급격히 줄은 것을 볼 수 있다.&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;span style=&quot;color: #000000;&quot;&gt;이렇게 외부 변수에 따른&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;처리량, 서버 리소스, 처리 속도 변화를 확인했다.&lt;/span&gt;&lt;/span&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;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwo7YC/dJMcafsiRlc/kf7JnAu4ZaKQltOKIrnXeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwo7YC/dJMcafsiRlc/kf7JnAu4ZaKQltOKIrnXeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwo7YC/dJMcafsiRlc/kf7JnAu4ZaKQltOKIrnXeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwo7YC%2FdJMcafsiRlc%2Fkf7JnAu4ZaKQltOKIrnXeK%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;2816&quot; height=&quot;832&quot; data-origin-width=&quot;2816&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;모니터링 환경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;실시간 리소스 값 확인과는&lt;/span&gt; 또 다른, 특정 기간 동안의 변화를 확인할 수 있는 방법 역시 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이에 주요 매트릭을 정의하고, 이를 애플리케이션에서 계산, 수집할 수 있도록 개발하고&amp;nbsp;그라파나 대시보드를 재구성하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래는 구성한 그라파나 대시보드 중 처리량과 관련된 매트릭을 모은 그룹의 예시 이미지이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;OpenTelemetry + J&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;aeger로 APM을 구성하여, 이벤트 처리 구간별 지연 시간을 확인했지만,&amp;nbsp;&lt;/span&gt;&lt;/span&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;&quot;&gt;이벤트 양이 워낙 많아&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Jaeger가 수집할 수 있는 샘플링 비율이 매우 적었다.&lt;/span&gt;&lt;/span&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;background-color: #ffffff; 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;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본 JVM 매트릭 만이 처리량, 유실률, 캐시 히트율과 API 요청과 응답 시간, 처리 지연 시간 등 파이프라인의 성능을 주로 보고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 데이터들은 단순히 '애플리케이션이 정상임'을 넘어, 다음 개선 포인트를 확인할 수 있는 배경이 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특히 '해당 시간대엔 몇 건의 유실이 발생했고, 평균 Nms로 처리되었다.'를 보이는 &lt;span style=&quot;text-align: start;&quot;&gt;외부 플랫폼과의 소통을 위한 증적으로도 활용된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;1102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x1wHF/dJMcad2e03Z/eo0wTZmdXNgRHN11J5v4Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x1wHF/dJMcad2e03Z/eo0wTZmdXNgRHN11J5v4Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x1wHF/dJMcad2e03Z/eo0wTZmdXNgRHN11J5v4Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx1wHF%2FdJMcad2e03Z%2Feo0wTZmdXNgRHN11J5v4Kk%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;1265&quot; height=&quot;1102&quot; data-origin-width=&quot;1265&quot; data-origin-height=&quot;1102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #5c5c5c; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;정리&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작업 : 이벤트 파이프라인 성능 개선&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배경 :&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 이벤트 파이프라인은 기기 상태 변경 이벤트를 수신하여, 기기가 연동된 플랫폼 서버에 이를 전달&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 서비스가 성장하며 이벤트 증가. 초당 2,000건에서 6,000건&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 기존 리소스와 로직으로는 처리량이 부족하여 OOM, 처리 지연 발생, 이에 개선 필요&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리 :&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 병렬&amp;nbsp;처리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 이벤트 브로커에서 수신한 이벤트를 기존 파티션별 순차처리에서 기기별 순차 처리로 변경&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 배치 수신한 이벤트를 기기 ID로 묶고, 같은 기기의 이벤트끼리는 순차 처리, 서로 다른 이벤트끼리는 병렬 처리 구현&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 초당 4200건에서 18000건으로, &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;초당 처리량 약 4.2배 증가&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 쿼리, 인덱스 튜닝&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- DB 동시 액세스가 늘며 CP 커넥션 부족&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 커넥션 풀 조정, 쿼리, 인덱스 재점검 등 커넥션 점유 시간 감소&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 캐시 처리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- DB 쿼리, API 조회 데이터 캐싱&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- Look aside, Evit Only, DB 트랜잭션 커밋 이후 캐시 업데이트 처리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 배치 처리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 이벤트 내용 DB 삽입 과정에서 병목 발생&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 이벤트 배치 수신 그룹별 Bulk insert 구현&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 단순 DB 삽입에 초당 3.5천 건 &amp;rarr; 2만 건으로 처리량 4배 이상 개선, CPU 사용률 정상화&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 버퍼, 백&amp;nbsp;프레셔&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 이벤트 수신 스레드와 분리된 워커 스레드 구성&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 이벤트 처리에 사용할 스레드 수 고정, 일시적 트래픽 급증에도 동일한 리소스 사용 제한&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 스레드 풀 대기열을 버퍼로 사용, 평소 처리량으로 처리되지 않은 메시지 임시 보관&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 대기열이 가득차면 메시지 수신을 대기, 처리량에 따라 메시지 수신을 조절하는 백-프레셔 구조 구성&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 외부 장애 대응&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 캐시 : 캐시 서버 장애 시 DB 직접 접근. Dirties 데이터 관리&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 외부 서비스 호출 : 서킷 브레이커를 사용하여, 장애 지속 시 요청 제한. DLT를 사용하여 유실 대비&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 테스트 환경 구성&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 외부 서비스를 WireMock으로 대체&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 응답 속도에 따른 리소스 사용, 처리량 변화 테스트&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;리소스를 조정하거나 회로 차단의 재난 기준을 설정하는 근거 마련&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;모니터링 대시보드 구성&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp;- 처리량, 유실률, 캐시 히트율과 API 요청과 응답 시간, 이벤트 단건 처리 시간 계산 및 수집&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp;- 외부 플랫폼과의 소통을 위한 증적 강화&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>KimJinHwan/Project</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1177</guid>
      <comments>https://ecsimsw.tistory.com/entry/event-pipeline-refactor#entry1177comment</comments>
      <pubDate>Wed, 11 Mar 2026 23:39:43 +0900</pubDate>
    </item>
    <item>
      <title>멱등성 보장 : 저장 구조 변경으로 레디스 메모리 사용률 33% 개선</title>
      <link>https://ecsimsw.tistory.com/entry/%EB%A0%88%EB%94%94%EC%8A%A4-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B0%9C%EC%84%A0-%EB%A9%B1%EB%93%B1%EC%84%B1-%EB%B3%B4%EC%9E%A5%EC%9D%84-%EC%9C%84%ED%95%9C-%ED%82%A4-%EC%A0%80%EC%9E%A5-%EA%B5%AC%EC%A1%B0-%EB%B3%80%EA%B2%BD</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;알림 서비스 이야기이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;카프카로부터 메시지를 수신하고 이를 사용자에게 전달하는 서비스를 운영하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 통신 비용 절감을 위해 카프카에서 메시지를 배치 수신, 그룹 단위로 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그룹 단위 처리 중 예외가 발생하면 이미 처리한 내용을 잊기에 복구 시 메시지를 중복 수신하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이에 수신한 메시지 ID를 레디스에 기록하고, 처리 전 비교하는 것으로 이전에 수신했던 메시지 여부를 검증한다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;간단한 메시지 ID 저장 구조 변경으로, 레디스 메모리 사용률을 33% 개선한 경험을 소개한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;String vs Hash&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 구조는 수신한 각 메시지 ID를 레디스 String 타입으로 저장했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;양이 많고, 짧은 보관 기간의 데이터이기에, 각 메시지 ID를 레디스 Key로 하여 TTL을 명확하게 지정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대신 각 데이터마다 메타 데이터가 필요하여 메모리 사용량이 많다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레디스 Hash는 해시테이블 하나를 레디스 Key로 하고, 그 안에서 Key:Value 구조로 저장하는 구조이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레디스 Key를 하나만 사용하기에 필요한 메타데이터를 최소화하고, Hash 내부에서 키 검색도 O(1)이면 된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대신 해시 테이블 자체를 하나의 레디스 Key로 하여, 메시지 ID 각각의 TTL 설정은 불가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;구간 기록&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;알림 서비스에선 이벤트 양이 많다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레디스 Key를 줄이는 것만으로도 메모리 절감에 효과적일 것이라 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 메시지 ID를 String 저장이 아닌, '일정 구간 동안의 메시지를 한 해시테이블로 담자'가 그 개선 아이디어였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 TTL이 10분이라고 하면, &lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10시 10분부터 10시 15분까지의 메시지 ID를 담는 테이블을 만들어 적재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수신한 메시지를 구간에 따라 해시테이블 내부에서 비교하는것으로 중복 여부를 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIPGnc/dJMcaf6L5pw/QYewvSKdzLKJMamvUAwlL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIPGnc/dJMcaf6L5pw/QYewvSKdzLKJMamvUAwlL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIPGnc/dJMcaf6L5pw/QYewvSKdzLKJMamvUAwlL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIPGnc%2FdJMcaf6L5pw%2FQYewvSKdzLKJMamvUAwlL0%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;541&quot; height=&quot;173&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래는 기존 String 방식의 setIfAbsentWithString와 개선한 Hash 방식의 setIfAbsentWithHash 의 코드 비교이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Hash 사용 코드에선 구간별 레디스 Key를 생성하고, 해시 테이블에 메시지 ID를 기록하는 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 현재 그 해시 테이블이 존재하는지 여부를 확인하지 않고 TTL을 지정하는 방법으로 처리했다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772466261598&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public boolean setIfAbsentWithString(String msgId, long timeoutMs) {
    var wasSet = redisTemplate.opsForValue().setIfAbsent(msgId, &quot;&quot;, timeoutMs, TimeUnit.MILLISECONDS);
    return wasSet != null &amp;amp;&amp;amp; wasSet;
}

public boolean setIfAbsentWithHash(String msgId, long timeoutMs) {
    var hashKey = generateHourlyKey();
    var isNewField = redisTemplate.opsForHash().putIfAbsent(hashKey, msgId, &quot;&quot;);
    if (isNewField) {
        redisTemplate.expire(hashKey, timeoutMs, TimeUnit.MILLISECONDS);
        return true;
    }
    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;메모리 개선&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 구간별 키 기록으로 개선한 메모리 사이즈를 확인하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 운영 기준 초당 6천 개의 메시지가 발생하고 있고, &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;TTL 10분을 기준으로 약 360만 개의 메시지가 쌓일 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;360만 개의 메시지 ID를 기존 구조와 개선 구조로 기록하고 각각의 사용 메모리를 비교했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 구조에선 레디스 키 360만 개가 생성, 약 390MB의 메모리가 사용되었고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개선 구조에선 레디스 키가 2개 생성, 약 260MB의 메모리가 사용되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;간단한 구조 개선만으로 약 약 33.3%가 개선된 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지금보다 더 큰 규모의 서비스에선 더 많은 메모리 변화가 있을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bms7ya/dJMcafThjue/taBp3DL1VAi2htjkDXEs9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bms7ya/dJMcafThjue/taBp3DL1VAi2htjkDXEs9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bms7ya/dJMcafThjue/taBp3DL1VAi2htjkDXEs9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbms7ya%2FdJMcafThjue%2FtaBp3DL1VAi2htjkDXEs9k%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;539&quot; height=&quot;121&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;424&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;HASH 타입의 두 가지 데이터 저장 방법&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레디스 Hash 타입에서 데이터를 저장하는 방식은 두 가지이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;'HashTable'는 익히 아는 Key:Value 방법이고, '&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Listpack' 방식은 {Key:Value} 자체를 연속된 메모리에 저장하는 식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HashTable 방식은 O(1)의 탐색 비용이 들지만, Listpack은 순차탐색(O(N))이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대신 Listpack 방식은 HashTable 방식보다 훨씬 더 적은 메모리를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇기에 Listpack은 메모리도 적으면서도 O(N)의 비용이 적을 때, 즉 탐색할 엔트리가 적을 때 유리하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레디스 Hash는 512 개 이하의 엔트리에선 Listpack으로, 그 이상은 HashTable를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 기준 값은 설정 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;엔트리가 늘어 Listpack에서 HashTable로 변경된 데이터는 다시 돌아가지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;OBJECT ENCODING $REDIS_KEY_NAME&quot; 을 커멘드로 사용하여 어떤 방법으로 저장하는지 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;아쉽지만 512개를 훌쩍 넘은 데이터를 저장하는 내 경우엔, &lt;/span&gt;Listpack의 공간 효율적인 메모리 관리는 불가능했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OkMax/dJMcac90dlN/WPkEOX3DwB5igvHBXgvrQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OkMax/dJMcac90dlN/WPkEOX3DwB5igvHBXgvrQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OkMax/dJMcac90dlN/WPkEOX3DwB5igvHBXgvrQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOkMax%2FdJMcac90dlN%2FWPkEOX3DwB5igvHBXgvrQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;680&quot; height=&quot;106&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;236&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;성능 개선과 Lua 스크립트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 구간별 기록에 레디스 키를 위한 계산하고, 매번 Hash에 TTL을 설정하는 두 커멘드를 전달했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 구간의 해시 테이블이 존재하는지 여부를 확인할 수 없어 매번 해당 레디스 키에 TTL을 지정하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;요청량이 많지 않은 경우에선 가시적인 영향을 주지 않겠지만, 요청량이 많은 경우는 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;레디스를 한번 더 요청하는 것만으로도 큰 성능 저하가 보였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존의 방식으로 레디스를 한 번만 요청할 때는 10초 동안 14만 개의 메시지를 처리할 수 있었고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개선 방식으로 HASH를 사용해 두 번의 커멘드를 요청할 때는 10초 동안 11만 개의 메시지를 처리할 수 있음을 테스트했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이에 아래와 같이&amp;nbsp;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Lua 스크립트를 사용하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;HSETNX와 TTL을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;한번에 처리할 수 있도록 개선했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트 결과 기존 String으로 단일 커멘드를 전달하는 것과 동일한 성능으로 처리할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772465931700&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;local wasSet = redis.call('HSETNX', KEYS[1], ARGV[1], ARGV[2])
if wasSet == 1 then
  redis.call('PEXPIRE', KEYS[1], ARGV[3])
end
return wasSet&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;구간 사이의 유실&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;성능 말고도 또 하나의 놓칠 수 있는 포인트가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 구간 사이에서 메시지 중복이 발생하면 이를 확인할 수 없다는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어 10시 10분 ~ 10시 15분 구간에 A라는 메시지 ID를 적재했음을 가정하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 구간 사이 동안 오류가 발생하여 A를 재수신했고, 다음 구간에서 중복 검사가 수행된다면 이는 미처리로 조회될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 구간 사이 문제를 피하기 위해 수신 시간이 아닌, 메시지 고유 시간을 기준으로 구간을 정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메시지 고유의 시간을 기준으로 한다면, 수신 시점에 상관없이 저장하고 확인할 구간이 동일히게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내 경우에는 이벤트 최초 발행된 시각을 기준으로 하여, 기준 사이의 오차 위험을 제거했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6f5GV/dJMcajafRw7/WWUnx9XKfR6idUklxCxwZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6f5GV/dJMcajafRw7/WWUnx9XKfR6idUklxCxwZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6f5GV/dJMcajafRw7/WWUnx9XKfR6idUklxCxwZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6f5GV%2FdJMcajafRw7%2FWWUnx9XKfR6idUklxCxwZK%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;511&quot; height=&quot;158&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;이슈&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 카프카 Batch 수신 시, 레디스 중복 처리 문제와 레디스를 사용한 멱등성&amp;nbsp;보장&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 수신한 메시지 ID를 레디스에 String으로 저장하고 비교하는 것으로, 이미 처리한 메시지 여부 확인&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 각 메시지 ID가 레디스 Key로 저장되기에, 이벤트 파이프라인처럼 많은 메시지 보관 시 메모리 비효율적 사용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;구간 적재&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- Hash를 사용하여 레디스 Key 사용 최소화, O(1) 조회&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 시간을 기준으로 구간별 여러 메시지 ID를 한 Hash로, 레디스 키를 하나만 사용하여 저장&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 메시지 사용 33.3% 절감&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;성능 개선&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- Hash 사용 시 HSETNX와 TTL 설정 쿼리, 레디스 요청 필요&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 매우 적은 레디스 사용 비용이지만, 이벤트 파이프라인처럼 이벤트가 많을 경우 유의미한 성능 차이 발생&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- Lua 스크립트를 사용하여 앞선 두 가지 쿼리를 하나의 요청으로 처리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 동일 성능 테스트&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;구간 사이 유실&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 레디스 조회 및 적재 시점을 기준으로 커멘드를 처리하면 구간 사이에서 중복 처리에 노출 위험&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 메시지 별 고유 시간, 예를 들어 메시지 발생 시간을 기준으로 구간을 지정하는 것으로 매번 동일한 구간 확인 처리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 구간 사이의 저장과 조회 시에도 중복 처리 노출 위험 제거&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture/Application</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1175</guid>
      <comments>https://ecsimsw.tistory.com/entry/%EB%A0%88%EB%94%94%EC%8A%A4-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B0%9C%EC%84%A0-%EB%A9%B1%EB%93%B1%EC%84%B1-%EB%B3%B4%EC%9E%A5%EC%9D%84-%EC%9C%84%ED%95%9C-%ED%82%A4-%EC%A0%80%EC%9E%A5-%EA%B5%AC%EC%A1%B0-%EB%B3%80%EA%B2%BD#entry1175comment</comments>
      <pubDate>Tue, 3 Mar 2026 00:20:58 +0900</pubDate>
    </item>
    <item>
      <title>카프카 사용 전략 : 수신부의 재시도, 복구 정책 고민</title>
      <link>https://ecsimsw.tistory.com/entry/kafka-consumer-retry-recovery</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 아래의 상황에서 카프카를 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 처리에 시간이 걸려서 메인 흐름 부에서 분리하고 싶을 때&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 서비스 간 안전하고 순서가 보장된 이벤트 전달이 필요할 때&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;물론 비동기 처리와 이벤트 전달 방법은 많지만 방법마다 다른 제약이 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- Http : 이벤트를 전달받는 쪽의 상태에 의존적&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- Redis PS : Fire and Forget. 쏘기만 하고 잘 받았는지는 검증 않음&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- RabbitMQ : 여러 수신처에서 동일하게 수신하려면 여러 개 큐가 필요. 꼼꼼한 순서 보장을 위해선 단일 컨슈머 필요&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;카프카를 다루면서 고민해야 하는 포인트가 있다면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 브로커 측면 : 파티션 크기, 브로커 성능과 수&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 프로듀서 측면 : 메시지 키, 배치 크기, 배치 간격, 직렬화 방법&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 컨슈머 측면 : 컨슈머 그룹, 커밋 방법, 초기 오프셋 위치, 수신 배치 크기, 멱등성, 재시도 처리와 DLT&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최근 이벤트 파이프라인 개선을 맡다 보니, 부족했던 재시도 처리나 복구 정책을 다시 고민할 수 있는 시간이 많았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스나 이벤트 종류별로 달랐던 고민들을 기억하고 싶어 이를 정리하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 글에서는 메시지 처리에 실패했을 때의 커밋 전략과 재시도, 복구 전략을 내 경험에 물려 소개한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Auto Commit&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Auto commit은 끄는 것을 선호한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Auto commit은 미리 설정한 인터벌 시간 간격으로 그 시점까지 수신한 Kafka 마지막 오프셋을 커밋한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;보통 카프카 수신처는 네트워크 통신 비용을 아끼기 위해 배치로 이벤트를 가져온다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 1000개의 이벤트를 배치로 가져와서 600개까지 정상 처리한 상황에서 커밋이 수행된다면,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나머지 400개는 처리를 시도하지도 않고 완료 커밋되는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 그 시점에 문제가 생겼다가 복구되면 400개의 유실이 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반대로 600개까지 정상 처리한 상황에서 서비스가 갑자기 종료되면 어떨까&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자동 커밋이 수행되기 이전이기에 브로커는 해당 컨슈머가 600개를 이미 처리했는지 모를 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨슈머가 복구되면 앞서 처리한 600개를 다시 수신하게 되는 중복 처리 문제가 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Ack Strategy&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Auto commit을 끄고, 커밋 전략을 직접 선택할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처리 단건 별로 커밋하는 Record, 배치 단위로 커밋하는 Batch, 애플리케이션에서 직접 결정하는 Manual이 대표적이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Batch 모드는 수신한 배치 메시지를 모두 처리한 시점에서 마지막 오프셋을 커밋한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Auto commit과는 달리, 처리되지 않은 메시지를 커밋하여 유실이 발생하는 경우는 생기지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대신 중간에 예외에 발생하면 Ack를 하지 않아, 모든 배치 메시지를 다시 수신하게 되고 중복이 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단건으로 커밋하는 Record를 사용하면 유실과 중복 수신 문제에서 안전해 보인다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대신 매 메시지를 처리할 때마다 ACK가 필요하기에 네트워크 비용이 든다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;안전하지만 네트워크 비용과 성능 측면에서 비효율적일 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내 경우에는 성능이 중요하고 중복 문제에 안전한 서비스에선 Batch를, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;안전이 우선되는 서비스에선 Record를 먼저 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결국 서비스 성격에 맞는 전략 선택이 필요해 보인다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;중복 처리 문제&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 커밋 모드에 따라 중복 처리에 대한 대비가 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 메시지 ID를 기록하고, 로직 전 처리 여부를 비교하는 방법을 사용하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB의 UPSERT나 키 중복 방지 쿼리를 사용할 수 있을 것 같고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Redis 같은 공유 메모리에 처리한 메시지 ID를 기록해 두고 비교하는 것도 방법이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;멱등성 처리가 안되어 있는 카프카 수신 서비스에 멱등성을 처리한 경험이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단순히 중복 처리에 안전하다는 것도 있지만, 배포할 때 큰 힘이 됨을 배웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존 멱등성 보장이 안되어 있을 때는 이전 버전과 새로운 버전을 동시에 띄우는 시점에 중복 문제가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다고 중복 문제를 피하려면 이벤트 처리를 완전히 끝내고 커밋까지 마쳐야 다음 버전을 배포할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;멱등성 처리 이후에는 두 버전을 동시에 띄워 중단을 최소화하면서도, 중복 문제에 안전한 배포를 만들 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;독약 문제&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;독약 문제(Poison Pill)는 카프카 토픽에 올라갔지만 수신처에서 항상 실패하는 메시지를 말한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;한번 독약이 발생하면 그 메시지가 유실되지 않는 이상 해당 파티션은 다음 메시지를 받아 처리할 수 없기에 매우 주의해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;예를 들어 약속되지 않는&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp;메시지 포맷으로 수신처에서 처리할 수 없는, 발신처의 코드 수정이 필요한 메시지가 독약이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;적절한 예외 처리가 없다면 Ack를 만들지 못하고, 메시지 수신과 처리 실패를 무한히 반복하게 될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;내 방법은 &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;수신처에서 최대한 &lt;/span&gt;넓은 범위로 예외를 잡아 대응하고, 카프카에는 정상 처리를 알리는 것이다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;수신처에서 기록이나 알림, 복구 정책을 처리하고 카프카엔 Ack를 전달한다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;메시지 자체나 수신처의 로직 문제로 커밋 실패가 되는 경우를 최소화하고, 브로커부터의 메시지 수신 성공 여부에 집중하게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;특히 재시도 이후에도 정상 처리를 완료하지 못한 메시지를 어떻게 다룰 것인지가 재밌다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;서비스 성격이나 이벤트에 따라 꾸준히 재시도해야 할 때도 있고, 알람과 로그만 남기고 무시해야 하는 경우도 있을 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;다음 단락부터는 서비스 성격에 따라 전략을 달리했던 몇 가지 경험을 소개한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;복구 전략 1. 수정과 복구&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이벤트를 수신해서 필터링, DB에 넣어야 하는 서비스였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;급한 재시도가 아닌, 유실 없이 처리되는 것이 중요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반복된 재시도가 아닌 에러 원인을 확인하고 수정, 다시 처리될 수 있도록 메시지의 관리의 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재시도까지 실패한 메시지를 담는 복구용 토픽(DLT)을 만들어 관리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DLT에 메시지가 올라오면 이를 수신하는 알림 서비스가 개발자가 알 수 있도록 알림을 보낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발자는 실패 원인을 분석해서 처리 부를 수정하고, DLT에 올라온 메시지를 재처리할 수 있도록 복구한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DLT의 메시지를 복구하는 방법도 여러 가지다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원본 토픽으로 Redrive 방법은 변경점 없이 쉽게 재수신을 만들 수 있지만, 다른 컨슈머들의 중복 처리 문제가 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;복구를 위한 임시 애플리케이션을 만들어 DLT 자체를 수신하고 처리를 마치는 것도 방법이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 DLT를 사용한 실패 이벤트 관리는 에러 메시지 자체를 정확하게 확인하고 간편히 복구 시도를 처리하는데 유리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;복구 전략 2. 재시도 반복&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자의 특정 상태 이벤트를 수신하여, 외부 서비스에 전달해야 하는 로직이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이벤트 양이 많지 않고 안전이 중요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 외부 서비스 장애로 요청 전달에 실패해도 넓은 간격으로 재시도 반복이 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 경우에는 요청 전달에 실패한 이벤트를 DB나 다른 큐에 쌓고, 반복적으로 이를 확인해서 전달 시도를 반복했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사전에 정의되지 않은 실패 코드인 경우에는 1번 정책과 마찬가지로 DLT로 관리했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;복구 전략 3. 무시&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;알림 처리를 위해 사용되는 서비스였다.&lt;/span&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;&quot;&gt;성능이 중요했고, 재시도를 오래 끌고 가지 않아야 했다.&lt;/span&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;&quot;&gt;유실보다 뒤늦은 전달을 더 피해야 하는 케이스였다.&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;span style=&quot;color: #000000;&quot;&gt;이 경우에는 수신처에서 빠른 재시도 이후에도 처리되지 않은&amp;nbsp;메시지는 단순 로그만 남기고 포기한다.&lt;/span&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;&quot;&gt;그런 버그가 지정 시간 안에서 여러 번 발생하면 이를 개발자가 알 수 있도록 알림을 남기는 정도였다.&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스 상황과 메시지 타입별 경험을 정리해 보았는데, 역시 정답은 없는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;이슈&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 카프카 커밋과 실패 시 복구 전략&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;커밋 전략&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - Auto commit : 간편하지만 중복 처리 문제와 유실 문제가 존재&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - Batch 모드 : Ack를 배치 수신 단위로 처리하여 유실 문제에서 안전하지만, 중복 처리 문제 여전히 존재&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - Record : 유실과 중복 처리 문제에 안전하지만, 네트워크 비용과 성능에 비효율적&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;두 가지 문제&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 중복 처리 문제 :&amp;nbsp; DB의 중복 키 방지 혹은 Upsert, Redis를 사용한 처리 메시지 비교로 멱등성 처리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 독약 문제 : 수신처에서 최대한 넓은 예외 처리와 커밋, Ack는 메시지 수신 성공 여부에 집중&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;복구 전략&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 수정과 복구 : 재시도까지 실패한 메시지를 DLT로 옮기고 수정, 재시도 반복&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 재시도 반복 : DB에 저장하고 넓은 간격으로 재시도 스케줄링&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 무시 : 단순 로그, 알림만 처리하고 무시. 오히려 늦은 재시도가 더 위험한 경우&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture/Application</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1162</guid>
      <comments>https://ecsimsw.tistory.com/entry/kafka-consumer-retry-recovery#entry1162comment</comments>
      <pubDate>Wed, 24 Dec 2025 18:26:25 +0900</pubDate>
    </item>
    <item>
      <title>웹 소켓 서버 구조 개선 : Api Gateway 를 사용한 상태 리스 전환</title>
      <link>https://ecsimsw.tistory.com/entry/api-gw-ws</link>
      <description>&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;IoT 서비스 회사에서 백엔드 개발자로 일하고 있다.&lt;/span&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;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;기기&amp;nbsp;&lt;/span&gt;사용 환경을 넓히기 위해, 자체 플랫폼 외 LG, 삼성, 네이버, 카카오, KT 등 다른 국내 IoT 플랫폼과의 연동 서비스를 지원한다.&lt;/span&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;&quot;&gt;브라우저용 웹 대시보드도 그중 하나이다.&lt;/span&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;&quot;&gt;나는 대시보드에서 발생한 제어 요청을 기기에 전달하고, 반대로 기기의 이벤트를 대시보드에 전달하는 중간 다리 역할을 맡는다.&lt;/span&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;&quot;&gt;이 글에선 웹 소켓 서버의 관리 어려움과 이를 개선하기 위한 구조 변경 경험을 소개한다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;전통적인 웹 소켓 서버 아키텍처&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;웹 소켓, SSE 같은 연결 유지가 필요한 서비스는 관리가 어렵다.&lt;/span&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;&quot;&gt;자원 제한으로 서버의 수평 확장이 불가피하면서도,&lt;/span&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;&quot;&gt;분산된 서버 환경에서&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;연결 정보가 있는 인스턴스를 찾을 수 있는 아키텍처가 필요하다.&lt;/span&gt;&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 제한된 자원&lt;/b&gt;&lt;/span&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;&quot;&gt;서버 한 대에서 연결할 수 있는 소켓&lt;span style=&quot;text-align: start;&quot;&gt;(FD)&lt;/span&gt;&amp;nbsp;수에 물리적 한계가 존재한다.&lt;/span&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;&quot;&gt;모든 세션 정보를 메모리에 상주시켜야 하므로 동시 접속자 수에 제약이 있다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. 강한 종속성&lt;/b&gt;&lt;/span&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;&quot;&gt;클라이언트와 서버가 한번 연결되면, 해당 연결은 최초 수립된 인스턴스에 종속된다.&lt;/span&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;&quot;&gt;이로 인해 메시지 전달을 위해선 클라이언트가 연결된 서버를 찾아야 하는 복잡함이 있다.&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;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 해결하기 위한 대표적인 아키텍처는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;고정 통신 (라우팅 테이블 방식)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;클라이언트가 연결된 서버를 기록하는 별도 테이블을 두고, 메시지 전달마다 클라이언트의 연결 서버를 확인하는 방법이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구조는 단순하지만, 연결과 끊김에 따라 동적으로 변하는 라우팅 테이블 관리가 어렵다는 단점이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;전역 발행 (브로드캐스트 방식)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;모든 웹 소켓 서버에 이벤트를 전역 발행하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수신한 웹 소켓 서버는&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;자신에게 연결된 클라이언트인지 여부를 확인하고 전송 여부를 결정한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 간 의존성을 낮춰 확장에는 유리하나, 불필요한 메시지 전송으로 인한 네트워크 비용과 서버 부하가 발생할 수 있다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;그룹 발행 (샤딩 방식)&lt;/b&gt;&lt;/span&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;&quot;&gt;전역 발행 방식의 불필요한 전송과 고정 통신 방식의 의존 문제를 섞는 대안책이다.&lt;/span&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;&quot;&gt;미리 정의된 규칙으로 샤드키를 구하고, 이에 따라 연결될 서버 그룹이 결정한다.&lt;/span&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;&quot;&gt;전역 발행 방식보다 비용과 서버 부하면에서 효율적이며,&amp;nbsp;고정 통신 방식보다 유연하다.&lt;/span&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;&quot;&gt;대신 해시 키에 대한 서버 그룹이 변경되면 이 또한 또 다른 관리 포인트가 되며,&lt;/span&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;&quot;&gt;해시 규칙을 잘못 정의했을 때 특정 서버 그룹에 연결이 쏠리는 핫스팟 문제도 유의해야 한다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;실제 운영 이슈&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존에는 브로드캐스트 방식으로 운영하고 있었다.&lt;/span&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;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;분명 부하를 못 버틸 상황은 아니다.&lt;/span&gt;&lt;/span&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;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;다만 웹 소켓 서버와 알림 서버가 분리되어 관리해야 할 서버가 늘었다는 불편함이 있었고,&lt;/span&gt;&lt;/span&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;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;소켓 연결의 메모리 차지에 대한 걱정으로, 실제 사용 메모리보다 여유로운 인스턴스를 사용한다는 비효율이 있었다.&lt;/span&gt;&lt;/span&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;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;또 여러 서비스를 거쳐 전달하게 되니, 알림이 유실되었을 때 쫓아가야 할 서비스 홉이 많은 것도 불편했다.&lt;/span&gt;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2408&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/calCCQ/dJMcabRckCx/ynoERJkViXacngseIvxrek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/calCCQ/dJMcabRckCx/ynoERJkViXacngseIvxrek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/calCCQ/dJMcabRckCx/ynoERJkViXacngseIvxrek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcalCCQ%2FdJMcabRckCx%2FynoERJkViXacngseIvxrek%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;686&quot; height=&quot;201&quot; data-origin-width=&quot;2408&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1.&amp;nbsp;알림&amp;nbsp;서버는&amp;nbsp;브로커로부터&amp;nbsp;기기&amp;nbsp;상태&amp;nbsp;이벤트를&amp;nbsp;수신하고,&amp;nbsp;전달&amp;nbsp;포맷에&amp;nbsp;맞춰&amp;nbsp;이벤트를&amp;nbsp;컨버팅 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2.&amp;nbsp;유저&amp;nbsp;정보와&amp;nbsp;함께&amp;nbsp;Redis&amp;nbsp;Pub/Sub을&amp;nbsp;사용하여&amp;nbsp;웹&amp;nbsp;소켓&amp;nbsp;서버에&amp;nbsp;광역&amp;nbsp;전달한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.&amp;nbsp;웹&amp;nbsp;소켓&amp;nbsp;서버는&amp;nbsp;Map&amp;nbsp;{&amp;nbsp;유저&amp;nbsp;정보&amp;nbsp;:&amp;nbsp;클라이언트&amp;nbsp;}&amp;nbsp;을&amp;nbsp;바탕으로,&amp;nbsp;연결된&amp;nbsp;클라이언트에&amp;nbsp;상태를&amp;nbsp;전달한다.&lt;/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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;API Gateway 를 활용한 서버리스 아키텍처&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;관리 포인트 간소화를 위해, API Gateway WebSocket 를 중심으로 한 서버리스 아키텍처를 도입하였다.&lt;/span&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;&quot;&gt;새로운 구조를 통해 다음과 같은 장점을 얻을 수 있었다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. 자동 확장&lt;/b&gt;&lt;/span&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;&quot;&gt;앞선 인스턴스의 물리적 연결 수 한계나 메모리 관리에서 벗어날 수 있다.&lt;/span&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;&quot;&gt;API Gateway가 연결 관리를 전담하고 수백만 개의 동시 연결이 자동으로 처리한다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. 사용량 기반 비용&lt;/b&gt;&lt;/span&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;&quot;&gt;연결 유지 시간과 메시지 수에 따라 사용한 만큼만 지불한다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. 상태 비저장&lt;/b&gt;&lt;/span&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;&quot;&gt;연결 상태를 API Gateway 관리하니 더 이상 전송 클라이언트가 어떤 서버에서 관리되는지 탐색하지 않아도 된다.&lt;/span&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;&quot;&gt;백엔드는 상태 없이 각 이벤트만 전송하면 되므로 확장에 유리하다.&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;span style=&quot;color: #000000;&quot;&gt;이런 관리의 용이도 좋지만, 단순 비용으로 따져도 전환이 유리할 수 있다.&lt;/span&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;&quot;&gt;API Gateway WebSocket는 프리티어로 달마다 첫 100만 분 연결과 100만 개 메시지는 무료이다.&lt;/span&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;&quot;&gt;만약 100명의 사용자가 24시간 연결되어 1분에 한 번씩 각각 메시지를 받을 때, 월에 1만 원 수준이다.&lt;/span&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;&quot;&gt;오히려 전통적 방식의 서버 운영을 위한 유지비가 더 비쌀 수 있다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Lambda의 역할&lt;/b&gt;&lt;/span&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;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Lambda는&amp;nbsp;&lt;/span&gt;Api gateway와 알림 서버 사이에서 세션 정보 관리의 역할을 한다.&lt;/span&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;&quot;&gt;인증된 사용자가 소켓 연결에 성공하면, 람다는 사용자 정보와 클라이언트 정보를 레디스에 저장한다.&lt;/span&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;&quot;&gt;알림 서버로부터 사용자 정보와 메시지를 수신하면, 람다는 레디스에서 클라이언트 정보를 읽어 게이트웨이로 전달한다.&lt;/span&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;&quot;&gt;반대로 게이트웨이로부터 연결 종료 이벤트를 수신하면 레디스에서 해당 매핑 정보를 제거한다.&lt;/span&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;&quot;&gt;이때 유저 한 명이 여러 클라이언트로 접속하는 경우를 고려하여 클라이언트들을 Set 으로 담게 된다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;연결 유지와 핑퐁&lt;/b&gt;&lt;/span&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;&quot;&gt;Api gatewa의 기본 설정으로, 10분간 동작이 없는 연결은 해제된다.&lt;/span&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;&quot;&gt;클라이언트 단에서는 IDLE로 인식되지 않기 위해, 주기적으로 더미 메시지를 전달하여 연결 해제를 방지한다.&lt;/span&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;&quot;&gt;람다는 이 요청에 대답하고, 클라이언트는 반대로 서버의 응답으로부터 연결이 활성화되었는지 다시 체크할 수 있게 된다.&lt;/span&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;&quot;&gt;이런 더미 패킷과 응답을 'Ping - Pong' 이라고 한다.&lt;/span&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;&quot;&gt;람다는 클라이언트가 { &quot;action&quot;: &quot;ping&quot; } 메시지를 보내면, { &quot;action&quot;: &quot;pong&quot; }으로 응답함을 정의해야 한다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;세션 정보 유지와 TTL&lt;/b&gt;&lt;/span&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;&quot;&gt;게이트웨이 기본 설정으로 'Maximum Connection Duration'으로 최대 연결 시간을 지정할 수 있다.&lt;/span&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;&quot;&gt;기본 값은 2시간인데, 앞선 Ping-Pong 에도 최대 연결 유지 시간은 2시간으로 한정된다.&lt;/span&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;&quot;&gt;따라서 레디스에 { 유저 : 연결 정보 } 를 저장할 때는 그 TTL을 2시간보다 조금 더 큰 값으로 한다.&lt;/span&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;&quot;&gt;시간이 짧으면 연결 정보가 누락되어 메시지가 유실될 것이고,&lt;/span&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;&quot;&gt;너무 크면 이미 끊긴 연결 정보를 불필요하게 유지하는 꼴이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2348&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ba5kHx/dJMcaiCMppN/Yn8oMO7MLkEHbvHJ0aeez0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ba5kHx/dJMcaiCMppN/Yn8oMO7MLkEHbvHJ0aeez0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ba5kHx/dJMcaiCMppN/Yn8oMO7MLkEHbvHJ0aeez0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fba5kHx%2FdJMcaiCMppN%2FYn8oMO7MLkEHbvHJ0aeez0%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;664&quot; height=&quot;240&quot; data-origin-width=&quot;2348&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;팀 상황에 맞는 최적화 고민&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;연결 미리 확인&lt;/b&gt;&lt;br /&gt;이런 기본적인 구조 외&amp;nbsp;팀의 특이 사항이 있다면, 너무 많은 기기 이벤트가 들어오고 있다는 것이다.&lt;/span&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;&quot;&gt;실제 사용과 다른 기기의 자체 상태 이벤트로, 람다로 전달되는 이벤트 수에 비해 실제 표시되는 수는 매우 적다.&lt;/span&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;람다로 전달되는 이벤트 중 5%도 소켓 전달에 사용되지 않고, 나머지는 버려지는 비효율이 발생한다.&lt;/span&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;&quot;&gt;이런 비효율을 피하기 위해 알림 서버에서 연결 여부를 미리 체크한다.&lt;/span&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;&quot;&gt;연결 정보가 있는 경우에만 람다를 호출하는 것으로, 불필요한 레디스 부하를 크게 줄일 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2264&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dX50F4/dJMcaa5QnPR/LtyPxSQRUlke2YhZZK33LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dX50F4/dJMcaa5QnPR/LtyPxSQRUlke2YhZZK33LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dX50F4/dJMcaa5QnPR/LtyPxSQRUlke2YhZZK33LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdX50F4%2FdJMcaa5QnPR%2FLtyPxSQRUlke2YhZZK33LK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;213&quot; data-origin-width=&quot;2264&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Lambda의&amp;nbsp;Redis&amp;nbsp;커넥션&amp;nbsp;관리&lt;/b&gt;&lt;br /&gt;&lt;/span&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;&quot;&gt;람다가 호출 시마다 Redis에 새로 연결되면, Redis 서버의 커넥션 부하로 이어질 수 있다.&lt;/span&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;&quot;&gt;람다 실행 컨텍스트에 Redis 클라이언트 객체를 초기화한다.&lt;/span&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;&quot;&gt;이렇게 되면 &lt;span style=&quot;text-align: start;&quot;&gt;람다의 &lt;/span&gt;Warm-start로 미리 &lt;span style=&quot;text-align: start;&quot;&gt;생성된 커넥션을 재사용하게 된다.&lt;/span&gt;&lt;/span&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;&quot;&gt;불필요한 TCP 핸드셰이크를 막아 레디스 부하와 람다 실행 시간을 모두 줄일 수 있었다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Lambda Authorizer&lt;/b&gt;&lt;/span&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;&quot;&gt;API Gateway에서 사용자 인증 없이 소켓 연결을 수립하면, 다른 사람의 알람을 가로챌 수 있는 큰 문제가 있다.&lt;/span&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;&quot;&gt;처음엔 이런 사용자 인증을 게이트웨이 이후에 처리하려고 했다.&lt;/span&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;&quot;&gt;게이트웨이의 연결 이벤트에서 JWT 토큰을 확인, 이를 검증하여 연결 정보를 레디스에 저장할지 여부를 결정하는 것이다.&lt;/span&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;&quot;&gt;이렇게 하면 알림 서버의 이벤트 전달을 막을 수 있지만, 이미 처리된 연결은 무의미하게 살아있게 된다.&lt;/span&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;&quot;&gt;Api gateway의 Authorizer 람다 기능은 연결 요청 시 연결보다 먼저 인증 로직을 수행한다.&lt;/span&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;&quot;&gt;검증에 실패한 연결을 미리 거부할 수 있게 되어 불필요한 연결 지속 낭비를 피할 수 있다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;성능 테스트&lt;/b&gt;&lt;/span&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;&quot;&gt;부하 테스트 도구 K6를 사용하여 10,000명 동시 접속 유지 테스트를 진행했다.&lt;/span&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;&quot;&gt;10,000개 연결 시도가 모두 성공했으며, 의도대로 최대 동시 접속자 수 10,000명을 달성하였다.&lt;br /&gt;&lt;/span&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;&quot;&gt;소켓 연결 수립에 걸린 시간은 평균 120ms, 상위 95%도 약 200ms 내에 처리되는 것을 확인하였다.&lt;br /&gt;&lt;/span&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;&quot;&gt;연결 유지 시간은 스크립트에 설정한 2분 30초 동안 안정적으로 유지되었다.&lt;/span&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;&quot;&gt;현재 운영보다 충분히 많은 사용자에서, 빠른 연결 수립 시간과 누락 없는 연결 유지를 테스트할 수 있었다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zbn64/dJMcadIdr2G/Bdz31DSM2kuXNKUNA5dna1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zbn64/dJMcadIdr2G/Bdz31DSM2kuXNKUNA5dna1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zbn64/dJMcadIdr2G/Bdz31DSM2kuXNKUNA5dna1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZbn64%2FdJMcadIdr2G%2FBdz31DSM2kuXNKUNA5dna1%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;745&quot; height=&quot;375&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;모니터링 지표&lt;/b&gt;&lt;/span&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;&quot;&gt;Api gateway에서 기본적으로 제공하는 CloudWatch 대시보드도 사용하기 좋았다.&lt;/span&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;&quot;&gt;아래는 위 테스트가 진행되었을 시간대의 메시지 수를 표시했다.&lt;/span&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;&quot;&gt;그 밖에도 메시지 수, 연결 수, 람다 처리 시간, 클라이언트 단, 람다 단, 게이트웨이 단에서의 에러가 기록된다.&amp;nbsp;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lBVsq/dJMcagSwOOc/tE0NNKUPI7N6oAhChWnwF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lBVsq/dJMcagSwOOc/tE0NNKUPI7N6oAhChWnwF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lBVsq/dJMcagSwOOc/tE0NNKUPI7N6oAhChWnwF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlBVsq%2FdJMcagSwOOc%2FtE0NNKUPI7N6oAhChWnwF0%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;814&quot; height=&quot;250&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&quot;&gt;기존 전통적 방식의 웹 소켓 서버 운영을 위해 만들었던 매트릭 대시보드이다.&lt;/span&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;&quot;&gt;Redis 단에선 메시지 발행 수, 발행 시간 지연을, 서버 단에선 커넥션 유지 개수&lt;span style=&quot;text-align: start;&quot;&gt;, 이벤트 전달 소요 시간과 성공률을&lt;/span&gt;&amp;nbsp;모니터링했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;연결 수가 급격히 늘진 않은지, 지연이 있진 않은지 확인이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;꼼꼼한 모니터링과 안전한 관리를 위한 고민과 노력이 필요했지만 완벽한 유실 없는 서비스는 불가능했고.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Api Gateway 로 구조를 변경하고는 높은 안정성에 관리 부담이 크게 줄어 도입 두 달이 지난 지금, 유실 문제는 1 건도 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHp2wk/dJMcaaLxOYs/2C30reJyvVA8RXdwDkOmGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHp2wk/dJMcaaLxOYs/2C30reJyvVA8RXdwDkOmGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHp2wk/dJMcaaLxOYs/2C30reJyvVA8RXdwDkOmGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHp2wk%2FdJMcaaLxOYs%2F2C30reJyvVA8RXdwDkOmGk%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;810&quot; height=&quot;375&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>KimJinHwan/Project</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1160</guid>
      <comments>https://ecsimsw.tistory.com/entry/api-gw-ws#entry1160comment</comments>
      <pubDate>Sun, 21 Dec 2025 20:52:01 +0900</pubDate>
    </item>
    <item>
      <title>WebRTC 시그널링 서버 개발 : P2P 통신 원리와 데이터 중개</title>
      <link>https://ecsimsw.tistory.com/entry/WebRTC-ST</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;재밌는 일이 들어왔다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; 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;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;회사에 DevOps가 따로 없다 보니 클라우드를 직접 만지고 비용을 관리한다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;특히나 우리 회사는 홈 카메라도 판매하기에 스트리밍을 위한 데이터 통신비가 얼마나 큰지 안다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;WebRTC를 위한 모든 데이터가 서버를 거친다면, 그&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;속도도 문제지만 &lt;/span&gt;통신 비용도 클 것이다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;그래서 노드 간 직접 연결이 중요하다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 여기가 재밌었다. 이 글 자체도 사실 이걸 얘기하고 싶었다.&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포되지 않은 로컬 네트워크의 기기끼리 어떻게 서로를 찾아 직접 통신하는지 궁금했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;NAT 동작 과정&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;P2P 통신의 원리를 이해하기 위해선, NAT의 동작 과정을 이해해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;NAT 내부에서 외부에 요청을 보내면, NAT는 연결 정보를 기억했다가 외부의 응답을 다시 내부 기기로 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내부 앱에서 외부 서버 (142.250.197.206:443)으로 요청을 했다고 하자.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;NAT는 내부 연결 정보가 겹치지 않도록 임의의 출발지 포트 (58829)를 만들어 외부 서버에 요청한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;외부에서 해당 포트에 응답이 오면 요청 정보 테이블을 확인하여, 내부 앱의 IP와 포트를 확인하고 이를 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;요청 정보에 기록된 포트는 허락하고, 기록되지 않은 포트는 거부한다.&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;요청 정보를 기억하고 요청 통로의 응답은 허용하는 것이 포인트다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1702&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cowEoY/dJMcaiBJnGV/Mm8RwaCZekReVMatDQzEMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cowEoY/dJMcaiBJnGV/Mm8RwaCZekReVMatDQzEMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cowEoY/dJMcaiBJnGV/Mm8RwaCZekReVMatDQzEMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcowEoY%2FdJMcaiBJnGV%2FMm8RwaCZekReVMatDQzEMk%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;753&quot; height=&quot;106&quot; data-origin-width=&quot;1702&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;NAT 홀 펀칭&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그럼 요청한 경로로 반대쪽에서도 데이터를 전송하면 어떨까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;NAT는 이를 응답으로 착각하고, 매핑 테이블에 따라 내부 앱에 전달할 것이다.&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;NAT가 요청 to 응답에 같은 경로를 사용함을 이용해서 데이터를 전달하는 방법을 홀 펀칭이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;카메라의 실시간 영상을 모바일 앱으로 전달하는 시나리오로 정리해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서로 다른 로컬 네트워크에 모바일 앱과 카메라가 각자의 외부 IP와 빈 포트를 알고 있다는 전제이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764502341736&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 모바일 앱이 카메라 기기의 IP:PORT로 UDP 빈 패킷을 전달한다.
2. 카메라 기기는 해당 경로로 스트리밍을 위한 데이터 프레임을 전달한다.
3. NAT는 2번의 패킷을 1번에 대한 응답으로 생각해 이를 모바일 앱에 전달할 것이다.
4. 1 ~ 3을 계속 반복하면서 모바일 앱은 전달 받은 데이터 프레임을 화면에 끊임없이 출력한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;NAT 출발 경로를 확인하는 방법&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 홀 펀칭이 가능하려면, 요청 출발 정보를&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;상대방에게&amp;nbsp;알려줘야 할 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;NAT에서 나가는 포트 정보는 NAT가 결정하기 때문에, 내부 앱에서는 알 방법이 없다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;내 요청이 NAT로부터 어떻게 전달되는지 알려주는 외부 서버가 STUN 서버이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;아래는 구글의 Stun 서버를 사용했을 때 예시이다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;공유기의 외부 IP와 함께 NAT가 사용한 포트를 확인할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이 정보를 상대방에 넘겨 데이터 통로로 사용할 수 있도록 하는 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764445186678&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$turnutils_stunclient stun.l.google.com

0: : IPv4. UDP reflexive addr: 49.x.x.38:40573
0: : IPv4. UDP reflexive addr: 49.x.x.38:40573&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;P2P가 불가능한 환경에서의 스트리밍&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;홀 펀칭을 모든 NAT에서 할 수 있는 것은 아니다.&lt;/span&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;&quot;&gt;요청마다 혹은 목적지마다 사용 포트가 바뀌는 NAT이거나, 보안이 엄격한 네트워크 환경에선 불가능하다.&lt;/span&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;&quot;&gt;Turn 서버는 이런 직접 연결이 불가능한 상황에서 사용하는 중개 서버이다.&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;span style=&quot;color: #000000;&quot;&gt;목적지마다 사용 포트가 바뀌는 &lt;span style=&quot;text-align: start;&quot;&gt;Symmetric&amp;nbsp;NAT가 대표적이다.&lt;/span&gt;&lt;/span&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;Stun 서버에 요청한 포트와 P2P 상대방에게 사용되는 포트가 달라지니 홀 펀칭이 불가능해진다.&lt;/span&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;이때 Turn 서버를 사용하면 고정된 목적지를 갖기에 NAT 가 매핑 통로를 만들 수 있다.&lt;/span&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;송신 측은 데이터를 Turn 서버에 전달하고, 수신 측은 Turn 서버의 세션 통로를 열어두고 데이터를 수신하는 것이다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2646&quot; data-origin-height=&quot;1054&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/du85IK/dJMcachgnwG/W1WcKIprLm1zKY9Zh6K4Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/du85IK/dJMcachgnwG/W1WcKIprLm1zKY9Zh6K4Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/du85IK/dJMcachgnwG/W1WcKIprLm1zKY9Zh6K4Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdu85IK%2FdJMcachgnwG%2FW1WcKIprLm1zKY9Zh6K4Wk%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;662&quot; height=&quot;264&quot; data-origin-width=&quot;2646&quot; data-origin-height=&quot;1054&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;한쪽만 홀 펀칭이 가능한 경우&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래는 개발을 마치고 테스트 중에 발견한 특이 케이스 로그이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Relay는 홀 펀칭이 불가능해서 중개 서버를 사용, Srflx는 홀 펀칭이 가능한 환경이라는 의미이다.&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;(추가로 Host 타입은 NAT와 외부 네트워크 없이, 내부 IP만으로도 P2P 통신이 가능한 상황을 의미한다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 경우 한쪽은 P2P, 다른 쪽은 Turn 서버를 사용하는 것이 아니라, 결국 둘 다 Turn 서버를 사용하게 된다.&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;즉 홀 펀칭이 가능한 쪽도 결국 Turn 서버로 데이터를 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;A의 최적의 후보지가 Relay, B의 최적의 후보지가 Srflx라고 하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;B가 A로 데이터를 전달하기 위해선 Turn 서버로 전달할 수밖에 없다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;A가 B로 데이터를 전달할 땐, B의 최적 후보대로 외부 IP로 던지려 할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;근데 A의 최적의 후보지가 Relay이라는 말은 A는 NAT 홀 펀칭이 안 된다는 말이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 B의 Srflx 주소로 데이터를 전달해도 B에 도달하지 못하고, 결국 Turn 서버를 사용하게 되는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764494683518&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[09:24:22 PM] ================================================================
[09:24:22 PM] CONNECTION ESTABLISHED VIA: Local: relay &amp;lt;-&amp;gt; Remote: srflx
[09:24:22 PM] - Local: 3.***.139.79:59716 (udp)
[09:24:22 PM] - Remote: 211.***.187.123:45930 (udp)
[09:24:22 PM] ================================================================&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;b&gt;시그널링 서버의 역할&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실 이런 Stun, Turn 서버만 있다고 WebRTC 기반 통신이 가능한 것은 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Stun, Turn 정보, &lt;/span&gt;어떤 코덱이나 암호화를 사용할지, 어떤 네트워크 경로가 가장 효율적인지 등 노드 간 대화가 필요하다.&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;WebRTC 연결 수립을 위해 사용자 단과 카메라 기기는 다음과 같은 대화 흐름의 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1764514837899&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. [FE -&amp;gt; CAM] SDP Offer : 코덱, 미디어 정보, 암호화 방식 등의 스트리밍 정보, 네트워크 경로 후보군 전달
2. [CAM -&amp;gt; FE] SDP Answer : OFFER의 요구사항에 처리 가능한 기기 환경을 응답하고, 미디어 스트림 준비
3. [FE -&amp;gt; CAM] ICE Candidate : FE에서 사용할 수 있는 네트워크 경로(ICE) 후보 전달
4. [CAM -&amp;gt; FE] ICE Candidate : 기기에서 사용할 수 있는 네트워크 경로 (ICE) 후보 전달&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;&quot;&gt;이때 3,4번의 통신 경로 후보군은 한 번씩 전달하고 끝나는 것이 아니라, 후보군마다 여러 번 전송한다.&lt;/span&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;&quot;&gt;또 통신이 끊기거나 네트워크 환경이 변경되었을 때도 동적으로 전달하여 최적의 경로를 찾아가야 한다.&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;span style=&quot;color: #000000; text-align: start;&quot;&gt;이때 사용자 단과 기기가 직접 sdp 값, Ice Candidate를 주고받지 않는다.&lt;/span&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;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;보안을 위해&amp;nbsp;&lt;/span&gt;Mqtt 브로커 접근은 외부 노출 없이, 중앙 서버만 처리하는 꼴이 안전하다.&lt;/span&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;&quot;&gt;시그널링 서버는 사용 단과 기기 사이에서, 사용자 세션, MQTT 송수신을 처리한다.&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;시그널링 서버와 사용자 간 직접 통신&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시그널링 서버에서 사용자 단으로 직접 데이터를 전달되어야 하는 경우, 보통 웹 소켓이나 SSE가 사용된다.&lt;/span&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;&quot;&gt;폴링은 실시간성이 떨어지고, 요청-응답이 많아져 서버 부하로 이어지기 좋다. &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;span style=&quot;color: #000000;&quot;&gt;SSE 서버의 연결을 지속해야 하는지는 판단이 필요하다.&lt;/span&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;&quot;&gt;연결을 지속하는 경우, 네트워크나 WebRTC 커넥션에 이상이 생겼을 때 빠르게 ICE를 주고받기 유리하다.&lt;/span&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;&quot;&gt;연결을 지속하지 않는 경우, 관리 포인트가 크게 준다는 장점이 있다.&lt;/span&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;&quot;&gt;서비스가 네트워크 이상에 얼마나 빠르게 대응해야 하고, SSE 세션 지속 관리 포인트를 어떻게 가져갈지에 대한 선택이 필요하다.&lt;/span&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;&quot;&gt;SSE 연결을 지속하는 것이 좋아 보이지만, 상황에 따라 WebRTC 커넥션을 새로 맺어 버리는 것이 더 깔끔할 수도 있겠다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BGoi7/dJMcacBB54x/ZDAMHuBtlcWBTf7ukvzM81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BGoi7/dJMcacBB54x/ZDAMHuBtlcWBTf7ukvzM81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BGoi7/dJMcacBB54x/ZDAMHuBtlcWBTf7ukvzM81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBGoi7%2FdJMcacBB54x%2FZDAMHuBtlcWBTf7ukvzM81%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;699&quot; height=&quot;271&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;시그널링 서버에서 플랫폼 측으로 Callback&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또는 사용자와의 통신 중간에 플랫폼 측 서버가 놓일 때도 있다.&lt;/span&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;&quot;&gt;이 경우에선 앱과 플랫폼 측 서버가 통신하고, 시그널링 서버는 플랫폼 측에 SDP Answer와 ICE 후보지들을 전달한다.&lt;/span&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;&quot;&gt;플랫폼 서버 측에서 요청을 전달할 때 후에 응답을 수신할 callback url과 auth key를 함께 전달한다.&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;span style=&quot;color: #000000;&quot;&gt;플랫폼 측의 요청을 수신한 WAS와 Mqtt Subscribe로 기기 메시지를 수신한 WAS가 다를 수 있다.&lt;/span&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;&quot;&gt;이 경우를 대비하여 공유 저장소를 사용하여 기기 메시지를 수신했을 때 Callback 정보를 확인하는 꼴도 가능하다.&lt;/span&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;&quot;&gt;TTL 시간이 명확하고, 장기간 보존이 불필요하기에 공유 저장소로 레디스를 사용하기 좋다.&lt;/span&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;&quot;&gt;또는 아예 요청을 수신하고 N초 동안 기기 이벤트를 대기 후, 요청의 응답으로 Callback 하는 꼴도 가능하다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/momgN/dJMcagD1rYd/c9ZjfPKyS5UzxzGfshoCP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/momgN/dJMcagD1rYd/c9ZjfPKyS5UzxzGfshoCP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/momgN/dJMcagD1rYd/c9ZjfPKyS5UzxzGfshoCP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmomgN%2FdJMcagD1rYd%2Fc9ZjfPKyS5UzxzGfshoCP0%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;828&quot; height=&quot;358&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;622&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;로그 확인&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래는 구현한 시그널링 서버가 P2P 연결을 맺기까지의 로그이다.&lt;/span&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;&quot;&gt;노란색은 FE로부터 WebSocket으로 전달받은 값, 빨간색은 기기로부터 Mqtt로 전달받은 값을 의미한다.&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;span style=&quot;color: #000000;&quot;&gt;앞선 WebRTC 수립 과정처럼, SDP Offer, Answer와 Ice Candidate를 양쪽에 전달한다.&lt;/span&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;&quot;&gt;특히 기기의 ICE Candidate로 로컬 네트워크, Stun 서버로 확인한 외부 IP, Relay 서버를 순서대로 전달하는 것이 재밌다.&lt;/span&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;&quot;&gt;그 FE와 기기의 후보군이 오가며, 후보군 중 최적의 경로를 찾게 된다.&amp;nbsp;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;1342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCaVLm/dJMcafLPn8k/hy3o9UoqKeEA4XL8cDYbfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCaVLm/dJMcafLPn8k/hy3o9UoqKeEA4XL8cDYbfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCaVLm/dJMcafLPn8k/hy3o9UoqKeEA4XL8cDYbfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCaVLm%2FdJMcafLPn8k%2Fhy3o9UoqKeEA4XL8cDYbfK%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;2508&quot; height=&quot;1342&quot; data-origin-width=&quot;2508&quot; data-origin-height=&quot;1342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번엔 위 시그널링 서버에서 연결된 프론트엔드 측 로그이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시그널링 서버로부터 Stun, Turn 서버를 전달받고, 마찬가지로 SDP Offer, Answer, ICE Candidate가 오가는 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;WebRTC의 RTCPeerConnection.getStats()를 사용하면, 양 쪽 Peer의 ICE Candidate type을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 우리는 양 쪽 모두 Stun 서버로 확인한 외부 IP로 NAT 홀 펀칭에 성공했고, P2P 연결이 진행되는 것을 이해할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sI3iF/dJMcaiIvMgo/BB0GOIwsl1gwSc93jV6Ufk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sI3iF/dJMcaiIvMgo/BB0GOIwsl1gwSc93jV6Ufk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sI3iF/dJMcaiIvMgo/BB0GOIwsl1gwSc93jV6Ufk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsI3iF%2FdJMcaiIvMgo%2FBB0GOIwsl1gwSc93jV6Ufk%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;2188&quot; height=&quot;1188&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1188&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이슈 : &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 외부 플랫폼에 카메라 기기 WebRTC 적용, 시그널링 서버 개발&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- WebRTC 통신 흐름, 내부망 기기끼리 직접 통신하는 원리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리 :&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- Nat 동작 원리, 홀 펀칭을 이용한 P2P 통신&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- Nat 연결 정보를 알려주는 Stun 서버&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 홀 펀칭이 불가능한 NAT, 네트워크 환경에선 Turn 서버를 사용한 릴레이 통신&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- WebRTC 수립 과정, SDP와 ICE 후보군 전달&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 시그널링 서버의 역할, 사용자 세션, Mqtt 연결 정보 저장과 데이터 송수신 중개&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYn7Uq/dJMcacau1v6/Su4Wxi3pklQuKJ4TGKxtVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYn7Uq/dJMcacau1v6/Su4Wxi3pklQuKJ4TGKxtVk/img.png&quot; data-alt=&quot;WebRTC 동작 과정을 공부하기 위한 간이 테스트 페이지. 옆에는 방향을 주셨던 펌웨어 개발자 형&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYn7Uq/dJMcacau1v6/Su4Wxi3pklQuKJ4TGKxtVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYn7Uq%2FdJMcacau1v6%2FSu4Wxi3pklQuKJ4TGKxtVk%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;2262&quot; height=&quot;1286&quot; data-origin-width=&quot;2262&quot; data-origin-height=&quot;1286&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;WebRTC 동작 과정을 공부하기 위한 간이 테스트 페이지. 옆에는 방향을 주셨던 펌웨어 개발자 형&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>KimJinHwan/Project</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1151</guid>
      <comments>https://ecsimsw.tistory.com/entry/WebRTC-ST#entry1151comment</comments>
      <pubDate>Fri, 28 Nov 2025 22:50:21 +0900</pubDate>
    </item>
    <item>
      <title>자바와 OS : CAS, CountDownLatch의 동작 원리</title>
      <link>https://ecsimsw.tistory.com/entry/cas-latch-kernal-op</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://ecsimsw.tistory.com/entry/mono-delay&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;지난 글&lt;/a&gt;에서 논 블록킹을 구현하는 두 가지 방법을 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Netty는 커널의 I/O 멀티플렉싱 연산을 사용하고, DelayQueue는 커널 단의 스레드 제어 연산을 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에는 CAS와 CountDownLatch가 궁금했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실 CAS와 CountDownLatch도 앞선 두 경우와 비슷할 것 같다는 생각에 파본 것인데, 그렇지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CAS는 전혀 다른 방법으로 동작했고, CountDownLatch는 짬뽕이었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;나는 컴퓨터공학을 전공했다. 부끄럽지만 컴퓨터구조와 운영체제 수업을 가장 재밌게 수강했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞선 멀티 플렉싱이라는 키워드도, 앞으로 얘기할 커널 모드와 시스템 콜도, 다 공부한 개념인데 참 낯설다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;오랜만에 복습할 겸, 운영체제와 자바 개발을 스르륵 녹일 수 있는 글이 되었으면 좋겠다는 생각에 정리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;스레드 상태와 스케줄러&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바에서 Thread.sleep()을 하면 스레드가 일정 시간 동안 멈춘다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 스레드를 반복문처럼 계속 연산시키며 시간을 확인하는 것이 아니라, 커널 단에서 스레드 연산을 멈춘다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스레드를 멈춘다는 얘기는 흐르는 물을 막는 것과는 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그보다는 그 스레드를 CPU 연산에서 일정 시간 동안 제외해 둔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커널은 스레드 정보와 상태를 담은 블록, TCB를 관리하고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스케줄러는 그 블록들을 확인해 스레드 상태를 체크하면서, 연산의 대기열로의 추가, 삭제를 결정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, Thread.sleep은 해당 스레드의 상태를 변경하여, 일정 시간 동안 연산 대기열에서 빠지게 하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;더 자세히는 해당 스레드의 TCB에서 상태를 TIMED_WAITING으로 값을 변경한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;커널 모드, &lt;b&gt;유저 모드&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;운영체제 첫 개념이 아직도 생각난다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;운영체제가 하는 주요 업무 네 가지, 파일 관리, 메모리 관리, 프로세스 관리, 입출력 관리.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런 주요 업무들은 일반 애플리케이션 코드에서 수행할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 보안과 시스템 안정성을 위해, 수행할 수 있는 명령어 모드를 구분한 것이 커널 모드와 유저 모드이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커널 모드는 완전한 권한을 얻어 모든 연산이 가능하고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반 애플리케이션은 유저 모드로서, 제한된 명령어만 수행할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JVM은 감히 TCB를 직접 건들 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;시스템 콜&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그럼 어떻게 Thread.sleep은 스레드의 상태를 변경할 수 있을까.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;직접 처리하지 못하고 커널에 부탁한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 요청이 시스템 콜이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;애플리케이션은 시스템 콜로 간접적으로 커널이 관리하는 자원을 다룬다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시스템 콜을 하면, CPU는 권한이 바뀌어 커널 모드로 진입하고, 작업을 마치고 다시 유저 모드로 돌아온다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 모드를 나타내는 레지스터의 비트를 변경하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;운영체제는 그렇게 수행되는 연산의 권한 검증하거나 리소스를 격리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대표적으로 MMU는 프로세스별로 사용할 수 있는 영역을 나누고 관리하는 하드웨어다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커널 모드에 접근해서 메모리를 다루더라도, 프로세스에 할당 밖 영역이나 커널의 메모리 영역의 침범을 MMU가 보호한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;자바 예시&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 슬슬 두 모드로 처리할 수 있는 연산이 헷갈려진다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;익숙한 자바를 기준으로 유저 모드와 커널 모드의 연산을 분리해 본다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반복문, 조건문은 유저 모드이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Thread.sleep(), System.out.println(), File.write()은 커널 모드이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바에서 변수의 값을 바꾸는 것은 커널 모드일까? &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미 할당된 내 메모리를 다루는 것은 유저 모드로 처리 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;new LinkedList()로 객체 메모리를 만드는 것은 커널 모드일까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미 JVM에서 할당된 힙 영역을 단순 사용하는 것은 유저 모드로 가능하다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 힙 영역이 부족해서, 메모리를 추가로 할당받는다면 이는 커널 모드가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CAS 의 동작 원리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하드웨어 명령어와 커널 단의 명령어는 구분해야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JNI, Netive method로 하드웨어 명령어를 사용한다고 모두 커널 모드는 아니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;돌고 돌아, CompareAndSet은 단순 유저 모드를 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바가 컴파일되는 바이트 코드에는 &quot;조회&quot;와 &quot;값 변경&quot;을 한 번에 할 수 있는 명령어가 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 &quot;조회 후 맞으면 값 변경&quot;은 CPU의 명령어로 처리해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇기에 JNI, Native method로 직접 하드웨어 명령어를 수행하는 것뿐이지,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇게 수행하는 명령어는 커널 모드가 필요 없는 유저 모드의 명령어이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;CountDownLatch 의 동작 원리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;CountDownLatch는 짬뽕이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 동작 원리를 들으면 이제는 감이 올 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. await(), 스레드를 대기 모드로 전환한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. countDown(), 원자적으로 메모리의 값을 1 줄인다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. count가 0이 되면, 대기 중이던 스레드를 깨운다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1번과 3번은 TCB의 스레드 상태를 변경해야 하기에 커널 모드 진입이 필요하다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2번은 CPU(하드웨어) 원자성 명령어를 사용한 것이고, 그 대상이 이미 할당된 메모리이니, 유저 모드로 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이슈 : CAS, CountDownLatch의 동작 원리가 궁금하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OS :&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 스레드 대기를 위해선 스레드 상태 변경이 필요하고, 이는 커널의 역할이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 스케줄러는 TCB의 스레드 상태를 읽고 CPU 연산 대상 여부를 결정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 일반 애플리케이션이 커널의 자원과 명령어를 수행하고자 하면 시스템 콜이 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 운영체제는 커널 모드와 유저 모드를 분리하여, 리소스를 격리하고 안정성을 지킨다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java :&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 파일, 메모리, 네트워크, 입출력과 관련된 연산은 커널 모드가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 대신 할당된 메모리에서 객체를 생성하거나, 값을 변경하는 것은 유저 모드로 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - JNI, Native method를 사용한 하드웨어 명령어 수행과 커널 단의 명령어 수행을 분리해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과 :&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- CAS는 하드웨어의 원자적 명령어 수행이다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- 자바 바이트 코드에는 원자적 연산이 없어, JNI/Native method를 사용해야 하는 것이지만, 커널 모드가 필요하진 않다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- CountDownLatch는 둘 다 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp;- Count를 위한 원자적 값 변경은 사용자 모드, 스레드를 대기하고 다시 활성화시키는 것은 커널 모드의 연산이다.&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;</description>
      <category>Language/Java, Kotlin</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1149</guid>
      <comments>https://ecsimsw.tistory.com/entry/cas-latch-kernal-op#entry1149comment</comments>
      <pubDate>Mon, 17 Nov 2025 01:16:26 +0900</pubDate>
    </item>
    <item>
      <title>이 이벤트는 1분 후에 처리해주세요 : Mono.delay가 시간을 체크하는 방법</title>
      <link>https://ecsimsw.tistory.com/entry/mono-delay</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리 회사는 IoT 기기를 다루고 있다. 보다 더 넓은 서비스를 제공하기 위해, 회사의 제품을 더 큰 국내 IoT 플랫폼에 연동이 가능하게 만드는 게 내 일이다. 외부 플랫폼에서 우리 회사 기기를 제어할 수 있는 Api를 개발하고, 반대로 외부 플랫폼으로 기기 이벤트를 전달해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번에 신규 기기를 연동시키면서 재밌는 요구 사항이 있었다. 특정 이벤트가 들어오면 외부 플랫폼 측으로 A&lt;span style=&quot;text-align: start;&quot;&gt;를&amp;nbsp;&lt;/span&gt;전달하고, 1분 후에 B를 전달해야 했다. 이벤트 파이프라인은 바쁘기에 1분을 블록킹 대기할 수 없다. 옆자리 형이 Mono.delay를 사용한 논 블록킹으로 딜레이 로직을 구현하였고, 이를 리뷰하다가 Mono.delay의 동작 방식을 파보게 되었다. 이를 정리해보려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;I/O Multiplexing을 사용한 논 블록킹&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;대표적인 리액티브 라이브러리로 WebClient, Reactive MongoDB가 생각난다. WebClient는 Netty, Reactive MongoDB는 MongoDB 드라이버 위에서 동작하고, 이들은 커널 단의 I/O 멀티플렉싱 커멘드를 사용하여 논 블록킹을 구현한다. Api 요청, DB 쿼리 등 응답에 대기가 필요한 작업을 전달하고, 처리 완료 이벤트를 미리 만들어둔 이벤트 루프 스레드를 사용하여 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;플랫폼 비종속적인 Java이기에, 코드 단에서는 어떤 멀티플랙싱 커멘드를 사용할 수 있는지 모른다는 점도 재밌다. Netty의 경우 코드 단에서 운영체제 정보와 사용할 수 있는 멀티플렉싱 커멘드를 찾는 코드가 포함되어 있다. 그 분기 로직은 바이트 코드로 컴파일되고, JVM 어셈블러/JIT에 의해 바이너리 코드로 변환될 때까지 결정되지 못한다. 코드가 실행되면서, OS 정보와 사용할 수 있는 멀티플렉싱 커멘드를 확인하고, 이를 포함한 네이티브 함수가 JNI에 의해 수행되며 실제 시스템 콜이 발생하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 사용할 수 있는 최적의 커멘드(ex, Linux의 epoll, Mac의 kqueue)를 찾지 못하는 경우, Fallback으로 Java NIO 라이브러리를 사용한다. NIO는 poll/select처럼 유닉스 계열에선 범용적인, 그렇지만 epoll, kqueue 보다는 성능이 부족한, 멀티플렉싱 커멘드를 사용하는 네이티브 함수를 호출한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;스케줄이 가능한 스레드 풀&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;이들과는 달리,&lt;/span&gt;&amp;nbsp;Mono.delay는 &lt;span style=&quot;text-align: start;&quot;&gt;커널 단의 멀티플렉싱 커멘드를 사용하지 않는다. 그보다 단순히 딜레이 시간을 지정할 수 있는 스레드 풀을 워커 스레드 풀로 사용하는 구조이다. 스레드 풀의 대기열에 작업과 딜레이 시간을 기록하고, 시간이 되면 해당 작업이 수행된다. 앞선 I/O Multiplexing을 활용한 논 블록킹과 동일하게, &lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;워커 &lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;스레드 풀이 고갈되지 않도록 처리 로직을 짧게 해야 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;딜레이 시간을 지정할 수 있는 스레드 풀은 어떻게 만료 시간을 체크하는지, 그 방법이 궁금했다. 작업을 메모리에 올려두고 별도의 스레드 하나가 계속 시간이 다 되었는지를 폴링 하려나? 코드를 파고 시작했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Mono.delay는 스케줄러를 따로 지정하지 않으면, Schedulers.parallel()를 기본으로 사용한다. Schedulers.parallel()의 생성 과정을 쭉 타고 들어가다 보면 결국 아래의 ParallerScheduler 임을 찾을 수 있다. 여기서 ScheduledExecutorService의 구현체로 ScheduledThreadPoolExecutor를 사용한다는 점이 포인트다. 이를 state라는 변수로 여러 개 들고 있다.&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2402&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HFsLx/dJMcabP6xNc/32mpNisvIRqCrPl9oYYVAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HFsLx/dJMcabP6xNc/32mpNisvIRqCrPl9oYYVAk/img.png&quot; data-alt=&quot;필요한 부분만 남겼기에, 실제와 다른 부분도 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HFsLx/dJMcabP6xNc/32mpNisvIRqCrPl9oYYVAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHFsLx%2FdJMcabP6xNc%2F32mpNisvIRqCrPl9oYYVAk%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;2402&quot; height=&quot;922&quot; data-origin-width=&quot;2402&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;필요한 부분만 남겼기에, 실제와 다른 부분도 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ParallerScheduler에 작업과 딜레이 시간을 등록하면, pick 메서드에서는 라운드 로빈으로 ScheduledThreadPoolExecutor 중 하나를 선택한다. Schedulers.directSchedule를 까보면, 선택한 ScheduledThreadPoolExecutor에 작업이 배정되는 것을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;1321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVj4MV/dJMcaf52hmQ/j5x7T9Mm9u545k8XSI4aM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVj4MV/dJMcaf52hmQ/j5x7T9Mm9u545k8XSI4aM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVj4MV/dJMcaf52hmQ/j5x7T9Mm9u545k8XSI4aM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVj4MV%2FdJMcaf52hmQ%2Fj5x7T9Mm9u545k8XSI4aM1%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;1686&quot; height=&quot;1321&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;1321&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ScheduledThreadPoolExecutor의 생성자를 확인하면 대기열로 DelayedWorkQueue를 사용하는 것을 볼 수 있다. DelayedWorkQueue는 아이템을 적재할 때 만료 시간을 함께 등록하고, 아이템을 요청하면&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 대기 후&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;만료 시간이 된 아이템을 반환한다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;즉, ScheduledThreadPoolExecutor는 작업을 배정하면 큐에 딜레이 시간과 작업을 적재하고, 큐에서 딜레이 시간이 만료된 작업이 반환되면, 이를 스레드에서 수행하는 것으로, 딜레이 후 작업 수행이 가능해진다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2143&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXeV7N/dJMcabCziVR/rJJxNybMmLAXNNWfbaTJtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXeV7N/dJMcabCziVR/rJJxNybMmLAXNNWfbaTJtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXeV7N/dJMcabCziVR/rJJxNybMmLAXNNWfbaTJtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXeV7N%2FdJMcabCziVR%2FrJJxNybMmLAXNNWfbaTJtk%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;2143&quot; height=&quot;278&quot; data-origin-width=&quot;2143&quot; data-origin-height=&quot;278&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지금까지의 흐름을 정리하면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. ParallelScheduler는 ScheduledThreadPoolExecutor 배열을 들고 있다. (Size = &lt;span style=&quot;text-align: start;&quot;&gt;Cpu 코어 수&lt;/span&gt;)&lt;/span&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;&quot;&gt;2. 각&amp;nbsp;ScheduledThreadPoolExecutor는 단일 스레드, DelayedWorkQueue를 대기열로 한다.&lt;/span&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;&quot;&gt;3.&amp;nbsp;Mono.delay()를 실행하면, ParallelScheduler에 딜레이 시간과 처리할 작업을 넘긴다.&lt;/span&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;&quot;&gt;4. ParallelScheduler는&amp;nbsp;들고 있는&amp;nbsp;ScheduledThreadPoolExecutor 배열에서 하나를 라운드로빈으로 선택한다.&lt;/span&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;&quot;&gt;5. 선택된 ScheduledThreadPoolExecutor는 대기열에 딜레이 시간과 작업을 등록한다.&lt;/span&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;&quot;&gt;6. 대기열은&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;DelayedWorkQueue으로 하여, 큐에서 아이템을 꺼내는 요청 시 딜레이 시간이 다 된 작업이 반환된다.&lt;/span&gt;&lt;/span&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;7. ScheduledThreadPoolExecutor가 반환된 작업을 스레드에 태우는 것으로, 딜레이 시간을 대기한 작업이 수행되는 것이다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&amp;nbsp;어떻게 딜레이 시간을 계산하는 걸까&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결국 딜레이 시간을 확인하고, 수행해야 하는 작업을 반환하는 것은 DelayedWorkQueue였다. 이제 찾아야 하는 것은 &quot;DelayedWorkQueue는&amp;nbsp;딜레이 시간이 다 끝난 건지 어떻게 계산할까&quot;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java concurrent 라이브러리의 DelayQueue는 아이템과 함께 딜레이 시간을 적재하고, 꺼낼 때는 딜레이 시간이 지난 아이템이 꺼내진다. DelayedWorkQueue도 이와 동작과&amp;nbsp;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;원리가 동일한데, DelayQueue 코드가 더 명확하여, 그 코드로 설명한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2792&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBb9UB/dJMcagRpn1T/r26RHARGfhRMN1JsMal0wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBb9UB/dJMcagRpn1T/r26RHARGfhRMN1JsMal0wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBb9UB/dJMcagRpn1T/r26RHARGfhRMN1JsMal0wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBb9UB%2FdJMcagRpn1T%2Fr26RHARGfhRMN1JsMal0wk%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;2792&quot; height=&quot;442&quot; data-origin-width=&quot;2792&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;큐에서 Take를 수행하면 우선순위 큐에서 아이템을 꺼낸다. 남은 딜레이 시간이 적은 아이템을 높은 우선순위로 하기 때문에, 가장 짧게 남은 아이템이 반환될 것이다. 그 아이템의 딜레이 시간이 지났다면 큐에서 꺼내 반환하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;딜레이 시간이 남아 있는 경우가 중요하다. Condition.awaitNanos()로 스레드를 남은 시간 동안 대기 상태로 한다. 지정한 딜레이 시간이 끝나면 대기 상태에서 벗어나 반복문을 돌 것이고, 다시 우선 순위 큐에서 아이템을 꺼내게 되면, 이번에는 딜레이 시간을 지난 경우가 되어 해당 아이템을 반환하게 되는 것이다. 이때 leader 변수는 현재 대기 상태 중인 스레드를 표시한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1769&quot; data-origin-height=&quot;1596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOArlh/dJMcafSvd6D/g40QFikdaixS46eAHcjN6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOArlh/dJMcafSvd6D/g40QFikdaixS46eAHcjN6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOArlh/dJMcafSvd6D/g40QFikdaixS46eAHcjN6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOArlh%2FdJMcafSvd6D%2Fg40QFikdaixS46eAHcjN6k%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;1769&quot; height=&quot;1596&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1769&quot; data-origin-height=&quot;1596&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 스레드가 대기 상태가 되는 동안, 다른 아이템이 들어오면 어떨까. 새로 추가된 아이템의 딜레이 시간이, 현재 스레드가 대기 상태로 기다리는 시간보다 더 짧다면 큰일이다. 이를 피하기 위해 추가된 아이템이 현재 큐에서 가장 적은 딜레이 시간의 아이템이라면, Condition.signal()으로 대기 상태였던 스레드를 다시 깨운다. 위 Take()에서 대기 상태에 빠졌던 스레드가 다시 깨어나 반복문을 돌 것이고, 이번엔 방금 추가된 아이템이 가장 짧은 딜레이로 확인되어, 스레드는 그 시간만큼 대기 상태가 될 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ8rmZ/dJMcaacAt0l/LdlmrtmdroqqR21oLuBBfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ8rmZ/dJMcaacAt0l/LdlmrtmdroqqR21oLuBBfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ8rmZ/dJMcaacAt0l/LdlmrtmdroqqR21oLuBBfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ8rmZ%2FdJMcaacAt0l%2FLdlmrtmdroqqR21oLuBBfk%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;1760&quot; height=&quot;750&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Condition.awaitNanos()를 까보면, 결국 스레드를 대기시키는 일은 LockSupport.parkNanos 메서드가 하는데, 그 안에서도 U.park는 네이티브로 구현되어, 스레드를 지정 시간 동안 대기하는 커널 단의 커멘드를 수행하게 된다. 운영체제에 따라 다르지만, Unix의 futex 나 pthread_cond_timedwait 가 대표적이다. 결국 커널 단의 스레드 블록을 사용하는 것으로 Busy wait를 피할 수 있게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dB7Dbk/dJMcaajmckl/hFSgVYHXgjLIAjr3HMbuzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dB7Dbk/dJMcaajmckl/hFSgVYHXgjLIAjr3HMbuzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dB7Dbk/dJMcaajmckl/hFSgVYHXgjLIAjr3HMbuzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdB7Dbk%2FdJMcaajmckl%2FhFSgVYHXgjLIAjr3HMbuzk%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;1756&quot; height=&quot;806&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;806&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이슈 : 'N초 딜레이 후 A 실행'에 Mono.delay 사용. 그 동작 원리 파보기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원리 1, &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;ScheduledThreadPoolExecutor &lt;/span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - N초 후에 워커 스레드가 처리할 작업을 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;ScheduledThreadPoolExecutor에 등록한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; - &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;ScheduledThreadPoolExecutor는 작업 대기열로 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;DelayedWorkQueue를 사용한다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; - 즉 N초 후에 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;DelayedWorkQueue에서 작업을 뱉는 것을 수행시켜 딜레이 후 작업 처리를 구현한다.&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;원리 2 - &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;DelayedWorkQueue :&lt;/span&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; - &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;DelayedWorkQueue 는 지정한 N초 후에 아이템을 반환하는 큐이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; - JVM의 네이티브 메서드 (&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;LockSupport.parkNanos)를 사용하여 스레드를 대기시키는 작업을 수행한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; - 해당 메서드는 시스템 콜을 호출하여 운영체제가 해당 스레드를 N초 후에 스케줄링되도록 한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp; - 중간에 다른 아이템이 들어오면 시간을 비교하여 스케줄링 대기 시간을 조정한다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&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;&lt;span style=&quot;color: #000000;&quot;&gt;결과 :&amp;nbsp;&lt;/span&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;&quot;&gt;&amp;nbsp; &amp;nbsp;- 스레드 자체를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;딜레이 시간 동안&lt;/span&gt;대기 처리하는 것으로 Busy wait / Spin lock을 피함&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;</description>
      <category>Language/Java, Kotlin</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1147</guid>
      <comments>https://ecsimsw.tistory.com/entry/mono-delay#entry1147comment</comments>
      <pubDate>Fri, 14 Nov 2025 01:02:20 +0900</pubDate>
    </item>
    <item>
      <title>데이터 적재 처리량 개선 : 단건 처리에서 배치 처리로</title>
      <link>https://ecsimsw.tistory.com/entry/device-history-batch</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;요구&amp;nbsp;사항&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;우리 팀은 IoT 기기를 다룬다. &lt;/span&gt;도어락의 문 열림 기록이나, 온습도계의 일간, 월간 온습도 변화 기록 조회 등, 기기의 상태 기록을 조회할 수 있는 기능을 제공한다. 이런 기능을 위해 &amp;lsquo;히스토리&amp;rsquo; 서비스는 기기의 상태 이벤트를 수신하고, 기록해야 할 테이터를 필터링하여 DB에 적재하는 역할을 수행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 히스토리 서비스에서 저장해야 하는 이벤트 양은 초당 초당 2600 ~ 2800건이다. 그리고 아래는 현재 히스토리 서비스 초당 처리량이다. 이벤트 유입량과 처리량이 크게 차이가 나지 않음을 알 수 있다. 지금까진 가까스로 처리되었지만, 지금보다 유입량이 조금만 더 많아지면, 유입량이 처리량보다 많아져 이벤트 유실이나 OOM이 발생하기 좋다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2344&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t4xHo/dJMcaaQ6YoJ/4Rv54gVkhw9CkKnq77sErK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t4xHo/dJMcaaQ6YoJ/4Rv54gVkhw9CkKnq77sErK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t4xHo/dJMcaaQ6YoJ/4Rv54gVkhw9CkKnq77sErK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft4xHo%2FdJMcaaQ6YoJ%2F4Rv54gVkhw9CkKnq77sErK%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;2344&quot; height=&quot;590&quot; data-origin-width=&quot;2344&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우리 팀의 목표 요구치는 초당 7천 건의 저장이었다. 처리량이 그 정도는 벌어져야, 안정적으로 서비스 운영이 가능하다고 판단했다. 참고로 지금의 초당 3천 건의 처리량은, 이벤트 유입량이 초당 1700건 정도 발생했던 때에 만들어진 로직과 설정 값이다. 이런 배경에 히스토리 서비스의 처리량 개선을 시도했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;가장 빠른 방법&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문제를 풀기 위한 가장 빠른 방법은 단순히 저장을 위한 스레드 수를 늘리는 것이었다. 이벤트 브로커의 파티션 수를 늘려 여러 스레드(커넥션)에서 동시에 적재하는 것이다. 테스트해 보니 파티션 개수를 2.5배 늘려야 요구 처리량인 초당 7천 건의 처리량을 만들 수 있었다. 다만 이 서비스 하나만의 개선을 위해, 카프카 파티션 수 자체를 늘리는 것이 옳은 방법이라고 생각하지 않았다. 리소스 증설에만 기댄 문제 처리는 한계가 분명하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE2Xvc/dJMcabbpKmW/TIyYKxFJ5rTxi8BCA1dciK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE2Xvc/dJMcabbpKmW/TIyYKxFJ5rTxi8BCA1dciK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE2Xvc/dJMcabbpKmW/TIyYKxFJ5rTxi8BCA1dciK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE2Xvc%2FdJMcabbpKmW%2FTIyYKxFJ5rTxi8BCA1dciK%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;2188&quot; height=&quot;567&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;567&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;파티션 개수는 그대로 두고 DB 저장 로직 자체만 멀티 스레드 혹은 리액티브로 처리할 수 있을 것 같다. 이런 방법도 마찬가지로 확장할 수 있는 리소스 자원에는 한계가 있으며, 스레드 풀 관리를 잘못하는 경우 처리량이 밀리거나 OOM으로 이어질 수 있다. 만일 WAS단의 리소스가 무한하다고 가정하더라도, DB 단의 연산이 늘어나 CPU를 과도하게 사용하게 될 것이다. 결국 WAS의 리소스 문제를 미뤄, 데이터베이스의 리소스 문제로 이어진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실 무엇보다 매번 이벤트가 수신될 때마다 DB를 접근하는 게 비효율적이라고 판단했다. 이벤트를 모아 Bulk insert를 사용하면 네트워크 통신 비용, 실제 클라우드 네트워크 트래픽 비용, DB의 연산량, 커넥션 점유 경합에 개선이 있을 것이라는 생각이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배치 처리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이벤트 수신 시 저장할 큐, 배치 처리를 위한 스케줄러를 구현하였다. 카프카에서 이벤트를 수신하여, Thread-safe 한 큐에 저장한다. 일정한 시간 간격으로 큐를 비우고, 배치 사이즈만큼씩 bulk insert를 처리한다. 큐에 이벤트를 쌓는 스레드와 큐에서 이벤트를 처리하는 스레드를 분리하도록 설계했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배치 스케줄링 주기가 데이터 삽입 시간에 영향받지 않도록&lt;/b&gt;&lt;/span&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;&quot;&gt;스케줄러는 주기적으로 수행하되, DB 쿼리 자체는 비동기로 처리한다. 동기로 처리하는 경우, 한번 쿼리가 느려지면 다음 스케줄링이 늦어지고, 큐에 또 많은 데이터가 쌓이게 되어 악순환이 반복된다. 비동기로 처리하여, DB 수행 시간에 배치 스케줄링 주기가 영향받지 않도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;큐 사이즈 지정, DLQ&lt;/b&gt;&lt;/span&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;&quot;&gt;DB에 저장하는 처리량보다 유입량이 많아 이벤트가 큐에 계속 쌓이고 늘어나면 OOM으로 이어질 수 있다. 큐가 저장할 수 있는 사이즈를 명확히 하여, 큐의 메모리가 OOM까지 늘어나는 상황을 피했다. 대신 큐 사이즈를 넘어선 이벤트에 대한 대비가 필요하다. 큐 저장에 실패한 이벤트를 DLQ에 전달하고 재시도 / 알람 / 로깅을 처리한다. 이벤트 수신 스레드와 Dead letter 처리 스레드를 분리하여, 이벤트 수신 스레드에서 유실 대비를 위해 블록킹 되어 수신이 느려지는 상황을 피한다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;1368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfcXnW/dJMcafrlGWK/qLNfWEfA6IwhBiDBw7uJ4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfcXnW/dJMcafrlGWK/qLNfWEfA6IwhBiDBw7uJ4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfcXnW/dJMcafrlGWK/qLNfWEfA6IwhBiDBw7uJ4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfcXnW%2FdJMcafrlGWK%2FqLNfWEfA6IwhBiDBw7uJ4K%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;2112&quot; height=&quot;1368&quot; data-origin-width=&quot;2112&quot; data-origin-height=&quot;1368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;안전하게 종료하기 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버가 다운될 때, 현재 큐에 적재되어 있는 이벤트와 작업 중인 스레드를 안전하게 정리하기 위한 고민이 필요하다.&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;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;새로운 자원 유입 막기&lt;/b&gt;&lt;/span&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;&quot;&gt;남아있는 것을 정리하기 전에, 우선 새로 인입되는 것을 막아야 한다. 내 상황에선 '1. 큐에 이벤트가 추가로 수신되는 것이 없는지', '2. 새로운 배치 작업이 추가로 수행되는 것은 없는지', 확인이 필요했다. Spring boot의 Graceful shutdown 발생 시, @KafkaListener를 통한 이벤트 수신이 종료되고, @Scheduled로 새로운 작업이 수행되지 않음을 확인할 수 있었다.&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;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;작업 중인 스레드 대기하기&lt;/b&gt;&lt;/span&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;&quot;&gt;새로 추가되는 이벤트, 배치 작업이 없는 것을 확인했으니, 이제 남은 이벤트와 작업을 정리한다. 우선 비동기로 처리되고 있는 작업을 대기한다. 아래 코드에서 'asyncWaitTimeSec' 변수로, 현재 진행 중인 비동기 작업 종료를 대기하는 시간을 지정했다. 이때 이 값은 Spring boot의 Graceful Shutdown 대기 시간보다 작고, DB 쿼리 수행 Timeout 시간보다는 크게 설정해야 할 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;큐에 남아있는 이벤트 처리&lt;/b&gt;&lt;/span&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;&quot;&gt;비동기 작업 스레드에 대한 정리가 끝나면, 현재 큐를 전부 비울 때까지, Bulk insert 반복한다. 이때 큐의 모든 데이터를 한 번에 처리하지 않고, Batch size로 구간을 나눠 작업을 반복한 이유는, 큐에 남은 데이터가 많아 쿼리 자체가 너무 느리거나, DB 커넥션 관리에 방해가 되지 않기 위함이다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;1424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUfWoy/dJMcac9ej9o/DsQaLFMlwddlCbjaBnkGbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUfWoy/dJMcac9ej9o/DsQaLFMlwddlCbjaBnkGbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUfWoy/dJMcac9ej9o/DsQaLFMlwddlCbjaBnkGbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUfWoy%2FdJMcac9ej9o%2FDsQaLFMlwddlCbjaBnkGbk%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;2114&quot; height=&quot;1424&quot; data-origin-width=&quot;2114&quot; data-origin-height=&quot;1424&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;테스트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Stage 환경에서 테스트를 진행했다. 처리량 확인을 위해 실제보다 많은 이벤트를 인위적으로 만들어 유입시켰다. &lt;span style=&quot;text-align: start;&quot;&gt;그 외 카프카 파티션 수나 DB 리소스 등은 기존과 동일하게 구성하였다. &lt;/span&gt;개선한 배치 작업이 안정적으로 처리할 수 있는지 확인한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;배치 사이즈와 주기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 기능 개발과 달리, 이번에는 배치 사이즈나 이벤트 적재 속도, 쿼리 실행 속도, DB 리소스 등 봐야 할 매트릭과 이에 따라 조정해야 하는 값들이 많았다. 특히 배치 주기와 사이즈에 따른 큐의 이벤트 유입 대비 처리 속도가 매우 중요했다. 배치 주기와 사이즈가 적절하지 않으면 DB 쿼리 시간이 길어져 커넥션 점유가 커지거나, 큐의 이벤트 유입 대비 처리가 늦어 이벤트가 처리가 지연되고 큐를 모두 다 차지하는 꼴로 이어질 수 있을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;조정한 값으로 수행한 테스트 결과는 목표했던 초당 처리량 6천 건을 훌쩍 넘은 수치였다. 이벤트 적재 큐 사이즈도 크게 증가하지 않았고, 유입량 대비 안정적인 처리량을 보였다. DB 리소스 사용량도 문제 되지 않음을 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2454&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wpN5X/dJMcahvVT83/Dxbdzmu2QajFKpGLymdByK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wpN5X/dJMcahvVT83/Dxbdzmu2QajFKpGLymdByK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wpN5X/dJMcahvVT83/Dxbdzmu2QajFKpGLymdByK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwpN5X%2FdJMcahvVT83%2FDxbdzmu2QajFKpGLymdByK%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;2454&quot; height=&quot;668&quot; data-origin-width=&quot;2454&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Graceful shutdown으로 종료 시점에서의 큐 비우기도 문제없었다. 아래 로그에서 SIG_TERM이 수신되고, 배치 처리 중이던 비동기 테스크들이 정상 종료되고, 큐에 남아있던 이벤트들이 두 번의 추가 배치 처리 이후에 적절히 제거되었음을 확인할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2526&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tAFZD/dJMcaezc04v/q2YuE7sp1plBJlrRTGxXg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tAFZD/dJMcaezc04v/q2YuE7sp1plBJlrRTGxXg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tAFZD/dJMcaezc04v/q2YuE7sp1plBJlrRTGxXg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtAFZD%2FdJMcaezc04v%2Fq2YuE7sp1plBJlrRTGxXg0%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;2526&quot; height=&quot;148&quot; data-origin-width=&quot;2526&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한 일주일 정도 Stage 환경에서 큐 사이즈와 처리량, Kafka 이벤트 수신 정도에 문제가 없는지, DB 리소스 매트릭과 Slow query 발생은 없었는지 확인하는 시간을 가질 생각이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;TIP 1 :: MongoRepository의 saveAll 동작 방식&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;처음 개발을 마치고 테스트했을 때, 처리량 개선이 크지 않았다. 쿼리 로그를 확인하니, 예상했던 Bulk insert가 아니라 매 엔티티마다 Insert 쿼리가 나가고 있었다. MongoRepository의 saveAll은 저장하려는 모든 엔티티의 id가 없는 상황에서만 bulk insert가 진행되는 것을 공부할 수 있었다. 기존 엔티티를 매핑하던 로직에서 무의미하게 지정했던 id를 제거하는 것으로, 의도대로 bulk insert를 처리하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1075&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbQvzx/dJMcaacvnjJ/sZcYDNJlhPsC9dxsEb91X1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbQvzx/dJMcaacvnjJ/sZcYDNJlhPsC9dxsEb91X1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbQvzx/dJMcaacvnjJ/sZcYDNJlhPsC9dxsEb91X1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbQvzx%2FdJMcaacvnjJ%2FsZcYDNJlhPsC9dxsEb91X1%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;1075&quot; height=&quot;378&quot; data-origin-width=&quot;1075&quot; data-origin-height=&quot;378&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;TIP 2 :: JPA 사용 시 Batch insert 처리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA + Mysql 을 사용하는 경우에는 쉽게 놓칠 수 있는 부분이 있어 소개하고자 한다. JPA는 기본적으로 쿼리를 최대한 늦게 쓰려고 한다. 한 트랜잭션 내에서 변경점을 최대한 메모리 단에서 관리하고, 그 마지막 모습을 DB에 반영하는 것으로 불필요한 쿼리를 최대한 줄인다. 그런 지연 쓰기의 기본 동작 원리는 영속성 컨텍스트이다. 영속성 컨텍스트에 엔티티를 {ID : 객체} 형태로 관리하고, 최종 모습으로 한 번에 쿼리를 발생시킨다. 이때 배치 처리를 의도했으면 'jdbc.batch_size' 을 조정하여, 쌓여있는 쿼리를 한 번에 던지는 것이 기본적인 JPA에서 배치를 구현하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 쉽게 놓칠 수 있는 부분이 생긴다. 엔티티의 ID 값을 할당하는 방법이 DB 처리 이후에 결정되는 경우에는, 영속성 컨텍스트로 객체를 우선 관리하고 나중에 쿼리 할 수 있는 방법이 없다. DB 접근 또는 쿼리가 발생할 때까지 ID는 null 이기 때문이다. 따라서 이런 경우, 예를 들어 Mysql + Auto_Increment로 ID가 결정되는 경우에는, JPA에서 쿼리를 먼저 발생시켜 ID를 확인하기에 지연 쓰기가 가능하지 않다. 또 그렇기에 배치 처리도 의도대로 적용되지 않는다. 이런 경우 가장 확실한 방법은 JdbcTemplate를 직접 사용하여 Batch Insert 쿼리를 직접 사용하는 메서드를 만들고 이를 사용하는 것이다. 의도대로 동작하며 다른 팀원들에게도 SaveAll을 했는데 배치 처리가 안 되는 경우보다 명확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이슈 : 기기 상태 내역 DB 저장 처리량 개선&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작업 :&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - 기기 상태 이벤트 수신 후 단건 저장하던 로직을, 배치 Bulk insert로 변경&lt;br /&gt;&amp;nbsp; - 프로세스 종료 시점에, 현재 진행 중인 배치 작업과 큐에 남아 있는 이벤트 정리&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;큐에 이벤트 유입량과 처리량을 기반으로, 적정&amp;nbsp;&lt;/span&gt;배치 사이즈, 주기 값 조정 테스트&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과 :&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; - &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;초당 3300개의 처리량에서 20000개 이상의 처리가 가능하도록 처리량 개선&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;&amp;nbsp; - &lt;span style=&quot;text-align: start;&quot;&gt;네트워크 트래픽 비용, DB 연산량, 커넥션 점유 경합 개선 예상&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Architecture/Application</category>
      <author>JinHwan Kim</author>
      <guid isPermaLink="true">https://ecsimsw.tistory.com/1145</guid>
      <comments>https://ecsimsw.tistory.com/entry/device-history-batch#entry1145comment</comments>
      <pubDate>Thu, 30 Oct 2025 00:14:41 +0900</pubDate>
    </item>
  </channel>
</rss>